From 54e5bff56d9368533a84069412e7c003f193ccbc Mon Sep 17 00:00:00 2001 From: Brian Canini <canini.16@osu.edu> Date: Tue, 30 Jun 2020 10:12:21 -0400 Subject: [PATCH] Updating drupal/embed (1.0.0 => 1.4.0) Updating drupal/entity_embed (1.0-beta2 => 1.1.0) Removing entity_embed patches --- composer.json | 8 +- composer.lock | 68 +- vendor/composer/installed.json | 70 +- web/modules/embed/README.md | 2 - web/modules/embed/composer.json | 11 - .../embed/config/schema/embed.schema.yml | 12 +- web/modules/embed/embed.info.yml | 9 +- web/modules/embed/embed.install | 14 + web/modules/embed/embed.module | 81 ++ web/modules/embed/embed.post_update.php | 41 + .../Access/EmbedButtonEditorAccessCheck.php | 2 +- .../embed/src/Controller/EmbedController.php | 59 +- .../embed/src/EmbedButtonInterface.php | 29 + .../embed/src/EmbedCKEditorPluginBase.php | 23 +- .../src/EmbedType/EmbedTypeInterface.php | 5 +- .../embed/src/EmbedType/EmbedTypeManager.php | 3 +- web/modules/embed/src/Entity/EmbedButton.php | 111 +-- .../embed/src/Form/EmbedButtonForm.php | 68 +- .../embed/src/Tests/EmbedButtonAdminTest.php | 84 -- web/modules/embed/src/Tests/EmbedTestBase.php | 224 ----- .../embed.button.embed_test_default.yml | 1 + .../tests/embed_test/embed_test.info.yml | 10 +- .../src/Plugin/EmbedType/Aircraft.php | 2 +- .../src/Plugin/EmbedType/Animal.php | 2 +- .../src/Plugin/EmbedType/EmbedTestDefault.php | 2 +- .../src/Plugin/Filter/EntityEmbedByID.php | 67 +- .../EmbedButtonEditorAccessCheckTest.php | 10 +- .../tests/src/Functional/EmbedPreviewTest.php | 6 + .../tests/src/Functional/EmbedTestBase.php | 111 +++ .../src/Functional}/PreviewTest.php | 9 +- .../EmbedButtonAdminTest.php | 200 +++++ .../tests/src/Kernel/IconFileUsageTest.php | 72 -- .../embed/tests/src/Kernel/IconTest.php | 90 ++ .../entity_embed/.travis-before-script.sh | 12 - web/modules/entity_embed/.travis.yml | 117 --- web/modules/entity_embed/DEVELOPING.md | 4 - web/modules/entity_embed/README.md | 10 +- web/modules/entity_embed/composer.json | 9 +- .../entity_embed/css/entity_embed.dialog.css | 6 +- .../entity_embed/css/entity_embed.editor.css | 15 + .../css/entity_embed.filter.caption.css | 10 + web/modules/entity_embed/entity_embed.api.php | 2 +- .../entity_embed/entity_embed.info.yml | 10 +- web/modules/entity_embed/entity_embed.install | 18 +- .../entity_embed/entity_embed.libraries.yml | 8 + web/modules/entity_embed/entity_embed.module | 250 +++++- .../entity_embed/entity_embed.routing.yml | 9 +- .../entity_embed/entity_embed.services.yml | 2 +- .../js/plugins/drupalentity/plugin.js | 385 ++++++-- .../src/Annotation/EntityEmbedDisplay.php | 14 +- .../src/Controller/PreviewController.php | 89 ++ .../src/Entity/EntityEmbedFakeEntity.php | 19 + .../entity_embed/src/EntityEmbedBuilder.php | 39 +- .../EntityEmbedDisplayBase.php | 41 +- .../EntityEmbedDisplayInterface.php | 5 +- .../EntityEmbedDisplayManager.php | 85 +- .../FieldFormatterEntityEmbedDisplayBase.php | 26 +- .../src/Form/EntityEmbedDialog.php | 443 ++++----- .../Plugin/CKEditorPlugin/DrupalEntity.php | 28 +- .../Derivative/FieldFormatterDeriver.php | 21 +- .../src/Plugin/Derivative/ViewModeDeriver.php | 16 +- .../src/Plugin/EmbedType/Entity.php | 56 +- .../src/Plugin/Filter/EntityEmbedFilter.php | 108 ++- .../EntityReferenceFieldFormatter.php | 175 +++- .../EntityEmbedDisplay/FileFieldFormatter.php | 6 +- .../ImageFieldFormatter.php | 53 +- .../MediaImageDecorator.php | 258 ++++++ .../ViewModeFieldFormatter.php | 24 + .../src/Tests/EntityEmbedDialogTest.php | 172 ---- .../src/Tests/EntityEmbedFilterTest.php | 198 ---- .../src/Twig/EntityEmbedTwigExtension.php | 6 +- .../update/entity_embed.update-hook-test.php | 1 + ...tity_form_display.node.article.default.yml | 42 + ....entity_view_display.media.image.embed.yml | 29 + ....entity_view_display.media.image.thumb.yml | 28 + ...tity_view_display.node.article.default.yml | 27 + .../core.entity_view_mode.media.embed.yml | 9 + .../core.entity_view_mode.media.thumb.yml | 9 + .../install/editor.editor.full_html.yml | 64 ++ .../embed.button.test_media_entity_embed.yml | 21 + .../config/install/embed.button.test_node.yml | 15 + ...ld.field.media.image.field_media_image.yml | 40 + .../install/field.field.node.article.body.yml | 21 + .../field.storage.media.field_media_image.yml | 32 + .../install/filter.format.full_html.yml | 49 + .../config/install/media.type.image.yml | 12 + .../config/install/node.type.article.yml | 10 + .../entity_embed_test.info.yml | 19 +- .../entity_embed_test.module | 27 +- .../src/EntityEmbedTestTwigController.php | 24 +- .../language.content_settings.media.image.yml | 15 + ...language.content_settings.node.article.yml | 15 + .../config/install/language.entity.fr.yml | 8 + .../entity_embed_translation_test.info.yml | 20 + .../src/Functional/EntityEmbedDialogTest.php | 122 +++ .../EntityEmbedDisplayManagerTest.php | 178 ++++ .../EntityEmbedEntityBrowserTest.php | 30 +- .../src/Functional}/EntityEmbedHooksTest.php | 32 +- .../src/Functional}/EntityEmbedTestBase.php | 41 +- .../src/Functional}/EntityEmbedTwigTest.php | 19 +- .../Functional}/EntityEmbedUpdateHookTest.php | 16 +- .../EntityReferenceFieldFormatterTest.php | 76 +- .../Functional}/FileFieldFormatterTest.php | 31 +- .../Functional}/ImageFieldFormatterTest.php | 45 +- .../Functional/RecursionProtectionTest.php | 84 ++ .../ViewModeFieldFormatterTest.php | 30 +- .../FunctionalJavascript/ButtonAdminTest.php | 138 +++ .../CKEditorIntegrationTest.php | 303 +++++++ .../ConfigurationUiTest.php | 266 ++++++ .../ContentTranslationTest.php | 344 +++++++ .../EntityEmbedDialogTest.php | 146 +++ .../EntityEmbedTestBase.php | 102 +++ .../ImageFieldFormatterTest.php | 265 ++++++ .../FunctionalJavascript/MediaImageTest.php | 846 ++++++++++++++++++ .../SortableTestTrait.php | 127 +++ ...ityEmbedFilterDisabledIntegrationsTest.php | 65 ++ .../Kernel/EntityEmbedFilterLegacyTest.php | 100 +++ .../Kernel/EntityEmbedFilterOverridesTest.php | 155 ++++ .../src/Kernel/EntityEmbedFilterTest.php | 454 ++++++++++ .../src/Kernel/EntityEmbedFilterTestBase.php | 192 ++++ .../EntityEmbedFilterTranslationTest.php | 119 +++ 121 files changed, 7343 insertions(+), 1775 deletions(-) delete mode 100644 web/modules/embed/composer.json create mode 100644 web/modules/embed/embed.install create mode 100644 web/modules/embed/embed.post_update.php delete mode 100644 web/modules/embed/src/Tests/EmbedButtonAdminTest.php delete mode 100644 web/modules/embed/src/Tests/EmbedTestBase.php rename web/modules/embed/{src/Tests => tests/src/Functional}/EmbedButtonEditorAccessCheckTest.php (94%) create mode 100644 web/modules/embed/tests/src/Functional/EmbedTestBase.php rename web/modules/embed/{src/Tests => tests/src/Functional}/PreviewTest.php (90%) create mode 100644 web/modules/embed/tests/src/FunctionalJavascript/EmbedButtonAdminTest.php delete mode 100644 web/modules/embed/tests/src/Kernel/IconFileUsageTest.php create mode 100644 web/modules/embed/tests/src/Kernel/IconTest.php delete mode 100644 web/modules/entity_embed/.travis-before-script.sh delete mode 100644 web/modules/entity_embed/.travis.yml delete mode 100644 web/modules/entity_embed/DEVELOPING.md create mode 100644 web/modules/entity_embed/css/entity_embed.editor.css create mode 100644 web/modules/entity_embed/css/entity_embed.filter.caption.css create mode 100644 web/modules/entity_embed/src/Controller/PreviewController.php create mode 100644 web/modules/entity_embed/src/Entity/EntityEmbedFakeEntity.php create mode 100644 web/modules/entity_embed/src/Plugin/entity_embed/EntityEmbedDisplay/MediaImageDecorator.php delete mode 100644 web/modules/entity_embed/src/Tests/EntityEmbedDialogTest.php delete mode 100644 web/modules/entity_embed/src/Tests/EntityEmbedFilterTest.php create mode 100644 web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_form_display.node.article.default.yml create mode 100644 web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_view_display.media.image.embed.yml create mode 100644 web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_view_display.media.image.thumb.yml create mode 100644 web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_view_display.node.article.default.yml create mode 100644 web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_view_mode.media.embed.yml create mode 100644 web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_view_mode.media.thumb.yml create mode 100644 web/modules/entity_embed/tests/modules/entity_embed_test/config/install/editor.editor.full_html.yml create mode 100644 web/modules/entity_embed/tests/modules/entity_embed_test/config/install/embed.button.test_media_entity_embed.yml create mode 100644 web/modules/entity_embed/tests/modules/entity_embed_test/config/install/embed.button.test_node.yml create mode 100644 web/modules/entity_embed/tests/modules/entity_embed_test/config/install/field.field.media.image.field_media_image.yml create mode 100644 web/modules/entity_embed/tests/modules/entity_embed_test/config/install/field.field.node.article.body.yml create mode 100644 web/modules/entity_embed/tests/modules/entity_embed_test/config/install/field.storage.media.field_media_image.yml create mode 100644 web/modules/entity_embed/tests/modules/entity_embed_test/config/install/filter.format.full_html.yml create mode 100644 web/modules/entity_embed/tests/modules/entity_embed_test/config/install/media.type.image.yml create mode 100644 web/modules/entity_embed/tests/modules/entity_embed_test/config/install/node.type.article.yml create mode 100644 web/modules/entity_embed/tests/modules/entity_embed_translation_test/config/install/language.content_settings.media.image.yml create mode 100644 web/modules/entity_embed/tests/modules/entity_embed_translation_test/config/install/language.content_settings.node.article.yml create mode 100644 web/modules/entity_embed/tests/modules/entity_embed_translation_test/config/install/language.entity.fr.yml create mode 100644 web/modules/entity_embed/tests/modules/entity_embed_translation_test/entity_embed_translation_test.info.yml create mode 100644 web/modules/entity_embed/tests/src/Functional/EntityEmbedDialogTest.php create mode 100644 web/modules/entity_embed/tests/src/Functional/EntityEmbedDisplayManagerTest.php rename web/modules/entity_embed/{src/Tests => tests/src/Functional}/EntityEmbedEntityBrowserTest.php (58%) rename web/modules/entity_embed/{src/Tests => tests/src/Functional}/EntityEmbedHooksTest.php (70%) rename web/modules/entity_embed/{src/Tests => tests/src/Functional}/EntityEmbedTestBase.php (71%) rename web/modules/entity_embed/{src/Tests => tests/src/Functional}/EntityEmbedTwigTest.php (74%) rename web/modules/entity_embed/{src/Tests => tests/src/Functional}/EntityEmbedUpdateHookTest.php (76%) rename web/modules/entity_embed/{src/Tests => tests/src/Functional}/EntityReferenceFieldFormatterTest.php (68%) rename web/modules/entity_embed/{src/Tests => tests/src/Functional}/FileFieldFormatterTest.php (70%) rename web/modules/entity_embed/{src/Tests => tests/src/Functional}/ImageFieldFormatterTest.php (69%) create mode 100644 web/modules/entity_embed/tests/src/Functional/RecursionProtectionTest.php rename web/modules/entity_embed/{src/Tests => tests/src/Functional}/ViewModeFieldFormatterTest.php (59%) create mode 100644 web/modules/entity_embed/tests/src/FunctionalJavascript/ButtonAdminTest.php create mode 100644 web/modules/entity_embed/tests/src/FunctionalJavascript/CKEditorIntegrationTest.php create mode 100644 web/modules/entity_embed/tests/src/FunctionalJavascript/ConfigurationUiTest.php create mode 100644 web/modules/entity_embed/tests/src/FunctionalJavascript/ContentTranslationTest.php create mode 100644 web/modules/entity_embed/tests/src/FunctionalJavascript/EntityEmbedDialogTest.php create mode 100644 web/modules/entity_embed/tests/src/FunctionalJavascript/EntityEmbedTestBase.php create mode 100644 web/modules/entity_embed/tests/src/FunctionalJavascript/ImageFieldFormatterTest.php create mode 100644 web/modules/entity_embed/tests/src/FunctionalJavascript/MediaImageTest.php create mode 100644 web/modules/entity_embed/tests/src/FunctionalJavascript/SortableTestTrait.php create mode 100644 web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterDisabledIntegrationsTest.php create mode 100644 web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterLegacyTest.php create mode 100644 web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterOverridesTest.php create mode 100644 web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterTest.php create mode 100644 web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterTestBase.php create mode 100644 web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterTranslationTest.php diff --git a/composer.json b/composer.json index aabb08e27f..afd0960ce2 100644 --- a/composer.json +++ b/composer.json @@ -112,11 +112,11 @@ "drupal/draggableviews": "1.0", "drupal/dropzonejs": "2.1", "drupal/editor_advanced_link": "1.8", - "drupal/embed": "1.0", + "drupal/embed": "1.4", "drupal/entity": "1.0-beta1", "drupal/entity_browser": "1.10", "drupal/entity_clone": "1.0.0-beta4", - "drupal/entity_embed": "1.0-beta2", + "drupal/entity_embed": "1.1", "drupal/entity_reference_revisions": "1.8", "drupal/externalauth": "1.1", "drupal/features": "3.8", @@ -285,10 +285,6 @@ "drupal/entity_clone": { "3060223": "https://www.drupal.org/files/issues/2019-10-17/%20entity_clone-corrupted-paragraph-cloning-3060223-5.patch" }, - "drupal/entity_embed": { - "2881745": "patches/entity_embed_2881745-22.patch", - "2511404": "patches/entity-embed-img-link-2511404-68.patch" - }, "drupal/honeypot": { "2811189": "https://www.drupal.org/files/issues/2019-08-08/honeypot_field_weight_2811189-18.patch" }, diff --git a/composer.lock b/composer.lock index cd907bc32a..3c8b50852b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7d3787aaafd1ba3db8d0785fee363f8b", + "content-hash": "660ffeb38362eee6436d2cb9a6cf8c4a", "packages": [ { "name": "alchemy/zippy", @@ -4199,29 +4199,26 @@ }, { "name": "drupal/embed", - "version": "1.0.0", + "version": "1.4.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/embed.git", - "reference": "8.x-1.0" + "reference": "8.x-1.4" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/embed-8.x-1.0.zip", - "reference": "8.x-1.0", - "shasum": "cc746ad807260e01c7788dd82110dcebbb4d678a" + "url": "https://ftp.drupal.org/files/projects/embed-8.x-1.4.zip", + "reference": "8.x-1.4", + "shasum": "09a2bda039bfbb3fff01c91964384bf3d924b8c5" }, "require": { - "drupal/core": "~8.0" + "drupal/core": "^8.7.7 || ^9" }, "type": "drupal-module", "extra": { - "branch-alias": { - "dev-1.x": "1.x-dev" - }, "drupal": { - "version": "8.x-1.0", - "datestamp": "1490755685", + "version": "8.x-1.4", + "datestamp": "1590176831", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -4230,7 +4227,7 @@ }, "notification-url": "https://packages.drupal.org/8/downloads", "license": [ - "GPL-2.0+" + "GPL-2.0-or-later" ], "authors": [ { @@ -4249,17 +4246,19 @@ "name": "cs_shadow", "homepage": "https://www.drupal.org/user/2828287" }, + { + "name": "phenaproxima", + "homepage": "https://www.drupal.org/user/205645" + }, { "name": "slashrsm", "homepage": "https://www.drupal.org/user/744628" } ], - "description": "Provide a framework for various different types of embeds in WYSIWYG editors, common functionality, interfaces, and standards.", + "description": "Provides a framework for different types of embeds in text editors.", "homepage": "https://www.drupal.org/project/embed", "support": { - "source": "http://cgit.drupalcode.org/embed", - "issues": "https://www.drupal.org/project/issues/embed", - "irc": "irc://irc.freenode.org/drupal-media" + "source": "https://git.drupalcode.org/project/embed" } }, { @@ -4538,41 +4537,34 @@ }, { "name": "drupal/entity_embed", - "version": "1.0.0-beta2", + "version": "1.1.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/entity_embed.git", - "reference": "8.x-1.0-beta2" + "reference": "8.x-1.1" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/entity_embed-8.x-1.0-beta2.zip", - "reference": "8.x-1.0-beta2", - "shasum": "21cdeb2b058efce461683aed9a8951053512dca7" + "url": "https://ftp.drupal.org/files/projects/entity_embed-8.x-1.1.zip", + "reference": "8.x-1.1", + "shasum": "f2c3f4b3071cbd69db94c5255e1db89510995b5d" }, "require": { - "drupal/core": "*", - "drupal/embed": "*" + "drupal/core": "^8.8 || ^9", + "drupal/embed": "^1.3" }, "require-dev": { - "drupal/entity_browser": "*" + "drupal/entity_browser": "^2.2" }, "type": "drupal-module", "extra": { - "branch-alias": { - "dev-1.x": "1.x-dev" - }, "drupal": { - "version": "8.x-1.0-beta2", - "datestamp": "1476698339", + "version": "8.x-1.1", + "datestamp": "1585252806", "security-coverage": { - "status": "not-covered", - "message": "Beta releases are not covered by Drupal security advisories." + "status": "covered", + "message": "Covered by Drupal's security advisory policy" } - }, - "patches_applied": { - "2881745": "patches/entity_embed_2881745-22.patch", - "2511404": "patches/entity-embed-img-link-2511404-68.patch" } }, "notification-url": "https://packages.drupal.org/8/downloads", @@ -4600,6 +4592,10 @@ "name": "cs_shadow", "homepage": "https://www.drupal.org/user/2828287" }, + { + "name": "oknate", + "homepage": "https://www.drupal.org/user/471638" + }, { "name": "phenaproxima", "homepage": "https://www.drupal.org/user/205645" diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index 9d55d91d2d..70eac805f2 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -4324,30 +4324,27 @@ }, { "name": "drupal/embed", - "version": "1.0.0", - "version_normalized": "1.0.0.0", + "version": "1.4.0", + "version_normalized": "1.4.0.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/embed.git", - "reference": "8.x-1.0" + "reference": "8.x-1.4" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/embed-8.x-1.0.zip", - "reference": "8.x-1.0", - "shasum": "cc746ad807260e01c7788dd82110dcebbb4d678a" + "url": "https://ftp.drupal.org/files/projects/embed-8.x-1.4.zip", + "reference": "8.x-1.4", + "shasum": "09a2bda039bfbb3fff01c91964384bf3d924b8c5" }, "require": { - "drupal/core": "~8.0" + "drupal/core": "^8.7.7 || ^9" }, "type": "drupal-module", "extra": { - "branch-alias": { - "dev-1.x": "1.x-dev" - }, "drupal": { - "version": "8.x-1.0", - "datestamp": "1490755685", + "version": "8.x-1.4", + "datestamp": "1590176831", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -4357,7 +4354,7 @@ "installation-source": "dist", "notification-url": "https://packages.drupal.org/8/downloads", "license": [ - "GPL-2.0+" + "GPL-2.0-or-later" ], "authors": [ { @@ -4376,17 +4373,19 @@ "name": "cs_shadow", "homepage": "https://www.drupal.org/user/2828287" }, + { + "name": "phenaproxima", + "homepage": "https://www.drupal.org/user/205645" + }, { "name": "slashrsm", "homepage": "https://www.drupal.org/user/744628" } ], - "description": "Provide a framework for various different types of embeds in WYSIWYG editors, common functionality, interfaces, and standards.", + "description": "Provides a framework for different types of embeds in text editors.", "homepage": "https://www.drupal.org/project/embed", "support": { - "source": "http://cgit.drupalcode.org/embed", - "issues": "https://www.drupal.org/project/issues/embed", - "irc": "irc://irc.freenode.org/drupal-media" + "source": "https://git.drupalcode.org/project/embed" } }, { @@ -4672,42 +4671,35 @@ }, { "name": "drupal/entity_embed", - "version": "1.0.0-beta2", - "version_normalized": "1.0.0.0-beta2", + "version": "1.1.0", + "version_normalized": "1.1.0.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/entity_embed.git", - "reference": "8.x-1.0-beta2" + "reference": "8.x-1.1" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/entity_embed-8.x-1.0-beta2.zip", - "reference": "8.x-1.0-beta2", - "shasum": "21cdeb2b058efce461683aed9a8951053512dca7" + "url": "https://ftp.drupal.org/files/projects/entity_embed-8.x-1.1.zip", + "reference": "8.x-1.1", + "shasum": "f2c3f4b3071cbd69db94c5255e1db89510995b5d" }, "require": { - "drupal/core": "*", - "drupal/embed": "*" + "drupal/core": "^8.8 || ^9", + "drupal/embed": "^1.3" }, "require-dev": { - "drupal/entity_browser": "*" + "drupal/entity_browser": "^2.2" }, "type": "drupal-module", "extra": { - "branch-alias": { - "dev-1.x": "1.x-dev" - }, "drupal": { - "version": "8.x-1.0-beta2", - "datestamp": "1476698339", + "version": "8.x-1.1", + "datestamp": "1585252806", "security-coverage": { - "status": "not-covered", - "message": "Beta releases are not covered by Drupal security advisories." + "status": "covered", + "message": "Covered by Drupal's security advisory policy" } - }, - "patches_applied": { - "2881745": "patches/entity_embed_2881745-22.patch", - "2511404": "patches/entity-embed-img-link-2511404-68.patch" } }, "installation-source": "dist", @@ -4736,6 +4728,10 @@ "name": "cs_shadow", "homepage": "https://www.drupal.org/user/2828287" }, + { + "name": "oknate", + "homepage": "https://www.drupal.org/user/471638" + }, { "name": "phenaproxima", "homepage": "https://www.drupal.org/user/205645" diff --git a/web/modules/embed/README.md b/web/modules/embed/README.md index e2039e148e..e2b97ae27b 100644 --- a/web/modules/embed/README.md +++ b/web/modules/embed/README.md @@ -1,5 +1,3 @@ # Embed Module -[](https://travis-ci.org/drupal-media/embed) [](https://scrutinizer-ci.com/g/drupal-media/embed) - Provides an API for embedding and a UI for creating embed buttons. diff --git a/web/modules/embed/composer.json b/web/modules/embed/composer.json deleted file mode 100644 index d0003e3988..0000000000 --- a/web/modules/embed/composer.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "drupal/embed", - "description": "Provide a framework for various different types of embeds in WYSIWYG editors, common functionality, interfaces, and standards.", - "type": "drupal-module", - "homepage": "https://www.drupal.org/project/embed", - "support": { - "issues": "https://www.drupal.org/project/issues/embed", - "irc": "irc://irc.freenode.org/drupal-media" - }, - "license": "GPL-2.0+" -} diff --git a/web/modules/embed/config/schema/embed.schema.yml b/web/modules/embed/config/schema/embed.schema.yml index e232a20d92..545262d9ce 100644 --- a/web/modules/embed/config/schema/embed.schema.yml +++ b/web/modules/embed/config/schema/embed.schema.yml @@ -26,9 +26,19 @@ embed.button.*: label: 'Embed type plugin ID' type_settings: type: embed.embed_type_settings.[%parent.type_id] + icon: + type: mapping + label: 'Image icon data' + mapping: + uri: + type: string + label: 'Image file URI' + data: + type: string + label: 'Base-64 encoded image contents' icon_uuid: type: string - label: 'Button icon UUID' + label: 'Deprecated icon image file entity UUID' embed.embed_type_settings.*: type: mapping diff --git a/web/modules/embed/embed.info.yml b/web/modules/embed/embed.info.yml index 3e2cea64b5..5a71075732 100644 --- a/web/modules/embed/embed.info.yml +++ b/web/modules/embed/embed.info.yml @@ -1,11 +1,10 @@ name: Embed type: module description: 'Provides a framework for different types of embeds in text editors.' -# core: 8.x +core_version_requirement: ^8.7.7 || ^9 configure: entity.embed_button.collection -# Information added by Drupal.org packaging script on 2017-03-29 -version: '8.x-1.0' -core: '8.x' +# Information added by Drupal.org packaging script on 2020-05-22 +version: '8.x-1.4' project: 'embed' -datestamp: 1490755690 +datestamp: 1590176834 diff --git a/web/modules/embed/embed.install b/web/modules/embed/embed.install new file mode 100644 index 0000000000..9f5ed97bfe --- /dev/null +++ b/web/modules/embed/embed.install @@ -0,0 +1,14 @@ +<?php + +/** + * @file + * Install, update and uninstall functions for the embed module. + */ + +/** + * Moved to embed_post_update_convert_encoded_icon_data(). + */ +function embed_update_8101() { + // This update function has been moved to + // embed_post_update_convert_encoded_icon_data(). +} diff --git a/web/modules/embed/embed.module b/web/modules/embed/embed.module index 77349e7f34..2834b39c6a 100644 --- a/web/modules/embed/embed.module +++ b/web/modules/embed/embed.module @@ -1,6 +1,8 @@ <?php use Drupal\Core\Form\FormStateInterface; +use Drupal\Component\Serialization\Json; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Implements hook_form_FORM_ID_alter() on behalf of ckeditor.module. @@ -31,3 +33,82 @@ function ckeditor_form_embed_button_add_form_validate(array &$form, FormStateInt $form_state->setErrorByName('id', t('A CKEditor button with ID %id already exists.', ['%id' => $button_id])); } } + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function embed_form_filter_format_edit_form_alter(array &$form) { + $form['#validate'][] = 'embed_filter_format_edit_form_validate'; +} + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function embed_form_filter_format_add_form_alter(array &$form) { + $form['#validate'][] = 'embed_filter_format_edit_form_validate'; +} + +/** + * Validate callback for buttons that have a required_filter_plugin_id. + */ +function embed_filter_format_edit_form_validate($form, FormStateInterface $form_state) { + if ($form_state->getTriggeringElement()['#name'] !== 'op') { + return; + } + + // Right now we can only validate CKEditor configurations. + if ($form_state->getValue(['editor', 'editor']) !== 'ckeditor') { + return; + } + + $button_group_path = [ + 'editor', + 'settings', + 'toolbar', + 'button_groups', + ]; + + $buttons_to_validate = []; + $buttons = \Drupal::service('plugin.manager.ckeditor.plugin')->getButtons(); + foreach ($buttons as $plugin_id => $plugin_buttons) { + foreach ($plugin_buttons as $button_id => $button) { + if (!empty($button['required_filter_plugin_id'])) { + $buttons_to_validate[$plugin_id . ':' . $button_id] = $button['required_filter_plugin_id']; + } + } + } + + if ($button_groups = $form_state->getValue($button_group_path)) { + $selected_buttons = []; + $button_groups = Json::decode($button_groups); + + foreach ($button_groups as $button_row) { + foreach ($button_row as $button_group) { + $selected_buttons = array_merge($selected_buttons, array_values($button_group['items'])); + } + } + + $get_filter_label = function ($filter_plugin_id) use ($form) { + return (string) $form['filters']['order'][$filter_plugin_id]['filter']['#markup']; + }; + + foreach ($buttons_to_validate as $button_id => $filter_plugin_id) { + list($plugin_id, $button_id) = explode(':', $button_id, 2); + if (in_array($button_id, $selected_buttons, TRUE)) { + $filter_enabled = $form_state->getValue([ + 'filters', + $filter_plugin_id, + 'status', + ]); + + if (!$filter_enabled) { + $error_message = new TranslatableMarkup('The %filter-label filter must be enabled to use the %button button.', [ + '%filter-label' => $get_filter_label($filter_plugin_id), + '%button' => $buttons[$plugin_id][$button_id]['label'], + ]); + $form_state->setErrorByName('filters][' . $filter_plugin_id . '][status', $error_message); + } + } + } + } +} diff --git a/web/modules/embed/embed.post_update.php b/web/modules/embed/embed.post_update.php new file mode 100644 index 0000000000..42a3e3640b --- /dev/null +++ b/web/modules/embed/embed.post_update.php @@ -0,0 +1,41 @@ +<?php + +/** + * @file + * Post update functions for Embed. + */ + +/** + * Convert embed button icons from managed files to encoded data. + */ +function embed_post_update_convert_encoded_icon_data() { + $file_storage = \Drupal::entityTypeManager()->getStorage('file'); + $file_usage = \Drupal::service('file.usage'); + + /** @var \Drupal\embed\EmbedButtonInterface[] $buttons */ + $buttons = \Drupal::entityTypeManager()->getStorage('embed_button')->loadMultiple(); + + foreach ($buttons as $button) { + if ($icon_uuid = $button->get('icon_uuid')) { + if (!$button->get('icon')) { + // Read in the button icon file and convert to base 64 encoded string. + if ($files = $file_storage->loadByProperties(['uuid' => $icon_uuid])) { + $file = reset($files); + $button->set('icon', $button::convertImageToEncodedData($file->getFileUri())); + + // Decrement file usage for this embed button. + $file_usage->delete($file, 'embed', 'embed_button', $button->id()); + } + else { + \Drupal::logger('embed')->warning('The embed button @label had an uploaded icon image file with UUID @uuid, but it no longer exists in the database. It has been reverted back to the default button icon and you may wish to re-upload a new version.', [ + '@label' => $button->label(), + '@uuid' => $icon_uuid, + ]); + } + } + + $button->set('icon_uuid', NULL); + $button->save(); + } + } +} diff --git a/web/modules/embed/src/Access/EmbedButtonEditorAccessCheck.php b/web/modules/embed/src/Access/EmbedButtonEditorAccessCheck.php index 543d498624..70fc5f4aea 100644 --- a/web/modules/embed/src/Access/EmbedButtonEditorAccessCheck.php +++ b/web/modules/embed/src/Access/EmbedButtonEditorAccessCheck.php @@ -21,7 +21,7 @@ class EmbedButtonEditorAccessCheck implements AccessInterface { * @code * pattern: '/foo/{editor}/{embed_button}' * requirements: - * _embed_button_filter_access: 'TRUE' + * _embed_button_editor_access: 'TRUE' * @endcode * * @param \Drupal\Core\Routing\RouteMatchInterface $route_match diff --git a/web/modules/embed/src/Controller/EmbedController.php b/web/modules/embed/src/Controller/EmbedController.php index 65df04fc9b..42c6009368 100644 --- a/web/modules/embed/src/Controller/EmbedController.php +++ b/web/modules/embed/src/Controller/EmbedController.php @@ -2,13 +2,18 @@ namespace Drupal\embed\Controller; +use Drupal\Core\Ajax\AjaxHelperTrait; use Drupal\Core\Ajax\AjaxResponse; use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Language\LanguageInterface; +use Drupal\Core\Render\RendererInterface; use Drupal\editor\EditorInterface; use Drupal\embed\Ajax\EmbedInsertCommand; use Drupal\embed\EmbedButtonInterface; use Drupal\filter\FilterFormatInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** @@ -16,6 +21,34 @@ */ class EmbedController extends ControllerBase { + use AjaxHelperTrait; + + /** + * The renderer service. + * + * @var \Drupal\Core\Render\RendererInterface + */ + protected $renderer; + + /** + * Constructs an EmbedController instance. + * + * @param \Drupal\Core\Render\RendererInterface $renderer + * The renderer service. + */ + public function __construct(RendererInterface $renderer) { + $this->renderer = $renderer; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('renderer') + ); + } + /** * Returns an Ajax response to generate preview of embedded items. * @@ -33,8 +66,8 @@ class EmbedController extends ControllerBase { * The preview of the embedded item specified by the data attributes. */ public function preview(Request $request, FilterFormatInterface $filter_format) { - $text = $request->get('value'); - if ($text == '') { + $text = $request->get('text') ?: $request->get('value'); + if (empty($text)) { throw new NotFoundHttpException(); } @@ -42,11 +75,27 @@ public function preview(Request $request, FilterFormatInterface $filter_format) '#type' => 'processed_text', '#text' => $text, '#format' => $filter_format->id(), + '#langcode' => $this->languageManager()->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId(), ]; - $response = new AjaxResponse(); - $response->addCommand(new EmbedInsertCommand($build)); - return $response; + if ($this->isAjax()) { + $response = new AjaxResponse(); + $response->addCommand(new EmbedInsertCommand($build)); + return $response; + } + else { + $html = $this->renderer->renderPlain($build); + // Note that we intentionally do not use: + // - \Drupal\Core\Cache\CacheableResponse because caching it on the server + // side is wasteful, hence there is no need for cacheability metadata. + // - \Drupal\Core\Render\HtmlResponse because there is no need for + // attachments nor cacheability metadata. + return (new Response($html)) + // Do not allow any intermediary to cache the response, only end user. + ->setPrivate() + // Allow the end user to cache it for up to 5 minutes. + ->setMaxAge(300); + } } /** diff --git a/web/modules/embed/src/EmbedButtonInterface.php b/web/modules/embed/src/EmbedButtonInterface.php index 6a2d5708fe..907c213e81 100644 --- a/web/modules/embed/src/EmbedButtonInterface.php +++ b/web/modules/embed/src/EmbedButtonInterface.php @@ -59,6 +59,9 @@ public function getTypeSettings(); * * @return \Drupal\file\FileInterface * The file entity of the button icon. + * + * @deprecated in embed:1.2 and will be removed in embed:1.3. Use + * \Drupal\embed\EmbedButtonInterface::getIconUrl() instead. */ public function getIconFile(); @@ -73,4 +76,30 @@ public function getIconFile(); */ public function getIconUrl(); + /** + * Convert a file on the filesystem to encoded data. + * + * @param string $uri + * An image file URI. + * + * @return array + * An array of data about the encoded image including: + * - uri: The URI of the file. + * - data: The base-64 encoded contents of the file. + */ + public static function convertImageToEncodedData($uri); + + /** + * Convert image encoded data to a file on the filesystem. + * + * @param array $data + * An array of data about the encoded image including: + * - uri: The URI of the file. + * - data: The base-64 encoded contents of the file. + * + * @return string + * An image file URI. + */ + public static function convertEncodedDataToImage(array $data); + } diff --git a/web/modules/embed/src/EmbedCKEditorPluginBase.php b/web/modules/embed/src/EmbedCKEditorPluginBase.php index 5ebbe7ac47..64867f0c61 100644 --- a/web/modules/embed/src/EmbedCKEditorPluginBase.php +++ b/web/modules/embed/src/EmbedCKEditorPluginBase.php @@ -2,14 +2,17 @@ namespace Drupal\embed; +use Drupal\ckeditor\CKEditorPluginBase; use Drupal\Component\Utility\Html; use Drupal\Core\Entity\Query\QueryInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; -use Drupal\ckeditor\CKEditorPluginBase; use Drupal\editor\Entity\Editor; use Drupal\embed\Entity\EmbedButton; use Symfony\Component\DependencyInjection\ContainerInterface; +/** + * Provides a base class for embed CKEditor plugins. + */ abstract class EmbedCKEditorPluginBase extends CKEditorPluginBase implements ContainerFactoryPluginInterface { /** @@ -47,7 +50,7 @@ public static function create(ContainerInterface $container, array $configuratio $configuration, $plugin_id, $plugin_definition, - $container->get('entity.query')->get('embed_button') + $container->get('entity_type.manager')->getStorage('embed_button')->getQuery() ); } @@ -67,13 +70,27 @@ public function getButtons() { return $buttons; } + /** + * Build the information about the specific button. + * + * @param \Drupal\embed\EmbedButtonInterface $embed_button + * The embed button. + * + * @return array + * The array for use with getButtons(). + */ protected function getButton(EmbedButtonInterface $embed_button) { - return [ + $info = [ 'id' => $embed_button->id(), 'name' => Html::escape($embed_button->label()), 'label' => Html::escape($embed_button->label()), 'image' => $embed_button->getIconUrl(), ]; + $definition = $this->getPluginDefinition(); + if (!empty($definition['required_filter_plugin_id'])) { + $info['required_filter_plugin_id'] = $definition['required_filter_plugin_id']; + } + return $info; } /** diff --git a/web/modules/embed/src/EmbedType/EmbedTypeInterface.php b/web/modules/embed/src/EmbedType/EmbedTypeInterface.php index b6d3adfdf8..9e33882e68 100644 --- a/web/modules/embed/src/EmbedType/EmbedTypeInterface.php +++ b/web/modules/embed/src/EmbedType/EmbedTypeInterface.php @@ -2,8 +2,9 @@ namespace Drupal\embed\EmbedType; +use Drupal\Component\Plugin\ConfigurableInterface; +use Drupal\Component\Plugin\DependentPluginInterface; use Drupal\Component\Plugin\PluginInspectionInterface; -use Drupal\Component\Plugin\ConfigurablePluginInterface; use Drupal\Core\Plugin\PluginFormInterface; /** @@ -11,7 +12,7 @@ * * @ingroup embed_api */ -interface EmbedTypeInterface extends ConfigurablePluginInterface, PluginFormInterface, PluginInspectionInterface { +interface EmbedTypeInterface extends ConfigurableInterface, DependentPluginInterface, PluginFormInterface, PluginInspectionInterface { /** * Gets a configuration value. diff --git a/web/modules/embed/src/EmbedType/EmbedTypeManager.php b/web/modules/embed/src/EmbedType/EmbedTypeManager.php index 8c2a5d5443..aaf4102fa0 100644 --- a/web/modules/embed/src/EmbedType/EmbedTypeManager.php +++ b/web/modules/embed/src/EmbedType/EmbedTypeManager.php @@ -5,6 +5,7 @@ use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Plugin\DefaultPluginManager; +use Drupal\embed\Annotation\EmbedType; /** * Provides an Embed type plugin manager. @@ -27,7 +28,7 @@ class EmbedTypeManager extends DefaultPluginManager { * The module handler to invoke the alter hook with. */ public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) { - parent::__construct('Plugin/EmbedType', $namespaces, $module_handler, 'Drupal\embed\EmbedType\EmbedTypeInterface', 'Drupal\embed\Annotation\EmbedType'); + parent::__construct('Plugin/EmbedType', $namespaces, $module_handler, EmbedTypeInterface::class, EmbedType::class); $this->alterInfo('embed_type_plugins'); $this->setCacheBackend($cache_backend, 'embed_type_plugins'); } diff --git a/web/modules/embed/src/Entity/EmbedButton.php b/web/modules/embed/src/Entity/EmbedButton.php index 9188b95b5a..7a653095ff 100644 --- a/web/modules/embed/src/Entity/EmbedButton.php +++ b/web/modules/embed/src/Entity/EmbedButton.php @@ -2,8 +2,9 @@ namespace Drupal\embed\Entity; +use Drupal\Component\Utility\UrlHelper; use Drupal\Core\Config\Entity\ConfigEntityBase; -use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\File\FileSystemInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\embed\EmbedButtonInterface; @@ -37,6 +38,7 @@ * "id", * "type_id", * "type_settings", + * "icon", * "icon_uuid", * } * ) @@ -76,11 +78,11 @@ class EmbedButton extends ConfigEntityBase implements EmbedButtonInterface { public $type_settings = []; /** - * UUID of the button's icon file. + * An array of data about the encoded button image. * - * @var string + * @var array */ - public $icon_uuid; + public $icon = []; /** * {@inheritdoc} @@ -96,9 +98,7 @@ public function getTypeLabel() { if ($definition = $this->embedTypeManager()->getDefinition($this->getTypeId(), FALSE)) { return $definition['label']; } - else { - return $this->t('Unknown'); - } + return $this->t('Unknown'); } /** @@ -114,8 +114,10 @@ public function getTypePlugin() { * {@inheritdoc} */ public function getIconFile() { - if ($this->icon_uuid) { - return $this->entityManager()->loadEntityByUuid('file', $this->icon_uuid); + @trigger_error(__METHOD__ . ' is deprecated in Embed 1.2 and will be removed before 1.3.', E_USER_DEPRECATED); + if (!empty($this->icon_uuid)) { + $files = $this->entityTypeManager()->getStorage('file')->loadByProperties(['uuid' => $this->icon_uuid]); + return reset($files); } } @@ -123,12 +125,18 @@ public function getIconFile() { * {@inheritdoc} */ public function getIconUrl() { - if ($image = $this->getIconFile()) { - return file_create_url($image->getFileUri()); + if (!empty($this->icon)) { + $uri = $this->icon['uri']; + if (!is_file($uri) && !UrlHelper::isExternal($uri)) { + static::convertEncodedDataToImage($this->icon); + } + $uri = file_create_url($uri); } else { - return $this->getTypePlugin()->getDefaultIconUrl(); + $uri = $this->getTypePlugin()->getDefaultIconUrl(); } + + return file_url_transform_relative($uri); } /** @@ -137,16 +145,12 @@ public function getIconUrl() { public function calculateDependencies() { parent::calculateDependencies(); - // Add the file icon entity as dependency if an UUID was specified. - if ($this->icon_uuid && $file_icon = $this->entityManager()->loadEntityByUuid('file', $this->icon_uuid)) { - $this->addDependency($file_icon->getConfigDependencyKey(), $file_icon->getConfigDependencyName()); - } - // Gather the dependencies of the embed type plugin. - $plugin = $this->getTypePlugin(); - $this->calculatePluginDependencies($plugin); - - return $this->dependencies; + if ($plugin = $this->getTypePlugin()) { + $this->calculatePluginDependencies($plugin); + return $this->dependencies; + } + return NULL; } /** @@ -159,70 +163,45 @@ protected function embedTypeManager() { return \Drupal::service('plugin.manager.embed.type'); } - /** - * Gets the file usage service. - * - * @return \Drupal\file\FileUsage\FileUsageInterface - * The file usage service. - */ - protected function fileUsage() { - return \Drupal::service('file.usage'); - } - /** * {@inheritdoc} */ - public function postSave(EntityStorageInterface $storage, $update = TRUE) { - parent::postSave($storage, $update); - - $icon_file = $this->getIconFile(); - if (isset($this->original) && $old_icon_file = $this->original->getIconFile()) { - /** @var \Drupal\file\FileInterface $old_icon_file */ - if (!$icon_file || $icon_file->uuid() != $old_icon_file->uuid()) { - $this->fileUsage()->delete($old_icon_file, 'embed', $this->getEntityTypeId(), $this->id()); - } - } - - if ($icon_file) { - $usage = $this->fileUsage()->listUsage($icon_file); - if (empty($usage['embed'][$this->getEntityTypeId()][$this->id()])) { - $this->fileUsage()->add($icon_file, 'embed', $this->getEntityTypeId(), $this->id()); - } + public function getTypeSetting($key, $default = NULL) { + if (isset($this->type_settings[$key])) { + return $this->type_settings[$key]; } + return $default; } /** * {@inheritdoc} */ - public static function postDelete(EntityStorageInterface $storage, array $entities) { - parent::postDelete($storage, $entities); - - // Remove file usage for any button icons. - foreach ($entities as $entity) { - /** @var \Drupal\embed\EmbedButtonInterface $entity */ - if ($icon_file = $entity->getIconFile()) { - \Drupal::service('file.usage')->delete($icon_file, 'embed', $entity->getEntityTypeId(), $entity->id()); - } - } + public function getTypeSettings() { + return $this->type_settings; } /** * {@inheritdoc} */ - public function getTypeSetting($key, $default = NULL) { - if (isset($this->type_settings[$key])) { - return $this->type_settings[$key]; - } - else { - return $default; - } + public static function convertImageToEncodedData($uri) { + return [ + 'data' => base64_encode(file_get_contents($uri)), + 'uri' => $uri, + ]; } /** * {@inheritdoc} */ - public function getTypeSettings() { - return $this->type_settings; + public static function convertEncodedDataToImage(array $data) { + if (!is_file($data['uri'])) { + $directory = dirname($data['uri']); + /** @var \Drupal\Core\File\FileSystemInterface $filesystem */ + $fileSystem = \Drupal::service('file_system'); + $fileSystem->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS); + $fileSystem->saveData(base64_decode($data['data']), $data['uri'], FileSystemInterface::EXISTS_REPLACE); + } + return $data['uri']; } } diff --git a/web/modules/embed/src/Form/EmbedButtonForm.php b/web/modules/embed/src/Form/EmbedButtonForm.php index 2702e22f5c..5379c90f37 100644 --- a/web/modules/embed/src/Form/EmbedButtonForm.php +++ b/web/modules/embed/src/Form/EmbedButtonForm.php @@ -7,10 +7,11 @@ use Drupal\Core\Ajax\ReplaceCommand; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Entity\EntityForm; -use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\embed\EmbedType\EmbedTypeManager; +use Drupal\embed\Entity\EmbedButton; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -84,7 +85,7 @@ public function form(array $form, FormStateInterface $form_state) { '#maxlength' => EntityTypeInterface::BUNDLE_MAX_LENGTH, '#disabled' => !$button->isNew(), '#machine_name' => [ - 'exists' => ['Drupal\embed\Entity\EmbedButton', 'load'], + 'exists' => [EmbedButton::class, 'load'], ], '#description' => $this->t('A unique machine-readable name for this embed button. It must only contain lowercase letters, numbers, and underscores.'), ]; @@ -101,8 +102,8 @@ public function form(array $form, FormStateInterface $form_state) { ], '#disabled' => !$button->isNew(), ]; - if (count($form['type_id']['#options']) == 0) { - drupal_set_message($this->t('No embed types found.'), 'warning'); + if (empty($form['type_id']['#options'])) { + $this->messenger()->addWarning($this->t('No embed types found.')); } // Add the embed type plugin settings. @@ -119,7 +120,7 @@ public function form(array $form, FormStateInterface $form_state) { } } catch (PluginNotFoundException $exception) { - drupal_set_message($exception->getMessage(), 'error'); + $this->messenger()->addError($exception->getMessage()); watchdog_exception('embed', $exception); $form['type_id']['#disabled'] = FALSE; } @@ -127,17 +128,38 @@ public function form(array $form, FormStateInterface $form_state) { $config = $this->config('embed.settings'); $upload_location = $config->get('file_scheme') . '://' . $config->get('upload_directory') . '/'; $form['icon_file'] = [ - '#title' => $this->t('Button icon'), '#type' => 'managed_file', - '#description' => $this->t('Icon for the button to be shown in CKEditor toolbar. Leave empty to use the default Entity icon.'), + '#title' => $this->t('Button icon'), '#upload_location' => $upload_location, '#upload_validators' => [ - 'file_validate_extensions' => ['gif png jpg jpeg'], + 'file_validate_extensions' => ['gif png jpg jpeg svg'], 'file_validate_image_resolution' => ['32x32', '16x16'], ], ]; - if ($file = $button->getIconFile()) { - $form['icon_file']['#default_value'] = ['target_id' => $file->id()]; + + if (!$button->isNew()) { + $form['icon_reset'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Reset to default icon'), + '#access' => $button->getIconUrl() !== $button->getTypePlugin()->getDefaultIconUrl(), + ]; + + $form['icon_preview'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Current icon preview'), + ]; + $form['icon_preview']['image'] = [ + '#theme' => 'image', + '#uri' => $button->getIconUrl(), + '#alt' => $this->t('Preview of @label button icon', ['@label' => $button->label()]), + ]; + + // Show an even nicer preview with CKEditor being used. + if ($this->moduleHandler->moduleExists('ckeditor')) { + $form['icon_preview']['image']['#prefix'] = '<div data-toolbar="active" role="form" class="ckeditor-toolbar ckeditor-toolbar-active clearfix"><ul class="ckeditor-active-toolbar-configuration" role="presentation" aria-label="CKEditor toolbar and button configuration."><li class="ckeditor-row" role="group" aria-labelledby="ckeditor-active-toolbar"><ul class="ckeditor-toolbar-groups clearfix js-sortable"><li class="ckeditor-toolbar-group" role="presentation" data-drupal-ckeditor-type="group" data-drupal-ckeditor-toolbar-group-name="Embed button icon preview" tabindex="0"><h3 class="ckeditor-toolbar-group-name" id="ckeditor-toolbar-group-aria-label-for-formatting">Embed button icon preview</h3><ul class="ckeditor-buttons ckeditor-toolbar-group-buttons js-sortable" role="toolbar" data-drupal-ckeditor-button-sorting="target" aria-labelledby="ckeditor-toolbar-group-aria-label-for-formatting"><li data-drupal-ckeditor-button-name="Bold" class="ckeditor-button"><a href="#" role="button" title="' . $button->label() . '" aria-label="' . $button->label() . '"><span class="cke_button_icon">'; + $form['icon_preview']['image']['#suffix'] = '</span></a></li></ul></li></ul></div>'; + $form['icon_preview']['#attached']['library'][] = 'ckeditor/drupal.ckeditor.admin'; + } } return $form; @@ -181,30 +203,32 @@ public function save(array $form, FormStateInterface $form_state) { $form_state->setValue('type_settings', $plugin->getConfiguration()); $button->set('type_settings', $plugin->getConfiguration()); + // If a file was uploaded to be used as the icon, get an encoded URL to be + // stored in the config entity. $icon_fid = $form_state->getValue(['icon_file', '0']); - // If a file was uploaded to be used as the icon, get its UUID to be stored - // in the config entity. if (!empty($icon_fid) && $file = $this->entityTypeManager->getStorage('file')->load($icon_fid)) { - $button->set('icon_uuid', $file->uuid()); + $file->setPermanent(); + $file->save(); + $button->set('icon', EmbedButton::convertImageToEncodedData($file->getFileUri())); } - else { - $button->set('icon_uuid', NULL); + elseif ($form_state->getValue('icon_reset')) { + $button->set('icon', NULL); } $status = $button->save(); $t_args = ['%label' => $button->label()]; - if ($status == SAVED_UPDATED) { - drupal_set_message($this->t('The embed button %label has been updated.', $t_args)); + if ($status === SAVED_UPDATED) { + $this->messenger()->addStatus($this->t('The embed button %label has been updated.', $t_args)); + $this->logger('embed')->info('Updated embed button %label.', $t_args); } - elseif ($status == SAVED_NEW) { - drupal_set_message($this->t('The embed button %label has been added.', $t_args)); - $context = array_merge($t_args, ['link' => $button->link($this->t('View'), 'collection')]); - $this->logger('embed')->notice('Added embed button %label.', $context); + elseif ($status === SAVED_NEW) { + $this->messenger()->addStatus($this->t('The embed button %label has been added.', $t_args)); + $this->logger('embed')->info('Added embed button %label.', $t_args); } - $form_state->setRedirectUrl($button->urlInfo('collection')); + $form_state->setRedirectUrl($button->toUrl()); } /** diff --git a/web/modules/embed/src/Tests/EmbedButtonAdminTest.php b/web/modules/embed/src/Tests/EmbedButtonAdminTest.php deleted file mode 100644 index 63c9c98b59..0000000000 --- a/web/modules/embed/src/Tests/EmbedButtonAdminTest.php +++ /dev/null @@ -1,84 +0,0 @@ -<?php - -namespace Drupal\embed\Tests; - -/** - * Tests the administrative UI. - * - * @group embed - */ -class EmbedButtonAdminTest extends EmbedTestBase { - - /** - * Tests the embed_button administration functionality. - */ - public function testEmbedButtonAdmin() { - // Ensure proper access to the Embed settings page. - $this->drupalGet('admin/config/content/embed'); - $this->assertResponse(403); - - $this->drupalLogin($this->adminUser); - $this->drupalGet('admin/config/content/embed'); - $this->assertResponse(200); - - // Add embed button. - $this->clickLink('Add embed button'); - $button_id = strtolower($this->randomMachineName()); - $button_label = $this->randomMachineName(); - $edit = [ - 'id' => $button_id, - 'label' => $button_label, - 'type_id' => 'embed_test_default', - ]; - $this->drupalPostForm(NULL, $edit, 'Save'); - // Ensure that the newly created button is listed. - $this->drupalGet('admin/config/content/embed'); - $this->assertText($button_label, 'Test embed_button appears on the list page'); - - // Edit embed button. - $this->drupalGet('admin/config/content/embed/button/manage/' . $button_id); - $new_button_label = $this->randomMachineName(); - $edit = [ - 'label' => $new_button_label, - ]; - $this->drupalPostForm(NULL, $edit, 'Save'); - // Ensure that name and label has been changed. - $this->drupalGet('admin/config/content/embed'); - $this->assertText($new_button_label, 'New label appears on the list page'); - $this->assertNoText($button_label, 'Old label does not appears on the list page'); - - // Delete embed button. - $this->drupalGet('admin/config/content/embed/button/manage/' . $button_id . '/delete'); - $this->drupalPostForm(NULL, [], 'Delete'); - // Ensure that the deleted embed button no longer exists. - $this->drupalGet('admin/config/content/embed/button/manage/' . $button_id); - $this->assertResponse(404, 'Deleted embed button no longer exists.'); - // Ensure that the deleted button is no longer listed. - $this->drupalGet('admin/config/content/embed'); - $this->assertNoText($button_label, 'Test embed_button does not appears on the list page'); - } - - public function testButtonValidation() { - $this->drupalLogin($this->adminUser); - $button_id = strtolower($this->randomMachineName()); - $edit = [ - 'id' => $button_id, - 'label' => $this->randomMachineName(), - 'type_id' => 'embed_test_aircraft', - ]; - $this->drupalPostAjaxForm('admin/config/content/embed/button/add', $edit, 'type_id'); - $this->assertFieldByName('type_settings[aircraft_type]', 'fixed-wing'); - - $edit['type_settings[aircraft_type]'] = 'invalid'; - $this->drupalPostForm(NULL, $edit, 'Save'); - $this->assertText('Cannot select invalid aircraft type.'); - - $edit['type_settings[aircraft_type]'] = 'helicopters'; - $this->drupalPostForm(NULL, $edit, 'Save'); - $this->assertText('Helicopters are just rotorcraft.'); - - $this->drupalGet('admin/config/content/embed/button/manage/' . $button_id); - $this->assertFieldByName('type_settings[aircraft_type]', 'rotorcraft'); - } - -} diff --git a/web/modules/embed/src/Tests/EmbedTestBase.php b/web/modules/embed/src/Tests/EmbedTestBase.php deleted file mode 100644 index 766748f84f..0000000000 --- a/web/modules/embed/src/Tests/EmbedTestBase.php +++ /dev/null @@ -1,224 +0,0 @@ -<?php - -namespace Drupal\embed\Tests; - -use Drupal\editor\Entity\Editor; -use Drupal\file\Entity\File; -use Drupal\filter\Entity\FilterFormat; -use Drupal\simpletest\WebTestBase; - -/** - * Base class for all embed tests. - */ -abstract class EmbedTestBase extends WebTestBase { - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = [ - 'block', - 'embed', - 'embed_test', - 'editor', - 'ckeditor', - ]; - - /** - * The test administrative user. - * - * @var \Drupal\user\UserInterface - */ - protected $adminUser; - - /** - * The test administrative user. - * - * @var \Drupal\user\UserInterface - */ - protected $webUser; - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - - // Create Filtered HTML text format and enable entity_embed filter. - $format = FilterFormat::create([ - 'format' => 'embed_test', - 'name' => 'Embed format', - 'filters' => [], - ]); - $format->save(); - - $editor_group = [ - 'name' => 'Embed', - 'items' => [ - 'embed_test_default', - ], - ]; - $editor = Editor::create([ - 'format' => 'embed_test', - 'editor' => 'ckeditor', - 'settings' => [ - 'toolbar' => [ - 'rows' => [[$editor_group]], - ], - ], - ]); - $editor->save(); - - // Create a user with required permissions. - $this->adminUser = $this->drupalCreateUser([ - 'administer embed buttons', - 'use text format embed_test', - ]); - - // Create a user with required permissions. - $this->webUser = $this->drupalCreateUser([ - 'use text format embed_test', - ]); - - // Set up some standard blocks for the testing theme (Classy). - // @see https://www.drupal.org/node/507488?page=1#comment-10291517 - $this->drupalPlaceBlock('local_tasks_block'); - $this->drupalPlaceBlock('local_actions_block'); - } - - /** - * Retrieves a sample file of the specified type. - * - * @return \Drupal\file\FileInterface - */ - protected function getTestFile($type_name, $size = NULL) { - // Get a file to upload. - $file = current($this->drupalGetTestFiles($type_name, $size)); - - // Add a filesize property to files as would be read by - // \Drupal\file\Entity\File::load(). - $file->filesize = filesize($file->uri); - - $file = File::create((array) $file); - $file->save(); - return $file; - } - - /** - * {@inheritdoc} - * - * This is a duplicate of WebTestBase::drupalProcessAjaxResponse() that - * includes the fix from https://www.drupal.org/node/2554449 for using ID - * selectors in AJAX commands. - */ - protected function drupalProcessAjaxResponse($content, array $ajax_response, array $ajax_settings, array $drupal_settings) { - // ajax.js applies some defaults to the settings object, so do the same - // for what's used by this function. - $ajax_settings += array( - 'method' => 'replaceWith', - ); - // DOM can load HTML soup. But, HTML soup can throw warnings, suppress - // them. - $dom = new \DOMDocument(); - @$dom->loadHTML($content); - // XPath allows for finding wrapper nodes better than DOM does. - $xpath = new \DOMXPath($dom); - foreach ($ajax_response as $command) { - // Error messages might be not commands. - if (!is_array($command)) { - continue; - } - switch ($command['command']) { - case 'settings': - $drupal_settings = NestedArray::mergeDeepArray([$drupal_settings, $command['settings']], TRUE); - break; - - case 'insert': - $wrapperNode = NULL; - // When a command specifies a specific selector, use it. - if (!empty($command['selector']) && strpos($command['selector'], '#') === 0) { - $wrapperNode = $xpath->query('//*[@id="' . substr($command['selector'], 1) . '"]')->item(0); - } - // When a command doesn't specify a selector, use the - // #ajax['wrapper'] which is always an HTML ID. - elseif (!empty($ajax_settings['wrapper'])) { - $wrapperNode = $xpath->query('//*[@id="' . $ajax_settings['wrapper'] . '"]')->item(0); - } - // @todo Ajax commands can target any jQuery selector, but these are - // hard to fully emulate with XPath. For now, just handle 'head' - // and 'body', since these are used by - // \Drupal\Core\Ajax\AjaxResponse::ajaxRender(). - elseif (in_array($command['selector'], array('head', 'body'))) { - $wrapperNode = $xpath->query('//' . $command['selector'])->item(0); - } - if ($wrapperNode) { - // ajax.js adds an enclosing DIV to work around a Safari bug. - $newDom = new \DOMDocument(); - // DOM can load HTML soup. But, HTML soup can throw warnings, - // suppress them. - @$newDom->loadHTML('<div>' . $command['data'] . '</div>'); - // Suppress warnings thrown when duplicate HTML IDs are encountered. - // This probably means we are replacing an element with the same ID. - $newNode = @$dom->importNode($newDom->documentElement->firstChild->firstChild, TRUE); - $method = isset($command['method']) ? $command['method'] : $ajax_settings['method']; - // The "method" is a jQuery DOM manipulation function. Emulate - // each one using PHP's DOMNode API. - switch ($method) { - case 'replaceWith': - $wrapperNode->parentNode->replaceChild($newNode, $wrapperNode); - break; - case 'append': - $wrapperNode->appendChild($newNode); - break; - case 'prepend': - // If no firstChild, insertBefore() falls back to - // appendChild(). - $wrapperNode->insertBefore($newNode, $wrapperNode->firstChild); - break; - case 'before': - $wrapperNode->parentNode->insertBefore($newNode, $wrapperNode); - break; - case 'after': - // If no nextSibling, insertBefore() falls back to - // appendChild(). - $wrapperNode->parentNode->insertBefore($newNode, $wrapperNode->nextSibling); - break; - case 'html': - foreach ($wrapperNode->childNodes as $childNode) { - $wrapperNode->removeChild($childNode); - } - $wrapperNode->appendChild($newNode); - break; - } - } - break; - - // @todo Add suitable implementations for these commands in order to - // have full test coverage of what ajax.js can do. - case 'remove': - break; - case 'changed': - break; - case 'css': - break; - case 'data': - break; - case 'restripe': - break; - case 'add_css': - break; - case 'update_build_id': - $buildId = $xpath->query('//input[@name="form_build_id" and @value="' . $command['old'] . '"]')->item(0); - if ($buildId) { - $buildId->setAttribute('value', $command['new']); - } - break; - } - } - $content = $dom->saveHTML(); - $this->setRawContent($content); - $this->setDrupalSettings($drupal_settings); - } - -} diff --git a/web/modules/embed/tests/embed_test/config/install/embed.button.embed_test_default.yml b/web/modules/embed/tests/embed_test/config/install/embed.button.embed_test_default.yml index 5e0f7b2b9f..a118f6b4d8 100644 --- a/web/modules/embed/tests/embed_test/config/install/embed.button.embed_test_default.yml +++ b/web/modules/embed/tests/embed_test/config/install/embed.button.embed_test_default.yml @@ -4,3 +4,4 @@ type_id: 'embed_test_default' dependencies: module: - embed_test +icon: { } diff --git a/web/modules/embed/tests/embed_test/embed_test.info.yml b/web/modules/embed/tests/embed_test/embed_test.info.yml index 3f95b38bee..2aeb758413 100644 --- a/web/modules/embed/tests/embed_test/embed_test.info.yml +++ b/web/modules/embed/tests/embed_test/embed_test.info.yml @@ -1,14 +1,14 @@ name: 'Embed test' type: module description: 'Support module for the Embed module tests.' -# core: 8.x +core: 8.x +core_version_requirement: ^8 || ^9 package: Testing dependencies: - embed - node -# Information added by Drupal.org packaging script on 2017-03-29 -version: '8.x-1.0' -core: '8.x' +# Information added by Drupal.org packaging script on 2020-05-22 +version: '8.x-1.4' project: 'embed' -datestamp: 1490755690 +datestamp: 1590176834 diff --git a/web/modules/embed/tests/embed_test/src/Plugin/EmbedType/Aircraft.php b/web/modules/embed/tests/embed_test/src/Plugin/EmbedType/Aircraft.php index b344fa9bc0..72d2045da4 100644 --- a/web/modules/embed/tests/embed_test/src/Plugin/EmbedType/Aircraft.php +++ b/web/modules/embed/tests/embed_test/src/Plugin/EmbedType/Aircraft.php @@ -59,7 +59,7 @@ public function validateConfigurationForm(array &$form, FormStateInterface $form */ public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { if ($form_state->getValue('aircraft_type') === 'helicopters') { - drupal_set_message($this->t('Helicopters are just rotorcraft.'), 'warning'); + $this->messenger()->addWarning($this->t('Helicopters are just rotorcraft.')); $form_state->setValue('aircraft_type', 'rotorcraft'); } diff --git a/web/modules/embed/tests/embed_test/src/Plugin/EmbedType/Animal.php b/web/modules/embed/tests/embed_test/src/Plugin/EmbedType/Animal.php index 3221f8053a..c319f44683 100644 --- a/web/modules/embed/tests/embed_test/src/Plugin/EmbedType/Animal.php +++ b/web/modules/embed/tests/embed_test/src/Plugin/EmbedType/Animal.php @@ -58,7 +58,7 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta * {@inheritdoc} */ public function getDefaultIconUrl() { - return ''; + return '/animal.png'; } } diff --git a/web/modules/embed/tests/embed_test/src/Plugin/EmbedType/EmbedTestDefault.php b/web/modules/embed/tests/embed_test/src/Plugin/EmbedType/EmbedTestDefault.php index 385bc5ff00..00f7bb8e2f 100644 --- a/web/modules/embed/tests/embed_test/src/Plugin/EmbedType/EmbedTestDefault.php +++ b/web/modules/embed/tests/embed_test/src/Plugin/EmbedType/EmbedTestDefault.php @@ -23,7 +23,7 @@ class EmbedTestDefault extends EmbedTypeBase { * {@inheritdoc} */ public function getDefaultIconUrl() { - return ''; + return '/default.png'; } } diff --git a/web/modules/embed/tests/embed_test/src/Plugin/Filter/EntityEmbedByID.php b/web/modules/embed/tests/embed_test/src/Plugin/Filter/EntityEmbedByID.php index 719562de82..06589f95c0 100644 --- a/web/modules/embed/tests/embed_test/src/Plugin/Filter/EntityEmbedByID.php +++ b/web/modules/embed/tests/embed_test/src/Plugin/Filter/EntityEmbedByID.php @@ -2,9 +2,14 @@ namespace Drupal\embed_test\Plugin\Filter; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\Render\BubbleableMetadata; +use Drupal\Core\Render\RenderContext; +use Drupal\Core\Render\RendererInterface; use Drupal\filter\FilterProcessResult; use Drupal\filter\Plugin\FilterBase; -use Drupal\node\Entity\Node; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Renders a full node view from an embed code like node:NID. @@ -16,7 +21,54 @@ * type = Drupal\filter\Plugin\FilterInterface::TYPE_MARKUP_LANGUAGE, * ) */ -class EntityEmbedByID extends FilterBase { +class EntityEmbedByID extends FilterBase implements ContainerFactoryPluginInterface { + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The renderer service. + * + * @var \Drupal\Core\Render\RendererInterface + */ + protected $renderer; + + /** + * Constructs a EntityEmbedByID 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\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * @param \Drupal\Core\Render\RendererInterface $renderer + * The renderer. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, RendererInterface $renderer) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->entityTypeManager = $entity_type_manager; + $this->renderer = $renderer; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager'), + $container->get('renderer') + ); + } /** * {@inheritdoc} @@ -28,9 +80,14 @@ public function process($text, $langcode) { preg_match_all('/node:([0-9]+)/', $text, $matches); foreach ($matches[0] as $i => $search) { - $node = Node::load($matches[1][$i]); - $build = \Drupal::entityTypeManager()->getViewBuilder('node')->view($node); - $replace = \Drupal::service('renderer')->render($build); + $replace = ''; + if ($node = $this->entityTypeManager->getStorage('node')->load($matches[1][$i])) { + $build = $this->entityTypeManager->getViewBuilder('node')->view($node); + $replace = $this->renderer->executeInRenderContext(new RenderContext(), function () use (&$build) { + return $this->renderer->render($build); + }); + $result = $result->merge(BubbleableMetadata::createFromRenderArray($build)); + } $text = str_replace($search, $replace, $text); } diff --git a/web/modules/embed/src/Tests/EmbedButtonEditorAccessCheckTest.php b/web/modules/embed/tests/src/Functional/EmbedButtonEditorAccessCheckTest.php similarity index 94% rename from web/modules/embed/src/Tests/EmbedButtonEditorAccessCheckTest.php rename to web/modules/embed/tests/src/Functional/EmbedButtonEditorAccessCheckTest.php index c73887acfc..e275664e03 100644 --- a/web/modules/embed/src/Tests/EmbedButtonEditorAccessCheckTest.php +++ b/web/modules/embed/tests/src/Functional/EmbedButtonEditorAccessCheckTest.php @@ -1,8 +1,9 @@ <?php -namespace Drupal\embed\Tests; +namespace Drupal\Tests\embed\Functional; use Drupal\editor\Entity\Editor; +use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait; /** * Tests EmbedButtonEditorAccessCheck. @@ -11,8 +12,15 @@ */ class EmbedButtonEditorAccessCheckTest extends EmbedTestBase { + use AssertPageCacheContextsAndTagsTrait; + const SUCCESS = 'Success!'; + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + /** * Tests \Drupal\embed\Access\EmbedButtonEditorAccessCheck. */ diff --git a/web/modules/embed/tests/src/Functional/EmbedPreviewTest.php b/web/modules/embed/tests/src/Functional/EmbedPreviewTest.php index fbbd8f367e..bf2e581b93 100644 --- a/web/modules/embed/tests/src/Functional/EmbedPreviewTest.php +++ b/web/modules/embed/tests/src/Functional/EmbedPreviewTest.php @@ -20,6 +20,11 @@ class EmbedPreviewTest extends BrowserTestBase { */ public static $modules = ['embed_test', 'filter']; + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + /** * Tests that out-of-band assets are included with previews. */ @@ -53,6 +58,7 @@ public function testPreview() { $response = $this->drupalGet('/embed/preview/foo', [ 'query' => [ 'value' => 'node:' . $node->id(), + '_wrapper_format' => 'drupal_ajax', ], ]); diff --git a/web/modules/embed/tests/src/Functional/EmbedTestBase.php b/web/modules/embed/tests/src/Functional/EmbedTestBase.php new file mode 100644 index 0000000000..17f990a3c4 --- /dev/null +++ b/web/modules/embed/tests/src/Functional/EmbedTestBase.php @@ -0,0 +1,111 @@ +<?php + +namespace Drupal\Tests\embed\Functional; + +use Drupal\editor\Entity\Editor; +use Drupal\file\Entity\File; +use Drupal\filter\Entity\FilterFormat; +use Drupal\Tests\BrowserTestBase; +use Drupal\Tests\TestFileCreationTrait; + +/** + * Base class for all embed tests. + */ +abstract class EmbedTestBase extends BrowserTestBase { + + use TestFileCreationTrait; + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = [ + 'block', + 'embed', + 'embed_test', + 'editor', + 'ckeditor', + ]; + + /** + * The test administrative user. + * + * @var \Drupal\user\UserInterface + */ + protected $adminUser; + + /** + * The test administrative user. + * + * @var \Drupal\user\UserInterface + */ + protected $webUser; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + // Create Filtered HTML text format and enable entity_embed filter. + $format = FilterFormat::create([ + 'format' => 'embed_test', + 'name' => 'Embed format', + 'filters' => [], + ]); + $format->save(); + + $editor_group = [ + 'name' => 'Embed', + 'items' => [ + 'embed_test_default', + ], + ]; + $editor = Editor::create([ + 'format' => 'embed_test', + 'editor' => 'ckeditor', + 'settings' => [ + 'toolbar' => [ + 'rows' => [[$editor_group]], + ], + ], + ]); + $editor->save(); + + // Create a user with required permissions. + $this->adminUser = $this->drupalCreateUser([ + 'administer embed buttons', + 'use text format embed_test', + ]); + + // Create a user with required permissions. + $this->webUser = $this->drupalCreateUser([ + 'use text format embed_test', + ]); + + // Set up some standard blocks for the testing theme (Classy). + // @see https://www.drupal.org/node/507488?page=1#comment-10291517 + $this->drupalPlaceBlock('local_tasks_block'); + $this->drupalPlaceBlock('local_actions_block'); + } + + /** + * Retrieves a sample file of the specified type. + * + * @return \Drupal\file\FileInterface + */ + protected function getTestFile($type_name, $size = NULL) { + // Get a file to upload. + $file = current($this->getTestFiles($type_name, $size)); + + // Add a filesize property to files as would be read by + // \Drupal\file\Entity\File::load(). + $file->filesize = filesize($file->uri); + + $file = File::create((array) $file); + $file->save(); + return $file; + } + +} diff --git a/web/modules/embed/src/Tests/PreviewTest.php b/web/modules/embed/tests/src/Functional/PreviewTest.php similarity index 90% rename from web/modules/embed/src/Tests/PreviewTest.php rename to web/modules/embed/tests/src/Functional/PreviewTest.php index 11613ee3e5..1163240327 100644 --- a/web/modules/embed/src/Tests/PreviewTest.php +++ b/web/modules/embed/tests/src/Functional/PreviewTest.php @@ -1,6 +1,6 @@ <?php -namespace Drupal\embed\Tests; +namespace Drupal\Tests\embed\Functional; /** * Tests the preview controller and route. @@ -11,6 +11,11 @@ class PreviewTest extends EmbedTestBase { const SUCCESS = 'Success!'; + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + /** * Tests the route used for generating preview of embedding entities. */ @@ -60,7 +65,7 @@ public function getRoute($filter_format_id, $value = NULL) { if (!isset($value)) { $value = static::SUCCESS; } - return $this->drupalGet($url, ['query' => ['value' => $value]]); + return $this->drupalGet($url, ['query' => ['text' => $value]]); } } diff --git a/web/modules/embed/tests/src/FunctionalJavascript/EmbedButtonAdminTest.php b/web/modules/embed/tests/src/FunctionalJavascript/EmbedButtonAdminTest.php new file mode 100644 index 0000000000..767cf43e31 --- /dev/null +++ b/web/modules/embed/tests/src/FunctionalJavascript/EmbedButtonAdminTest.php @@ -0,0 +1,200 @@ +<?php + +namespace Drupal\Tests\embed\FunctionalJavascript; + +use Drupal\editor\Entity\Editor; +use Drupal\filter\Entity\FilterFormat; +use Drupal\FunctionalJavascriptTests\WebDriverTestBase; + +/** + * Tests the administrative UI. + * + * @group embed + */ +class EmbedButtonAdminTest extends WebDriverTestBase { + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = [ + 'block', + 'embed', + 'embed_test', + 'editor', + 'ckeditor', + ]; + + /** + * The test administrative user. + * + * @var \Drupal\user\UserInterface + */ + protected $adminUser; + + /** + * The test administrative user. + * + * @var \Drupal\user\UserInterface + */ + protected $webUser; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + // Create Filtered HTML text format and enable entity_embed filter. + $format = FilterFormat::create([ + 'format' => 'embed_test', + 'name' => 'Embed format', + 'filters' => [], + ]); + $format->save(); + + $editor_group = [ + 'name' => 'Embed', + 'items' => [ + 'embed_test_default', + ], + ]; + $editor = Editor::create([ + 'format' => 'embed_test', + 'editor' => 'ckeditor', + 'settings' => [ + 'toolbar' => [ + 'rows' => [[$editor_group]], + ], + ], + ]); + $editor->save(); + + // Create a user with required permissions. + $this->adminUser = $this->drupalCreateUser([ + 'administer embed buttons', + 'use text format embed_test', + ]); + + // Create a user with required permissions. + $this->webUser = $this->drupalCreateUser([ + 'use text format embed_test', + ]); + + // Set up some standard blocks for the testing theme (Classy). + // @see https://www.drupal.org/node/507488?page=1#comment-10291517 + $this->drupalPlaceBlock('local_tasks_block'); + $this->drupalPlaceBlock('local_actions_block'); + } + + /** + * Tests the embed_button administration functionality. + */ + public function testEmbedButtonAdmin() { + $page = $this->getSession()->getPage(); + $assert_session = $this->assertSession(); + + // Ensure proper access to the Embed settings page. + $this->drupalGet('admin/config/content/embed'); + $assert_session->pageTextContains('You are not authorized to access this page.'); + + $this->drupalLogin($this->adminUser); + $this->drupalGet('admin/config/content/embed'); + + // Add embed button. + $this->clickLink('Add embed button'); + $button_label = $this->randomMachineName(); + $button_id = strtolower($button_label); + $page->fillField('label', $button_label); + $this->assertNotEmpty($assert_session->waitForText("Machine name: $button_id")); + $edit = [ + 'type_id' => 'embed_test_default', + ]; + $this->drupalPostForm(NULL, $edit, 'Save'); + // Ensure that the newly created button is listed. + $this->drupalGet('admin/config/content/embed'); + $assert_session->pageTextContains($button_label); + + // Edit embed button. + $this->drupalGet('admin/config/content/embed/button/manage/' . $button_id); + $new_button_label = $this->randomMachineName(); + $edit = [ + 'label' => $new_button_label, + ]; + $this->drupalPostForm(NULL, $edit, 'Save'); + // Ensure that name and label has been changed. + $this->drupalGet('admin/config/content/embed'); + $assert_session->pageTextContains($new_button_label); + $assert_session->pageTextNotContains($button_label); + + // Delete embed button. + $this->drupalGet('admin/config/content/embed/button/manage/' . $button_id . '/delete'); + $this->drupalPostForm(NULL, [], 'Delete'); + // Ensure that the deleted embed button no longer exists. + $this->drupalGet('admin/config/content/embed/button/manage/' . $button_id); + $assert_session->pageTextContains('The requested page could not be found.'); + // Ensure that the deleted button is no longer listed. + $this->drupalGet('admin/config/content/embed'); + $assert_session->pageTextNotContains($button_label); + } + + public function testButtonValidation() { + $page = $this->getSession()->getPage(); + $assert_session = $this->assertSession(); + + $this->drupalLogin($this->adminUser); + $this->drupalGet('admin/config/content/embed/button/add'); + + $button_label = $this->randomMachineName(); + $button_id = strtolower($button_label); + $page->fillField('label', $button_label); + $this->assertNotEmpty($assert_session->waitForText("Machine name: $button_id")); + $page->selectFieldOption('type_id', 'embed_test_aircraft'); + $aircraft_type = $assert_session->waitForField('type_settings[aircraft_type]'); + $this->assertNotEmpty($aircraft_type); + $this->assertSame('fixed-wing', $aircraft_type->getValue()); + + $edit['type_settings[aircraft_type]'] = 'invalid'; + $this->drupalPostForm(NULL, $edit, 'Save'); + $assert_session->pageTextContains('Cannot select invalid aircraft type.'); + + $edit['type_settings[aircraft_type]'] = 'helicopters'; + $this->drupalPostForm(NULL, $edit, 'Save'); + $assert_session->pageTextContains('Helicopters are just rotorcraft.'); + + $this->drupalGet('admin/config/content/embed/button/manage/' . $button_id); + $this->assertFieldByName('type_settings[aircraft_type]', 'rotorcraft'); + } + + public function testCKEditorButtonConflict() { + $page = $this->getSession()->getPage(); + $assert_session = $this->assertSession(); + + $this->drupalLogin($this->adminUser); + $this->drupalGet('admin/config/content/embed/button/add'); + + $button_label = $this->randomMachineName(); + $button_id = strtolower($button_label); + $page->fillField('label', $button_label); + $this->assertNotEmpty($assert_session->waitForText("Machine name: $button_id")); + + $assert_session->elementExists('css', '#edit-label-machine-name-suffix') + ->pressButton('Edit'); + + $id = $assert_session->waitForField('id'); + $this->assertNotEmpty($id); + $id->setValue('DrupalImage'); + + $edit = [ + 'type_id' => 'embed_test_default', + ]; + $this->drupalPostForm(NULL, $edit, 'Save'); + } + +} diff --git a/web/modules/embed/tests/src/Kernel/IconFileUsageTest.php b/web/modules/embed/tests/src/Kernel/IconFileUsageTest.php deleted file mode 100644 index d815426f8c..0000000000 --- a/web/modules/embed/tests/src/Kernel/IconFileUsageTest.php +++ /dev/null @@ -1,72 +0,0 @@ -<?php - -namespace Drupal\Tests\embed\Kernel; - -use Drupal\embed\Entity\EmbedButton; -use Drupal\file\Entity\File; -use Drupal\KernelTests\KernelTestBase; - -/** - * Tests embed button icon file usage. - * - * @group embed - */ -class IconFileUsageTest extends KernelTestBase { - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = ['embed', 'embed_test']; - - /** - * Tests the embed_button and file usage integration. - */ - public function testEmbedButtonIconUsage() { - $this->enableModules(['system', 'user', 'file']); - - $this->installSchema('file', ['file_usage']); - $this->installConfig(['system']); - $this->installEntitySchema('user'); - $this->installEntitySchema('file'); - $this->installEntitySchema('embed_button'); - - $file1 = file_save_data(file_get_contents('core/misc/druplicon.png')); - $file1->setTemporary(); - $file1->save(); - - $file2 = file_save_data(file_get_contents('core/misc/druplicon.png')); - $file2->setTemporary(); - $file2->save(); - - $button = [ - 'id' => 'test_button', - 'label' => 'Testing embed button instance', - 'type_id' => 'embed_test_default', - 'icon_uuid' => $file1->uuid(), - ]; - - $entity = EmbedButton::create($button); - $entity->save(); - $this->assertTrue(File::load($file1->id())->isPermanent()); - - // Delete the icon from the button. - $entity->icon_uuid = NULL; - $entity->save(); - $this->assertTrue(File::load($file1->id())->isTemporary()); - - $entity->icon_uuid = $file1->uuid(); - $entity->save(); - $this->assertTrue(File::load($file1->id())->isPermanent()); - - $entity->icon_uuid = $file2->uuid(); - $entity->save(); - $this->assertTrue(File::load($file1->id())->isTemporary()); - $this->assertTrue(File::load($file2->id())->isPermanent()); - - $entity->delete(); - $this->assertTrue(File::load($file2->id())->isTemporary()); - } - -} diff --git a/web/modules/embed/tests/src/Kernel/IconTest.php b/web/modules/embed/tests/src/Kernel/IconTest.php new file mode 100644 index 0000000000..ca34d46d30 --- /dev/null +++ b/web/modules/embed/tests/src/Kernel/IconTest.php @@ -0,0 +1,90 @@ +<?php + +namespace Drupal\Tests\embed\Kernel; + +use Drupal\embed\EmbedButtonInterface; +use Drupal\embed\Entity\EmbedButton; +use Drupal\KernelTests\KernelTestBase; + +/** + * Tests embed button icon file handling. + * + * @group embed + * @coversDefaultClass \Drupal\embed\Entity\EmbedButton + */ +class IconTest extends KernelTestBase { + + /** + * {@inheritdoc} + */ + public static $modules = [ + 'system', + 'embed', + 'embed_test', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + $this->installConfig('system'); + $this->installEntitySchema('embed_button'); + } + + /** + * Tests the icon functionality. + * + * @covers ::convertImageToEncodedData + * @covers ::convertEncodedDataToImage + * @covers ::getIconUrl + */ + public function testIcon() { + $button = EmbedButton::create([ + 'id' => 'test', + 'label' => 'Test', + 'type_id' => 'embed_test_default', + ]); + $this->assertEmpty($button->icon); + $this->assertIconUrl('/default.png', $button); + + $uri = 'public://button.png'; + $image_contents = file_get_contents('core/misc/favicon.ico'); + file_put_contents($uri, $image_contents); + + $button->set('icon', EmbedButton::convertImageToEncodedData($uri)); + $this->assertSame([ + 'data' => base64_encode($image_contents), + 'uri' => $uri, + ], $button->icon); + $this->assertIconUrl($uri, $button); + + // Delete the file and call getIconUrl and test that it recreated the file. + unlink($uri); + $this->assertFalse(is_file($uri)); + $this->assertIconUrl($uri, $button); + $this->assertTrue(is_file($uri)); + $this->assertSame(file_get_contents($uri), file_get_contents('core/misc/favicon.ico')); + + // Test a manual, external URL for the icon image. + $button->set('icon', [ + 'uri' => 'http://www.example.com/button.png', + ]); + $this->assertIconUrl('http://www.example.com/button.png', $button); + } + + /** + * Test a button's icon URL. + * + * @param string $uri + * The exepcted URI to the icon file. + * @param \Drupal\embed\EmbedButtonInterface $button + * The embed button. + * @param string $message + * The assertion message. + */ + protected function assertIconUrl($uri, EmbedButtonInterface $button, string $message = '') { + $this->assertSame(file_url_transform_relative(file_create_url($uri)), $button->getIconUrl(), $message); + } + +} diff --git a/web/modules/entity_embed/.travis-before-script.sh b/web/modules/entity_embed/.travis-before-script.sh deleted file mode 100644 index 8c29b41ef7..0000000000 --- a/web/modules/entity_embed/.travis-before-script.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -set -e $DRUPAL_TI_DEBUG - -# Ensure the right Drupal version is installed. -# Note: This function is re-entrant. -drupal_ti_ensure_drupal - -# Download dependencies -mkdir -p "$DRUPAL_TI_DRUPAL_DIR/$DRUPAL_TI_MODULES_PATH" -cd "$DRUPAL_TI_DRUPAL_DIR/$DRUPAL_TI_MODULES_PATH" -git clone --depth 1 --branch 8.x-1.x https://github.com/drupal-media/embed.git diff --git a/web/modules/entity_embed/.travis.yml b/web/modules/entity_embed/.travis.yml deleted file mode 100644 index 7db1ba782b..0000000000 --- a/web/modules/entity_embed/.travis.yml +++ /dev/null @@ -1,117 +0,0 @@ -# @file -# .travis.yml - Drupal for Travis CI Integration -# -# Template provided by https://github.com/LionsAd/drupal_ti. -# -# Based for simpletest upon: -# https://github.com/sonnym/travis-ci-drupal-module-example - -language: php -sudo: false - -cache: - directories: - - $HOME/.composer/cache - -php: - - 5.5 - - 5.6 - - 7 - - hhvm - -matrix: - fast_finish: true - allow_failures: - - php: 7 - - php: hhvm - -branches: - only: - - "8.x-1.x" - -env: - global: - # add composer's global bin directory to the path - # see: https://github.com/drush-ops/drush#install---composer - - PATH="$PATH:$HOME/.composer/vendor/bin" - - # Configuration variables. - - DRUPAL_TI_MODULE_NAME="entity_embed" - - DRUPAL_TI_SIMPLETEST_GROUP="entity_embed" - - # Define runners and environment vars to include before and after the - # main runners / environment vars. - #- DRUPAL_TI_SCRIPT_DIR_BEFORE="./drupal_ti/before" - #- DRUPAL_TI_SCRIPT_DIR_AFTER="./drupal_ti/after" - - # The environment to use, supported are: drupal-7, drupal-8 - - DRUPAL_TI_ENVIRONMENT="drupal-8" - - # Drupal specific variables. - - DRUPAL_TI_DB="drupal_travis_db" - - DRUPAL_TI_DB_URL="mysql://root:@127.0.0.1/drupal_travis_db" - # Note: Do not add a trailing slash here. - - DRUPAL_TI_WEBSERVER_URL="http://127.0.0.1" - - DRUPAL_TI_WEBSERVER_PORT="8080" - - # Simpletest specific commandline arguments, the DRUPAL_TI_SIMPLETEST_GROUP is appended at the end. - - DRUPAL_TI_SIMPLETEST_ARGS="--verbose --color --concurrency 4 --url $DRUPAL_TI_WEBSERVER_URL:$DRUPAL_TI_WEBSERVER_PORT" - - # === Behat specific variables. - # This is relative to $TRAVIS_BUILD_DIR - - DRUPAL_TI_BEHAT_DIR="./tests/behat" - # These arguments are passed to the bin/behat command. - - DRUPAL_TI_BEHAT_ARGS="" - # Specify the filename of the behat.yml with the $DRUPAL_TI_DRUPAL_DIR variables. - - DRUPAL_TI_BEHAT_YML="behat.yml.dist" - # This is used to setup Xvfb. - - DRUPAL_TI_BEHAT_SCREENSIZE_COLOR="1280x1024x16" - # The version of seleniumthat should be used. - - DRUPAL_TI_BEHAT_SELENIUM_VERSION="2.44" - # Set DRUPAL_TI_BEHAT_DRIVER to "selenium" to use "firefox" or "chrome" here. - - DRUPAL_TI_BEHAT_DRIVER="phantomjs" - - DRUPAL_TI_BEHAT_BROWSER="firefox" - - # PHPUnit specific commandline arguments. - - DRUPAL_TI_PHPUNIT_ARGS="" - - # Code coverage via coveralls.io - - DRUPAL_TI_COVERAGE="satooshi/php-coveralls:0.6.*" - # This needs to match your .coveralls.yml file. - - DRUPAL_TI_COVERAGE_FILE="build/logs/clover.xml" - - # Debug options - #- DRUPAL_TI_DEBUG="-x -v" - # Set to "all" to output all files, set to e.g. "xvfb selenium" or "selenium", - # etc. to only output those channels. - #- DRUPAL_TI_DEBUG_FILE_OUTPUT="selenium xvfb webserver" - - matrix: - # [[[ SELECT ANY OR MORE OPTIONS ]]] - #- DRUPAL_TI_RUNNERS="phpunit" - - DRUPAL_TI_RUNNERS="simpletest" - #- DRUPAL_TI_RUNNERS="behat" - #- DRUPAL_TI_RUNNERS="phpunit simpletest behat" - -mysql: - database: drupal_travis_db - username: root - encoding: utf8 - -before_install: - - composer self-update - - composer global require "lionsad/drupal_ti:1.*" - - drupal-ti before_install - -install: - - drupal-ti install - -before_script: - - drupal-ti --include .travis-before-script.sh - - drupal-ti before_script - -script: - - drupal-ti script - -after_script: - - drupal-ti after_script diff --git a/web/modules/entity_embed/DEVELOPING.md b/web/modules/entity_embed/DEVELOPING.md deleted file mode 100644 index 15a659f19a..0000000000 --- a/web/modules/entity_embed/DEVELOPING.md +++ /dev/null @@ -1,4 +0,0 @@ -# Developing - -* Issues should be filed at http://drupal.org/project/issues/entity_embed -* Pull requests can be made against https://github.com/drupal-media/entity_embed/pulls diff --git a/web/modules/entity_embed/README.md b/web/modules/entity_embed/README.md index a13275ecf7..a11575c2f4 100644 --- a/web/modules/entity_embed/README.md +++ b/web/modules/entity_embed/README.md @@ -1,8 +1,5 @@ # Entity Embed Module -[](https://travis-ci.org/drupal-media/entity_embed) -[](https://scrutinizer-ci.com/g/drupal-media/entity_embed) - [Entity Embed](https://www.drupal.org/project/entity_embed) module allows any entity to be embedded using a text editor. @@ -30,7 +27,7 @@ Entity Embed can be installed via the * If the text format uses the 'Limit allowed HTML tags and correct faulty HTML' filter, ensure the necessary tags and attributes were automatically whitelisted: - ```<drupal-entity data-entity-type data-entity-uuid data-view-mode data-entity-embed-display data-entity-embed-display-settings data-align data-caption data-embed-button>``` + ```<drupal-entity data-entity-type data-entity-uuid data-view-mode data-entity-embed-display data-entity-embed-display-settings data-align data-caption data-embed-button data-langcode alt title>``` appears in the 'Allowed HTML tags' setting. *Warning: If you were using the module in very early pre-alpha stages you might need to add `data-entity-id` to the list of allowed @@ -90,7 +87,10 @@ different Entity Embed Display plugins out of the box: formatter. This will only work if the entity is a file entity type. - image:_formatter_id_: Renders the entity using a specific Image field formatter. This will only work if the entity is a file entity type, - and the file is an image. + and the file is an image. For the alt and title text to save, the `alt` + and `title` attributes must be allowed on the `<drupal-entity>` HTML tag + in the "Allowed HTML tags" for text formats that have the "Limit allowed + HTML tags and correct faulty HTML" filter enabled. Configuration for the Entity Embed Display plugin can be provided by using a `data-entity-embed-display-settings` attribute, which contains a diff --git a/web/modules/entity_embed/composer.json b/web/modules/entity_embed/composer.json index c10ecf4038..5fd24c40b6 100644 --- a/web/modules/entity_embed/composer.json +++ b/web/modules/entity_embed/composer.json @@ -7,5 +7,12 @@ "issues": "https://www.drupal.org/project/issues/entity_embed", "irc": "irc://irc.freenode.org/drupal-media" }, - "license": "GPL-2.0+" + "license": "GPL-2.0+", + "require": { + "drupal/core": "^8.8 || ^9", + "drupal/embed": "^1.3" + }, + "require-dev": { + "drupal/entity_browser": "^2.2" + } } diff --git a/web/modules/entity_embed/css/entity_embed.dialog.css b/web/modules/entity_embed/css/entity_embed.dialog.css index 5dabd951cb..23d7f0ec3f 100644 --- a/web/modules/entity_embed/css/entity_embed.dialog.css +++ b/web/modules/entity_embed/css/entity_embed.dialog.css @@ -13,18 +13,18 @@ @media screen and (min-width: 768px) { .ui-dialog--narrow.entity-select-dialog .entity-embed-dialog-step--select { - min-width: 730px; // 95% + min-width: 730px; /* 95% */ } } @media screen and (min-width: 1000px) { .ui-dialog--narrow.entity-select-dialog .entity-embed-dialog-step--select { - min-width: 950px; // 95% + min-width: 950px; /* 95% */ } } @media screen and (min-width: 1200px) { .ui-dialog--narrow.entity-select-dialog .entity-embed-dialog-step--select { - min-width: 1140px; // 95% + min-width: 1140px; /* 95% */ } } diff --git a/web/modules/entity_embed/css/entity_embed.editor.css b/web/modules/entity_embed/css/entity_embed.editor.css new file mode 100644 index 0000000000..f453a09150 --- /dev/null +++ b/web/modules/entity_embed/css/entity_embed.editor.css @@ -0,0 +1,15 @@ +/** + * @file + * Styles for CKEditor iframe. + */ + +drupal-entity { + display: inline-block; +} +drupal-entity[data-align=left], +drupal-entity[data-align=right] { + display: inline; +} +drupal-entity[data-align=center] { + display: flex; +} diff --git a/web/modules/entity_embed/css/entity_embed.filter.caption.css b/web/modules/entity_embed/css/entity_embed.filter.caption.css new file mode 100644 index 0000000000..d1648f4ed9 --- /dev/null +++ b/web/modules/entity_embed/css/entity_embed.filter.caption.css @@ -0,0 +1,10 @@ +/** + * @file + * Caption filter: default styling for displaying Entity Embed captions. + */ + +.caption .media .field, +.caption .media .field * { + float: none; + margin: unset; +} diff --git a/web/modules/entity_embed/entity_embed.api.php b/web/modules/entity_embed/entity_embed.api.php index fa762ad125..ec83c6e91e 100644 --- a/web/modules/entity_embed/entity_embed.api.php +++ b/web/modules/entity_embed/entity_embed.api.php @@ -72,7 +72,7 @@ function hook_entity_embed_context_alter(array &$context, \Drupal\Core\Entity\En } /** - * Alter the context of an particular embedded entity type before it is rendered. + * Alter the context of a particular embedded entity type before it is rendered. * * @param array &$context * The context array. diff --git a/web/modules/entity_embed/entity_embed.info.yml b/web/modules/entity_embed/entity_embed.info.yml index b0927604f3..23026e80f1 100644 --- a/web/modules/entity_embed/entity_embed.info.yml +++ b/web/modules/entity_embed/entity_embed.info.yml @@ -1,17 +1,17 @@ name: Entity Embed type: module description: 'Allows entities to be embedded using a text editor.' -# core: 8.x +core_version_requirement: ^8.8 || ^9 package: Filters dependencies: - drupal:editor - embed:embed - drupal:filter + - drupal:system test_dependencies: - entity_browser:entity_browser -# Information added by Drupal.org packaging script on 2016-10-17 -version: '8.x-1.0-beta2' -core: '8.x' +# Information added by Drupal.org packaging script on 2020-03-26 +version: '8.x-1.1' project: 'entity_embed' -datestamp: 1476698379 +datestamp: 1585252809 diff --git a/web/modules/entity_embed/entity_embed.install b/web/modules/entity_embed/entity_embed.install index 30a70f6641..6ebd6fa7fc 100644 --- a/web/modules/entity_embed/entity_embed.install +++ b/web/modules/entity_embed/entity_embed.install @@ -5,6 +5,8 @@ * Contains install and update functions for Entity Embed. */ +use Drupal\Core\Entity\ContentEntityType; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\embed\Entity\EmbedButton; use Drupal\Core\Utility\UpdateException; @@ -55,8 +57,7 @@ function entity_embed_update_8002() { } /** - * Updates allowed HTML for all filter format config entities that have an - * Entity Embed button. + * Updates allowed HTML for all filter configs that have an Entity Embed button. */ function entity_embed_update_8003() { $buttons = \Drupal::entityTypeManager()->getStorage('embed_button')->loadMultiple(); @@ -85,3 +86,16 @@ function entity_embed_update_8003() { } } } + +/** + * Adds new content entity type to remove dependency on node module. + */ +function entity_embed_update_8004() { + \Drupal::entityDefinitionUpdateManager()->installEntityType(new ContentEntityType([ + 'id' => 'entity_embed_fake_entity', + 'label' => new TranslatableMarkup('Fake entity type'), + 'handlers' => [ + 'storage' => 'Drupal\\Core\\Entity\\ContentEntityNullStorage', + ], + ])); +} diff --git a/web/modules/entity_embed/entity_embed.libraries.yml b/web/modules/entity_embed/entity_embed.libraries.yml index 5eca8e772c..d56978dc70 100644 --- a/web/modules/entity_embed/entity_embed.libraries.yml +++ b/web/modules/entity_embed/entity_embed.libraries.yml @@ -8,3 +8,11 @@ drupal.entity_embed.dialog: dependencies: - core/drupal - core/jquery + +caption: + version: VERSION + css: + component: + css/entity_embed.filter.caption.css: {} + dependencies: + - filter/caption diff --git a/web/modules/entity_embed/entity_embed.module b/web/modules/entity_embed/entity_embed.module index ee8fbaee84..a531922b78 100644 --- a/web/modules/entity_embed/entity_embed.module +++ b/web/modules/entity_embed/entity_embed.module @@ -2,12 +2,33 @@ /** * @file - * Framework for allowing entities to be embedded using CKEditor plugin and text - * format. + * Framework for allowing entities to be embedded in CKEditor. */ -use Drupal\Component\Utility\UrlHelper; -use Drupal\Core\Url; +use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Field\FieldItemListInterface; +use Drupal\Core\Entity\ContentEntityInterface; + +/** + * Implements hook_help(). + */ +function entity_embed_help($route_name, RouteMatchInterface $route_match) { + switch ($route_name) { + case 'help.page.entity_embed': + $output = ''; + $output .= '<h3>' . t('About') . '</h3>'; + $output .= '<p>' . t('The Entity Embed module allows entities to be embedded in formatted text.') . '</p>'; + $output .= '<h3>' . t('Uses') . '</h3>'; + $output .= '<dl>'; + $output .= '<dt>' . t('Embedding media') . '</dt>'; + $output .= '<dd>' . t('This module, and the text filter that it provides along with the CKEditor integration, is especially suited to allow content authors to embed media in their textual content: images, video, and so on.') . '</dd>'; + $output .= '<dt>' . t('Embedding arbitrary content') . '</dt>'; + $output .= '<dd>' . t('As mentioned above, this module is especially helpful for embedding media in textual content, but it is not necessarily restricted to that; it allows <em>any</em> entity to be embedded. On an e-commerce site, you may want to embed products, on a company blog you may want to embed past projects, and so on.') . '</dd>'; + $output .= '</dl>'; + return $output; + } +} /** * Implements hook_theme(). @@ -30,33 +51,16 @@ function entity_embed_theme() { * - element: An associative array containing the properties of the element. * Properties used: #attributes, #children. */ -function template_preprocess_entity_embed_container(&$variables) { +function template_preprocess_entity_embed_container(array &$variables) { $variables['element'] += ['#attributes' => []]; $variables['attributes'] = $variables['element']['#attributes']; $variables['children'] = $variables['element']['#children']; - if (!empty($variables['element']['#context']['data-entity-embed-display-settings']['link_url'])) { - $link = UrlHelper::filterBadProtocol($variables['element']['#context']['data-entity-embed-display-settings']['link_url']); - if (!UrlHelper::isExternal($link)) { - $link = 'internal:/' . ltrim($link, '/'); - } - $link = Url::fromUri($link); - $attributes = []; - if (!empty($variables['element']['#context']['data-entity-embed-display-settings']['link_url_target']) && $variables['element']['#context']['data-entity-embed-display-settings']['link_url_target'] == 1) { - $attributes = ['attributes' => ['target' => '_blank']]; - } - $variables['children'] = [ - [ - '#type' => 'link', - '#title' => $variables['children'], - '#options' => $attributes, - '#url' => $link, - ] - ]; - } } /** - * Implements hook_entity_embed_display_plugins_alter() on behalf of file.module. + * Implements hook_entity_embed_display_plugins_alter(). + * + * Implementation on behalf of the file module. */ function file_entity_embed_display_plugins_alter(array &$plugins) { // The RSS enclosure field formatter is not usable for Entity Embed. @@ -64,7 +68,9 @@ function file_entity_embed_display_plugins_alter(array &$plugins) { } /** - * Implements hook_entity_embed_display_plugins_alter() on behalf of taxonomy.module. + * Implements hook_entity_embed_display_plugins_alter(). + * + * Implementation on behalf of the taxonomy module. */ function taxonomy_entity_embed_display_plugins_alter(array &$plugins) { // The RSS category field formatter is not usable for Entity Embed. @@ -86,3 +92,195 @@ function entity_embed_entity_embed_display_plugins_for_context_alter(array &$def unset($definitions['entity_reference:entity_reference_entity_view']); } } + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function entity_embed_form_filter_format_edit_form_alter(array &$form, FormStateInterface $form_state, $form_id) { + // Add an additional validate callback so we can ensure the order of filters + // is correct. + $form['#validate'][] = 'entity_embed_filter_format_edit_form_validate'; +} + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function entity_embed_form_filter_format_add_form_alter(array &$form, FormStateInterface $form_state, $form_id) { + // Add an additional validate callback so we can ensure the order of filters + // is correct. + $form['#validate'][] = 'entity_embed_filter_format_edit_form_validate'; +} + +/** + * Validate callback to ensure filter order and allowed_html are compatible. + */ +function entity_embed_filter_format_edit_form_validate($form, FormStateInterface $form_state) { + // This validate handler is not applicable when using the 'Configure' button. + if ($form_state->getTriggeringElement()['#name'] === 'editor_configure') { + return; + } + + $allowed_html_path = [ + 'filters', + 'filter_html', + 'settings', + 'allowed_html', + ]; + + $button_group_path = [ + 'editor', + 'settings', + 'toolbar', + 'button_groups', + ]; + + $filter_html_settings_path = [ + 'filters', + 'filter_html', + 'settings', + ]; + + $filter_html_enabled = $form_state->getValue([ + 'filters', + 'filter_html', + 'status', + ]); + + $entity_embed_enabled = $form_state->getValue([ + 'filters', + 'entity_embed', + 'status', + ]); + + if ($entity_embed_enabled && $filter_html_enabled && $allowed_html = $form_state->getValue($allowed_html_path)) { + if ($button_groups = $form_state->getValue($button_group_path)) { + $buttons = []; + $button_groups = json_decode($button_groups, TRUE); + if (!empty($button_groups[0])) { + foreach ($button_groups[0] as $button_group) { + foreach ($button_group['items'] as $item) { + $buttons[] = $item; + } + } + } + + /** @var \Drupal\filter\Entity\FilterFormat $filter_format */ + $filter_format = $form_state->getFormObject()->getEntity(); + + $filter_html = $filter_format->filters()->get('filter_html'); + $filter_html->setConfiguration(['settings' => $form_state->getValue($filter_html_settings_path)]); + $restrictions = $filter_html->getHTMLRestrictions(); + $allowed = $restrictions['allowed']; + + $embeds = \Drupal::entityTypeManager() + ->getStorage('embed_button') + ->loadMultiple($buttons); + + /** @var \Drupal\embed\Entity\EmbedButton $embed */ + foreach ($embeds as $embed) { + if ($embed->getTypeId() !== 'entity') { + continue; + } + // Require `<drupal-entity>` HTML tag if filter_html is enabled. + if (!isset($allowed['drupal-entity'])) { + $form_state->setError($form['filters']['settings']['filter_html']['allowed_html'], t('The %embed button requires <code><drupal-entity></code> among the allowed HTML tags.', [ + '%embed' => $embed->label(), + ])); + break; + } + else { + $required_attributes = [ + 'data-entity-type', + 'data-entity-uuid', + 'data-entity-embed-display', + 'data-entity-embed-display-settings', + 'data-align', + 'data-caption', + 'data-embed-button', + 'alt', + 'title', + ]; + + // If there are no attributes, the allowed item is set to FALSE, + // otherwise, it is set to an array. + if ($allowed['drupal-entity'] === FALSE) { + $missing_attributes = $required_attributes; + } + else { + $missing_attributes = array_diff($required_attributes, array_keys($allowed['drupal-entity'])); + } + + if ($missing_attributes) { + $form_state->setError($form['filters']['settings']['filter_html']['allowed_html'], t('The <code><drupal-entity></code> tag in the allowed HTML tags is missing the following attributes: <code>%list</code>.', [ + '%list' => implode(', ', $missing_attributes), + ])); + break; + } + } + } + } + } + + $filters = $form_state->getValue('filters'); + + $get_filter_label = function ($filter_plugin_id) use ($form) { + return (string) $form['filters']['order'][$filter_plugin_id]['filter']['#markup']; + }; + + // The "entity_embed" filter must run after "filter_align", "filter_caption", + // and "filter_html_image_secure". + if ($entity_embed_enabled) { + $precedents = [ + 'filter_align', + 'filter_caption', + 'filter_html_image_secure', + ]; + + $error_filters = []; + foreach ($precedents as $filter_name) { + + // A filter that should run before entity embed filter. + $precedent = $filters[$filter_name]; + + if (empty($precedent['status']) || !isset($precedent['weight'])) { + continue; + } + + if ($precedent['weight'] >= $filters['entity_embed']['weight']) { + $error_filters[$filter_name] = $get_filter_label($filter_name); + } + } + + if (!empty($error_filters)) { + $singular = 'The %entity-embed-filter-label filter needs to be placed after the %filter filter.'; + $plural = 'The %entity-embed-filter-label filter needs to be placed after the following filters: %filters.'; + $error_message = \Drupal::translation()->formatPlural(count($error_filters), $singular, $plural, [ + '%entity-embed-filter-label' => $get_filter_label('entity_embed'), + '%filter' => reset($error_filters), + '%filters' => implode(', ', $error_filters), + ]); + + $form_state->setErrorByName('filters', $error_message); + } + } +} + +/** + * Implements hook_field_widget_form_alter(). + */ +function entity_embed_field_widget_form_alter(&$element, FormStateInterface $form_state, $context) { + // Add a `data-entity_embed-host-entity-langcode` attribute so that + // entity_embed's JavaScript can pass the host entity's language to + // EntityEmbedDialog, allowing it to present entities in the same language. + if (!empty($element['#type']) && $element['#type'] == 'text_format') { + if (!empty($context['items']) && $context['items'] instanceof FieldItemListInterface) { + $element['#attributes']['data-entity_embed-host-entity-langcode'] = $context['items']->getLangcode(); + } + else { + $entity = $form_state->getFormObject()->getEntity(); + if ($entity instanceof ContentEntityInterface) { + $element['#attributes']['data-entity_embed-host-entity-langcode'] = $entity->language()->getId(); + } + } + } +} diff --git a/web/modules/entity_embed/entity_embed.routing.yml b/web/modules/entity_embed/entity_embed.routing.yml index 0c61eed6ed..57b67103c4 100644 --- a/web/modules/entity_embed/entity_embed.routing.yml +++ b/web/modules/entity_embed/entity_embed.routing.yml @@ -5,5 +5,10 @@ entity_embed.dialog: _title: 'Embed entity' requirements: _embed_button_editor_access: 'TRUE' - options: - _theme: ajax_base_page + +entity_embed.preview: + path: '/entity-embed/preview/{filter_format}' + defaults: + _controller: '\Drupal\entity_embed\Controller\PreviewController::preview' + requirements: + _entity_access: 'filter_format.use' diff --git a/web/modules/entity_embed/entity_embed.services.yml b/web/modules/entity_embed/entity_embed.services.yml index 076de85fbb..9202c46bff 100644 --- a/web/modules/entity_embed/entity_embed.services.yml +++ b/web/modules/entity_embed/entity_embed.services.yml @@ -4,7 +4,7 @@ services: arguments: ['@container.namespaces', '@cache.discovery', '@module_handler'] entity_embed.twig.entity_embed_twig_extension: class: Drupal\entity_embed\Twig\EntityEmbedTwigExtension - arguments: ['@entity.manager', '@entity_embed.builder'] + arguments: ['@entity_type.manager', '@entity_embed.builder'] tags: - { name: twig.extension } entity_embed.builder: diff --git a/web/modules/entity_embed/js/plugins/drupalentity/plugin.js b/web/modules/entity_embed/js/plugins/drupalentity/plugin.js index b8a5090c97..aa620e6de0 100644 --- a/web/modules/entity_embed/js/plugins/drupalentity/plugin.js +++ b/web/modules/entity_embed/js/plugins/drupalentity/plugin.js @@ -3,27 +3,98 @@ * Drupal Entity embed plugin. */ -(function ($, Drupal, CKEDITOR) { +(function (jQuery, Drupal, CKEDITOR) { "use strict"; + function getFocusedWidget(editor) { + var widget = editor.widgets.focused; + + if (widget && widget.name === 'drupalentity') { + return widget; + } + + return null; + } + + function linkCommandIntegrator(editor) { + if (!editor.plugins.drupallink) { + return; + } + + editor.getCommand('drupalunlink').on('exec', function (evt) { + var widget = getFocusedWidget(editor); + + if (!widget) { + return; + } + + widget.setData('link', null); + + this.refresh(editor, editor.elementPath()); + + evt.cancel(); + }); + + editor.getCommand('drupalunlink').on('refresh', function (evt) { + var widget = getFocusedWidget(editor); + + if (!widget) { + return; + } + + this.setState(widget.data.link ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED); + + evt.cancel(); + }); + } + CKEDITOR.plugins.add('drupalentity', { - // This plugin requires the Widgets System defined in the 'widget' plugin. requires: 'widget', - // The plugin initialization logic goes inside this method. beforeInit: function (editor) { // Configure CKEditor DTD for custom drupal-entity element. // @see https://www.drupal.org/node/2448449#comment-9717735 var dtd = CKEDITOR.dtd, tagName; dtd['drupal-entity'] = {'#': 1}; - // Register drupal-entity element as allowed child, in each tag that can + // Register drupal-entity element as an allowed child in each tag that can // contain a div element. for (tagName in dtd) { if (dtd[tagName].div) { dtd[tagName]['drupal-entity'] = 1; } } + dtd['a']['drupal-entity'] = 1; + + // The drupallink plugin has a hardcoded integration with + // drupalimage. If the drupallink plugin has the + // registerLinkableWidget() method (which was added in Drupal 8.8), we + // don't need this workaround. + if (editor.plugins.drupallink) { + if (CKEDITOR.plugins.drupallink.hasOwnProperty('registerLinkableWidget')) { + CKEDITOR.plugins.drupallink.registerLinkableWidget('drupalentity'); + } else { + // drupallink has a hardcoded integration with drupalimage. Workaround + // that, to reuse the same integration. + var originalGetFocusedWidget = null; + if (CKEDITOR.plugins.drupalimage) { + originalGetFocusedWidget = CKEDITOR.plugins.drupalimage.getFocusedWidget; + } else { + CKEDITOR.plugins.drupalimage = {}; + } + CKEDITOR.plugins.drupalimage.getFocusedWidget = function () { + var ourFocusedWidget = getFocusedWidget(editor); + if (ourFocusedWidget) { + return ourFocusedWidget; + } + // If drupalimage is loaded, call that next, to not break its link command integration. + if (originalGetFocusedWidget) { + return originalGetFocusedWidget(editor); + } + return null; + }; + } + } // Generic command for adding/editing entities of all types. editor.addCommand('editdrupalentity', { @@ -35,20 +106,18 @@ data = data || {}; var existingElement = getSelectedEmbeddedEntity(editor); + var existingWidget = (existingElement) ? editor.widgets.getByElement(existingElement, true) : null; var existingValues = {}; - if (existingElement && existingElement.$ && existingElement.$.firstChild) { - var embedDOMElement = existingElement.$.firstChild; - // Populate array with the entity's current attributes. - var attribute = null, attributeName; - for (var key = 0; key < embedDOMElement.attributes.length; key++) { - attribute = embedDOMElement.attributes.item(key); - attributeName = attribute.nodeName.toLowerCase(); - if (attributeName.substring(0, 15) === 'data-cke-saved-') { - continue; - } - existingValues[attributeName] = existingElement.data('cke-saved-' + attributeName) || attribute.nodeValue; - } + + // Host entity's langcode added in entity_embed_field_widget_form_alter(). + var hostEntityLangcode = document.getElementById(editor.name).getAttribute('data-entity_embed-host-entity-langcode'); + if (hostEntityLangcode) { + existingValues['data-langcode'] = hostEntityLangcode; + } + + if (existingWidget) { + existingValues = existingWidget.data.attributes; } var embed_button_id = data.id ? data.id : existingValues['data-embed-button']; @@ -59,19 +128,24 @@ }; var saveCallback = function (values) { - var entityElement = editor.document.createElement('drupal-entity'); - var attributes = values.attributes; - for (var key in attributes) { - entityElement.setAttribute(key, attributes[key]); + editor.fire('saveSnapshot'); + if (!existingElement) { + var entityElement = editor.document.createElement('drupal-entity'); + var attributes = values.attributes; + for (var key in attributes) { + entityElement.setAttribute(key, attributes[key]); + } + editor.insertHtml(entityElement.getOuterHtml()); } - - editor.insertHtml(entityElement.getOuterHtml()); - if (existingElement) { - // Detach the behaviors that were attached when the entity content - // was inserted. - Drupal.runEmbedBehaviors('detach', existingElement.$); - existingElement.remove(); + else { + var hasCaption = false; + if (values.attributes['data-caption']) { + values.attributes['data-caption'] = CKEDITOR.tools.htmlDecodeAttr(values.attributes['data-caption']); + hasCaption = true; + } + existingWidget.setData({ attributes: values.attributes, hasCaption: hasCaption }); } + editor.fire('saveSnapshot'); }; // Open the entity embed dialog for corresponding EmbedButton. @@ -85,49 +159,192 @@ allowedContent: 'drupal-entity[data-entity-type,data-entity-uuid,data-entity-embed-display,data-entity-embed-display-settings,data-align,data-caption]', requiredContent: 'drupal-entity[data-entity-type,data-entity-uuid,data-entity-embed-display,data-entity-embed-display-settings,data-align,data-caption]', - // Simply recognize the element as our own. The inner markup if fetched - // and inserted the init() callback, since it requires the actual DOM - // element. - upcast: function (element) { + pathName: Drupal.t('Embedded entity'), + + editables: { + caption: { + selector: 'figcaption', + allowedContent: 'a[!href]; em strong cite code br', + pathName: Drupal.t('Caption'), + } + }, + + upcast: function (element, data) { var attributes = element.attributes; - if (attributes['data-entity-type'] === undefined || (attributes['data-entity-id'] === undefined && attributes['data-entity-uuid'] === undefined) || (attributes['data-view-mode'] === undefined && attributes['data-entity-embed-display'] === undefined)) { + if (element.name !== 'drupal-entity' || attributes['data-entity-type'] === undefined || (attributes['data-entity-id'] === undefined && attributes['data-entity-uuid'] === undefined) || (attributes['data-view-mode'] === undefined && attributes['data-entity-embed-display'] === undefined)) { return; } - // Generate an ID for the element, so that we can use the Ajax - // framework. - element.attributes.id = generateEmbedId(); + data.attributes = CKEDITOR.tools.copy(attributes); + data.hasCaption = data.attributes.hasOwnProperty('data-caption'); + data.link = null; + if (element.parent.name === 'a') { + data.link = CKEDITOR.tools.copy(element.parent.attributes); + // Omit CKEditor-internal attributes. + Object.keys(element.parent.attributes).forEach(function (attrName) { + if (attrName.indexOf('data-cke-') !== -1) { + delete data.link[attrName]; + } + }); + } return element; }, - // Fetch the rendered entity. init: function () { /** @type {CKEDITOR.dom.element} */ var element = this.element; - // Use the Ajax framework to fetch the HTML, so that we can retrieve - // out-of-band assets (JS, CSS...). - var entityEmbedPreview = Drupal.ajax({ - base: element.getId(), - element: element.$, - url: Drupal.url('embed/preview/' + editor.config.drupal.format + '?' + $.param({ - value: element.getOuterHtml() - })), - progress: {type: 'none'}, - // Use a custom event to trigger the call. - event: 'entity_embed_dummy_event' - }); - entityEmbedPreview.execute(); + + // See https://www.drupal.org/node/2544018. + if (element.hasAttribute('data-embed-button')) { + var buttonId = element.getAttribute('data-embed-button'); + if (editor.config.DrupalEntity_buttons[buttonId]) { + var button = editor.config.DrupalEntity_buttons[buttonId]; + this.wrapper.data('cke-display-name', Drupal.t('Embedded @buttonLabel', {'@buttonLabel': button.label})); + } + } }, - // Downcast the element. - downcast: function (element) { - // Only keep the wrapping element. - element.setHtml(''); - // Remove the auto-generated ID. - delete element.attributes.id; - return element; + destroy: function() { + this._tearDownDynamicEditables(); + }, + + data: function (event) { + if (this._previewNeedsServersideUpdate()) { + editor.fire('lockSnapshot'); + this._tearDownDynamicEditables(); + + this._loadPreview(function (widget) { + widget._setUpDynamicEditables(); + editor.fire('unlockSnapshot'); + }); + } + // @todo Remove in https://www.drupal.org/project/entity_embed/issues/3060397 + else if (this._previewNeedsClientsideUpdate()) { + this._performClientsideUpdate(); + editor.fire('saveSnapshot'); + } + + // Allow entity_embed.editor.css to respond to changes (for example in alignment). + this.element.setAttributes(this.data.attributes); + + // Track the previous state, to allow for smarter decisions. + this.oldData = CKEDITOR.tools.clone(this.data); + }, + + downcast: function () { + var downcastElement = new CKEDITOR.htmlParser.element('drupal-entity', this.data.attributes); + if (this.data.link) { + var link = new CKEDITOR.htmlParser.element('a', this.data.link); + link.add(downcastElement); + downcastElement = link; + } + return downcastElement; + }, + + _setUpDynamicEditables: function () { + // Now that the caption is available in the DOM, make it editable. + if (this.initEditable('caption', this.definition.editables.caption)) { + var captionEditable = this.editables.caption; + // @see core/modules/filter/css/filter.caption.css + // @see ckeditor_ckeditor_css_alter() + captionEditable.setAttribute('data-placeholder', Drupal.t('Enter caption here')); + // And ensure that any changes made to it are persisted. + var config = {characterData: true, attributes: true, childList: true, subtree: true}; + var widget = this; + this.captionEditableMutationObserver = new MutationObserver(function () { + var entityAttributes = CKEDITOR.tools.clone(widget.data.attributes); + entityAttributes['data-caption'] = captionEditable.getData(); + widget.setData('attributes', entityAttributes); + }); + this.captionEditableMutationObserver.observe(captionEditable.$, config); + } + }, + + _tearDownDynamicEditables: function () { + if (this.captionEditableMutationObserver) { + this.captionEditableMutationObserver.disconnect(); + } + }, + + _previewNeedsServersideUpdate: function () { + // When the widget is first loading, it of course needs to still get a preview! + if (!this.ready) { + return true; + } + + return this._hashData(this.oldData) !== this._hashData(this.data); + }, + + // @todo Remove in https://www.drupal.org/project/entity_embed/issues/3060397 + _previewNeedsClientsideUpdate: function () { + // The preview's caption must be updated when the caption was edited in EntityEmbedDialog. + // @see https://www.drupal.org/project/entity_embed/issues/3060397 + if (this.data.hasCaption && this.editables.caption.getData() !== this.data.attributes['data-caption']) { + return true; + } + + return false; + }, + + // @todo Remove in https://www.drupal.org/project/entity_embed/issues/3060397 + _performClientsideUpdate: function () { + if (this.data.hasCaption) { + this.captionEditableMutationObserver.disconnect(); + this.editables.caption.$.innerHTML = this.data.attributes['data-caption']; + var config = {characterData: true, attributes: false, childList: true, subtree: true}; + this.captionEditableMutationObserver.observe(this.editables.caption.$, config); + } + }, + + /** + * Computes a hash of the data that can only be previewed by the server. + */ + _hashData: function (data) { + var dataToHash = CKEDITOR.tools.clone(data); + // The caption does not need rendering. + if (dataToHash.attributes.hasOwnProperty('data-caption')) { + delete dataToHash.attributes['data-caption']; + } + // Changed link destinations do not affect the visual preview. + if (dataToHash.link && dataToHash.link.hasOwnProperty('href')) { + delete dataToHash.link.href; + } + return JSON.stringify(dataToHash); + }, + + /** + * Loads an entity embed preview, calls a callback after insertion. + * + * @param {function} callback + * A callback function that will be called after the preview has loaded, and receives the widget instance. + */ + _loadPreview: function (callback) { + var widget = this; + jQuery.get({ + url: Drupal.url('entity-embed/preview/' + editor.config.drupal.format + '?text=' + encodeURIComponent(this.downcast().getOuterHtml())), + dataType: 'html', + }).done(function(previewHtml) { + widget.element.setHtml(previewHtml); + callback(widget); + }); } }); + editor.widgets.on('instanceCreated', function (event) { + var widget = event.data; + + if (widget.name !== 'drupalentity') { + return; + } + + widget.on('edit', function (event) { + event.cancel(); + // @see https://www.drupal.org/node/2544018 + if (isEditableEntityWidget(editor, event.sender.wrapper)) { + editor.execCommand('editdrupalentity'); + } + }); + }); + // Register the toolbar buttons. if (editor.ui.addButton) { for (var key in editor.config.DrupalEntity_buttons) { @@ -135,40 +352,46 @@ editor.ui.addButton(button.id, { label: button.label, data: button, - allowedContent: 'drupal-entity[!data-entity-type,!data-entity-uuid,!data-entity-embed-display,!data-entity-embed-display-settings,!data-align,!data-caption,!data-embed-button]', + allowedContent: 'drupal-entity[!data-entity-type,!data-entity-uuid,!data-entity-embed-display,!data-entity-embed-display-settings,!data-align,!data-caption,!data-embed-button,!data-langcode,!alt,!title]', click: function(editor) { editor.execCommand('editdrupalentity', this.data); }, - icon: button.image + icon: button.image, + modes: {wysiwyg: 1, source: 0} }); } } - // Register context menu option for editing widget. + // Register context menu items for editing widget. if (editor.contextMenu) { editor.addMenuGroup('drupalentity'); - editor.addMenuItem('drupalentity', { - label: Drupal.t('Edit Entity'), - icon: this.path + 'entity.png', - command: 'editdrupalentity', - group: 'drupalentity' - }); + + for (var key in editor.config.DrupalEntity_buttons) { + var button = editor.config.DrupalEntity_buttons[key]; + + var label = Drupal.t('Edit @buttonLabel', { '@buttonLabel': button.label }); + + editor.addMenuItem('drupalentity_' + button.id, { + label: label, + icon: button.image, + command: 'editdrupalentity', + group: 'drupalentity' + }); + } editor.contextMenu.addListener(function(element) { if (isEditableEntityWidget(editor, element)) { - return { drupalentity: CKEDITOR.TRISTATE_OFF }; + var button_id = element.getFirst().getAttribute('data-embed-button'); + var returnData = {}; + returnData['drupalentity_' + button_id] = CKEDITOR.TRISTATE_OFF; + return returnData; } }); } + }, - // Execute widget editing action on double click. - editor.on('doubleclick', function (evt) { - var element = getSelectedEmbeddedEntity(editor) || evt.data.element; - - if (isEditableEntityWidget(editor, element)) { - editor.execCommand('editdrupalentity'); - } - }); + afterInit: function (editor) { + linkCommandIntegrator(editor); } }); @@ -199,7 +422,7 @@ return false; } - var button = $(element.$.firstChild).attr('data-embed-button'); + var button = element.$.firstChild.getAttribute('data-embed-button'); if (!button) { // If there was no data-embed-button attribute, not editable. return false; @@ -209,16 +432,4 @@ return editor.config.DrupalEntity_buttons.hasOwnProperty(button); } - /** - * Generates unique HTML IDs for the widgets. - * - * @returns {string} - */ - function generateEmbedId() { - if (typeof generateEmbedId.counter == 'undefined') { - generateEmbedId.counter = 0; - } - return 'entity-embed-' + generateEmbedId.counter++; - } - })(jQuery, Drupal, CKEDITOR); diff --git a/web/modules/entity_embed/src/Annotation/EntityEmbedDisplay.php b/web/modules/entity_embed/src/Annotation/EntityEmbedDisplay.php index 776ef82f69..3339088ca0 100644 --- a/web/modules/entity_embed/src/Annotation/EntityEmbedDisplay.php +++ b/web/modules/entity_embed/src/Annotation/EntityEmbedDisplay.php @@ -32,9 +32,9 @@ class EntityEmbedDisplay extends Plugin { /** * The human-readable name of the Entity Embed Display plugin. * - * @ingroup plugin_translatable - * * @var \Drupal\Core\Annotation\Translation + * + * @ingroup plugin_translatable */ public $label = ''; @@ -55,4 +55,14 @@ class EntityEmbedDisplay extends Plugin { */ public $no_ui = FALSE; + /** + * Alt and title access. + * + * Whether the plugin supports per-embed alt and title overrides for media + * entities with an image source. + * + * @var bool + */ + public $supports_image_alt_and_title = FALSE; + } diff --git a/web/modules/entity_embed/src/Controller/PreviewController.php b/web/modules/entity_embed/src/Controller/PreviewController.php new file mode 100644 index 0000000000..33102d9a14 --- /dev/null +++ b/web/modules/entity_embed/src/Controller/PreviewController.php @@ -0,0 +1,89 @@ +<?php + +namespace Drupal\entity_embed\Controller; + +use Drupal\Core\DependencyInjection\ContainerInjectionInterface; +use Drupal\Core\Render\RendererInterface; +use Drupal\filter\FilterFormatInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; + +/** + * Controller which renders a preview of the provided text. + */ +class PreviewController implements ContainerInjectionInterface { + + /** + * The renderer service. + * + * @var \Drupal\Core\Render\RendererInterface + */ + protected $renderer; + + /** + * Constructs an PreviewController instance. + * + * @param \Drupal\Core\Render\RendererInterface $renderer + * The renderer service. + */ + public function __construct(RendererInterface $renderer) { + $this->renderer = $renderer; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('renderer') + ); + } + + /** + * Returns a HTML response containing a preview of the text after filtering. + * + * Applies all of the given text format's filters, not just the `entity_embed` + * filter, because for example `filter_align` and `filter_caption` may apply + * to it as well. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request object. + * @param \Drupal\filter\FilterFormatInterface $filter_format + * The text format. + * + * @return \Symfony\Component\HttpFoundation\Response + * The filtered text. + * + * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException + * Throws an exception if 'text' parameter is not found in the request. + * + * @see \Drupal\editor\EditorController::getUntransformedText + */ + public function preview(Request $request, FilterFormatInterface $filter_format) { + $text = $request->get('text'); + if ($text == '') { + throw new NotFoundHttpException(); + } + + $build = [ + '#type' => 'processed_text', + '#text' => $text, + '#format' => $filter_format->id(), + ]; + $html = $this->renderer->renderPlain($build); + + // Note that we intentionally do not use: + // - \Drupal\Core\Cache\CacheableResponse because caching it on the server + // side is wasteful, hence there is no need for cacheability metadata. + // - \Drupal\Core\Render\HtmlResponse because there is no need for + // attachments nor cacheability metadata. + return (new Response($html)) + // Do not allow any intermediary to cache the response, only the end user. + ->setPrivate() + // Allow the end user to cache it for up to 5 minutes. + ->setMaxAge(300); + } + +} diff --git a/web/modules/entity_embed/src/Entity/EntityEmbedFakeEntity.php b/web/modules/entity_embed/src/Entity/EntityEmbedFakeEntity.php new file mode 100644 index 0000000000..34241cc61d --- /dev/null +++ b/web/modules/entity_embed/src/Entity/EntityEmbedFakeEntity.php @@ -0,0 +1,19 @@ +<?php + +namespace Drupal\entity_embed\Entity; + +use Drupal\Core\Entity\ContentEntityBase; + +/** + * Fake entity type. + * + * @ContentEntityType( + * id = "entity_embed_fake_entity", + * label = @Translation("Fake entity type"), + * handlers = { + * "storage" = "Drupal\Core\Entity\ContentEntityNullStorage", + * }, + * ) + */ +class EntityEmbedFakeEntity extends ContentEntityBase { +} diff --git a/web/modules/entity_embed/src/EntityEmbedBuilder.php b/web/modules/entity_embed/src/EntityEmbedBuilder.php index 24e716afbb..892ec3acb5 100644 --- a/web/modules/entity_embed/src/EntityEmbedBuilder.php +++ b/web/modules/entity_embed/src/EntityEmbedBuilder.php @@ -34,6 +34,7 @@ class EntityEmbedBuilder implements EntityEmbedBuilderInterface { * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * The module handler service. * @param \Drupal\entity_embed\EntityEmbedDisplay\EntityEmbedDisplayManager $display_manager + * The entity embed display plugin manager. */ public function __construct(ModuleHandlerInterface $module_handler, EntityEmbedDisplayManager $display_manager) { $this->moduleHandler = $module_handler; @@ -58,6 +59,18 @@ public function buildEntityEmbed(EntityInterface $entity, array $context = []) { 'data-entity-embed-display-settings' => [], ]; + // If the data-entity-embed-display-settings isn't an array reset it, + // otherwise we'll encounter a fatal error when calling + // $this->buildEntityEmbedDisplayPlugin() further down the line. + if (!is_array($context['data-entity-embed-display-settings'])) { + \Drupal::logger('entity_embed')->warning('Invalid display settings encountered. Could not process following settings for entity type "@entity_type" with the uuid "@uuid": @settings', [ + '@settings' => $context['data-entity-embed-display-settings'], + '@entity_type' => $entity->getEntityTypeId(), + '@uuid' => $entity->uuid(), + ]); + $context['data-entity-embed-display-settings'] = []; + } + // The default Entity Embed Display plugin has been deprecated by the // rendered entity field formatter. if ($context['data-entity-embed-display'] === 'default') { @@ -76,7 +89,6 @@ public function buildEntityEmbed(EntityInterface $entity, array $context = []) { // alter the result before rendering. $build = [ '#theme_wrappers' => ['entity_embed_container'], - '#attributes' => ['class' => ['embedded-entity']], '#entity' => $entity, '#context' => $context, ]; @@ -86,18 +98,25 @@ public function buildEntityEmbed(EntityInterface $entity, array $context = []) { $context['data-entity-embed-display-settings'], $context ); - - // Maintain data-align if it is there. - if (isset($context['data-align'])) { - $build['#attributes']['data-align'] = $context['data-align']; + // Don't ever cache a representation of an embedded entity, since the host + // entity may be overriding specific values (such as an `alt` attribute) + // which means that this particular rendered representation is unique to + // the host entity, and hence nonsensical to cache separately anyway. + unset($build['entity']['#cache']['keys']); + + if (isset($context['class'])) { + if (is_string($context['class'])) { + $context['class'] = explode(' ', $context['class']); + } } - elseif ((isset($context['class']))) { - $build['#attributes']['class'][] = $context['class']; + else { + $context['class'] = []; } + $context['class'][] = 'embedded-entity'; - // Maintain data-caption if it is there. - if (isset($context['data-caption'])) { - $build['#attributes']['data-caption'] = $context['data-caption']; + // Maintain data- attributes. + if (isset($context)) { + $build['#attributes'] = $context; } // Make sure that access to the entity is respected. diff --git a/web/modules/entity_embed/src/EntityEmbedDisplay/EntityEmbedDisplayBase.php b/web/modules/entity_embed/src/EntityEmbedDisplay/EntityEmbedDisplayBase.php index ca0a41277c..3cb86cdbac 100644 --- a/web/modules/entity_embed/src/EntityEmbedDisplay/EntityEmbedDisplayBase.php +++ b/web/modules/entity_embed/src/EntityEmbedDisplay/EntityEmbedDisplayBase.php @@ -35,7 +35,7 @@ abstract class EntityEmbedDisplayBase extends PluginBase implements ContainerFac /** * The language manager. * - * @var \Drupal\Core\Language\LanguageManagerInterface $language_manager + * @var \Drupal\Core\Language\LanguageManagerInterface */ protected $languageManager; @@ -44,18 +44,24 @@ abstract class EntityEmbedDisplayBase extends PluginBase implements ContainerFac * * @var array */ - public $context = array(); + public $context = []; /** * The attributes on the embedded entity. * * @var array */ - public $attributes = array(); + public $attributes = []; /** - * {@inheritdoc} + * Constructs an EntityEmbedDisplayBase 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\Entity\EntityTypeManagerInterface $entity_type_manager * The entity type manager service. * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager @@ -94,8 +100,7 @@ public function access(AccountInterface $account = NULL) { } /** - * Validates that this Entity Embed Display plugin applies to the current - * entity type. + * Validates that this display plugin applies to the current entity type. * * This checks the plugin annotation's 'entity_types' value, which should be * an array of entity types that this plugin can process, or FALSE if the @@ -131,14 +136,14 @@ abstract public function build(); * {@inheritdoc} */ public function calculateDependencies() { - return array(); + return []; } /** * {@inheritdoc} */ public function defaultConfiguration() { - return array(); + return []; } /** @@ -232,7 +237,7 @@ public function getContextValues() { * The currently set context value. */ public function getContextValue($name) { - return $this->context[$name]; + return !empty($this->context[$name]) ? $this->context[$name] : NULL; } /** @@ -266,7 +271,7 @@ public function getEntityTypeFromContext() { /** * Gets the entity from the current context. * - * @todo Where doe sthis come from? The value must come from somewhere, yet + * @todo Where does this come from? The value must come from somewhere, yet * this does not implement any context-related interfaces. This is an *input*, * so we need cache contexts and possibly cache tags to reflect where this * came from. We need that for *everything* that this class does that relies @@ -274,6 +279,7 @@ public function getEntityTypeFromContext() { * global that breaks cacheability metadata. * * @return \Drupal\Core\Entity\EntityInterface + * The entity from the current context. */ public function getEntityFromContext() { if ($this->hasContextValue('entity')) { @@ -317,10 +323,25 @@ public function getAttributeValue($name, $default = NULL) { return array_key_exists($name, $attributes) ? $attributes[$name] : $default; } + /** + * Checks if an attribute is set. + * + * @param string $name + * The name of the attribute. + * + * @return bool + * Returns TRUE if value is set. + */ + public function hasAttribute($name) { + return array_key_exists($name, $this->getAttributeValues()); + } + /** * Gets the current language code. * * @return string + * The langcode present in the 'data-langcode', if present, or the current + * langcode from the language manager, otherwise. */ public function getLangcode() { $langcode = $this->getAttributeValue('data-langcode'); diff --git a/web/modules/entity_embed/src/EntityEmbedDisplay/EntityEmbedDisplayInterface.php b/web/modules/entity_embed/src/EntityEmbedDisplay/EntityEmbedDisplayInterface.php index 3d97fac2cd..f35f28b0f3 100644 --- a/web/modules/entity_embed/src/EntityEmbedDisplay/EntityEmbedDisplayInterface.php +++ b/web/modules/entity_embed/src/EntityEmbedDisplay/EntityEmbedDisplayInterface.php @@ -3,9 +3,10 @@ namespace Drupal\entity_embed\EntityEmbedDisplay; use Drupal\Component\Plugin\PluginInspectionInterface; -use Drupal\Component\Plugin\ConfigurablePluginInterface; use Drupal\Core\Plugin\PluginFormInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\Component\Plugin\ConfigurableInterface; +use Drupal\Component\Plugin\DependentPluginInterface; /** * Defines the interface for Entity Embed Display plugins. @@ -41,7 +42,7 @@ * * @ingroup entity_embed_api */ -interface EntityEmbedDisplayInterface extends ConfigurablePluginInterface, PluginFormInterface, PluginInspectionInterface { +interface EntityEmbedDisplayInterface extends ConfigurableInterface, DependentPluginInterface, PluginFormInterface, PluginInspectionInterface { /** * Indicates whether this Entity Embed display can be used. diff --git a/web/modules/entity_embed/src/EntityEmbedDisplay/EntityEmbedDisplayManager.php b/web/modules/entity_embed/src/EntityEmbedDisplay/EntityEmbedDisplayManager.php index f2d9d69b3f..67531e2399 100644 --- a/web/modules/entity_embed/src/EntityEmbedDisplay/EntityEmbedDisplayManager.php +++ b/web/modules/entity_embed/src/EntityEmbedDisplay/EntityEmbedDisplayManager.php @@ -7,6 +7,7 @@ use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Plugin\DefaultPluginManager; use Drupal\Component\Plugin\Exception\PluginException; +use Drupal\entity_embed\Plugin\entity_embed\EntityEmbedDisplay\MediaImageDecorator; /** * Provides an Entity Embed display plugin manager. @@ -32,20 +33,8 @@ class EntityEmbedDisplayManager extends DefaultPluginManager { public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) { parent::__construct('Plugin/entity_embed/EntityEmbedDisplay', $namespaces, $module_handler, 'Drupal\entity_embed\EntityEmbedDisplay\EntityEmbedDisplayInterface', 'Drupal\entity_embed\Annotation\EntityEmbedDisplay'); $this->alterInfo('entity_embed_display_plugins'); - $this->setCacheBackend($cache_backend, 'entity_embed_display_plugins'); - } - - /** - * Overrides DefaultPluginManager::processDefinition(). - */ - public function processDefinition(&$definition, $plugin_id) { - $definition += array( - 'entity_types' => FALSE, - ); - - if ($definition['entity_types'] !== FALSE && !is_array($definition['entity_types'])) { - $definition['entity_types'] = array($definition['entity_types']); - } + // @todo Move the cache tag to the derivers once https://www.drupal.org/node/3001284 lands. + $this->setCacheBackend($cache_backend, 'entity_embed_display_plugins', ['config:entity_view_mode_list']); } /** @@ -61,8 +50,22 @@ public function processDefinition(&$definition, $plugin_id) { * * @see https://drupal.org/node/2277981 */ - public function getDefinitionsForContexts(array $contexts = array()) { + public function getDefinitionsForContexts(array $contexts = []) { $definitions = $this->getDefinitions(); + + if (!empty($contexts['embed_button'])) { + $button_plugins = $contexts['embed_button']->getTypeSetting('display_plugins'); + if (!empty($button_plugins)) { + $allowed_definitions = []; + foreach ($button_plugins as $plugin_id) { + if (!empty($definitions[$plugin_id])) { + $allowed_definitions[$plugin_id] = $definitions[$plugin_id]; + } + } + $definitions = $allowed_definitions; + } + } + $valid_ids = array_filter(array_keys($definitions), function ($id) use ($contexts) { try { $display = $this->createInstance($id); @@ -82,6 +85,35 @@ public function getDefinitionsForContexts(array $contexts = array()) { return $definitions_for_context; } + /** + * Gets definition options for context. + * + * Provides a list of plugins that can be used for a certain context and + * filters out plugins that should be hidden in the UI. + * + * @param array $context + * An array of context options; possible keys are 'entity', 'entity_type' + * and 'embed_button'. + * + * @return string[] + * An array of valid plugin labels, keyed by plugin ID. + */ + public function getDefinitionOptionsForContext(array $context) { + $values = [ + 'entity' => TRUE, + 'entity_type' => TRUE, + 'embed_button' => TRUE, + ]; + assert(empty(array_diff_key($context, $values))); + $definitions = $this->getDefinitionsForContexts($context); + $definitions = $this->filterExposedDefinitions($definitions); + $options = array_map(function ($definition) { + return (string) $definition['label']; + }, $definitions); + natsort($options); + return $options; + } + /** * Gets definition options for entity. * @@ -95,7 +127,7 @@ public function getDefinitionsForContexts(array $contexts = array()) { * An array of valid plugin labels, keyed by plugin ID. */ public function getDefinitionOptionsForEntity(EntityInterface $entity) { - $definitions = $this->getDefinitionsForContexts(array('entity' => $entity, 'entity_type' => $entity->getEntityTypeId())); + $definitions = $this->getDefinitionsForContexts(['entity' => $entity, 'entity_type' => $entity->getEntityTypeId()]); $definitions = $this->filterExposedDefinitions($definitions); return array_map(function ($definition) { return (string) $definition['label']; @@ -112,7 +144,7 @@ public function getDefinitionOptionsForEntity(EntityInterface $entity) { * Returns plugin definitions that should be displayed in the UI. */ protected function filterExposedDefinitions(array $definitions) { - return array_filter($definitions, function($definition) { + return array_filter($definitions, function ($definition) { return empty($definition['no_ui']); }); } @@ -130,11 +162,28 @@ protected function filterExposedDefinitions(array $definitions) { * An array of valid plugin labels, keyed by plugin ID. */ public function getDefinitionOptionsForEntityType($entity_type) { - $definitions = $this->getDefinitionsForContexts(array('entity_type' => $entity_type)); + $definitions = $this->getDefinitionsForContexts(['entity_type' => $entity_type]); $definitions = $this->filterExposedDefinitions($definitions); return array_map(function ($definition) { return (string) $definition['label']; }, $definitions); } + /** + * {@inheritdoc} + */ + public function createInstance($plugin_id, array $configuration = []) { + $instance = parent::createInstance($plugin_id, $configuration); + $definition = $instance->getPluginDefinition(); + + if (empty($definition['supports_image_alt_and_title'])) { + return $instance; + } + else { + // Use decorator pattern to add alt and title fields to dialog when + // embedding media with image source. + return new MediaImageDecorator($instance); + } + } + } diff --git a/web/modules/entity_embed/src/EntityEmbedDisplay/FieldFormatterEntityEmbedDisplayBase.php b/web/modules/entity_embed/src/EntityEmbedDisplay/FieldFormatterEntityEmbedDisplayBase.php index fd80bf8544..038bba2435 100644 --- a/web/modules/entity_embed/src/EntityEmbedDisplay/FieldFormatterEntityEmbedDisplayBase.php +++ b/web/modules/entity_embed/src/EntityEmbedDisplay/FieldFormatterEntityEmbedDisplayBase.php @@ -11,11 +11,11 @@ use Drupal\Core\Plugin\PluginDependencyTrait; use Drupal\Core\Session\AccountInterface; use Drupal\Core\TypedData\TypedDataManager; -use Drupal\node\Entity\Node; +use Drupal\entity_embed\Entity\EntityEmbedFakeEntity; use Symfony\Component\DependencyInjection\ContainerInterface; /** - * + * Base class for field formatter display plugins. */ abstract class FieldFormatterEntityEmbedDisplayBase extends EntityEmbedDisplayBase { use PluginDependencyTrait; @@ -51,6 +51,12 @@ abstract class FieldFormatterEntityEmbedDisplayBase extends EntityEmbedDisplayBa /** * Constructs a FieldFormatterEntityEmbedDisplayBase 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\Entity\EntityTypeManagerInterface $entity_type_manager * The entity type manager service. * @param \Drupal\Core\Field\FormatterPluginManager $formatter_plugin_manager @@ -142,9 +148,9 @@ public function getFieldFormatterId() { * {@inheritdoc} */ public function build() { - // Create a temporary node object to which our fake field value can be + // Create a temporary entity to which our fake field value can be // added. - $node = Node::create(array('type' => '_entity_embed')); + $fakeEntity = EntityEmbedFakeEntity::create(['type' => '_entity_embed']); $definition = $this->getFieldDefinition(); @@ -156,12 +162,12 @@ public function build() { $definition, $this->getFieldValue($definition), $definition->getName(), - $node->getTypedData() + $fakeEntity->getTypedData() ); // Prepare, expects an array of items, keyed by parent entity ID. $formatter = $this->getFieldFormatter(); - $formatter->prepareView(array($node->id() => $items)); + $formatter->prepareView([$fakeEntity->id() => $items]); $build = $formatter->viewElements($items, $this->getLangcode()); // For some reason $build[0]['#printed'] is TRUE, which means it will fail // to render later. So for now we manually fix that. @@ -192,20 +198,20 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta */ public function getFieldFormatter() { if (!isset($this->fieldFormatter)) { - $display = array( + $display = [ 'type' => $this->getFieldFormatterId(), 'settings' => $this->getConfiguration(), 'label' => 'hidden', - ); + ]; // Create the formatter plugin. Will use the default formatter for that // field type if none is passed. $this->fieldFormatter = $this->formatterPluginManager->getInstance( - array( + [ 'field_definition' => $this->getFieldDefinition(), 'view_mode' => '_entity_embed', 'configuration' => $display, - ) + ] ); } diff --git a/web/modules/entity_embed/src/Form/EntityEmbedDialog.php b/web/modules/entity_embed/src/Form/EntityEmbedDialog.php index fe16bd97c8..cc9daaf6eb 100644 --- a/web/modules/entity_embed/src/Form/EntityEmbedDialog.php +++ b/web/modules/entity_embed/src/Form/EntityEmbedDialog.php @@ -3,15 +3,15 @@ namespace Drupal\entity_embed\Form; use Drupal\Component\Utility\Html; -use Drupal\Component\Utility\Unicode; use Drupal\Core\Ajax\AjaxResponse; use Drupal\Core\Ajax\CloseModalDialogCommand; use Drupal\Core\Ajax\HtmlCommand; use Drupal\Core\Ajax\SetDialogTitleCommand; -use Drupal\Core\Entity\Element\EntityAutocomplete; use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Entity\FieldableEntityInterface; +use Drupal\Core\Entity\TranslatableInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormBuilderInterface; @@ -82,6 +82,8 @@ class EntityEmbedDialog extends FormBase { /** * The entity browser settings from the entity embed button. + * + * @var array */ protected $entityBrowserSettings = []; @@ -120,7 +122,8 @@ public static function create(ContainerInterface $container) { $container->get('entity_type.manager'), $container->get('event_dispatcher'), $container->get('entity_field.manager'), - $container->get('module_handler') + $container->get('module_handler'), + $container->get('language_manager') ); } @@ -132,8 +135,35 @@ public function getFormId() { } /** - * {@inheritdoc} + * Loads an entity (in the appropriate translation) given HTML attributes. * + * @param string[] $attributes + * An array of HTML attributes, including at least `data-entity-type` and + * `data-entity-uuid`, and optionally `data-langcode`. + * + * @return \Drupal\Core\Entity\EntityInterface|null + * The requested entity, or NULL. + */ + protected function loadEntityByAttributes(array $attributes) { + $entity = $this->entityTypeManager->getStorage($attributes['data-entity-type']) + ->loadByProperties(['uuid' => $attributes['data-entity-uuid']]); + $entity = current($entity); + if ($entity && $entity instanceof TranslatableInterface && !empty($attributes['data-langcode'])) { + if ($entity->hasTranslation($attributes['data-langcode'])) { + $entity = $entity->getTranslation($attributes['data-langcode']); + } + } + + return $entity; + } + + /** + * Form constructor. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. * @param \Drupal\editor\EditorInterface $editor * The editor to which this dialog corresponds. * @param \Drupal\embed\EmbedButtonInterface $embed_button @@ -147,12 +177,12 @@ public function buildForm(array $form, FormStateInterface $form_state, EditorInt $form_state->set('embed_button', $embed_button); $form_state->set('editor', $editor); // Initialize entity element with form attributes, if present. - $entity_element = empty($values['attributes']) ? array() : $values['attributes']; - $entity_element += empty($input['attributes']) ? array() : $input['attributes']; + $entity_element = empty($values['attributes']) ? [] : $values['attributes']; + $entity_element += empty($input['attributes']) ? [] : $input['attributes']; // The default values are set directly from \Drupal::request()->request, // provided by the editor plugin opening the dialog. if (!$form_state->get('entity_element')) { - $form_state->set('entity_element', isset($input['editor_object']) ? $input['editor_object'] : array()); + $form_state->set('entity_element', isset($input['editor_object']) ? $input['editor_object'] : []); } $entity_element += $form_state->get('entity_element'); $entity_element += [ @@ -162,9 +192,8 @@ public function buildForm(array $form, FormStateInterface $form_state, EditorInt 'data-entity-embed-display-settings' => isset($form_state->get('entity_element')['data-entity-embed-settings']) ? $form_state->get('entity_element')['data-entity-embed-settings'] : [], ]; $form_state->set('entity_element', $entity_element); - $entity = $this->entityTypeManager->getStorage($entity_element['data-entity-type']) - ->loadByProperties(['uuid' => $entity_element['data-entity-uuid']]); - $form_state->set('entity', current($entity) ?: NULL); + $entity = $this->loadEntityByAttributes($entity_element); + $form_state->set('entity', $entity ?: NULL); if (!$form_state->get('step')) { // If an entity has been selected, then always skip to the embed options. @@ -209,30 +238,31 @@ public function buildForm(array $form, FormStateInterface $form_state, EditorInt * @return array * The form structure. */ - public function buildSelectStep(array &$form, FormStateInterface $form_state) { - // Entity element is calculated on every AJAX request/submit. See ::buildForm(). + public function buildSelectStep(array $form, FormStateInterface $form_state) { + // Entity element is calculated on every AJAX request/submit. + // See self::buildForm(). $entity_element = $form_state->get('entity_element'); /** @var \Drupal\embed\EmbedButtonInterface $embed_button */ $embed_button = $form_state->get('embed_button'); $entity = $form_state->get('entity'); - $form['attributes']['data-entity-type'] = array( + $form['attributes']['data-entity-type'] = [ '#type' => 'value', '#value' => $entity_element['data-entity-type'], - ); + ]; $label = $this->t('Label'); // Attempt to display a better label if we can by getting it from // the label field definition. $entity_type = $this->entityTypeManager->getDefinition($entity_element['data-entity-type']); - if ($entity_type->isSubclassOf('\Drupal\Core\Entity\FieldableEntityInterface') && $entity_type->hasKey('label')) { + if ($entity_type->entityClassImplements(FieldableEntityInterface::class) && $entity_type->hasKey('label')) { $field_definitions = $this->entityFieldManager->getBaseFieldDefinitions($entity_type->id()); if (isset($field_definitions[$entity_type->getKey('label')])) { $label = $field_definitions[$entity_type->getKey('label')]->getLabel(); } } - $form['#title'] = $this->t('Select @type to embed', array('@type' => $entity_type->getLowercaseLabel())); + $form['#title'] = $this->t('Select @type to embed', ['@type' => $entity_type->getSingularLabel()]); if ($this->entityBrowser) { $this->eventDispatcher->addListener(Events::REGISTER_JS_CALLBACKS, [$this, 'registerJSCallback']); @@ -246,43 +276,51 @@ public function buildSelectStep(array &$form, FormStateInterface $form_state) { ]; } else { - $form['entity_id'] = array( + $form['entity_id'] = [ '#type' => 'entity_autocomplete', '#target_type' => $entity_element['data-entity-type'], '#title' => $label, '#default_value' => $entity, '#required' => TRUE, '#description' => $this->t('Type label and pick the right one from suggestions. Note that the unique ID will be saved.'), - ); + '#maxlength' => 255, + ]; if ($bundles = $embed_button->getTypeSetting('bundles')) { $form['entity_id']['#selection_settings']['target_bundles'] = $bundles; } } - $form['attributes']['data-entity-uuid'] = array( + if (!empty($entity_element['data-langcode'])) { + $form['attributes']['data-langcode'] = [ + '#type' => 'hidden', + '#value' => $entity_element['data-langcode'], + ]; + } + + $form['attributes']['data-entity-uuid'] = [ '#type' => 'value', '#title' => $entity_element['data-entity-uuid'], - ); - $form['actions'] = array( + ]; + $form['actions'] = [ '#type' => 'actions', - ); + ]; - $form['actions']['save_modal'] = array( + $form['actions']['save_modal'] = [ '#type' => 'submit', '#value' => $this->t('Next'), '#button_type' => 'primary', // No regular submit-handler. This form only works via JavaScript. - '#submit' => array(), - '#ajax' => array( + '#submit' => [], + '#ajax' => [ 'callback' => '::submitSelectStep', 'event' => 'click', - ), + ], '#attributes' => [ 'class' => [ 'js-button-next', ], ], - ); + ]; return $form; } @@ -298,47 +336,47 @@ public function buildSelectStep(array &$form, FormStateInterface $form_state) { * @return array * The form structure. */ - public function buildReviewStep(array &$form, FormStateInterface $form_state) { + public function buildReviewStep(array $form, FormStateInterface $form_state) { /** @var \Drupal\Core\Entity\EntityInterface $entity */ $entity = $form_state->get('entity'); - $form['#title'] = $this->t('Review selected @type', array('@type' => $entity->getEntityType()->getLowercaseLabel())); + $form['#title'] = $this->t('Review selected @type', ['@type' => $entity->getEntityType()->getSingularLabel()]); $form['selection'] = [ '#markup' => $entity->label(), ]; - $form['actions'] = array( + $form['actions'] = [ '#type' => 'actions', - ); + ]; - $form['actions']['back'] = array( + $form['actions']['back'] = [ '#type' => 'submit', '#value' => $this->t('Replace selection'), // No regular submit-handler. This form only works via JavaScript. - '#submit' => array(), - '#ajax' => array( + '#submit' => [], + '#ajax' => [ 'callback' => '::submitAndShowSelect', 'event' => 'click', - ), - ); + ], + ]; - $form['actions']['save_modal'] = array( + $form['actions']['save_modal'] = [ '#type' => 'submit', '#value' => $this->t('Next'), '#button_type' => 'primary', // No regular submit-handler. This form only works via JavaScript. - '#submit' => array(), - '#ajax' => array( + '#submit' => [], + '#ajax' => [ 'callback' => '::submitAndShowEmbed', 'event' => 'click', - ), + ], '#attributes' => [ 'class' => [ 'js-button-next', ], ], - ); + ]; return $form; } @@ -355,7 +393,8 @@ public function buildReviewStep(array &$form, FormStateInterface $form_state) { * The form structure. */ public function buildEmbedStep(array $form, FormStateInterface $form_state) { - // Entity element is calculated on every AJAX request/submit. See ::buildForm(). + // Entity element is calculated on every AJAX request/submit. + // See self::buildForm(). $entity_element = $form_state->get('entity_element'); /** @var \Drupal\embed\EmbedButtonInterface $embed_button */ $embed_button = $form_state->get('embed_button'); @@ -365,31 +404,48 @@ public function buildEmbedStep(array $form, FormStateInterface $form_state) { $entity = $form_state->get('entity'); $values = $form_state->getValues(); - $form['#title'] = $this->t('Embed @type', array('@type' => $entity->getEntityType()->getLowercaseLabel())); + $form['#title'] = $this->t('Embed @type', ['@type' => $entity->getEntityType()->getSingularLabel()]); - $entity_label = ''; try { - $entity_label = $entity->link(); + if ($entity->getEntityType()->hasLinkTemplate('canonical')) { + $options = [ + 'attributes' => [ + 'target' => '_blank', + ], + ]; + $entity_label = $entity->toLink($entity->label(), 'canonical', $options)->toString(); + } + elseif ($entity->getEntityTypeId() == 'file') { + $entity_label = '<a href="' . file_create_url($entity->getFileUri()) . '" target="_blank">' . $entity->label() . '</a>'; + } + else { + $entity_label = '<a href="' . $entity->toUrl()->toString() . '" target="_blank">' . $entity->label() . '</a>'; + } } catch (\Exception $e) { - // Construct markup of the link to the entity manually if link() fails. - // @see https://www.drupal.org/node/2402533 - $entity_label = '<a href="' . $entity->url() . '">' . $entity->label() . '</a>'; + $entity_label = $entity->label(); } - $form['entity'] = array( + $form['entity'] = [ '#type' => 'item', '#title' => $this->t('Selected entity'), '#markup' => $entity_label, - ); - $form['attributes']['data-entity-type'] = array( + ]; + $form['attributes']['data-entity-type'] = [ '#type' => 'hidden', '#value' => $entity_element['data-entity-type'], - ); - $form['attributes']['data-entity-uuid'] = array( + ]; + $form['attributes']['data-entity-uuid'] = [ '#type' => 'hidden', '#value' => $entity_element['data-entity-uuid'], - ); + ]; + + if (!empty($entity_element['data-langcode'])) { + $form['attributes']['data-langcode'] = [ + '#type' => 'hidden', + '#value' => $entity_element['data-langcode'], + ]; + } // Build the list of allowed Entity Embed Display plugins. $display_plugin_options = $this->getDisplayPluginOptions($embed_button, $entity); @@ -407,55 +463,37 @@ public function buildEmbedStep(array $form, FormStateInterface $form_state) { $entity_element['data-entity-embed-display'] = 'entity_reference:entity_reference_entity_view'; } - $form['attributes']['data-entity-embed-display'] = array( + $form['attributes']['data-entity-embed-display'] = [ '#type' => 'select', '#title' => $this->t('Display as'), '#options' => $display_plugin_options, '#default_value' => $entity_element['data-entity-embed-display'], '#required' => TRUE, - '#ajax' => array( + '#ajax' => [ 'callback' => '::updatePluginConfigurationForm', 'wrapper' => 'data-entity-embed-display-settings-wrapper', 'effect' => 'fade', - ), + ], // Hide the selection if only one option is available. '#access' => count($display_plugin_options) > 1, - ); - $form['attributes']['data-entity-embed-display-settings'] = array( + ]; + $form['attributes']['data-entity-embed-display-settings'] = [ '#type' => 'container', '#prefix' => '<div id="data-entity-embed-display-settings-wrapper">', '#suffix' => '</div>', - ); - $form['attributes']['data-embed-button'] = array( + ]; + $form['attributes']['data-embed-button'] = [ '#type' => 'value', '#value' => $embed_button->id(), - ); + ]; $plugin_id = !empty($values['attributes']['data-entity-embed-display']) ? $values['attributes']['data-entity-embed-display'] : $entity_element['data-entity-embed-display']; if (!empty($plugin_id)) { - if (is_string($entity_element['data-entity-embed-display-settings'])) { - $entity_element['data-entity-embed-display-settings'] = Json::decode($entity_element['data-entity-embed-display-settings']); + if (empty($entity_element['data-entity-embed-display-settings'])) { + $entity_element['data-entity-embed-display-settings'] = []; } - - // Supress Drupal's "Link image to" dropdown when embedding an image, - // since the 'Link to' option provides this functionality. - if (isset($form['attributes']['data-entity-embed-display-settings']['image_link'])) { - $form['attributes']['data-entity-embed-display-settings']['image_link']['#type'] = 'hidden'; - $form['attributes']['data-entity-embed-display-settings']['image_link']['#value'] = ''; + elseif (is_string($entity_element['data-entity-embed-display-settings'])) { + $entity_element['data-entity-embed-display-settings'] = Json::decode($entity_element['data-entity-embed-display-settings']); } - $form['attributes']['data-entity-embed-display-settings']['link_url'] = [ - '#title' => t('Link to'), - '#type' => 'entity_autocomplete', - '#target_type' => 'node', - '#attributes' => [ - 'data-autocomplete-first-character-blacklist' => '/#?' - ], - '#element_validate' => [[get_called_class(), 'validateUriElement']], - '#process_default_value' => FALSE, - '#description' => $this->t('Start typing the title of a piece of content to select it. You can also enter an internal path such as %add-node or an external URL such as %url. Enter %front to link to the front page.', ['%front' => '<front>', '%add-node' => '/node/add', '%url' => 'http://example.com']), - '#default_value' => isset($entity_element['data-entity-embed-display-settings']['link_url']) ? $this->getUriAsDisplayableString($entity_element['data-entity-embed-display-settings']['link_url']) : '', - '#maxlength' => 2048, - ]; - $display = $this->entityEmbedDisplayManager->createInstance($plugin_id, $entity_element['data-entity-embed-display-settings']); $display->setContextValue('entity', $entity); $display->setAttributes($entity_element); @@ -465,175 +503,64 @@ public function buildEmbedStep(array $form, FormStateInterface $form_state) { // When Drupal core's filter_align is being used, the text editor may // offer the ability to change the alignment. if ($editor->getFilterFormat()->filters('filter_align')->status) { - $form['attributes']['data-align'] = array( + $form['attributes']['data-align'] = [ '#title' => $this->t('Align'), '#type' => 'radios', - '#options' => array( + '#options' => [ '' => $this->t('None'), 'left' => $this->t('Left'), 'center' => $this->t('Center'), 'right' => $this->t('Right'), - ), + ], '#default_value' => isset($entity_element['data-align']) ? $entity_element['data-align'] : '', - '#wrapper_attributes' => array('class' => array('container-inline')), - '#attributes' => array('class' => array('container-inline')), - ); + '#wrapper_attributes' => ['class' => ['container-inline']], + '#attributes' => ['class' => ['container-inline']], + ]; } // When Drupal core's filter_caption is being used, the text editor may // offer the ability to add a caption. if ($editor->getFilterFormat()->filters('filter_caption')->status) { - $form['attributes']['data-caption'] = array( + $form['attributes']['data-caption'] = [ '#title' => $this->t('Caption'), - '#type' => 'textfield', + '#type' => 'textarea', + '#rows' => 3, '#default_value' => isset($entity_element['data-caption']) ? Html::decodeEntities($entity_element['data-caption']) : '', - '#element_validate' => array('::escapeValue'), - ); + '#element_validate' => ['::escapeValue'], + ]; } - $form['actions'] = array( + $form['actions'] = [ '#type' => 'actions', - ); - $form['actions']['back'] = array( + ]; + $form['actions']['back'] = [ '#type' => 'submit', '#value' => $this->t('Back'), // No regular submit-handler. This form only works via JavaScript. - '#submit' => array(), - '#ajax' => array( + '#submit' => [], + '#ajax' => [ 'callback' => !empty($this->entityBrowserSettings['display_review']) ? '::submitAndShowReview' : '::submitAndShowSelect', 'event' => 'click', - ), - ); - $form['actions']['save_modal'] = array( + ], + ]; + $form['actions']['save_modal'] = [ '#type' => 'submit', '#value' => $this->t('Embed'), '#button_type' => 'primary', // No regular submit-handler. This form only works via JavaScript. - '#submit' => array(), - '#ajax' => array( + '#submit' => [], + '#ajax' => [ 'callback' => '::submitEmbedStep', 'event' => 'click', - ), - ); + ], + ]; return $form; } /** - * Gets the URI without the 'internal:' or 'entity:' scheme. - * - * The following two forms of URIs are transformed: - * - 'entity:' URIs: to entity autocomplete ("label (entity id)") strings; - * - 'internal:' URIs: the scheme is stripped. - * - * This method is the inverse of ::getUserEnteredStringAsUri(). - * - * @param string $uri - * The URI to get the displayable string for. - * - * @return string - * - * @see static::getUserEnteredStringAsUri() - */ - protected function getUriAsDisplayableString($uri) { - $uri = Html::decodeEntities($uri); - $scheme = parse_url($uri, PHP_URL_SCHEME); - - // By default, the displayable string is the URI. - $displayable_string = $uri; - - // A different displayable string may be chosen in case of the 'internal:' - // or 'entity:' built-in schemes. - if ($scheme === 'internal') { - $uri_reference = explode(':', $uri, 2)[1]; - - // @todo '<front>' is valid input for BC reasons, may be removed by - // https://www.drupal.org/node/2421941 - $path = parse_url($uri, PHP_URL_PATH); - if ($path === '/') { - $uri_reference = '<front>' . substr($uri_reference, 1); - } - - $displayable_string = $uri_reference; - } - elseif ($scheme === 'entity') { - list($entity_type, $entity_id) = explode('/', substr($uri, 7), 2); - // Show the 'entity:' URI as the entity autocomplete would. - // @todo Support entity types other than 'node'. Will be fixed in - // https://www.drupal.org/node/2423093. - if ($entity_type == 'node' && $entity = \Drupal::entityTypeManager()->getStorage($entity_type)->load($entity_id)) { - $displayable_string = EntityAutocomplete::getEntityLabels([$entity]); - } - } - - return $displayable_string; - } - - /** - * Gets the user-entered string as a URI. - * - * The following two forms of input are mapped to URIs: - * - entity autocomplete ("label (entity id)") strings: to 'entity:' URIs; - * - strings without a detectable scheme: to 'internal:' URIs. - * - * This method is the inverse of ::getUriAsDisplayableString(). - * - * @param string $string - * The user-entered string. - * - * @return string - * The URI, if a non-empty $uri was passed. - * - * @see static::getUriAsDisplayableString() - */ - protected static function getUserEnteredStringAsUri($string) { - // By default, assume the entered string is an URI. - $uri = $string; - - // Detect entity autocomplete string, map to 'entity:' URI. - $entity_id = EntityAutocomplete::extractEntityIdFromAutocompleteInput($string); - if ($entity_id !== NULL) { - // @todo Support entity types other than 'node'. Will be fixed in - // https://www.drupal.org/node/2423093. - $uri = 'entity:node/' . $entity_id; - } - // Detect a schemeless string, map to 'internal:' URI. - elseif (!empty($string) && parse_url($string, PHP_URL_SCHEME) === NULL) { - // @todo '<front>' is valid input for BC reasons, may be removed by - // https://www.drupal.org/node/2421941 - // - '<front>' -> '/' - // - '<front>#foo' -> '/#foo' - if (strpos($string, '<front>') === 0) { - $string = '/' . substr($string, strlen('<front>')); - } - $uri = 'internal:' . $string; - } - - return $uri; - } - - /** - * Form element validation handler for the 'uri' element. - * - * Disallows saving inaccessible or untrusted URLs. + * {@inheritdoc} */ - public static function validateUriElement($element, FormStateInterface $form_state, $form) { - $uri = static::getUserEnteredStringAsUri($element['#value']); - $form_state->setValueForElement($element, $uri); - - // If getUserEnteredStringAsUri() mapped the entered value to a 'internal:' - // URI , ensure the raw value begins with '/', '?' or '#'. - // @todo '<front>' is valid input for BC reasons, may be removed by - // https://www.drupal.org/node/2421941 - if (parse_url($uri, PHP_URL_SCHEME) === 'internal' && !in_array($element['#value'][0], ['/', '?', '#'], TRUE) && substr($element['#value'], 0, 7) !== '<front>') { - $form_state->setError($element, t('Manually entered paths should start with /, ? or #.')); - return; - } - } - - /** - * {@inheritdoc} - */ public function validateForm(array &$form, FormStateInterface $form_state) { parent::validateForm($form, $form_state); @@ -655,7 +582,9 @@ public function validateForm(array &$form, FormStateInterface $form_state) { */ public function validateSelectStep(array $form, FormStateInterface $form_state) { if ($form_state->hasValue(['entity_browser', 'entities'])) { - $id = $form_state->getValue(['entity_browser', 'entities', 0])->id(); + if (count($form_state->getValue(['entity_browser', 'entities'])) > 0) { + $id = $form_state->getValue(['entity_browser', 'entities', 0])->id(); + } $element = $form['entity_browser']; } else { @@ -665,16 +594,20 @@ public function validateSelectStep(array $form, FormStateInterface $form_state) $entity_type = $form_state->getValue(['attributes', 'data-entity-type']); + if (!isset($id)) { + $form_state->setError($element, $this->t('No entity selected.')); + return; + } if ($entity = $this->entityTypeManager->getStorage($entity_type)->load($id)) { if (!$entity->access('view')) { - $form_state->setError($element, $this->t('Unable to access @type entity @id.', array('@type' => $entity_type, '@id' => $id))); + $form_state->setError($element, $this->t('Unable to access @type entity @id.', ['@type' => $entity_type, '@id' => $id])); } else { if ($uuid = $entity->uuid()) { $form_state->setValueForElement($form['attributes']['data-entity-uuid'], $uuid); } else { - $form_state->setError($element, $this->t('Cannot embed @type entity @id because it does not have a UUID.', array('@type' => $entity_type, '@id' => $id))); + $form_state->setError($element, $this->t('Cannot embed @type entity @id because it does not have a UUID.', ['@type' => $entity_type, '@id' => $id])); } // Ensure that at least one Entity Embed Display plugin is present @@ -684,13 +617,17 @@ public function validateSelectStep(array $form, FormStateInterface $form_state) // If no plugin is available after taking the intersection, raise error. // Also log an exception. if (empty($display_plugin_options)) { - $form_state->setError($element, $this->t('No display options available for the selected entity. Please select another entity.')); - $this->logger('entity_embed')->warning('No display options available for "@type:" entity "@id" while embedding using button "@button". Please ensure that at least one Entity Embed Display plugin is allowed for this embed button which is available for this entity.', array('@type' => $entity_type, '@id' => $entity->id(), '@button' => $embed_button->id())); + $form_state->setError($element, $this->t('No display options available for the selected %entity-type. Please select another %entity_type.', ['%entity_type' => $entity->getEntityType()->getLabel()])); + $this->logger('entity_embed')->warning('No display options available for "@type:" entity "@id" while embedding using button "@button". Please ensure that at least one Entity Embed Display plugin is allowed for this embed button which is available for this entity.', [ + '@type' => $entity_type, + '@id' => $entity->id(), + '@button' => $embed_button->id(), + ]); } } } else { - $form_state->setError($element, $this->t('Unable to load @type entity @id.', array('@type' => $entity_type, '@id' => $id))); + $form_state->setError($element, $this->t('Unable to load @type entity @id.', ['@type' => $entity_type, '@id' => $id])); } } @@ -705,11 +642,9 @@ public function validateSelectStep(array $form, FormStateInterface $form_state) public function validateEmbedStep(array $form, FormStateInterface $form_state) { // Validate configuration forms for the Entity Embed Display plugin used. $entity_element = $form_state->getValue('attributes'); - $entity = $this->entityTypeManager->getStorage($entity_element['data-entity-type']) - ->loadByProperties(['uuid' => $entity_element['data-entity-uuid']]); - $entity = current($entity) ?: NULL; + $entity = $form_state->get('entity'); $plugin_id = $entity_element['data-entity-embed-display']; - $plugin_settings = !empty($entity_element['data-entity-embed-display-settings']) ? $entity_element['data-entity-embed-display-settings'] : array(); + $plugin_settings = !empty($entity_element['data-entity-embed-display-settings']) ? $entity_element['data-entity-embed-display-settings'] : []; $display = $this->entityEmbedDisplayManager->createInstance($plugin_id, $plugin_settings); $display->setContextValue('entity', $entity); $display->setAttributes($entity_element); @@ -740,6 +675,8 @@ public function updatePluginConfigurationForm(array &$form, FormStateInterface $ * The form array. * @param \Drupal\Core\Form\FormStateInterface $form_state * The form state. + * @param string $step + * The next step name, such as 'select', 'review' or 'embed'. * * @return \Drupal\Core\Ajax\AjaxResponse * The ajax response. @@ -778,10 +715,10 @@ public function submitSelectStep(array &$form, FormStateInterface $form_state) { // Display errors in form, if any. if ($form_state->hasAnyErrors()) { unset($form['#prefix'], $form['#suffix']); - $form['status_messages'] = array( + $form['status_messages'] = [ '#type' => 'status_messages', '#weight' => -10, - ); + ]; $response->addCommand(new HtmlCommand('#entity-embed-dialog-form', $form)); } else { @@ -851,8 +788,8 @@ public function submitAndShowEmbed(array $form, FormStateInterface $form_state) * * @param array $form * An associative array containing the structure of the form. - * @param FormStateInterface $form_state - * An associative array containing the current state of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state object. * * @return \Drupal\Core\Ajax\AjaxResponse * The ajax response. @@ -862,11 +799,9 @@ public function submitEmbedStep(array &$form, FormStateInterface $form_state) { // Submit configuration form the selected Entity Embed Display plugin. $entity_element = $form_state->getValue('attributes'); - $entity = $this->entityTypeManager->getStorage($entity_element['data-entity-type']) - ->loadByProperties(['uuid' => $entity_element['data-entity-uuid']]); - $entity = current($entity); + $entity = $this->loadEntityByAttributes($entity_element); $plugin_id = $entity_element['data-entity-embed-display']; - $plugin_settings = !empty($entity_element['data-entity-embed-display-settings']) ? $entity_element['data-entity-embed-display-settings'] : array(); + $plugin_settings = !empty($entity_element['data-entity-embed-display-settings']) ? $entity_element['data-entity-embed-display-settings'] : []; $display = $this->entityEmbedDisplayManager->createInstance($plugin_id, $plugin_settings); $display->setContextValue('entity', $entity); $display->setAttributes($entity_element); @@ -876,10 +811,10 @@ public function submitEmbedStep(array &$form, FormStateInterface $form_state) { // Display errors in form, if any. if ($form_state->hasAnyErrors()) { unset($form['#prefix'], $form['#suffix']); - $form['status_messages'] = array( + $form['status_messages'] = [ '#type' => 'status_messages', '#weight' => -10, - ); + ]; $response->addCommand(new HtmlCommand('#entity-embed-dialog-form', $form)); } else { @@ -889,12 +824,13 @@ public function submitEmbedStep(array &$form, FormStateInterface $form_state) { } // Filter out empty attributes. - $values['attributes'] = array_filter($values['attributes'], function($value) { - return (bool) Unicode::strlen((string) $value); + $values['attributes'] = array_filter($values['attributes'], function ($value) { + return (bool) mb_strlen((string) $value); }); - // Allow other modules to alter the values before getting submitted to the WYSIWYG. - $this->moduleHandler->alter('entity_embed_values', $values, $entity, $display, $form_state); + // Allow other modules to alter the values before getting submitted to the + // WYSIWYG. + $this->moduleHandler->alter('entity_embed_values', $values, $entity, $display); $response->addCommand(new EditorDialogSave($values)); $response->addCommand(new CloseModalDialogCommand()); @@ -916,8 +852,7 @@ public static function escapeValue($element, FormStateInterface $form_state) { } /** - * Returns the allowed Entity Embed Display plugins given an embed button and - * an entity. + * Returns the allowed display plugins given an embed button and an entity. * * @param \Drupal\embed\EmbedButtonInterface $embed_button * The embed button. @@ -928,19 +863,20 @@ public static function escapeValue($element, FormStateInterface $form_state) { * List of allowed Entity Embed Display plugins. */ public function getDisplayPluginOptions(EmbedButtonInterface $embed_button, EntityInterface $entity) { - $plugins = $this->entityEmbedDisplayManager->getDefinitionOptionsForEntity($entity); + $plugins = $this->entityEmbedDisplayManager->getDefinitionOptionsForContext([ + 'entity' => $entity, + 'entity_type' => $entity->getEntityTypeId(), + 'embed_button' => $embed_button, + ]); - if ($allowed_plugins = $embed_button->getTypeSetting('display_plugins')) { - $plugins = array_intersect_key($plugins, array_flip($allowed_plugins)); - } - - natsort($plugins); return $plugins; } /** - * Registers JS callback that gets entities from entity browser and updates - * form values accordingly. + * Registers JS callbacks. + * + * Callbacks are responsible for getting entities from entity browser and + * updating form values accordingly. */ public function registerJSCallback(RegisterJSCallbacks $event) { if ($event->getBrowserID() == $this->entityBrowser->id()) { @@ -952,6 +888,7 @@ public function registerJSCallback(RegisterJSCallbacks $event) { * Load the current entity browser and its settings from the form state. * * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state object. */ protected function loadEntityBrowser(FormStateInterface $form_state) { $this->entityBrowser = NULL; diff --git a/web/modules/entity_embed/src/Plugin/CKEditorPlugin/DrupalEntity.php b/web/modules/entity_embed/src/Plugin/CKEditorPlugin/DrupalEntity.php index 2a6fa14b95..6e71eaadba 100644 --- a/web/modules/entity_embed/src/Plugin/CKEditorPlugin/DrupalEntity.php +++ b/web/modules/entity_embed/src/Plugin/CKEditorPlugin/DrupalEntity.php @@ -2,6 +2,7 @@ namespace Drupal\entity_embed\Plugin\CKEditorPlugin; +use Drupal\ckeditor\CKEditorPluginCssInterface; use Drupal\editor\Entity\Editor; use Drupal\embed\EmbedButtonInterface; use Drupal\embed\EmbedCKEditorPluginBase; @@ -15,7 +16,7 @@ * embed_type_id = "entity" * ) */ -class DrupalEntity extends EmbedCKEditorPluginBase { +class DrupalEntity extends EmbedCKEditorPluginBase implements CKEditorPluginCssInterface { /** * {@inheritdoc} @@ -33,15 +34,36 @@ public function getFile() { return drupal_get_path('module', 'entity_embed') . '/js/plugins/drupalentity/plugin.js'; } + /** + * {@inheritdoc} + */ + public function getLibraries(Editor $editor) { + return [ + 'core/jquery', + 'core/drupal', + 'core/drupal.ajax', + ]; + } + /** * {@inheritdoc} */ public function getConfig(Editor $editor) { - return array( + return [ 'DrupalEntity_dialogTitleAdd' => t('Insert entity'), 'DrupalEntity_dialogTitleEdit' => t('Edit entity'), 'DrupalEntity_buttons' => $this->getButtons(), - ); + ]; + } + + /** + * {@inheritdoc} + */ + public function getCssFiles(Editor $editor) { + return [ + drupal_get_path('module', 'system') . '/css/components/hidden.module.css', + drupal_get_path('module', 'entity_embed') . '/css/entity_embed.editor.css', + ]; } } diff --git a/web/modules/entity_embed/src/Plugin/Derivative/FieldFormatterDeriver.php b/web/modules/entity_embed/src/Plugin/Derivative/FieldFormatterDeriver.php index 22740687ed..d420acccf2 100644 --- a/web/modules/entity_embed/src/Plugin/Derivative/FieldFormatterDeriver.php +++ b/web/modules/entity_embed/src/Plugin/Derivative/FieldFormatterDeriver.php @@ -18,7 +18,7 @@ class FieldFormatterDeriver extends DeriverBase implements ContainerDeriverInter /** * The manager for formatter plugins. * - * @var \Drupal\Core\Field\FormatterPluginManager. + * @var \Drupal\Core\Field\FormatterPluginManager */ protected $formatterManager; @@ -65,16 +65,21 @@ public function getDerivativeDefinitions($base_plugin_definition) { if (!isset($base_plugin_definition['field_type'])) { throw new \LogicException("Undefined field_type definition in plugin {$base_plugin_definition['id']}."); } - $mode = $this->configFactory->get('entity_embed.settings')->get('rendered_entity_mode'); + + $no_media_image_decorator = [ + 'entity_reference_entity_id', + 'entity_reference_label', + ]; + foreach ($this->formatterManager->getOptions($base_plugin_definition['field_type']) as $formatter => $label) { $this->derivatives[$formatter] = $base_plugin_definition; $this->derivatives[$formatter]['label'] = $label; - // Don't show entity_reference_entity_view in the UI if the rendered - // entity mode is FALSE. In that case we show view modes from - // ViewModeDeriver, entity_reference_entity_view is kept for backwards - // compatibility. - if ($formatter == 'entity_reference_entity_view' && $mode == FALSE) { - $this->derivatives[$formatter]['no_ui'] = TRUE; + + // The base entity embed display plugin annotation has opted into + // `supports_image_alt_and_title`. For some derivatives we know that they + // do not support this, so opt them back out. + if (in_array($formatter, $no_media_image_decorator, TRUE)) { + $this->derivatives[$formatter]['supports_image_alt_and_title'] = FALSE; } } return $this->derivatives; diff --git a/web/modules/entity_embed/src/Plugin/Derivative/ViewModeDeriver.php b/web/modules/entity_embed/src/Plugin/Derivative/ViewModeDeriver.php index f23d70e032..75fc762c3e 100644 --- a/web/modules/entity_embed/src/Plugin/Derivative/ViewModeDeriver.php +++ b/web/modules/entity_embed/src/Plugin/Derivative/ViewModeDeriver.php @@ -56,14 +56,18 @@ public static function create(ContainerInterface $container, $base_plugin_id) { * {@inheritdoc} */ public function getDerivativeDefinitions($base_plugin_definition) { - $mode = $this->configFactory->get('entity_embed.settings')->get('rendered_entity_mode'); + $no_ui = $this->configFactory->get('entity_embed.settings')->get('rendered_entity_mode'); foreach ($this->entityDisplayRepository->getAllViewModes() as $view_modes) { foreach ($view_modes as $view_mode => $definition) { - $this->derivatives[$definition['id']] = $base_plugin_definition; - $this->derivatives[$definition['id']]['label'] = $definition['label']; - $this->derivatives[$definition['id']]['view_mode'] = $view_mode; - $this->derivatives[$definition['id']]['entity_types'] = $definition['targetEntityType']; - $this->derivatives[$definition['id']]['no_ui'] = $mode; + $this->derivatives[$definition['id']] = [ + 'label' => $definition['label'], + 'view_mode' => $view_mode, + 'entity_types' => [$definition['targetEntityType']], + 'no_ui' => $no_ui, + // Check if the plugin should run through MediaImageDecorator. A more + // fine-grained access check happens there. + 'supports_image_alt_and_title' => ($definition['targetEntityType'] === 'media'), + ] + $base_plugin_definition; } } return $this->derivatives; diff --git a/web/modules/entity_embed/src/Plugin/EmbedType/Entity.php b/web/modules/entity_embed/src/Plugin/EmbedType/Entity.php index 967ffb3a13..554ff3d502 100644 --- a/web/modules/entity_embed/src/Plugin/EmbedType/Entity.php +++ b/web/modules/entity_embed/src/Plugin/EmbedType/Entity.php @@ -10,6 +10,7 @@ use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Plugin\PluginDependencyTrait; use Drupal\embed\EmbedType\EmbedTypeBase; +use Drupal\entity_browser\EntityBrowserInterface; use Drupal\entity_embed\EntityEmbedDisplay\EntityEmbedDisplayManager; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -102,7 +103,9 @@ public function defaultConfiguration() { 'bundles' => [], 'display_plugins' => [], 'entity_browser' => '', - 'entity_browser_settings' => [], + 'entity_browser_settings' => [ + 'display_review' => 0, + ], ]; } @@ -113,56 +116,59 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta $embed_button = $form_state->getTemporaryValue('embed_button'); $entity_type_id = $this->getConfigurationValue('entity_type'); - $form['entity_type'] = array( + $form['entity_type'] = [ '#type' => 'select', '#title' => $this->t('Entity type'), '#options' => $this->getEntityTypeOptions(), '#default_value' => $entity_type_id, - '#description' => $this->t("Entity type for which this button is to enabled."), + '#description' => $this->t("The entity type this button will embed."), '#required' => TRUE, - '#ajax' => array( - 'callback' => array($form_state->getFormObject(), 'updateTypeSettings'), + '#ajax' => [ + 'callback' => [$form_state->getFormObject(), 'updateTypeSettings'], 'effect' => 'fade', - ), + ], '#disabled' => !$embed_button->isNew(), - ); + ]; if ($entity_type_id) { $entity_type = $this->entityTypeManager->getDefinition($entity_type_id); - $form['bundles'] = array( + $form['bundles'] = [ '#type' => 'checkboxes', '#title' => $entity_type->getBundleLabel() ?: $this->t('Bundles'), '#options' => $this->getEntityBundleOptions($entity_type), '#default_value' => $this->getConfigurationValue('bundles'), '#description' => $this->t('If none are selected, all are allowed.'), - ); + ]; $form['bundles']['#access'] = !empty($form['bundles']['#options']); // Allow option to limit Entity Embed Display plugins. - $form['display_plugins'] = array( + $form['display_plugins'] = [ '#type' => 'checkboxes', '#title' => $this->t('Allowed Entity Embed Display plugins'), '#options' => $this->displayPluginManager->getDefinitionOptionsForEntityType($entity_type_id), '#default_value' => $this->getConfigurationValue('display_plugins'), '#description' => $this->t('If none are selected, all are allowed. Note that these are the plugins which are allowed for this entity type, all of these might not be available for the selected entity.'), - ); + ]; $form['display_plugins']['#access'] = !empty($form['display_plugins']['#options']); /** @var \Drupal\entity_browser\EntityBrowserInterface[] $browsers */ if ($this->entityTypeManager->hasDefinition('entity_browser') && ($browsers = $this->entityTypeManager->getStorage('entity_browser')->loadMultiple())) { - $ids = array_keys($browsers); - $labels = array_map( + // Filter out unsupported displays & return array of ids and labels. + $browsers = array_map( function ($item) { /** @var \Drupal\entity_browser\EntityBrowserInterface $item */ return $item->label(); }, - $browsers + // Filter out both modal and standalone forms as they don't work. + array_filter($browsers, function (EntityBrowserInterface $browser) { + return !in_array($browser->getDisplay()->getPluginId(), ['modal', 'standalone'], TRUE); + }) ); - $options = ['_none' => $this->t('None (autocomplete)')] + array_combine($ids, $labels); + $options = ['_none' => $this->t('None (autocomplete)')] + $browsers; $form['entity_browser'] = [ '#type' => 'select', '#title' => $this->t('Entity browser'), - '#description' => $this->t('Entity browser to be used to select entities to be embedded.'), + '#description' => $this->t('Entity browser to be used to select entities to be embedded. Only compatible browsers will be available to be chosen.'), '#options' => $options, '#default_value' => $this->getConfigurationValue('entity_browser'), ]; @@ -204,7 +210,7 @@ public function submitConfigurationForm(array &$form, FormStateInterface $form_s $form_state->setValue('display_plugins', array_keys(array_filter($display_plugins))); $entity_browser = $form_state->getValue('entity_browser') == '_none' ? '' : $form_state->getValue('entity_browser'); $form_state->setValue('entity_browser', $entity_browser); - $form_state->setValue('entity_browser_settings', $form_state->getValue('entity_browser_settings')); + $form_state->setValue('entity_browser_settings', $form_state->getValue('entity_browser_settings', [])); parent::submitConfigurationForm($form, $form_state); } @@ -228,12 +234,12 @@ protected function getEntityTypeOptions() { unset($options[$group][$entity_type_id]); } // Filter out entity types that do not support UUIDs. - if (!$this->entityTypeManager->getDefinition($entity_type_id)->hasKey('uuid')) { + elseif (!$this->entityTypeManager->getDefinition($entity_type_id)->hasKey('uuid')) { unset($options[$group][$entity_type_id]); } // Filter out entity types that will not have any Entity Embed Display // plugins. - if (!$this->displayPluginManager->getDefinitionOptionsForEntityType($entity_type_id)) { + elseif (!$this->displayPluginManager->getDefinitionOptionsForEntityType($entity_type_id)) { unset($options[$group][$entity_type_id]); } } @@ -252,7 +258,7 @@ protected function getEntityTypeOptions() { * An array of bundle labels, keyed by bundle name. */ protected function getEntityBundleOptions(EntityTypeInterface $entity_type) { - $bundle_options = array(); + $bundle_options = []; // If the entity has bundles, allow option to restrict to bundle(s). if ($entity_type->hasKey('bundle')) { foreach ($this->entityTypeBundleInfo->getBundleInfo($entity_type->id()) as $bundle_id => $bundle_info) { @@ -292,6 +298,16 @@ public function calculateDependencies() { $this->calculatePluginDependencies($instance); } + $entity_browser = $this->getConfigurationValue('entity_browser'); + if ($entity_browser && $this->entityTypeManager->hasDefinition('entity_browser')) { + $browser = $this->entityTypeManager + ->getStorage('entity_browser') + ->load($entity_browser); + if ($browser) { + $this->addDependency($browser->getConfigDependencyKey(), $browser->getConfigDependencyName()); + } + } + return $this->dependencies; } diff --git a/web/modules/entity_embed/src/Plugin/Filter/EntityEmbedFilter.php b/web/modules/entity_embed/src/Plugin/Filter/EntityEmbedFilter.php index b230b570c2..f244f89686 100644 --- a/web/modules/entity_embed/src/Plugin/Filter/EntityEmbedFilter.php +++ b/web/modules/entity_embed/src/Plugin/Filter/EntityEmbedFilter.php @@ -3,14 +3,15 @@ namespace Drupal\entity_embed\Plugin\Filter; use Drupal\Component\Utility\Html; +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Logger\LoggerChannelFactoryInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Render\BubbleableMetadata; use Drupal\Core\Render\RenderContext; use Drupal\Core\Render\RendererInterface; use Drupal\entity_embed\EntityEmbedBuilderInterface; use Drupal\entity_embed\Exception\EntityNotFoundException; -use Drupal\entity_embed\Exception\RecursiveRenderingException; use Drupal\filter\FilterProcessResult; use Drupal\filter\Plugin\FilterBase; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -22,14 +23,22 @@ * @Filter( * id = "entity_embed", * title = @Translation("Display embedded entities"), - * description = @Translation("Embeds entities using data attributes: data-entity-type, data-entity-uuid, and data-view-mode."), - * type = Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_REVERSIBLE + * description = @Translation("Embeds entities using data attributes: data-entity-type, data-entity-uuid, and data-view-mode. Should usually run as the last filter, since it does not contain user input."), + * type = Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_REVERSIBLE, + * weight = 100, * ) */ class EntityEmbedFilter extends FilterBase implements ContainerFactoryPluginInterface { use DomHelperTrait; + /** + * The number of times this formatter allows rendering the same entity. + * + * @var int + */ + const RECURSIVE_RENDER_LIMIT = 20; + /** * The renderer service. * @@ -51,6 +60,25 @@ class EntityEmbedFilter extends FilterBase implements ContainerFactoryPluginInte */ protected $builder; + /** + * The logger factory. + * + * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface + */ + protected $loggerFactory; + + /** + * An array of counters for the recursive rendering protection. + * + * Each counter takes into account all the relevant information about the + * field and the referenced entity that is being rendered. + * + * @var array + * + * @see \Drupal\Core\Field\Plugin\Field\FieldFormatter\EntityReferenceEntityFormatter::$recursiveRenderDepth + */ + protected static $recursiveRenderDepth = []; + /** * Constructs a EntityEmbedFilter object. * @@ -66,12 +94,15 @@ class EntityEmbedFilter extends FilterBase implements ContainerFactoryPluginInte * The renderer. * @param \Drupal\entity_embed\EntityEmbedBuilderInterface $builder * The entity embed builder service. + * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory + * The logger factory. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, RendererInterface $renderer, EntityEmbedBuilderInterface $builder) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, RendererInterface $renderer, EntityEmbedBuilderInterface $builder, LoggerChannelFactoryInterface $logger_factory) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->entityTypeManager = $entity_type_manager; $this->renderer = $renderer; $this->builder = $builder; + $this->loggerFactory = $logger_factory; } /** @@ -84,7 +115,8 @@ public static function create(ContainerInterface $container, array $configuratio $plugin_definition, $container->get('entity_type.manager'), $container->get('renderer'), - $container->get('entity_embed.builder') + $container->get('entity_embed.builder'), + $container->get('logger.factory') ); } @@ -111,10 +143,10 @@ public function process($text, $langcode) { $node->removeAttribute('data-entity-embed-settings'); } + $entity = NULL; try { // Load the entity either by UUID (preferred) or ID. $id = NULL; - $entity = NULL; if ($id = $node->getAttribute('data-entity-uuid')) { $entity = $this->entityTypeManager->getStorage($entity_type) ->loadByProperties(['uuid' => $id]); @@ -124,22 +156,48 @@ public function process($text, $langcode) { $id = $node->getAttribute('data-entity-id'); $entity = $this->entityTypeManager->getStorage($entity_type)->load($id); } + if (!$entity instanceof EntityInterface) { + $missing_text = $this->t('Missing @type.', ['@type' => $this->entityTypeManager->getDefinition($entity_type)->getSingularLabel()]); + $entity_output = '<img src="' . file_url_transform_relative(file_create_url('core/modules/media/images/icons/no-thumbnail.png')) . '" width="180" height="180" alt="' . $missing_text . '" title="' . $missing_text . '"/>'; + throw new EntityNotFoundException(sprintf('Unable to load embedded %s entity %s.', $entity_type, $id)); + } + } + catch (EntityNotFoundException $e) { + watchdog_exception('entity_embed', $e); + } + + if ($entity instanceof EntityInterface) { + // If a UUID was not used, but is available, add it to the HTML. + if (!$node->getAttribute('data-entity-uuid') && $uuid = $entity->uuid()) { + $node->setAttribute('data-entity-uuid', $uuid); + } + + $context = $this->getNodeAttributesAsArray($node); + $context += ['data-langcode' => $langcode]; + + // Due to render caching and delayed calls, filtering happens later + // in the rendering process through a '#pre_render' callback, so we + // need to generate a counter that takes into account all the + // relevant information about this field and the referenced entity + // that is being rendered. + // @see \Drupal\filter\Element\ProcessedText::preRenderText() + $recursive_render_id = $entity->uuid() . json_encode($context); + if (isset(static::$recursiveRenderDepth[$recursive_render_id])) { + static::$recursiveRenderDepth[$recursive_render_id]++; + } + else { + static::$recursiveRenderDepth[$recursive_render_id] = 1; + } - if ($entity) { - // Protect ourselves from recursive rendering. - static $depth = 0; - $depth++; - if ($depth > 20) { - throw new RecursiveRenderingException(sprintf('Recursive rendering detected when rendering embedded %s entity %s.', $entity_type, $entity->id())); - } - - // If a UUID was not used, but is available, add it to the HTML. - if (!$node->getAttribute('data-entity-uuid') && $uuid = $entity->uuid()) { - $node->setAttribute('data-entity-uuid', $uuid); - } - - $context = $this->getNodeAttributesAsArray($node); - $context += array('data-langcode' => $langcode); + // Protect ourselves from recursive rendering. + if (static::$recursiveRenderDepth[$recursive_render_id] > static::RECURSIVE_RENDER_LIMIT) { + $this->loggerFactory->get('entity')->error('Recursive rendering detected when rendering embedded entity %entity_type: %entity_id. Aborting rendering.', [ + '%entity_type' => $entity->getEntityTypeId(), + '%entity_id' => $entity->id(), + ]); + $entity_output = ''; + } + else { $build = $this->builder->buildEntityEmbed($entity, $context); // We need to render the embedded entity: // - without replacing placeholders, so that the placeholders are @@ -154,16 +212,8 @@ public function process($text, $langcode) { return $this->renderer->render($build); }); $result = $result->merge(BubbleableMetadata::createFromRenderArray($build)); - - $depth--; - } - else { - throw new EntityNotFoundException(sprintf('Unable to load embedded %s entity %s.', $entity_type, $id)); } } - catch (\Exception $e) { - watchdog_exception('entity_embed', $e); - } $this->replaceNodeContent($node, $entity_output); } diff --git a/web/modules/entity_embed/src/Plugin/entity_embed/EntityEmbedDisplay/EntityReferenceFieldFormatter.php b/web/modules/entity_embed/src/Plugin/entity_embed/EntityEmbedDisplay/EntityReferenceFieldFormatter.php index 318635a51b..b9e3d3ddc9 100644 --- a/web/modules/entity_embed/src/Plugin/entity_embed/EntityEmbedDisplay/EntityReferenceFieldFormatter.php +++ b/web/modules/entity_embed/src/Plugin/entity_embed/EntityEmbedDisplay/EntityReferenceFieldFormatter.php @@ -2,7 +2,16 @@ namespace Drupal\entity_embed\Plugin\entity_embed\EntityEmbedDisplay; +use Drupal\Core\Access\AccessResult; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Field\FormatterPluginManager; +use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\Core\Security\TrustedCallbackInterface; +use Drupal\Core\TypedData\TypedDataManager; use Drupal\entity_embed\EntityEmbedDisplay\FieldFormatterEntityEmbedDisplayBase; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Entity Embed Display reusing entity reference field formatters. @@ -13,10 +22,70 @@ * id = "entity_reference", * label = @Translation("Entity Reference"), * deriver = "Drupal\entity_embed\Plugin\Derivative\FieldFormatterDeriver", - * field_type = "entity_reference" + * field_type = "entity_reference", + * supports_image_alt_and_title = TRUE * ) */ -class EntityReferenceFieldFormatter extends FieldFormatterEntityEmbedDisplayBase { +class EntityReferenceFieldFormatter extends FieldFormatterEntityEmbedDisplayBase implements TrustedCallbackInterface { + + /** + * The configuration factory. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $configFactory; + + /** + * Constructs a new EntityReferenceFieldFormatter. + * + * @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\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager service. + * @param \Drupal\Core\Field\FormatterPluginManager $formatter_plugin_manager + * The field formatter plugin manager. + * @param \Drupal\Core\TypedData\TypedDataManager $typed_data_manager + * The typed data manager. + * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager + * The language manager. + * @param \Drupal\Core\Config\ConfigFactoryInterface|null $config_factory + * The configuration factory, or null to get from global container for + * backwards compatibility. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, FormatterPluginManager $formatter_plugin_manager, TypedDataManager $typed_data_manager, LanguageManagerInterface $language_manager, ConfigFactoryInterface $config_factory = NULL) { + parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager, $formatter_plugin_manager, $typed_data_manager, $language_manager); + $this->configFactory = $config_factory instanceof ConfigFactoryInterface ? $config_factory : \Drupal::configFactory(); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager'), + $container->get('plugin.manager.field.formatter'), + $container->get('typed_data_manager'), + $container->get('language_manager'), + $container->get('config.factory') + ); + } + + /** + * {@inheritdoc} + */ + public static function trustedCallbacks() { + return [ + 'disableContextualLinks', + 'disableQuickEdit', + ]; + } /** * {@inheritdoc} @@ -33,7 +102,107 @@ public function getFieldDefinition() { * {@inheritdoc} */ public function getFieldValue() { - return array('target_id' => $this->getContextValue('entity')->id()); + return ['target_id' => $this->getContextValue('entity')->id()]; + } + + /** + * {@inheritdoc} + */ + protected function isApplicableFieldFormatter() { + $access = parent::isApplicableFieldFormatter(); + + // Don't bother checking if not allowed. + if ($access->isAllowed()) { + if ($this->getPluginId() === 'entity_reference:entity_reference_entity_view') { + // This option disables entity_reference_entity_view plugin for content + // entity types. If it is truthy then the plugin is enabled for all + // entity types. + $mode = $this->configFactory->get('entity_embed.settings')->get('rendered_entity_mode'); + if ($mode) { + // Return *allowed* object. + return $access; + } + + // Only allow this if this is not a content entity type. + $entity_type_id = $this->getEntityTypeFromContext(); + if ($entity_type_id) { + $definition = $this->entityTypeManager->getDefinition($entity_type_id); + return $access->andIf(AccessResult::allowedIf(!$definition->entityClassImplements(ContentEntityInterface::class))); + } + } + } + + return $access; + } + + /** + * {@inheritdoc} + */ + public function build() { + $build = parent::build(); + + // Early return if this derived plugin is not using an EntityViewBuilder. + // @see \Drupal\Core\Entity\EntityViewBuilder::getBuildDefaults() + if (!isset($build['#view_mode'])) { + return $build; + } + + /** @var \Drupal\Core\Entity\EntityInterface $entity */ + $entity = $this->getEntityFromContext(); + + // There are a few concerns when rendering an embedded media entity: + // - entity access checking happens not during rendering but during routing, + // and therefore we have to do it explicitly here for the embedded entity. + $build['#access'] = $entity->access('view', NULL, TRUE); + // - caching an embedded entity separately is unnecessary; the host entity + // is already render cached; plus specific values may be overridden (such + // as an `alt` attribute) which would mean this particular rendered + // representation is unique to the host entity and hence nonsensical to + // cache separately anyway. + unset($build['#cache']['keys']); + // - Contextual Links do not make sense for embedded entities; we only allow + // the host entity to be contextually managed. + $build['#pre_render'][] = static::class . '::disableContextualLinks'; + // - Quick Edit does not make sense for embedded entities; we only allow the + // host entity to be edited in-place. + $build['#pre_render'][] = static::class . '::disableQuickEdit'; + // - default styling may break captioned media embeds; attach asset library + // to ensure captions behave as intended. + $build['#attached']['library'][] = 'entity_embed/caption'; + + return $build; + } + + /** + * Disables Contextual Links for the embedded media by removing its property. + * + * @param array $build + * The render array for the embedded media. + * + * @return array + * The updated render array. + * + * @see \Drupal\Core\Entity\EntityViewBuilder::addContextualLinks() + */ + public static function disableContextualLinks(array $build) { + unset($build['#contextual_links']); + return $build; + } + + /** + * Disables Quick Edit for the embedded media by removing its attributes. + * + * @param array $build + * The render array for the embedded media. + * + * @return array + * The updated render array. + * + * @see quickedit_entity_view_alter() + */ + public static function disableQuickEdit(array $build) { + unset($build['#attributes']['data-quickedit-entity-id']); + return $build; } } diff --git a/web/modules/entity_embed/src/Plugin/entity_embed/EntityEmbedDisplay/FileFieldFormatter.php b/web/modules/entity_embed/src/Plugin/entity_embed/EntityEmbedDisplay/FileFieldFormatter.php index ee29ae3d6a..7101f8512c 100644 --- a/web/modules/entity_embed/src/Plugin/entity_embed/EntityEmbedDisplay/FileFieldFormatter.php +++ b/web/modules/entity_embed/src/Plugin/entity_embed/EntityEmbedDisplay/FileFieldFormatter.php @@ -25,7 +25,7 @@ class FileFieldFormatter extends EntityReferenceFieldFormatter { */ public function getFieldValue() { $value = parent::getFieldValue(); - $value += array_intersect_key($this->getConfiguration(), array('description' => '')); + $value += array_intersect_key($this->getConfiguration(), ['description' => '']); return $value; } @@ -47,12 +47,12 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta // Description is stored in the configuration since it doesn't map to an // actual HTML attribute. - $form['description'] = array( + $form['description'] = [ '#type' => 'textfield', '#title' => $this->t('Description'), '#default_value' => $this->getConfigurationValue('description'), '#description' => $this->t('The description may be used as the label of the link to the file.'), - ); + ]; return $form; } diff --git a/web/modules/entity_embed/src/Plugin/entity_embed/EntityEmbedDisplay/ImageFieldFormatter.php b/web/modules/entity_embed/src/Plugin/entity_embed/EntityEmbedDisplay/ImageFieldFormatter.php index 76b16413ec..c175ded478 100644 --- a/web/modules/entity_embed/src/Plugin/entity_embed/EntityEmbedDisplay/ImageFieldFormatter.php +++ b/web/modules/entity_embed/src/Plugin/entity_embed/EntityEmbedDisplay/ImageFieldFormatter.php @@ -8,6 +8,7 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Image\ImageFactory; use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\TypedData\TypedDataManager; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -36,8 +37,21 @@ class ImageFieldFormatter extends FileFieldFormatter { protected $imageFactory; /** - * {@inheritdoc} + * The messenger. + * + * @var \Drupal\Core\Messenger\MessengerInterface + */ + protected $messenger; + + /** + * Constructs an ImageFieldFormatter 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\Entity\EntityTypeManagerInterface $entity_type_manager * The entity manager service. * @param \Drupal\Core\Field\FormatterPluginManager $formatter_plugin_manager @@ -48,10 +62,13 @@ class ImageFieldFormatter extends FileFieldFormatter { * The image factory. * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager * The language manager. + * @param \Drupal\Core\Messenger\MessengerInterface $messenger + * The messenger. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, FormatterPluginManager $formatter_plugin_manager, TypedDataManager $typed_data_manager, ImageFactory $image_factory, LanguageManagerInterface $language_manager) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, FormatterPluginManager $formatter_plugin_manager, TypedDataManager $typed_data_manager, ImageFactory $image_factory, LanguageManagerInterface $language_manager, MessengerInterface $messenger) { parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager, $formatter_plugin_manager, $typed_data_manager, $language_manager); $this->imageFactory = $image_factory; + $this->messenger = $messenger; } /** @@ -66,7 +83,8 @@ public static function create(ContainerInterface $container, array $configuratio $container->get('plugin.manager.field.formatter'), $container->get('typed_data_manager'), $container->get('image.factory'), - $container->get('language_manager') + $container->get('language_manager'), + $container->get('messenger') ); } @@ -77,7 +95,7 @@ public function getFieldValue() { $value = parent::getFieldValue(); // File field support descriptions, but images do not. unset($value['description']); - $value += array_intersect_key($this->getAttributeValues(), array('alt' => '', 'title' => '')); + $value += array_intersect_key($this->getAttributeValues(), ['alt' => '', 'title' => '']); return $value; } @@ -111,7 +129,14 @@ protected function isValidImage() { // Loading large files is slow, make sure it is an image mime type before // doing that. list($type,) = explode('/', $entity->getMimeType(), 2); - $access = AccessResult::allowedIf($type == 'image' && $this->imageFactory->get($entity->getFileUri())->isValid()) + $is_valid_image = FALSE; + if ($type == 'image') { + $is_valid_image = $this->imageFactory->get($entity->getFileUri())->isValid(); + if (!$is_valid_image) { + $this->messenger->addMessage($this->t('The selected image "@image" is invalid.', ['@image' => $entity->label()]), 'error'); + } + } + $access = AccessResult::allowedIf($type == 'image' && $is_valid_image) // See the above @todo, this is the best we can do for now. ->addCacheableDependency($entity); } @@ -153,29 +178,29 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta // double quotes in place of empty alt text only if that was filled // intentionally by the user. if (!empty($entity_element) && $entity_element['data-entity-embed-display'] == 'image:image') { - $alt = '""'; + $alt = MediaImageDecorator::EMPTY_STRING; } } // Add support for editing the alternate and title text attributes. - $form['alt'] = array( + $form['alt'] = [ '#type' => 'textfield', '#title' => $this->t('Alternate text'), '#default_value' => $alt, '#description' => $this->t('This text will be used by screen readers, search engines, or when the image cannot be loaded.'), - '#parents' => array('attributes', 'alt'), + '#parents' => ['attributes', 'alt'], '#required' => TRUE, '#required_error' => $this->t('Alternative text is required.<br />(Only in rare cases should this be left empty. To create empty alternative text, enter <code>""</code> — two double quotes without any content).'), '#maxlength' => 512, - ); - $form['title'] = array( + ]; + $form['title'] = [ '#type' => 'textfield', '#title' => $this->t('Title'), '#default_value' => $this->getAttributeValue('title', ''), '#description' => t('The title is used as a tool tip when the user hovers the mouse over the image.'), - '#parents' => array('attributes', 'title'), + '#parents' => ['attributes', 'title'], '#maxlength' => 1024, - ); + ]; return $form; } @@ -186,8 +211,8 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { // When the alt attribute is set to two double quotes, transform it to the // empty string: two double quotes signify "empty alt attribute". See above. - if (trim($form_state->getValue(array('attributes', 'alt'))) === '""') { - $form_state->setValue(array('attributes', 'alt'), ''); + if (trim($form_state->getValue(['attributes', 'alt'])) === MediaImageDecorator::EMPTY_STRING) { + $form_state->setValue(['attributes', 'alt'], ''); } } diff --git a/web/modules/entity_embed/src/Plugin/entity_embed/EntityEmbedDisplay/MediaImageDecorator.php b/web/modules/entity_embed/src/Plugin/entity_embed/EntityEmbedDisplay/MediaImageDecorator.php new file mode 100644 index 0000000000..7d319eb9e1 --- /dev/null +++ b/web/modules/entity_embed/src/Plugin/entity_embed/EntityEmbedDisplay/MediaImageDecorator.php @@ -0,0 +1,258 @@ +<?php + +namespace Drupal\entity_embed\Plugin\entity_embed\EntityEmbedDisplay; + +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Session\AccountInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\entity_embed\EntityEmbedDisplay\EntityEmbedDisplayInterface; +use Drupal\image\Plugin\Field\FieldType\ImageItem; +use Drupal\media\MediaInterface; + +/** + * Decorator on all EntityEmbedDisplays that adds alt and title overriding. + */ +class MediaImageDecorator implements EntityEmbedDisplayInterface { + + use StringTranslationTrait; + + /** + * A string that signifies not to render the alt text. + * + * @const string + */ + const EMPTY_STRING = '""'; + + /** + * The decorated EntityEmbedDisplay class. + * + * @var \Drupal\entity_embed\EntityEmbedDisplay\EntityEmbedDisplayInterface + */ + private $decorated; + + /** + * MediaImageDecorator constructor. + * + * @param \Drupal\entity_embed\EntityEmbedDisplay\EntityEmbedDisplayInterface $decorated + * The decorated EntityEmbedDisplay plugin. + */ + public function __construct(EntityEmbedDisplayInterface $decorated) { + $this->decorated = $decorated; + } + + /** + * Passes through all unknown calls to the decorated object. + */ + public function __call($method, $args) { + return call_user_func_array([$this->decorated, $method], $args); + } + + /** + * {@inheritdoc} + */ + public function access(AccountInterface $account = NULL) { + return $this->decorated->access($account); + } + + /** + * {@inheritdoc} + */ + public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { + return $this->decorated->validateConfigurationForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return $this->decorated->defaultConfiguration(); + } + + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + return $this->decorated->calculateDependencies(); + } + + /** + * {@inheritdoc} + */ + public function getConfiguration() { + return $this->decorated->getConfiguration(); + } + + /** + * {@inheritdoc} + */ + public function getPluginDefinition() { + return $this->decorated->getPluginDefinition(); + } + + /** + * {@inheritdoc} + */ + public function getPluginId() { + return $this->decorated->getPluginId(); + } + + /** + * {@inheritdoc} + */ + public function setConfiguration(array $configuration) { + return $this->decorated->setConfiguration($configuration); + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $form = $this->decorated->buildConfigurationForm($form, $form_state); + + /** @var \Drupal\Core\Entity\EntityInterface $entity */ + $entity = $this->decorated->getEntityFromContext(); + + if ($image_field = $this->getMediaImageSourceField($entity)) { + + $settings = $entity->{$image_field}->getItemDefinition()->getSettings(); + $attributes = $this->getAttributeValues(); + + $alt = isset($attributes['alt']) ? $attributes['alt'] : NULL; + $title = isset($attributes['title']) ? $attributes['title'] : NULL; + + // Setting empty alt to double quotes. See ImageFieldFormatter. + if ($settings['alt_field_required'] && $alt === '') { + $alt = static::EMPTY_STRING; + } + + if (!empty($settings['alt_field'])) { + // Add support for editing the alternate and title text attributes. + $form['alt'] = [ + '#type' => 'textfield', + '#title' => $this->t('Alternate text'), + '#default_value' => $alt, + '#description' => $this->t('This text will be used by screen readers, search engines, or when the image cannot be loaded.'), + '#required_error' => $this->t('Alternative text is required.<br />(Only in rare cases should this be left empty. To create empty alternative text, enter <code>""</code> — two double quotes without any content).'), + '#maxlength' => 512, + '#placeholder' => $entity->{$image_field}->alt, + ]; + } + + if (!empty($settings['title_field'])) { + $form['title'] = [ + '#type' => 'textfield', + '#title' => $this->t('Title'), + '#default_value' => $title, + '#description' => t('The title is used as a tool tip when the user hovers the mouse over the image.'), + '#maxlength' => 1024, + '#placeholder' => $entity->{$image_field}->title, + ]; + } + } + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + /** @var \Drupal\Core\Entity\EntityInterface $entity */ + $entity = $this->decorated->getEntityFromContext(); + if ($image_field = $this->getMediaImageSourceField($entity)) { + $settings = $entity->{$image_field}->getItemDefinition()->getSettings(); + $values = $form_state->getValue(['attributes', 'data-entity-embed-display-settings']); + + if (!empty($settings['alt_field'])) { + // When the alt attribute is set to two double quotes, transform it to + // the empty string: two double quotes signify "empty alt attribute". + // See ImagefieldFormatter. + if (trim($values['alt']) === static::EMPTY_STRING) { + $values['alt'] = static::EMPTY_STRING; + } + // If the alt text is unchanged from the values set on the + // field, there's no need for the alt property to be set. + elseif ($values['alt'] === $entity->{$image_field}->alt) { + $values['alt'] = ''; + } + + $form_state->setValue(['attributes', 'alt'], $values['alt']); + $form_state->unsetValue([ + 'attributes', + 'data-entity-embed-display-settings', + 'alt', + ]); + } + + if (!empty($settings['title_field'])) { + if (empty($values['title'])) { + $values['title'] = ''; + } + // If the title text is unchanged from the values set on the + // field, there's no need for the title property to be set. + elseif ($values['title'] === $entity->{$image_field}->title) { + $values['title'] = ''; + } + + $form_state->setValue(['attributes', 'title'], $values['title']); + $form_state->unsetValue([ + 'attributes', + 'data-entity-embed-display-settings', + 'title', + ]); + } + } + $this->decorated->submitConfigurationForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function build() { + $build = $this->decorated->build(); + + /** @var \Drupal\Core\Entity\EntityInterface $entity */ + $entity = $this->decorated->getEntityFromContext(); + + if ($image_field = $this->getMediaImageSourceField($entity)) { + $settings = $entity->{$image_field}->getItemDefinition()->getSettings(); + + if (!empty($settings['alt_field']) && $this->hasAttribute('alt')) { + $entity->{$image_field}->alt = $this->getAttributeValue('alt'); + $entity->thumbnail->alt = $this->getAttributeValue('alt'); + } + + if (!empty($settings['title_field']) && $this->hasAttribute('title')) { + $entity->{$image_field}->title = $this->getAttributeValue('title'); + $entity->thumbnail->title = $this->getAttributeValue('title'); + } + } + + return $build; + } + + /** + * Get image field from source config. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * Embedded entity. + * + * @return string|null + * String of image field name. + */ + protected function getMediaImageSourceField(EntityInterface $entity) { + if (!$entity instanceof MediaInterface) { + return NULL; + } + + $field_definition = $entity->getSource() + ->getSourceFieldDefinition($entity->bundle->entity); + $item_class = $field_definition->getItemDefinition()->getClass(); + if ($item_class == ImageItem::class || is_subclass_of($item_class, ImageItem::class)) { + return $field_definition->getName(); + } + return NULL; + } + +} diff --git a/web/modules/entity_embed/src/Plugin/entity_embed/EntityEmbedDisplay/ViewModeFieldFormatter.php b/web/modules/entity_embed/src/Plugin/entity_embed/EntityEmbedDisplay/ViewModeFieldFormatter.php index 14a8a6a70f..b69dae831f 100644 --- a/web/modules/entity_embed/src/Plugin/entity_embed/EntityEmbedDisplay/ViewModeFieldFormatter.php +++ b/web/modules/entity_embed/src/Plugin/entity_embed/EntityEmbedDisplay/ViewModeFieldFormatter.php @@ -59,4 +59,28 @@ public function getFieldFormatterId() { return 'entity_reference_entity_view'; } + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + $definition = $this->getPluginDefinition(); + $view_mode = $definition['view_mode']; + + $view_modes = []; + + foreach ($definition['entity_types'] as $type) { + $view_modes[] = "$type.$view_mode"; + } + + $entity_view_modes = $this->entityTypeManager + ->getStorage('entity_view_mode') + ->loadMultiple($view_modes); + + foreach ($entity_view_modes as $view_mode) { + $this->addDependency($view_mode->getConfigDependencyKey(), $view_mode->getConfigDependencyName()); + } + + return $this->dependencies; + } + } diff --git a/web/modules/entity_embed/src/Tests/EntityEmbedDialogTest.php b/web/modules/entity_embed/src/Tests/EntityEmbedDialogTest.php deleted file mode 100644 index b7d84d9b87..0000000000 --- a/web/modules/entity_embed/src/Tests/EntityEmbedDialogTest.php +++ /dev/null @@ -1,172 +0,0 @@ -<?php - -namespace Drupal\entity_embed\Tests; - -use Drupal\editor\Entity\Editor; - -/** - * Tests the entity_embed dialog controller and route. - * - * @group entity_embed - */ -class EntityEmbedDialogTest extends EntityEmbedTestBase { - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = ['image']; - - /** - * Tests the entity embed dialog. - */ - public function testEntityEmbedDialog() { - // Ensure that the route is not accessible without specifying all the - // parameters. - $this->getEmbedDialog(); - $this->assertResponse(404, 'Embed dialog is not accessible without specifying filter format and embed button.'); - $this->getEmbedDialog('custom_format'); - $this->assertResponse(404, 'Embed dialog is not accessible without specifying embed button.'); - - // Ensure that the route is not accessible with an invalid embed button. - $this->getEmbedDialog('custom_format', 'invalid_button'); - $this->assertResponse(404, 'Embed dialog is not accessible without specifying filter format and embed button.'); - - // Ensure that the route is not accessible with text format without the - // button configured. - $this->getEmbedDialog('plain_text', 'node'); - $this->assertResponse(404, 'Embed dialog is not accessible with a filter that does not have an editor configuration.'); - - // Add an empty configuration for the plain_text editor configuration. - $editor = Editor::create([ - 'format' => 'plain_text', - 'editor' => 'ckeditor', - ]); - $editor->save(); - $this->getEmbedDialog('plain_text', 'node'); - $this->assertResponse(403, 'Embed dialog is not accessible with a filter that does not have the embed button assigned to it.'); - - // Ensure that the route is accessible with a valid embed button. - // 'Node' embed button is provided by default by the module and hence the - // request must be successful. - $this->getEmbedDialog('custom_format', 'node'); - $this->assertResponse(200, 'Embed dialog is accessible with correct filter format and embed button.'); - - // Ensure form structure of the 'select' step and submit form. - $this->assertFieldByName('entity_id', '', 'Entity ID/UUID field is present.'); - - // $edit = ['attributes[data-entity-id]' => $this->node->id()]; - // $this->drupalPostAjaxForm(NULL, $edit, 'op'); - // Ensure form structure of the 'embed' step and submit form. - // $this->assertFieldByName('attributes[data-entity-embed-display]', 'Entity Embed Display plugin field is present.');. - } - - /** - * Tests the entity embed button markup. - */ - public function testEntityEmbedButtonMarkup() { - // Ensure that the route is not accessible with text format without the - // button configured. - $this->getEmbedDialog('plain_text', 'node'); - $this->assertResponse(404, 'Embed dialog is not accessible with a filter that does not have an editor configuration.'); - - // Add an empty configuration for the plain_text editor configuration. - $editor = Editor::create([ - 'format' => 'plain_text', - 'editor' => 'ckeditor', - ]); - $editor->save(); - $this->getEmbedDialog('plain_text', 'node'); - $this->assertResponse(403, 'Embed dialog is not accessible with a filter that does not have the embed button assigned to it.'); - - // Ensure that the route is accessible with a valid embed button. - // 'Node' embed button is provided by default by the module and hence the - // request must be successful. - $this->getEmbedDialog('custom_format', 'node'); - $this->assertResponse(200, 'Embed dialog is accessible with correct filter format and embed button.'); - - // Ensure form structure of the 'select' step and submit form. - $this->assertFieldByName('entity_id', '', 'Entity ID/UUID field is present.'); - - // Check that 'Next' is a primary button. - $this->assertFieldByXPath('//input[contains(@class, "button--primary")]', 'Next', 'Next is a primary button'); - - $title = $this->node->getTitle() . ' (' . $this->node->id() . ')'; - $edit = ['entity_id' => $title]; - $response = $this->drupalPostAjaxForm(NULL, $edit, 'op'); - $plugins = [ - 'entity_reference:entity_reference_label', - 'entity_reference:entity_reference_entity_id', - 'view_mode:node.full', - 'view_mode:node.rss', - 'view_mode:node.search_index', - 'view_mode:node.search_result', - 'view_mode:node.teaser', - ]; - foreach ($plugins as $plugin) { - $this->assertTrue(strpos($response[2]['data'], $plugin), 'Plugin ' . $plugin . ' is available in selection.'); - } - - $this->container->get('config.factory')->getEditable('entity_embed.settings') - ->set('rendered_entity_mode', TRUE)->save(); - $this->container->get('plugin.manager.entity_embed.display')->clearCachedDefinitions(); - - $this->getEmbedDialog('custom_format', 'node'); - $title = $this->node->getTitle() . ' (' . $this->node->id() . ')'; - $edit = ['entity_id' => $title]; - $response = $this->drupalPostAjaxForm(NULL, $edit, 'op'); - - $plugins = [ - 'entity_reference:entity_reference_label', - 'entity_reference:entity_reference_entity_id', - 'entity_reference:entity_reference_entity_view', - ]; - foreach ($plugins as $plugin) { - $this->assertTrue(strpos($response[2]['data'], $plugin), 'Plugin ' . $plugin . ' is available in selection.'); - } - /*$this->drupalPostForm(NULL, $edit, 'Next'); - // Ensure form structure of the 'embed' step and submit form. - $this->assertFieldByName('attributes[data-entity-embed-display]', 'Entity Embed Display plugin field is present.'); - - // Check that 'Embed' is a primary button. - $this->assertFieldByXPath('//input[contains(@class, "button--primary")]', 'Embed', 'Embed is a primary button');*/ - } - - /** - * Tests entity embed functionality. - */ - public function testEntityEmbedFunctionality() { - $edit = [ - 'entity_id' => $this->node->getTitle() . ' (' . $this->node->id() . ')', - ]; - $this->getEmbedDialog('custom_format', 'node'); - $this->drupalPostForm(NULL, $edit, t('Next')); - // Tests that the embed dialog doesn't trow a fatal in - // ImageFieldFormatter::isValidImage() - $this->assertResponse(200); - } - - /** - * Retrieves an embed dialog based on given parameters. - * - * @param string $filter_format_id - * ID of the filter format. - * @param string $embed_button_id - * ID of the embed button. - * - * @return string - * The retrieved HTML string. - */ - public function getEmbedDialog($filter_format_id = NULL, $embed_button_id = NULL) { - $url = 'entity-embed/dialog'; - if (!empty($filter_format_id)) { - $url .= '/' . $filter_format_id; - if (!empty($embed_button_id)) { - $url .= '/' . $embed_button_id; - } - } - return $this->drupalGet($url); - } - -} diff --git a/web/modules/entity_embed/src/Tests/EntityEmbedFilterTest.php b/web/modules/entity_embed/src/Tests/EntityEmbedFilterTest.php deleted file mode 100644 index 594ff953bf..0000000000 --- a/web/modules/entity_embed/src/Tests/EntityEmbedFilterTest.php +++ /dev/null @@ -1,198 +0,0 @@ -<?php - -namespace Drupal\entity_embed\Tests; - -/** - * Tests the entity_embed filter. - * - * @group entity_embed - */ -class EntityEmbedFilterTest extends EntityEmbedTestBase { - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = [ - 'file', - 'image', - 'entity_embed', - 'entity_embed_test', - 'node', - 'ckeditor', - ]; - - /** - * Tests the entity_embed filter. - * - * Ensures that entities are getting rendered when correct data attributes - * are passed. Also tests situations when embed fails. - */ - public function testFilter() { - // Tests entity embed using entity ID and view mode. - $content = '<drupal-entity data-entity-type="node" data-entity-id="' . $this->node->id() . '" data-view-mode="teaser">This placeholder should not be rendered.</drupal-entity>'; - $settings = array(); - $settings['type'] = 'page'; - $settings['title'] = 'Test entity embed with entity-id and view-mode'; - $settings['body'] = array(array('value' => $content, 'format' => 'custom_format')); - $node = $this->drupalCreateNode($settings); - $this->drupalGet('node/' . $node->id()); - $this->assertNoRaw('<drupal-entity data-entity-type="node" data-entity'); - $this->assertText($this->node->body->value, 'Embedded node exists in page'); - $this->assertNoText(strip_tags($content), 'Placeholder does not appear in the output when embed is successful.'); - $this->assertRaw('<article class="embedded-entity">', 'Embed container found.'); - - // Tests that embedded entity is not rendered if not accessible. - $this->node->setPublished(FALSE)->save(); - $settings = []; - $settings['type'] = 'page'; - $settings['title'] = 'Test un-accessible entity embed with entity-id and view-mode'; - $settings['body'] = [['value' => $content, 'format' => 'custom_format']]; - $node = $this->drupalCreateNode($settings); - $this->drupalGet('node/' . $node->id()); - $this->assertNoRaw('<drupal-entity data-entity-type="node" data-entity'); - $this->assertNoText($this->node->body->value, 'Embedded node does not exist in the page.'); - $this->assertNoText(strip_tags($content), 'Placeholder does not appear in the output when embed is successful.'); - // Tests that embedded entity is displayed to the user who has the view - // unpublished content permission. - $this->createRole(['view own unpublished content'], 'access_unpublished'); - $this->webUser->addRole('access_unpublished'); - $this->webUser->save(); - $this->drupalGet('node/' . $node->id()); - $this->assertNoRaw('<drupal-entity data-entity-type="node" data-entity'); - $this->assertText($this->node->body->value, 'Embedded node exists in the page.'); - $this->assertNoText(strip_tags($content), 'Placeholder does not appear in the output when embed is successful.'); - $this->assertRaw('<article class="embedded-entity">', 'Embed container found.'); - $this->webUser->removeRole('access_unpublished'); - $this->webUser->save(); - $this->node->setPublished(TRUE)->save(); - - // Tests entity embed using entity UUID and view mode. - $content = '<drupal-entity data-entity-type="node" data-entity-uuid="' . $this->node->uuid() . '" data-view-mode="teaser">This placeholder should not be rendered.</drupal-entity>'; - $settings = array(); - $settings['type'] = 'page'; - $settings['title'] = 'Test entity embed with entity-uuid and view-mode'; - $settings['body'] = array(array('value' => $content, 'format' => 'custom_format')); - $node = $this->drupalCreateNode($settings); - $this->drupalGet('node/' . $node->id()); - $this->assertNoRaw('<drupal-entity data-entity-type="node" data-entity'); - $this->assertText($this->node->body->value, 'Embedded node exists in page.'); - $this->assertNoText(strip_tags($content), 'Placeholder does not appear in the output when embed is successful.'); - $this->assertRaw('<article class="embedded-entity">', 'Embed container found.'); - $this->assertCacheTag('foo:' . $this->node->id()); - - // Ensure that placeholder is not replaced when embed is unsuccessful. - $content = '<drupal-entity data-entity-type="node" data-entity-id="InvalidID" data-view-mode="teaser">This placeholder should be rendered since specified entity does not exists.</drupal-entity>'; - $settings = array(); - $settings['type'] = 'page'; - $settings['title'] = 'Test that placeholder is retained when specified entity does not exists'; - $settings['body'] = array(array('value' => $content, 'format' => 'custom_format')); - $node = $this->drupalCreateNode($settings); - $this->drupalGet('node/' . $node->id()); - $this->assertNoRaw('<drupal-entity data-entity-type="node" data-entity'); - $this->assertNoText(strip_tags($content), 'Placeholder does not appear in the output when embed is unsuccessful.'); - - // Ensure that UUID is preferred over ID when both attributes are present. - $sample_node = $this->drupalCreateNode(); - $content = '<drupal-entity data-entity-type="node" data-entity-id="' . $sample_node->id() . '" data-entity-uuid="' . $this->node->uuid() . '" data-view-mode="teaser">This placeholder should not be rendered.</drupal-entity>'; - $settings = array(); - $settings['type'] = 'page'; - $settings['title'] = 'Test that entity-uuid is preferred over entity-id when both attributes are present'; - $settings['body'] = array(array('value' => $content, 'format' => 'custom_format')); - $node = $this->drupalCreateNode($settings); - $this->drupalGet('node/' . $node->id()); - $this->assertNoRaw('<drupal-entity data-entity-type="node" data-entity'); - $this->assertText($this->node->body->value, 'Entity specifed with UUID exists in the page.'); - $this->assertNoText($sample_node->body->value, 'Entity specifed with ID does not exists in the page.'); - $this->assertNoText(strip_tags($content), 'Placeholder does not appear in the output when embed is successful.'); - $this->assertRaw('<article class="embedded-entity"', 'Embed container found.'); - - // Test deprecated 'default' Entity Embed Display plugin. - $content = '<drupal-entity data-entity-type="node" data-entity-uuid="' . $this->node->uuid() . '" data-entity-embed-display="default" data-entity-embed-display-settings=\'{"view_mode":"teaser"}\'>This placeholder should not be rendered.</drupal-entity>'; - $settings = array(); - $settings['type'] = 'page'; - $settings['title'] = 'Test entity embed with entity-embed-display and data-entity-embed-display-settings'; - $settings['body'] = array(array('value' => $content, 'format' => 'custom_format')); - $node = $this->drupalCreateNode($settings); - $this->drupalGet('node/' . $node->id()); - $this->assertText($this->node->body->value, 'Embedded node exists in page.'); - $this->assertNoText(strip_tags($content), 'Placeholder does not appear in the output when embed is successful.'); - $this->assertRaw('<article class="embedded-entity"', 'Embed container found.'); - - // Ensure that Entity Embed Display plugin is preferred over view mode when - // both attributes are present. - $content = '<drupal-entity data-entity-type="node" data-entity-uuid="' . $this->node->uuid() . '" data-entity-embed-display="default" data-entity-embed-display-settings=\'{"view_mode":"full"}\' data-view-mode="some-invalid-view-mode" data-align="left" data-caption="test caption">This placeholder should not be rendered.</drupal-entity>'; - $settings = array(); - $settings['type'] = 'page'; - $settings['title'] = 'Test entity embed with entity-embed-display and data-entity-embed-display-settings'; - $settings['body'] = array(array('value' => $content, 'format' => 'custom_format')); - $node = $this->drupalCreateNode($settings); - $this->drupalGet('node/' . $node->id()); - $this->assertText($this->node->body->value, 'Embedded node exists in page with the view mode specified by entity-embed-settings.'); - $this->assertNoText(strip_tags($content), 'Placeholder does not appear in the output when embed is successful.'); - $this->assertRaw('<article class="embedded-entity"', 'Embed container found.'); - - // Ensure the embedded node doesn't contain data tags on the full page. - $this->drupalGet('node/' . $this->node->id()); - $this->assertNoRaw('data-align="left"', 'Align data attribute not found.'); - $this->assertNoRaw('data-caption="test caption"', 'Caption data attribute not found.'); - - // Test that tag of container element is not replaced when it's not - // <drupal-entity>. - $content = '<not-drupal-entity data-entity-type="node" data-entity-id="' . $this->node->id() . '" data-view-mode="teaser">this placeholder should not be rendered.</not-drupal-entity>'; - $settings = array(); - $settings['type'] = 'page'; - $settings['title'] = 'test entity embed with entity-id and view-mode'; - $settings['body'] = array(array('value' => $content, 'format' => 'custom_format')); - $node = $this->drupalCreateNode($settings); - $this->drupalget('node/' . $node->id()); - $this->assertNoText($this->node->body->value, 'embedded node exists in page'); - $this->assertRaw('</not-drupal-entity>'); - $content = '<div data-entity-type="node" data-entity-id="' . $this->node->id() . '" data-view-mode="teaser">this placeholder should not be rendered.</div>'; - $settings = array(); - $settings['type'] = 'page'; - $settings['title'] = 'test entity embed with entity-id and view-mode'; - $settings['body'] = array(array('value' => $content, 'format' => 'custom_format')); - $node = $this->drupalCreateNode($settings); - $this->drupalget('node/' . $node->id()); - $this->assertNoText($this->node->body->value, 'embedded node exists in page'); - $this->assertRaw('<div data-entity-type="node" data-entity-id'); - - // Test that attributes are correctly added when image formatter is used. - /** @var \Drupal\file\FileInterface $image */ - $image = $this->getTestFile('image'); - $image->setPermanent(); - $image->save(); - $content = '<drupal-entity data-entity-type="file" data-entity-uuid="' . $image->uuid() . '" data-entity-embed-display="image:image" data-entity-embed-display-settings=\'{"image_style":"","image_link":""}\' data-align="left" data-caption="test caption" alt="This is alt text" title="This is title text">This placeholder should not be rendered.</drupal-entity>'; - $settings = []; - $settings['type'] = 'page'; - $settings['title'] = 'test entity image formatter'; - $settings['body'] = [['value' => $content, 'format' => 'custom_format']]; - $node = $this->drupalCreateNode($settings); - $this->drupalget('node/' . $node->id()); - $this->assertRaw('<img src', 'IMG tag found.'); - $this->assertRaw('data-caption="test caption"', 'Caption found.'); - $this->assertRaw('data-align="left"', 'Alignment information found.'); - $this->assertTrue((bool) $this->xpath("//img[@alt='This is alt text']"), 'Alt text found'); - $this->assertTrue((bool) $this->xpath("//img[@title='This is title text']"), 'Title text found'); - $this->assertRaw('<article class="embedded-entity"', 'Embed container found.'); - - // data-entity-embed-settings is replaced with - // data-entity-embed-display-settings. Check to see if - // data-entity-embed-settings is still working. - $content = '<drupal-entity data-entity-type="node" data-entity-uuid="' . $this->node->uuid() . '" data-entity-embed-display="entity_reference:entity_reference_label" data-entity-embed-settings=\'{"link":"0"}\' data-align="left" data-caption="test caption">This placeholder should not be rendered.</drupal-entity>'; - $settings = []; - $settings['type'] = 'page'; - $settings['title'] = 'Test entity embed with data-entity-embed-settings'; - $settings['body'] = [['value' => $content, 'format' => 'custom_format']]; - $node = $this->drupalCreateNode($settings); - $this->drupalGet('node/' . $node->id()); - $this->assertText($this->node->getTitle(), 'Embeded node title is displayed.'); - $this->assertNoLink($this->node->getTitle(), 'Embed settings are respected.'); - $this->assertNoText($this->node->body->value, 'Embedded node exists in page.'); - $this->assertNoText(strip_tags($content), 'Placeholder does not appear in the output when embed is successful.'); - $this->assertRaw('<article class="embedded-entity"', 'Embed container found.'); - } - -} diff --git a/web/modules/entity_embed/src/Twig/EntityEmbedTwigExtension.php b/web/modules/entity_embed/src/Twig/EntityEmbedTwigExtension.php index 28ef108cb8..5dac420b72 100644 --- a/web/modules/entity_embed/src/Twig/EntityEmbedTwigExtension.php +++ b/web/modules/entity_embed/src/Twig/EntityEmbedTwigExtension.php @@ -30,6 +30,8 @@ class EntityEmbedTwigExtension extends \Twig_Extension { * * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * The entity type manager service. + * @param \Drupal\entity_embed\EntityEmbedBuilderInterface $builder + * The Entity embed builder service. */ public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityEmbedBuilderInterface $builder) { $this->entityTypeManager = $entity_type_manager; @@ -57,9 +59,9 @@ public function getName() { * {@inheritdoc} */ public function getFunctions() { - return array( + return [ new \Twig_SimpleFunction('entity_embed', [$this, 'getRenderArray']), - ); + ]; } /** diff --git a/web/modules/entity_embed/tests/fixtures/update/entity_embed.update-hook-test.php b/web/modules/entity_embed/tests/fixtures/update/entity_embed.update-hook-test.php index e847f5294b..860975cba9 100644 --- a/web/modules/entity_embed/tests/fixtures/update/entity_embed.update-hook-test.php +++ b/web/modules/entity_embed/tests/fixtures/update/entity_embed.update-hook-test.php @@ -2,6 +2,7 @@ /** * @file + * Update hook test file. */ use Drupal\Core\Database\Database; diff --git a/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_form_display.node.article.default.yml b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_form_display.node.article.default.yml new file mode 100644 index 0000000000..f8a6bfe4de --- /dev/null +++ b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_form_display.node.article.default.yml @@ -0,0 +1,42 @@ +langcode: en +status: true +dependencies: + config: + - field.field.node.article.body + - node.type.article + module: + - text +id: node.article.default +targetEntityType: node +bundle: article +mode: default +content: + title: + type: string_textfield + weight: 1 + region: content + settings: + size: 60 + placeholder: '' + third_party_settings: { } + body: + type: text_textarea_with_summary + weight: 2 + settings: + rows: 9 + summary_rows: 3 + placeholder: '' + third_party_settings: { } + region: content + status: + type: boolean_checkbox + settings: + display_label: true + weight: 3 + region: content + third_party_settings: { } +hidden: + created: true + promote: true + sticky: true + uid: true diff --git a/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_view_display.media.image.embed.yml b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_view_display.media.image.embed.yml new file mode 100644 index 0000000000..60ae55d6a5 --- /dev/null +++ b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_view_display.media.image.embed.yml @@ -0,0 +1,29 @@ +langcode: en +status: true +dependencies: + config: + - field.field.media.image.field_media_image + - image.style.medium + - media.type.image + module: + - image + - user +id: media.image.default +targetEntityType: media +bundle: image +mode: embed +content: + field_media_image: + type: image + weight: 2 + region: content + label: hidden + settings: + image_style: medium + image_link: '' + third_party_settings: { } +hidden: + name: true + thumbnail: true + created: true + uid: true diff --git a/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_view_display.media.image.thumb.yml b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_view_display.media.image.thumb.yml new file mode 100644 index 0000000000..4c4338c028 --- /dev/null +++ b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_view_display.media.image.thumb.yml @@ -0,0 +1,28 @@ +langcode: en +status: true +dependencies: + config: + - image.style.medium + - media.type.image + module: + - image + - user +id: media.image.default +targetEntityType: media +bundle: image +mode: thumb +content: + thumbnail: + type: image + weight: 2 + region: content + label: hidden + settings: + image_style: medium + image_link: '' + third_party_settings: { } +hidden: + name: true + field_media_image: true + created: true + uid: true diff --git a/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_view_display.node.article.default.yml b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_view_display.node.article.default.yml new file mode 100644 index 0000000000..edf834f62d --- /dev/null +++ b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_view_display.node.article.default.yml @@ -0,0 +1,27 @@ +langcode: en +status: true +dependencies: + config: + - field.field.node.article.body + - node.type.article + module: + - text + - user +id: node.article.default +targetEntityType: node +bundle: article +mode: default +content: + body: + label: hidden + type: text_default + weight: 101 + settings: { } + third_party_settings: { } + region: content + links: + weight: 100 + settings: { } + third_party_settings: { } + region: content +hidden: { } diff --git a/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_view_mode.media.embed.yml b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_view_mode.media.embed.yml new file mode 100644 index 0000000000..6a87a051b4 --- /dev/null +++ b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_view_mode.media.embed.yml @@ -0,0 +1,9 @@ +langcode: en +status: false +dependencies: + module: + - media +id: media.embed +label: 'Embed' +targetEntityType: media +cache: true diff --git a/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_view_mode.media.thumb.yml b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_view_mode.media.thumb.yml new file mode 100644 index 0000000000..54f487c9d0 --- /dev/null +++ b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_view_mode.media.thumb.yml @@ -0,0 +1,9 @@ +langcode: en +status: false +dependencies: + module: + - media +id: media.thumb +label: 'Thumb (View Mode)' +targetEntityType: media +cache: true diff --git a/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/editor.editor.full_html.yml b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/editor.editor.full_html.yml new file mode 100644 index 0000000000..4468000706 --- /dev/null +++ b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/editor.editor.full_html.yml @@ -0,0 +1,64 @@ +langcode: en +status: true +dependencies: + config: + - filter.format.full_html + module: + - ckeditor +format: full_html +editor: ckeditor +settings: + toolbar: + rows: + - + - + name: Formatting + items: + - Bold + - Italic + - Strike + - Superscript + - Subscript + - '-' + - RemoveFormat + - + name: Linking + items: + - DrupalLink + - DrupalUnlink + - + name: Lists + items: + - BulletedList + - NumberedList + - + name: Media + items: + - Blockquote + - DrupalImage + - Table + - HorizontalRule + - test_node + - test_media_entity_embed + - + name: 'Block Formatting' + items: + - Format + - + name: Tools + items: + - ShowBlocks + - Source + plugins: + language: + language_list: un + stylescombo: + styles: '' +image_upload: + status: true + scheme: public + directory: inline-images + max_size: '' + max_dimensions: + width: null + height: null diff --git a/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/embed.button.test_media_entity_embed.yml b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/embed.button.test_media_entity_embed.yml new file mode 100644 index 0000000000..7373631bd0 --- /dev/null +++ b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/embed.button.test_media_entity_embed.yml @@ -0,0 +1,21 @@ +langcode: en +status: true +dependencies: + config: + - media.type.image + module: + - entity_embed + - media +label: 'Media Entity Embed' +id: test_media_entity_embed +type_id: entity +type_settings: + entity_type: media + bundles: + - image + display_plugins: { } + entity_browser: '' + entity_browser_settings: + display_review: false +icon_uuid: null +icon_path: null diff --git a/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/embed.button.test_node.yml b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/embed.button.test_node.yml new file mode 100644 index 0000000000..db25535602 --- /dev/null +++ b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/embed.button.test_node.yml @@ -0,0 +1,15 @@ +langcode: en +status: true +dependencies: + module: + - entity_embed + - node +label: Node +id: test_node +type_id: entity +type_settings: + entity_type: node + bundles: { } + display_plugins: { } +icon_uuid: null +icon_path: null diff --git a/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/field.field.media.image.field_media_image.yml b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/field.field.media.image.field_media_image.yml new file mode 100644 index 0000000000..d625807b7d --- /dev/null +++ b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/field.field.media.image.field_media_image.yml @@ -0,0 +1,40 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.media.field_media_image + - media.type.image + enforced: + module: + - media + module: + - image +id: media.image.field_media_image +field_name: field_media_image +entity_type: media +bundle: image +label: Image +description: '' +required: true +translatable: true +default_value: { } +default_value_callback: '' +settings: + alt_field: true + alt_field_required: true + title_field: true + title_field_required: false + max_resolution: '' + min_resolution: '' + default_image: + uuid: null + alt: '' + title: '' + width: null + height: null + file_directory: '[date:custom:Y]-[date:custom:m]' + file_extensions: 'png gif jpg jpeg' + max_filesize: '' + handler: 'default:file' + handler_settings: { } +field_type: image diff --git a/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/field.field.node.article.body.yml b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/field.field.node.article.body.yml new file mode 100644 index 0000000000..8f3681d962 --- /dev/null +++ b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/field.field.node.article.body.yml @@ -0,0 +1,21 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.node.body + - node.type.article + module: + - text +id: node.article.body +field_name: body +entity_type: node +bundle: article +label: Body +description: '' +required: false +translatable: true +default_value: { } +default_value_callback: '' +settings: + display_summary: true +field_type: text_with_summary diff --git a/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/field.storage.media.field_media_image.yml b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/field.storage.media.field_media_image.yml new file mode 100644 index 0000000000..231200d59b --- /dev/null +++ b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/field.storage.media.field_media_image.yml @@ -0,0 +1,32 @@ +langcode: en +status: true +dependencies: + enforced: + module: + - media + module: + - file + - image + - media +id: media.field_media_image +field_name: field_media_image +entity_type: media +type: image +settings: + default_image: + uuid: null + alt: '' + title: '' + width: null + height: null + target_type: file + display_field: false + display_default: false + uri_scheme: public +module: image +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/filter.format.full_html.yml b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/filter.format.full_html.yml new file mode 100644 index 0000000000..9b9f864ffe --- /dev/null +++ b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/filter.format.full_html.yml @@ -0,0 +1,49 @@ +langcode: en +status: true +dependencies: + module: + - editor + - entity_embed +name: 'Full HTML' +format: full_html +weight: 10 +filters: + filter_align: + id: filter_align + provider: filter + status: true + weight: 8 + settings: { } + filter_caption: + id: filter_caption + provider: filter + status: true + weight: 9 + settings: { } + filter_htmlcorrector: + id: filter_htmlcorrector + provider: filter + status: true + weight: 10 + settings: { } + editor_file_reference: + id: editor_file_reference + provider: editor + status: true + weight: 11 + settings: { } + entity_embed: + id: entity_embed + provider: entity_embed + status: true + weight: 0 + settings: { } + filter_html: + id: filter_html + provider: filter + status: true + weight: -10 + settings: + allowed_html: '<a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id> <s> <sup> <sub> <img src alt data-entity-type data-entity-uuid data-align data-caption> <table> <caption> <tbody> <thead> <tfoot> <th> <td> <tr> <hr> <p> <h1> <pre> <drupal-entity data-entity-type data-entity-uuid data-entity-embed-display data-entity-embed-display-settings data-align data-caption data-embed-button data-langcode alt title>' + filter_html_help: true + filter_html_nofollow: false diff --git a/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/media.type.image.yml b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/media.type.image.yml new file mode 100644 index 0000000000..3a53b77365 --- /dev/null +++ b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/media.type.image.yml @@ -0,0 +1,12 @@ +langcode: en +status: true +dependencies: { } +id: image +label: Image +description: 'Use local images for reusable media.' +source: image +queue_thumbnail_downloads: false +new_revision: true +source_configuration: + source_field: field_media_image +field_map: { } diff --git a/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/node.type.article.yml b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/node.type.article.yml new file mode 100644 index 0000000000..a65ff908d0 --- /dev/null +++ b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/node.type.article.yml @@ -0,0 +1,10 @@ +langcode: en +status: true +dependencies: { } +name: 'Article' +type: article +description: 'Article' +help: '' +new_revision: false +preview_mode: 1 +display_submitted: true diff --git a/web/modules/entity_embed/tests/modules/entity_embed_test/entity_embed_test.info.yml b/web/modules/entity_embed/tests/modules/entity_embed_test/entity_embed_test.info.yml index 5ffb92825f..2c0ac91d49 100644 --- a/web/modules/entity_embed/tests/modules/entity_embed_test/entity_embed_test.info.yml +++ b/web/modules/entity_embed/tests/modules/entity_embed_test/entity_embed_test.info.yml @@ -1,12 +1,19 @@ name: 'Entity Embed test' type: module description: 'Support module for the Entity Embed module tests.' -# core: 8.x package: Testing -# version: VERSION +dependencies: + - drupal:file + - drupal:image + - drupal:node + - drupal:text + - drupal:media + - drupal:ckeditor + - drupal:editor + - embed:embed + - entity_embed:entity_embed -# Information added by Drupal.org packaging script on 2016-10-17 -version: '8.x-1.0-beta2' -core: '8.x' +# Information added by Drupal.org packaging script on 2020-03-26 +version: '8.x-1.1' project: 'entity_embed' -datestamp: 1476698379 +datestamp: 1585252809 diff --git a/web/modules/entity_embed/tests/modules/entity_embed_test/entity_embed_test.module b/web/modules/entity_embed/tests/modules/entity_embed_test/entity_embed_test.module index b34a430c02..a8ecd923fb 100644 --- a/web/modules/entity_embed/tests/modules/entity_embed_test/entity_embed_test.module +++ b/web/modules/entity_embed/tests/modules/entity_embed_test/entity_embed_test.module @@ -6,6 +6,7 @@ */ use Drupal\Core\Access\AccessResult; +use Drupal\Core\Entity\Display\EntityViewDisplayInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Session\AccountInterface; @@ -13,15 +14,15 @@ * Implements hook_theme(). */ function entity_embed_test_theme($existing, $type, $theme, $path) { - $items['entity_embed_twig_test'] = array( + $items['entity_embed_twig_test'] = [ 'template' => 'entity_embed_twig_test', - 'variables' => array( + 'variables' => [ 'entity_type' => '', 'id' => '', 'display_plugin' => 'default', - 'display_settings' => array(), - ), - ); + 'display_settings' => [], + ], + ]; return $items; } @@ -35,7 +36,7 @@ function entity_embed_test_entity_embed_display_plugins_alter(&$info) { } // Prefix each plugin name with 'testing_hook:'. - $new_info = array(); + $new_info = []; foreach ($info as $key => $value) { $new_key = "testing_hook:" . $key; $new_info[$new_key] = $info[$key]; @@ -55,7 +56,7 @@ function entity_embed_test_entity_embed_context_alter(array &$context, EntityInt // Force to use 'Label' plugin. $context['data-entity-embed-display'] = 'entity_reference:entity_reference_label'; - $context['data-entity-embed-display-settings'] = array('link' => 1); + $context['data-entity-embed-display-settings'] = ['link' => 1]; // Set title of the entity. $entity->setTitle("Title set by hook_entity_embed_context_alter"); @@ -70,6 +71,9 @@ function entity_embed_test_entity_embed_alter(array &$build, EntityInterface $en return; } + // Adding classes is as simple as appending to an array. + $build['#attributes']['class'][] = 'test-class-added-in-alter-hook'; + // Set title of the 'node' entity. $entity->setTitle("Title set by hook_entity_embed_alter"); } @@ -82,3 +86,12 @@ function entity_embed_test_entity_access(EntityInterface $entity, $operation, Ac return AccessResult::neutral()->addCacheTags(['foo:' . $entity->id()]); } } + +/** + * Implements hook_entity_view_alter(). + */ +function entity_embed_test_entity_view_alter(&$build, EntityInterface $entity, EntityViewDisplayInterface $display) { + $build['#attributes']['data-entity-embed-test-view-mode'] = $display->getMode(); + // @see \Drupal\Tests\entity_embed\FunctionalJavascript\MediaImageTest::testPreviewUsesDefaultTheme() + $build['#attributes']['data-entity-embed-test-active-theme'] = \Drupal::theme()->getActiveTheme()->getName(); +} diff --git a/web/modules/entity_embed/tests/modules/entity_embed_test/src/EntityEmbedTestTwigController.php b/web/modules/entity_embed/tests/modules/entity_embed_test/src/EntityEmbedTestTwigController.php index 7c60f948a4..d0eb4fe02c 100644 --- a/web/modules/entity_embed/tests/modules/entity_embed_test/src/EntityEmbedTestTwigController.php +++ b/web/modules/entity_embed/tests/modules/entity_embed_test/src/EntityEmbedTestTwigController.php @@ -11,36 +11,42 @@ class EntityEmbedTestTwigController { * Menu callback for testing entity_embed twig extension using entity ID. */ public function idRender() { - return array( + return [ '#theme' => 'entity_embed_twig_test', '#entity_type' => 'node', '#id' => '1', - ); + ]; } /** - * Menu callback for testing entity_embed twig extension using 'label' Entity Embed Display plugin. + * Menu callback. + * + * Used for testing entity_embed twig extension using 'label' Entity Embed + * Display plugin. */ public function labelPluginRender() { - return array( + return [ '#theme' => 'entity_embed_twig_test', '#entity_type' => 'node', '#id' => '1', '#display_plugin' => 'entity_reference:entity_reference_label', - ); + ]; } /** - * Menu callback for testing entity_embed twig extension using 'label' Entity Embed Display plugin without linking to the node. + * Menu callback. + * + * Used for testing entity_embed twig extension using 'label' Entity Embed + * Display plugin without linking to the node. */ public function labelPluginNoLinkRender() { - return array( + return [ '#theme' => 'entity_embed_twig_test', '#entity_type' => 'node', '#id' => '1', '#display_plugin' => 'entity_reference:entity_reference_label', - '#display_settings' => array('link' => 0), - ); + '#display_settings' => ['link' => 0], + ]; } } diff --git a/web/modules/entity_embed/tests/modules/entity_embed_translation_test/config/install/language.content_settings.media.image.yml b/web/modules/entity_embed/tests/modules/entity_embed_translation_test/config/install/language.content_settings.media.image.yml new file mode 100644 index 0000000000..653a78e661 --- /dev/null +++ b/web/modules/entity_embed/tests/modules/entity_embed_translation_test/config/install/language.content_settings.media.image.yml @@ -0,0 +1,15 @@ +langcode: en +status: true +dependencies: + config: + - media.type.image + module: + - content_translation +third_party_settings: + content_translation: + enabled: true +id: media.image +target_entity_type_id: media +target_bundle: image +default_langcode: site_default +language_alterable: true diff --git a/web/modules/entity_embed/tests/modules/entity_embed_translation_test/config/install/language.content_settings.node.article.yml b/web/modules/entity_embed/tests/modules/entity_embed_translation_test/config/install/language.content_settings.node.article.yml new file mode 100644 index 0000000000..de684ef1f4 --- /dev/null +++ b/web/modules/entity_embed/tests/modules/entity_embed_translation_test/config/install/language.content_settings.node.article.yml @@ -0,0 +1,15 @@ +langcode: en +status: true +dependencies: + config: + - node.type.article + module: + - content_translation +third_party_settings: + content_translation: + enabled: true +id: node.article +target_entity_type_id: node +target_bundle: article +default_langcode: site_default +language_alterable: true diff --git a/web/modules/entity_embed/tests/modules/entity_embed_translation_test/config/install/language.entity.fr.yml b/web/modules/entity_embed/tests/modules/entity_embed_translation_test/config/install/language.entity.fr.yml new file mode 100644 index 0000000000..280469e139 --- /dev/null +++ b/web/modules/entity_embed/tests/modules/entity_embed_translation_test/config/install/language.entity.fr.yml @@ -0,0 +1,8 @@ +langcode: en +status: true +dependencies: { } +id: fr +label: French +direction: ltr +weight: 1 +locked: false diff --git a/web/modules/entity_embed/tests/modules/entity_embed_translation_test/entity_embed_translation_test.info.yml b/web/modules/entity_embed/tests/modules/entity_embed_translation_test/entity_embed_translation_test.info.yml new file mode 100644 index 0000000000..3daeda6ddd --- /dev/null +++ b/web/modules/entity_embed/tests/modules/entity_embed_translation_test/entity_embed_translation_test.info.yml @@ -0,0 +1,20 @@ +name: 'Entity Embed Translation test' +type: module +description: 'Aids in testing translation within entity embed' +package: Testing +dependencies: + - drupal:content_translation + - drupal:file + - drupal:image + - drupal:node + - drupal:text + - drupal:media + - drupal:ckeditor + - drupal:editor + - embed:embed + - entity_embed:entity_embed + +# Information added by Drupal.org packaging script on 2020-03-26 +version: '8.x-1.1' +project: 'entity_embed' +datestamp: 1585252809 diff --git a/web/modules/entity_embed/tests/src/Functional/EntityEmbedDialogTest.php b/web/modules/entity_embed/tests/src/Functional/EntityEmbedDialogTest.php new file mode 100644 index 0000000000..67d305c0f2 --- /dev/null +++ b/web/modules/entity_embed/tests/src/Functional/EntityEmbedDialogTest.php @@ -0,0 +1,122 @@ +<?php + +namespace Drupal\Tests\entity_embed\Functional; + +use Drupal\editor\Entity\Editor; + +/** + * Tests the entity_embed dialog controller and route. + * + * @group entity_embed + */ +class EntityEmbedDialogTest extends EntityEmbedTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = ['image']; + + /** + * Tests the entity embed dialog. + */ + public function testEntityEmbedDialog() { + // Ensure that the route is not accessible without specifying all the + // parameters. + $this->drupalGet('/entity-embed/dialog'); + // Verify embed dialog is not accessible without specifying filter format + // and embed button. + $this->assertSession()->statusCodeEquals(404); + $this->drupalGet('/entity-embed/dialog/custom_format'); + // Verify embed dialog is not accessible without specifying embed button. + $this->assertSession()->statusCodeEquals(404); + + // Ensure that the route is not accessible with an invalid embed button. + $this->drupalGet('/entity-embed/dialog/custom_format/invalid_button'); + // Verify embed dialog is not accessible without specifying filter format + // and embed button. + $this->assertSession()->statusCodeEquals(404); + + // Ensure that the route is not accessible with text format without the + // button configured. + $this->drupalGet('/entity-embed/dialog/plain_text/node'); + // Verify embed dialog is not accessible with a filter that does not have + // an editor configuration. + $this->assertSession()->statusCodeEquals(404); + + // Add an empty configuration for the plain_text editor configuration. + $editor = Editor::create([ + 'format' => 'plain_text', + 'editor' => 'ckeditor', + ]); + $editor->save(); + $this->drupalGet('/entity-embed/dialog/plain_text/node'); + // Verify embed dialog is not accessible with a filter that does not have + // the embed button assigned to it. + $this->assertSession()->statusCodeEquals(403); + + // Ensure that the route is accessible with a valid embed button. + // 'Node' embed button is provided by default by the module and hence the + // request must be successful. + $this->drupalGet('/entity-embed/dialog/custom_format/node'); + // Verify embed dialog is accessible with correct filter format + // and embed button. + $this->assertSession()->statusCodeEquals(200); + + // Ensure form structure of the 'select' step and submit form. + $this->assertSession()->fieldExists('entity_id'); + } + + /** + * Tests the entity embed button markup. + */ + public function testEntityEmbedButtonMarkup() { + // Ensure that the route is not accessible with text format without the + // button configured. + $this->drupalGet('/entity-embed/dialog/plain_text/node'); + // Verify embed dialog is not accessible with a filter that does not have + // an editor configuration. + $this->assertSession()->statusCodeEquals(404); + + // Add an empty configuration for the plain_text editor configuration. + $editor = Editor::create([ + 'format' => 'plain_text', + 'editor' => 'ckeditor', + ]); + $editor->save(); + $this->drupalGet('/entity-embed/dialog/plain_text/node'); + // Verify embed dialog is not accessible with a filter that does not have + // the embed button assigned to it. + $this->assertSession()->statusCodeEquals(403); + + // Ensure that the route is accessible with a valid embed button. + // 'Node' embed button is provided by default by the module and hence the + // request must be successful. + $this->drupalGet('/entity-embed/dialog/custom_format/node'); + // Verify embed dialog is accessible with correct filter format + // and embed button. + $this->assertSession()->statusCodeEquals(200); + + // Ensure form structure of the 'select' step and submit form. + $this->assertSession()->fieldExists('entity_id'); + + // Check that 'Next' is a primary button. + $this->assertSession()->elementExists('xpath', '//input[contains(@class, "button--primary")]'); + } + + /** + * Tests entity embed functionality. + */ + public function testEntityEmbedFunctionality() { + $edit = [ + 'entity_id' => $this->node->getTitle() . ' (' . $this->node->id() . ')', + ]; + $this->drupalGet('/entity-embed/dialog/custom_format/node'); + $this->drupalPostForm(NULL, $edit, t('Next')); + // Tests that the embed dialog doesn't trow a fatal in + // ImageFieldFormatter::isValidImage() + $this->assertSession()->statusCodeEquals(200); + } + +} diff --git a/web/modules/entity_embed/tests/src/Functional/EntityEmbedDisplayManagerTest.php b/web/modules/entity_embed/tests/src/Functional/EntityEmbedDisplayManagerTest.php new file mode 100644 index 0000000000..dbd5067cd4 --- /dev/null +++ b/web/modules/entity_embed/tests/src/Functional/EntityEmbedDisplayManagerTest.php @@ -0,0 +1,178 @@ +<?php + +namespace Drupal\Tests\entity_embed\Functional; + +use Drupal\Tests\BrowserTestBase; + +/** + * @coversDefaultClass \Drupal\entity_embed\EntityEmbedDisplay\EntityEmbedDisplayManager + * @group entity_embed + */ +class EntityEmbedDisplayManagerTest extends BrowserTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'field_ui', + 'node', + 'file', + 'image', + 'ckeditor', + 'entity_embed', + ]; + + /** + * The test button that embeds image files. + * + * @var \Drupal\embed\Entity\EmbedButton + */ + protected $imageButton; + + /** + * Created file entity. + * + * @var \Drupal\file\FileInterface + */ + protected $image; + + /** + * File created with invalid image. + * + * @var \Drupal\file\FileInterface + */ + protected $invalidImage; + + /** + * The EntityEmbedDisplay plugin manager. + * + * @var \Drupal\entity_embed\EntityEmbedDisplay\EntityEmbedDisplayManager + */ + protected $entityEmbedDisplayManager; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->imageButton = $this->container->get('entity_type.manager') + ->getStorage('embed_button') + ->create([ + 'label' => 'Image Embed', + 'id' => 'image_embed', + 'type_id' => 'entity', + 'type_settings' => [ + 'entity_type' => 'file', + 'display_plugins' => [ + 'image:image', + ], + 'entity_browser' => '', + 'entity_browser_settings' => [ + 'display_review' => FALSE, + ], + ], + 'icon_uuid' => NULL, + ]); + $this->imageButton->save(); + + // Create a sample image to embed. + $entity_embed_path = $this->container->get('module_handler') + ->getModule('entity_embed') + ->getPath(); + \Drupal::service('file_system')->copy($entity_embed_path . '/js/plugins/drupalentity/entity.png', 'public://example1.png'); + + // Resize the test image so that it will be scaled down during token + // replacement. + $this->image1 = $this->container->get('image.factory')->get('public://example1.png'); + $this->image1->resize(500, 500); + $this->image1->save(); + + $this->image = $this->container->get('entity_type.manager') + ->getStorage('file') + ->create([ + 'uri' => 'public://example1.png', + 'status' => 1, + ]); + $this->image->save(); + + $this->invalidImage = $this->container->get('entity_type.manager') + ->getStorage('file') + ->create([ + 'uri' => 'public://nonexistentimage.jpg', + 'filename' => 'nonexistentimage.jpg', + 'status' => 1, + ]); + $this->invalidImage->save(); + + $this->entityEmbedDisplayManager = $this->container->get('plugin.manager.entity_embed.display'); + } + + /** + * @covers ::getDefinitionsForContexts + */ + public function testGetDefinitionsForContexts() { + $options = $this->entityEmbedDisplayManager + ->getDefinitionOptionsForContext([ + 'entity' => $this->image, + 'entity_type' => $this->image->getEntityTypeId(), + 'embed_button' => $this->imageButton, + ]); + $expected = [ + 'image:image' => 'Image', + ]; + $this->assertEquals($expected, $options); + + $options = $this->entityEmbedDisplayManager + ->getDefinitionOptionsForContext([ + 'entity' => $this->image, + 'entity_type' => $this->image->getEntityTypeId(), + ]); + // All available plugins for the entity type. + $expected = [ + 'image:image' => 'Image', + 'entity_reference:entity_reference_entity_id' => 'Entity ID', + 'file:file_default' => 'Generic file', + 'entity_reference:entity_reference_label' => 'Label', + 'file:file_table' => 'Table of files', + 'file:file_url_plain' => 'URL to file', + 'image:image_url' => 'URL to image', + ]; + $this->assertEquals($expected, $options); + + // Test that output is the same as ::getDefinitionOptionsForEntity(). + $options = $this->entityEmbedDisplayManager + ->getDefinitionOptionsForEntity($this->image); + $this->assertEquals($expected, $options); + + $options = $this->entityEmbedDisplayManager + ->getDefinitionOptionsForContext([ + 'entity' => $this->invalidImage, + 'entity_type' => $this->invalidImage->getEntityTypeId(), + 'embed_button' => $this->imageButton, + ]); + // Since the image is invalid, the `image:image` display isn't returned. + $this->assertEmpty($options); + + $options = $this->entityEmbedDisplayManager + ->getDefinitionOptionsForContext([ + 'entity' => $this->invalidImage, + 'entity_type' => $this->invalidImage->getEntityTypeId(), + ]); + // Since the image is invalid, the image display plugins aren't returned. + $expected = [ + 'entity_reference:entity_reference_entity_id' => 'Entity ID', + 'file:file_default' => 'Generic file', + 'entity_reference:entity_reference_label' => 'Label', + 'file:file_table' => 'Table of files', + 'file:file_url_plain' => 'URL to file', + ]; + $this->assertEquals($expected, $options); + + // Test that output is the same as ::getDefinitionOptionsForEntity(). + $options = $this->entityEmbedDisplayManager + ->getDefinitionOptionsForEntity($this->invalidImage); + $this->assertEquals($expected, $options); + } + +} diff --git a/web/modules/entity_embed/src/Tests/EntityEmbedEntityBrowserTest.php b/web/modules/entity_embed/tests/src/Functional/EntityEmbedEntityBrowserTest.php similarity index 58% rename from web/modules/entity_embed/src/Tests/EntityEmbedEntityBrowserTest.php rename to web/modules/entity_embed/tests/src/Functional/EntityEmbedEntityBrowserTest.php index 60367956e2..28ee225e59 100644 --- a/web/modules/entity_embed/src/Tests/EntityEmbedEntityBrowserTest.php +++ b/web/modules/entity_embed/tests/src/Functional/EntityEmbedEntityBrowserTest.php @@ -1,6 +1,6 @@ <?php -namespace Drupal\entity_embed\Tests; +namespace Drupal\Tests\entity_embed\Functional; use Drupal\entity_browser\Entity\EntityBrowser; use Drupal\embed\Entity\EmbedButton; @@ -25,12 +25,15 @@ class EntityEmbedEntityBrowserTest extends EntityEmbedDialogTest { * Tests the entity browser integration. */ public function testEntityEmbedEntityBrowserIntegration() { - $this->getEmbedDialog('custom_format', 'node'); - $this->assertResponse(200, 'Embed dialog is accessible with custom filter format and default embed button.'); + $this->drupalGet('/entity-embed/dialog/custom_format/node'); + // Verify embed dialog is accessible with custom filter format and + // default embed button. + $this->assertSession()->statusCodeEquals(200); // Verify that an autocomplete field is available by default. - $this->assertFieldByName('entity_id', '', 'Entity ID/UUID field is present.'); - $this->assertNoText('Select entities to embed', 'Entity browser button is not present.'); + $this->assertSession()->fieldExists('entity_id'); + $this->assertSession() + ->linkNotExists('Select entities to embed', 'Entity browser button is not present.'); // Set up entity browser. $entity_browser = EntityBrowser::create([ @@ -55,13 +58,22 @@ public function testEntityEmbedEntityBrowserIntegration() { $embed_button->type_settings['entity_browser'] = 'entity_embed_entity_browser_test'; $embed_button->save(); - $this->getEmbedDialog('custom_format', 'node'); - $this->assertResponse(200, 'Embed dialog is accessible with custom filter format and default embed button.'); + // Rebuild routes, so the route called by getEmbedDialog() exists. + $this->container->get('router.builder')->rebuild(); + + $dependencies = $embed_button->getDependencies(); + $this->assertContains('entity_browser.browser.entity_embed_entity_browser_test', $dependencies['config']); + + $this->drupalGet('/entity-embed/dialog/custom_format/node'); + + // Verify embed dialog is accessible with custom filter format and + // default embed button. + $this->assertSession()->statusCodeEquals(200); // Verify that the autocomplete field is replaced by an entity browser // button. - $this->assertNoFieldByName('entity_id', '', 'Entity ID/UUID field is present.'); - $this->assertText('Select entities to embed', 'Entity browser button is present.'); + $this->assertSession()->fieldNotExists('entity_id'); + $this->assertSession()->buttonExists('Select entities to embed'); } } diff --git a/web/modules/entity_embed/src/Tests/EntityEmbedHooksTest.php b/web/modules/entity_embed/tests/src/Functional/EntityEmbedHooksTest.php similarity index 70% rename from web/modules/entity_embed/src/Tests/EntityEmbedHooksTest.php rename to web/modules/entity_embed/tests/src/Functional/EntityEmbedHooksTest.php index 98964e7378..99d30f9735 100644 --- a/web/modules/entity_embed/src/Tests/EntityEmbedHooksTest.php +++ b/web/modules/entity_embed/tests/src/Functional/EntityEmbedHooksTest.php @@ -1,6 +1,6 @@ <?php -namespace Drupal\entity_embed\Tests; +namespace Drupal\Tests\entity_embed\Functional; /** * Tests the hooks provided by entity_embed module. @@ -17,7 +17,7 @@ class EntityEmbedHooksTest extends EntityEmbedTestBase { protected $state; /** - * + * {@inheritdoc} */ protected function setUp() { parent::setUp(); @@ -52,36 +52,38 @@ public function testEntityEmbedHooks() { // implementation and ensure it is working as designed. $this->state->set('entity_embed_test_entity_embed_alter', TRUE); $content = '<drupal-entity data-entity-type="node" data-entity-uuid="' . $this->node->uuid() . '" data-entity-embed-display="default" data-entity-embed-display-settings=\'{"view_mode":"teaser"}\'>This placeholder should not be rendered.</drupal-entity>'; - $settings = array(); + $settings = []; $settings['type'] = 'page'; $settings['title'] = 'Test hook_entity_embed_alter()'; - $settings['body'] = array(array('value' => $content, 'format' => 'custom_format')); + $settings['body'] = [['value' => $content, 'format' => 'custom_format']]; $node = $this->drupalCreateNode($settings); $this->drupalGet('node/' . $node->id()); - $this->assertText($this->node->body->value, 'Embedded node exists in page.'); - $this->assertNoText(strip_tags($content), 'Placeholder does not appears in the output when embed is successful.'); + // Verify embedded node body exists in page. + $this->assertSession()->responseContains($this->node->body->value); + $this->assertSession()->responseNotContains('This placeholder should not be rendered.'); // Ensure that embedded node's title has been replaced. - $this->assertText('Title set by hook_entity_embed_alter', 'Title of the embedded node is replaced by hook_entity_embed_alter()'); - $this->assertNoText($this->node->title->value, 'Original title of the embedded node is not visible.'); + $this->assertSession()->responseContains('Title set by hook_entity_embed_alter'); + $this->assertSession()->responseContains('test-class-added-in-alter-hook'); + // Verify the original title of the embedded node is not visible. + $this->assertSession()->responseNotContains($this->node->title->value); $this->state->set('entity_embed_test_entity_embed_alter', FALSE); // Enable entity_embed_test.module's hook_entity_embed_context_alter() // implementation and ensure it is working as designed. $this->state->set('entity_embed_test_entity_embed_context_alter', TRUE); $content = '<drupal-entity data-entity-type="node" data-entity-uuid="' . $this->node->uuid() . '" data-entity-embed-display="default" data-entity-embed-display-settings=\'{"view_mode":"teaser"}\'>This placeholder should not be rendered.</drupal-entity>'; - $settings = array(); + $settings = []; $settings['type'] = 'page'; $settings['title'] = 'Test hook_entity_embed_context_alter()'; - $settings['body'] = array(array('value' => $content, 'format' => 'custom_format')); + $settings['body'] = [['value' => $content, 'format' => 'custom_format']]; $node = $this->drupalCreateNode($settings); $this->drupalGet('node/' . $node->id()); - $this->assertNoText(strip_tags($content), 'Placeholder does not appears in the output when embed is successful.'); + $this->assertSession()->responseNotContains('This placeholder should not be rendered.'); // To ensure that 'label' plugin is used, verify that the body of the // embedded node is not visible and the title links to the embedded node. - $this->assertNoText($this->node->body->value, 'Body of the embedded node does not exists in page.'); - $this->assertText('Title set by hook_entity_embed_context_alter', 'Title of the embedded node is replaced by hook_entity_embed_context_alter()'); - $this->assertNoText($this->node->title->value, 'Original title of the embedded node is not visible.'); - $this->assertLinkByHref('node/' . $this->node->id(), 0, 'Link to the embedded node exists.'); + $this->assertSession()->responseNotContains($this->node->body->value); + $this->assertSession()->responseContains('Title set by hook_entity_embed_context_alter'); + $this->assertSession()->linkByHrefExists('node/' . $this->node->id(), 0, 'Link to the embedded node exists.'); $this->state->set('entity_embed_test_entity_embed_context_alter', FALSE); } diff --git a/web/modules/entity_embed/src/Tests/EntityEmbedTestBase.php b/web/modules/entity_embed/tests/src/Functional/EntityEmbedTestBase.php similarity index 71% rename from web/modules/entity_embed/src/Tests/EntityEmbedTestBase.php rename to web/modules/entity_embed/tests/src/Functional/EntityEmbedTestBase.php index 4833947083..c92bcecd6b 100644 --- a/web/modules/entity_embed/src/Tests/EntityEmbedTestBase.php +++ b/web/modules/entity_embed/tests/src/Functional/EntityEmbedTestBase.php @@ -1,24 +1,32 @@ <?php -namespace Drupal\entity_embed\Tests; +namespace Drupal\Tests\entity_embed\Functional; use Drupal\Core\Entity\EntityInterface; use Drupal\editor\Entity\Editor; use Drupal\file\Entity\File; use Drupal\filter\Entity\FilterFormat; -use Drupal\simpletest\WebTestBase; +use Drupal\Tests\BrowserTestBase; +use Drupal\Tests\TestFileCreationTrait; /** * Base class for all entity_embed tests. */ -abstract class EntityEmbedTestBase extends WebTestBase { +abstract class EntityEmbedTestBase extends BrowserTestBase { + + use TestFileCreationTrait; /** * Modules to enable. * * @var array */ - public static $modules = ['entity_embed', 'entity_embed_test', 'node', 'ckeditor']; + protected static $modules = [ + 'entity_embed', + 'entity_embed_test', + 'node', + 'ckeditor', + ]; /** * The test user. @@ -35,7 +43,7 @@ abstract class EntityEmbedTestBase extends WebTestBase { protected $node; /** - * + * {@inheritdoc} */ protected function setUp() { parent::setUp(); @@ -48,6 +56,15 @@ protected function setUp() { 'format' => 'custom_format', 'name' => 'Custom format', 'filters' => [ + 'filter_align' => [ + 'status' => 1, + ], + 'filter_caption' => [ + 'status' => 1, + ], + 'filter_html_image_secure' => [ + 'status' => 1, + ], 'entity_embed' => [ 'status' => 1, ], @@ -81,10 +98,10 @@ protected function setUp() { $this->drupalLogin($this->webUser); // Create a sample node to be embedded. - $settings = array(); + $settings = []; $settings['type'] = 'page'; $settings['title'] = 'Embed Test Node'; - $settings['body'] = array('value' => 'This node is to be used for embedding in other nodes.', 'format' => 'custom_format'); + $settings['body'] = ['value' => 'This node is to be used for embedding in other nodes.', 'format' => 'custom_format']; $this->node = $this->drupalCreateNode($settings); } @@ -92,10 +109,11 @@ protected function setUp() { * Retrieves a sample file of the specified type. * * @return \Drupal\file\FileInterface + * The test file created. */ protected function getTestFile($type_name, $size = NULL) { // Get a file to upload. - $file = current($this->drupalGetTestFiles($type_name, $size)); + $file = current($this->getTestFiles($type_name, $size)); // Add a filesize property to files as would be read by // \Drupal\file\Entity\File::load(). @@ -107,15 +125,12 @@ protected function getTestFile($type_name, $size = NULL) { } /** - * + * Assert that the expected display plugins are available for the entity. */ public function assertAvailableDisplayPlugins(EntityInterface $entity, array $expected_plugins, $message = '') { $plugin_options = $this->container->get('plugin.manager.entity_embed.display') ->getDefinitionOptionsForEntity($entity); - // @todo Remove the sorting so we can actually test return order. - ksort($plugin_options); - sort($expected_plugins); - $this->assertEqual(array_keys($plugin_options), $expected_plugins, $message); + $this->assertEquals([], array_diff($expected_plugins, array_keys($plugin_options)), $message); } } diff --git a/web/modules/entity_embed/src/Tests/EntityEmbedTwigTest.php b/web/modules/entity_embed/tests/src/Functional/EntityEmbedTwigTest.php similarity index 74% rename from web/modules/entity_embed/src/Tests/EntityEmbedTwigTest.php rename to web/modules/entity_embed/tests/src/Functional/EntityEmbedTwigTest.php index 6ea1c13dc6..6ec214a652 100644 --- a/web/modules/entity_embed/src/Tests/EntityEmbedTwigTest.php +++ b/web/modules/entity_embed/tests/src/Functional/EntityEmbedTwigTest.php @@ -1,6 +1,8 @@ <?php -namespace Drupal\entity_embed\Tests; +namespace Drupal\Tests\entity_embed\Functional; + +use Drupal\entity_embed\Twig\EntityEmbedTwigExtension; /** * Tests Twig extension provided by entity_embed. @@ -10,25 +12,20 @@ class EntityEmbedTwigTest extends EntityEmbedTestBase { /** - * + * {@inheritdoc} */ protected function setUp() { parent::setUp(); - \Drupal::service('theme_handler')->install(array('test_theme')); + \Drupal::service('theme_installer')->install(['test_theme']); } /** * Tests that the provided Twig extension loads the service appropriately. */ public function testTwigExtensionLoaded() { - $twig_service = \Drupal::service('twig'); - - $ext = $twig_service->getExtension('entity_embed.twig.entity_embed_twig_extension'); - - // @todo why is the string - // 'Drupal\\entity_embed\\Twig\\EntityEmbedTwigExtension' - // and not '\Drupal\entity_embed\Twig\EntityEmbedTwigExtension' ? - $this->assertEqual(get_class($ext), 'Drupal\\entity_embed\\Twig\\EntityEmbedTwigExtension', 'Extension loaded successfully.'); + $ext = $this->container->get('twig')->getExtension(EntityEmbedTwigExtension::class); + $this->assertNotEmpty($ext); + $this->assertInstanceOf(EntityEmbedTwigExtension::class, $ext, 'Extension loaded successfully.'); } /** diff --git a/web/modules/entity_embed/src/Tests/EntityEmbedUpdateHookTest.php b/web/modules/entity_embed/tests/src/Functional/EntityEmbedUpdateHookTest.php similarity index 76% rename from web/modules/entity_embed/src/Tests/EntityEmbedUpdateHookTest.php rename to web/modules/entity_embed/tests/src/Functional/EntityEmbedUpdateHookTest.php index e8572b26ac..afa86b3e55 100644 --- a/web/modules/entity_embed/src/Tests/EntityEmbedUpdateHookTest.php +++ b/web/modules/entity_embed/tests/src/Functional/EntityEmbedUpdateHookTest.php @@ -1,8 +1,8 @@ <?php -namespace Drupal\entity_embed\Tests; +namespace Drupal\Tests\entity_embed\Functional; -use Drupal\system\Tests\Update\UpdatePathTestBase; +use Drupal\FunctionalTests\Update\UpdatePathTestBase; /** * Tests the update hooks in entity_embed module. @@ -16,8 +16,8 @@ class EntityEmbedUpdateHookTest extends UpdatePathTestBase { */ protected function setDatabaseDumpFiles() { $this->databaseDumpFiles = [ - DRUPAL_ROOT . '/core/modules/system/tests/fixtures/update/drupal-8.bare.standard.php.gz', - __DIR__ . '/../../tests/fixtures/update/entity_embed.update-hook-test.php', + $this->getDrupalRoot() . '/core/modules/system/tests/fixtures/update/drupal-8.8.0.bare.standard.php.gz', + __DIR__ . '/../../fixtures/update/entity_embed.update-hook-test.php', ]; } @@ -40,8 +40,8 @@ protected function setUp() { */ protected function doSelectionTest() { parent::doSelectionTest(); - $this->assertRaw('8002 - Updates the default mode settings.'); - $this->assertRaw('8003 - Updates allowed HTML for all filter format config entities that have an Entity Embed button.'); + $this->assertSession()->responseContains('8002 - Updates the default mode settings.'); + $this->assertSession()->responseContains('8003 - Updates allowed HTML for all filter configs that have an Entity Embed button.'); } /** @@ -58,7 +58,7 @@ public function todotestPostUpdate() { /** * Tests entity_embed_update_8003(). */ - public function testAllowedHTML() { + public function testAllowedAttributes() { $allowed_html = '<drupal-entity data-entity-type data-entity-uuid data-entity-embed-display data-entity-embed-settings data-align data-caption data-embed-button>'; $expected_allowed_html = '<drupal-entity data-entity-type data-entity-uuid data-entity-embed-display data-entity-embed-settings data-entity-embed-display-settings data-align data-caption data-embed-button>'; $filter_format = $this->container->get('entity_type.manager')->getStorage('filter_format')->load('full_html'); @@ -74,7 +74,7 @@ public function testAllowedHTML() { $this->runUpdates(); $filter_format = $this->container->get('entity_type.manager')->getStorage('filter_format')->load('full_html'); $filter_html = $filter_format->filters('filter_html'); - $this->assertEqual($expected_allowed_html, $filter_html->getConfiguration()['settings']['allowed_html'], 'Allowed html is correct'); + $this->assertEquals($expected_allowed_html, $filter_html->getConfiguration()['settings']['allowed_html'], 'Allowed html is correct'); } } diff --git a/web/modules/entity_embed/src/Tests/EntityReferenceFieldFormatterTest.php b/web/modules/entity_embed/tests/src/Functional/EntityReferenceFieldFormatterTest.php similarity index 68% rename from web/modules/entity_embed/src/Tests/EntityReferenceFieldFormatterTest.php rename to web/modules/entity_embed/tests/src/Functional/EntityReferenceFieldFormatterTest.php index 916578798a..397b2ecaba 100644 --- a/web/modules/entity_embed/src/Tests/EntityReferenceFieldFormatterTest.php +++ b/web/modules/entity_embed/tests/src/Functional/EntityReferenceFieldFormatterTest.php @@ -1,6 +1,6 @@ <?php -namespace Drupal\entity_embed\Tests; +namespace Drupal\Tests\entity_embed\Functional; use Drupal\Core\Form\FormState; @@ -19,17 +19,19 @@ class EntityReferenceFieldFormatterTest extends EntityEmbedTestBase { protected $menu; /** - * + * {@inheritdoc} */ protected function setUp() { parent::setUp(); // Add a new menu entity which does not has a view controller. - $this->menu = entity_create('menu', array( - 'id' => 'menu_name', - 'label' => 'Label', - 'description' => 'Description text', - )); + $this->menu = \Drupal::entityTypeManager() + ->getStorage('menu') + ->create([ + 'id' => 'menu_name', + 'label' => 'Label', + 'description' => 'Description text', + ]); $this->menu->save(); } @@ -60,33 +62,33 @@ public function testEntityReferenceFieldFormatter() { // Ensure that correct form attributes are returned for // 'entity_reference:entity_reference_entity_id' plugin. - $form = array(); + $form = []; $form_state = new FormState(); - $display = $this->container->get('plugin.manager.entity_embed.display')->createInstance('entity_reference:entity_reference_entity_id', array()); + $display = $this->container->get('plugin.manager.entity_embed.display')->createInstance('entity_reference:entity_reference_entity_id', []); $display->setContextValue('entity', $this->node); $conf_form = $display->buildConfigurationForm($form, $form_state); - $this->assertIdentical(array_keys($conf_form), array()); + $this->assertSame([], array_keys($conf_form)); // Ensure that correct form attributes are returned for // 'entity_reference:entity_reference_entity_view' plugin. - $form = array(); + $form = []; $form_state = new FormState(); - $display = $this->container->get('plugin.manager.entity_embed.display')->createInstance('entity_reference:entity_reference_entity_view', array()); + $display = $this->container->get('plugin.manager.entity_embed.display')->createInstance('entity_reference:entity_reference_entity_view', []); $display->setContextValue('entity', $this->node); $conf_form = $display->buildConfigurationForm($form, $form_state); - $this->assertIdentical($conf_form['view_mode']['#type'], 'select'); - $this->assertIdentical((string) $conf_form['view_mode']['#title'], 'View mode'); + $this->assertSame('select', $conf_form['view_mode']['#type']); + $this->assertSame('View mode', (string) $conf_form['view_mode']['#title']); // Ensure that correct form attributes are returned for // 'entity_reference:entity_reference_label' plugin. - $form = array(); + $form = []; $form_state = new FormState(); - $display = $this->container->get('plugin.manager.entity_embed.display')->createInstance('entity_reference:entity_reference_label', array()); + $display = $this->container->get('plugin.manager.entity_embed.display')->createInstance('entity_reference:entity_reference_label', []); $display->setContextValue('entity', $this->node); $conf_form = $display->buildConfigurationForm($form, $form_state); - $this->assertIdentical(array_keys($conf_form), array('link')); - $this->assertIdentical($conf_form['link']['#type'], 'checkbox'); - $this->assertIdentical((string) $conf_form['link']['#title'], 'Link label to the referenced entity'); + $this->assertSame(['link'], array_keys($conf_form)); + $this->assertSame('checkbox', $conf_form['link']['#type']); + $this->assertSame('Link label to the referenced entity', (string) $conf_form['link']['#title']); // Ensure that 'Rendered Entity' plugin is not available for an entity not // having a view controller. @@ -100,41 +102,43 @@ public function testEntityReferenceFieldFormatter() { public function testFilterEntityReferencePlugins() { // Test 'Label' Entity Embed Display plugin. $content = '<drupal-entity data-entity-type="node" data-entity-uuid="' . $this->node->uuid() . '" data-entity-embed-display="entity_reference:entity_reference_label" data-entity-embed-display-settings=\'{"link":1}\'>This placeholder should not be rendered.</drupal-entity>'; - $settings = array(); + $settings = []; $settings['type'] = 'page'; $settings['title'] = 'Test entity_reference:entity_reference_label Entity Embed Display plugin'; - $settings['body'] = array(array('value' => $content, 'format' => 'custom_format')); + $settings['body'] = [['value' => $content, 'format' => 'custom_format']]; $node = $this->drupalCreateNode($settings); $this->drupalGet('node/' . $node->id()); - $this->assertText($this->node->title->value, 'Title of the embedded node exists in page.'); - $this->assertNoText($this->node->body->value, 'Body of embedded node does not exists in page.'); - $this->assertNoText(strip_tags($content), 'Placeholder does not appears in the output when embed is successful.'); - $this->assertLinkByHref('node/' . $this->node->id(), 0, 'Link to the embedded node exists.'); + // Verify title of embedded node exists in page. + $this->assertSession()->responseContains($this->node->title->value); + // Verify body of embedded node does not exists in page. + $this->assertSession()->responseNotContains($this->node->body->value); + $this->assertSession()->responseNotContains('This placeholder should not be rendered.'); + $this->assertSession()->linkByHrefExists('node/' . $this->node->id(), 0, 'Link to the embedded node exists.'); // Test 'Entity ID' Entity Embed Display plugin. $content = '<drupal-entity data-entity-type="node" data-entity-uuid="' . $this->node->uuid() . '" data-entity-embed-display="entity_reference:entity_reference_entity_id">This placeholder should not be rendered.</drupal-entity>'; - $settings = array(); + $settings = []; $settings['type'] = 'page'; $settings['title'] = 'Test entity_reference:entity_reference_entity_id Entity Embed Display plugin'; - $settings['body'] = array(array('value' => $content, 'format' => 'custom_format')); + $settings['body'] = [['value' => $content, 'format' => 'custom_format']]; $node = $this->drupalCreateNode($settings); $this->drupalGet('node/' . $node->id()); - $this->assertText($this->node->id(), 'ID of the embedded node exists in page.'); - $this->assertNoText($this->node->title->value, 'Title of the embedded node does not exists in page.'); - $this->assertNoText($this->node->body->value, 'Body of embedded node does not exists in page.'); - $this->assertNoText(strip_tags($content), 'Placeholder does not appears in the output when embed is successful.'); - $this->assertNoLinkByHref('node/' . $this->node->id(), 'Link to the embedded node does not exists.'); + $this->assertSession()->responseContains($this->node->id()); + $this->assertSession()->responseNotContains($this->node->title->value); + $this->assertSession()->responseNotContains($this->node->body->value); + $this->assertSession()->responseNotContains('This placeholder should not be rendered.'); + $this->assertSession()->linkByHrefNotExists('node/' . $this->node->id(), 'Link to the embedded node does not exists.'); // Test 'Rendered entity' Entity Embed Display plugin. $content = '<drupal-entity data-entity-type="node" data-entity-uuid="' . $this->node->uuid() . '" data-entity-embed-display="entity_reference:entity_reference_entity_view" data-entity-embed-display-settings=\'{"view_mode":"teaser"}\'>This placeholder should not be rendered.</drupal-entity>'; - $settings = array(); + $settings = []; $settings['type'] = 'page'; $settings['title'] = 'Test entity_reference:entity_reference_label Entity Embed Display plugin'; - $settings['body'] = array(array('value' => $content, 'format' => 'custom_format')); + $settings['body'] = [['value' => $content, 'format' => 'custom_format']]; $node = $this->drupalCreateNode($settings); $this->drupalGet('node/' . $node->id()); - $this->assertText($this->node->body->value, 'Body of embedded node does not exists in page.'); - $this->assertNoText(strip_tags($content), 'Placeholder does not appears in the output when embed is successful.'); + $this->assertSession()->responseContains($this->node->body->value, 'Body of embedded node does not exists in page.'); + $this->assertSession()->responseNotContains('This placeholder should not be rendered.'); } } diff --git a/web/modules/entity_embed/src/Tests/FileFieldFormatterTest.php b/web/modules/entity_embed/tests/src/Functional/FileFieldFormatterTest.php similarity index 70% rename from web/modules/entity_embed/src/Tests/FileFieldFormatterTest.php rename to web/modules/entity_embed/tests/src/Functional/FileFieldFormatterTest.php index ba566c6b68..07b78da5b5 100644 --- a/web/modules/entity_embed/src/Tests/FileFieldFormatterTest.php +++ b/web/modules/entity_embed/tests/src/Functional/FileFieldFormatterTest.php @@ -1,6 +1,6 @@ <?php -namespace Drupal\entity_embed\Tests; +namespace Drupal\Tests\entity_embed\Functional; use Drupal\Component\Serialization\Json; use Drupal\Core\Form\FormState; @@ -27,7 +27,7 @@ class FileFieldFormatterTest extends EntityEmbedTestBase { protected $file; /** - * + * {@inheritdoc} */ protected function setUp() { parent::setUp(); @@ -49,36 +49,39 @@ public function testFileFieldFormatter() { // Ensure that correct form attributes are returned for the file field // formatter plugins. - $form = array(); + $form = []; $form_state = new FormState(); - $plugins = array( + $plugins = [ 'file:file_table', 'file:file_default', 'file:file_url_plain', - ); + ]; // Ensure that description field is available for all the 'file' plugins. foreach ($plugins as $plugin) { $display = $this->container->get('plugin.manager.entity_embed.display') ->createInstance($plugin, []); $display->setContextValue('entity', $this->file); $conf_form = $display->buildConfigurationForm($form, $form_state); - $this->assertIdentical(array_keys($conf_form), array('description')); - $this->assertIdentical($conf_form['description']['#type'], 'textfield'); - $this->assertIdentical((string) $conf_form['description']['#title'], 'Description'); + $this->assertArrayHasKey('description', $conf_form); + $this->assertSame('textfield', $conf_form['description']['#type']); + $this->assertSame('Description', (string) $conf_form['description']['#title']); } // Test entity embed using 'Generic file' Entity Embed Display plugin. - $embed_settings = array('description' => "This is sample description"); + $embed_settings = [ + 'description' => 'This is sample description', + ]; $content = '<drupal-entity data-entity-type="file" data-entity-uuid="' . $this->file->uuid() . '" data-entity-embed-display="file:file_default" data-entity-embed-display-settings=\'' . Json::encode($embed_settings) . '\'>This placeholder should not be rendered.</drupal-entity>'; - $settings = array(); + $settings = []; $settings['type'] = 'page'; $settings['title'] = 'Test entity embed with file:file_default'; - $settings['body'] = array(array('value' => $content, 'format' => 'custom_format')); + $settings['body'] = [['value' => $content, 'format' => 'custom_format']]; $node = $this->drupalCreateNode($settings); $this->drupalGet('node/' . $node->id()); - $this->assertText($embed_settings['description'], 'Description of the embedded file exists in page.'); - $this->assertNoText(strip_tags($content), 'Placeholder does not appears in the output when embed is successful.'); - $this->assertLinkByHref(file_create_url($this->file->getFileUri()), 0, 'Link to the embedded file exists.'); + // Verify description of the embedded file exists in page. + $this->assertSession()->responseContains($embed_settings['description']); + $this->assertSession()->responseNotContains('This placeholder should not be rendered.'); + $this->assertSession()->linkByHrefExists(file_create_url($this->file->getFileUri()), 0, 'Link to the embedded file exists.'); } } diff --git a/web/modules/entity_embed/src/Tests/ImageFieldFormatterTest.php b/web/modules/entity_embed/tests/src/Functional/ImageFieldFormatterTest.php similarity index 69% rename from web/modules/entity_embed/src/Tests/ImageFieldFormatterTest.php rename to web/modules/entity_embed/tests/src/Functional/ImageFieldFormatterTest.php index 19398c0987..d45d50624d 100644 --- a/web/modules/entity_embed/src/Tests/ImageFieldFormatterTest.php +++ b/web/modules/entity_embed/tests/src/Functional/ImageFieldFormatterTest.php @@ -1,6 +1,6 @@ <?php -namespace Drupal\entity_embed\Tests; +namespace Drupal\Tests\entity_embed\Functional; use Drupal\Component\Serialization\Json; use Drupal\Core\Form\FormState; @@ -34,7 +34,7 @@ class ImageFieldFormatterTest extends EntityEmbedTestBase { protected $file; /** - * + * {@inheritdoc} */ protected function setUp() { parent::setUp(); @@ -58,50 +58,53 @@ public function testImageFieldFormatter() { ]); // Ensure that correct form attributes are returned for the image plugin. - $form = array(); + $form = []; $form_state = new FormState(); $display = $this->container->get('plugin.manager.entity_embed.display') ->createInstance('image:image', []); $display->setContextValue('entity', $this->image); $conf_form = $display->buildConfigurationForm($form, $form_state); - $this->assertIdentical(array_keys($conf_form), array( + $expected = [ 'image_style', 'image_link', 'alt', 'title', - )); - $this->assertIdentical($conf_form['image_style']['#type'], 'select'); - $this->assertIdentical((string) $conf_form['image_style']['#title'], 'Image style'); - $this->assertIdentical($conf_form['image_link']['#type'], 'select'); - $this->assertIdentical((string) $conf_form['image_link']['#title'], 'Link image to'); - $this->assertIdentical($conf_form['alt']['#type'], 'textfield'); - $this->assertIdentical((string) $conf_form['alt']['#title'], 'Alternate text'); - $this->assertIdentical($conf_form['title']['#type'], 'textfield'); - $this->assertIdentical((string) $conf_form['title']['#title'], 'Title'); + ]; + $this->assertSame($expected, array_keys($conf_form)); + $this->assertSame('select', $conf_form['image_style']['#type']); + $this->assertSame('Image style', (string) $conf_form['image_style']['#title']); + $this->assertSame('select', $conf_form['image_link']['#type']); + $this->assertSame('Link image to', (string) $conf_form['image_link']['#title']); + $this->assertSame('textfield', $conf_form['alt']['#type']); + $this->assertSame('Alternate text', (string) $conf_form['alt']['#title']); + $this->assertSame('textfield', $conf_form['title']['#type']); + $this->assertSame('Title', (string) $conf_form['title']['#title']); // Test entity embed using 'Image' Entity Embed Display plugin. $alt_text = "This is sample description"; $title = "This is sample title"; - $embed_settings = array('image_link' => 'file'); + $embed_settings = ['image_link' => 'file']; $content = '<drupal-entity data-entity-type="file" data-entity-uuid="' . $this->image->uuid() . '" data-entity-embed-display="image:image" data-entity-embed-display-settings=\'' . Json::encode($embed_settings) . '\' alt="' . $alt_text . '" title="' . $title . '">This placeholder should not be rendered.</drupal-entity>'; - $settings = array(); + $settings = []; $settings['type'] = 'page'; $settings['title'] = 'Test entity embed with image:image'; - $settings['body'] = array(array('value' => $content, 'format' => 'custom_format')); + $settings['body'] = [['value' => $content, 'format' => 'custom_format']]; $node = $this->drupalCreateNode($settings); $this->drupalGet('node/' . $node->id()); - $this->assertRaw($alt_text, 'Alternate text for the embedded image is visible when embed is successful.'); - $this->assertNoText(strip_tags($content), 'Placeholder does not appears in the output when embed is successful.'); - $this->assertLinkByHref(file_create_url($this->image->getFileUri()), 0, 'Link to the embedded image exists.'); + // Verify alternate text for the embedded image is visible + // when embed is successful. + $this->assertSession()->responseContains($alt_text); + $this->assertSession()->responseNotContains('This placeholder should not be rendered.'); + $this->assertSession()->linkByHrefExists(file_create_url($this->image->getFileUri()), 0, 'Link to the embedded image exists.'); // Embed all three field types in one, to ensure they all render correctly. $content = '<drupal-entity data-entity-type="node" data-entity-uuid="' . $this->node->uuid() . '" data-entity-embed-display="entity_reference:entity_reference_label"></drupal-entity>'; $content .= '<drupal-entity data-entity-type="file" data-entity-uuid="' . $this->file->uuid() . '" data-entity-embed-display="file:file_default"></drupal-entity>'; $content .= '<drupal-entity data-entity-type="file" data-entity-uuid="' . $this->image->uuid() . '" data-entity-embed-display="image:image"></drupal-entity>'; - $settings = array(); + $settings = []; $settings['type'] = 'page'; $settings['title'] = 'Test node entity embedded first then a file entity'; - $settings['body'] = array(array('value' => $content, 'format' => 'custom_format')); + $settings['body'] = [['value' => $content, 'format' => 'custom_format']]; $node = $this->drupalCreateNode($settings); $this->drupalGet('node/' . $node->id()); } diff --git a/web/modules/entity_embed/tests/src/Functional/RecursionProtectionTest.php b/web/modules/entity_embed/tests/src/Functional/RecursionProtectionTest.php new file mode 100644 index 0000000000..d72290138b --- /dev/null +++ b/web/modules/entity_embed/tests/src/Functional/RecursionProtectionTest.php @@ -0,0 +1,84 @@ +<?php + +namespace Drupal\Tests\entity_embed\Functional; + +use Drupal\entity_embed\Plugin\Filter\EntityEmbedFilter; + +/** + * Tests recursive rendering protection. + * + * @group entity_embed + * + * @see \Drupal\Tests\entity_embed\Kernel\EntityEmbedFilterTest::testRecursionProtection + */ +class RecursionProtectionTest extends EntityEmbedTestBase { + + /** + * Tests self embedding. + */ + public function testSelfEmbedding() { + $node = $this->drupalCreateNode([ + 'type' => 'article', + 'title' => "Pirate Chinchilla LLama", + 'body' => [ + 'value' => 'temp', + 'format' => 'custom_format', + ], + ]); + $node->save(); + $content = '<div class="pirate">Ahoy, Matey!</div> <drupal-entity data-entity-type="node" data-entity-uuid="' . $node->uuid() . '" data-entity-embed-display="view_mode:node.full"></drupal-entity>'; + $node->set('body', [ + 'value' => $content, + 'format' => 'custom_format', + ]); + $node->save(); + $this->drupalGet('node/' . $node->id()); + $this->assertCount(EntityEmbedFilter::RECURSIVE_RENDER_LIMIT + 1, $this->getSession()->getPage()->findAll('xpath', '//div[@class="pirate"]')); + } + + /** + * Tests circular embedding. + */ + public function testCircularEmbedding() { + $node1 = $this->drupalCreateNode([ + 'type' => 'article', + 'title' => "Grandpa", + 'body' => [ + 'value' => 'temp', + 'format' => 'custom_format', + ], + ]); + $node1->save(); + $node2 = $this->drupalCreateNode([ + 'type' => 'article', + 'title' => "Son", + 'body' => [ + 'value' => 'temp', + 'format' => 'custom_format', + ], + ]); + $node2->save(); + $content = '<div class="node-2-embed">Embedded Son</div> <drupal-entity data-entity-type="node" data-entity-uuid="' . $node2->uuid() . '" data-view-mode="full"></drupal-entity>'; + $node1->set('body', [ + 'value' => $content, + 'format' => 'custom_format', + ]); + $node1->save(); + $content = '<div class="node-1-embed">Embedded Son who is own grandpa</div> <drupal-entity data-entity-type="node" data-entity-uuid="' . $node1->uuid() . '" data-view-mode="full"></drupal-entity>'; + $node2->set('body', [ + 'value' => $content, + 'format' => 'custom_format', + ]); + $node2->save(); + $this->drupalGet('node/' . $node1->id()); + $page = $this->getSession()->getPage(); + $this->assertCount(EntityEmbedFilter::RECURSIVE_RENDER_LIMIT, $page->findAll('xpath', '//div[@class="node-1-embed"]')); + $this->assertCount(EntityEmbedFilter::RECURSIVE_RENDER_LIMIT + 1, $page->findAll('xpath', '//div[@class="node-2-embed"]')); + + $this->drupalGet('node/' . $node2->id()); + $page = $this->getSession()->getPage(); + $this->assertCount(EntityEmbedFilter::RECURSIVE_RENDER_LIMIT + 1, $page->findAll('xpath', '//div[@class="node-1-embed"]')); + $this->assertCount(EntityEmbedFilter::RECURSIVE_RENDER_LIMIT, $page->findAll('xpath', '//div[@class="node-2-embed"]')); + } + +} diff --git a/web/modules/entity_embed/src/Tests/ViewModeFieldFormatterTest.php b/web/modules/entity_embed/tests/src/Functional/ViewModeFieldFormatterTest.php similarity index 59% rename from web/modules/entity_embed/src/Tests/ViewModeFieldFormatterTest.php rename to web/modules/entity_embed/tests/src/Functional/ViewModeFieldFormatterTest.php index 7712da9fc2..cab2e3f44f 100644 --- a/web/modules/entity_embed/src/Tests/ViewModeFieldFormatterTest.php +++ b/web/modules/entity_embed/tests/src/Functional/ViewModeFieldFormatterTest.php @@ -1,6 +1,6 @@ <?php -namespace Drupal\entity_embed\Tests; +namespace Drupal\Tests\entity_embed\Functional; use Drupal\Core\Form\FormState; @@ -31,7 +31,7 @@ public function testViewModeFieldFormatter() { ->createInstance($plugin, []); $display->setContextValue('entity', $this->node); $conf_form = $display->buildConfigurationForm($form, $form_state); - $this->assertIdentical(array_keys($conf_form), []); + $this->assertSame([], array_keys($conf_form)); } } @@ -49,8 +49,32 @@ public function testFilterViewModePlugins() { $this->drupalGet('node/' . $node->id()); $plugin = explode('.', $plugin); $view_mode = str_replace('_', '-', end($plugin)); - $this->assertRaw('node--view-mode-' . $view_mode, 'Node rendered in the correct view mode: ' . $view_mode . '.'); + $this->assertSession()->responseContains('node--view-mode-' . $view_mode, 'Node rendered in the correct view mode: ' . $view_mode . '.'); } } + /** + * Tests dependencies on EntityViewMode config entities. + */ + public function testViewModeDependencies() { + $button = $this->container + ->get('entity_type.manager') + ->getStorage('embed_button') + ->load('node'); + + $config = $button->get('type_settings'); + $config['display_plugins'] = ['view_mode:node.teaser']; + $button->set('type_settings', $config); + $button->save(); + $dependencies = $button->getDependencies(); + $this->assertContains('core.entity_view_mode.node.teaser', $dependencies['config']); + + // Test that removing teaser view mode removes the dependency. + $config['display_plugins'] = ['view_mode:node.full']; + $button->set('type_settings', $config); + $button->save(); + $dependencies = $button->getDependencies(); + $this->assertNotContains('core.entity_view_mode.node.teaser', $dependencies['config']); + } + } diff --git a/web/modules/entity_embed/tests/src/FunctionalJavascript/ButtonAdminTest.php b/web/modules/entity_embed/tests/src/FunctionalJavascript/ButtonAdminTest.php new file mode 100644 index 0000000000..4ed8503e86 --- /dev/null +++ b/web/modules/entity_embed/tests/src/FunctionalJavascript/ButtonAdminTest.php @@ -0,0 +1,138 @@ +<?php + +namespace Drupal\Tests\entity_embed\FunctionalJavascript; + +use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use Drupal\Tests\media\Traits\MediaTypeCreationTrait; + +/** + * Tests the creation and configuration of entity embed buttons. + * + * @group entity_embed + */ +class ButtonAdminTest extends WebDriverTestBase { + + use MediaTypeCreationTrait; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'node', + 'user', + 'media', + 'entity_embed', + ]; + + /** + * The user to use during testing. + * + * @var \Drupal\user\UserInterface + */ + protected $adminUser; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->container + ->get('entity_type.manager') + ->getStorage('entity_view_mode') + ->create([ + 'id' => 'media.thumb', + 'targetEntityType' => 'media', + ]) + ->save(); + + $this->createContentType([ + 'type' => 'article', + 'label' => 'Article', + ]); + + $this->createMediaType('image', [ + 'id' => 'image', + 'label' => 'Image', + ]); + + $this->adminUser = $this->drupalCreateUser([ + 'administer embed buttons', + ]); + + // Delete the existing node button provided by entity_embed module, so that + // we can create a button with the same machine name. + $this->container->get('entity_type.manager') + ->getStorage('embed_button') + ->load('node') + ->delete(); + } + + /** + * Tests the entity embed button administration functionality. + * + * @param string $entity_type_id + * The entity type ID as well as the label and machine name of the button. + * @param string $bundle_id + * The bundle to select, if provided. + * @param string $entity_embed_display_plugin_id + * The entity embed display plugin ID to select on the form. + * + * @dataProvider embedButtonAdminProvider + */ + public function testEmbedButtonAdmin($entity_type_id, $bundle_id, $entity_embed_display_plugin_id) { + $this->drupalLogin($this->adminUser); + + $this->drupalGet('admin/config/content/embed/button/add'); + + $page = $this->getSession()->getPage(); + $page->fillField('label', $entity_type_id); + $page->selectFieldOption('type_id', 'entity'); + $page->waitFor(10, function () use ($page) { + return $page->hasField('type_settings[entity_type]'); + }); + $page->selectFieldOption('type_settings[entity_type]', $entity_type_id); + $page->waitFor(10, function () use ($page, $bundle_id) { + return $page->hasField('type_settings[bundles][' . $bundle_id . ']'); + }); + $page->checkField('type_settings[display_plugins][' . $entity_embed_display_plugin_id . ']'); + $page->pressButton('Save'); + + $this->assertContains('The embed button ' . $entity_type_id . ' has been added.', $page->getText()); + $this->assertSession()->linkByHrefExists('/admin/config/content/embed/button/manage/' . $entity_type_id); + + $this->drupalGet('/admin/config/content/embed/button/manage/' . $entity_type_id); + + $page->findField('type_id')->hasAttribute('disabled'); + $page->findField('type_settings[entity_type]')->hasAttribute('disabled'); + } + + /** + * Data provider for ::testEmbedButtonAdmin(). + */ + public function embedButtonAdminProvider() { + return [ + 'article nodes embedded using teaser view mode' => [ + 'node', + 'article', + 'view_mode:node.teaser', + ], + 'users embedded using full view mode' => [ + 'user', + NULL, + 'view_mode:user.full', + ], + 'image media items embedded using thumb view mode' => [ + 'media', + 'image', + 'view_mode:media.thumb', + ], + 'files embedded using plain URL' => [ + 'file', + NULL, + 'file:file_url_plain', + ], + ]; + } + +} diff --git a/web/modules/entity_embed/tests/src/FunctionalJavascript/CKEditorIntegrationTest.php b/web/modules/entity_embed/tests/src/FunctionalJavascript/CKEditorIntegrationTest.php new file mode 100644 index 0000000000..c4d97df4f4 --- /dev/null +++ b/web/modules/entity_embed/tests/src/FunctionalJavascript/CKEditorIntegrationTest.php @@ -0,0 +1,303 @@ +<?php + +namespace Drupal\Tests\entity_embed\FunctionalJavascript; + +use Drupal\editor\Entity\Editor; +use Drupal\filter\Entity\FilterFormat; + +/** + * Tests ckeditor integration. + * + * @group entity_embed + */ +class CKEditorIntegrationTest extends EntityEmbedTestBase { + + use SortableTestTrait; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'field_ui', + 'node', + 'ckeditor', + 'views', + 'entity_embed', + 'entity_embed_test', + ]; + + /** + * The test button. + * + * @var Drupal\embed\Entity\EmbedButton + */ + protected $button; + + /** + * The test administrative user. + * + * @var \Drupal\user\UserInterface + */ + protected $adminUser; + + /** + * A test node to be used for embedding. + * + * @var \Drupal\node\NodeInterface + */ + protected $node; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->button = $this->container->get('entity_type.manager') + ->getStorage('embed_button') + ->load('node'); + $settings = $this->button->getTypeSettings(); + $settings['display_plugins'] = [ + 'entity_reference:entity_reference_label', + ]; + $this->button->set('type_settings', $settings); + $this->button->save(); + + $format = FilterFormat::create([ + 'format' => 'embed_test', + 'name' => 'Embed format', + 'filters' => [], + ]); + $format->save(); + + Editor::create([ + 'format' => 'embed_test', + 'editor' => 'ckeditor', + 'settings' => [ + 'toolbar' => [ + 'rows' => [ + [ + [ + 'name' => 'Tools', + 'items' => [ + 'Source', + 'Undo', + 'Redo', + ], + ], + ], + ], + ], + ], + ])->save(); + + // Create a page content type. + $this->drupalCreateContentType([ + 'type' => 'page', + 'name' => 'Basic page', + ]); + + $this->adminUser = $this->drupalCreateUser([ + 'access administration pages', + 'administer filters', + 'administer display modes', + 'administer embed buttons', + 'administer site configuration', + 'administer display modes', + 'administer content types', + 'administer node display', + 'access content', + 'create page content', + 'edit own page content', + $format->getPermissionName(), + ]); + + $this->drupalLogin($this->adminUser); + + // Create a sample node. + $this->drupalCreateNode([ + 'type' => 'page', + 'title' => 'Billy Bones', + 'body' => [ + 'value' => 'He lacks two fingers.', + ], + ]); + + $this->drupalCreateNode([ + 'type' => 'page', + 'title' => 'Long John Silver', + 'body' => [ + 'value' => 'A one-legged seafaring man.', + ], + ]); + } + + /** + * Test integration with Filter, Editor and Ckeditor. + */ + public function testIntegration() { + $this->drupalGet('admin/config/content/formats/manage/embed_test'); + + $page = $this->getSession()->getPage(); + + $page->checkField('filters[entity_embed][status]'); + $page->checkField('filters[filter_html][status]'); + + // Add "Embeds" toolbar button group to the active toolbar. + $this->assertSession()->buttonExists('Show group names')->press(); + $this->assertSession()->waitForElementVisible('css', '.ckeditor-add-new-group'); + $this->assertSession()->buttonExists('Add group')->press(); + $this->assertSession()->waitForElementVisible('css', '[name="group-name"]')->setValue('Embeds'); + $this->assertSession()->buttonExists('Apply')->press(); + + // Verify the <drupal-entity> tag is not yet allowed. + $allowed_html = $this->assertSession()->fieldExists('filters[filter_html][settings][allowed_html]')->getValue(); + $this->assertNotContains('drupal-entity', $allowed_html); + + // Verify that after dragging the Entity Embed CKEditor plugin button into + // the active toolbar, the <drupal-entity> tag is allowed, as well as some + // attributes. + $item = 'li[data-drupal-ckeditor-button-name="' . $this->button->id() . '"]'; + $from = "ul $item"; + $target = 'ul.ckeditor-toolbar-group-buttons'; + + $this->assertSession()->waitForElementVisible('css', $target); + $this->sortableTo($item, $from, $target); + $allowed_html_updated = $this->assertSession() + ->fieldExists('filters[filter_html][settings][allowed_html]') + ->getValue(); + $this->assertContains('drupal-entity data-entity-type data-entity-uuid data-entity-embed-display data-entity-embed-display-settings data-align data-caption data-embed-button', $allowed_html_updated); + + $this->assertSession()->buttonExists('Save configuration')->press(); + $this->assertSession()->responseContains('The text format <em class="placeholder">Embed format</em> has been updated.'); + $filterFormat = $this->container->get('entity_type.manager') + ->getStorage('filter_format') + ->load('embed_test'); + + $settings = $filterFormat->filters('filter_html')->settings; + $allowed_html = $settings['allowed_html']; + + $this->assertContains('drupal-entity data-entity-type data-entity-uuid data-entity-embed-display data-entity-embed-display-settings data-align data-caption data-embed-button', $allowed_html); + + // Verify that the Entity Embed button shows up and results in an + // operational entity embedding experience in the text editor. + $this->drupalGet('/node/add/page'); + $this->waitForEditor(); + $this->assertSame(1, $this->getCkeditorUndoSnapshotCount()); + $this->getSession()->executeScript("CKEDITOR.instances['edit-body-0-value'].setData('<p>Goodbye world!</p>');"); + $this->assertSame(2, $this->getCkeditorUndoSnapshotCount()); + $this->assignNameToCkeditorIframe(); + $this->getSession()->switchToIFrame('ckeditor'); + $this->assertSession()->responseContains('entity_embed.editor.css'); + $this->assertSession()->responseContains('hidden.module.css'); + $this->assertSession()->pageTextNotContains('Billy Bones'); + $this->pressEditorButton($this->button->id()); + $this->assertSession()->waitForId('drupal-modal'); + $this->assertSession() + ->fieldExists('entity_id') + ->setValue('Billy Bones (1)'); + $this->assertSession()->elementExists('css', 'button.js-button-next')->click(); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->assertSession()->responseContains('Selected entity'); + $this->assertSession()->responseContains('Billy Bones'); + $this->assertSession()->elementExists('css', 'button.button--primary')->press(); + $this->assertSession()->assertWaitOnAjaxRequest(); + // Verify that the embedded entity gets a preview inside the text editor. + $this->getSession()->switchToIFrame('ckeditor'); + $this->assertSession()->pageTextContains('Billy Bones'); + $this->getSession()->switchToIFrame(); + $this->assertSame(3, $this->getCkeditorUndoSnapshotCount()); + $this->getSession() + ->getPage() + ->find('css', 'input[name="title[0][value]"]') + ->setValue('Pirates'); + // Verify that undo/redo work. + $this->assertCkeditorUndoOrRedo('undo', ['Goodbye world!'], ['Billy Bones']); + $this->assertCkeditorUndoOrRedo('undo', [], ['Billy Bones', 'Goodbye world!']); + $this->assertCkeditorUndoOrRedo('redo', ['Goodbye world!'], ['Billy Bones']); + $this->assertCkeditorUndoOrRedo('redo', ['Billy Bones', 'Goodbye world!'], []); + // Verify that the embedded entity is rendered by the filter for end users. + $this->assertSession()->buttonExists('Save')->press(); + $this->assertSession()->responseContains('Billy Bones'); + + $this->drupalGet('/node/3/edit'); + $this->assignNameToCkeditorIframe(); + + // Verify that the text editor previews the current embedded entity. + $this->getSession()->switchToIFrame('ckeditor'); + $this->assertSession()->waitForText('Billy Bones'); + $this->getSession()->switchToIFrame(); + + // Test opening the dialog and switching embedded nodes. + $this->reopenDialog(); + + $this->assertSession() + ->waitForElementVisible('css', 'div.ui-dialog-buttonset') + ->findButton('Back') + ->click(); + + $this->assertSession()->assertWaitOnAjaxRequest(); + + $this->assertSession() + ->fieldExists('entity_id') + ->setValue('Long John Silver (2)'); + + $this->assertSession()->elementExists('css', 'button.js-button-next')->click(); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->assertSession()->responseContains('Selected entity'); + $this->assertSession()->responseContains('Long John Silver'); + $this->assertSession()->elementExists('css', 'button.button--primary')->press(); + $this->assertSession()->assertWaitOnAjaxRequest(); + + // Verify that the text editor previews the updated embedded entity. + $this->getSession()->switchToIFrame('ckeditor'); + $this->assertSession()->waitForText('Long John Silver'); + $this->getSession()->switchToIFrame(); + $this->assertSession()->buttonExists('Save')->press(); + // Verify that the embedded entity is rendered by the filter for end users. + $this->assertSession()->responseContains('Long John Silver'); + } + + /** + * Asserts the consequences of CKEditor undo/redo actions. + * + * @param string $action + * Either 'undo' or 'redo'. + * @param array $contains + * The strings the CKEditor instance is expected to contain. + * @param array $not_contains + * The strings the CKEditor instance is expected to not contain. + * + * @throws \Behat\Mink\Exception\ElementNotFoundException + * @throws \Behat\Mink\Exception\ResponseTextException + */ + protected function assertCkeditorUndoOrRedo($action, array $contains, array $not_contains) { + if ($action !== 'undo' && $action !== 'redo') { + throw new \LogicException(); + } + $this->pressEditorButton($action); + $this->getSession()->switchToIFrame('ckeditor'); + foreach ($contains as $string) { + $this->assertSession()->pageTextContains($string); + } + foreach ($not_contains as $string) { + $this->assertSession()->pageTextNotContains($string); + } + $this->getSession()->switchToIFrame(); + } + + /** + * Get a CKEditor instance's undo snapshot count. + * + * @param string $instance_id + * The CKEditor instance ID. + * + * @return int + * The undo snapshot count. + */ + protected function getCkeditorUndoSnapshotCount($instance_id = 'edit-body-0-value') { + $this->waitForEditor($instance_id); + return $this->getSession()->evaluateScript("CKEDITOR.instances['$instance_id'].undoManager.snapshots.length"); + } + +} diff --git a/web/modules/entity_embed/tests/src/FunctionalJavascript/ConfigurationUiTest.php b/web/modules/entity_embed/tests/src/FunctionalJavascript/ConfigurationUiTest.php new file mode 100644 index 0000000000..319863fbfe --- /dev/null +++ b/web/modules/entity_embed/tests/src/FunctionalJavascript/ConfigurationUiTest.php @@ -0,0 +1,266 @@ +<?php + +namespace Drupal\Tests\entity_embed\FunctionalJavascript; + +use Drupal\editor\Entity\Editor; +use Drupal\filter\Entity\FilterFormat; + +/** + * Tests text format & text editor configuration UI validation. + * + * @group entity_embed + */ +class ConfigurationUiTest extends EntityEmbedTestBase { + + use SortableTestTrait; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'ckeditor', + 'entity_embed', + ]; + + /** + * The test administrative user. + * + * @var \Drupal\user\UserInterface + */ + protected $adminUser; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $format = FilterFormat::create([ + 'format' => 'embed_test', + 'name' => 'Embed format', + 'filters' => [], + ]); + $format->save(); + + Editor::create([ + 'format' => $format->id(), + 'editor' => 'ckeditor', + 'settings' => [ + 'toolbar' => [ + 'rows' => [ + [ + [ + 'name' => 'Tools', + 'items' => [ + 'Source', + 'Undo', + 'Redo', + ], + ], + ], + ], + ], + ], + ])->save(); + + $this->adminUser = $this->drupalCreateUser([ + 'administer filters', + $format->getPermissionName(), + ]); + + $this->drupalLogin($this->adminUser); + } + + /** + * Test integration with Filter and Text Editor form validation. + * + * @param bool $filter_html_status + * Whether to enable filter_html. + * @param bool $entity_embed_status + * Whether to enabled entity_embed. + * @param string|false $allowed_html + * The allowed HTML to set. Set to 'default' to test 'drupal-entity' is + * missing or FALSE to leave the properly alone. + * @param string $expected_error_message + * The error message that should display. + * + * @dataProvider providerTestValidations + * @dataProvider providerTestValidationWhenAdding + */ + public function testValidationWhenAdding($filter_html_status, $entity_embed_status, $allowed_html, $expected_error_message) { + $this->drupalGet('admin/config/content/formats/add'); + + // Enable the `filter_html` and `entity_embed` filters, and select CKEditor + // as the text editor. + $page = $this->getSession()->getPage(); + $page->fillField('name', 'Test Format'); + $this->showHiddenFields(); + $page->findField('format')->setValue('test_format'); + + if ($filter_html_status) { + $page->checkField('filters[filter_html][status]'); + } + if ($entity_embed_status) { + $page->checkField('filters[entity_embed][status]'); + } + $page->selectFieldOption('editor[editor]', 'ckeditor'); + + // Verify that after dragging the Entity Embed CKEditor plugin button into + // the active toolbar, the <drupal-entity> tag is allowed, as well as some + // attributes. + $item = 'li[data-drupal-ckeditor-button-name="test_media_entity_embed"]'; + $from = "ul $item"; + $target = 'ul.ckeditor-toolbar-group-buttons'; + + $this->assertSession()->waitForElementVisible('css', $target); + $this->sortableTo($item, $from, $target); + + if ($allowed_html == 'default' && $entity_embed_status) { + // Unfortunately the <drupal-entity> tag is not yet allowed due to + // https://www.drupal.org/project/drupal/issues/2763075. + $allowed_html = $this->assertSession()->fieldExists('filters[filter_html][settings][allowed_html]')->getValue(); + $this->assertNotContains('drupal-entity', $allowed_html); + } + elseif (!empty($allowed_html)) { + $page->fillField('filters[filter_html][settings][allowed_html]', $allowed_html); + } + + $this->assertSession()->buttonExists('Save configuration')->press(); + + if ($expected_error_message) { + $this->assertSession()->pageTextNotContains('Added text format Test Format.'); + $this->assertSession()->pageTextContains($expected_error_message); + } + else { + $this->assertSession()->pageTextContains('Added text format Test Format.'); + } + } + + /** + * Data provider for testValidationWhenAdding(). + */ + public function providerTestValidationWhenAdding() { + return [ + 'Tests validation when drupal-entity not added.' => [ + 'filters[filter_html][status]' => TRUE, + 'filters[entity_embed][status]' => TRUE, + 'allowed_html' => 'default', + 'expected_error_message' => 'The Media Entity Embed button requires <drupal-entity> among the allowed HTML tags.', + ], + ]; + } + + /** + * Test integration with Filter and Text Editor form validation. + * + * @param bool $filter_html_status + * Whether to enable filter_html. + * @param bool $entity_embed_status + * Whether to enabled entity_embed. + * @param string $allowed_html + * The allowed HTML to set. Set to 'default' to test 'drupal-entity' is + * present or FALSE to leave the properly alone. + * @param string $expected_error_message + * The error message that should display. + * + * @dataProvider providerTestValidations + * @dataProvider providerTestValidationWhenEditing + */ + public function testValidationWhenEditing($filter_html_status, $entity_embed_status, $allowed_html, $expected_error_message) { + $this->drupalGet('admin/config/content/formats/manage/embed_test'); + + // Enable the `filter_html` and `entity_embed` filters, and select CKEditor + // as the text editor. + $page = $this->getSession()->getPage(); + + if ($filter_html_status) { + $page->checkField('filters[filter_html][status]'); + } + if ($entity_embed_status) { + $page->checkField('filters[entity_embed][status]'); + } + $page->selectFieldOption('editor[editor]', 'ckeditor'); + + // Verify that after dragging the Entity Embed CKEditor plugin button into + // the active toolbar, the <drupal-entity> tag is allowed, as well as some + // attributes. + $item = 'li[data-drupal-ckeditor-button-name="test_media_entity_embed"]'; + $from = "ul $item"; + $target = 'ul.ckeditor-toolbar-group-buttons'; + + $this->assertSession()->waitForElementVisible('css', $target); + $this->sortableTo($item, $from, $target); + + if ($allowed_html == 'default' && $entity_embed_status) { + $allowed_html = $this->assertSession()->fieldExists('filters[filter_html][settings][allowed_html]')->getValue(); + $this->assertContains('drupal-entity', $allowed_html); + } + elseif (!empty($allowed_html)) { + $page->fillField('filters[filter_html][settings][allowed_html]', $allowed_html); + } + + $this->assertSession()->buttonExists('Save configuration')->press(); + + if ($expected_error_message) { + $this->assertSession()->pageTextNotContains('The text format Embed format has been updated.'); + $this->assertSession()->pageTextContains($expected_error_message); + } + else { + $this->assertSession()->pageTextContains('The text format Embed format has been updated.'); + } + } + + /** + * Data provider for testValidationWhenEditing(). + */ + public function providerTestValidationWhenEditing() { + return [ + 'Tests validation when drupal-entity not added.' => [ + 'filters[filter_html][status]' => TRUE, + 'filters[entity_embed][status]' => TRUE, + 'allowed_html' => 'default', + 'expected_error_message' => FALSE, + ], + ]; + } + + /** + * Data provider for testValidationWhenAdding() and + * testValidationWhenEditing(). + */ + public function providerTestValidations() { + return [ + 'Tests that no filter_html occurs when filter_html not enabled.' => [ + 'filters[filter_html][status]' => FALSE, + 'filters[entity_embed][status]' => TRUE, + 'allowed_html' => FALSE, + 'expected_error_message' => FALSE, + ], + 'Tests validation when both filter_html and entity_embed are disabled.' => [ + 'filters[filter_html][status]' => FALSE, + 'filters[entity_embed][status]' => FALSE, + 'allowed_html' => FALSE, + 'expected_error_message' => FALSE, + ], + 'Tests validation when entity_embed filter not enabled and filter_html is enabled.' => [ + 'filters[filter_html][status]' => TRUE, + 'filters[entity_embed][status]' => FALSE, + 'allowed_html' => 'default', + 'expected_error_message' => FALSE, + ], + 'Tests validation when drupal-entity element has no attributes.' => [ + 'filters[filter_html][status]' => TRUE, + 'filters[entity_embed][status]' => TRUE, + 'allowed_html' => "<a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type='1 A I'> <li> <dl> <dt> <dd> <h2 id='jump-*'> <h3 id> <h4 id> <h5 id> <h6 id> <drupal-entity>", + 'expected_error_message' => 'The <drupal-entity> tag in the allowed HTML tags is missing the following attributes: data-entity-type, data-entity-uuid, data-entity-embed-display, data-entity-embed-display-settings, data-align, data-caption, data-embed-button, alt, title.', + ], + 'Tests validation when drupal-entity element lacks some required attributes.' => [ + 'filters[filter_html][status]' => TRUE, + 'filters[entity_embed][status]' => TRUE, + 'allowed_html' => "<a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type='1 A I'> <li> <dl> <dt> <dd> <h2 id='jump-*'> <h3 id> <h4 id> <h5 id> <h6 id> <drupal-entity data-entity-type data-entity-uuid data-entity-embed-display data-entity-embed-display-settings data-align data-embed-button data-langcode>", + 'expected_error_message' => 'The <drupal-entity> tag in the allowed HTML tags is missing the following attributes: data-caption, alt, title.', + ], + ]; + } + +} diff --git a/web/modules/entity_embed/tests/src/FunctionalJavascript/ContentTranslationTest.php b/web/modules/entity_embed/tests/src/FunctionalJavascript/ContentTranslationTest.php new file mode 100644 index 0000000000..5e9749a0b9 --- /dev/null +++ b/web/modules/entity_embed/tests/src/FunctionalJavascript/ContentTranslationTest.php @@ -0,0 +1,344 @@ +<?php + +namespace Drupal\Tests\entity_embed\FunctionalJavascript; + +use Drupal\Component\Utility\Html; +use Drupal\file\Entity\File; +use Drupal\media\Entity\Media; + +/** + * Test integration with content_translation. + * + * @group entity_embed + */ +class ContentTranslationTest extends EntityEmbedTestBase { + + /** + * The 'translator' user to use during testing. + * + * @var \Drupal\user\UserInterface + */ + protected $translator; + + /** + * {@inheritdoc} + */ + public static $modules = ['entity_embed_translation_test']; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->translator = $this->drupalCreateUser([ + 'use text format full_html', + 'administer nodes', + 'edit any article content', + 'translate any entity', + ]); + + $this->config('field.storage.node.body') + ->set('translatable', TRUE) + ->save(); + } + + /** + * Return autocomplete suggestions from the entity_id field. + * + * @param string $search_string + * The search string. + * + * @return string + * The text of the autocomplete suggestions. + */ + protected function getAutocompleteSuggestions($search_string) { + $page = $this->getSession()->getPage(); + $autocomplete_field = $field = $page->findField('entity_id'); + $this->assertNotEmpty($autocomplete_field); + $autocomplete_field->setValue($search_string); + $this->getSession()->getDriver()->keyDown($autocomplete_field->getXpath(), ' '); + $this->assertSession()->waitOnAutocomplete(); + $suggestions = $this->assertSession() + ->waitForElementVisible('css', '.ui-autocomplete'); + $this->assertNotEmpty($suggestions); + return $suggestions->getText(); + } + + /** + * Tests the host entity's langcode is available in EntityEmbedDialog. + */ + public function testHostEntityLangcode() { + $node = $this->createNode([ + 'type' => 'article', + 'title' => 'Clark Kent', + ]); + $node_fr = $node->addTranslation('fr'); + $node_fr->title = 'Superhomme'; + $node_fr->save(); + + \Drupal::service('file_system')->copy($this->root . '/core/misc/druplicon.png', 'public://Smeagol.jpg'); + /** @var \Drupal\file\FileInterface $file */ + $file = File::create([ + 'uri' => 'public://Smeagol.jpg', + 'uid' => $this->translator->id(), + ]); + $file->save(); + + $media = Media::create([ + 'bundle' => 'image', + 'name' => 'Smeagol likes cheese', + 'field_media_image' => [ + [ + 'target_id' => $file->id(), + 'alt' => 'Smeagol likes cheese alt', + 'title' => 'Smeagol likes cheese title', + ], + ], + ]); + $media->save(); + + $media_fr = $media->addTranslation('fr'); + $media_fr->name = "Gollum n'aime que la bague"; + $media_fr->field_media_image->setValue([ + [ + 'target_id' => $file->id(), + 'alt' => "Gollum n'aime que la bague alt", + 'title' => "Gollum n'aime que la bague title", + ], + ]); + $media_fr->save(); + + $host = $this->createNode([ + 'type' => 'article', + 'title' => 'host', + 'body' => [ + 'value' => '', + 'format' => 'full_html', + ], + ]); + $host_fr = $host->addTranslation('fr'); + $host_fr->title = 'host'; + $host_fr->body->value = ''; + $host_fr->body->format = 'full_html'; + $host_fr->body->lang = 'fr'; + $host_fr->save(); + + // Test the default language, as a baseline for comparison. + $this->drupalLogin($this->translator); + $this->drupalGet('node/' . $host->id() . '/edit'); + $this->waitForEditor(); + $this->pressEditorButton('test_node'); + $this->assertNotEmpty($this->assertSession()->waitForElementVisible('css', '#entity-embed-dialog-form')); + + // Assert autocomplete suggestions are in host entity language (en). + $suggestions = $this->getAutocompleteSuggestions('clar'); + $this->assertContains('Clark Kent', $suggestions); + + // Assert autocomplete does not show suggestions for translations not + // matching the host entity language. + $suggestions = $this->getAutocompleteSuggestions('super'); + $this->assertNotContains('Superhomme', $suggestions); + + // Select the suggestion matching the host entity language, and proceed to + // the review step. + $this->assertSession() + ->fieldExists('entity_id') + ->setValue('Clark Kent (1)'); + $this->assertSession()->elementExists('css', 'button.js-button-next')->click(); + $form = $this->assertSession()->waitForElementVisible('css', 'form.entity-embed-dialog-step--embed'); + + // Assert that the review step displays the selected entity with the label + // in the host language. + $text = $form->getText(); + $this->assertContains('Clark Kent', $text); + $this->assertNotContains('Superhomme', $text); + + // Repeat the same test pattern, but now for a Media entity instead of Node. + $this->getSession()->reload(); + $this->assertSession() + ->waitForElementVisible('css', 'a.cke_button__test_media_entity_embed') + ->click(); + $this->assertSession()->waitForId('drupal-modal'); + + // Assert autocomplete suggestions are in host entity language (en). + $suggestions = $this->getAutocompleteSuggestions('Smeagol likes cheese'); + $this->assertContains('Smeagol likes cheese', $suggestions); + + // Assert autocomplete does not show suggestions for translations not + // matching the host entity language. + $suggestions = $this->getAutocompleteSuggestions("Gollum n'aime que la bague"); + $this->assertNotContains("Gollum n'aime que la bague", $suggestions); + + // Select the suggestion matching the host entity language, and proceed to + // the review step. + $this->assertSession() + ->fieldExists('entity_id') + ->setValue('Smeagol likes cheese (1)'); + $this->assertSession()->elementExists('css', 'button.js-button-next')->click(); + $form = $this->assertSession()->waitForElementVisible('css', 'form.entity-embed-dialog-step--embed'); + + // Assert that the review step displays the selected entity with the label + // in the host language. + $text = $form->getText(); + $this->assertContains('Smeagol likes cheese', $text); + $this->assertNotContains("Gollum n'aime que la bague", $text); + + // Get translation of host entity. + $this->drupalGet('/fr/node/' . $host->id() . '/edit'); + $this->waitForEditor(); + $this->pressEditorButton('test_node'); + $this->assertNotEmpty($this->assertSession()->waitForElementVisible('css', '#entity-embed-dialog-form')); + + // Assert autocomplete suggestions are in host entity language (fr). + $suggestions = $this->getAutocompleteSuggestions('super'); + $this->assertContains('Superhomme', $suggestions); + + // Assert autocomplete does not show suggestions for translations not + // matching the host entity language. + $suggestions = $this->getAutocompleteSuggestions('clark'); + $this->assertNotContains('Clark Kent', $suggestions); + + // Select the suggestion matching the host entity language, and proceed to + // the review step. + $this->assertSession() + ->fieldExists('entity_id') + ->setValue('Superhomme (1)'); + $this->assertSession()->elementExists('css', 'button.js-button-next')->click(); + $form = $this->assertSession()->waitForElementVisible('css', 'form.entity-embed-dialog-step--embed'); + + // Assert the translated label appears, not the original. + $text = $form->getText(); + $this->assertContains('Superhomme', $text); + $this->assertNotContains('Clark Kent', $text); + + // Choose to display as label without link. + $this->assertSession() + ->selectExists('attributes[data-entity-embed-display]') + ->setValue('entity_reference:entity_reference_label'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->assertSession() + ->fieldExists('attributes[data-entity-embed-display-settings][link]') + ->uncheck(); + $this->assertSession()->elementExists('css', 'button.button--primary')->press(); + $this->assertSession()->assertWaitOnAjaxRequest(); + + // Verify that the embedded entity preview in CKEditor displays the label in + // the correct language (fr). + $this->assignNameToCkeditorIframe(); + $this->getSession()->switchToIFrame('ckeditor'); + $this->assertSession()->pageTextContains('Superhomme'); + + // Repeat the same test pattern, but now for a Media entity instead of Node. + $this->getSession()->reload(); + $this->assertSession() + ->waitForElementVisible('css', 'a.cke_button__test_media_entity_embed') + ->click(); + $this->assertSession()->waitForId('drupal-modal'); + + // Assert autocomplete suggestions are in host entity language (fr). + $suggestions = $this->getAutocompleteSuggestions("Gollum n'aime que la bague"); + $this->assertContains("Gollum n'aime que la bague", $suggestions); + + // Assert autocomplete does not show suggestions for translations not + // matching the host entity language. + $suggestions = $this->getAutocompleteSuggestions('Smeagol likes cheese'); + $this->assertNotContains('Smeagol likes cheese', $suggestions); + + // Select the suggestion matching the host entity language, and proceed to + // the review step. + $this->assertSession() + ->fieldExists('entity_id') + ->setValue("Gollum n'aime que la bague (1)"); + $this->assertSession()->elementExists('css', 'button.js-button-next')->click(); + $form = $this->assertSession()->waitForElementVisible('css', 'form.entity-embed-dialog-step--embed'); + + // Assert the translated label appears, not the original. + $text = $form->getText(); + $this->assertContains("Gollum n'aime que la bague", $text); + $this->assertNotContains('Smeagol likes cheese', $text); + + // Choose to display as thumbnail with 'medium' image style. + $this->assertSession() + ->selectExists('attributes[data-entity-embed-display]') + ->setValue('entity_reference:media_thumbnail'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->assertSession() + ->selectExists('attributes[data-entity-embed-display-settings][image_style]') + ->setValue('medium'); + $this->assertSession()->elementExists('css', 'button.button--primary')->press(); + $this->assertSession()->assertWaitOnAjaxRequest(); + + // Verify that the embedded entity preview in CKEditor displays the image + // with an `alt` attribute in the correct language (fr). + $this->assignNameToCkeditorIframe(); + $this->getSession()->switchToIFrame('ckeditor'); + $img = $this->assertSession()->elementExists('css', 'img'); + $this->assertContains('Smeagol.jpg', $img->getAttribute('src')); + $this->assertEquals("Gollum n'aime que la bague alt", $img->getAttribute('alt')); + + // Save the host entity, verify that it also shows up the same way on the + // front end, so again with an `alt` attribute in the correct language (fr). + // This tests the filter plugin's integration. + $this->getSession()->switchToIFrame(); + $this->assertSession()->buttonExists('Save')->press(); + $img = $this->assertSession()->elementExists('css', 'img'); + $this->assertContains('Smeagol.jpg', $img->getAttribute('src')); + $this->assertEquals("Gollum n'aime que la bague alt", $img->getAttribute('alt')); + + // Verify that editing the host entity and then triggering the Entity Embed + // Dialog for the embedded entity again shows the embedded entity in the + // same language (fr). + $this->drupalGet('/fr/node/' . $host->id() . '/edit'); + $this->waitForEditor(); + $select_and_edit_embed = "var editor = CKEDITOR.instances['edit-body-0-value']; + var entityEmbed = editor.widgets.getByElement(editor.editable().findOne('div')); + entityEmbed.focus(); + editor.execCommand('editdrupalentity');"; + $this->getSession()->executeScript($select_and_edit_embed); + $this->assertSession()->assertWaitOnAjaxRequest(); + $form = $this->assertSession()->waitForElementVisible('css', 'form.entity-embed-dialog-step--embed'); + $text = $form->getText(); + $this->assertContains("Gollum n'aime que la bague", $text); + $this->assertNotContains('Smeagol likes cheese', $text); + + // Close the Entity Embed Dialog, and enter CKEditor's "source" mode. + $this->assertSession()->elementExists('css', '.ui-dialog-titlebar-close')->press(); + $this->assertSession() + ->waitForElementVisible('css', 'a.cke_button__source') + ->click(); + + // Manually override the langcode to set it back to 'en', so that that the + // embed shows the original language, even though this node is translated. + $source = $this->assertSession() + ->waitForElementVisible('xpath', "//textarea[contains(@class, 'cke_source')]"); + $value = $source->getValue(); + $dom = Html::load($value); + $xpath = new \DOMXPath($dom); + $drupal_entity = $xpath->query('//drupal-entity')[0]; + $drupal_entity->setAttribute("data-langcode", "en"); + $source->setValue(Html::serialize($dom)); + + // Exit "source" mode. + $this->pressEditorButton('source'); + $this->assignNameToCkeditorIframe(); + $this->getSession()->switchToIFrame('ckeditor'); + + // Assert that the image appears with correct alt text (en). + $img = $this->assertSession()->waitForElementVisible('css', 'img'); + $this->assertContains('Smeagol.jpg', $img->getAttribute('src')); + $this->assertEquals("Smeagol likes cheese alt", $img->getAttribute('alt')); + + // Save the host entity, verify that it also shows up the same way on the + // front end, so again with an `alt` attribute in the correct language (en). + // This tests the filter plugin's integration. + $this->getSession()->switchToIFrame(); + $this->assertSession()->buttonExists('Save')->press(); + + // Assert that the image appears with correct alt text. + $img = $this->assertSession()->elementExists('css', 'img'); + $this->assertContains('Smeagol.jpg', $img->getAttribute('src')); + $this->assertEquals("Smeagol likes cheese alt", $img->getAttribute('alt')); + } + +} diff --git a/web/modules/entity_embed/tests/src/FunctionalJavascript/EntityEmbedDialogTest.php b/web/modules/entity_embed/tests/src/FunctionalJavascript/EntityEmbedDialogTest.php new file mode 100644 index 0000000000..8008cdc52f --- /dev/null +++ b/web/modules/entity_embed/tests/src/FunctionalJavascript/EntityEmbedDialogTest.php @@ -0,0 +1,146 @@ +<?php + +namespace Drupal\Tests\entity_embed\FunctionalJavascript; + +use Drupal\editor\Entity\Editor; +use Drupal\filter\Entity\FilterFormat; + +/** + * Tests the entity_embed dialog controller and route. + * + * @group entity_embed + * @requires function Drupal\FunctionalJavascriptTests\WebDriverTestBase::assertSession + */ +class EntityEmbedDialogTest extends EntityEmbedTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = ['image']; + + /** + * The test user. + * + * @var \Drupal\user\UserInterface + */ + protected $webUser; + + /** + * A test node to be used for embedding. + * + * @var \Drupal\node\NodeInterface + */ + protected $node; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + // Create a page content type. + $this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']); + + // Create a text format and enable the entity_embed filter. + $format = FilterFormat::create([ + 'format' => 'custom_format', + 'name' => 'Custom format', + 'filters' => [ + 'entity_embed' => [ + 'status' => 1, + ], + ], + ]); + $format->save(); + + $editor_group = [ + 'name' => 'Entity Embed', + 'items' => [ + 'node', + ], + ]; + $editor = Editor::create([ + 'format' => 'custom_format', + 'editor' => 'ckeditor', + 'settings' => [ + 'toolbar' => [ + 'rows' => [[$editor_group]], + ], + ], + ]); + $editor->save(); + + // Create a user with required permissions. + $this->webUser = $this->drupalCreateUser([ + 'access content', + 'create page content', + 'use text format custom_format', + ]); + $this->drupalLogin($this->webUser); + + // Create a sample node to be embedded. + $settings = []; + $settings['type'] = 'page'; + $settings['title'] = 'Embed Test Node'; + $settings['body'] = [ + 'value' => 'This node is to be used for embedding in other nodes.', + 'format' => 'custom_format', + ]; + $this->node = $this->drupalCreateNode($settings); + } + + /** + * Tests the entity embed button markup. + */ + public function testEntityEmbedButtonMarkup() { + // Ensure that the route is accessible with a valid embed button. + // 'Node' embed button is provided by default by the module and hence the + // request must be successful. + $this->drupalGet('/entity-embed/dialog/custom_format/node'); + + // Ensure form structure of the 'select' step and submit form. + $this->assertSession()->fieldExists('entity_id'); + + // Check that 'Next' is a primary button. + $this->assertSession()->elementExists('xpath', '//input[contains(@class, "button--primary")]'); + + $title = $this->node->getTitle() . ' (' . $this->node->id() . ')'; + $this->assertSession()->fieldExists('entity_id')->setValue($title); + $this->assertSession()->buttonExists('Next')->press(); + $this->assertSession()->assertWaitOnAjaxRequest(); + $plugins = [ + 'entity_reference:entity_reference_label', + 'entity_reference:entity_reference_entity_id', + 'view_mode:node.full', + 'view_mode:node.rss', + 'view_mode:node.search_index', + 'view_mode:node.search_result', + 'view_mode:node.teaser', + ]; + foreach ($plugins as $plugin) { + $this->assertSession()->optionExists('Display as', $plugin); + } + + $this->container->get('config.factory')->getEditable('entity_embed.settings') + ->set('rendered_entity_mode', TRUE)->save(); + $this->container->get('plugin.manager.entity_embed.display')->clearCachedDefinitions(); + + $this->drupalGet('/entity-embed/dialog/custom_format/node'); + $title = $this->node->getTitle() . ' (' . $this->node->id() . ')'; + $this->assertSession()->fieldExists('entity_id')->setValue($title); + $this->assertSession()->buttonExists('Next')->press(); + $this->assertSession()->assertWaitOnAjaxRequest(); + + $plugins = [ + 'entity_reference:entity_reference_label', + 'entity_reference:entity_reference_entity_id', + 'entity_reference:entity_reference_entity_view', + ]; + foreach ($plugins as $plugin) { + $this->assertSession()->optionExists('Display as', $plugin); + } + } + +} diff --git a/web/modules/entity_embed/tests/src/FunctionalJavascript/EntityEmbedTestBase.php b/web/modules/entity_embed/tests/src/FunctionalJavascript/EntityEmbedTestBase.php new file mode 100644 index 0000000000..03283f2c75 --- /dev/null +++ b/web/modules/entity_embed/tests/src/FunctionalJavascript/EntityEmbedTestBase.php @@ -0,0 +1,102 @@ +<?php + +namespace Drupal\Tests\entity_embed\FunctionalJavascript; + +use Drupal\FunctionalJavascriptTests\WebDriverTestBase; + +/** + * Base class for all entity_embed tests. + */ +abstract class EntityEmbedTestBase extends WebDriverTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'entity_embed', + 'entity_embed_test', + 'node', + 'ckeditor', + ]; + + /** + * Assigns a name to the CKEditor iframe, to allow use of ::switchToIFrame(). + * + * @see \Behat\Mink\Session::switchToIFrame() + */ + protected function assignNameToCkeditorIframe() { + $javascript = <<<JS +(function(){ + document.getElementsByClassName('cke_wysiwyg_frame')[0].id = 'ckeditor'; +})() +JS; + $this->getSession()->evaluateScript($javascript); + } + + /** + * Clicks a CKEditor button. + * + * @param string $name + * The name of the button, such as drupalink, source, etc. + */ + protected function pressEditorButton($name) { + $this->getSession()->switchToIFrame(); + $this->assertSession() + ->waitForElementVisible('css', 'a.cke_button__' . $name) + ->click(); + } + + /** + * Waits for CKEditor to initialize. + * + * @param string $instance_id + * The CKEditor instance ID. + * @param int $timeout + * (optional) Timeout in milliseconds, defaults to 10000. + */ + protected function waitForEditor($instance_id = 'edit-body-0-value', $timeout = 10000) { + $condition = <<<JS + (function() { + return ( + typeof CKEDITOR !== 'undefined' + && typeof CKEDITOR.instances["$instance_id"] !== 'undefined' + && CKEDITOR.instances["$instance_id"].instanceReady + ); + }()); +JS; + + $this->getSession()->wait($timeout, $condition); + } + + /** + * Helper function to reopen EntityEmbedDialog for first embed. + */ + protected function reopenDialog() { + $this->getSession()->switchToIFrame(); + $select_and_edit_embed = <<<JS +var editor = CKEDITOR.instances['edit-body-0-value']; +var entityEmbed = editor.widgets.getByElement(editor.editable().findOne('div')); +entityEmbed.focus(); +editor.execCommand('editdrupalentity'); +JS; + $this->getSession()->executeScript($select_and_edit_embed); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->assertSession()->waitForElementVisible('css', 'form.entity-embed-dialog-step--embed'); + } + + /** + * Show visually hidden fields. + */ + protected function showHiddenFields() { + $script = <<<JS + var hidden_fields = document.querySelectorAll(".visually-hidden"); + + [].forEach.call(hidden_fields, function(el) { + el.classList.remove("visually-hidden"); + }); +JS; + + $this->getSession()->executeScript($script); + } + +} diff --git a/web/modules/entity_embed/tests/src/FunctionalJavascript/ImageFieldFormatterTest.php b/web/modules/entity_embed/tests/src/FunctionalJavascript/ImageFieldFormatterTest.php new file mode 100644 index 0000000000..47e95ba851 --- /dev/null +++ b/web/modules/entity_embed/tests/src/FunctionalJavascript/ImageFieldFormatterTest.php @@ -0,0 +1,265 @@ +<?php + +namespace Drupal\Tests\entity_embed\FunctionalJavascript; + +use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use Drupal\editor\Entity\Editor; +use Drupal\filter\Entity\FilterFormat; + +/** + * Tests ckeditor integration. + * + * @group entity_embed + */ +class ImageFieldFormatterTest extends WebDriverTestBase { + + use SortableTestTrait; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'field_ui', + 'node', + 'file', + 'image', + 'ckeditor', + 'entity_embed', + ]; + + /** + * The test button. + * + * @var Drupal\embed\Entity\EmbedButton + */ + protected $button; + + /** + * The test administrative user. + * + * @var \Drupal\user\UserInterface + */ + protected $adminUser; + + /** + * Created file entity. + * + * @var \Drupal\file\FileInterface + */ + protected $image; + + /** + * File created with invalid image. + * + * @var \Drupal\file\FileInterface + */ + protected $invalidImage; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->button = $this->container->get('entity_type.manager') + ->getStorage('embed_button') + ->create([ + 'label' => 'Image Embed', + 'id' => 'image_embed', + 'type_id' => 'entity', + 'type_settings' => [ + 'entity_type' => 'file', + 'display_plugins' => [ + 'image:image', + ], + 'entity_browser' => '', + 'entity_browser_settings' => [ + 'display_review' => FALSE, + ], + ], + 'icon_uuid' => NULL, + ]); + + $this->button->save(); + + $format = FilterFormat::create([ + 'format' => 'embed_test', + 'name' => 'Embed format', + 'filters' => [], + ]); + $format->save(); + $editor = Editor::create([ + 'format' => 'embed_test', + 'editor' => 'ckeditor', + 'settings' => [ + 'toolbar' => [ + 'rows' => [], + ], + ], + ]); + $editor->save(); + + $this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']); + + $this->adminUser = $this->drupalCreateUser([ + 'access administration pages', + 'administer filters', + 'administer display modes', + 'administer embed buttons', + 'administer site configuration', + 'administer display modes', + 'administer content types', + 'administer node display', + 'access content', + 'create page content', + $format->getPermissionName(), + ]); + + $this->drupalLogin($this->adminUser); + + // Create a sample image to embed. + \Drupal::service('file_system')->copy(\Drupal::root() . '/core/misc/druplicon.png', 'public://rainbow-kitten.png'); + + // Resize the test image so that it will be scaled down during token + // replacement. + $image1 = $this->container->get('image.factory')->get('public://rainbow-kitten.png'); + $image1->resize(500, 500); + $image1->save(); + + $this->image = $this->container->get('entity_type.manager') + ->getStorage('file') + ->create([ + 'uri' => 'public://rainbow-kitten.png', + 'status' => 1, + ]); + $this->image->save(); + + $this->invalidImage = $this->container->get('entity_type.manager') + ->getStorage('file') + ->create([ + 'uri' => 'public://nonexistentimage.jpg', + 'filename' => 'nonexistentimage.jpg', + 'status' => 1, + ]); + $this->invalidImage->save(); + } + + /** + * Test invalid image error. + */ + public function testInvalidImageError() { + $this->drupalGet('admin/config/content/formats/manage/embed_test'); + $this->assertSession()->buttonExists('Show group names')->press(); + $this->assertSession()->waitForElementVisible('css', '.ckeditor-add-new-group'); + $this->assertSession()->buttonExists('Add group')->press(); + $this->assertSession()->waitForElementVisible('css', '[name="group-name"]')->setValue('Embeds'); + $this->assertSession()->buttonExists('Apply')->press(); + + $item = 'li[data-drupal-ckeditor-button-name="' . $this->button->id() . '"]'; + $from = "ul $item"; + $target = 'ul.ckeditor-toolbar-group-buttons'; + + $this->assertSession()->waitForElementVisible('css', $target); + $this->sortableTo($item, $from, $target); + + // Verify filter checkbox exists, then check it. + $page = $this->getSession()->getPage(); + $page->checkField('filters[entity_embed][status]'); + $page->checkField('filters[filter_html][status]'); + $this->assertSession()->buttonExists('Show row weights')->press(); + $page->selectFieldOption('filters[entity_embed][weight]', '0'); + $this->assertSession()->buttonExists('Save configuration')->press(); + $this->assertSession()->responseContains('The text format <em class="placeholder">Embed format</em> has been updated.'); + $this->assertSession()->responseNotContains('The <em class="placeholder">Image Embed</em> button requires "alt" and "title" among the attributes of the "drupal-entity" tag within the allowed html tags.'); + + $filterFormat = $this->container->get('entity_type.manager') + ->getStorage('filter_format') + ->load('embed_test'); + + $settings = $filterFormat->filters('filter_html')->settings; + $allowed_html = $settings['allowed_html']; + + $this->assertContains('drupal-entity data-entity-type data-entity-uuid data-entity-embed-display data-entity-embed-display-settings data-align data-caption data-embed-button data-langcode alt title', $allowed_html); + + $this->drupalGet('/node/add/page'); + $this->assertSession()->waitForElement('css', 'a.cke_button__' . $this->button->id())->click(); + $this->assertSession()->waitForId('drupal-modal'); + $this->assertSession() + ->waitForField('entity_id') + ->setValue($this->invalidImage->label() . ' (' . $this->invalidImage->id() . ')'); + $this->assertSession()->elementExists('css', 'button.js-button-next')->click(); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->assertSession()->responseContains('The selected image "' . $this->invalidImage->label() . '" is invalid.'); + $title = $this->image->label() . ' (' . $this->image->id() . ')'; + $this->assertSession()->fieldExists('entity_id')->setValue($title); + $this->assertSession()->elementExists('css', 'button.js-button-next')->click(); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->assertSession()->responseNotContains('The selected image "' . $this->image->label() . '" is invalid.'); + $this->assertSession()->responseContains('Selected entity'); + $this->assertSession()->responseContains($this->image->label()); + $alt_text = 'Hello world alt text'; + $title_text = 'Hello world title text'; + $this->assertSession()->fieldExists('attributes[alt]')->setValue($alt_text); + $this->assertSession()->fieldExists('attributes[title]')->setValue($title_text); + $this->assertSession()->elementExists('css', 'button.button--primary')->press(); + $this->assertSession()->assertWaitOnAjaxRequest(); + + $this->assignNameToCkeditorIframe(); + $this->getSession()->switchToIFrame('ckeditor'); + $drupal_entity = $this->assertSession()->waitForElementVisible('css', 'drupal-entity[data-embed-button="' . $this->button->id() . '"]'); + $this->assertEquals('Hello world alt text', $drupal_entity->getAttribute('alt')); + $this->assertEquals('Hello world title text', $drupal_entity->getAttribute('title')); + $image = $drupal_entity->find('css', 'img'); + $this->assertContains('rainbow-kitten.png', $image->getAttribute('src')); + $this->getSession()->switchToIFrame(); + + $this->assertSession()->fieldExists('title[0][value]')->setValue('Testing Page with Embed'); + $this->assertSession()->buttonExists('Save')->press(); + + $wrapper = $this->assertSession() + ->elementExists('xpath', "//div[contains(@data-embed-button, 'image_embed')]"); + $img = $wrapper->find('css', 'img'); + $this->assertContains('rainbow-kitten.png', $img->getAttribute('src')); + $this->assertEquals('Hello world alt text', $img->getAttribute('alt')); + $this->assertEquals('Hello world title text', $img->getAttribute('title')); + + // Test allowed_html validation. + $this->drupalGet('admin/config/content/formats/manage/embed_test'); + $allowed_html_field = $this->assertSession()->fieldExists('filters[filter_html][settings][allowed_html]'); + $base_tags = '<a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>'; + $drupal_entity_no_entity_type = '<drupal-entity data-entity-uuid data-view-mode data-entity-embed-display data-entity-embed-display-settings data-align data-caption data-embed-button alt title>'; + $drupal_entity_with_alt_title = '<drupal-entity data-entity-type data-entity-uuid data-view-mode data-entity-embed-display data-entity-embed-display-settings data-align data-caption data-embed-button alt title>'; + + // Verify error message when `<drupal-entity>` absent but `image_embed` + // button in the active toolbar. + $allowed_html_field->setValue($base_tags); + $this->assertSession()->buttonExists('Save configuration')->press(); + $this->assertSession()->responseContains('The <em class="placeholder">Image Embed</em> button requires <code><drupal-entity></code> among the allowed HTML tags.'); + + // Verify error message when `<drupal-entity>` present, `alt` and `title` + // absent, but `image_embed` button in the active toolbar. + $allowed_html_field->setValue($base_tags . ' ' . $drupal_entity_no_entity_type); + $this->assertSession()->buttonExists('Save configuration')->press(); + $this->assertSession()->responseContains('The <code><drupal-entity></code> tag in the allowed HTML tags is missing the following attributes: <code><em class="placeholder">data-entity-type</em></code>.'); + + // Verify if validation errors fixed, form is submitted successfully. + $allowed_html_field->setValue($base_tags . ' ' . $drupal_entity_with_alt_title); + $this->assertSession()->buttonExists('Save configuration')->press(); + $this->assertSession()->responseContains('The text format <em class="placeholder">Embed format</em> has been updated.'); + } + + /** + * Assigns a name to the CKEditor iframe, to allow use of ::switchToIFrame(). + * + * @see \Behat\Mink\Session::switchToIFrame() + */ + protected function assignNameToCkeditorIframe() { + $javascript = <<<JS +(function(){ + document.getElementsByClassName('cke_wysiwyg_frame')[0].id = 'ckeditor'; +})() +JS; + $this->getSession()->evaluateScript($javascript); + } + +} diff --git a/web/modules/entity_embed/tests/src/FunctionalJavascript/MediaImageTest.php b/web/modules/entity_embed/tests/src/FunctionalJavascript/MediaImageTest.php new file mode 100644 index 0000000000..c4bb77fd58 --- /dev/null +++ b/web/modules/entity_embed/tests/src/FunctionalJavascript/MediaImageTest.php @@ -0,0 +1,846 @@ +<?php + +namespace Drupal\Tests\entity_embed\FunctionalJavascript; + +use Drupal\Component\Utility\Html; +use Drupal\editor\Entity\Editor; +use Drupal\entity_embed\Plugin\entity_embed\EntityEmbedDisplay\MediaImageDecorator; +use Drupal\field\Entity\FieldConfig; +use Drupal\file\Entity\File; +use Drupal\media\Entity\Media; +use Drupal\Tests\node\Traits\ContentTypeCreationTrait; +use Drupal\Tests\TestFileCreationTrait; + +/** + * Test Media Image specific functionality. + * + * @group entity_embed + */ +class MediaImageTest extends EntityEmbedTestBase { + + use ContentTypeCreationTrait; + use TestFileCreationTrait; + + /** + * The user to use during testing. + * + * @var \Drupal\user\UserInterface + */ + protected $adminUser; + + /** + * The sample Media entity to embed. + * + * @var \Drupal\media\MediaInterface + */ + protected $media; + + /** + * A host entity with a body field to embed media in. + * + * @var \Drupal\node\NodeInterface + */ + protected $host; + + /** + * {@inheritdoc} + */ + public static $modules = ['entity_embed_test']; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + // Note that media_install() grants 'view media' to all users by default. + $this->adminUser = $this->drupalCreateUser([ + 'use text format full_html', + 'bypass node access', + ]); + + $this->createNode([ + 'type' => 'article', + 'title' => 'Red-lipped batfish', + ]); + + // Create a sample media entity to be embedded. + File::create([ + 'uri' => $this->getTestFiles('image')[0]->uri, + ])->save(); + $this->media = Media::create([ + 'bundle' => 'image', + 'name' => 'Screaming hairy armadillo', + 'field_media_image' => [ + [ + 'target_id' => 1, + 'alt' => 'default alt', + 'title' => 'default title', + ], + ], + ]); + $this->media->save(); + + // Create a sample host entity to embed media in. + $this->drupalCreateContentType(['type' => 'blog']); + $this->host = $this->createNode([ + 'type' => 'blog', + 'title' => 'Animals with strange names', + 'body' => [ + 'value' => '', + 'format' => 'full_html', + ], + ]); + $this->host->save(); + + $this->drupalLogin($this->adminUser); + } + + /** + * Tests alt and title overriding for embedded images. + */ + public function testAltAndTitle() { + $this->drupalGet($this->host->toUrl('edit-form')); + $this->waitForEditor(); + + $this->assignNameToCkeditorIframe(); + + $this->pressEditorButton('test_node'); + $this->assertSession()->waitForId('drupal-modal'); + + // Test that node embed doesn't display alt and title fields. + $this->assertSession() + ->fieldExists('entity_id') + ->setValue('Red-lipped batfish (1)'); + $this->assertSession()->elementExists('css', 'button.js-button-next')->click(); + $form = $this->assertSession()->waitForElementVisible('css', 'form.entity-embed-dialog-step--embed'); + + // Assert that the review step displays the selected entity with the label. + $text = $form->getText(); + $this->assertContains('Red-lipped batfish', $text); + + $select = $this->assertSession() + ->selectExists('attributes[data-entity-embed-display]'); + + $select->setValue('view_mode:node.full'); + $this->assertSession()->assertWaitOnAjaxRequest(); + + // The view_mode:node.full display shouldn't have alt and title fields. + $this->assertSession() + ->fieldNotExists('attributes[data-entity-embed-display-settings][alt]'); + $this->assertSession() + ->fieldNotExists('attributes[data-entity-embed-display-settings][title]'); + + $select = $this->assertSession() + ->selectExists('attributes[data-entity-embed-display]'); + + $select->setValue('view_mode:node.teaser'); + $this->assertSession()->assertWaitOnAjaxRequest(); + + // The view_mode:node.teaser display shouldn't have alt and title fields. + $this->assertSession() + ->fieldNotExists('attributes[data-entity-embed-display-settings][alt]'); + $this->assertSession() + ->fieldNotExists('attributes[data-entity-embed-display-settings][title]'); + + // Close the dialog. + $this->assertSession()->elementExists('css', '.ui-dialog-titlebar-close')->press(); + + // Now test with media. + $this->pressEditorButton('test_media_entity_embed'); + $this->assertSession()->waitForId('drupal-modal'); + + $this->assertSession() + ->fieldExists('entity_id') + ->setValue('Screaming hairy armadillo (1)'); + $this->assertSession()->elementExists('css', 'button.js-button-next')->click(); + $form = $this->assertSession()->waitForElementVisible('css', 'form.entity-embed-dialog-step--embed'); + + // Assert that the review step displays the selected entity with the label. + $text = $form->getText(); + $this->assertContains('Screaming hairy armadillo', $text); + + $select = $this->assertSession() + ->selectExists('attributes[data-entity-embed-display]'); + + $select->setValue('entity_reference:entity_reference_entity_id'); + $this->assertSession()->assertWaitOnAjaxRequest(); + + // The entity_reference:entity_reference_entity_id display shouldn't have + // alt and title fields. + $this->assertSession() + ->fieldNotExists('attributes[data-entity-embed-display-settings][alt]'); + $this->assertSession() + ->fieldNotExists('attributes[data-entity-embed-display-settings][title]'); + + $select->setValue('entity_reference:entity_reference_label'); + $this->assertSession()->assertWaitOnAjaxRequest(); + + // The entity_reference:entity_reference_label display shouldn't have alt + // and title fields. + $this->assertSession() + ->fieldNotExists('attributes[data-entity-embed-display-settings][alt]'); + $this->assertSession() + ->fieldNotExists('attributes[data-entity-embed-display-settings][title]'); + + // Test the entity embed display that ships with core media. + $select->setValue('entity_reference:media_thumbnail'); + $this->assertSession()->assertWaitOnAjaxRequest(); + + $this->assertSession() + ->selectExists('attributes[data-entity-embed-display]') + ->setValue('view_mode:media.embed'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $alt = $this->assertSession() + ->fieldExists('attributes[data-entity-embed-display-settings][alt]'); + $this->assertEquals($this->media->field_media_image->alt, $alt->getAttribute('placeholder')); + $title = $this->assertSession() + ->fieldExists('attributes[data-entity-embed-display-settings][title]'); + $this->assertEquals($this->media->field_media_image->title, $title->getAttribute('placeholder')); + + $this->submitDialog(); + + $img = $this->assertSession()->elementExists('css', 'img'); + $this->assertEquals("default alt", $img->getAttribute('alt')); + $this->assertEquals("default title", $img->getAttribute('title')); + + $this->reopenDialog(); + + $this->assertSession() + ->fieldExists('attributes[data-entity-embed-display-settings][alt]') + ->setValue('Satanic leaf-tailed gecko alt'); + $this->assertSession() + ->fieldExists('attributes[data-entity-embed-display-settings][title]') + ->setValue('Satanic leaf-tailed gecko title'); + + $this->submitDialog(); + + $img = $this->assertSession()->elementExists('css', 'img'); + $this->assertEquals("Satanic leaf-tailed gecko alt", $img->getAttribute('alt')); + $this->assertEquals("Satanic leaf-tailed gecko title", $img->getAttribute('title')); + + $this->reopenDialog(); + + // Test a view mode that displays thumbnail field. + $select->setValue('view_mode:media.thumb'); + $this->assertSession()->assertWaitOnAjaxRequest(); + + $this->assertSession() + ->selectExists('attributes[data-entity-embed-display]') + ->setValue('view_mode:media.embed'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $alt = $this->assertSession() + ->fieldExists('attributes[data-entity-embed-display-settings][alt]'); + $this->assertEquals('Satanic leaf-tailed gecko alt', $alt->getValue()); + $title = $this->assertSession() + ->fieldExists('attributes[data-entity-embed-display-settings][title]'); + $this->assertEquals('Satanic leaf-tailed gecko title', $title->getValue()); + + $this->submitDialog(); + + $img = $this->assertSession()->elementExists('css', 'img'); + $this->assertEquals('Satanic leaf-tailed gecko alt', $img->getAttribute('alt')); + $this->assertEquals('Satanic leaf-tailed gecko title', $img->getAttribute('title')); + + $this->reopenDialog(); + + $this->assertSession() + ->fieldExists('attributes[data-entity-embed-display-settings][alt]') + ->setValue('Goblin shark alt'); + $this->assertSession() + ->fieldExists('attributes[data-entity-embed-display-settings][title]') + ->setValue('Goblin shark title'); + + $this->submitDialog(); + + $img = $this->assertSession()->elementExists('css', 'img'); + $this->assertEquals("Goblin shark alt", $img->getAttribute('alt')); + $this->assertEquals("Goblin shark title", $img->getAttribute('title')); + + $this->reopenDialog(); + + // Test a view mode that displays the media's image field. + $select->setValue('view_mode:media.embed'); + $this->assertSession()->assertWaitOnAjaxRequest(); + + // Test that the view_mode:media.embed display has alt and title fields, + // and that the default values match the values on the media's + // source image field. + $this->assertSession() + ->selectExists('attributes[data-entity-embed-display]') + ->setValue('view_mode:media.embed'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $alt = $this->assertSession() + ->fieldExists('attributes[data-entity-embed-display-settings][alt]'); + $this->assertEquals("Goblin shark alt", $alt->getValue()); + $title = $this->assertSession() + ->fieldExists('attributes[data-entity-embed-display-settings][title]'); + $this->assertEquals("Goblin shark title", $title->getValue()); + + $this->submitDialog(); + + $img = $this->assertSession()->elementExists('css', 'img'); + $this->assertEquals("Goblin shark alt", $img->getAttribute('alt')); + $this->assertEquals("Goblin shark title", $img->getAttribute('title')); + + $this->reopenDialog(); + + $this->assertSession() + ->fieldExists('attributes[data-entity-embed-display-settings][alt]') + ->setValue('Satanic leaf-tailed gecko alt'); + $this->assertSession() + ->fieldExists('attributes[data-entity-embed-display-settings][title]') + ->setValue('Satanic leaf-tailed gecko title'); + + $this->submitDialog(); + + $img = $this->assertSession()->elementExists('css', 'img'); + $this->assertEquals('Satanic leaf-tailed gecko alt', $img->getAttribute('alt')); + $this->assertEquals('Satanic leaf-tailed gecko title', $img->getAttribute('title')); + + $this->config('field.field.media.image.field_media_image') + ->set('settings.alt_field', FALSE) + ->set('settings.title_field', FALSE) + ->save(); + + drupal_flush_all_caches(); + + $this->reopenDialog(); + + $this->assertSession() + ->fieldNotExists('attributes[data-entity-embed-display-settings][alt]'); + $this->assertSession() + ->fieldNotExists('attributes[data-entity-embed-display-settings][title]'); + + $this->submitDialog(); + + $img = $this->assertSession()->elementExists('css', 'img'); + $this->assertEquals('default alt', $img->getAttribute('alt')); + $this->assertEquals('default title', $img->getAttribute('title')); + + $field = FieldConfig::load('media.image.field_media_image'); + $settings = $field->getSettings(); + $settings['alt_field'] = TRUE; + $field->set('settings', $settings); + $field->save(); + + drupal_flush_all_caches(); + + $this->reopenDialog(); + + // Test that when only the alt field is enabled, only alt field should + // display. + $this->assertSession() + ->fieldExists('attributes[data-entity-embed-display-settings][alt]')->setValue('Satanic leaf-tailed gecko alt'); + $this->assertSession() + ->fieldNotExists('attributes[data-entity-embed-display-settings][title]'); + + $this->submitDialog(); + + $img = $this->assertSession()->elementExists('css', 'img'); + $this->assertEquals('Satanic leaf-tailed gecko alt', $img->getAttribute('alt')); + $this->assertEquals('default title', $img->getAttribute('title')); + + $field = FieldConfig::load('media.image.field_media_image'); + $settings = $field->getSettings(); + $settings['alt_field'] = FALSE; + $settings['title_field'] = TRUE; + $field->set('settings', $settings); + $field->save(); + + drupal_flush_all_caches(); + + $this->reopenDialog(); + + // With only title field enabled, only title field should display. + $this->assertSession() + ->fieldExists('attributes[data-entity-embed-display-settings][title]')->setValue('Satanic leaf-tailed gecko title'); + $this->assertSession() + ->fieldNotExists('attributes[data-entity-embed-display-settings][alt]'); + + $this->submitDialog(); + + $img = $this->assertSession()->elementExists('css', 'img'); + $this->assertEquals('Satanic leaf-tailed gecko title', $img->getAttribute('title')); + $this->assertEquals('default alt', $img->getAttribute('alt')); + + $field = FieldConfig::load('media.image.field_media_image'); + $settings = $field->getSettings(); + $settings['alt_field'] = TRUE; + $settings['title_field'] = TRUE; + $field->set('settings', $settings); + $field->save(); + + drupal_flush_all_caches(); + + $this->reopenDialog(); + + // Test that setting value to double quote will allow setting the alt + // and title to empty. + $alt->setValue(MediaImageDecorator::EMPTY_STRING); + $title->setValue(MediaImageDecorator::EMPTY_STRING); + + $this->submitDialog(); + + $img = $this->assertSession()->elementExists('css', 'img'); + $this->assertEmpty($img->getAttribute('alt')); + $this->assertEmpty($img->getAttribute('title')); + + $this->reopenDialog(); + + // Test that *not* filling out the fields makes it fall back to the default. + $alt->setValue(''); + $title->setValue(''); + $this->submitDialog(); + $img = $this->assertSession()->elementExists('css', 'img'); + $this->assertEquals('default alt', $img->getAttribute('alt')); + $this->assertEquals('default title', $img->getAttribute('title')); + } + + /** + * Tests caption editing in the CKEditor widget. + */ + public function testCkeditorWidgetHasEditableCaption() { + $this->drupalGet($this->host->toUrl('edit-form')); + $this->waitForEditor(); + $this->assignNameToCkeditorIframe(); + $this->pressEditorButton('test_media_entity_embed'); + $this->assertSession()->waitForId('drupal-modal'); + + // Embed media. + $this->assertSession() + ->fieldExists('entity_id') + ->setValue('Screaming hairy armadillo (1)'); + $this->assertSession() + ->elementExists('css', 'button.js-button-next') + ->click(); + $this->assertSession() + ->waitForElementVisible('css', 'form.entity-embed-dialog-step--embed'); + $this->assertSession() + ->selectExists('attributes[data-entity-embed-display]') + ->setValue('entity_reference:media_thumbnail'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->assertSession() + ->fieldExists('attributes[data-caption]') + ->setValue('Is this the real life? Is this just fantasy?'); + $this->submitDialog(); + + // Assert that the embedded media was upcasted to a CKEditor widget. + $figcaption = $this->assertSession() + ->elementExists('css', 'figcaption'); + $this->assertTrue($figcaption->hasAttribute('contenteditable')); + + // Type in the widget's editable for the caption. + $this->assertSession()->waitForElement('css', 'figcaption'); + $this->setCaption('Caught in a <strong>landslide</strong>! No escape from <em>reality</em>!'); + $this->getSession()->switchToIFrame('ckeditor'); + $this->assertSession()->elementExists('css', 'figcaption > em'); + $this->assertSession()->elementExists('css', 'figcaption > strong')->click(); + + // Select the <strong> element and unbold it. + $this->clickPathLinkByTitleAttribute("strong element"); + $this->pressEditorButton('bold'); + $this->getSession()->switchToIFrame('ckeditor'); + $this->assertSession()->elementExists('css', 'figcaption > em'); + $this->assertSession()->elementNotExists('css', 'figcaption > strong'); + + // Select the <em> element and unitalicize it. + $this->assertSession()->elementExists('css', 'figcaption > em')->click(); + $this->clickPathLinkByTitleAttribute("em element"); + $this->pressEditorButton('italic'); + + // The "source" button should reveal the HTML source in a state matching + // what is shown in the CKEditor widget. + $this->pressEditorButton('source'); + $source = $this->assertSession()->elementExists('css', 'textarea.cke_source'); + $value = $source->getValue(); + $dom = Html::load($value); + $xpath = new \DOMXPath($dom); + $drupal_entity = $xpath->query('//drupal-entity')[0]; + $this->assertEquals('Caught in a landslide! No escape from reality!', $drupal_entity->getAttribute('data-caption')); + + // Change the caption by modifying the HTML source directly. When exiting + // "source" mode, this should be respected. + $poor_boy_text = "I'm just a <strong>poor boy</strong>, I need no sympathy!"; + $drupal_entity->setAttribute("data-caption", $poor_boy_text); + $source->setValue(Html::serialize($dom)); + $this->pressEditorButton('source'); + $this->assignNameToCkeditorIframe(); + $this->getSession()->switchToIFrame('ckeditor'); + $figcaption = $this->assertSession()->waitForElement('css', 'figcaption'); + $this->assertEquals($poor_boy_text, $figcaption->getHtml()); + + // Select the <strong> element that we just set in "source" mode. This + // proves that it was indeed rendered by the CKEditor widget. + $figcaption->find('css', 'strong')->click(); + $this->pressEditorButton('bold'); + + // Insert a link into the caption. + $this->clickPathLinkByTitleAttribute("Caption element"); + $this->pressEditorButton('drupallink'); + $this->assertSession()->waitForId('drupal-modal'); + $this->assertSession() + ->waitForElementVisible('css', '#editor-link-dialog-form') + ->findField('attributes[href]') + ->setValue('https://www.drupal.org'); + $this->assertSession()->elementExists('css', 'button.form-submit')->press(); + $this->assertSession()->assertWaitOnAjaxRequest(); + + // Wait for the live preview in the CKEditor widget to finish loading, then + // edit the link; no `data-cke-saved-href` attribute should exist on it. + $this->getSession()->switchToIFrame('ckeditor'); + $figcaption = $this->assertSession()->waitForElement('css', 'figcaption'); + $figcaption->find('css', 'a')->click(); + $this->clickPathLinkByTitleAttribute("a element"); + $this->pressEditorButton('drupallink'); + $this->assertSession()->waitForId('drupal-modal'); + $this->assertSession() + ->waitForElementVisible('css', '#editor-link-dialog-form') + ->findField('attributes[href]') + ->setValue('https://www.drupal.org/project/drupal'); + $this->assertSession()->elementExists('css', 'button.form-submit')->press(); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->pressEditorButton('source'); + $source = $this->assertSession()->elementExists('css', "textarea.cke_source"); + $value = $source->getValue(); + $this->assertContains('https://www.drupal.org/project/drupal', $value); + $this->assertNotContains('data-cke-saved-href', $value); + + // Save the entity. + $this->assertSession()->buttonExists('Save')->press(); + + // Verify the saved entity when viewed also contains the captioned media. + $link = $this->assertSession()->elementExists('css', 'figcaption > a'); + $this->assertEquals('https://www.drupal.org/project/drupal', $link->getAttribute('href')); + $this->assertEquals("I'm just a poor boy, I need no sympathy!", $link->getText()); + + // Edit it again, type a different caption in the widget. + $this->drupalGet($this->host->toUrl('edit-form')); + $this->waitForEditor(); + $this->assignNameToCkeditorIframe(); + $this->getSession()->switchToIFrame('ckeditor'); + $this->assertSession()->waitForElementVisible('css', 'figcaption'); + $this->setCaption('Scaramouch, <em>Scaramouch</em>, will you do the <strong>Fandango</strong>?'); + + // Verify that the element path usefully indicates the specific media type + // that is being embedded. + $this->getSession()->switchToIFrame('ckeditor'); + $this->assertSession()->elementExists('xpath', '//figcaption//em')->click(); + $this->getSession()->switchToIFrame(); + $this->assertSession() + ->elementTextContains('css', '#cke_1_path', 'Embedded Media Entity Embed'); + + // Test that removing caption in the EntityEmbedDialog form sets the embed + // to be captionless. + $this->reopenDialog(); + $this->assertSession() + ->fieldExists('attributes[data-caption]') + ->setValue(''); + $this->submitDialog(); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->assertSession()->elementExists('css', 'drupal-entity'); + $this->assertSession()->elementNotExists('css', 'figcaption'); + + // Set a caption again; this time not using the CKEditor Widget, but through + // the dialog. We're typing HTML in the form field, but it will have to be + // HTML-encoded for it to actually show up properly in the CKEditor Widget. + $this->reopenDialog(); + $freddys_lament = "Mama, life had just begun. But now I've gone and <strong>thrown it all away</strong>! :("; + $this->assertSession() + ->fieldExists('attributes[data-caption]') + ->setValue($freddys_lament); + $this->submitDialog(); + $this->assertSession()->elementExists('css', 'figcaption'); + + // Change the caption in the dialog to contain a link. + $wind_markup = '<a href="http://www.drupal.org">anyway the wind blows</a>'; + $this->reopenDialog(); + $this->assertSession() + ->fieldExists('attributes[data-caption]') + ->setValue($wind_markup); + $this->submitDialog(); + + // Assert the caption in the CKEditor widget was updated. + $figcaption = $this->assertSession() + ->waitForElementVisible('css', 'figcaption'); + $this->assertEquals('anyway the wind blows', $figcaption->getText()); + + // Change the text of the link in the caption. + $gallileo = '<a href="http://www.drupal.org">Gallileo, figaro, magnifico</a>'; + $this->reopenDialog(); + $this->assertSession() + ->fieldExists('attributes[data-caption]') + ->setValue($gallileo); + $this->submitDialog(); + + // Assert the caption in the CKEditor widget was updated. + $figcaption = $this->assertSession() + ->waitForElementVisible('css', 'figcaption'); + $this->assertEquals('Gallileo, figaro, magnifico', $figcaption->getText()); + + // Erase the caption in the CKEditor Widget, verify the <figcaption> still + // exists and contains placeholder text, then type something else. + $this->setCaption(''); + $this->getSession()->switchToIFrame('ckeditor'); + $this->assertSession()->elementContains('css', 'figcaption', ''); + $this->assertSession()->elementAttributeContains('css', 'figcaption', 'data-placeholder', 'Enter caption here'); + $this->setCaption('Fin.'); + $this->getSession()->switchToIFrame('ckeditor'); + $this->assertSession()->elementContains('css', 'figcaption', 'Fin.'); + } + + /** + * Tests linkability of the CKEditor widget when `drupalimage` is disabled. + */ + public function testCkeditorWidgetIsLinkableWhenDrupalImageIsAbsent() { + // Remove the `drupalimage` plugin's `DrupalImage` button. + $editor = Editor::load('full_html'); + $settings = $editor->getSettings(); + $rows = $settings['toolbar']['rows']; + foreach ($rows as $row_key => $row) { + foreach ($row as $group_key => $group) { + foreach ($group['items'] as $item_key => $item) { + if ($item === 'DrupalImage') { + unset($settings['toolbar']['rows'][$row_key][$group_key]['items'][$item_key]); + } + } + } + } + $editor->setSettings($settings); + $editor->save(); + + $this->testCkeditorWidgetIsLinkable(); + } + + /** + * Tests linkability of the CKEditor widget. + */ + public function testCkeditorWidgetIsLinkable() { + $this->host->body->value = '<drupal-entity data-caption="baz" data-embed-button="test_media_entity_embed" data-entity-embed-display="entity_reference:media_thumbnail" data-entity-embed-display-settings="{"image_style":"","image_link":""}" data-entity-type="media" data-entity-uuid="' . $this->media->uuid() . '"></drupal-entity>'; + $this->host->save(); + + $this->drupalGet($this->host->toUrl('edit-form')); + $this->waitForEditor(); + $this->assignNameToCkeditorIframe(); + $this->getSession()->switchToIFrame('ckeditor'); + + // Select the CKEditor Widget and click the "link" button. + $drupal_entity = $this->assertSession()->waitForElementVisible('css', 'drupal-entity'); + $this->assertNotEmpty($drupal_entity); + $drupal_entity->click(); + $this->pressEditorButton('drupallink'); + $this->assertSession()->waitForId('drupal-modal'); + + // Enter a link in the link dialog and save. + $this->assertSession() + ->waitForElementVisible('css', '#editor-link-dialog-form') + ->findField('attributes[href]') + ->setValue('https://www.drupal.org'); + $this->assertSession()->elementExists('css', 'button.form-submit')->press(); + $this->assertSession()->assertWaitOnAjaxRequest(); + + // Save the entity. + $this->assertSession()->buttonExists('Save')->press(); + + // Verify the saved entity when viewed also contains the linked media. + $this->assertSession()->elementExists('css', 'figure > a[href="https://www.drupal.org"] > div[data-embed-button="test_media_entity_embed"] > img[src*="image-test.png"]'); + + // Test that `drupallink` also still works independently: inserting a link + // is possible. + $this->drupalGet($this->host->toUrl('edit-form')); + $this->waitForEditor(); + $this->pressEditorButton('drupallink'); + $this->assertSession()->waitForId('drupal-modal'); + $this->assertSession() + ->waitForElementVisible('css', '#editor-link-dialog-form') + ->findField('attributes[href]') + ->setValue('https://wikipedia.org'); + $this->assertSession()->elementExists('css', 'button.form-submit')->press(); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->assignNameToCkeditorIframe(); + $this->getSession()->switchToIFrame('ckeditor'); + $this->assertSession()->elementExists('css', 'body > a[href="https://wikipedia.org"]'); + $this->assertSession()->elementExists('css', 'body > .cke_widget_drupalentity > drupal-entity > figure > a[href="https://www.drupal.org"]'); + } + + /** + * Tests that only <drupal-entity> tags are processed. + * + * @see \Drupal\Tests\entity_embed\Kernel\EntityEmbedFilterTest::testOnlyDrupalEntityTagProcessed() + */ + public function testOnlyDrupalEntityTagProcessed() { + $embed_code = '<drupal-entity data-caption="baz" data-embed-button="test_media_entity_embed" data-entity-embed-display="entity_reference:media_thumbnail" data-entity-embed-display-settings="{"image_style":"","image_link":""}" data-entity-type="media" data-entity-uuid="' . $this->media->uuid() . '"></drupal-entity>'; + $this->host->body->value = str_replace('drupal-entity', 'p', $embed_code); + $this->host->save(); + + // Assert that `<p data-* …>` is not upcast into a CKEditor Widget. + $this->drupalLogin($this->adminUser); + $this->drupalGet($this->host->toUrl('edit-form')); + $this->waitForEditor(); + $this->assignNameToCkeditorIframe(); + $this->getSession()->switchToIFrame('ckeditor'); + $this->assertSession()->waitForElementVisible('css', 'img[src*="example.jpg"]', 1000); + $this->assertSession()->elementNotExists('css', 'figure'); + + $this->host->body->value = $embed_code; + $this->host->save(); + + // Assert that `<drupal-entity data-* …>` is upcast into a CKEditor Widget. + $this->getSession()->reload(); + $this->waitForEditor(); + $this->assignNameToCkeditorIframe(); + $this->getSession()->switchToIFrame('ckeditor'); + $this->assertSession()->waitForElementVisible('css', 'img[src*="example.jpg"]'); + $this->assertSession()->elementExists('css', 'figure'); + } + + /** + * The CKEditor Widget must load a preview generated using the default theme. + */ + public function testPreviewUsesDefaultThemeAndIsClientCacheable() { + // Make the node edit form use the admin theme, like on most Drupal sites. + $this->config('node.settings') + ->set('use_admin_theme', TRUE) + ->save(); + $this->container->get('router.builder')->rebuild(); + + // Allow the test user to view the admin theme. + $this->adminUser->addRole($this->drupalCreateRole(['view the administration theme'])); + $this->adminUser->save(); + + // Configure a different default and admin theme, like on most Drupal sites. + $this->config('system.theme') + ->set('default', 'stable') + ->set('admin', 'classy') + ->save(); + + // Assert that when looking at an embedded entity in the CKEditor Widget, + // the preview is generated using the default theme, not the admin theme. + // @see entity_embed_test_entity_view_alter() + $this->host->body->value = '<drupal-entity data-caption="baz" data-embed-button="test_media_entity_embed" data-entity-embed-display="entity_reference:entity_reference_entity_view" data-entity-embed-display-settings="full" data-entity-type="media" data-entity-uuid="' . $this->media->uuid() . '"></drupal-entity>'; + $this->host->save(); + $this->drupalGet($this->host->toUrl('edit-form')); + $this->waitForEditor(); + $this->assignNameToCkeditorIframe(); + $this->getSession()->switchToIFrame('ckeditor'); + $this->assertSession()->waitForElementVisible('css', 'img[src*="image-test.png"]'); + $element = $this->assertSession()->elementExists('css', '[data-entity-embed-test-active-theme]'); + $this->assertSame('stable', $element->getAttribute('data-entity-embed-test-active-theme')); + + // Assert that the first preview request transferred data over the wire. + // Then toggle source mode on and off. This causes the CKEditor widget to be + // destroyed and then reconstructed. Assert that during this reconstruction, + // a second request is sent. This second request should have transferred 0 + // bytes: the browser should have cached the response, thus resulting in a + // much better user experience. + $this->assertGreaterThan(0, $this->getLastPreviewRequestTransferSize()); + $this->pressEditorButton('source'); + $this->assertSession()->waitForElement('css', 'textarea.cke_source'); + $this->pressEditorButton('source'); + $this->assignNameToCkeditorIframe(); + $this->getSession()->switchToIFrame('ckeditor'); + $this->assertSession()->waitForElementVisible('css', 'img[src*="image-test.png"]'); + $this->assertSame(0, $this->getLastPreviewRequestTransferSize()); + } + + /** + * Gets the transfer size of the last preview request. + * + * @return int + * The transfer size in octets. + */ + protected function getLastPreviewRequestTransferSize() { + $this->getSession()->switchToIFrame(); + $javascript = <<<JS +(function(){ + return window.performance + .getEntries() + .filter(function (entry) { + return entry.initiatorType == 'xmlhttprequest' && entry.name.indexOf('/entity-embed/preview/') !== -1; + }) + .pop() + .transferSize; +})() +JS; + return $this->getSession()->evaluateScript($javascript); + } + + /** + * Tests even <drupal-entity> elements whose button is not present are upcast. + * + * @param string $data_embed_button_attribute + * The HTML for a data-embed-button atttribute. + * + * @dataProvider providerCkeditorWidgetWorksForAllEmbeds + */ + public function testCkeditorWidgetWorksForAllEmbeds($data_embed_button_attribute) { + $this->host->body->value = '<drupal-entity data-caption="baz" ' . $data_embed_button_attribute . ' data-entity-embed-display="entity_reference:media_thumbnail" data-entity-embed-display-settings="{"image_style":"","image_link":""}" data-entity-type="media" data-entity-uuid="' . $this->media->uuid() . '"></drupal-entity>'; + $this->host->save(); + + $this->drupalLogin($this->adminUser); + $this->drupalGet('node/' . $this->host->id() . '/edit'); + $this->waitForEditor(); + $this->assignNameToCkeditorIframe(); + + $this->getSession()->switchToIFrame('ckeditor'); + $this->assertNotNull($this->assertSession()->waitForElementVisible('css', 'figcaption')); + } + + /** + * Data provider for testCkeditorWidgetWorksForAllEmbeds(). + */ + public function providerCkeditorWidgetWorksForAllEmbeds() { + return [ + 'present and active CKEditor button ID' => [ + 'data-embed-button="test_media_entity_embed"', + ], + 'present and inactive CKEditor button ID' => [ + 'data-embed-button="user"', + ], + 'present and nonsensical CKEditor button ID' => [ + 'data-embed-button="ceci nest pas une pipe"', + ], + 'absent' => [ + '', + ], + ]; + } + + /** + * Helper function to submit dialog and focus on ckeditor frame. + */ + protected function submitDialog() { + $this->assertSession()->elementExists('css', 'button.button--primary')->press(); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->getSession()->switchToIFrame('ckeditor'); + } + + /** + * Set the text of the editable caption to the given text. + * + * @param string $text + * The text to set in the caption. + */ + protected function setCaption($text) { + $this->getSession()->switchToIFrame(); + $select_and_edit_caption = "var editor = CKEDITOR.instances['edit-body-0-value']; + var figcaption = editor.widgets.getByElement(editor.editable().findOne('figcaption')); + figcaption.editables.caption.setData('" . $text . "')"; + $this->getSession()->executeScript($select_and_edit_caption); + } + + /** + * Clicks a link in the editor's path links with the given title text. + * + * @param string $text + * The title attribute of the link to click. + * + * @throws \Behat\Mink\Exception\ElementNotFoundException + */ + protected function clickPathLinkByTitleAttribute($text) { + $this->getSession()->switchToIFrame(); + $selector = '//span[@id="cke_1_path"]//a[@title="' . $text . '"]'; + $this->assertSession()->elementExists('xpath', $selector)->click(); + } + +} diff --git a/web/modules/entity_embed/tests/src/FunctionalJavascript/SortableTestTrait.php b/web/modules/entity_embed/tests/src/FunctionalJavascript/SortableTestTrait.php new file mode 100644 index 0000000000..3d1891a424 --- /dev/null +++ b/web/modules/entity_embed/tests/src/FunctionalJavascript/SortableTestTrait.php @@ -0,0 +1,127 @@ +<?php + +namespace Drupal\Tests\entity_embed\FunctionalJavascript; + +/** + * Extends \Drupal\FunctionalJavascriptTests\SortableTestTrait. + * + * This trait is a bridge to allow dragging to work on all versions of Drupal. + * Drupal 8.8 and higher use Sortable (i.e., the HTML5 drag and drop API), and + * are therefore incompatible with Chromedriver, at least for now. + * + * @see https://www.drupal.org/project/entity_embed/issues/3108151 + * + * @internal + * This trait is completely and totally internal and not meant to be used in + * ANY way by code that is not part of the Entity Embed module. It may be + * changed in any number of ways, or even deleted outright, at any time + * without warning. External code should not rely on or use this trait at all. + * If you need its functionality, copy it wholesale into your own code base. + * + * @todo Remove this trait entirey when Drupal 8.8 is the minimum version of + * core supported by Entity Embed. + */ +trait SortableTestTrait { + + /** + * {@inheritdoc} + */ + protected function sortableUpdate($item, $from, $to = NULL) { + $script = <<<JS +(function () { + // Set backbone model after a DOM change. + Drupal.ckeditor.models.Model.set('isDirty', true); +})() + +JS; + + $options = [ + 'script' => $script, + 'args' => [], + ]; + + $this->getSession()->getDriver()->getWebDriverSession()->execute($options); + } + + /** + * Simulates a drag on an element from one container to another. + * + * @param string $item + * The HTML selector for the element to be moved. + * @param string $from + * The HTML selector for the previous container element. + * @param null|string $to + * The HTML selector for the target container. + */ + protected function sortableTo($item, $from, $to) { + // Versions of Drupal older than 8.8 allow normal Selenium-style dragging + // and dropping. + if (version_compare(\Drupal::VERSION, '8.8.0', '<')) { + $this->doLegacyDrag($item, $to); + return; + } + + $item = addslashes($item); + $from = addslashes($from); + $to = addslashes($to); + + $script = <<<JS +(function (src, to) { + var sourceElement = document.querySelector(src); + var toElement = document.querySelector(to); + + toElement.insertBefore(sourceElement, toElement.firstChild); +})('{$item}', '{$to}') + +JS; + + $options = [ + 'script' => $script, + 'args' => [], + ]; + + $this->getSession()->getDriver()->getWebDriverSession()->execute($options); + $this->sortableUpdate($item, $from, $to); + } + + /** + * Simulates a drag moving an element after its sibling in the same container. + * + * @param string $item + * The HTML selector for the element to be moved. + * @param string $target + * The HTML selector for the sibling element. + * @param string $from + * The HTML selector for the element container. + */ + protected function sortableAfter($item, $target, $from) { + $item = addslashes($item); + $target = addslashes($target); + $from = addslashes($from); + + $script = <<<JS +(function (src, to) { + var sourceElement = document.querySelector(src); + var toElement = document.querySelector(to); + + toElement.insertAdjacentElement('afterend', sourceElement); +})('{$item}', '{$target}') + +JS; + + $options = [ + 'script' => $script, + 'args' => [], + ]; + + $this->getSession()->getDriver()->getWebDriverSession()->execute($options); + $this->sortableUpdate($item, $from); + } + + protected function doLegacyDrag($item, $target) { + $assert_session = $this->assertSession(); + $target = $assert_session->elementExists('css', $target); + $assert_session->elementExists('css', $item)->dragTo($target); + } + +} diff --git a/web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterDisabledIntegrationsTest.php b/web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterDisabledIntegrationsTest.php new file mode 100644 index 0000000000..f47ada07a8 --- /dev/null +++ b/web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterDisabledIntegrationsTest.php @@ -0,0 +1,65 @@ +<?php + +namespace Drupal\Tests\entity_embed\Kernel; + +/** + * Tests that entity embed disables certain integrations. + * + * @coversDefaultClass \Drupal\entity_embed\Plugin\Filter\EntityEmbedFilter + * @group entity_embed + */ +class EntityEmbedFilterDisabledIntegrationsTest extends EntityEmbedFilterTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'contextual', + 'quickedit', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->installConfig('system'); + $this->container->get('current_user') + ->addRole($this->drupalCreateRole([ + 'access contextual links', + 'access in-place editing', + ])); + } + + /** + * @covers \Drupal\entity_embed\Plugin\entity_embed\EntityEmbedDisplay\EntityReferenceFieldFormatter::disableContextualLinks + * @covers \Drupal\entity_embed\Plugin\entity_embed\EntityEmbedDisplay\EntityReferenceFieldFormatter::disableQuickEdit + * @dataProvider providerDisabledIntegrations + */ + public function testDisabledIntegrations($integration_detection_selector) { + $text = $this->createEmbedCode([ + 'data-entity-type' => 'node', + 'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID, + 'data-view-mode' => 'teaser', + ]); + + $this->applyFilter($text); + $this->assertCount(0, $this->cssSelect($integration_detection_selector)); + } + + /** + * Data provider for testDisabledIntegrations(). + */ + public function providerDisabledIntegrations() { + return [ + 'contextual' => [ + 'div.embedded-entity > .contextual-region', + ], + 'quickedit' => [ + 'div.embedded-entity > [data-quickedit-entity-id]', + ], + ]; + } + +} diff --git a/web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterLegacyTest.php b/web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterLegacyTest.php new file mode 100644 index 0000000000..f060f90b32 --- /dev/null +++ b/web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterLegacyTest.php @@ -0,0 +1,100 @@ +<?php + +namespace Drupal\Tests\entity_embed\Kernel; + +/** + * @coversDefaultClass \Drupal\entity_embed\Plugin\Filter\EntityEmbedFilter + * @group entity_embed + * @group legacy + */ +class EntityEmbedFilterLegacyTest extends EntityEmbedFilterTestBase { + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->installConfig('system'); + } + + /** + * Tests BC for `data-entity-uuid`'s predecessor, `data-entity-id`. + */ + public function testEntityIdBackwardsCompatibility() { + $content = $this->createEmbedCode([ + 'data-entity-type' => 'node', + 'data-entity-id' => 1, + 'data-view-mode' => 'teaser', + ]); + $this->applyFilter($content); + $this->assertHasAttributes($this->cssSelect('div.embedded-entity')[0], [ + 'data-entity-type' => 'node', + 'data-entity-id' => 1, + 'data-view-mode' => 'teaser', + 'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID, + 'data-langcode' => 'en', + 'data-entity-embed-display' => 'entity_reference:entity_reference_entity_view', + 'data-entity-embed-display-settings' => 'teaser', + ]); + } + + /** + * Verifies `data-entity-id` is ignored when `data-entity-uuid` is present. + */ + public function testEntityIdIgnoredIfEntityUuidPresent() { + $nonsensical_id = $this->randomMachineName(); + $content = $this->createEmbedCode([ + 'data-entity-type' => 'node', + 'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID, + 'data-entity-id' => $nonsensical_id, + 'data-view-mode' => 'teaser', + ]); + $this->applyFilter($content); + $this->assertHasAttributes($this->cssSelect('div.embedded-entity')[0], [ + 'data-entity-type' => 'node', + 'data-entity-id' => $nonsensical_id, + 'data-view-mode' => 'teaser', + 'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID, + 'data-langcode' => 'en', + 'data-entity-embed-display' => 'entity_reference:entity_reference_entity_view', + 'data-entity-embed-display-settings' => 'teaser', + ]); + } + + /** + * Tests BC for `data-entity-embed-display-settings`'s predecessor. + */ + public function testEntityEmbedSettingsBackwardsCompatibility() { + $content = $this->createEmbedCode([ + 'data-entity-type' => 'node', + 'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID, + 'data-entity-embed-display' => 'entity_reference:entity_reference_label', + 'data-entity-embed-settings' => '{"link":"0"}', + ]); + $this->applyFilter($content); + $this->assertCount(0, $this->cssSelect('div.embedded-entity a')); + $this->assertSame($this->embeddedEntity->label(), (string) $this->cssSelect('div.embedded-entity')[0]); + } + + /** + * Tests BC for `data-entity-embed-display="default"`. + */ + public function testEntityEmbedDisplayDefaultBackwardsCompatibility() { + $content = $this->createEmbedCode([ + 'data-entity-type' => 'node', + 'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID, + 'data-entity-embed-display' => 'default', + 'data-entity-embed-display-settings' => '{"view_mode":"teaser"}', + ]); + $this->applyFilter($content); + $this->assertHasAttributes($this->cssSelect('div.embedded-entity')[0], [ + 'data-entity-type' => 'node', + 'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID, + 'data-entity-embed-display' => 'entity_reference:entity_reference_entity_view', + 'data-entity-embed-display-settings' => 'teaser', + 'data-langcode' => 'en', + ]); + } + +} diff --git a/web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterOverridesTest.php b/web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterOverridesTest.php new file mode 100644 index 0000000000..22a4de2ac4 --- /dev/null +++ b/web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterOverridesTest.php @@ -0,0 +1,155 @@ +<?php + +namespace Drupal\Tests\entity_embed\Kernel; + +use Drupal\field\Entity\FieldConfig; +use Drupal\file\Entity\File; +use Drupal\media\Entity\Media; +use Drupal\Tests\media\Traits\MediaTypeCreationTrait; +use Drupal\Tests\TestFileCreationTrait; + +/** + * Tests that entity embeds can have per-embed overrides for e.g. `alt`. + * + * @coversDefaultClass \Drupal\entity_embed\Plugin\Filter\EntityEmbedFilter + * @group entity_embed + */ +class EntityEmbedFilterOverridesTest extends EntityEmbedFilterTestBase { + + use MediaTypeCreationTrait; + use TestFileCreationTrait; + + /** + * The image file to use in tests. + * + * @var \Drupal\file\FileInterface + */ + protected $image; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'file', + 'image', + 'media', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->installSchema('file', ['file_usage']); + $this->installEntitySchema('file'); + $this->installEntitySchema('media'); + $this->installConfig('image'); + $this->installConfig('media'); + $this->installConfig('system'); + + $this->image = File::create([ + 'uri' => $this->getTestFiles('image')[0]->uri, + 'uid' => 2, + ]); + $this->image->setPermanent(); + $this->image->save(); + } + + /** + * Tests overriding of `alt` and `title` for default image field formatter. + */ + public function testOverrideAltAndTitleForImage() { + $content = $this->createEmbedCode([ + 'data-entity-type' => 'file', + 'data-entity-uuid' => $this->image->uuid(), + 'data-entity-embed-display' => 'image:image', + 'data-entity-embed-display-settings' => '{"image_style":"","image_link":""}', + 'alt' => 'This is alt text', + 'title' => 'This is title text', + ]); + + $this->applyFilter($content); + + $this->assertHasAttributes($this->cssSelect('div.embedded-entity')[0], [ + 'alt' => 'This is alt text', + 'data-entity-embed-display' => 'image:image', + 'data-entity-type' => 'file', + 'data-entity-uuid' => $this->image->uuid(), + 'title' => 'This is title text', + 'data-langcode' => 'en', + ]); + $this->assertHasAttributes($this->cssSelect('div.embedded-entity img')[0], [ + 'alt' => 'This is alt text', + 'title' => 'This is title text', + ]); + } + + /** + * Tests overriding of `alt` and `title` for image media items. + */ + public function testOverridesAltAndTitleForImageMedia() { + $this->createMediaType('image', ['id' => 'image']); + // The `alt` field property is enabled by default, the `title` one is not. + // Since we want to test it, enable it. + $source_field = FieldConfig::load('media.image.field_media_image'); + $source_field->setSetting('title_field', TRUE); + $source_field->save(); + $this->container->get('current_user') + ->addRole($this->drupalCreateRole(['view media'])); + + $media = Media::create([ + 'bundle' => 'image', + 'name' => 'Screaming hairy armadillo', + 'field_media_image' => [ + [ + 'target_id' => $this->image->id(), + 'alt' => 'default alt', + 'title' => 'default title', + ], + ], + ]); + $media->save(); + + $base = [ + 'data-entity-embed-display' => 'view_mode:media.full', + 'data-entity-embed-display-settings' => '', + 'data-entity-type' => 'media', + 'data-entity-uuid' => $media->uuid(), + ]; + $input = $this->createEmbedCode($base); + $input .= $this->createEmbedCode([ + 'alt' => 'alt 1', + 'title' => 'title 1', + ] + $base); + $input .= $this->createEmbedCode([ + 'alt' => 'alt 2', + 'title' => 'title 2', + ] + $base); + $input .= $this->createEmbedCode([ + 'alt' => 'alt 3', + 'title' => 'title 3', + ] + $base); + + $this->applyFilter($input); + + $img_nodes = $this->cssSelect('img'); + $this->assertCount(4, $img_nodes); + $this->assertHasAttributes($img_nodes[0], [ + 'alt' => 'default alt', + ]); + $this->assertHasAttributes($img_nodes[1], [ + 'alt' => 'alt 1', + 'title' => 'title 1', + ]); + $this->assertHasAttributes($img_nodes[2], [ + 'alt' => 'alt 2', + 'title' => 'title 2', + ]); + $this->assertHasAttributes($img_nodes[3], [ + 'alt' => 'alt 3', + 'title' => 'title 3', + ]); + } + +} diff --git a/web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterTest.php b/web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterTest.php new file mode 100644 index 0000000000..09ff02db5b --- /dev/null +++ b/web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterTest.php @@ -0,0 +1,454 @@ +<?php + +namespace Drupal\Tests\entity_embed\Kernel; + +use Drupal\Core\Cache\Cache; +use Drupal\Core\Cache\CacheableMetadata; + +/** + * @coversDefaultClass \Drupal\entity_embed\Plugin\Filter\EntityEmbedFilter + * @group entity_embed + */ +class EntityEmbedFilterTest extends EntityEmbedFilterTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = [ + // @see entity_embed_test_entity_access() + // @see entity_embed_test_entity_view_alter() + 'entity_embed_test', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->installConfig('system'); + } + + /** + * Ensures entities are rendered with correct data attributes. + * + * @dataProvider providerTestBasics + */ + public function testBasics(array $embed_attributes, $expected_view_mode, array $expected_attributes) { + $content = $this->createEmbedCode($embed_attributes); + + $result = $this->applyFilter($content); + + $this->assertCount(1, $this->cssSelect('div.embedded-entity > [data-entity-embed-test-view-mode="' . $expected_view_mode . '"]')); + $this->assertHasAttributes($this->cssSelect('div.embedded-entity')[0], $expected_attributes); + $this->assertSame([ + 'config:filter.format.plain_text', + 'foo:1', + 'node:1', + 'node_view', + 'user:2', + 'user_view', + ], $result->getCacheTags()); + $this->assertSame(['timezone', 'user.permissions'], $result->getCacheContexts()); + $this->assertSame(Cache::PERMANENT, $result->getCacheMaxAge()); + $this->assertSame(['library'], array_keys($result->getAttachments())); + $this->assertSame(['entity_embed/caption'], $result->getAttachments()['library']); + } + + /** + * Data provider for testBasics(). + */ + public function providerTestBasics() { + return [ + 'data-entity-uuid + data-view-mode=teaser' => [ + [ + 'data-entity-type' => 'node', + 'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID, + 'data-view-mode' => 'teaser', + ], + 'teaser', + [ + 'data-entity-type' => 'node', + 'data-view-mode' => 'teaser', + 'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID, + 'data-langcode' => 'en', + 'data-entity-embed-display' => 'entity_reference:entity_reference_entity_view', + 'data-entity-embed-display-settings' => 'teaser', + ], + ], + 'data-entity-uuid + data-view-mode=full' => [ + [ + 'data-entity-type' => 'node', + 'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID, + 'data-view-mode' => 'full', + ], + 'default', + [ + 'data-entity-type' => 'node', + 'data-view-mode' => 'full', + 'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID, + 'data-langcode' => 'en', + 'data-entity-embed-display' => 'entity_reference:entity_reference_entity_view', + 'data-entity-embed-display-settings' => 'full', + ], + ], + 'data-entity-uuid + data-view-mode=default' => [ + [ + 'data-entity-type' => 'node', + 'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID, + 'data-view-mode' => 'default', + ], + 'default', + [ + 'data-entity-type' => 'node', + 'data-view-mode' => 'default', + 'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID, + 'data-langcode' => 'en', + 'data-entity-embed-display' => 'entity_reference:entity_reference_entity_view', + 'data-entity-embed-display-settings' => 'default', + ], + ], + 'data-entity-uuid + data-entity-embed-display' => [ + [ + 'data-entity-type' => 'node', + 'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID, + 'data-entity-embed-display' => 'entity_reference:entity_reference_entity_view', + 'data-entity-embed-display-settings' => '{"view_mode":"full"}', + ], + 'default', + [ + 'data-entity-embed-display' => 'entity_reference:entity_reference_entity_view', + 'data-entity-embed-display-settings' => 'full', + 'data-entity-type' => 'node', + 'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID, + 'data-langcode' => 'en', + ], + ], + 'data-entity-uuid + data-entity-embed-display + data-view-mode ⇒ data-entity-embed-display wins' => [ + [ + 'data-entity-type' => 'node', + 'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID, + 'data-entity-embed-display' => 'default', + 'data-entity-embed-display-settings' => '{"view_mode":"full"}', + 'data-view-mode' => 'some-invalid-view-mode', + ], + 'default', + [ + 'data-entity-embed-display' => 'entity_reference:entity_reference_entity_view', + 'data-entity-embed-display-settings' => 'full', + 'data-entity-type' => 'node', + 'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID, + 'data-view-mode' => 'some-invalid-view-mode', + 'data-langcode' => 'en', + ], + ], + 'custom attributes are retained' => [ + [ + 'data-foo' => 'bar', + 'foo' => 'bar', + 'data-entity-type' => 'node', + 'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID, + 'data-view-mode' => 'teaser', + ], + 'teaser', + [ + 'data-foo' => 'bar', + 'foo' => 'bar', + 'data-entity-type' => 'node', + 'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID, + 'data-view-mode' => 'teaser', + 'data-langcode' => 'en', + 'data-entity-embed-display' => 'entity_reference:entity_reference_entity_view', + 'data-entity-embed-display-settings' => 'teaser', + ], + ], + ]; + } + + /** + * Tests that entity access is respected by embedding an unpublished entity. + * + * @dataProvider providerAccessUnpublished + */ + public function testAccessUnpublished($allowed_to_view_unpublished, $expected_rendered, CacheableMetadata $expected_cacheability, array $expected_attachments) { + // Unpublish the embedded entity so we can test variations in behavior. + $this->embeddedEntity->setUnpublished()->save(); + + // Are we testing as a user who is allowed to view the embedded entity? + if ($allowed_to_view_unpublished) { + $this->container->get('current_user') + ->addRole($this->drupalCreateRole(['view own unpublished content'])); + } + + $content = $this->createEmbedCode([ + 'data-entity-type' => 'node', + 'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID, + 'data-view-mode' => 'teaser', + ]); + $result = $this->applyFilter($content); + + if (!$expected_rendered) { + $this->assertEmpty($this->getRawContent()); + } + else { + $this->assertCount(1, $this->cssSelect('div.embedded-entity > [data-entity-embed-test-view-mode="teaser"]')); + } + + // Expected bubbleable metadata. + $this->assertSame($expected_cacheability->getCacheTags(), $result->getCacheTags()); + $this->assertSame($expected_cacheability->getCacheContexts(), $result->getCacheContexts()); + $this->assertSame($expected_cacheability->getCacheMaxAge(), $result->getCacheMaxAge()); + $this->assertSame($expected_attachments, $result->getAttachments()); + } + + /** + * Data provider for testAccessUnpublished(). + */ + public function providerAccessUnpublished() { + return [ + 'user cannot access embedded entity' => [ + FALSE, + FALSE, + (new CacheableMetadata()) + ->setCacheTags(['foo:1', 'node:1']) + ->setCacheContexts(['user.permissions']) + ->setCacheMaxAge(Cache::PERMANENT), + [], + ], + 'user can access embedded entity' => [ + TRUE, + TRUE, + (new CacheableMetadata()) + ->setCacheTags([ + 'config:filter.format.plain_text', + 'foo:1', + 'node:1', + 'node_view', + 'user:2', + 'user_view', + ]) + ->setCacheContexts(['timezone', 'user', 'user.permissions']) + ->setCacheMaxAge(Cache::PERMANENT), + ['library' => ['entity_embed/caption']], + ], + ]; + } + + /** + * Tests the indicator for missing entities. + * + * @dataProvider providerMissingEntityIndicator + */ + public function testMissingEntityIndicator($entity_type_id, $uuid, $expected_missing_text) { + $content = $this->createEmbedCode([ + 'data-entity-type' => $entity_type_id, + 'data-entity-uuid' => $uuid, + 'data-view-mode' => 'default', + ]); + + // If the UUID being used in the embed is that of the sample entity, first + // assert that it currently results in a functional embed, then delete it. + if ($uuid === static::EMBEDDED_ENTITY_UUID) { + $this->applyFilter($content); + $this->assertCount(1, $this->cssSelect('div.embedded-entity > [data-entity-embed-test-view-mode="default"]')); + $this->embeddedEntity->delete(); + } + + $this->applyFilter($content); + $this->assertCount(0, $this->cssSelect('div.embedded-entity > [data-entity-embed-test-view-mode="default"]')); + $this->assertCount(0, $this->cssSelect('div.embedded-entity')); + $deleted_embed_warning = $this->cssSelect('img')[0]; + $this->assertNotEmpty($deleted_embed_warning); + $this->assertHasAttributes($deleted_embed_warning, [ + 'alt' => $expected_missing_text, + 'src' => file_url_transform_relative(file_create_url('core/modules/media/images/icons/no-thumbnail.png')), + 'title' => $expected_missing_text, + ]); + } + + /** + * Data provider for testMissingEntityIndicator(). + */ + public function providerMissingEntityIndicator() { + return [ + 'node; valid UUID but for a deleted entity' => [ + 'node', + static::EMBEDDED_ENTITY_UUID, + 'Missing content item.', + ], + 'node; invalid UUID' => [ + 'node', + 'invalidUUID', + 'Missing content item.', + ], + 'user; invalid UUID' => [ + 'user', + 'invalidUUID', + 'Missing user.', + ], + ]; + } + + /** + * Tests that only <drupal-entity> tags are processed. + * + * @see \Drupal\Tests\entity_embed\FunctionalJavascript\MediaImageTest::testOnlyDrupalEntityTagProcessed() + */ + public function testOnlyDrupalEntityTagProcessed() { + $content = $this->createEmbedCode([ + 'data-entity-type' => 'node', + 'data-entity-uuid' => $this->embeddedEntity->uuid(), + 'data-view-mode' => 'teaser', + ]); + $content = str_replace('drupal-entity', 'entity-embed', $content); + + $filter_result = $this->processText($content, 'en', ['entity_embed']); + // If input equals output, the filter didn't change anything. + $this->assertSame($content, $filter_result->getProcessedText()); + } + + /** + * Tests recursive rendering protection. + */ + public function testRecursionProtection() { + $text = $this->createEmbedCode([ + 'data-entity-type' => 'node', + 'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID, + 'data-view-mode' => 'default', + ]); + + // Render and verify the presence of the embedded entity 20 times. + for ($i = 0; $i < 20; $i++) { + $this->applyFilter($text); + $this->assertCount(1, $this->cssSelect('div.embedded-entity > [data-entity-embed-test-view-mode="default"]')); + } + + // Render a 21st time, this is exceeding the recursion limit. The entity + // embed markup will be stripped. + $this->applyFilter($text); + $this->assertEmpty($this->getRawContent()); + } + + /** + * @covers \Drupal\filter\Plugin\Filter\FilterAlign + * @covers \Drupal\filter\Plugin\Filter\FilterCaption + * @dataProvider providerFilterIntegration + */ + public function testFilterIntegration(array $filter_ids, array $additional_attributes, $verification_selector, $expected_verification_success, array $expected_asset_libraries, $prefix = '', $suffix = '') { + $content = $this->createEmbedCode([ + 'data-entity-type' => 'node', + 'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID, + 'data-view-mode' => 'teaser', + ] + $additional_attributes); + $content = $prefix . $content . $suffix; + + $result = $this->processText($content, 'en', $filter_ids); + $this->setRawContent($result->getProcessedText()); + $this->assertCount($expected_verification_success ? 1 : 0, $this->cssSelect($verification_selector)); + $this->assertHasAttributes($this->cssSelect('div.embedded-entity')[0], [ + 'data-entity-type' => 'node', + 'data-view-mode' => 'teaser', + 'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID, + 'data-langcode' => 'en', + 'data-entity-embed-display' => 'entity_reference:entity_reference_entity_view', + 'data-entity-embed-display-settings' => 'teaser', + ]); + $this->assertSame([ + 'config:filter.format.plain_text', + 'foo:1', + 'node:1', + 'node_view', + 'user:2', + 'user_view', + ], $result->getCacheTags()); + $this->assertSame(['timezone', 'user.permissions'], $result->getCacheContexts()); + $this->assertSame(Cache::PERMANENT, $result->getCacheMaxAge()); + $this->assertSame(['library'], array_keys($result->getAttachments())); + $this->assertSame($expected_asset_libraries, $result->getAttachments()['library']); + } + + /** + * Data provider for testFilterIntegration(). + */ + public function providerFilterIntegration() { + $default_asset_libraries = ['entity_embed/caption']; + + $caption_additional_attributes = ['data-caption' => 'Yo.']; + $caption_verification_selector = 'figure > figcaption'; + $caption_test_cases = [ + '`data-caption`; only `entity_embed` ⇒ caption absent' => [ + ['entity_embed'], + $caption_additional_attributes, + $caption_verification_selector, + FALSE, + $default_asset_libraries, + ], + '`data-caption`; `filter_caption` + `entity_embed` ⇒ caption present' => [ + ['filter_caption', 'entity_embed'], + $caption_additional_attributes, + $caption_verification_selector, + TRUE, + ['filter/caption', 'entity_embed/caption'], + ], + '`<a>` + `data-caption`; `filter_caption` + `entity_embed` ⇒ caption present, link preserved' => [ + ['filter_caption', 'entity_embed'], + $caption_additional_attributes, + 'figure > a[href="https://www.drupal.org"] + figcaption', + TRUE, + ['filter/caption', 'entity_embed/caption'], + '<a href="https://www.drupal.org">', + '</a>', + ], + ]; + + $align_additional_attributes = ['data-align' => 'center']; + $align_verification_selector = 'div.embedded-entity.align-center'; + $align_test_cases = [ + '`data-align`; `entity_embed` ⇒ alignment absent' => [ + ['entity_embed'], + $align_additional_attributes, + $align_verification_selector, + FALSE, + $default_asset_libraries, + ], + '`data-align`; `filter_align` + `entity_embed` ⇒ alignment present' => [ + ['filter_align', 'entity_embed'], + $align_additional_attributes, + $align_verification_selector, + TRUE, + $default_asset_libraries, + ], + '`<a>` + `data-align`; `filter_align` + `entity_embed` ⇒ alignment present, link preserved' => [ + ['filter_align', 'entity_embed'], + $align_additional_attributes, + 'a[href="https://www.drupal.org"] > div.embedded-entity.align-center', + TRUE, + $default_asset_libraries, + '<a href="https://www.drupal.org">', + '</a>', + ], + ]; + + $caption_and_align_test_cases = [ + '`data-caption` + `data-align`; `filter_align` + `filter_caption` + `entity_embed` ⇒ aligned caption present' => [ + ['filter_align', 'filter_caption', 'entity_embed'], + $align_additional_attributes + $caption_additional_attributes, + 'figure.align-center > figcaption', + TRUE, + ['filter/caption', 'entity_embed/caption'], + ], + '`<a>` + `data-caption` + `data-align`; `filter_align` + `filter_caption` + `entity_embed` ⇒ aligned caption present, link preserved' => [ + ['filter_align', 'filter_caption', 'entity_embed'], + $align_additional_attributes + $caption_additional_attributes, + 'figure.align-center > a[href="https://www.drupal.org"] + figcaption', + TRUE, + ['filter/caption', 'entity_embed/caption'], + '<a href="https://www.drupal.org">', + '</a>', + ], + ]; + + return $caption_test_cases + $align_test_cases + $caption_and_align_test_cases; + } + +} diff --git a/web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterTestBase.php b/web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterTestBase.php new file mode 100644 index 0000000000..fa0bc0a6e7 --- /dev/null +++ b/web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterTestBase.php @@ -0,0 +1,192 @@ +<?php + +namespace Drupal\Tests\entity_embed\Kernel; + +use Drupal\Component\Utility\Html; +use Drupal\Core\Render\BubbleableMetadata; +use Drupal\Core\Render\RenderContext; +use Drupal\filter\FilterPluginCollection; +use Drupal\filter\FilterProcessResult; +use Drupal\KernelTests\KernelTestBase; +use Drupal\Tests\node\Traits\ContentTypeCreationTrait; +use Drupal\Tests\node\Traits\NodeCreationTrait; +use Drupal\Tests\user\Traits\UserCreationTrait; + +/** + * Base class for Entity Embed filter tests. + */ +abstract class EntityEmbedFilterTestBase extends KernelTestBase { + + use NodeCreationTrait { + createNode as drupalCreateNode; + } + use UserCreationTrait { + createUser as drupalCreateUser; + createRole as drupalCreateRole; + } + use ContentTypeCreationTrait { + createContentType as drupalCreateContentType; + } + + /** + * The UUID to use for the embedded entity. + * + * @var string + */ + const EMBEDDED_ENTITY_UUID = 'e7a3e1fe-b69b-417e-8ee4-c80cb7640e63'; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'embed', + 'entity_embed', + 'field', + 'filter', + 'node', + 'system', + 'text', + 'user', + ]; + + /** + * The sample Node entity to embed. + * + * @var \Drupal\node\NodeInterface + */ + protected $embeddedEntity; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->installSchema('node', 'node_access'); + $this->installSchema('system', 'sequences'); + $this->installEntitySchema('node'); + $this->installEntitySchema('user'); + $this->installConfig('filter'); + $this->installConfig('node'); + + // Create a user with required permissions. Ensure that we don't use user 1 + // because that user is treated in special ways by access control handlers. + $admin_user = $this->drupalCreateUser([]); + $user = $this->drupalCreateUser([ + 'access content', + ]); + $this->container->set('current_user', $user); + + // Create a sample node to be embedded. + $this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']); + $this->embeddedEntity = $this->drupalCreateNode([ + 'title' => 'Embed Test Node', + 'uuid' => static::EMBEDDED_ENTITY_UUID, + ]); + } + + /** + * Gets an embed code with given attributes. + * + * @param array $attributes + * The attributes to add. + * + * @return string + * A string containing a drupal-entity dom element. + * + * @see assertEntityEmbedFilterHasRun() + */ + protected function createEmbedCode(array $attributes) { + $dom = Html::load('<drupal-entity>This placeholder should not be rendered.</drupal-entity>'); + $xpath = new \DOMXPath($dom); + $drupal_entity = $xpath->query('//drupal-entity')[0]; + foreach ($attributes as $attribute => $value) { + $drupal_entity->setAttribute($attribute, $value); + } + return Html::serialize($dom); + } + + /** + * Applies the `@Filter=entity_embed` filter to text, pipes to raw content. + * + * @param string $text + * The text string to be filtered. + * @param string $langcode + * The language code of the text to be filtered. + * + * @return \Drupal\filter\FilterProcessResult + * The filtered text, wrapped in a FilterProcessResult object, and possibly + * with associated assets, cacheability metadata and placeholders. + * + * @see \Drupal\Tests\entity_embed\Kernel\EntityEmbedFilterTestBase::createEmbedCode() + * @see \Drupal\KernelTests\AssertContentTrait::setRawContent() + */ + protected function applyFilter($text, $langcode = 'en') { + $this->assertContains('<drupal-entity', $text); + $this->assertContains('This placeholder should not be rendered.', $text); + $filter_result = $this->processText($text, $langcode); + $output = $filter_result->getProcessedText(); + $this->assertNotContains('<drupal-entity', $output); + $this->assertNotContains('This placeholder should not be rendered.', $output); + $this->setRawContent($output); + return $filter_result; + } + + /** + * Assert that the SimpleXMLElement object has the given attributes. + * + * @param \SimpleXMLElement $element + * The SimpleXMLElement object to check. + * @param array $attributes + * An array of attributes. + */ + protected function assertHasAttributes(\SimpleXMLElement $element, array $attributes) { + foreach ($attributes as $attribute => $value) { + $this->assertSame((string) $value, (string) $element[$attribute]); + } + } + + /** + * Processes text through the provided filters. + * + * @param string $text + * The text string to be filtered. + * @param string $langcode + * The language code of the text to be filtered. + * @param string[] $filter_ids + * (optional) The filter plugin IDs to apply to the given text, in the order + * they are being requested to be executed. + * + * @return \Drupal\filter\FilterProcessResult + * The filtered text, wrapped in a FilterProcessResult object, and possibly + * with associated assets, cacheability metadata and placeholders. + * + * @see \Drupal\filter\Element\ProcessedText::preRenderText() + */ + protected function processText($text, $langcode = 'und', array $filter_ids = ['entity_embed']) { + $manager = $this->container->get('plugin.manager.filter'); + $bag = new FilterPluginCollection($manager, []); + $filters = []; + foreach ($filter_ids as $filter_id) { + $filters[] = $bag->get($filter_id); + } + + $render_context = new RenderContext(); + /** @var \Drupal\filter\FilterProcessResult $filter_result */ + $filter_result = $this->container->get('renderer')->executeInRenderContext($render_context, function () use ($text, $filters, $langcode) { + $metadata = new BubbleableMetadata(); + foreach ($filters as $filter) { + /** @var \Drupal\filter\FilterProcessResult $result */ + $result = $filter->process($text, $langcode); + $metadata = $metadata->merge($result); + $text = $result->getProcessedText(); + } + return (new FilterProcessResult($text))->merge($metadata); + }); + if (!$render_context->isEmpty()) { + $filter_result = $filter_result->merge($render_context->pop()); + } + return $filter_result; + } + +} diff --git a/web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterTranslationTest.php b/web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterTranslationTest.php new file mode 100644 index 0000000000..7f907aa46c --- /dev/null +++ b/web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterTranslationTest.php @@ -0,0 +1,119 @@ +<?php + +namespace Drupal\Tests\entity_embed\Kernel; + +use Drupal\language\Entity\ConfigurableLanguage; + +/** + * Tests that entity embeds are translated based on host and `data-langcode`. + * + * @coversDefaultClass \Drupal\entity_embed\Plugin\Filter\EntityEmbedFilter + * @group entity_embed + */ +class EntityEmbedFilterTranslationTest extends EntityEmbedFilterTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'language', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + ConfigurableLanguage::createFromLangcode('pt-br')->save(); + // Reload the entity to ensure it is aware of the newly created language. + $this->embeddedEntity = $this->container->get('entity_type.manager') + ->getStorage($this->embeddedEntity->getEntityTypeId()) + ->load($this->embeddedEntity->id()); + + $this->embeddedEntity->addTranslation('pt-br') + ->setTitle('Embed em portugues') + ->save(); + } + + /** + * Tests that the expected embedded entity translation is selected. + * + * @dataProvider providerTranslationSituations + */ + public function testTranslationSelection($text_langcode, array $additional_attributes, $expected_title_langcode) { + $text = $this->createEmbedCode([ + 'data-entity-type' => 'node', + 'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID, + 'data-view-mode' => 'teaser', + 'data-entity-embed-display' => 'entity_reference:entity_reference_label', + 'data-entity-embed-settings' => '{"link":"0"}', + ] + $additional_attributes); + + $result = $this->processText($text, $text_langcode, ['entity_embed']); + $this->setRawContent($result->getProcessedText()); + + $this->assertSame( + $this->embeddedEntity->getTranslation($expected_title_langcode)->label(), + (string) $this->cssSelect('div.embedded-entity')[0] + ); + // Verify that the filtered text does not vary by translation-related cache + // contexts: a particular translation of the embedded entity is selected + // based on either the `data-langcode` attribute or the host entity's + // language, neither of which should require a cache context to be + // associated. (The host entity's language may itself be selected based on + // some request context, but that is of no concern to this filter.) + $this->assertSame($result->getCacheContexts(), ['user.permissions']); + } + + /** + * Data provider for testTranslationSelection(). + */ + public function providerTranslationSituations() { + $embedded_entity_translation_languages = ['en', 'pt-br']; + + foreach (['en', 'pt-br', 'nl'] as $text_langcode) { + // When no `data-langcode` attribute is specified, the text language + // (which is set to the host entity's language) is respected. If that + // translation does not exist, it falls back to the default translation of + // the embedded entity. + $match_or_fallback_langcode = in_array($text_langcode, $embedded_entity_translation_languages) + ? $text_langcode + : 'en'; + yield "text_langcode=$text_langcode (✅) ⇒ $match_or_fallback_langcode" => [ + $text_langcode, + [], + $match_or_fallback_langcode, + ]; + + // When the embedded entity has a translation for the language code in the + // `data-langcode` attribute, that translation is used, regardless of the + // language of the text (which is set to the language of the host entity). + foreach ($embedded_entity_translation_languages as $data_langcode) { + yield "text_langcode=$text_langcode (✅); data-langcode=$data_langcode (✅) ⇒ $data_langcode" => [ + $text_langcode, + ['data-langcode' => $data_langcode], + $data_langcode, + ]; + } + + // When specifying a (valid) language code but the embedded entity has no + // translation for that language, it falls back to the default translation + // of the embedded entity. + yield "text_langcode=$text_langcode (✅); data-langcode=nl (🚫) ⇒ en" => [ + $text_langcode, + ['data-langcode' => 'nl'], + 'en', + ]; + + // When specifying a invalid language code, it falls back to the default + // translation of the embedded entity. + yield "text_langcode=$text_langcode (✅); data-langcode=non-existing-and-even-invalid-langcode (🚫) ⇒ en" => [ + $text_langcode, + ['data-langcode' => 'non-existing-and-even-invalid-langcode'], + 'en', + ]; + } + } + +} -- GitLab