Further Drupal 8.6.4 changes. Some core files were not committed before a commit...
authorJeff Veit <jeff.veit@gmail.com>
Wed, 27 Oct 2021 12:58:23 +0000 (13:58 +0100)
committerJeff Veit <jeff.veit@gmail.com>
Wed, 27 Oct 2021 12:58:23 +0000 (13:58 +0100)
224 files changed:
composer.json
composer.lock
vendor/composer/installed.json
web/.ht.router.php
web/core/MAINTAINERS.txt
web/core/includes/common.inc
web/core/lib/Drupal.php
web/core/lib/Drupal/Component/Plugin/ContextAwarePluginBase.php
web/core/lib/Drupal/Component/Utility/Mail.php [new file with mode: 0644]
web/core/lib/Drupal/Core/Block/BlockBase.php
web/core/lib/Drupal/Core/Database/Install/Tasks.php
web/core/lib/Drupal/Core/Entity/Entity/EntityViewDisplay.php
web/core/lib/Drupal/Core/Entity/Plugin/DataType/ConfigEntityAdapter.php
web/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php
web/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
web/core/lib/Drupal/Core/Entity/entity.api.php
web/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/StringFormatter.php
web/core/lib/Drupal/Core/Layout/LayoutPluginManager.php
web/core/lib/Drupal/Core/Mail/MailManager.php
web/core/lib/Drupal/Core/Path/AliasStorage.php
web/core/lib/Drupal/Core/Plugin/ContextAwarePluginBase.php
web/core/lib/Drupal/Core/Render/Element/StatusMessages.php
web/core/lib/Drupal/Core/Render/PreviewFallbackInterface.php [new file with mode: 0644]
web/core/lib/Drupal/Core/Routing/Router.php
web/core/lib/Drupal/Core/Session/SessionManager.php
web/core/lib/Drupal/Core/TempStore/PrivateTempStore.php
web/core/lib/Drupal/Core/TypedData/Validation/RecursiveContextualValidator.php
web/core/misc/tableheader.es6.js
web/core/misc/tableheader.js
web/core/modules/aggregator/src/Tests/AggregatorTestBase.php
web/core/modules/aggregator/tests/src/Functional/AggregatorCronTest.php
web/core/modules/aggregator/tests/src/Functional/AggregatorTestBase.php
web/core/modules/aggregator/tests/src/Functional/DeleteFeedTest.php
web/core/modules/aggregator/tests/src/Functional/FeedParserTest.php
web/core/modules/aggregator/tests/src/Functional/ImportOpmlTest.php
web/core/modules/aggregator/tests/src/Functional/UpdateFeedItemTest.php
web/core/modules/block/js/block.admin.es6.js
web/core/modules/block/js/block.admin.js
web/core/modules/block/src/BlockListBuilder.php
web/core/modules/block/src/Plugin/migrate/source/d7/BlockTranslation.php [new file with mode: 0644]
web/core/modules/block/tests/src/Functional/BlockTest.php
web/core/modules/block/tests/src/Kernel/Migrate/d7/MigrateBlockContentTranslationTest.php [new file with mode: 0644]
web/core/modules/block/tests/src/Kernel/Migrate/d7/MigrateBlockTest.php
web/core/modules/block/tests/src/Kernel/Plugin/migrate/source/d7/BlockTranslationTest.php [new file with mode: 0644]
web/core/modules/comment/tests/src/Kernel/Migrate/d7/MigrateCommentTypeTest.php
web/core/modules/content_moderation/src/EntityOperations.php
web/core/modules/content_moderation/src/Plugin/Field/ModerationStateFieldItemList.php
web/core/modules/content_moderation/tests/src/Functional/ModerationContentTranslationTest.php [new file with mode: 0644]
web/core/modules/content_moderation/tests/src/Kernel/ContentModerationStateTest.php
web/core/modules/content_moderation/tests/src/Kernel/ModerationStateFieldItemListTest.php
web/core/modules/content_translation/migrations/d6_taxonomy_term_localized_translation.yml [new file with mode: 0644]
web/core/modules/content_translation/migrations/d7_block_translation.yml [new file with mode: 0644]
web/core/modules/content_translation/tests/src/Kernel/Migrate/d6/MigrateTaxonomyTermTranslationTest.php
web/core/modules/field/tests/src/Functional/EntityReference/EntityReferenceAdminTest.php [moved from web/core/modules/field/src/Tests/EntityReference/EntityReferenceAdminTest.php with 64% similarity]
web/core/modules/field/tests/src/FunctionalJavascript/EntityReference/EntityReferenceAdminTest.php [new file with mode: 0644]
web/core/modules/field/tests/src/Kernel/String/StringFormatterTest.php
web/core/modules/field_ui/tests/src/Functional/ManageFieldsFunctionalTest.php
web/core/modules/file/file.module
web/core/modules/file/tests/src/Functional/MultipleFileUploadTest.php [new file with mode: 0644]
web/core/modules/language/src/ConfigurableLanguageManager.php
web/core/modules/language/tests/src/Functional/ConfigurableLanguageManagerTest.php [new file with mode: 0644]
web/core/modules/language/tests/src/Kernel/Migrate/d6/MigrateLanguageContentTaxonomyVocabularySettingsTest.php
web/core/modules/layout_builder/css/layout-builder.css
web/core/modules/layout_builder/layout_builder.info.yml
web/core/modules/layout_builder/layout_builder.module
web/core/modules/layout_builder/layout_builder.routing.yml
web/core/modules/layout_builder/src/Controller/ChooseBlockController.php
web/core/modules/layout_builder/src/Controller/LayoutBuilderController.php
web/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php
web/core/modules/layout_builder/src/EventSubscriber/BlockComponentRenderArray.php
web/core/modules/layout_builder/src/Form/LayoutBuilderEntityViewDisplayForm.php
web/core/modules/layout_builder/src/LayoutEntityHelperTrait.php
web/core/modules/layout_builder/src/LayoutTempstoreRepository.php
web/core/modules/layout_builder/src/LayoutTempstoreRepositoryInterface.php
web/core/modules/layout_builder/src/Plugin/Block/ExtraFieldBlock.php
web/core/modules/layout_builder/src/Plugin/Block/FieldBlock.php
web/core/modules/layout_builder/src/Plugin/Derivative/FieldBlockDeriver.php
web/core/modules/layout_builder/src/Plugin/Derivative/LayoutBuilderLocalTaskDeriver.php
web/core/modules/layout_builder/src/Plugin/SectionStorage/DefaultsSectionStorage.php
web/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
web/core/modules/layout_builder/src/Plugin/SectionStorage/SectionStorageLocalTaskProviderInterface.php [new file with mode: 0644]
web/core/modules/layout_builder/src/Section.php
web/core/modules/layout_builder/src/SectionStorage/SectionStorageTrait.php
web/core/modules/layout_builder/tests/modules/layout_builder_fieldblock_test/config/schema/layout_builder_fieldblock_test.schema.yml [new file with mode: 0644]
web/core/modules/layout_builder/tests/modules/layout_builder_fieldblock_test/layout_builder_fieldblock_test.info.yml [new file with mode: 0644]
web/core/modules/layout_builder/tests/modules/layout_builder_fieldblock_test/src/Plugin/Block/FieldBlock.php [new file with mode: 0644]
web/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php
web/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php
web/core/modules/layout_builder/tests/src/FunctionalJavascript/FieldBlockTest.php
web/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockPrivateFilesTest.php
web/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockTest.php
web/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockTestBase.php
web/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderUiTest.php [new file with mode: 0644]
web/core/modules/layout_builder/tests/src/Kernel/FieldBlockTest.php
web/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderFieldLayoutCompatibilityTest.php
web/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderInstallTest.php
web/core/modules/layout_builder/tests/src/Kernel/LayoutSectionItemListTest.php
web/core/modules/layout_builder/tests/src/Kernel/SectionStorageTestBase.php
web/core/modules/layout_builder/tests/src/Unit/LayoutTempstoreRepositoryTest.php
web/core/modules/layout_builder/tests/src/Unit/OverridesSectionStorageTest.php
web/core/modules/media/src/Entity/Media.php
web/core/modules/media/src/OEmbed/ResourceFetcher.php
web/core/modules/media/src/Plugin/media/Source/Image.php
web/core/modules/media/tests/fixtures/oembed/video_collegehumor.xml
web/core/modules/media/tests/src/Functional/Rest/MediaResourceTestBase.php
web/core/modules/media/tests/src/FunctionalJavascript/MediaSourceImageTest.php
web/core/modules/media/tests/src/Kernel/MediaSourceTest.php
web/core/modules/menu_link_content/src/Plugin/Menu/MenuLinkContent.php
web/core/modules/menu_link_content/tests/src/Unit/MenuLinkPluginTest.php [new file with mode: 0644]
web/core/modules/migrate/src/Plugin/MigrationPluginManager.php
web/core/modules/migrate/src/Plugin/migrate/process/FormatDate.php
web/core/modules/migrate/src/Plugin/migrate/source/SourcePluginBase.php
web/core/modules/migrate/src/Plugin/migrate/source/SqlBase.php
web/core/modules/migrate/tests/src/Kernel/HighWaterTest.php
web/core/modules/migrate/tests/src/Kernel/MigrateEmbeddedDataTest.php
web/core/modules/migrate/tests/src/Unit/process/FormatDateTest.php
web/core/modules/migrate_drupal/src/Plugin/migrate/source/DrupalSqlBase.php
web/core/modules/migrate_drupal/tests/fixtures/drupal6.php
web/core/modules/migrate_drupal/tests/fixtures/drupal7.php
web/core/modules/migrate_drupal/tests/src/Kernel/Plugin/migrate/source/ContentEntityTest.php
web/core/modules/migrate_drupal/tests/src/Unit/source/DrupalSqlBaseTest.php
web/core/modules/migrate_drupal_ui/tests/src/Functional/d6/MigrateUpgrade6Test.php
web/core/modules/migrate_drupal_ui/tests/src/Functional/d7/MigrateUpgrade7ReviewPageTest.php
web/core/modules/node/node.post_update.php
web/core/modules/node/src/NodeViewsData.php
web/core/modules/node/src/Plugin/migrate/source/d7/NodeType.php
web/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_node_revision_nid.yml
web/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_node_revision_vid.yml
web/core/modules/node/tests/src/Kernel/Views/RevisionRelationshipsTest.php
web/core/modules/path/src/Plugin/Field/FieldType/PathFieldItemList.php
web/core/modules/path/src/Plugin/Field/FieldType/PathItem.php
web/core/modules/path/src/Plugin/Validation/Constraint/PathAliasConstraintValidator.php
web/core/modules/path/tests/src/Functional/PathContentModerationTest.php
web/core/modules/path/tests/src/Functional/PathLanguageUiTest.php
web/core/modules/shortcut/src/ShortcutForm.php
web/core/modules/simpletest/simpletest.module
web/core/modules/system/src/Tests/Ajax/ElementValidationTest.php [deleted file]
web/core/modules/system/src/Tests/Ajax/FormValuesTest.php [deleted file]
web/core/modules/system/src/Tests/Ajax/FrameworkTest.php [deleted file]
web/core/modules/system/src/Tests/Form/TriggeringElementTest.php [deleted file]
web/core/modules/system/src/Tests/Session/StackSessionHandlerIntegrationTest.php [deleted file]
web/core/modules/system/tests/modules/ajax_forms_test/src/Callbacks.php
web/core/modules/system/tests/modules/ajax_forms_test/src/Form/AjaxFormsTestSimpleForm.php
web/core/modules/system/tests/src/Functional/Ajax/FrameworkTest.php [new file with mode: 0644]
web/core/modules/system/tests/src/Functional/Form/ElementsTableSelectTest.php [moved from web/core/modules/system/src/Tests/Form/ElementsTableSelectTest.php with 65% similarity]
web/core/modules/system/tests/src/Functional/Form/RebuildTest.php [new file with mode: 0644]
web/core/modules/system/tests/src/Functional/Form/StorageTest.php [moved from web/core/modules/system/src/Tests/Form/StorageTest.php with 61% similarity]
web/core/modules/system/tests/src/Functional/Mail/MailTest.php
web/core/modules/system/tests/src/Functional/Routing/RouterTest.php [moved from web/core/modules/system/src/Tests/Routing/RouterTest.php with 79% similarity]
web/core/modules/system/tests/src/Functional/Session/SessionAuthenticationTest.php [moved from web/core/modules/system/src/Tests/Session/SessionAuthenticationTest.php with 77% similarity]
web/core/modules/system/tests/src/Functional/Session/SessionTest.php [moved from web/core/modules/system/src/Tests/Session/SessionTest.php with 89% similarity]
web/core/modules/system/tests/src/Functional/Session/StackSessionHandlerIntegrationTest.php [new file with mode: 0644]
web/core/modules/system/tests/src/Functional/Theme/ThemeTest.php
web/core/modules/system/tests/src/FunctionalJavascript/Form/ElementsTableSelectTest.php [new file with mode: 0644]
web/core/modules/system/tests/src/FunctionalJavascript/Form/RebuildTest.php [moved from web/core/modules/system/src/Tests/Form/RebuildTest.php with 53% similarity]
web/core/modules/system/tests/src/FunctionalJavascript/Form/TriggeringElementTest.php [new file with mode: 0644]
web/core/modules/system/tests/src/FunctionalJavascript/FrameworkTest.php [new file with mode: 0644]
web/core/modules/system/tests/src/Kernel/Extension/ModuleHandlerTest.php
web/core/modules/system/tests/themes/test_theme/test_theme.info.yml
web/core/modules/system/tests/themes/test_theme_settings/config/schema/test_theme_settings.schema.yml
web/core/modules/system/tests/themes/test_theme_settings/theme-settings.php
web/core/modules/taxonomy/src/Plugin/migrate/source/d6/TermLocalizedTranslation.php [new file with mode: 0644]
web/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTaxonomyTermTest.php
web/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermLocalizedTranslationTest.php [new file with mode: 0644]
web/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateVocabularyFieldInstanceTest.php
web/core/modules/taxonomy/tests/src/Kernel/Plugin/migrate/source/d6/TermLocalizedTranslationTest.php [new file with mode: 0644]
web/core/modules/user/user.api.php
web/core/modules/views/src/Plugin/Block/ViewsBlockBase.php
web/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_click_sort_ajax.yml [new file with mode: 0644]
web/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_mini_pager_ajax.yml [new file with mode: 0644]
web/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_pager_full_ajax.yml [new file with mode: 0644]
web/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_user_path.yml [new file with mode: 0644]
web/core/modules/views/tests/src/Functional/UserPathTest.php [new file with mode: 0644]
web/core/modules/views_ui/src/Tests/PreviewTest.php [deleted file]
web/core/modules/views_ui/tests/src/Functional/PreviewTest.php [new file with mode: 0644]
web/core/modules/views_ui/tests/src/FunctionalJavascript/PreviewTest.php [new file with mode: 0644]
web/core/modules/workspaces/src/EntityTypeInfo.php
web/core/modules/workspaces/src/Plugin/Validation/Constraint/EntityReferenceSupportedNewEntitiesConstraint.php [new file with mode: 0644]
web/core/modules/workspaces/src/Plugin/Validation/Constraint/EntityReferenceSupportedNewEntitiesConstraintValidator.php [new file with mode: 0644]
web/core/modules/workspaces/src/WorkspaceManager.php
web/core/modules/workspaces/src/WorkspaceManagerInterface.php
web/core/modules/workspaces/tests/src/Functional/WorkspaceTest.php
web/core/modules/workspaces/tests/src/Kernel/EntityReferenceSupportedNewEntitiesConstraintValidatorTest.php [new file with mode: 0644]
web/core/modules/workspaces/tests/src/Kernel/WorkspaceIntegrationTest.php
web/core/modules/workspaces/tests/src/Kernel/WorkspaceTestTrait.php [new file with mode: 0644]
web/core/modules/workspaces/workspaces.module
web/core/modules/workspaces/workspaces.services.yml
web/core/profiles/demo_umami/config/install/views.view.articles_aside.yml
web/core/profiles/demo_umami/config/install/views.view.featured_articles.yml
web/core/profiles/demo_umami/config/install/views.view.frontpage.yml
web/core/profiles/demo_umami/config/install/views.view.recipes.yml
web/core/profiles/demo_umami/themes/umami/css/components/blocks/footer-promo/footer-promo.css
web/core/profiles/demo_umami/themes/umami/css/components/blocks/search/search-results.css
web/core/profiles/demo_umami/themes/umami/css/components/blocks/search/search.css
web/core/profiles/demo_umami/themes/umami/css/components/content-types/recipe/recipe.css
web/core/profiles/demo_umami/themes/umami/css/components/fields/recipe-instruction.css
web/core/profiles/demo_umami/themes/umami/css/components/forms/contact.css
web/core/profiles/demo_umami/themes/umami/css/components/messages/messages.css
web/core/profiles/demo_umami/themes/umami/css/components/navigation/breadcrumbs/breadcrumbs.css
web/core/profiles/demo_umami/themes/umami/css/components/navigation/menu-account/menu-account.css
web/core/profiles/demo_umami/themes/umami/css/components/navigation/menu-main/menu-main.css
web/core/profiles/demo_umami/themes/umami/css/components/navigation/tabs/tabs.css
web/core/profiles/demo_umami/themes/umami/css/components/views/promoted-items.css
web/core/tests/Drupal/FunctionalJavascriptTests/Ajax/ElementValidationTest.php [new file with mode: 0644]
web/core/tests/Drupal/FunctionalJavascriptTests/Ajax/FormValuesTest.php [new file with mode: 0644]
web/core/tests/Drupal/FunctionalTests/BrowserTestBaseTest.php
web/core/tests/Drupal/FunctionalTests/Installer/InstallerDatabaseErrorMessagesTest.php
web/core/tests/Drupal/FunctionalTests/Installer/InstallerTranslationTest.php
web/core/tests/Drupal/KernelTests/AssertConfigTrait.php
web/core/tests/Drupal/KernelTests/Core/Common/SizeTest.php
web/core/tests/Drupal/KernelTests/Core/Entity/EntityFieldTest.php
web/core/tests/Drupal/KernelTests/Core/Entity/EntityQueryTest.php
web/core/tests/Drupal/KernelTests/Core/Plugin/EntityContextTypedDataTest.php [new file with mode: 0644]
web/core/tests/Drupal/KernelTests/Core/Theme/MessageTest.php
web/core/tests/Drupal/Tests/BrowserTestBase.php
web/core/tests/Drupal/Tests/Component/Utility/MailTest.php [new file with mode: 0644]
web/core/tests/Drupal/Tests/Core/Layout/LayoutPluginManagerTest.php
web/core/tests/Drupal/Tests/Core/Plugin/Context/ContextAwarePluginBaseTest.php [new file with mode: 0644]
web/core/tests/Drupal/Tests/Core/Routing/RouterTest.php [new file with mode: 0644]
web/core/tests/Drupal/Tests/UiHelperTrait.php
web/core/tests/Drupal/Tests/WebAssert.php
web/core/themes/bartik/bartik.theme
web/robots.txt
web/sites/example.sites.php

index bea1fd9b4533734521c31b58bcd975ed718c0500..f8d00ee3eef075b8e6ac810ab9f2dc90e2927d04 100644 (file)
@@ -19,7 +19,7 @@
         "composer/installers": "^1.2",
         "drupal-composer/drupal-scaffold": "^2.2",
         "cweagans/composer-patches": "~1.0",
-        "drupal/core": "^8.5.0",
+        "drupal/core": "8.6.4",
         "drupal/console": "^1.0",
         "drupal/token": "^1.0",
         "drupal/ctools": "^3.0",
index 7c7370821082d195877938a5d219cf61f9225fa8..62ee265946f8ed58afc3de1455545e1109561046 100644 (file)
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "8cfc3879a25f15d6aba3eaccfc3cb818",
+    "content-hash": "38bc6fe27fbd03929a4718f3c529c7d0",
     "packages": [
         {
             "name": "alchemy/zippy",
         },
         {
             "name": "drupal/core",
-            "version": "8.6.3",
+            "version": "8.6.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/drupal/core.git",
-                "reference": "9e9a1dd9e280ebaf10622217e54448b529167965"
+                "reference": "652bdf56b5e9e84648ea53ac7b1e7e73e7608ef4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/drupal/core/zipball/9e9a1dd9e280ebaf10622217e54448b529167965",
-                "reference": "9e9a1dd9e280ebaf10622217e54448b529167965",
+                "url": "https://api.github.com/repos/drupal/core/zipball/652bdf56b5e9e84648ea53ac7b1e7e73e7608ef4",
+                "reference": "652bdf56b5e9e84648ea53ac7b1e7e73e7608ef4",
                 "shasum": ""
             },
             "require": {
                 "GPL-2.0-or-later"
             ],
             "description": "Drupal is an open source content management platform powering millions of websites and applications.",
-            "time": "2018-11-07T14:45:40+00:00"
+            "time": "2018-12-05T11:58:02+00:00"
         },
         {
             "name": "drupal/crop",
index c65055a1cf99a04a1112ee83a8966698720df545..f1047d580ba30bd4fb89225c91f8bef9e1f94d2a 100644 (file)
     },
     {
         "name": "drupal/core",
-        "version": "8.6.3",
-        "version_normalized": "8.6.3.0",
+        "version": "8.6.4",
+        "version_normalized": "8.6.4.0",
         "source": {
             "type": "git",
             "url": "https://github.com/drupal/core.git",
-            "reference": "9e9a1dd9e280ebaf10622217e54448b529167965"
+            "reference": "652bdf56b5e9e84648ea53ac7b1e7e73e7608ef4"
         },
         "dist": {
             "type": "zip",
-            "url": "https://api.github.com/repos/drupal/core/zipball/9e9a1dd9e280ebaf10622217e54448b529167965",
-            "reference": "9e9a1dd9e280ebaf10622217e54448b529167965",
+            "url": "https://api.github.com/repos/drupal/core/zipball/652bdf56b5e9e84648ea53ac7b1e7e73e7608ef4",
+            "reference": "652bdf56b5e9e84648ea53ac7b1e7e73e7608ef4",
             "shasum": ""
         },
         "require": {
             "symfony/debug": "^3.4.0",
             "symfony/phpunit-bridge": "^3.4.3"
         },
-        "time": "2018-11-07T14:45:40+00:00",
+        "time": "2018-12-05T11:58:02+00:00",
         "type": "drupal-core",
         "extra": {
             "merge-plugin": {
index 3da80a17f2baf57fb40c2e57fbb1b9547e1486e6..054f7119b0eadf8955b3048a2c5aaad14257829a 100644 (file)
@@ -25,7 +25,7 @@
  */
 
 $url = parse_url($_SERVER['REQUEST_URI']);
-if (file_exists('.' . $url['path'])) {
+if (file_exists(__DIR__ . $url['path'])) {
   // Serve the requested resource as-is.
   return FALSE;
 }
@@ -38,7 +38,7 @@ if (strpos($path, '.php') !== FALSE) {
   // fallback to index.php.
   do {
     $path = dirname($path);
-    if (preg_match('/\.php$/', $path) && is_file('.' . $path)) {
+    if (preg_match('/\.php$/', $path) && is_file(__DIR__ . $path)) {
       // Discovered that the path contains an existing PHP file. Use that as the
       // script to include.
       $script = ltrim($path, '/');
index f56097cf3bb1d956443abd4632a93521d70803d1..e29eb69e4961ad0256c75760b105955f98c327a3 100644 (file)
@@ -284,6 +284,7 @@ Menu UI
 Migrate
 - Adam Globus-Hoenich 'phenaproxima' https://www.drupal.org/u/phenaproxima
 - Lucas Hedding 'heddn' https://www.drupal.org/u/heddn
+- Michael Lutz 'mikelutz' https://www.drupal.org/u/mikelutz
 - Markus Sipilä 'masipila' https://www.drupal.org/u/masipila
 - Vicki Spagnolo 'quietone' https://www.drupal.org/u/quietone
 - Maxime Turcotte 'maxocub' https://www.drupal.org/u/maxocub
index 49cf6e104a72aafac9435f9aaf721716d9ab9634..5090c45fa541a2ee8b240ef9a4565ede6f3b0a7e 100644 (file)
@@ -251,42 +251,46 @@ function check_url($uri) {
  *   A translated string representation of the size.
  */
 function format_size($size, $langcode = NULL) {
-  if ($size < Bytes::KILOBYTE) {
+  $absolute_size = abs($size);
+  if ($absolute_size < Bytes::KILOBYTE) {
     return \Drupal::translation()->formatPlural($size, '1 byte', '@count bytes', [], ['langcode' => $langcode]);
   }
-  else {
-    // Convert bytes to kilobytes.
-    $size = $size / Bytes::KILOBYTE;
-    $units = ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
-    foreach ($units as $unit) {
-      if (round($size, 2) >= Bytes::KILOBYTE) {
-        $size = $size / Bytes::KILOBYTE;
-      }
-      else {
-        break;
-      }
-    }
-    $args = ['@size' => round($size, 2)];
-    $options = ['langcode' => $langcode];
-    switch ($unit) {
-      case 'KB':
-        return new TranslatableMarkup('@size KB', $args, $options);
-      case 'MB':
-        return new TranslatableMarkup('@size MB', $args, $options);
-      case 'GB':
-        return new TranslatableMarkup('@size GB', $args, $options);
-      case 'TB':
-        return new TranslatableMarkup('@size TB', $args, $options);
-      case 'PB':
-        return new TranslatableMarkup('@size PB', $args, $options);
-      case 'EB':
-        return new TranslatableMarkup('@size EB', $args, $options);
-      case 'ZB':
-        return new TranslatableMarkup('@size ZB', $args, $options);
-      case 'YB':
-        return new TranslatableMarkup('@size YB', $args, $options);
+  // Create a multiplier to preserve the sign of $size.
+  $sign = $absolute_size / $size;
+  foreach (['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] as $unit) {
+    $absolute_size /= Bytes::KILOBYTE;
+    $rounded_size = round($absolute_size, 2);
+    if ($rounded_size < Bytes::KILOBYTE) {
+      break;
     }
   }
+  $args = ['@size' => $rounded_size * $sign];
+  $options = ['langcode' => $langcode];
+  switch ($unit) {
+    case 'KB':
+      return new TranslatableMarkup('@size KB', $args, $options);
+
+    case 'MB':
+      return new TranslatableMarkup('@size MB', $args, $options);
+
+    case 'GB':
+      return new TranslatableMarkup('@size GB', $args, $options);
+
+    case 'TB':
+      return new TranslatableMarkup('@size TB', $args, $options);
+
+    case 'PB':
+      return new TranslatableMarkup('@size PB', $args, $options);
+
+    case 'EB':
+      return new TranslatableMarkup('@size EB', $args, $options);
+
+    case 'ZB':
+      return new TranslatableMarkup('@size ZB', $args, $options);
+
+    case 'YB':
+      return new TranslatableMarkup('@size YB', $args, $options);
+  }
 }
 
 /**
@@ -490,7 +494,7 @@ function drupal_js_defaults($data = NULL) {
  *
  * Every condition is a key/value pair, whose key is a jQuery selector that
  * denotes another element on the page, and whose value is an array of
- * conditions, which must bet met on that element:
+ * conditions, which must be met on that element:
  * @code
  * array(
  *   'visible' => array(
index ebd52afbaa2d7826e2220849cc62bc40f67289df..014aea27f0f0a4eab65cbd32baf8203c58858c83 100644 (file)
@@ -82,7 +82,7 @@ class Drupal {
   /**
    * The current system version.
    */
-  const VERSION = '8.6.3';
+  const VERSION = '8.6.4';
 
   /**
    * Core API compatibility.
index b9f6e02217ad136bb9fe539152250d580f4dfe9c..2d499fd7dd16219f3109daf23a70bb7d3317d881 100644 (file)
@@ -3,6 +3,7 @@
 namespace Drupal\Component\Plugin;
 
 use Drupal\Component\Plugin\Context\ContextInterface;
+use Drupal\Component\Plugin\Definition\ContextAwarePluginDefinitionInterface;
 use Drupal\Component\Plugin\Exception\ContextException;
 use Drupal\Component\Plugin\Context\Context;
 use Symfony\Component\Validator\ConstraintViolationList;
@@ -67,7 +68,12 @@ abstract class ContextAwarePluginBase extends PluginBase implements ContextAware
    */
   public function getContextDefinitions() {
     $definition = $this->getPluginDefinition();
-    return !empty($definition['context']) ? $definition['context'] : [];
+    if ($definition instanceof ContextAwarePluginDefinitionInterface) {
+      return $definition->getContextDefinitions();
+    }
+    else {
+      return !empty($definition['context']) ? $definition['context'] : [];
+    }
   }
 
   /**
@@ -75,10 +81,15 @@ abstract class ContextAwarePluginBase extends PluginBase implements ContextAware
    */
   public function getContextDefinition($name) {
     $definition = $this->getPluginDefinition();
-    if (empty($definition['context'][$name])) {
-      throw new ContextException(sprintf("The %s context is not a valid context.", $name));
+    if ($definition instanceof ContextAwarePluginDefinitionInterface) {
+      if ($definition->hasContextDefinition($name)) {
+        return $definition->getContextDefinition($name);
+      }
+    }
+    elseif (!empty($definition['context'][$name])) {
+      return $definition['context'][$name];
     }
-    return $definition['context'][$name];
+    throw new ContextException(sprintf("The %s context is not a valid context.", $name));
   }
 
   /**
diff --git a/web/core/lib/Drupal/Component/Utility/Mail.php b/web/core/lib/Drupal/Component/Utility/Mail.php
new file mode 100644 (file)
index 0000000..423cfb2
--- /dev/null
@@ -0,0 +1,67 @@
+<?php
+
+namespace Drupal\Component\Utility;
+
+/**
+ * Provides helpers to ensure emails are compliant with RFCs.
+ *
+ * @ingroup utility
+ */
+class Mail {
+
+  /**
+   * RFC-2822 "specials" characters.
+   */
+  const RFC_2822_SPECIALS = '()<>[]:;@\,."';
+
+  /**
+   * Return a RFC-2822 compliant "display-name" component.
+   *
+   * The "display-name" component is used in mail header "Originator" fields
+   * (From, Sender, Reply-to) to give a human-friendly description of the
+   * address, i.e. From: My Display Name <xyz@example.org>. RFC-822 and
+   * RFC-2822 define its syntax and rules. This method gets as input a string
+   * to be used as "display-name" and formats it to be RFC compliant.
+   *
+   * @param string $string
+   *   A string to be used as "display-name".
+   *
+   * @return string
+   *   A RFC compliant version of the string, ready to be used as
+   *   "display-name" in mail originator header fields.
+   */
+  public static function formatDisplayName($string) {
+    // Make sure we don't process html-encoded characters. They may create
+    // unneeded trouble if left encoded, besides they will be correctly
+    // processed if decoded.
+    $string = Html::decodeEntities($string);
+
+    // If string contains non-ASCII characters it must be (short) encoded
+    // according to RFC-2047. The output of a "B" (Base64) encoded-word is
+    // always safe to be used as display-name.
+    $safe_display_name = Unicode::mimeHeaderEncode($string, TRUE);
+
+    // Encoded-words are always safe to be used as display-name because don't
+    // contain any RFC 2822 "specials" characters. However
+    // Unicode::mimeHeaderEncode() encodes a string only if it contains any
+    // non-ASCII characters, and leaves its value untouched (un-encoded) if
+    // ASCII only. For this reason in order to produce a valid display-name we
+    // still need to make sure there are no "specials" characters left.
+    if (preg_match('/[' . preg_quote(Mail::RFC_2822_SPECIALS) . ']/', $safe_display_name)) {
+
+      // If string is already quoted, it may or may not be escaped properly, so
+      // don't trust it and reset.
+      if (preg_match('/^"(.+)"$/', $safe_display_name, $matches)) {
+        $safe_display_name = str_replace(['\\\\', '\\"'], ['\\', '"'], $matches[1]);
+      }
+
+      // Transform the string in a RFC-2822 "quoted-string" by wrapping it in
+      // double-quotes. Also make sure '"' and '\' occurrences are escaped.
+      $safe_display_name = '"' . str_replace(['\\', '"'], ['\\\\', '\\"'], $safe_display_name) . '"';
+
+    }
+
+    return $safe_display_name;
+  }
+
+}
index a5f7fa9ed9d6997fafb7558fd7807082ead42846..bebdf09bd41041d863d41addd77e08484ac122de 100644 (file)
@@ -11,6 +11,7 @@ use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Language\LanguageInterface;
 use Drupal\Core\Plugin\PluginWithFormsInterface;
 use Drupal\Core\Plugin\PluginWithFormsTrait;
+use Drupal\Core\Render\PreviewFallbackInterface;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Component\Transliteration\TransliterationInterface;
 
@@ -23,7 +24,7 @@ use Drupal\Component\Transliteration\TransliterationInterface;
  *
  * @ingroup block_api
  */
-abstract class BlockBase extends ContextAwarePluginBase implements BlockPluginInterface, PluginWithFormsInterface {
+abstract class BlockBase extends ContextAwarePluginBase implements BlockPluginInterface, PluginWithFormsInterface, PreviewFallbackInterface {
 
   use ContextAwarePluginAssignmentTrait;
   use MessengerTrait;
@@ -252,6 +253,13 @@ abstract class BlockBase extends ContextAwarePluginBase implements BlockPluginIn
     return $transliterated;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getPreviewFallbackString() {
+    return $this->t('Placeholder for the "@block" block', ['@block' => $this->label()]);
+  }
+
   /**
    * Wraps the transliteration service.
    *
index 44bddea81da7979006223c434c33671a2ef6d4f2..90a0772005eaaa67e40f5816b6b696345a58f848 100644 (file)
@@ -33,7 +33,7 @@ abstract class Tasks {
     ],
     [
       'arguments'   => [
-        'CREATE TABLE {drupal_install_test} (id int NULL)',
+        'CREATE TABLE {drupal_install_test} (id int NOT NULL PRIMARY KEY)',
         'Drupal can use CREATE TABLE database commands.',
         'Failed to <strong>CREATE</strong> a test table on your database server with the command %query. The server reports the following message: %error.<p>Are you sure the configured username has the necessary permissions to create tables in the database?</p>',
         TRUE,
index cd36418d14ea62291363bc84db9d377d6cd9a875..6783522ac9baca6262bd5f3c57a383c042c19c0d 100644 (file)
@@ -8,6 +8,7 @@ use Drupal\Core\Entity\EntityDisplayPluginCollection;
 use Drupal\Core\Entity\EntityStorageInterface;
 use Drupal\Core\Entity\FieldableEntityInterface;
 use Drupal\Core\Entity\EntityDisplayBase;
+use Drupal\Core\Render\Element;
 use Drupal\Core\TypedData\TranslatableInterface as TranslatableDataInterface;
 
 /**
@@ -269,7 +270,7 @@ class EntityViewDisplay extends EntityDisplayBase implements EntityViewDisplayIn
     foreach ($entities as $id => $entity) {
       // Assign the configured weights.
       foreach ($this->getComponents() as $name => $options) {
-        if (isset($build_list[$id][$name])) {
+        if (isset($build_list[$id][$name]) && !Element::isEmpty($build_list[$id][$name])) {
           $build_list[$id][$name]['#weight'] = $options['weight'];
         }
       }
index 093c77dd16b110db2f1f78c7852e155d87243002..55a8bba0b988e2216dfb97035248470865e8f94d 100644 (file)
@@ -2,7 +2,9 @@
 
 namespace Drupal\Core\Entity\Plugin\DataType;
 
+use Drupal\Core\Config\TypedConfigManagerInterface;
 use Drupal\Core\TypedData\Exception\MissingDataException;
+use Drupal\Core\TypedData\TypedDataManagerInterface;
 
 /**
  * Enhances EntityAdapter for config entities.
@@ -16,6 +18,13 @@ class ConfigEntityAdapter extends EntityAdapter {
    */
   protected $entity;
 
+  /**
+   * The typed config manager.
+   *
+   * @var \Drupal\Core\Config\TypedConfigManagerInterface
+   */
+  protected $typedConfigManager;
+
   /**
    * {@inheritdoc}
    */
@@ -68,10 +77,31 @@ class ConfigEntityAdapter extends EntityAdapter {
   }
 
   /**
-   * Gets the typed data manager.
+   * Gets the typed config manager.
    *
    * @return \Drupal\Core\Config\TypedConfigManagerInterface
-   *   The typed data manager.
+   *   The typed config manager.
+   */
+  protected function getTypedConfigManager() {
+    if (empty($this->typedConfigManager)) {
+      // Use the typed data manager if it is also the typed config manager.
+      // @todo Remove this in https://www.drupal.org/node/3011137.
+      $typed_data_manager = $this->getTypedDataManager();
+      if ($typed_data_manager instanceof TypedConfigManagerInterface) {
+        $this->typedConfigManager = $typed_data_manager;
+      }
+      else {
+        $this->typedConfigManager = \Drupal::service('config.typed');
+      }
+    }
+
+    return $this->typedConfigManager;
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * @todo Remove this in https://www.drupal.org/node/3011137.
    */
   public function getTypedDataManager() {
     if (empty($this->typedDataManager)) {
@@ -81,6 +111,19 @@ class ConfigEntityAdapter extends EntityAdapter {
     return $this->typedDataManager;
   }
 
+  /**
+   * {@inheritdoc}
+   *
+   * @todo Remove this in https://www.drupal.org/node/3011137.
+   */
+  public function setTypedDataManager(TypedDataManagerInterface $typed_data_manager) {
+    $this->typedDataManager = $typed_data_manager;
+    if ($typed_data_manager instanceof TypedConfigManagerInterface) {
+      $this->typedConfigManager = $typed_data_manager;
+    }
+    return $this;
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -97,7 +140,7 @@ class ConfigEntityAdapter extends EntityAdapter {
    *   The typed data.
    */
   protected function getConfigTypedData() {
-    return $this->getTypedDataManager()->createFromNameAndData($this->entity->getConfigDependencyName(), $this->entity->toArray());
+    return $this->getTypedConfigManager()->createFromNameAndData($this->entity->getConfigDependencyName(), $this->entity->toArray());
   }
 
 }
index a9ff4b136dcc6aac9f4e0db3b6944b12e889a865..1f99d0f7716b74c734e6ffb99d7fa750964fee5a 100644 (file)
@@ -184,6 +184,7 @@ class Tables implements TablesInterface {
         // finds the property first. The data table is preferred, which is why
         // it gets added before the base table.
         $entity_tables = [];
+        $revision_table = NULL;
         if ($all_revisions && $field_storage && $field_storage->isRevisionable()) {
           $data_table = $entity_type->getRevisionDataTable();
           $entity_base_table = $entity_type->getRevisionTable();
@@ -191,11 +192,18 @@ class Tables implements TablesInterface {
         else {
           $data_table = $entity_type->getDataTable();
           $entity_base_table = $entity_type->getBaseTable();
+
+          if ($field_storage && $field_storage->isRevisionable() && in_array($field_storage->getName(), $entity_type->getRevisionMetadataKeys())) {
+            $revision_table = $entity_type->getRevisionTable();
+          }
         }
         if ($data_table) {
           $this->sqlQuery->addMetaData('simple_query', FALSE);
           $entity_tables[$data_table] = $this->getTableMapping($data_table, $entity_type_id);
         }
+        if ($revision_table) {
+          $entity_tables[$revision_table] = $this->getTableMapping($revision_table, $entity_type_id);
+        }
         $entity_tables[$entity_base_table] = $this->getTableMapping($entity_base_table, $entity_type_id);
         $sql_column = $specifier;
 
index 3fdf81b7488f2d9a0e2e6fee32cf413968cb680b..48545d22ded94b4a7f6b651200196095f18458a5 100644 (file)
@@ -821,10 +821,13 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
       if ($update) {
         $default_revision = $entity->isDefaultRevision();
         if ($default_revision) {
+          // Remove the ID from the record to enable updates on SQL variants
+          // that prevent updating serial columns, for example, mssql.
+          unset($record->{$this->idKey});
           $this->database
             ->update($this->baseTable)
             ->fields((array) $record)
-            ->condition($this->idKey, $record->{$this->idKey})
+            ->condition($this->idKey, $entity->get($this->idKey)->value)
             ->execute();
         }
         if ($this->revisionTable) {
@@ -833,11 +836,15 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
           }
           else {
             $record = $this->mapToStorageRecord($entity->getUntranslated(), $this->revisionTable);
+            // Remove the revision ID from the record to enable updates on SQL
+            // variants that prevent updating serial columns, for example,
+            // mssql.
+            unset($record->{$this->revisionKey});
             $entity->preSaveRevision($this, $record);
             $this->database
               ->update($this->revisionTable)
               ->fields((array) $record)
-              ->condition($this->revisionKey, $record->{$this->revisionKey})
+              ->condition($this->revisionKey, $entity->getRevisionId())
               ->execute();
           }
         }
@@ -1064,19 +1071,21 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
           ->condition($this->idKey, $record->{$this->idKey})
           ->execute();
       }
+      // Make sure to update the new revision key for the entity.
+      $entity->{$this->revisionKey}->value = $record->{$this->revisionKey};
     }
     else {
+      // Remove the revision ID from the record to enable updates on SQL
+      // variants that prevent updating serial columns, for example,
+      // mssql.
+      unset($record->{$this->revisionKey});
       $this->database
         ->update($this->revisionTable)
         ->fields((array) $record)
-        ->condition($this->revisionKey, $record->{$this->revisionKey})
+        ->condition($this->revisionKey, $entity->getRevisionId())
         ->execute();
     }
-
-    // Make sure to update the new revision key for the entity.
-    $entity->{$this->revisionKey}->value = $record->{$this->revisionKey};
-
-    return $record->{$this->revisionKey};
+    return $entity->getRevisionId();
   }
 
   /**
index ac137330f8dc7e1ae1e82663dca095741c7ef76f..65b913d125b45f56f28f512fb10ec204ccc10cd2 100644 (file)
@@ -811,10 +811,10 @@ function hook_entity_view_mode_info_alter(&$view_modes) {
  *   An associative array of all entity bundles, keyed by the entity
  *   type name, and then the bundle name, with the following keys:
  *   - label: The human-readable name of the bundle.
- *   - uri_callback: The same as the 'uri_callback' key defined for the entity
- *     type in the EntityManager, but for the bundle only. When determining
- *     the URI of an entity, if a 'uri_callback' is defined for both the
- *     entity type and the bundle, the one for the bundle is used.
+ *   - uri_callback: (optional) The same as the 'uri_callback' key defined for
+ *     the entity type in the EntityManager, but for the bundle only. When
+ *     determining the URI of an entity, if a 'uri_callback' is defined for both
+ *     the entity type and the bundle, the one for the bundle is used.
  *   - translatable: (optional) A boolean value specifying whether this bundle
  *     has translation support enabled. Defaults to FALSE.
  *
index 320005ceef003029eb1fc2b1539f1511b9432404..ef2f43c283e9edf302c369e5e41e70c98ff5c2ef 100644 (file)
@@ -124,7 +124,6 @@ class StringFormatter extends FormatterBase implements ContainerFactoryPluginInt
     $elements = [];
     $url = NULL;
     if ($this->getSetting('link_to_entity')) {
-      // For the default revision this falls back to 'canonical'.
       $url = $this->getEntityUrl($items->getEntity());
     }
 
@@ -173,8 +172,11 @@ class StringFormatter extends FormatterBase implements ContainerFactoryPluginInt
    *   The URI elements of the entity.
    */
   protected function getEntityUrl(EntityInterface $entity) {
-    // For the default revision this falls back to 'canonical'.
-    return $entity->toUrl('revision');
+    // For the default revision, the 'revision' link template falls back to
+    // 'canonical'.
+    // @see \Drupal\Core\Entity\Entity::toUrl()
+    $rel = $entity->getEntityType()->hasLinkTemplate('revision') ? 'revision' : 'canonical';
+    return $entity->toUrl($rel);
   }
 
 }
index a5a465293775b4b75c7d84899d44bcba1f28eb3a..2cd0c689573591a414b4732cc3be1bfc0412c6df 100644 (file)
@@ -13,6 +13,7 @@ use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator;
 use Drupal\Core\Plugin\Discovery\YamlDiscoveryDecorator;
 use Drupal\Core\Layout\Annotation\Layout;
 use Drupal\Core\Plugin\FilteredPluginManagerTrait;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 
 /**
  * Provides a plugin manager for layouts.
@@ -71,6 +72,10 @@ class LayoutPluginManager extends DefaultPluginManager implements LayoutPluginMa
     if (!$this->discovery) {
       $discovery = new AnnotatedClassDiscovery($this->subdir, $this->namespaces, $this->pluginDefinitionAnnotationName, $this->additionalAnnotationNamespaces);
       $discovery = new YamlDiscoveryDecorator($discovery, 'layouts', $this->moduleHandler->getModuleDirectories() + $this->themeHandler->getThemeDirectories());
+      $discovery
+        ->addTranslatableProperty('label')
+        ->addTranslatableProperty('description')
+        ->addTranslatableProperty('category');
       $discovery = new AnnotationBridgeDecorator($discovery, $this->pluginDefinitionAnnotationName);
       $discovery = new ContainerDerivativeDiscoveryDecorator($discovery);
       $this->discovery = $discovery;
@@ -140,6 +145,15 @@ class LayoutPluginManager extends DefaultPluginManager implements LayoutPluginMa
     if (!$definition->getDefaultRegion()) {
       $definition->setDefaultRegion(key($definition->getRegions()));
     }
+    // Makes sure region names are translatable.
+    $regions = array_map(function ($region) {
+      if (!$region['label'] instanceof TranslatableMarkup) {
+        // Region labels from YAML discovery needs translation.
+        $region['label'] = new TranslatableMarkup($region['label'], [], ['context' => 'layout_region']);
+      }
+      return $region;
+    }, $definition->getRegions());
+    $definition->setRegions($regions);
   }
 
   /**
index bbc0432c6da4df16c23f6f6a6127d81af1b38a06..5cf3c9ea05b1b0e0872235941c05659ac43ef054 100644 (file)
@@ -5,7 +5,7 @@ namespace Drupal\Core\Mail;
 use Drupal\Component\Render\MarkupInterface;
 use Drupal\Component\Render\PlainTextOutput;
 use Drupal\Component\Utility\Html;
-use Drupal\Component\Utility\Unicode;
+use Drupal\Component\Utility\Mail as MailHelper;
 use Drupal\Core\Logger\LoggerChannelFactoryInterface;
 use Drupal\Core\Messenger\MessengerTrait;
 use Drupal\Core\Plugin\DefaultPluginManager;
@@ -254,12 +254,8 @@ class MailManager extends DefaultPluginManager implements MailManagerInterface {
     // Return-Path headers should have a domain authorized to use the
     // originating SMTP server.
     $headers['Sender'] = $headers['Return-Path'] = $site_mail;
-    // Headers are usually encoded in the mail plugin that implements
-    // \Drupal\Core\Mail\MailInterface::mail(), for example,
-    // \Drupal\Core\Mail\Plugin\Mail\PhpMail::mail(). The site name must be
-    // encoded here to prevent mail plugins from encoding the email address,
-    // which would break the header.
-    $headers['From'] = Unicode::mimeHeaderEncode($site_config->get('name'), TRUE) . ' <' . $site_mail . '>';
+    // Make sure the site-name is a RFC-2822 compliant 'display-name'.
+    $headers['From'] = MailHelper::formatDisplayName($site_config->get('name')) . ' <' . $site_mail . '>';
     if ($reply) {
       $headers['Reply-to'] = $reply;
     }
index a02bc3534d0157ba61813f0dad1a9b8e567acdf5..b6898487197d0b2d9b67644918613fcedc9abb96 100644 (file)
@@ -106,11 +106,11 @@ class AliasStorage implements AliasStorageInterface {
         $this->catchException($e);
         $original = FALSE;
       }
-      $fields['pid'] = $pid;
       $query = $this->connection->update(static::TABLE)
         ->fields($fields)
         ->condition('pid', $pid);
       $pid = $query->execute();
+      $fields['pid'] = $pid;
       $fields['original'] = $original;
       $operation = 'update';
     }
index 80f10d202149dff797e5ea8e1a2158ec06d56925..ed8abf17fad0af571d1a68a6723cae3cf1200958 100644 (file)
@@ -70,7 +70,7 @@ abstract class ContextAwarePluginBase extends ComponentContextAwarePluginBase im
    * {@inheritdoc}
    */
   public function setContextValue($name, $value) {
-    $this->context[$name] = Context::createFromContext($this->getContext($name), $value);
+    $this->setContext($name, Context::createFromContext($this->getContext($name), $value));
     return $this;
   }
 
index a5a28429812f4a0152c834071f1171366499bd1d..d8627343eea01bb766de507d518bd0c139492fa7 100644 (file)
@@ -76,7 +76,9 @@ class StatusMessages extends RenderElement {
   public static function renderMessages($type = NULL) {
     $render = [];
     if (isset($type)) {
-      $messages = \Drupal::messenger()->deleteByType($type);
+      $messages = [
+        $type => \Drupal::messenger()->deleteByType($type),
+      ];
     }
     else {
       $messages = \Drupal::messenger()->deleteAll();
diff --git a/web/core/lib/Drupal/Core/Render/PreviewFallbackInterface.php b/web/core/lib/Drupal/Core/Render/PreviewFallbackInterface.php
new file mode 100644 (file)
index 0000000..cbaf6ac
--- /dev/null
@@ -0,0 +1,21 @@
+<?php
+
+namespace Drupal\Core\Render;
+
+/**
+ * Allows an element to provide a fallback representation of itself for preview.
+ */
+interface PreviewFallbackInterface {
+
+  /**
+   * Returns a string to be used as a fallback during preview.
+   *
+   * This is typically used when an element has no output and must be displayed,
+   * for example during configuration.
+   *
+   * @return string|\Drupal\Core\StringTranslation\TranslatableMarkup
+   *   A string representing for this.
+   */
+  public function getPreviewFallbackString();
+
+}
index ee2dc28acd041f38549f84c272d61a874fab2cb3..c6c5dc0aa7662d805ee1688f0c80adc8f800d017 100644 (file)
@@ -125,6 +125,7 @@ class Router extends UrlMatcher implements RequestMatcherInterface, RouterInterf
       throw new ResourceNotFoundException(sprintf('No routes found for "%s".', $this->currentPath->getPath()));
     }
     $collection = $this->applyRouteFilters($collection, $request);
+    $collection = $this->applyFitOrder($collection);
 
     if ($ret = $this->matchCollection(rawurldecode($this->currentPath->getPath($request)), $collection)) {
       return $this->applyRouteEnhancers($ret, $request);
@@ -286,6 +287,44 @@ class Router extends UrlMatcher implements RequestMatcherInterface, RouterInterf
     return $collection;
   }
 
+  /**
+   * Reapplies the fit order to a RouteCollection object.
+   *
+   * Route filters can reorder route collections. For example, routes with an
+   * explicit _format requirement will be preferred. This can result in a less
+   * fit route being used. For example, as a result of filtering /user/% comes
+   * before /user/login. In order to not break this fundamental property of
+   * routes, we need to reapply the fit order. We also need to ensure that order
+   * within each group of the same fit is preserved.
+   *
+   * @param \Symfony\Component\Routing\RouteCollection $collection
+   *   The route collection.
+   *
+   * @return \Symfony\Component\Routing\RouteCollection
+   *   The reordered route collection.
+   */
+  protected function applyFitOrder(RouteCollection $collection) {
+    $buckets = [];
+    // Sort all the routes by fit descending.
+    foreach ($collection->all() as $name => $route) {
+      $fit = $route->compile()->getFit();
+      $buckets += [$fit => []];
+      $buckets[$fit][] = [$name, $route];
+    }
+    krsort($buckets);
+
+    $flattened = array_reduce($buckets, 'array_merge', []);
+
+    // Add them back onto a new route collection.
+    $collection = new RouteCollection();
+    foreach ($flattened as $pair) {
+      $name = $pair[0];
+      $route = $pair[1];
+      $collection->add($name, $route);
+    }
+    return $collection;
+  }
+
   /**
    * {@inheritdoc}
    */
index 607103109dbcc809520b236665fd2ca0b4377670..79813986786bc1567d495d1cc0ba2f63151c2eea 100644 (file)
@@ -218,6 +218,11 @@ class SessionManager extends NativeSessionStorage implements SessionManagerInter
 
     if ($this->isStarted()) {
       $old_session_id = $this->getId();
+      // Save and close the old session. Call the parent method to avoid issue
+      // with session destruction due to the session being considered obsolete.
+      parent::save();
+      // Ensure the session is reloaded correctly.
+      $this->startedLazy = TRUE;
     }
     session_id(Crypt::randomBytesBase64());
 
@@ -230,10 +235,7 @@ class SessionManager extends NativeSessionStorage implements SessionManagerInter
       $this->migrateStoredSession($old_session_id);
     }
 
-    if (!$this->isStarted()) {
-      // Start the session when it doesn't exist yet.
-      $this->startNow();
-    }
+    $this->startNow();
   }
 
   /**
index ea7ec3fc3725d655e223167106f9aacc3a64a3b0..606a0dc9e8db4952383cc2a6e892cc0c68854995 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace Drupal\Core\TempStore;
 
+use Drupal\Core\DependencyInjection\DependencySerializationTrait;
 use Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface;
 use Drupal\Core\Lock\LockBackendInterface;
 use Drupal\Core\Session\AccountProxyInterface;
@@ -27,6 +28,7 @@ use Symfony\Component\HttpFoundation\RequestStack;
  * \Drupal\Core\TempStore\SharedTempStore.
  */
 class PrivateTempStore {
+  use DependencySerializationTrait;
 
   /**
    * The key/value storage object used for this data.
index a5d453ae58e5a6b8bf3b879e0bf5ddb7f78f6496..449996710eb60a9fa54d933801b7f2b713e9105d 100644 (file)
@@ -126,10 +126,16 @@ class RecursiveContextualValidator implements ContextualValidatorInterface {
     $metadata = $this->metadataFactory->getMetadataFor($data);
     $cache_key = spl_object_hash($data);
     $property_path = $is_root_call ? '' : PropertyPath::append($previous_path, $data->getName());
+
+    // Prefer a specific instance of the typed data manager stored by the data
+    // if it is available. This is necessary for specialized typed data objects,
+    // for example those using the typed config subclass of the manager.
+    $typed_data_manager = method_exists($data, 'getTypedDataManager') ? $data->getTypedDataManager() : $this->typedDataManager;
+
     // Pass the canonical representation of the data as validated value to
     // constraint validators, such that they do not have to care about Typed
     // Data.
-    $value = $this->typedDataManager->getCanonicalRepresentation($data);
+    $value = $typed_data_manager->getCanonicalRepresentation($data);
     $this->context->setNode($value, $data, $metadata, $property_path);
 
     if (isset($constraints) || !$this->context->isGroupValidated($cache_key, Constraint::DEFAULT_GROUP)) {
index 9e26be5fc223b0934cd1cd25721d0d37c7f0d3b9..c43d1dece3cfbe72588d1d030b24da2a0ec4953a 100644 (file)
 
   // Expose constructor in the public space.
   Drupal.TableHeader = TableHeader;
-})(jQuery, Drupal, window.parent.Drupal.displace);
+})(jQuery, Drupal, window.Drupal.displace);
index 1fd60086db89dc1a0516c7d2c3944f725c9d9926..d5ad0a235e699fca2d6b4590c519ea4f7564fe6d 100644 (file)
   });
 
   Drupal.TableHeader = TableHeader;
-})(jQuery, Drupal, window.parent.Drupal.displace);
\ No newline at end of file
+})(jQuery, Drupal, window.Drupal.displace);
\ No newline at end of file
index f42248975ae9f27eb4b3bc29f67033567575781b..00827a223c94e0faaa8dcb38e329dc4cea079345 100644 (file)
@@ -70,9 +70,9 @@ abstract class AggregatorTestBase extends WebTestBase {
     $view_link = $this->xpath('//div[@class="messages"]//a[contains(@href, :href)]', [':href' => 'aggregator/sources/']);
     $this->assert(isset($view_link), 'The message area contains a link to a feed');
 
-    $fid = db_query("SELECT fid FROM {aggregator_feed} WHERE title = :title AND url = :url", [':title' => $edit['title[0][value]'], ':url' => $edit['url[0][value]']])->fetchField();
-    $this->assertTrue(!empty($fid), 'The feed found in database.');
-    return Feed::load($fid);
+    $fids = \Drupal::entityQuery('aggregator_feed')->condition('title', $edit['title[0][value]'])->condition('url', $edit['url[0][value]'])->execute();
+    $this->assertNotEmpty($fids, 'The feed found in database.');
+    return Feed::load(array_values($fids)[0]);
   }
 
   /**
@@ -179,10 +179,10 @@ abstract class AggregatorTestBase extends WebTestBase {
     $this->clickLink('Update items');
 
     // Ensure we have the right number of items.
-    $result = db_query('SELECT iid FROM {aggregator_item} WHERE fid = :fid', [':fid' => $feed->id()]);
+    $iids = \Drupal::entityQuery('aggregator_item')->condition('fid', $feed->id())->execute();
     $feed->items = [];
-    foreach ($result as $item) {
-      $feed->items[] = $item->iid;
+    foreach ($iids as $iid) {
+      $feed->items[] = $iid;
     }
 
     if ($expected_count !== NULL) {
@@ -211,11 +211,12 @@ abstract class AggregatorTestBase extends WebTestBase {
    *   Expected number of feed items.
    */
   public function updateAndDelete(FeedInterface $feed, $expected_count) {
+    $count_query = \Drupal::entityQuery('aggregator_item')->condition('fid', $feed->id())->count();
     $this->updateFeedItems($feed, $expected_count);
-    $count = db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', [':fid' => $feed->id()])->fetchField();
+    $count = $count_query->execute();
     $this->assertTrue($count);
     $this->deleteFeedItems($feed);
-    $count = db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', [':fid' => $feed->id()])->fetchField();
+    $count = $count_query->execute();
     $this->assertTrue($count == 0);
   }
 
@@ -231,7 +232,7 @@ abstract class AggregatorTestBase extends WebTestBase {
    *   TRUE if feed is unique.
    */
   public function uniqueFeed($feed_name, $feed_url) {
-    $result = db_query("SELECT COUNT(*) FROM {aggregator_feed} WHERE title = :title AND url = :url", [':title' => $feed_name, ':url' => $feed_url])->fetchField();
+    $result = \Drupal::entityQuery('aggregator_feed')->condition('title', $feed_name)->condition('url', $feed_url)->count()->execute();
     return (1 == $result);
   }
 
index bb7c90ae32f238f02fe266ee3270e064755eb64d..4a3ca0b07d9016e048a41b7d6de59c3a60bf66e6 100644 (file)
@@ -20,31 +20,23 @@ class AggregatorCronTest extends AggregatorTestBase {
     // Create feed and test basic updating on cron.
     $this->createSampleNodes();
     $feed = $this->createFeed();
+    $count_query = \Drupal::entityQuery('aggregator_item')->condition('fid', $feed->id())->count();
+
     $this->cronRun();
-    $this->assertEqual(5, db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', [':fid' => $feed->id()])->fetchField());
+    $this->assertEqual(5, $count_query->execute());
     $this->deleteFeedItems($feed);
-    $this->assertEqual(0, db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', [':fid' => $feed->id()])->fetchField());
+    $this->assertEqual(0, $count_query->execute());
     $this->cronRun();
-    $this->assertEqual(5, db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', [':fid' => $feed->id()])->fetchField());
+    $this->assertEqual(5, $count_query->execute());
 
     // Test feed locking when queued for update.
     $this->deleteFeedItems($feed);
-    db_update('aggregator_feed')
-      ->condition('fid', $feed->id())
-      ->fields([
-        'queued' => REQUEST_TIME,
-      ])
-      ->execute();
+    $feed->setQueuedTime(REQUEST_TIME)->save();
     $this->cronRun();
-    $this->assertEqual(0, db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', [':fid' => $feed->id()])->fetchField());
-    db_update('aggregator_feed')
-      ->condition('fid', $feed->id())
-      ->fields([
-        'queued' => 0,
-      ])
-      ->execute();
+    $this->assertEqual(0, $count_query->execute());
+    $feed->setQueuedTime(0)->save();
     $this->cronRun();
-    $this->assertEqual(5, db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', [':fid' => $feed->id()])->fetchField());
+    $this->assertEqual(5, $count_query->execute());
   }
 
 }
index ddd98bb82f48fce4426fed8be7376a45e5abef8f..71c19031ac680682bcd22dd86b41cc9d965a093f 100644 (file)
@@ -67,9 +67,9 @@ abstract class AggregatorTestBase extends BrowserTestBase {
     $view_link = $this->xpath('//div[@class="messages"]//a[contains(@href, :href)]', [':href' => 'aggregator/sources/']);
     $this->assert(isset($view_link), 'The message area contains a link to a feed');
 
-    $fid = db_query("SELECT fid FROM {aggregator_feed} WHERE title = :title AND url = :url", [':title' => $edit['title[0][value]'], ':url' => $edit['url[0][value]']])->fetchField();
-    $this->assertTrue(!empty($fid), 'The feed found in database.');
-    return Feed::load($fid);
+    $fids = \Drupal::entityQuery('aggregator_feed')->condition('title', $edit['title[0][value]'])->condition('url', $edit['url[0][value]'])->execute();
+    $this->assertNotEmpty($fids, 'The feed found in database.');
+    return Feed::load(array_values($fids)[0]);
   }
 
   /**
@@ -176,10 +176,10 @@ abstract class AggregatorTestBase extends BrowserTestBase {
     $this->clickLink('Update items');
 
     // Ensure we have the right number of items.
-    $result = db_query('SELECT iid FROM {aggregator_item} WHERE fid = :fid', [':fid' => $feed->id()]);
+    $iids = \Drupal::entityQuery('aggregator_item')->condition('fid', $feed->id())->execute();
     $feed->items = [];
-    foreach ($result as $item) {
-      $feed->items[] = $item->iid;
+    foreach ($iids as $iid) {
+      $feed->items[] = $iid;
     }
 
     if ($expected_count !== NULL) {
@@ -208,11 +208,12 @@ abstract class AggregatorTestBase extends BrowserTestBase {
    *   Expected number of feed items.
    */
   public function updateAndDelete(FeedInterface $feed, $expected_count) {
+    $count_query = \Drupal::entityQuery('aggregator_item')->condition('fid', $feed->id())->count();
     $this->updateFeedItems($feed, $expected_count);
-    $count = db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', [':fid' => $feed->id()])->fetchField();
+    $count = $count_query->execute();
     $this->assertTrue($count);
     $this->deleteFeedItems($feed);
-    $count = db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', [':fid' => $feed->id()])->fetchField();
+    $count = $count_query->execute();
     $this->assertTrue($count == 0);
   }
 
@@ -228,7 +229,7 @@ abstract class AggregatorTestBase extends BrowserTestBase {
    *   TRUE if feed is unique.
    */
   public function uniqueFeed($feed_name, $feed_url) {
-    $result = db_query("SELECT COUNT(*) FROM {aggregator_feed} WHERE title = :title AND url = :url", [':title' => $feed_name, ':url' => $feed_url])->fetchField();
+    $result = \Drupal::entityQuery('aggregator_feed')->condition('title', $feed_name)->condition('url', $feed_url)->count()->execute();
     return (1 == $result);
   }
 
index 557720658da2555fb0174c73ae88526b6dd97360..7f6a4087f750c803a88aa253fb0ac584860d981c 100644 (file)
@@ -43,8 +43,8 @@ class DeleteFeedTest extends AggregatorTestBase {
     $this->assertResponse(404, 'Deleted feed source does not exist.');
 
     // Check database for feed.
-    $result = db_query("SELECT COUNT(*) FROM {aggregator_feed} WHERE title = :title AND url = :url", [':title' => $feed1->label(), ':url' => $feed1->getUrl()])->fetchField();
-    $this->assertFalse($result, 'Feed not found in database');
+    $result = \Drupal::entityQuery('aggregator_feed')->condition('title', $feed1->label())->condition('url', $feed1->getUrl())->count()->execute();
+    $this->assertEquals(0, $result, 'Feed not found in database');
   }
 
 }
index ecb2e50c76e587759d62324ed912974f8b733550..e340b27a9819cd5a01305c9761c0681fffce936e 100644 (file)
@@ -4,6 +4,7 @@ namespace Drupal\Tests\aggregator\Functional;
 
 use Drupal\Core\Url;
 use Drupal\aggregator\Entity\Feed;
+use Drupal\aggregator\Entity\Item;
 
 /**
  * Tests the built-in feed parser with valid feed samples.
@@ -57,16 +58,17 @@ class FeedParserTest extends AggregatorTestBase {
     $this->assertText('Atom-Powered Robots Run Amok');
     $this->assertLinkByHref('http://example.org/2003/12/13/atom03');
     $this->assertText('Some text.');
-    $this->assertEqual('urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a', db_query('SELECT guid FROM {aggregator_item} WHERE link = :link', [':link' => 'http://example.org/2003/12/13/atom03'])->fetchField(), 'Atom entry id element is parsed correctly.');
+    $iids = \Drupal::entityQuery('aggregator_item')->condition('link', 'http://example.org/2003/12/13/atom03')->execute();
+    $item = Item::load(array_values($iids)[0]);
+    $this->assertEqual('urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a', $item->getGuid(), 'Atom entry id element is parsed correctly.');
 
     // Check for second feed entry.
     $this->assertText('We tried to stop them, but we failed.');
     $this->assertLinkByHref('http://example.org/2003/12/14/atom03');
     $this->assertText('Some other text.');
-    $db_guid = db_query('SELECT guid FROM {aggregator_item} WHERE link = :link', [
-      ':link' => 'http://example.org/2003/12/14/atom03',
-    ])->fetchField();
-    $this->assertEqual('urn:uuid:1225c695-cfb8-4ebb-bbbb-80da344efa6a', $db_guid, 'Atom entry id element is parsed correctly.');
+    $iids = \Drupal::entityQuery('aggregator_item')->condition('link', 'http://example.org/2003/12/14/atom03')->execute();
+    $item = Item::load(array_values($iids)[0]);
+    $this->assertEqual('urn:uuid:1225c695-cfb8-4ebb-bbbb-80da344efa6a', $item->getGuid(), 'Atom entry id element is parsed correctly.');
   }
 
   /**
index 2500b9ebb541b19670ee6d215e10531c61679b2e..f9099e0eb8210bf54962ea88cec23f5ecede372d 100644 (file)
@@ -2,6 +2,8 @@
 
 namespace Drupal\Tests\aggregator\Functional;
 
+use Drupal\aggregator\Entity\Feed;
+
 /**
  * Tests OPML import.
  *
@@ -44,7 +46,8 @@ class ImportOpmlTest extends AggregatorTestBase {
    * Submits form filled with invalid fields.
    */
   public function validateImportFormFields() {
-    $before = db_query('SELECT COUNT(*) FROM {aggregator_feed}')->fetchField();
+    $count_query = \Drupal::entityQuery('aggregator_feed')->count();
+    $before = $count_query->execute();
 
     $edit = [];
     $this->drupalPostForm('admin/config/services/aggregator/add/opml', $edit, t('Import'));
@@ -62,7 +65,7 @@ class ImportOpmlTest extends AggregatorTestBase {
     $this->drupalPostForm('admin/config/services/aggregator/add/opml', $edit, t('Import'));
     $this->assertText(t('The URL invalidUrl://empty is not valid.'), 'Error if the URL is invalid.');
 
-    $after = db_query('SELECT COUNT(*) FROM {aggregator_feed}')->fetchField();
+    $after = $count_query->execute();
     $this->assertEqual($before, $after, 'No feeds were added during the three last form submissions.');
   }
 
@@ -70,7 +73,8 @@ class ImportOpmlTest extends AggregatorTestBase {
    * Submits form with invalid, empty, and valid OPML files.
    */
   protected function submitImportForm() {
-    $before = db_query('SELECT COUNT(*) FROM {aggregator_feed}')->fetchField();
+    $count_query = \Drupal::entityQuery('aggregator_feed')->count();
+    $before = $count_query->execute();
 
     $form['files[upload]'] = $this->getInvalidOpml();
     $this->drupalPostForm('admin/config/services/aggregator/add/opml', $form, t('Import'));
@@ -80,10 +84,12 @@ class ImportOpmlTest extends AggregatorTestBase {
     $this->drupalPostForm('admin/config/services/aggregator/add/opml', $edit, t('Import'));
     $this->assertText(t('No new feed has been added.'), 'Attempting to load empty OPML from remote URL.');
 
-    $after = db_query('SELECT COUNT(*) FROM {aggregator_feed}')->fetchField();
+    $after = $count_query->execute();
     $this->assertEqual($before, $after, 'No feeds were added during the two last form submissions.');
 
-    db_delete('aggregator_feed')->execute();
+    foreach (Feed::loadMultiple() as $feed) {
+      $feed->delete();
+    }
 
     $feeds[0] = $this->getFeedEditArray();
     $feeds[1] = $this->getFeedEditArray();
@@ -96,15 +102,15 @@ class ImportOpmlTest extends AggregatorTestBase {
     $this->assertRaw(t('A feed with the URL %url already exists.', ['%url' => $feeds[0]['url[0][value]']]), 'Verifying that a duplicate URL was identified');
     $this->assertRaw(t('A feed named %title already exists.', ['%title' => $feeds[1]['title[0][value]']]), 'Verifying that a duplicate title was identified');
 
-    $after = db_query('SELECT COUNT(*) FROM {aggregator_feed}')->fetchField();
+    $after = $count_query->execute();
     $this->assertEqual($after, 2, 'Verifying that two distinct feeds were added.');
 
-    $feeds_from_db = db_query("SELECT title, url, refresh FROM {aggregator_feed}");
+    $feed_entities = Feed::loadMultiple();
     $refresh = TRUE;
-    foreach ($feeds_from_db as $feed) {
-      $title[$feed->url] = $feed->title;
-      $url[$feed->title] = $feed->url;
-      $refresh = $refresh && $feed->refresh == 900;
+    foreach ($feed_entities as $feed_entity) {
+      $title[$feed_entity->getUrl()] = $feed_entity->label();
+      $url[$feed_entity->label()] = $feed_entity->getUrl();
+      $refresh = $refresh && $feed_entity->getRefreshRate() == 900;
     }
 
     $this->assertEqual($title[$feeds[0]['url[0][value]']], $feeds[0]['title[0][value]'], 'First feed was added correctly.');
index a7ec0edd5a33777f3da44ee05ca08c5c6cdfbd95..fd2a742443cb2cbf9d4fb181e56c99729f78ae6e 100644 (file)
@@ -3,6 +3,7 @@
 namespace Drupal\Tests\aggregator\Functional;
 
 use Drupal\aggregator\Entity\Feed;
+use Drupal\aggregator\Entity\Item;
 
 /**
  * Update feed items from a feed.
@@ -43,26 +44,24 @@ class UpdateFeedItemTest extends AggregatorTestBase {
     $view_link = $this->xpath('//div[@class="messages"]//a[contains(@href, :href)]', [':href' => 'aggregator/sources/']);
     $this->assert(isset($view_link), 'The message area contains a link to a feed');
 
-    $fid = db_query("SELECT fid FROM {aggregator_feed} WHERE url = :url", [':url' => $edit['url[0][value]']])->fetchField();
-    $feed = Feed::load($fid);
+    $fids = \Drupal::entityQuery('aggregator_feed')->condition('url', $edit['url[0][value]'])->execute();
+    $feed = Feed::load(array_values($fids)[0]);
 
     $feed->refreshItems();
-    $before = db_query('SELECT timestamp FROM {aggregator_item} WHERE fid = :fid', [':fid' => $feed->id()])->fetchField();
+    $iids = \Drupal::entityQuery('aggregator_item')->condition('fid', $feed->id())->execute();
+    $before = Item::load(array_values($iids)[0])->getPostedTime();
 
     // Sleep for 3 second.
     sleep(3);
-    db_update('aggregator_feed')
-      ->condition('fid', $feed->id())
-      ->fields([
-        'checked' => 0,
-        'hash' => '',
-        'etag' => '',
-        'modified' => 0,
-      ])
-      ->execute();
+    $feed
+      ->setLastCheckedTime(0)
+      ->setHash('')
+      ->setEtag('')
+      ->setLastModified(0)
+      ->save();
     $feed->refreshItems();
 
-    $after = db_query('SELECT timestamp FROM {aggregator_item} WHERE fid = :fid', [':fid' => $feed->id()])->fetchField();
+    $after = Item::load(array_values($iids)[0])->getPostedTime();
     $this->assertTrue($before === $after, format_string('Publish timestamp of feed item was not updated (@before === @after)', ['@before' => $before, '@after' => $after]));
 
     // Make sure updating items works even after uninstalling a module
index 197bc4beadd6460a4f9383152e68e76f2e613361..ff4d8eb20df24b7bb589b5473b55131371eebd4c 100644 (file)
@@ -93,7 +93,8 @@
    */
   Drupal.behaviors.blockHighlightPlacement = {
     attach(context, settings) {
-      if (settings.blockPlacement) {
+      // Ensure that the block we are attempting to scroll to actually exists.
+      if (settings.blockPlacement && $('.js-block-placed').length) {
         $(context)
           .find('[data-drupal-selector="edit-blocks"]')
           .once('block-highlight')
index 7cafed16ac64cf2108a40a7c33a39312cc0dadef..641f12d3f3a09c53b9d003657078e7ffc3750353 100644 (file)
@@ -41,7 +41,7 @@
 
   Drupal.behaviors.blockHighlightPlacement = {
     attach: function attach(context, settings) {
-      if (settings.blockPlacement) {
+      if (settings.blockPlacement && $('.js-block-placed').length) {
         $(context).find('[data-drupal-selector="edit-blocks"]').once('block-highlight').each(function () {
           var $container = $(this);
 
index c1ccc504dbcacc811770079ef20b7c60ed26e105..d2286fcdd3644cd9b90a490a45e0a3cb399e31b7 100644 (file)
@@ -354,6 +354,23 @@ class BlockListBuilder extends ConfigEntityListBuilder implements FormInterface
 
     if (isset($operations['delete'])) {
       $operations['delete']['title'] = $this->t('Remove');
+      // Block operation links should have the `block-placement` query string
+      // parameter removed to ensure that JavaScript does not receive a block
+      // name that has been recently removed.
+      foreach ($operations as $operation) {
+        /** @var \Drupal\Core\Url $url */
+        $url = $operation['url'];
+        $query = $url->getOption('query');
+        $destination = $query['destination'];
+
+        $destinationUrl = Url::fromUserInput($destination);
+        $destinationQuery = $destinationUrl->getOption('query');
+        unset($destinationQuery['block-placement']);
+
+        $destinationUrl->setOption('query', $destinationQuery);
+        $query['destination'] = $destinationUrl->toString();
+        $url->setOption('query', $query);
+      }
     }
     return $operations;
   }
diff --git a/web/core/modules/block/src/Plugin/migrate/source/d7/BlockTranslation.php b/web/core/modules/block/src/Plugin/migrate/source/d7/BlockTranslation.php
new file mode 100644 (file)
index 0000000..acf9e2b
--- /dev/null
@@ -0,0 +1,99 @@
+<?php
+
+namespace Drupal\block\Plugin\migrate\source\d7;
+
+use Drupal\block\Plugin\migrate\source\Block;
+
+/**
+ * Gets i18n block data from source database.
+ *
+ * @MigrateSource(
+ *   id = "d7_block_translation",
+ *   source_module = "i18n_block"
+ * )
+ */
+class BlockTranslation extends Block {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function query() {
+    // Let the parent set the block table to use, but do not use the parent
+    // query. Instead build a query so can use an inner join to the selected
+    // block table.
+    parent::query();
+    $query = $this->select('i18n_string', 'i18n')
+      ->fields('i18n')
+      ->fields('b', [
+        'bid',
+        'module',
+        'delta',
+        'theme',
+        'status',
+        'weight',
+        'region',
+        'custom',
+        'visibility',
+        'pages',
+        'title',
+        'cache',
+        'i18n_mode',
+      ])
+      ->fields('lt', [
+        'lid',
+        'translation',
+        'language',
+        'plid',
+        'plural',
+        'i18n_status',
+      ])
+      ->condition('i18n_mode', 1);
+    $query->leftjoin($this->blockTable, 'b', ('b.delta = i18n.objectid'));
+    $query->leftjoin('locales_target', 'lt', 'lt.lid = i18n.lid');
+    return $query;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function fields() {
+    return [
+      'bid' => $this->t('The block numeric identifier.'),
+      'module' => $this->t('The module providing the block.'),
+      'delta' => $this->t("The block's delta."),
+      'theme' => $this->t('Which theme the block is placed in.'),
+      'status' => $this->t('Block enabled status'),
+      'weight' => $this->t('Block weight within region'),
+      'region' => $this->t('Theme region within which the block is set'),
+      'visibility' => $this->t('Visibility'),
+      'pages' => $this->t('Pages list.'),
+      'title' => $this->t('Block title.'),
+      'cache' => $this->t('Cache rule.'),
+      'i18n_mode' => $this->t('Multilingual mode'),
+      'lid' => $this->t('Language string ID'),
+      'textgroup' => $this->t('A module defined group of translations'),
+      'context' => $this->t('Full string ID for quick search: type:objectid:property.'),
+      'objectid' => $this->t('Object ID'),
+      'type' => $this->t('Object type for this string'),
+      'property' => $this->t('Object property for this string'),
+      'objectindex' => $this->t('Integer value of Object ID'),
+      'format' => $this->t('The {filter_format}.format of the string'),
+      'translation' => $this->t('Translation'),
+      'language' => $this->t('Language code'),
+      'plid' => $this->t('Parent lid'),
+      'plural' => $this->t('Plural index number'),
+      'i18n_status' => $this->t('Translation needs update'),
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getIds() {
+    $ids['delta']['type'] = 'string';
+    $ids['delta']['alias'] = 'b';
+    $ids['language']['type'] = 'string';
+    return $ids;
+  }
+
+}
index 53b48662cd234f1d22ffcfef4914d12a201b901e..9fb33573c46cbf4bc42e3d0c68abc9093c145c09 100644 (file)
@@ -238,6 +238,30 @@ class BlockTest extends BlockTestBase {
     $this->assertNoRaw($block->id());
   }
 
+  /**
+   * Tests the block operation links.
+   */
+  public function testBlockOperationLinks() {
+    $this->drupalGet('admin/structure/block');
+    // Go to the select block form.
+    $this->clickLink('Place block');
+    // Select the first available block.
+    $this->clickLink('Place block');
+    // Finally place the block
+    $this->submitForm([], 'Save block');
+
+    $url = $this->getUrl();
+    $parsed = parse_url($url);
+    $this->assertContains('block-placement', $parsed['query']);
+
+    $this->clickLink('Remove');
+    $this->submitForm([], 'Remove');
+
+    $url = $this->getUrl();
+    $parsed = parse_url($url);
+    $this->assertTrue(empty($parsed['query']));
+  }
+
   /**
    * Tests that the block form has a theme selector when not passed via the URL.
    */
diff --git a/web/core/modules/block/tests/src/Kernel/Migrate/d7/MigrateBlockContentTranslationTest.php b/web/core/modules/block/tests/src/Kernel/Migrate/d7/MigrateBlockContentTranslationTest.php
new file mode 100644 (file)
index 0000000..0d082a8
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+
+namespace Drupal\Tests\block\Kernel\Migrate\d7;
+
+use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
+
+/**
+ * Tests migration of i18n block translations.
+ *
+ * @group migrate_drupal_7
+ */
+class MigrateBlockContentTranslationTest extends MigrateDrupal7TestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'node',
+    'text',
+    'aggregator',
+    'book',
+    'block',
+    'comment',
+    'forum',
+    'views',
+    'block_content',
+    'config_translation',
+    'content_translation',
+    'language',
+    'statistics',
+    'taxonomy',
+    // Required for translation migrations.
+    'migrate_drupal_multilingual',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->installConfig(['block']);
+    $this->installConfig(['block_content']);
+    $this->installEntitySchema('block_content');
+
+    $this->executeMigrations([
+      'language',
+      'd7_filter_format',
+      'block_content_type',
+      'block_content_body_field',
+      'd7_custom_block',
+      'd7_user_role',
+      'd7_block',
+      'd7_block_translation',
+    ]);
+    block_rebuild();
+  }
+
+  /**
+   * Tests the migration of block title translation.
+   */
+  public function testBlockContentTranslation() {
+    /** @var \Drupal\language\ConfigurableLanguageManagerInterface $language_manager */
+    $language_manager = $this->container->get('language_manager');
+
+    $config = $language_manager->getLanguageConfigOverride('fr', 'block.block.bartik_user_login');
+    $this->assertSame('fr - User login title', $config->get('settings.label'));
+  }
+
+}
index 6f07dc9c21e7bc38f865e94d2b2ba48574f2e9ff..d56b9ed19625ff0173bbbf200a93abd46cbdd07e 100644 (file)
@@ -112,10 +112,10 @@ class MigrateBlockTest extends MigrateDrupal7TestBase {
   public function testBlockMigration() {
     $this->assertEntity('bartik_system_main', 'system_main_block', [], '', 'content', 'bartik', 0, '', '0');
     $this->assertEntity('bartik_search_form', 'search_form_block', [], '', 'sidebar_first', 'bartik', -1, '', '0');
-    $this->assertEntity('bartik_user_login', 'user_login_block', [], '', 'sidebar_first', 'bartik', 0, '', '0');
+    $this->assertEntity('bartik_user_login', 'user_login_block', [], '', 'sidebar_first', 'bartik', 0, 'User login title', 'visible');
     $this->assertEntity('bartik_system_powered_by', 'system_powered_by_block', [], '', 'footer_fifth', 'bartik', 10, '', '0');
     $this->assertEntity('seven_system_main', 'system_main_block', [], '', 'content', 'seven', 0, '', '0');
-    $this->assertEntity('seven_user_login', 'user_login_block', [], '', 'content', 'seven', 10, '', '0');
+    $this->assertEntity('seven_user_login', 'user_login_block', [], '', 'content', 'seven', 10, 'User login title', 'visible');
 
     // The d7_custom_block migration should have migrated a block containing a
     // mildly amusing limerick. We'll need its UUID to determine
diff --git a/web/core/modules/block/tests/src/Kernel/Plugin/migrate/source/d7/BlockTranslationTest.php b/web/core/modules/block/tests/src/Kernel/Plugin/migrate/source/d7/BlockTranslationTest.php
new file mode 100644 (file)
index 0000000..01b2dce
--- /dev/null
@@ -0,0 +1,147 @@
+<?php
+
+namespace Drupal\Tests\block\Kernel\Plugin\migrate\source\d7;
+
+use Drupal\Tests\migrate\Kernel\MigrateSqlSourceTestBase;
+
+/**
+ * Tests i18n block source plugin.
+ *
+ * @covers \Drupal\block\Plugin\migrate\source\d7\BlockTranslation
+ *
+ * @group content_translation
+ */
+class BlockTranslationTest extends MigrateSqlSourceTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['block', 'migrate_drupal'];
+
+  /**
+   * {@inheritdoc}
+   */
+  public function providerSource() {
+
+    // The source data.
+    $tests[0]['source_data']['block'] = [
+      [
+        'bid' => 1,
+        'module' => 'system',
+        'delta' => 'main',
+        'theme' => 'bartik',
+        'status' => 1,
+        'weight' => 0,
+        'region' => 'content',
+        'custom' => '0',
+        'visibility' => 0,
+        'pages' => '',
+        'title' => '',
+        'cache' => -1,
+        'i18n_mode' => 0,
+      ],
+      [
+        'bid' => 2,
+        'module' => 'system',
+        'delta' => 'navigation',
+        'theme' => 'bartik',
+        'status' => 1,
+        'weight' => 0,
+        'region' => 'sidebar_first',
+        'custom' => '0',
+        'visibility' => 0,
+        'pages' => '',
+        'title' => 'Navigation',
+        'cache' => -1,
+        'i18n_mode' => 1,
+      ],
+    ];
+    $tests[0]['source_data']['block_role'] = [
+      [
+        'module' => 'block',
+        'delta' => 1,
+        'rid' => 2,
+      ],
+      [
+        'module' => 'block',
+        'delta' => 2,
+        'rid' => 2,
+      ],
+      [
+        'module' => 'block',
+        'delta' => 2,
+        'rid' => 100,
+      ],
+    ];
+    $tests[0]['source_data']['i18n_string'] = [
+      [
+        'lid' => 1,
+        'textgroup' => 'block',
+        'context' => '1',
+        'objectid' => 'navigation',
+        'type' => 'system',
+        'property' => 'title',
+        'objectindex' => 0,
+        'format' => '',
+      ],
+    ];
+
+    $tests[0]['source_data']['locales_target'] = [
+      [
+        'lid' => 1,
+        'translation' => 'fr - Navigation',
+        'language' => 'fr',
+        'plid' => 0,
+        'plural' => 0,
+        'i18n_status' => 0,
+      ],
+    ];
+    $tests[0]['source_data']['role'] = [
+      [
+        'rid' => 2,
+        'name' => 'authenticated user',
+      ],
+    ];
+    $tests[0]['source_data']['system'] = [
+      [
+        'filename' => 'modules/system/system.module',
+        'name' => 'system',
+        'type' => 'module',
+        'owner' => '',
+        'status' => '1',
+        'throttle' => '0',
+        'bootstrap' => '0',
+        'schema_version' => '7055',
+        'weight' => '0',
+        'info' => 'a:0:{}',
+      ],
+    ];
+    // The expected results.
+    $tests[0]['expected_data'] = [
+      [
+        'bid' => 2,
+        'module' => 'system',
+        'delta' => 'navigation',
+        'theme' => 'bartik',
+        'status' => 1,
+        'weight' => 0,
+        'region' => 'sidebar_first',
+        'custom' => '0',
+        'visibility' => 0,
+        'pages' => '',
+        'title' => 'Navigation',
+        'cache' => -1,
+        'i18n_mode' => 1,
+        'lid' => 1,
+        'translation' => 'fr - Navigation',
+        'language' => 'fr',
+        'plid' => 0,
+        'plural' => 0,
+        'i18n_status' => 0,
+      ],
+    ];
+
+    return $tests;
+  }
+
+}
index b0c96dab837ce78a3a18efb7cba016a816d6a255..9a82b5b0cb743bcd7b53129d5fc6665bcd86a8fc 100644 (file)
@@ -46,6 +46,17 @@ class MigrateCommentTypeTest extends MigrateDrupal7TestBase {
    * Tests the migrated comment types.
    */
   public function testMigration() {
+    $comment_fields = [
+      'comment' => 'Default comment setting',
+      'comment_default_mode' => 'Default display mode',
+      'comment_default_per_page' => 'Default comments per page',
+      'comment_anonymous' => 'Anonymous commenting',
+      'comment_subject_field' => 'Comment subject field',
+      'comment_preview' => 'Preview comment',
+      'comment_form_location' => 'Location of comment submission form',
+    ];
+    $this->assertArraySubset($comment_fields, $this->migration->getSourcePlugin()->fields());
+
     $this->assertEntity('comment_node_article', 'Article comment');
     $this->assertEntity('comment_node_blog', 'Blog entry comment');
     $this->assertEntity('comment_node_book', 'Book page comment');
index 9ab40eac05691fb70202c4bbfb4f26377690d5a2..8c6d3325089840426b950cfebeaef7f20fe03ad8 100644 (file)
@@ -184,11 +184,16 @@ class EntityOperations implements ContainerInjectionInterface {
     // Sync translations.
     if ($entity->getEntityType()->hasKey('langcode')) {
       $entity_langcode = $entity->language()->getId();
-      if (!$content_moderation_state->hasTranslation($entity_langcode)) {
-        $content_moderation_state->addTranslation($entity_langcode);
+      if ($entity->isDefaultTranslation()) {
+        $content_moderation_state->langcode = $entity_langcode;
       }
-      if ($content_moderation_state->language()->getId() !== $entity_langcode) {
-        $content_moderation_state = $content_moderation_state->getTranslation($entity_langcode);
+      else {
+        if (!$content_moderation_state->hasTranslation($entity_langcode)) {
+          $content_moderation_state->addTranslation($entity_langcode);
+        }
+        if ($content_moderation_state->language()->getId() !== $entity_langcode) {
+          $content_moderation_state = $content_moderation_state->getTranslation($entity_langcode);
+        }
       }
     }
 
index 64c5ad849796d21b3623719d3bea82b97f40f5f1..357c60c9b300e40db3153395059309174519edf6 100644 (file)
@@ -93,7 +93,7 @@ class ModerationStateFieldItemList extends FieldItemList {
     if ($entity->getEntityType()->hasKey('langcode')) {
       $langcode = $entity->language()->getId();
       if (!$content_moderation_state->hasTranslation($langcode)) {
-        $content_moderation_state->addTranslation($langcode);
+        $content_moderation_state->addTranslation($langcode, $content_moderation_state->toArray());
       }
       if ($content_moderation_state->language()->getId() !== $langcode) {
         $content_moderation_state = $content_moderation_state->getTranslation($langcode);
diff --git a/web/core/modules/content_moderation/tests/src/Functional/ModerationContentTranslationTest.php b/web/core/modules/content_moderation/tests/src/Functional/ModerationContentTranslationTest.php
new file mode 100644 (file)
index 0000000..4afa97d
--- /dev/null
@@ -0,0 +1,109 @@
+<?php
+
+namespace Drupal\Tests\content_moderation\Functional;
+
+use Drupal\Tests\BrowserTestBase;
+use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
+
+/**
+ * Test content_moderation functionality with content_translation.
+ *
+ * @group content_moderation
+ */
+class ModerationContentTranslationTest extends BrowserTestBase {
+
+  use ContentModerationTestTrait;
+
+  /**
+   * A user with permission to bypass access content.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $adminUser;
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = [
+    'node',
+    'locale',
+    'content_translation',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->drupalLogin($this->rootUser);
+    // Create an Article content type.
+    $this->drupalCreateContentType(['type' => 'article', 'name' => 'Article'])->save();
+    $edit = [
+      'predefined_langcode' => 'fr',
+    ];
+    $this->drupalPostForm('admin/config/regional/language/add', $edit, 'Add language');
+    // Enable content translation on articles.
+    $this->drupalGet('admin/config/regional/content-language');
+    $edit = [
+      'entity_types[node]' => TRUE,
+      'settings[node][article][translatable]' => TRUE,
+      'settings[node][article][settings][language][language_alterable]' => TRUE,
+    ];
+    $this->drupalPostForm(NULL, $edit, 'Save configuration');
+    // Adding languages requires a container rebuild in the test running
+    // environment so that multilingual services are used.
+    $this->rebuildContainer();
+  }
+
+  /**
+   * Tests existing translations being edited after enabling content moderation.
+   */
+  public function testModerationWithExistingContent() {
+    // Create a published article in English.
+    $edit = [
+      'title[0][value]' => 'Published English node',
+      'langcode[0][value]' => 'en',
+    ];
+    $this->drupalPostForm('node/add/article', $edit, 'Save');
+    $this->assertSession()->pageTextContains('Article Published English node has been created.');
+    $english_node = $this->drupalGetNodeByTitle('Published English node');
+
+    // Add a French translation.
+    $this->drupalGet('node/' . $english_node->id() . '/translations');
+    $this->clickLink('Add');
+    $edit = [
+      'title[0][value]' => 'Published French node',
+    ];
+    $this->drupalPostForm(NULL, $edit, 'Save (this translation)');
+    $this->assertSession()->pageTextContains('Article Published French node has been updated.');
+
+    // Install content moderation and enable moderation on Article node type.
+    \Drupal::service('module_installer')->install(['content_moderation']);
+    $workflow = $this->createEditorialWorkflow();
+    $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'article');
+    $workflow->save();
+    $this->drupalLogin($this->rootUser);
+
+    // Edit the English node.
+    $this->drupalGet('node/' . $english_node->id() . '/edit');
+    $this->assertSession()->statusCodeEquals(200);
+    $edit = [
+      'title[0][value]' => 'Published English new node',
+    ];
+    $this->drupalPostForm(NULL, $edit, 'Save');
+    $this->assertSession()->statusCodeEquals(200);
+    $this->assertSession()->pageTextContains('Article Published English new node has been updated.');
+    // Edit the French translation.
+    $this->drupalGet('fr/node/' . $english_node->id() . '/edit');
+    $this->assertSession()->statusCodeEquals(200);
+    $edit = [
+      'title[0][value]' => 'Published French new node',
+    ];
+    $this->drupalPostForm(NULL, $edit, 'Save (this translation)');
+    $this->assertSession()->statusCodeEquals(200);
+    $this->assertSession()->pageTextContains('Article Published French new node has been updated.');
+  }
+
+}
index 8574ad6a171b4c60ce082132835b0b084090e862..4b7d75be4dba4ab2db6258bccb6912c1650fee8d 100644 (file)
@@ -296,7 +296,7 @@ class ContentModerationStateTest extends KernelTestBase {
     // Create a French translation.
     $french_node = $english_node->addTranslation('fr', ['title' => 'French title']);
     $french_node->setUnpublished();
-    // Revision 1 (fr).
+    // Revision 2 (fr).
     $french_node->save();
     $french_node = $this->reloadEntity($english_node)->getTranslation('fr');
     $this->assertEquals('draft', $french_node->moderation_state->value);
@@ -305,7 +305,7 @@ class ContentModerationStateTest extends KernelTestBase {
     // Move English node to create another draft.
     $english_node = $this->reloadEntity($english_node);
     $english_node->moderation_state->value = 'draft';
-    // Revision 2 (en, fr).
+    // Revision 3 (en, fr).
     $english_node->save();
     $english_node = $this->reloadEntity($english_node);
     $this->assertEquals('draft', $english_node->moderation_state->value);
@@ -316,7 +316,7 @@ class ContentModerationStateTest extends KernelTestBase {
 
     // Publish the French node.
     $french_node->moderation_state->value = 'published';
-    // Revision 3 (en, fr).
+    // Revision 4 (en, fr).
     $french_node->save();
     $french_node = $this->reloadEntity($french_node)->getTranslation('fr');
     $this->assertTrue($french_node->isPublished());
@@ -327,7 +327,7 @@ class ContentModerationStateTest extends KernelTestBase {
 
     // Publish the English node.
     $english_node->moderation_state->value = 'published';
-    // Revision 4 (en, fr).
+    // Revision 5 (en, fr).
     $english_node->save();
     $english_node = $this->reloadEntity($english_node);
     $this->assertTrue($english_node->isPublished());
@@ -336,15 +336,15 @@ class ContentModerationStateTest extends KernelTestBase {
     $french_node = $this->reloadEntity($english_node)->getTranslation('fr');
     $this->assertTrue($french_node->isPublished());
     $french_node->moderation_state->value = 'draft';
-    // Revision 5 (en, fr).
+    // Revision 6 (en, fr).
     $french_node->save();
-    $french_node = $this->reloadEntity($english_node, 5)->getTranslation('fr');
+    $french_node = $this->reloadEntity($english_node, 6)->getTranslation('fr');
     $this->assertFalse($french_node->isPublished());
     $this->assertTrue($french_node->getTranslation('en')->isPublished());
 
     // Republish the French node.
     $french_node->moderation_state->value = 'published';
-    // Revision 6 (en, fr).
+    // Revision 7 (en, fr).
     $french_node->save();
     $french_node = $this->reloadEntity($english_node)->getTranslation('fr');
     $this->assertTrue($french_node->isPublished());
@@ -353,7 +353,7 @@ class ContentModerationStateTest extends KernelTestBase {
     $content_moderation_state = ContentModerationState::load(1);
     $content_moderation_state->set('moderation_state', 'draft');
     $content_moderation_state->setNewRevision(TRUE);
-    // Revision 7 (en, fr).
+    // Revision 8 (en, fr).
     $content_moderation_state->save();
     $english_node = $this->reloadEntity($french_node, $french_node->getRevisionId() + 1);
 
@@ -366,12 +366,12 @@ class ContentModerationStateTest extends KernelTestBase {
     $content_moderation_state = $content_moderation_state->getTranslation('fr');
     $content_moderation_state->set('moderation_state', 'draft');
     $content_moderation_state->setNewRevision(TRUE);
-    // Revision 8 (en, fr).
+    // Revision 9 (en, fr).
     $content_moderation_state->save();
 
     $english_node = $this->reloadEntity($english_node, $english_node->getRevisionId());
     $this->assertEquals('draft', $english_node->moderation_state->value);
-    $french_node = $this->reloadEntity($english_node, '8')->getTranslation('fr');
+    $french_node = $this->reloadEntity($english_node, '9')->getTranslation('fr');
     $this->assertEquals('draft', $french_node->moderation_state->value);
     // Switching the moderation state to an unpublished state should update the
     // entity.
@@ -380,7 +380,7 @@ class ContentModerationStateTest extends KernelTestBase {
     // Get the default english node.
     $english_node = $this->reloadEntity($english_node);
     $this->assertTrue($english_node->isPublished());
-    $this->assertEquals(6, $english_node->getRevisionId());
+    $this->assertEquals(7, $english_node->getRevisionId());
   }
 
   /**
@@ -416,25 +416,83 @@ class ContentModerationStateTest extends KernelTestBase {
 
   /**
    * Tests that entities with special languages can be moderated.
+   *
+   * @dataProvider moderationWithSpecialLanguagesTestCases
    */
-  public function testModerationWithSpecialLanguages() {
+  public function testModerationWithSpecialLanguages($original_language, $updated_language) {
     $workflow = $this->createEditorialWorkflow();
     $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_rev', 'entity_test_rev');
     $workflow->save();
 
     // Create a test entity.
     $entity = EntityTestRev::create([
-      'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
+      'langcode' => $original_language,
     ]);
     $entity->save();
     $this->assertEquals('draft', $entity->moderation_state->value);
 
     $entity->moderation_state->value = 'published';
+    $entity->langcode = $updated_language;
     $entity->save();
 
     $this->assertEquals('published', EntityTestRev::load($entity->id())->moderation_state->value);
   }
 
+  /**
+   * Test cases for ::testModerationWithSpecialLanguages().
+   */
+  public function moderationWithSpecialLanguagesTestCases() {
+    return [
+      'Not specified to not specified' => [
+        LanguageInterface::LANGCODE_NOT_SPECIFIED,
+        LanguageInterface::LANGCODE_NOT_SPECIFIED,
+      ],
+      'English to not specified' => [
+        'en',
+        LanguageInterface::LANGCODE_NOT_SPECIFIED,
+      ],
+      'Not specified to english' => [
+        LanguageInterface::LANGCODE_NOT_SPECIFIED,
+        'en',
+      ],
+    ];
+  }
+
+  /**
+   * Test changing the language of content without adding a translation.
+   */
+  public function testChangingContentLangcode() {
+    ConfigurableLanguage::createFromLangcode('fr')->save();
+    NodeType::create([
+      'type' => 'test_type',
+    ])->save();
+    $workflow = $this->createEditorialWorkflow();
+    $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'test_type');
+    $workflow->save();
+
+    $entity = Node::create([
+      'title' => 'Test node',
+      'langcode' => 'en',
+      'type' => 'test_type',
+    ]);
+    $entity->save();
+
+    $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity);
+    $this->assertCount(1, $entity->getTranslationLanguages());
+    $this->assertCount(1, $content_moderation_state->getTranslationLanguages());
+    $this->assertEquals('en', $entity->langcode->value);
+    $this->assertEquals('en', $content_moderation_state->langcode->value);
+
+    $entity->langcode = 'fr';
+    $entity->save();
+
+    $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity);
+    $this->assertCount(1, $entity->getTranslationLanguages());
+    $this->assertCount(1, $content_moderation_state->getTranslationLanguages());
+    $this->assertEquals('fr', $entity->langcode->value);
+    $this->assertEquals('fr', $content_moderation_state->langcode->value);
+  }
+
   /**
    * Tests that a non-translatable entity type with a langcode can be moderated.
    */
index 7b57bd704913e4e1c55b343c4ea251b328fb7fbb..0c8db872a5597cc3fdbe07115ef728bfc661d91d 100644 (file)
@@ -3,9 +3,11 @@
 namespace Drupal\Tests\content_moderation\Kernel;
 
 use Drupal\KernelTests\KernelTestBase;
+use Drupal\language\Entity\ConfigurableLanguage;
 use Drupal\node\Entity\Node;
 use Drupal\node\Entity\NodeType;
 use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
+use Drupal\workflows\Entity\Workflow;
 
 /**
  * @coversDefaultClass \Drupal\content_moderation\Plugin\Field\ModerationStateFieldItemList
@@ -64,6 +66,8 @@ class ModerationStateFieldItemListTest extends KernelTestBase {
     $this->testNode->save();
     \Drupal::entityTypeManager()->getStorage('node')->resetCache();
     $this->testNode = Node::load($this->testNode->id());
+
+    ConfigurableLanguage::createFromLangcode('de')->save();
   }
 
   /**
@@ -332,4 +336,37 @@ class ModerationStateFieldItemListTest extends KernelTestBase {
     ];
   }
 
+  /**
+   * Test the field item list when used with existing unmoderated content.
+   */
+  public function testWithExistingUnmoderatedContent() {
+    $node = Node::create([
+      'title' => 'Test title',
+      'type' => 'unmoderated',
+    ]);
+    $node->save();
+    $translation = $node->addTranslation('de', $node->toArray());
+    $translation->title = 'Translated';
+    $translation->save();
+
+    $workflow = Workflow::load('editorial');
+    $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'unmoderated');
+    $workflow->save();
+
+    // After enabling moderation, both the original node and translation should
+    // have a published moderation state.
+    $node = Node::load($node->id());
+    $translation = $node->getTranslation('de');
+    $this->assertEquals('published', $node->moderation_state->value);
+    $this->assertEquals('published', $translation->moderation_state->value);
+
+    // After the node has been updated, both the original node and translation
+    // should still have a value.
+    $node->title = 'Updated title';
+    $node->save();
+    $translation = $node->getTranslation('de');
+    $this->assertEquals('published', $node->moderation_state->value);
+    $this->assertEquals('published', $translation->moderation_state->value);
+  }
+
 }
diff --git a/web/core/modules/content_translation/migrations/d6_taxonomy_term_localized_translation.yml b/web/core/modules/content_translation/migrations/d6_taxonomy_term_localized_translation.yml
new file mode 100644 (file)
index 0000000..980baf4
--- /dev/null
@@ -0,0 +1,44 @@
+id: d6_taxonomy_term_localized_translation
+label: Taxonomy localized term translations
+migration_tags:
+  - Drupal 6
+  - Content
+  - Multilingual
+source:
+  plugin: d6_term_localized_translation
+  translations: true
+process:
+  # If you are using this file to build a custom migration consider removing
+  # the tid field to allow incremental migrations.
+  tid: tid
+  langcode: language
+  vid:
+    plugin: migration
+    migration: d6_taxonomy_vocabulary
+    source: vid
+  name:
+    -
+      plugin: callback
+      source:
+        - name_translated
+        - name
+      callable: array_filter
+    -
+      plugin: callback
+      callable: current
+  description:
+    -
+      plugin: callback
+      source:
+        - description_translated
+        - description
+      callable: array_filter
+    -
+      plugin: callback
+      callable: current
+destination:
+  plugin: entity:taxonomy_term
+  translations: true
+migration_dependencies:
+  required:
+    - d6_taxonomy_term
diff --git a/web/core/modules/content_translation/migrations/d7_block_translation.yml b/web/core/modules/content_translation/migrations/d7_block_translation.yml
new file mode 100644 (file)
index 0000000..a664ec1
--- /dev/null
@@ -0,0 +1,77 @@
+id: d7_block_translation
+label: Block translation
+migration_tags:
+  - Drupal 7
+  - Configuration
+  - Multilingual
+source:
+  plugin: d7_block_translation
+  constants:
+    dest_label: 'settings/label'
+process:
+  multilingual:
+    plugin: skip_on_empty
+    source: i18n_mode
+    method: row
+  langcode: language
+  property: constants/dest_label
+  translation: translation
+  id:
+    -
+      plugin: migration_lookup
+      migration: d7_block
+      source:
+        - module
+        - delta
+    -
+      plugin: skip_on_empty
+      method: row
+  # The plugin process is copied from d7_block.yml
+  plugin:
+    -
+      plugin: static_map
+      bypass: true
+      source:
+        - module
+        - delta
+      map:
+        book:
+          navigation: book_navigation
+        comment:
+          recent: views_block:comments_recent-block_1
+        forum:
+          active: forum_active_block
+          new: forum_new_block
+        # locale:
+        #  0: language_block
+        node:
+          syndicate: node_syndicate_block
+        search:
+          form: search_form_block
+        statistics:
+          popular: statistics_popular_block
+        system:
+          main: system_main_block
+          'powered-by': system_powered_by_block
+        user:
+          login: user_login_block
+          # 1: system_menu_block:tools
+          new: views_block:who_s_new-block_1
+          online: views_block:who_s_online-who_s_online_block
+    -
+      plugin: block_plugin_id
+    -
+      plugin: skip_on_empty
+      method: row
+  # The theme process is copied from d7_block.yml
+  theme:
+    plugin: block_theme
+    source:
+      - theme
+      - default_theme
+      - admin_theme
+destination:
+  plugin: entity:block
+migration_dependencies:
+  optional:
+    - d7_block
index 3e32406d28db764bb783c90f2236ffe3ee039e88..1baf584efe01333b26aa04cef3780e1dc97430d8 100644 (file)
@@ -108,7 +108,12 @@ class MigrateTaxonomyTermTranslationTest extends MigrateDrupal6TestBase {
 
     $this->assertArrayHasKey($tid, $this->treeData[$vid], "Term $tid exists in taxonomy tree");
     $term = $this->treeData[$vid][$tid];
-    $this->assertEquals($parent_ids, array_filter($term->parents), "Term $tid has correct parents in taxonomy tree");
+    // PostgreSQL, MySQL and SQLite may not return the parent terms in the same
+    // order so sort before testing.
+    sort($parent_ids);
+    $actual_terms = array_filter($term->parents);
+    sort($actual_terms);
+    $this->assertEquals($parent_ids, $actual_terms, "Term $tid has correct parents in taxonomy tree");
   }
 
   /**
similarity index 64%
rename from web/core/modules/field/src/Tests/EntityReference/EntityReferenceAdminTest.php
rename to web/core/modules/field/tests/src/Functional/EntityReference/EntityReferenceAdminTest.php
index 7a5e3ea4cc7e8e827ef0be09d9e8d0760c54c021..8510539c56834748ec363ea7ae1633545da5fd91 100644 (file)
@@ -1,20 +1,21 @@
 <?php
 
-namespace Drupal\field\Tests\EntityReference;
+namespace Drupal\Tests\field\Functional\EntityReference;
 
-use Drupal\field\Entity\FieldConfig;
+use Behat\Mink\Element\NodeElement;
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
-use Drupal\field_ui\Tests\FieldUiTestTrait;
+use Drupal\field\Entity\FieldConfig;
 use Drupal\node\Entity\Node;
-use Drupal\simpletest\WebTestBase;
 use Drupal\taxonomy\Entity\Vocabulary;
+use Drupal\Tests\BrowserTestBase;
+use Drupal\Tests\field_ui\Traits\FieldUiTestTrait;
 
 /**
  * Tests for the administrative UI.
  *
  * @group entity_reference
  */
-class EntityReferenceAdminTest extends WebTestBase {
+class EntityReferenceAdminTest extends BrowserTestBase {
 
   use FieldUiTestTrait;
 
@@ -65,145 +66,6 @@ class EntityReferenceAdminTest extends WebTestBase {
    */
   public function testFieldAdminHandler() {
     $bundle_path = 'admin/structure/types/manage/' . $this->type;
-
-    // First step: 'Add new field' on the 'Manage fields' page.
-    $this->drupalGet($bundle_path . '/fields/add-field');
-
-    // Check if the commonly referenced entity types appear in the list.
-    $this->assertOption('edit-new-storage-type', 'field_ui:entity_reference:node');
-    $this->assertOption('edit-new-storage-type', 'field_ui:entity_reference:user');
-
-    $this->drupalPostForm(NULL, [
-      'label' => 'Test label',
-      'field_name' => 'test',
-      'new_storage_type' => 'entity_reference',
-    ], t('Save and continue'));
-
-    // Node should be selected by default.
-    $this->assertFieldByName('settings[target_type]', 'node');
-
-    // Check that all entity types can be referenced.
-    $this->assertFieldSelectOptions('settings[target_type]', array_keys(\Drupal::entityManager()->getDefinitions()));
-
-    // Second step: 'Field settings' form.
-    $this->drupalPostForm(NULL, [], t('Save field settings'));
-
-    // The base handler should be selected by default.
-    $this->assertFieldByName('settings[handler]', 'default:node');
-
-    // The base handler settings should be displayed.
-    $entity_type_id = 'node';
-    // Check that the type label is correctly displayed.
-    $this->assertText('Content type');
-    $bundles = $this->container->get('entity_type.bundle.info')->getBundleInfo($entity_type_id);
-    foreach ($bundles as $bundle_name => $bundle_info) {
-      $this->assertFieldByName('settings[handler_settings][target_bundles][' . $bundle_name . ']');
-    }
-
-    reset($bundles);
-
-    // Test the sort settings.
-    // Option 0: no sort.
-    $this->assertFieldByName('settings[handler_settings][sort][field]', '_none');
-    $this->assertNoFieldByName('settings[handler_settings][sort][direction]');
-    // Option 1: sort by field.
-    $this->drupalPostAjaxForm(NULL, ['settings[handler_settings][sort][field]' => 'nid'], 'settings[handler_settings][sort][field]');
-    $this->assertFieldByName('settings[handler_settings][sort][direction]', 'ASC');
-
-    // Test that a non-translatable base field is a sort option.
-    $this->assertFieldByXPath("//select[@name='settings[handler_settings][sort][field]']/option[@value='nid']");
-    // Test that a translatable base field is a sort option.
-    $this->assertFieldByXPath("//select[@name='settings[handler_settings][sort][field]']/option[@value='title']");
-    // Test that a configurable field is a sort option.
-    $this->assertFieldByXPath("//select[@name='settings[handler_settings][sort][field]']/option[@value='body.value']");
-
-    // Set back to no sort.
-    $this->drupalPostAjaxForm(NULL, ['settings[handler_settings][sort][field]' => '_none'], 'settings[handler_settings][sort][field]');
-    $this->assertNoFieldByName('settings[handler_settings][sort][direction]');
-
-    // Third step: confirm.
-    $this->drupalPostForm(NULL, [
-      'required' => '1',
-      'settings[handler_settings][target_bundles][' . key($bundles) . ']' => key($bundles),
-    ], t('Save settings'));
-
-    // Check that the field appears in the overview form.
-    $this->assertFieldByXPath('//table[@id="field-overview"]//tr[@id="field-test"]/td[1]', 'Test label', 'Field was created and appears in the overview page.');
-
-    // Check that the field settings form can be submitted again, even when the
-    // field is required.
-    // The first 'Edit' link is for the Body field.
-    $this->clickLink(t('Edit'), 1);
-    $this->drupalPostForm(NULL, [], t('Save settings'));
-
-    // Switch the target type to 'taxonomy_term' and check that the settings
-    // specific to its selection handler are displayed.
-    $field_name = 'node.' . $this->type . '.field_test';
-    $edit = [
-      'settings[target_type]' => 'taxonomy_term',
-    ];
-    $this->drupalPostForm($bundle_path . '/fields/' . $field_name . '/storage', $edit, t('Save field settings'));
-    $this->drupalGet($bundle_path . '/fields/' . $field_name);
-    $this->assertFieldByName('settings[handler_settings][auto_create]');
-
-    // Switch the target type to 'user' and check that the settings specific to
-    // its selection handler are displayed.
-    $field_name = 'node.' . $this->type . '.field_test';
-    $edit = [
-      'settings[target_type]' => 'user',
-    ];
-    $this->drupalPostForm($bundle_path . '/fields/' . $field_name . '/storage', $edit, t('Save field settings'));
-    $this->drupalGet($bundle_path . '/fields/' . $field_name);
-    $this->assertFieldByName('settings[handler_settings][filter][type]', '_none');
-
-    // Switch the target type to 'node'.
-    $field_name = 'node.' . $this->type . '.field_test';
-    $edit = [
-      'settings[target_type]' => 'node',
-    ];
-    $this->drupalPostForm($bundle_path . '/fields/' . $field_name . '/storage', $edit, t('Save field settings'));
-
-    // Try to select the views handler.
-    $edit = [
-      'settings[handler]' => 'views',
-    ];
-    $this->drupalPostAjaxForm($bundle_path . '/fields/' . $field_name, $edit, 'settings[handler]');
-    $this->assertRaw(t('No eligible views were found. <a href=":create">Create a view</a> with an <em>Entity Reference</em> display, or add such a display to an <a href=":existing">existing view</a>.', [
-      ':create' => \Drupal::url('views_ui.add'),
-      ':existing' => \Drupal::url('entity.view.collection'),
-    ]));
-    $this->drupalPostForm(NULL, $edit, t('Save settings'));
-    // If no eligible view is available we should see a message.
-    $this->assertText('The views entity selection mode requires a view.');
-
-    // Enable the entity_reference_test module which creates an eligible view.
-    $this->container->get('module_installer')->install(['entity_reference_test']);
-    $this->resetAll();
-    $this->drupalGet($bundle_path . '/fields/' . $field_name);
-    $this->drupalPostAjaxForm($bundle_path . '/fields/' . $field_name, $edit, 'settings[handler]');
-    $edit = [
-      'settings[handler_settings][view][view_and_display]' => 'test_entity_reference:entity_reference_1',
-    ];
-    $this->drupalPostForm(NULL, $edit, t('Save settings'));
-    $this->assertResponse(200);
-
-    // Switch the target type to 'entity_test'.
-    $edit = [
-      'settings[target_type]' => 'entity_test',
-    ];
-    $this->drupalPostForm($bundle_path . '/fields/' . $field_name . '/storage', $edit, t('Save field settings'));
-    $this->drupalGet($bundle_path . '/fields/' . $field_name);
-    $edit = [
-      'settings[handler]' => 'views',
-    ];
-    $this->drupalPostAjaxForm($bundle_path . '/fields/' . $field_name, $edit, 'settings[handler]');
-    $edit = [
-      'required' => FALSE,
-      'settings[handler_settings][view][view_and_display]' => 'test_entity_reference_entity_test:entity_reference_1',
-    ];
-    $this->drupalPostForm(NULL, $edit, t('Save settings'));
-    $this->assertResponse(200);
-
     // Create a new view and display it as a entity reference.
     $edit = [
       'id' => 'node_test_view',
@@ -253,7 +115,7 @@ class EntityReferenceAdminTest extends WebTestBase {
     $edit = [
       'settings[handler]' => 'views',
     ];
-    $this->drupalPostAjaxForm(NULL, $edit, 'settings[handler]');
+    $this->drupalPostForm(NULL, $edit, t('Change handler'));
     $edit = [
       'required' => FALSE,
       'settings[handler_settings][view][view_and_display]' => 'node_test_view:entity_reference_1',
@@ -275,7 +137,7 @@ class EntityReferenceAdminTest extends WebTestBase {
     // Try to add a new node and fill the entity reference field.
     $this->drupalGet('node/add/' . $this->type);
     $result = $this->xpath('//input[@name="field_test_entity_ref_field[0][target_id]" and contains(@data-autocomplete-path, "/entity_reference_autocomplete/node/views/")]');
-    $target_url = $this->getAbsoluteUrl($result[0]['data-autocomplete-path']);
+    $target_url = $this->getAbsoluteUrl($result[0]->getAttribute('data-autocomplete-path'));
     $this->drupalGet($target_url, ['query' => ['q' => 'Foo']]);
     $this->assertRaw($node1->getTitle() . ' (' . $node1->id() . ')');
     $this->assertRaw($node2->getTitle() . ' (' . $node2->id() . ')');
@@ -446,7 +308,8 @@ class EntityReferenceAdminTest extends WebTestBase {
       'settings[handler_settings][target_bundles][' . $vocabularies[1]->id() . ']' => TRUE,
     ];
     // Enable the second vocabulary as a target bundle.
-    $this->drupalPostAjaxForm($path, $edit, key($edit));
+    $this->drupalPostForm($path, $edit, 'Save settings');
+    $this->drupalGet($path);
     // Expect a select element with the two vocabularies as options.
     $this->assertFieldByXPath("//select[@name='settings[handler_settings][auto_create_bundle]']/option[@value='" . $vocabularies[0]->id() . "']");
     $this->assertFieldByXPath("//select[@name='settings[handler_settings][auto_create_bundle]']/option[@value='" . $vocabularies[1]->id() . "']");
@@ -513,49 +376,23 @@ class EntityReferenceAdminTest extends WebTestBase {
    *   The field name.
    * @param array $expected_options
    *   An array of expected options.
-   *
-   * @return bool
-   *   TRUE if the assertion succeeded, FALSE otherwise.
    */
   protected function assertFieldSelectOptions($name, array $expected_options) {
     $xpath = $this->buildXPathQuery('//select[@name=:name]', [':name' => $name]);
     $fields = $this->xpath($xpath);
     if ($fields) {
       $field = $fields[0];
-      $options = $this->getAllOptionsList($field);
-
+      $options = $field->findAll('xpath', 'option');
+      array_walk($options, function (NodeElement &$option) {
+        $option = $option->getValue();
+      });
       sort($options);
       sort($expected_options);
-
-      return $this->assertIdentical($options, $expected_options);
+      $this->assertIdentical($options, $expected_options);
     }
     else {
-      return $this->fail('Unable to find field ' . $name);
+      $this->fail('Unable to find field ' . $name);
     }
   }
 
-  /**
-   * Extracts all options from a select element.
-   *
-   * @param \SimpleXMLElement $element
-   *   The select element field information.
-   *
-   * @return array
-   *   An array of option values as strings.
-   */
-  protected function getAllOptionsList(\SimpleXMLElement $element) {
-    $options = [];
-    // Add all options items.
-    foreach ($element->option as $option) {
-      $options[] = (string) $option['value'];
-    }
-
-    // Loops trough all the option groups
-    foreach ($element->optgroup as $optgroup) {
-      $options = array_merge($this->getAllOptionsList($optgroup), $options);
-    }
-
-    return $options;
-  }
-
 }
diff --git a/web/core/modules/field/tests/src/FunctionalJavascript/EntityReference/EntityReferenceAdminTest.php b/web/core/modules/field/tests/src/FunctionalJavascript/EntityReference/EntityReferenceAdminTest.php
new file mode 100644 (file)
index 0000000..89a8ce1
--- /dev/null
@@ -0,0 +1,244 @@
+<?php
+
+namespace Drupal\Tests\field\FunctionalJavascript\EntityReference;
+
+use Behat\Mink\Element\NodeElement;
+use Drupal\Component\Render\FormattableMarkup;
+use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use Drupal\Tests\field_ui\Traits\FieldUiTestTrait;
+
+/**
+ * Tests for the administrative UI.
+ *
+ * @group entity_reference
+ */
+class EntityReferenceAdminTest extends WebDriverTestBase {
+
+  use FieldUiTestTrait;
+
+  /**
+   * Modules to install.
+   *
+   * Enable path module to ensure that the selection handler does not fail for
+   * entities with a path field.
+   *
+   * @var array
+   */
+  public static $modules = ['node', 'field_ui', 'path', 'taxonomy', 'block', 'views_ui'];
+
+  /**
+   * The name of the content type created for testing purposes.
+   *
+   * @var string
+   */
+  protected $type;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->drupalPlaceBlock('system_breadcrumb_block');
+
+    // Create a content type, with underscores.
+    $type_name = strtolower($this->randomMachineName(8)) . '_test';
+    $type = $this->drupalCreateContentType(['name' => $type_name, 'type' => $type_name]);
+    $this->type = $type->id();
+
+    // Create test user.
+    $admin_user = $this->drupalCreateUser([
+      'access content',
+      'administer node fields',
+      'administer node display',
+      'administer views',
+      'create ' . $type_name . ' content',
+      'edit own ' . $type_name . ' content',
+    ]);
+    $this->drupalLogin($admin_user);
+  }
+
+  /**
+   * Tests the Entity Reference Admin UI.
+   */
+  public function testFieldAdminHandler() {
+    $bundle_path = 'admin/structure/types/manage/' . $this->type;
+
+    $page = $this->getSession()->getPage();
+    $assert_session = $this->assertSession();
+
+    // First step: 'Add new field' on the 'Manage fields' page.
+    $this->drupalGet($bundle_path . '/fields/add-field');
+
+    // Check if the commonly referenced entity types appear in the list.
+    $this->assertOption('edit-new-storage-type', 'field_ui:entity_reference:node');
+    $this->assertOption('edit-new-storage-type', 'field_ui:entity_reference:user');
+
+    $page->findField('new_storage_type')->setValue('entity_reference');
+    $assert_session->waitForField('label')->setValue('Test');
+    $machine_name = $assert_session->waitForElement('xpath', '//*[@id="edit-label-machine-name-suffix"]/span[2]/span[contains(text(), "field_test")]');
+    $this->assertNotEmpty($machine_name);
+    $page->pressButton('Save and continue');
+
+    // Node should be selected by default.
+    $this->assertFieldByName('settings[target_type]', 'node');
+
+    // Check that all entity types can be referenced.
+    $this->assertFieldSelectOptions('settings[target_type]', array_keys(\Drupal::entityManager()->getDefinitions()));
+
+    // Second step: 'Field settings' form.
+    $this->drupalPostForm(NULL, [], t('Save field settings'));
+
+    // The base handler should be selected by default.
+    $this->assertFieldByName('settings[handler]', 'default:node');
+
+    // The base handler settings should be displayed.
+    $entity_type_id = 'node';
+    // Check that the type label is correctly displayed.
+    $assert_session->pageTextContains('Content type');
+    $bundles = $this->container->get('entity_type.bundle.info')->getBundleInfo($entity_type_id);
+    foreach ($bundles as $bundle_name => $bundle_info) {
+      $this->assertFieldByName('settings[handler_settings][target_bundles][' . $bundle_name . ']');
+    }
+
+    reset($bundles);
+
+    // Test the sort settings.
+    // Option 0: no sort.
+    $this->assertFieldByName('settings[handler_settings][sort][field]', '_none');
+    $this->assertNoFieldByName('settings[handler_settings][sort][direction]');
+    // Option 1: sort by field.
+    $page->findField('settings[handler_settings][sort][field]')->setValue('nid');
+    $assert_session->waitForField('settings[handler_settings][sort][direction]');
+    $this->assertFieldByName('settings[handler_settings][sort][direction]', 'ASC');
+
+    // Test that a non-translatable base field is a sort option.
+    $this->assertFieldByXPath("//select[@name='settings[handler_settings][sort][field]']/option[@value='nid']");
+    // Test that a translatable base field is a sort option.
+    $this->assertFieldByXPath("//select[@name='settings[handler_settings][sort][field]']/option[@value='title']");
+    // Test that a configurable field is a sort option.
+    $this->assertFieldByXPath("//select[@name='settings[handler_settings][sort][field]']/option[@value='body.value']");
+
+    // Set back to no sort.
+    $page->findField('settings[handler_settings][sort][field]')->setValue('_none');
+    $assert_session->assertWaitOnAjaxRequest();
+    $this->assertNoFieldByName('settings[handler_settings][sort][direction]');
+
+    // Third step: confirm.
+    $page->findField('settings[handler_settings][target_bundles][' . key($bundles) . ']')->setValue(key($bundles));
+    $assert_session->assertWaitOnAjaxRequest();
+    $this->drupalPostForm(NULL, [
+      'required' => '1',
+    ], t('Save settings'));
+
+    // Check that the field appears in the overview form.
+    $this->assertFieldByXPath('//table[@id="field-overview"]//tr[@id="field-test"]/td[1]', 'Test', 'Field was created and appears in the overview page.');
+
+    // Check that the field settings form can be submitted again, even when the
+    // field is required.
+    // The first 'Edit' link is for the Body field.
+    $this->clickLink(t('Edit'), 1);
+    $this->drupalPostForm(NULL, [], t('Save settings'));
+
+    // Switch the target type to 'taxonomy_term' and check that the settings
+    // specific to its selection handler are displayed.
+    $field_name = 'node.' . $this->type . '.field_test';
+    $edit = [
+      'settings[target_type]' => 'taxonomy_term',
+    ];
+    $this->drupalPostForm($bundle_path . '/fields/' . $field_name . '/storage', $edit, t('Save field settings'));
+    $this->drupalGet($bundle_path . '/fields/' . $field_name);
+    $this->assertFieldByName('settings[handler_settings][auto_create]');
+
+    // Switch the target type to 'user' and check that the settings specific to
+    // its selection handler are displayed.
+    $field_name = 'node.' . $this->type . '.field_test';
+    $edit = [
+      'settings[target_type]' => 'user',
+    ];
+    $this->drupalPostForm($bundle_path . '/fields/' . $field_name . '/storage', $edit, t('Save field settings'));
+    $this->drupalGet($bundle_path . '/fields/' . $field_name);
+    $this->assertFieldByName('settings[handler_settings][filter][type]', '_none');
+
+    // Switch the target type to 'node'.
+    $field_name = 'node.' . $this->type . '.field_test';
+    $edit = [
+      'settings[target_type]' => 'node',
+    ];
+    $this->drupalPostForm($bundle_path . '/fields/' . $field_name . '/storage', $edit, t('Save field settings'));
+
+    // Try to select the views handler.
+    $this->drupalGet($bundle_path . '/fields/' . $field_name);
+    $page->findField('settings[handler]')->setValue('views');
+    $views_text = (string) new FormattableMarkup('No eligible views were found. <a href=":create">Create a view</a> with an <em>Entity Reference</em> display, or add such a display to an <a href=":existing">existing view</a>.', [
+      ':create' => \Drupal::url('views_ui.add'),
+      ':existing' => \Drupal::url('entity.view.collection'),
+    ]);
+    $assert_session->waitForElement('xpath', '//a[contains(text(), "Create a view")]');
+    $assert_session->responseContains($views_text);
+
+    $this->drupalPostForm(NULL, [], t('Save settings'));
+    // If no eligible view is available we should see a message.
+    $assert_session->pageTextContains('The views entity selection mode requires a view.');
+
+    // Enable the entity_reference_test module which creates an eligible view.
+    $this->container->get('module_installer')
+      ->install(['entity_reference_test']);
+    $this->resetAll();
+    $this->drupalGet($bundle_path . '/fields/' . $field_name);
+    $page->findField('settings[handler]')->setValue('views');
+    $assert_session
+      ->waitForField('settings[handler_settings][view][view_and_display]')
+      ->setValue('test_entity_reference:entity_reference_1');
+    $this->drupalPostForm(NULL, [], t('Save settings'));
+    $assert_session->pageTextContains('Saved Test configuration.');
+
+    // Switch the target type to 'entity_test'.
+    $edit = [
+      'settings[target_type]' => 'entity_test',
+    ];
+    $this->drupalPostForm($bundle_path . '/fields/' . $field_name . '/storage', $edit, t('Save field settings'));
+    $this->drupalGet($bundle_path . '/fields/' . $field_name);
+    $page->findField('settings[handler]')->setValue('views');
+    $assert_session
+      ->waitForField('settings[handler_settings][view][view_and_display]')
+      ->setValue('test_entity_reference_entity_test:entity_reference_1');
+    $edit = [
+      'required' => FALSE,
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Save settings'));
+    $assert_session->pageTextContains('Saved Test configuration.');
+  }
+
+  /**
+   * Checks if a select element contains the specified options.
+   *
+   * @param string $name
+   *   The field name.
+   * @param array $expected_options
+   *   An array of expected options.
+   */
+  protected function assertFieldSelectOptions($name, array $expected_options) {
+    $xpath = $this->buildXPathQuery('//select[@name=:name]', [':name' => $name]);
+    $fields = $this->xpath($xpath);
+    if ($fields) {
+      $field = $fields[0];
+      $options = $field->findAll('xpath', 'option');
+      $optgroups = $field->findAll('xpath', 'optgroup');
+      foreach ($optgroups as $optgroup) {
+        $options = array_merge($options, $optgroup->findAll('xpath', 'option'));
+      }
+      array_walk($options, function (NodeElement &$option) {
+        $option = $option->getAttribute('value');
+      });
+
+      sort($options);
+      sort($expected_options);
+
+      $this->assertIdentical($options, $expected_options);
+    }
+    else {
+      $this->fail('Unable to find field ' . $name);
+    }
+  }
+
+}
index ae3a371c81d94bc8e3c1b102d25ce156641efecb..74f16d582a514e7b3e0a581aab770ea75e99abf6 100644 (file)
@@ -24,6 +24,13 @@ class StringFormatterTest extends KernelTestBase {
    */
   public static $modules = ['field', 'text', 'entity_test', 'system', 'filter', 'user'];
 
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
   /**
    * @var string
    */
@@ -79,6 +86,8 @@ class StringFormatterTest extends KernelTestBase {
         'settings' => [],
       ]);
     $this->display->save();
+
+    $this->entityTypeManager = \Drupal::entityTypeManager();
   }
 
   /**
@@ -145,7 +154,7 @@ class StringFormatterTest extends KernelTestBase {
     $value2 = $this->randomMachineName();
     $entity->{$this->fieldName}->value = $value2;
     $entity->save();
-    $entity_new_revision = \Drupal::entityManager()->getStorage('entity_test_rev')->loadRevision($old_revision_id);
+    $entity_new_revision = $this->entityTypeManager->getStorage('entity_test_rev')->loadRevision($old_revision_id);
 
     $this->renderEntityFields($entity, $this->display);
     $this->assertLink($value2, 0);
@@ -154,6 +163,19 @@ class StringFormatterTest extends KernelTestBase {
     $this->renderEntityFields($entity_new_revision, $this->display);
     $this->assertLink($value, 0);
     $this->assertLinkByHref('/entity_test_rev/' . $entity_new_revision->id() . '/revision/' . $entity_new_revision->getRevisionId() . '/view');
+
+    // Check that linking to a revisionable entity works if the entity type does
+    // not specify a 'revision' link template.
+    $entity_type = clone $this->entityTypeManager->getDefinition('entity_test_rev');
+    $link_templates = $entity_type->getLinkTemplates();
+    unset($link_templates['revision']);
+    $entity_type->set('links', $link_templates);
+    \Drupal::state()->set('entity_test_rev.entity_type', $entity_type);
+    $this->entityTypeManager->clearCachedDefinitions();
+
+    $this->renderEntityFields($entity_new_revision, $this->display);
+    $this->assertLink($value, 0);
+    $this->assertLinkByHref($entity->url('canonical'));
   }
 
 }
index 3e31c815e439e46df93d831ce2049c356758a894..85a5017cb1d129764aa2881660c747b06f9e6850 100644 (file)
@@ -176,7 +176,7 @@ class ManageFieldsFunctionalTest extends BrowserTestBase {
   /**
    * Tests adding a new field.
    *
-   * @todo Assert properties can bet set in the form and read back in
+   * @todo Assert properties can be set in the form and read back in
    * $field_storage and $fields.
    */
   public function createField() {
index c0003b350af3d7943d8f3b808605d7b709cd84b4..a6f68094a96025cf423aeb018d577e1223e1ee54 100644 (file)
@@ -859,7 +859,6 @@ function _file_save_upload_from_form(array $element, FormStateInterface $form_st
  * @todo: move this logic to a service in https://www.drupal.org/node/2244513.
  */
 function file_save_upload($form_field_name, $validators = [], $destination = FALSE, $delta = NULL, $replace = FILE_EXISTS_RENAME) {
-  $user = \Drupal::currentUser();
   static $upload_cache;
 
   $all_files = \Drupal::request()->files->get('files', []);
@@ -887,182 +886,208 @@ function file_save_upload($form_field_name, $validators = [], $destination = FAL
 
   $files = [];
   foreach ($uploaded_files as $i => $file_info) {
-    // Check for file upload errors and return FALSE for this file if a lower
-    // level system error occurred. For a complete list of errors:
-    // See http://php.net/manual/features.file-upload.errors.php.
-    switch ($file_info->getError()) {
-      case UPLOAD_ERR_INI_SIZE:
-      case UPLOAD_ERR_FORM_SIZE:
-        \Drupal::messenger()->addError(t('The file %file could not be saved because it exceeds %maxsize, the maximum allowed size for uploads.', ['%file' => $file_info->getFilename(), '%maxsize' => format_size(file_upload_max_size())]));
-        $files[$i] = FALSE;
-        continue;
-
-      case UPLOAD_ERR_PARTIAL:
-      case UPLOAD_ERR_NO_FILE:
-        \Drupal::messenger()->addError(t('The file %file could not be saved because the upload did not complete.', ['%file' => $file_info->getFilename()]));
-        $files[$i] = FALSE;
-        continue;
-
-      case UPLOAD_ERR_OK:
-        // Final check that this is a valid upload, if it isn't, use the
-        // default error handler.
-        if (is_uploaded_file($file_info->getRealPath())) {
-          break;
-        }
+    $files[$i] = _file_save_upload_single($file_info, $form_field_name, $validators, $destination, $replace);
+  }
 
-        // Unknown error
-      default:
-        \Drupal::messenger()->addError(t('The file %file could not be saved. An unknown error has occurred.', ['%file' => $file_info->getFilename()]));
-        $files[$i] = FALSE;
-        continue;
+  // Add files to the cache.
+  $upload_cache[$form_field_name] = $files;
 
-    }
-    // Begin building file entity.
-    $values = [
-      'uid' => $user->id(),
-      'status' => 0,
-      'filename' => $file_info->getClientOriginalName(),
-      'uri' => $file_info->getRealPath(),
-      'filesize' => $file_info->getSize(),
-    ];
-    $values['filemime'] = \Drupal::service('file.mime_type.guesser')->guess($values['filename']);
-    $file = File::create($values);
-
-    $extensions = '';
-    if (isset($validators['file_validate_extensions'])) {
-      if (isset($validators['file_validate_extensions'][0])) {
-        // Build the list of non-munged extensions if the caller provided them.
-        $extensions = $validators['file_validate_extensions'][0];
-      }
-      else {
-        // If 'file_validate_extensions' is set and the list is empty then the
-        // caller wants to allow any extension. In this case we have to remove the
-        // validator or else it will reject all extensions.
-        unset($validators['file_validate_extensions']);
-      }
-    }
-    else {
-      // No validator was provided, so add one using the default list.
-      // Build a default non-munged safe list for file_munge_filename().
-      $extensions = 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp';
-      $validators['file_validate_extensions'] = [];
-      $validators['file_validate_extensions'][0] = $extensions;
-    }
+  return isset($delta) ? $files[$delta] : $files;
+}
 
-    if (!empty($extensions)) {
-      // Munge the filename to protect against possible malicious extension
-      // hiding within an unknown file type (ie: filename.html.foo).
-      $file->setFilename(file_munge_filename($file->getFilename(), $extensions));
-    }
+/**
+ * Saves a file upload to a new location.
+ *
+ * @param \SplFileInfo $file_info
+ *   The file upload to save.
+ * @param string $form_field_name
+ *   A string that is the associative array key of the upload form element in
+ *   the form array.
+ * @param array $validators
+ *   (optional) An associative array of callback functions used to validate the
+ *   file.
+ * @param bool $destination
+ *   (optional) A string containing the URI that the file should be copied to.
+ * @param int $replace
+ *   (optional) The replace behavior when the destination file already exists.
+ *
+ * @return \Drupal\file\FileInterface|false
+ *   The created file entity or FALSE if the uploaded file not saved.
+ *
+ * @throws \Drupal\Core\Entity\EntityStorageException
+ *
+ * @internal
+ *   This method should only be called from file_save_upload(). Use that method
+ *   instead.
+ *
+ * @see file_save_upload()
+ */
+function _file_save_upload_single(\SplFileInfo $file_info, $form_field_name, $validators = [], $destination = FALSE, $replace = FILE_EXISTS_RENAME) {
+  $user = \Drupal::currentUser();
+  // Check for file upload errors and return FALSE for this file if a lower
+  // level system error occurred. For a complete list of errors:
+  // See http://php.net/manual/features.file-upload.errors.php.
+  switch ($file_info->getError()) {
+    case UPLOAD_ERR_INI_SIZE:
+    case UPLOAD_ERR_FORM_SIZE:
+      \Drupal::messenger()->addError(t('The file %file could not be saved because it exceeds %maxsize, the maximum allowed size for uploads.', ['%file' => $file_info->getFilename(), '%maxsize' => format_size(file_upload_max_size())]));
+      return FALSE;
+
+    case UPLOAD_ERR_PARTIAL:
+    case UPLOAD_ERR_NO_FILE:
+      \Drupal::messenger()->addError(t('The file %file could not be saved because the upload did not complete.', ['%file' => $file_info->getFilename()]));
+      return FALSE;
 
-    // Rename potentially executable files, to help prevent exploits (i.e. will
-    // rename filename.php.foo and filename.php to filename.php.foo.txt and
-    // filename.php.txt, respectively). Don't rename if 'allow_insecure_uploads'
-    // evaluates to TRUE.
-    if (!\Drupal::config('system.file')->get('allow_insecure_uploads') && preg_match(FILE_INSECURE_EXTENSION_REGEX, $file->getFilename()) && (substr($file->getFilename(), -4) != '.txt')) {
-      $file->setMimeType('text/plain');
-      // The destination filename will also later be used to create the URI.
-      $file->setFilename($file->getFilename() . '.txt');
-      // The .txt extension may not be in the allowed list of extensions. We have
-      // to add it here or else the file upload will fail.
-      if (!empty($extensions)) {
-        $validators['file_validate_extensions'][0] .= ' txt';
-        \Drupal::messenger()->addStatus(t('For security reasons, your upload has been renamed to %filename.', ['%filename' => $file->getFilename()]));
+    case UPLOAD_ERR_OK:
+      // Final check that this is a valid upload, if it isn't, use the
+      // default error handler.
+      if (is_uploaded_file($file_info->getRealPath())) {
+        break;
       }
-    }
 
-    // If the destination is not provided, use the temporary directory.
-    if (empty($destination)) {
-      $destination = 'temporary://';
-    }
+    default:
+      // Unknown error
+      \Drupal::messenger()->addError(t('The file %file could not be saved. An unknown error has occurred.', ['%file' => $file_info->getFilename()]));
+      return FALSE;
 
-    // Assert that the destination contains a valid stream.
-    $destination_scheme = file_uri_scheme($destination);
-    if (!file_stream_wrapper_valid_scheme($destination_scheme)) {
-      \Drupal::messenger()->addError(t('The file could not be uploaded because the destination %destination is invalid.', ['%destination' => $destination]));
-      $files[$i] = FALSE;
-      continue;
+  }
+  // Begin building file entity.
+  $values = [
+    'uid' => $user->id(),
+    'status' => 0,
+    'filename' => $file_info->getClientOriginalName(),
+    'uri' => $file_info->getRealPath(),
+    'filesize' => $file_info->getSize(),
+  ];
+  $values['filemime'] = \Drupal::service('file.mime_type.guesser')->guess($values['filename']);
+  $file = File::create($values);
+
+  $extensions = '';
+  if (isset($validators['file_validate_extensions'])) {
+    if (isset($validators['file_validate_extensions'][0])) {
+      // Build the list of non-munged extensions if the caller provided them.
+      $extensions = $validators['file_validate_extensions'][0];
     }
-
-    $file->source = $form_field_name;
-    // A file URI may already have a trailing slash or look like "public://".
-    if (substr($destination, -1) != '/') {
-      $destination .= '/';
+    else {
+      // If 'file_validate_extensions' is set and the list is empty then the
+      // caller wants to allow any extension. In this case we have to remove the
+      // validator or else it will reject all extensions.
+      unset($validators['file_validate_extensions']);
     }
-    $file->destination = file_destination($destination . $file->getFilename(), $replace);
-    // If file_destination() returns FALSE then $replace === FILE_EXISTS_ERROR and
-    // there's an existing file so we need to bail.
-    if ($file->destination === FALSE) {
-      \Drupal::messenger()->addError(t('The file %source could not be uploaded because a file by that name already exists in the destination %directory.', ['%source' => $form_field_name, '%directory' => $destination]));
-      $files[$i] = FALSE;
-      continue;
+  }
+  else {
+    // No validator was provided, so add one using the default list.
+    // Build a default non-munged safe list for file_munge_filename().
+    $extensions = 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp';
+    $validators['file_validate_extensions'] = [];
+    $validators['file_validate_extensions'][0] = $extensions;
+  }
+
+  if (!empty($extensions)) {
+    // Munge the filename to protect against possible malicious extension
+    // hiding within an unknown file type (ie: filename.html.foo).
+    $file->setFilename(file_munge_filename($file->getFilename(), $extensions));
+  }
+
+  // Rename potentially executable files, to help prevent exploits (i.e. will
+  // rename filename.php.foo and filename.php to filename.php.foo.txt and
+  // filename.php.txt, respectively). Don't rename if 'allow_insecure_uploads'
+  // evaluates to TRUE.
+  if (!\Drupal::config('system.file')->get('allow_insecure_uploads') && preg_match(FILE_INSECURE_EXTENSION_REGEX, $file->getFilename()) && (substr($file->getFilename(), -4) != '.txt')) {
+    $file->setMimeType('text/plain');
+    // The destination filename will also later be used to create the URI.
+    $file->setFilename($file->getFilename() . '.txt');
+    // The .txt extension may not be in the allowed list of extensions. We have
+    // to add it here or else the file upload will fail.
+    if (!empty($extensions)) {
+      $validators['file_validate_extensions'][0] .= ' txt';
+      \Drupal::messenger()->addStatus(t('For security reasons, your upload has been renamed to %filename.', ['%filename' => $file->getFilename()]));
     }
+  }
 
-    // Add in our check of the file name length.
-    $validators['file_validate_name_length'] = [];
+  // If the destination is not provided, use the temporary directory.
+  if (empty($destination)) {
+    $destination = 'temporary://';
+  }
 
-    // Call the validation functions specified by this function's caller.
-    $errors = file_validate($file, $validators);
+  // Assert that the destination contains a valid stream.
+  $destination_scheme = file_uri_scheme($destination);
+  if (!file_stream_wrapper_valid_scheme($destination_scheme)) {
+    \Drupal::messenger()->addError(t('The file could not be uploaded because the destination %destination is invalid.', ['%destination' => $destination]));
+    return FALSE;
+  }
 
-    // Check for errors.
-    if (!empty($errors)) {
-      $message = [
-        'error' => [
-          '#markup' => t('The specified file %name could not be uploaded.', ['%name' => $file->getFilename()]),
-        ],
-        'item_list' => [
-          '#theme' => 'item_list',
-          '#items' => $errors,
-        ],
-      ];
-      // @todo Add support for render arrays in
-      // \Drupal\Core\Messenger\MessengerInterface::addMessage()?
-      // @see https://www.drupal.org/node/2505497.
-      \Drupal::messenger()->addError(\Drupal::service('renderer')->renderPlain($message));
-      $files[$i] = FALSE;
-      continue;
-    }
+  $file->source = $form_field_name;
+  // A file URI may already have a trailing slash or look like "public://".
+  if (substr($destination, -1) != '/') {
+    $destination .= '/';
+  }
+  $file->destination = file_destination($destination . $file->getFilename(), $replace);
+  // If file_destination() returns FALSE then $replace === FILE_EXISTS_ERROR and
+  // there's an existing file so we need to bail.
+  if ($file->destination === FALSE) {
+    \Drupal::messenger()->addError(t('The file %source could not be uploaded because a file by that name already exists in the destination %directory.', ['%source' => $form_field_name, '%directory' => $destination]));
+    return FALSE;
+  }
 
-    $file->setFileUri($file->destination);
-    if (!drupal_move_uploaded_file($file_info->getRealPath(), $file->getFileUri())) {
-      \Drupal::messenger()->addError(t('File upload error. Could not move uploaded file.'));
-      \Drupal::logger('file')->notice('Upload error. Could not move uploaded file %file to destination %destination.', ['%file' => $file->getFilename(), '%destination' => $file->getFileUri()]);
-      $files[$i] = FALSE;
-      continue;
-    }
+  // Add in our check of the file name length.
+  $validators['file_validate_name_length'] = [];
 
-    // Set the permissions on the new file.
-    drupal_chmod($file->getFileUri());
+  // Call the validation functions specified by this function's caller.
+  $errors = file_validate($file, $validators);
+
+  // Check for errors.
+  if (!empty($errors)) {
+    $message = [
+      'error' => [
+        '#markup' => t('The specified file %name could not be uploaded.', ['%name' => $file->getFilename()]),
+      ],
+      'item_list' => [
+        '#theme' => 'item_list',
+        '#items' => $errors,
+      ],
+    ];
+    // @todo Add support for render arrays in
+    // \Drupal\Core\Messenger\MessengerInterface::addMessage()?
+    // @see https://www.drupal.org/node/2505497.
+    \Drupal::messenger()->addError(\Drupal::service('renderer')->renderPlain($message));
+    return FALSE;
+  }
 
-    // If we are replacing an existing file re-use its database record.
-    // @todo Do not create a new entity in order to update it. See
-    //   https://www.drupal.org/node/2241865.
-    if ($replace == FILE_EXISTS_REPLACE) {
-      $existing_files = entity_load_multiple_by_properties('file', ['uri' => $file->getFileUri()]);
-      if (count($existing_files)) {
-        $existing = reset($existing_files);
-        $file->fid = $existing->id();
-        $file->setOriginalId($existing->id());
-      }
-    }
+  $file->setFileUri($file->destination);
+  if (!drupal_move_uploaded_file($file_info->getRealPath(), $file->getFileUri())) {
+    \Drupal::messenger()->addError(t('File upload error. Could not move uploaded file.'));
+    \Drupal::logger('file')->notice('Upload error. Could not move uploaded file %file to destination %destination.', ['%file' => $file->getFilename(), '%destination' => $file->getFileUri()]);
+    return FALSE;
+  }
 
-    // If we made it this far it's safe to record this file in the database.
-    $file->save();
-    $files[$i] = $file;
-    // Allow an anonymous user who creates a non-public file to see it. See
-    // \Drupal\file\FileAccessControlHandler::checkAccess().
-    if ($user->isAnonymous() && $destination_scheme !== 'public') {
-      $session = \Drupal::request()->getSession();
-      $allowed_temp_files = $session->get('anonymous_allowed_file_ids', []);
-      $allowed_temp_files[$file->id()] = $file->id();
-      $session->set('anonymous_allowed_file_ids', $allowed_temp_files);
+  // Set the permissions on the new file.
+  drupal_chmod($file->getFileUri());
+
+  // If we are replacing an existing file re-use its database record.
+  // @todo Do not create a new entity in order to update it. See
+  //   https://www.drupal.org/node/2241865.
+  if ($replace == FILE_EXISTS_REPLACE) {
+    $existing_files = entity_load_multiple_by_properties('file', ['uri' => $file->getFileUri()]);
+    if (count($existing_files)) {
+      $existing = reset($existing_files);
+      $file->fid = $existing->id();
+      $file->setOriginalId($existing->id());
     }
   }
 
-  // Add files to the cache.
-  $upload_cache[$form_field_name] = $files;
+  // If we made it this far it's safe to record this file in the database.
+  $file->save();
 
-  return isset($delta) ? $files[$delta] : $files;
+  // Allow an anonymous user who creates a non-public file to see it. See
+  // \Drupal\file\FileAccessControlHandler::checkAccess().
+  if ($user->isAnonymous() && $destination_scheme !== 'public') {
+    $session = \Drupal::request()->getSession();
+    $allowed_temp_files = $session->get('anonymous_allowed_file_ids', []);
+    $allowed_temp_files[$file->id()] = $file->id();
+    $session->set('anonymous_allowed_file_ids', $allowed_temp_files);
+  }
+  return $file;
 }
 
 /**
diff --git a/web/core/modules/file/tests/src/Functional/MultipleFileUploadTest.php b/web/core/modules/file/tests/src/Functional/MultipleFileUploadTest.php
new file mode 100644 (file)
index 0000000..587d447
--- /dev/null
@@ -0,0 +1,59 @@
+<?php
+
+namespace Drupal\Tests\file\Functional;
+
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Tests multiple file upload.
+ *
+ * @group file
+ */
+class MultipleFileUploadTest extends BrowserTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['file'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $admin = $this->drupalCreateUser(['administer themes']);
+    $this->drupalLogin($admin);
+  }
+
+  /**
+   * Tests multiple file field with all file extensions.
+   */
+  public function testMultipleFileFieldWithAllFileExtensions() {
+    $theme = 'test_theme_settings';
+    \Drupal::service('theme_handler')->install([$theme]);
+    $this->drupalGet("admin/appearance/settings/$theme");
+
+    $edit = [];
+    // Create few files with non-typical extensions.
+    foreach (['file1.wtf', 'file2.wtf'] as $i => $file) {
+      $file_path = $this->root . "/sites/default/files/simpletest/$file";
+      file_put_contents($file_path, 'File with non-default extension.', FILE_APPEND | LOCK_EX);
+      $edit["files[multi_file][$i]"] = $file_path;
+    }
+
+    // @todo: Replace after https://www.drupal.org/project/drupal/issues/2917885
+    $this->drupalGet("admin/appearance/settings/$theme");
+    $submit_xpath = $this->assertSession()->buttonExists('Save configuration')->getXpath();
+    $client = $this->getSession()->getDriver()->getClient();
+    $form = $client->getCrawler()->filterXPath($submit_xpath)->form();
+    $client->request($form->getMethod(), $form->getUri(), $form->getPhpValues(), $edit);
+
+    $page = $this->getSession()->getPage();
+    $this->assertNotContains('Only files with the following extensions are allowed', $page->getContent());
+    $this->assertContains('The configuration options have been saved.', $page->getContent());
+    $this->assertContains('file1.wtf', $page->getContent());
+    $this->assertContains('file2.wtf', $page->getContent());
+  }
+
+}
index 13079f85541b8bc280a7c078307608129d70a17e..25927b52532c789b818f96a2a87f8a9de12c9893 100644 (file)
@@ -90,11 +90,11 @@ class ConfigurableLanguageManager extends LanguageManager implements Configurabl
   protected $initialized = FALSE;
 
   /**
-   * Whether already in the process of language initialization.
+   * Whether language types are in the process of language initialization.
    *
-   * @var bool
+   * @var bool[]
    */
-  protected $initializing = FALSE;
+  protected $initializing = [];
 
   /**
    * {@inheritdoc}
@@ -213,12 +213,12 @@ class ConfigurableLanguageManager extends LanguageManager implements Configurabl
       $this->negotiatedLanguages[$type] = $this->getDefaultLanguage();
 
       if ($this->negotiator && $this->isMultilingual()) {
-        if (!$this->initializing) {
-          $this->initializing = TRUE;
+        if (!isset($this->initializing[$type])) {
+          $this->initializing[$type] = TRUE;
           $negotiation = $this->negotiator->initializeType($type);
           $this->negotiatedLanguages[$type] = reset($negotiation);
           $this->negotiatedMethods[$type] = key($negotiation);
-          $this->initializing = FALSE;
+          unset($this->initializing[$type]);
         }
         // If the current interface language needs to be retrieved during
         // initialization we return the system language. This way string
diff --git a/web/core/modules/language/tests/src/Functional/ConfigurableLanguageManagerTest.php b/web/core/modules/language/tests/src/Functional/ConfigurableLanguageManagerTest.php
new file mode 100644 (file)
index 0000000..d36aee0
--- /dev/null
@@ -0,0 +1,189 @@
+<?php
+
+namespace Drupal\Tests\language\Functional;
+
+use Drupal\Core\Cache\Cache;
+use Drupal\language\Entity\ConfigurableLanguage;
+use Drupal\language\Entity\ContentLanguageSettings;
+use Drupal\node\Entity\Node;
+use Drupal\node\Entity\NodeType;
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Tests Language Negotiation.
+ *
+ * Uses different negotiators for content and interface.
+ *
+ * @group language
+ */
+class ConfigurableLanguageManagerTest extends BrowserTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'language',
+    'content_translation',
+    'node',
+    'locale',
+    'block',
+    'system',
+    'user',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    /** @var \Drupal\user\UserInterface $user */
+    $user = $this->createUser([], '', TRUE);
+    $this->drupalLogin($user);
+    ConfigurableLanguage::createFromLangcode('es')->save();
+
+    // Create a page node type and make it translatable.
+    NodeType::create([
+      'type' => 'page',
+      'name' => t('Page'),
+    ])->save();
+
+    $config = ContentLanguageSettings::loadByEntityTypeBundle('node', 'page');
+    $config->setDefaultLangcode('en')
+      ->setLanguageAlterable(TRUE)
+      ->save();
+
+    // Create a Node with title 'English' and translate it to Spanish.
+    $node = Node::create([
+      'type' => 'page',
+      'title' => 'English',
+    ]);
+    $node->save();
+    $node->addTranslation('es', ['title' => 'Español']);
+    $node->save();
+
+    // Enable both language_interface and language_content language negotiation.
+    \Drupal::getContainer()->get('language_negotiator')->updateConfiguration([
+      'language_interface',
+      'language_content',
+    ]);
+
+    // Set the preferred language of the user for admin pages to English.
+    $user->set('preferred_admin_langcode', 'en')->save();
+
+    // Make sure node edit pages are administration pages.
+    $this->config('node.settings')->set('use_admin_theme', '1')->save();
+    $this->container->get('router.builder')->rebuild();
+
+    // Place a Block with a translatable string on the page.
+    $this->placeBlock('system_powered_by_block', ['region' => 'content']);
+
+    // Load the Spanish Node page once, to register the translatable string.
+    $this->drupalGet('/es/node/1');
+
+    // Translate the Powered by string.
+    /** @var \Drupal\locale\StringStorageInterface $string_storage */
+    $string_storage = \Drupal::getContainer()->get('locale.storage');
+    $source = $string_storage->findString(['source' => 'Powered by <a href=":poweredby">Drupal</a>']);
+    $string_storage->createTranslation([
+      'lid' => $source->lid,
+      'language' => 'es',
+      'translation' => 'Funciona con ...',
+    ])->save();
+    // Invalidate caches so that the new translation will be used.
+    Cache::invalidateTags(['rendered', 'locale']);
+  }
+
+  /**
+   * Test translation with URL and Preferred Admin Language negotiators.
+   *
+   * The interface language uses the preferred language for admin pages of the
+   * user and after that the URL. The Content uses just the URL.
+   */
+  public function testUrlContentTranslationWithPreferredAdminLanguage() {
+    $assert_session = $this->assertSession();
+    // Set the interface language to use the preferred administration language
+    // and then the URL.
+    /** @var \Drupal\language\LanguageNegotiatorInterface $language_negotiator */
+    $language_negotiator = \Drupal::getContainer()->get('language_negotiator');
+    $language_negotiator->saveConfiguration('language_interface', [
+      'language-user-admin' => 1,
+      'language-url' => 2,
+      'language-selected' => 3,
+    ]);
+    // Set Content Language Negotiator to use just the URL.
+    $language_negotiator->saveConfiguration('language_content', [
+      'language-url' => 4,
+      'language-selected' => 5,
+    ]);
+
+    // See if the full view of the node in english is present and the
+    // string in the Powered By Block is in English.
+    $this->drupalGet('/node/1');
+    $assert_session->pageTextContains('English');
+    $assert_session->pageTextContains('Powered by');
+
+    // Load the spanish node page again and see if both the node and the string
+    // are translated.
+    $this->drupalGet('/es/node/1');
+    $assert_session->pageTextContains('Español');
+    $assert_session->pageTextContains('Funciona con');
+    $assert_session->pageTextNotContains('Powered by');
+
+    // Check if the Powered by string is shown in English on an
+    // administration page, and the node content is shown in Spanish.
+    $this->drupalGet('/es/node/1/edit');
+    $assert_session->pageTextContains('Español');
+    $assert_session->pageTextContains('Powered by');
+    $assert_session->pageTextNotContains('Funciona con');
+  }
+
+  /**
+   * Test translation with URL and Session Language Negotiators.
+   */
+  public function testUrlContentTranslationWithSessionLanguage() {
+    $assert_session = $this->assertSession();
+    /** @var \Drupal\language\LanguageNegotiatorInterface $language_negotiator */
+    $language_negotiator = \Drupal::getContainer()->get('language_negotiator');
+    // Set Interface Language Negotiator to Session.
+    $language_negotiator->saveConfiguration('language_interface', [
+      'language-session' => 1,
+      'language-url' => 2,
+      'language-selected' => 3,
+    ]);
+
+    // Set Content Language Negotiator to URL.
+    $language_negotiator->saveConfiguration('language_content', [
+      'language-url' => 4,
+      'language-selected' => 5,
+    ]);
+
+    // See if the full view of the node in english is present and the
+    // string in the Powered By Block is in English.
+    $this->drupalGet('/node/1');
+    $assert_session->pageTextContains('English');
+    $assert_session->pageTextContains('Powered by');
+
+    // The language session variable has not been set yet, so
+    // The string should be in Spanish.
+    $this->drupalGet('/es/node/1');
+    $assert_session->pageTextContains('Español');
+    $assert_session->pageTextNotContains('Powered by');
+    $assert_session->pageTextContains('Funciona con');
+
+    // Set the session language to Spanish but load the English node page.
+    $this->drupalGet('/node/1', ['query' => ['language' => 'es']]);
+    $assert_session->pageTextContains('English');
+    $assert_session->pageTextNotContains('Español');
+    $assert_session->pageTextContains('Funciona con');
+    $assert_session->pageTextNotContains('Powered by');
+
+    // Set the session language to English but load the node page in Spanish.
+    $this->drupalGet('/es/node/1', ['query' => ['language' => 'en']]);
+    $assert_session->pageTextNotContains('English');
+    $assert_session->pageTextContains('Español');
+    $assert_session->pageTextNotContains('Funciona con');
+    $assert_session->pageTextContains('Powered by');
+  }
+
+}
index 1f7fd035d4c0b1b58ce79e94a4a86e369909bcd0..b7b3f4b8f3919320cfcf0d2901a59960fd39c9fa 100644 (file)
@@ -45,9 +45,9 @@ class MigrateLanguageContentTaxonomyVocabularySettingsTest extends MigrateDrupal
     // Set language to vocabulary.
     $this->assertLanguageContentSettings($target_entity, 'vocabulary_2_i_1_', 'fr', FALSE, ['enabled' => FALSE]);
     // Localize terms.
-    $this->assertLanguageContentSettings($target_entity, 'vocabulary_3_i_2_', LanguageInterface::LANGCODE_SITE_DEFAULT, TRUE, ['enabled' => TRUE]);
+    $this->assertLanguageContentSettings($target_entity, 'vocabulary_3_i_2_', LanguageInterface::LANGCODE_SITE_DEFAULT, TRUE, ['enabled' => FALSE]);
     // None translation enabled.
-    $this->assertLanguageContentSettings($target_entity, 'vocabulary_name_much_longer_than', LanguageInterface::LANGCODE_SITE_DEFAULT, FALSE, ['enabled' => FALSE]);
+    $this->assertLanguageContentSettings($target_entity, 'vocabulary_name_much_longer_than', LanguageInterface::LANGCODE_SITE_DEFAULT, TRUE, ['enabled' => TRUE]);
     $this->assertLanguageContentSettings($target_entity, 'tags', LanguageInterface::LANGCODE_SITE_DEFAULT, FALSE, ['enabled' => FALSE]);
     $this->assertLanguageContentSettings($target_entity, 'forums', LanguageInterface::LANGCODE_SITE_DEFAULT, FALSE, ['enabled' => FALSE]);
     $this->assertLanguageContentSettings($target_entity, 'type', LanguageInterface::LANGCODE_SITE_DEFAULT, FALSE, ['enabled' => FALSE]);
index c920f23bd40bcb3b8c4b542b27099aa767695d2f..edb0c48cb75af3915a923711844fb15d93ffb0b6 100644 (file)
   display: block;
   padding-top: 0.55em;
 }
+
+#drupal-off-canvas .inline-block-create-button {
+  display: block;
+  padding: 24px;
+  padding-left: 44px;
+  font-size: 16px;
+  color: #eee;
+  background: url(../../../misc/icons/bebebe/plus.svg) transparent 16px no-repeat;
+}
+
+#drupal-off-canvas .inline-block-create-button,
+#drupal-off-canvas .inline-block-list__item {
+  margin: 0 -20px;
+  background-color: #444;
+}
+
+#drupal-off-canvas .inline-block-create-button:hover,
+#drupal-off-canvas .inline-block-list__item:hover {
+  background-color: #333;
+}
+
+#drupal-off-canvas .inline-block-list {
+  margin-bottom: 15px;
+}
+
+#drupal-off-canvas .inline-block-list__item {
+  display: block;
+  padding: 15px 0 15px 25px;
+}
index dfd9922df05c8e680b2d2b0ca5f844170262b987..f70cdcaec06729a1edca5d6957b6163511ac1bee 100644 (file)
@@ -9,3 +9,5 @@ dependencies:
   - drupal:contextual
   # @todo Discuss removing in https://www.drupal.org/project/drupal/issues/2935999.
   - drupal:field_ui
+  # @todo Discuss removing in https://www.drupal.org/project/drupal/issues/3003610.
+  - drupal:block
index 5d7c60615cf0f4924e0ce30dbcc1be7e13a218c9..373f7d8a0f1e3bfa4cc59ee0589503fb257626c4 100644 (file)
@@ -19,6 +19,7 @@ use Drupal\layout_builder\Plugin\Block\ExtraFieldBlock;
 use Drupal\layout_builder\InlineBlockEntityOperations;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Access\AccessResult;
+use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
 
 /**
  * Implements hook_help().
@@ -62,8 +63,8 @@ function layout_builder_entity_type_alter(array &$entity_types) {
 function layout_builder_form_entity_form_display_edit_form_alter(&$form, FormStateInterface $form_state) {
   // Hides the Layout Builder field. It is rendered directly in
   // \Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay::buildMultiple().
-  unset($form['fields']['layout_builder__layout']);
-  $key = array_search('layout_builder__layout', $form['#fields']);
+  unset($form['fields'][OverridesSectionStorage::FIELD_NAME]);
+  $key = array_search(OverridesSectionStorage::FIELD_NAME, $form['#fields']);
   if ($key !== FALSE) {
     unset($form['#fields'][$key]);
   }
@@ -177,7 +178,7 @@ function layout_builder_cron() {
 function layout_builder_plugin_filter_block_alter(array &$definitions, array $extra, $consumer) {
   // @todo Determine the 'inline_block' blocks should be allowed outside
   //   of layout_builder https://www.drupal.org/node/2979142.
-  if ($consumer !== 'layout_builder') {
+  if ($consumer !== 'layout_builder' || !isset($extra['list']) || $extra['list'] !== 'inline_blocks') {
     foreach ($definitions as $id => $definition) {
       if ($definition['id'] === 'inline_block') {
         unset($definitions[$id]);
@@ -202,3 +203,21 @@ function layout_builder_block_content_access(EntityInterface $entity, $operation
   }
   return AccessResult::forbidden();
 }
+
+/**
+ * Implements hook_plugin_filter_TYPE__CONSUMER_alter().
+ */
+function layout_builder_plugin_filter_block__block_ui_alter(array &$definitions, array $extra) {
+  foreach ($definitions as $id => $definition) {
+    // Filter out any layout_builder definition with required contexts.
+    if ($definition['provider'] === 'layout_builder' && !empty($definition['context'])) {
+      /** @var \Drupal\Core\Plugin\Context\ContextDefinitionInterface $context */
+      foreach ($definition['context'] as $context) {
+        if ($context->isRequired()) {
+          unset($definitions[$id]);
+          break;
+        }
+      }
+    }
+  }
+}
index 54c9cf25b23a46e5d64ca004a0e67c55c6d303c5..3e3ee1b0241ca371ccd507b7006a1592e6a2b431 100644 (file)
@@ -80,6 +80,19 @@ layout_builder.add_block:
       section_storage:
         layout_builder_tempstore: TRUE
 
+layout_builder.choose_inline_block:
+  path: '/layout_builder/choose/inline-block/{section_storage_type}/{section_storage}/{delta}/{region}'
+  defaults:
+    _controller: '\Drupal\layout_builder\Controller\ChooseBlockController::inlineBlockList'
+    _title: 'Add a new Inline Block'
+  requirements:
+    _permission: 'configure any layout'
+  options:
+    _admin_route: TRUE
+    parameters:
+      section_storage:
+        layout_builder_tempstore: TRUE
+
 layout_builder.update_block:
   path: '/layout_builder/update/block/{section_storage_type}/{section_storage}/{delta}/{region}/{uuid}'
   defaults:
index ac8514c48d1c44dc26ae6f4ab0cf4c2f06f04e09..9bd76bacc13f7df47d918053e167a034e4bdeb5a 100644 (file)
@@ -5,6 +5,7 @@ namespace Drupal\layout_builder\Controller;
 use Drupal\Core\Ajax\AjaxHelperTrait;
 use Drupal\Core\Block\BlockManagerInterface;
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\Core\Url;
 use Drupal\layout_builder\Context\LayoutBuilderContextTrait;
@@ -29,14 +30,24 @@ class ChooseBlockController implements ContainerInjectionInterface {
    */
   protected $blockManager;
 
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
   /**
    * ChooseBlockController constructor.
    *
    * @param \Drupal\Core\Block\BlockManagerInterface $block_manager
    *   The block manager.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
    */
-  public function __construct(BlockManagerInterface $block_manager) {
+  public function __construct(BlockManagerInterface $block_manager, EntityTypeManagerInterface $entity_type_manager) {
     $this->blockManager = $block_manager;
+    $this->entityTypeManager = $entity_type_manager;
   }
 
   /**
@@ -44,7 +55,8 @@ class ChooseBlockController implements ContainerInjectionInterface {
    */
   public static function create(ContainerInterface $container) {
     return new static(
-      $container->get('plugin.manager.block')
+      $container->get('plugin.manager.block'),
+      $container->get('entity_type.manager')
     );
   }
 
@@ -63,8 +75,43 @@ class ChooseBlockController implements ContainerInjectionInterface {
    */
   public function build(SectionStorageInterface $section_storage, $delta, $region) {
     $build['#title'] = $this->t('Choose a block');
-    $build['#type'] = 'container';
-    $build['#attributes']['class'][] = 'block-categories';
+    if ($this->entityTypeManager->hasDefinition('block_content_type') && $types = $this->entityTypeManager->getStorage('block_content_type')->loadMultiple()) {
+      if (count($types) === 1) {
+        $type = reset($types);
+        $plugin_id = 'inline_block:' . $type->id();
+        if ($this->blockManager->hasDefinition($plugin_id)) {
+          $url = Url::fromRoute('layout_builder.add_block', [
+            'section_storage_type' => $section_storage->getStorageType(),
+            'section_storage' => $section_storage->getStorageId(),
+            'delta' => $delta,
+            'region' => $region,
+            'plugin_id' => $plugin_id,
+          ]);
+        }
+      }
+      else {
+        $url = Url::fromRoute('layout_builder.choose_inline_block', [
+          'section_storage_type' => $section_storage->getStorageType(),
+          'section_storage' => $section_storage->getStorageId(),
+          'delta' => $delta,
+          'region' => $region,
+        ]);
+      }
+      if (isset($url)) {
+        $build['add_block'] = [
+          '#type' => 'link',
+          '#url' => $url,
+          '#title' => $this->t('Create @entity_type', [
+            '@entity_type' => $this->entityTypeManager->getDefinition('block_content')->getSingularLabel(),
+          ]),
+          '#attributes' => $this->getAjaxAttributes(),
+        ];
+        $build['add_block']['#attributes']['class'][] = 'inline-block-create-button';
+      }
+    }
+
+    $block_categories['#type'] = 'container';
+    $block_categories['#attributes']['class'][] = 'block-categories';
 
     // @todo Explicitly cast delta to an integer, remove this in
     //   https://www.drupal.org/project/drupal/issues/2984509.
@@ -75,35 +122,116 @@ class ChooseBlockController implements ContainerInjectionInterface {
       'delta' => $delta,
       'region' => $region,
     ]);
-    foreach ($this->blockManager->getGroupedDefinitions($definitions) as $category => $blocks) {
-      $build[$category]['#type'] = 'details';
-      $build[$category]['#open'] = TRUE;
-      $build[$category]['#title'] = $category;
-      $build[$category]['links'] = [
-        '#theme' => 'links',
-      ];
-      foreach ($blocks as $block_id => $block) {
-        $link = [
-          'title' => $block['admin_label'],
-          'url' => Url::fromRoute('layout_builder.add_block',
-            [
-              'section_storage_type' => $section_storage->getStorageType(),
-              'section_storage' => $section_storage->getStorageId(),
-              'delta' => $delta,
-              'region' => $region,
-              'plugin_id' => $block_id,
-            ]
-          ),
-        ];
-        if ($this->isAjax()) {
-          $link['attributes']['class'][] = 'use-ajax';
-          $link['attributes']['data-dialog-type'][] = 'dialog';
-          $link['attributes']['data-dialog-renderer'][] = 'off_canvas';
-        }
-        $build[$category]['links']['#links'][] = $link;
+    $grouped_definitions = $this->blockManager->getGroupedDefinitions($definitions);
+    foreach ($grouped_definitions as $category => $blocks) {
+      $block_categories[$category]['#type'] = 'details';
+      $block_categories[$category]['#open'] = TRUE;
+      $block_categories[$category]['#title'] = $category;
+      $block_categories[$category]['links'] = $this->getBlockLinks($section_storage, $delta, $region, $blocks);
+    }
+    $build['block_categories'] = $block_categories;
+    return $build;
+  }
+
+  /**
+   * Provides the UI for choosing a new inline block.
+   *
+   * @param \Drupal\layout_builder\SectionStorageInterface $section_storage
+   *   The section storage.
+   * @param int $delta
+   *   The delta of the section to splice.
+   * @param string $region
+   *   The region the block is going in.
+   *
+   * @return array
+   *   A render array.
+   */
+  public function inlineBlockList(SectionStorageInterface $section_storage, $delta, $region) {
+    $definitions = $this->blockManager->getFilteredDefinitions('layout_builder', $this->getAvailableContexts($section_storage), [
+      'section_storage' => $section_storage,
+      'region' => $region,
+      'list' => 'inline_blocks',
+    ]);
+    $blocks = $this->blockManager->getGroupedDefinitions($definitions);
+    $build = [];
+    if (isset($blocks['Inline blocks'])) {
+      $build['links'] = $this->getBlockLinks($section_storage, $delta, $region, $blocks['Inline blocks']);
+      $build['links']['#attributes']['class'][] = 'inline-block-list';
+      foreach ($build['links']['#links'] as &$link) {
+        $link['attributes']['class'][] = 'inline-block-list__item';
       }
+      $build['back_button'] = [
+        '#type' => 'link',
+        '#url' => Url::fromRoute('layout_builder.choose_block',
+          [
+            'section_storage_type' => $section_storage->getStorageType(),
+            'section_storage' => $section_storage->getStorageId(),
+            'delta' => $delta,
+            'region' => $region,
+          ]
+        ),
+        '#title' => $this->t('Back'),
+        '#attributes' => $this->getAjaxAttributes(),
+      ];
     }
     return $build;
   }
 
+  /**
+   * Gets a render array of block links.
+   *
+   * @param \Drupal\layout_builder\SectionStorageInterface $section_storage
+   *   The section storage.
+   * @param int $delta
+   *   The delta of the section to splice.
+   * @param string $region
+   *   The region the block is going in.
+   * @param array $blocks
+   *   The information for each block.
+   *
+   * @return array
+   *   The block links render array.
+   */
+  protected function getBlockLinks(SectionStorageInterface $section_storage, $delta, $region, array $blocks) {
+    $links = [];
+    foreach ($blocks as $block_id => $block) {
+      $link = [
+        'title' => $block['admin_label'],
+        'url' => Url::fromRoute('layout_builder.add_block',
+          [
+            'section_storage_type' => $section_storage->getStorageType(),
+            'section_storage' => $section_storage->getStorageId(),
+            'delta' => $delta,
+            'region' => $region,
+            'plugin_id' => $block_id,
+          ]
+        ),
+        'attributes' => $this->getAjaxAttributes(),
+      ];
+
+      $links[] = $link;
+    }
+    return [
+      '#theme' => 'links',
+      '#links' => $links,
+    ];
+  }
+
+  /**
+   * Get dialog attributes if an ajax request.
+   *
+   * @return array
+   *   The attributes array.
+   */
+  protected function getAjaxAttributes() {
+    if ($this->isAjax()) {
+      return [
+        'class' => ['use-ajax'],
+        'data-dialog-type' => 'dialog',
+        'data-dialog-renderer' => 'off_canvas',
+      ];
+    }
+    return [];
+  }
+
 }
index c01fea544b2bf3320b5f871d71738cdf7d541b7d..00bb40261911761f30bc0a22dcf0082578ce97e8 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace Drupal\layout_builder\Controller;
 
+use Drupal\Core\Ajax\AjaxHelperTrait;
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
 use Drupal\Core\Messenger\MessengerInterface;
 use Drupal\Core\Plugin\PluginFormInterface;
@@ -24,6 +25,7 @@ class LayoutBuilderController implements ContainerInjectionInterface {
 
   use LayoutBuilderContextTrait;
   use StringTranslationTrait;
+  use AjaxHelperTrait;
 
   /**
    * The layout tempstore repository.
@@ -90,6 +92,11 @@ class LayoutBuilderController implements ContainerInjectionInterface {
     $this->prepareLayout($section_storage, $is_rebuilding);
 
     $output = [];
+    if ($this->isAjax()) {
+      $output['status_messages'] = [
+        '#type' => 'status_messages',
+      ];
+    }
     $count = 0;
     for ($i = 0; $i < $section_storage->count(); $i++) {
       $output[] = $this->buildAddSectionLink($section_storage, $count);
@@ -114,6 +121,11 @@ class LayoutBuilderController implements ContainerInjectionInterface {
    *   Indicates if the layout is rebuilding.
    */
   protected function prepareLayout(SectionStorageInterface $section_storage, $is_rebuilding) {
+    // If the layout has pending changes, add a warning.
+    if ($this->layoutTempstoreRepository->has($section_storage)) {
+      $this->messenger->addWarning($this->t('You have unsaved changes.'));
+    }
+
     // Only add sections if the layout is new and empty.
     if (!$is_rebuilding && $section_storage->count() === 0) {
       $sections = [];
@@ -269,7 +281,7 @@ class LayoutBuilderController implements ContainerInjectionInterface {
       ],
       'remove' => [
         '#type' => 'link',
-        '#title' => $this->t('Remove section'),
+        '#title' => $this->t('Remove section <span class="visually-hidden">@section</span>', ['@section' => $delta + 1]),
         '#url' => Url::fromRoute('layout_builder.remove_section', [
           'section_storage_type' => $storage_type,
           'section_storage' => $storage_id,
index 0118768f77b776d8ddda9f1adb48f580dd845114..7f10d6f51f9c186490989115e28f137768f04195 100644 (file)
@@ -9,6 +9,7 @@ use Drupal\Core\Plugin\Context\EntityContext;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\field\Entity\FieldConfig;
 use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
 use Drupal\layout_builder\Section;
 use Drupal\layout_builder\SectionComponent;
 use Drupal\layout_builder\SectionStorage\SectionStorageTrait;
@@ -110,10 +111,10 @@ class LayoutBuilderEntityViewDisplay extends BaseEntityViewDisplay implements La
       $bundle = $this->getTargetBundle();
 
       if ($new_value) {
-        $this->addSectionField($entity_type_id, $bundle, 'layout_builder__layout');
+        $this->addSectionField($entity_type_id, $bundle, OverridesSectionStorage::FIELD_NAME);
       }
       else {
-        $this->removeSectionField($entity_type_id, $bundle, 'layout_builder__layout');
+        $this->removeSectionField($entity_type_id, $bundle, OverridesSectionStorage::FIELD_NAME);
       }
     }
 
@@ -274,8 +275,8 @@ class LayoutBuilderEntityViewDisplay extends BaseEntityViewDisplay implements La
    *   The sections.
    */
   protected function getRuntimeSections(FieldableEntityInterface $entity) {
-    if ($this->isOverridable() && !$entity->get('layout_builder__layout')->isEmpty()) {
-      return $entity->get('layout_builder__layout')->getSections();
+    if ($this->isOverridable() && !$entity->get(OverridesSectionStorage::FIELD_NAME)->isEmpty()) {
+      return $entity->get(OverridesSectionStorage::FIELD_NAME)->getSections();
     }
 
     return $this->getSections();
index c2e3bfb188899606daad512542260c377b03d872..276e680b43382ab3b99231f152d0c2d451574f2b 100644 (file)
@@ -5,6 +5,8 @@ namespace Drupal\layout_builder\EventSubscriber;
 use Drupal\block_content\Access\RefinableDependentAccessInterface;
 use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Block\BlockPluginInterface;
+use Drupal\Core\Render\Element;
+use Drupal\Core\Render\PreviewFallbackInterface;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\layout_builder\Access\LayoutPreviewAccessAllowed;
 use Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent;
@@ -98,6 +100,9 @@ class BlockComponentRenderArray implements EventSubscriberInterface {
         '#weight' => $event->getComponent()->getWeight(),
         'content' => $block->build(),
       ];
+      if ($event->inPreview() && Element::isEmpty($build['content']) && $block instanceof PreviewFallbackInterface) {
+        $build['content']['#markup'] = $block->getPreviewFallbackString();
+      }
       $event->setBuild($build);
     }
   }
index fea971bbcd6ddf9071382230dc92a0fdc38e8e67..72edc2f5b78901ca6c9409c4b29785d52ff13fb2 100644 (file)
@@ -7,6 +7,7 @@ use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\field_ui\Form\EntityViewDisplayEditForm;
 use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface;
+use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
 use Drupal\layout_builder\SectionStorageInterface;
 
 /**
@@ -48,8 +49,8 @@ class LayoutBuilderEntityViewDisplayForm extends EntityViewDisplayEditForm {
     $form = parent::form($form, $form_state);
 
     // Remove the Layout Builder field from the list.
-    $form['#fields'] = array_diff($form['#fields'], ['layout_builder__layout']);
-    unset($form['fields']['layout_builder__layout']);
+    $form['#fields'] = array_diff($form['#fields'], [OverridesSectionStorage::FIELD_NAME]);
+    unset($form['fields'][OverridesSectionStorage::FIELD_NAME]);
 
     $is_enabled = $this->entity->isLayoutBuilderEnabled();
     if ($is_enabled) {
@@ -133,7 +134,7 @@ class LayoutBuilderEntityViewDisplayForm extends EntityViewDisplayEditForm {
 
     $entity_type = $this->entityTypeManager->getDefinition($display->getTargetEntityTypeId());
     $query = $this->entityTypeManager->getStorage($display->getTargetEntityTypeId())->getQuery()
-      ->exists('layout_builder__layout');
+      ->exists(OverridesSectionStorage::FIELD_NAME);
     if ($bundle_key = $entity_type->getKey('bundle')) {
       $query->condition($bundle_key, $display->getTargetBundle());
     }
index 9124027542ef13cfdba94f2b30775032e1ea0311..fc22168a99e5fb0afb6824c048944b0edaf6b473 100644 (file)
@@ -6,6 +6,7 @@ use Drupal\Component\Plugin\DerivativeInspectionInterface;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\FieldableEntityInterface;
 use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface;
+use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
 
 /**
  * Methods to help with entities using the layout builder.
@@ -65,7 +66,7 @@ trait LayoutEntityHelperTrait {
       return $entity->getSections();
     }
     elseif ($this->isEntityUsingFieldOverride($entity)) {
-      return $entity->get('layout_builder__layout')->getSections();
+      return $entity->get(OverridesSectionStorage::FIELD_NAME)->getSections();
     }
     return NULL;
   }
@@ -102,7 +103,7 @@ trait LayoutEntityHelperTrait {
    *   TRUE if the entity is using a field for a layout override.
    */
   protected function isEntityUsingFieldOverride(EntityInterface $entity) {
-    return $entity instanceof FieldableEntityInterface && $entity->hasField('layout_builder__layout');
+    return $entity instanceof FieldableEntityInterface && $entity->hasField(OverridesSectionStorage::FIELD_NAME);
   }
 
 }
index 39725afc7e20068e9d25fdea0af447d0c32c117f..69676861db43583b426fe836d6d36ac3f71cf660 100644 (file)
@@ -45,6 +45,15 @@ class LayoutTempstoreRepository implements LayoutTempstoreRepositoryInterface {
     return $section_storage;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function has(SectionStorageInterface $section_storage) {
+    $id = $section_storage->getStorageId();
+    $tempstore = $this->getTempstore($section_storage)->get($id);
+    return !empty($tempstore['section_storage']);
+  }
+
   /**
    * {@inheritdoc}
    */
index 4972a47f6f058359b5702b84c12ab2ca164dbf77..67dc59ca99d807d34b60a44778a138f5df88dca4 100644 (file)
@@ -35,6 +35,17 @@ interface LayoutTempstoreRepositoryInterface {
    */
   public function set(SectionStorageInterface $section_storage);
 
+  /**
+   * Checks for the existence of a tempstore version of a section storage.
+   *
+   * @param \Drupal\layout_builder\SectionStorageInterface $section_storage
+   *   The section storage to check for in tempstore.
+   *
+   * @return bool
+   *   TRUE if there is a tempstore version of this section storage.
+   */
+  public function has(SectionStorageInterface $section_storage);
+
   /**
    * Removes the tempstore version of a section storage.
    *
index ddb7f01fd9bfa3047c57499b49e81486e1bfa316..6b991037687d4d9c9ae76afadad3a74a5e8aa602 100644 (file)
@@ -130,13 +130,22 @@ class ExtraFieldBlock extends BlockBase implements ContextAwarePluginInterface,
         // render array. If the hook is invoked the placeholder will be
         // replaced.
         // @see ::replaceFieldPlaceholder()
-        '#markup' => new TranslatableMarkup('Placeholder for the "@field" field', ['@field' => $extra_fields['display'][$this->fieldName]['label']]),
+        '#markup' => $this->getPreviewFallbackString(),
       ];
     }
     CacheableMetadata::createFromObject($this)->applyTo($build);
     return $build;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getPreviewFallbackString() {
+    $entity = $this->getEntity();
+    $extra_fields = $this->entityFieldManager->getExtraFields($entity->getEntityTypeId(), $entity->bundle());
+    return new TranslatableMarkup('Placeholder for the "@field" field', ['@field' => $extra_fields['display'][$this->fieldName]['label']]);
+  }
+
   /**
    * Replaces all placeholders for a given field.
    *
index bb8ed11797cf04f4314f36d1d7868a4eb193677a..88e608f6f465d8a2e62da64b251cb227136db0e7 100644 (file)
@@ -17,7 +17,6 @@ use Drupal\Core\Field\FormatterPluginManager;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\Core\Plugin\ContextAwarePluginInterface;
-use Drupal\Core\Render\Element;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Psr\Log\LoggerInterface;
@@ -160,13 +159,17 @@ class FieldBlock extends BlockBase implements ContextAwarePluginInterface, Conta
       $build = [];
       $this->logger->warning('The field "%field" failed to render with the error of "%error".', ['%field' => $this->fieldName, '%error' => $e->getMessage()]);
     }
-    if (!empty($entity->in_preview) && !Element::getVisibleChildren($build)) {
-      $build['content']['#markup'] = new TranslatableMarkup('Placeholder for the "@field" field', ['@field' => $this->getFieldDefinition()->getLabel()]);
-    }
     CacheableMetadata::createFromObject($this)->applyTo($build);
     return $build;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getPreviewFallbackString() {
+    return new TranslatableMarkup('Placeholder for the "@field" field', ['@field' => $this->getFieldDefinition()->getLabel()]);
+  }
+
   /**
    * {@inheritdoc}
    */
index 3e15fcd78227aa021080a0c8e9bc00996003176d..57d2c1ec7c4596b382241d3ac71fc971abaf86f9 100644 (file)
@@ -106,7 +106,7 @@ class FieldBlockDeriver extends DeriverBase implements ContainerDeriverInterface
             $derivative['default_formatter'] = $field_type_definition['default_formatter'];
           }
 
-          $derivative['category'] = $this->t('@entity', ['@entity' => $entity_type_labels[$entity_type_id]]);
+          $derivative['category'] = $this->t('@entity fields', ['@entity' => $entity_type_labels[$entity_type_id]]);
 
           $derivative['admin_label'] = $field_definition->getLabel();
 
index e2053f342eebb9f052154d4d22b35e1d06a70dfb..9e4a2ce42c4fd5bb41ce4ec084d3054d11467372 100644 (file)
@@ -3,11 +3,11 @@
 namespace Drupal\layout_builder\Plugin\Derivative;
 
 use Drupal\Component\Plugin\Derivative\DeriverBase;
-use Drupal\Core\Entity\EntityTypeInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
-use Drupal\Core\Entity\FieldableEntityInterface;
 use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\layout_builder\Plugin\SectionStorage\SectionStorageLocalTaskProviderInterface;
+use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -28,14 +28,24 @@ class LayoutBuilderLocalTaskDeriver extends DeriverBase implements ContainerDeri
    */
   protected $entityTypeManager;
 
+  /**
+   * The section storage manager.
+   *
+   * @var \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface
+   */
+  protected $sectionStorageManager;
+
   /**
    * Constructs a new LayoutBuilderLocalTaskDeriver.
    *
    * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
    *   The entity type manager.
+   * @param \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface $section_storage_manager
+   *   The section storage manager.
    */
-  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
+  public function __construct(EntityTypeManagerInterface $entity_type_manager, SectionStorageManagerInterface $section_storage_manager) {
     $this->entityTypeManager = $entity_type_manager;
+    $this->sectionStorageManager = $section_storage_manager;
   }
 
   /**
@@ -43,7 +53,8 @@ class LayoutBuilderLocalTaskDeriver extends DeriverBase implements ContainerDeri
    */
   public static function create(ContainerInterface $container, $base_plugin_id) {
     return new static(
-      $container->get('entity_type.manager')
+      $container->get('entity_type.manager'),
+      $container->get('plugin.manager.layout_builder.section_storage')
     );
   }
 
@@ -51,84 +62,13 @@ class LayoutBuilderLocalTaskDeriver extends DeriverBase implements ContainerDeri
    * {@inheritdoc}
    */
   public function getDerivativeDefinitions($base_plugin_definition) {
-    foreach ($this->getEntityTypesForOverrides() as $entity_type_id => $entity_type) {
-      // Overrides.
-      $this->derivatives["layout_builder.overrides.$entity_type_id.view"] = $base_plugin_definition + [
-        'route_name' => "layout_builder.overrides.$entity_type_id.view",
-        'weight' => 15,
-        'title' => $this->t('Layout'),
-        'base_route' => "entity.$entity_type_id.canonical",
-        'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
-      ];
-      $this->derivatives["layout_builder.overrides.$entity_type_id.save"] = $base_plugin_definition + [
-        'route_name' => "layout_builder.overrides.$entity_type_id.save",
-        'title' => $this->t('Save Layout'),
-        'parent_id' => "layout_builder_ui:layout_builder.overrides.$entity_type_id.view",
-        'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
-      ];
-      $this->derivatives["layout_builder.overrides.$entity_type_id.cancel"] = $base_plugin_definition + [
-        'route_name' => "layout_builder.overrides.$entity_type_id.cancel",
-        'title' => $this->t('Cancel Layout'),
-        'parent_id' => "layout_builder_ui:layout_builder.overrides.$entity_type_id.view",
-        'weight' => 5,
-        'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
-      ];
-      // @todo This link should be conditionally displayed, see
-      //   https://www.drupal.org/node/2917777.
-      $this->derivatives["layout_builder.overrides.$entity_type_id.revert"] = $base_plugin_definition + [
-        'route_name' => "layout_builder.overrides.$entity_type_id.revert",
-        'title' => $this->t('Revert to defaults'),
-        'parent_id' => "layout_builder_ui:layout_builder.overrides.$entity_type_id.view",
-        'weight' => 10,
-        'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
-      ];
+    foreach ($this->sectionStorageManager->getDefinitions() as $plugin_id => $definition) {
+      $section_storage = $this->sectionStorageManager->loadEmpty($plugin_id);
+      if ($section_storage instanceof SectionStorageLocalTaskProviderInterface) {
+        $this->derivatives += $section_storage->buildLocalTasks($base_plugin_definition);
+      }
     }
-
-    foreach ($this->getEntityTypesForDefaults() as $entity_type_id => $entity_type) {
-      // Defaults.
-      $this->derivatives["layout_builder.defaults.$entity_type_id.view"] = $base_plugin_definition + [
-        'route_name' => "layout_builder.defaults.$entity_type_id.view",
-        'title' => $this->t('Manage layout'),
-        'base_route' => "layout_builder.defaults.$entity_type_id.view",
-      ];
-      $this->derivatives["layout_builder.defaults.$entity_type_id.save"] = $base_plugin_definition + [
-        'route_name' => "layout_builder.defaults.$entity_type_id.save",
-        'title' => $this->t('Save Layout'),
-        'parent_id' => "layout_builder_ui:layout_builder.defaults.$entity_type_id.view",
-      ];
-      $this->derivatives["layout_builder.defaults.$entity_type_id.cancel"] = $base_plugin_definition + [
-        'route_name' => "layout_builder.defaults.$entity_type_id.cancel",
-        'title' => $this->t('Cancel Layout'),
-        'weight' => 5,
-        'parent_id' => "layout_builder_ui:layout_builder.defaults.$entity_type_id.view",
-      ];
-    }
-
     return $this->derivatives;
   }
 
-  /**
-   * Returns an array of entity types relevant for defaults.
-   *
-   * @return \Drupal\Core\Entity\EntityTypeInterface[]
-   *   An array of entity types.
-   */
-  protected function getEntityTypesForDefaults() {
-    return array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $entity_type) {
-      return $entity_type->entityClassImplements(FieldableEntityInterface::class) && $entity_type->hasViewBuilderClass() && $entity_type->get('field_ui_base_route');
-    });
-  }
-
-  /**
-   * Returns an array of entity types relevant for overrides.
-   *
-   * @return \Drupal\Core\Entity\EntityTypeInterface[]
-   *   An array of entity types.
-   */
-  protected function getEntityTypesForOverrides() {
-    return array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $entity_type) {
-      return $entity_type->entityClassImplements(FieldableEntityInterface::class) && $entity_type->hasViewBuilderClass() && $entity_type->hasLinkTemplate('canonical');
-    });
-  }
-
 }
index 6fab11996b02d3de386112ad6268370ea5cd6167..d35041d03d8fe09835e442cf85cd3b538b2d5c20 100644 (file)
@@ -32,7 +32,7 @@ use Symfony\Component\Routing\RouteCollection;
  *   experimental modules and development releases of contributed modules.
  *   See https://www.drupal.org/core/experimental for more information.
  */
-class DefaultsSectionStorage extends SectionStorageBase implements ContainerFactoryPluginInterface, DefaultsSectionStorageInterface {
+class DefaultsSectionStorage extends SectionStorageBase implements ContainerFactoryPluginInterface, DefaultsSectionStorageInterface, SectionStorageLocalTaskProviderInterface {
 
   /**
    * The entity type manager.
@@ -196,6 +196,32 @@ class DefaultsSectionStorage extends SectionStorageBase implements ContainerFact
     }
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function buildLocalTasks($base_plugin_definition) {
+    $local_tasks = [];
+    foreach ($this->getEntityTypes() as $entity_type_id => $entity_type) {
+      $local_tasks["layout_builder.defaults.$entity_type_id.view"] = $base_plugin_definition + [
+        'route_name' => "layout_builder.defaults.$entity_type_id.view",
+        'title' => $this->t('Manage layout'),
+        'base_route' => "layout_builder.defaults.$entity_type_id.view",
+      ];
+      $local_tasks["layout_builder.defaults.$entity_type_id.save"] = $base_plugin_definition + [
+        'route_name' => "layout_builder.defaults.$entity_type_id.save",
+        'title' => $this->t('Save Layout'),
+        'parent_id' => "layout_builder_ui:layout_builder.defaults.$entity_type_id.view",
+      ];
+      $local_tasks["layout_builder.defaults.$entity_type_id.cancel"] = $base_plugin_definition + [
+        'route_name' => "layout_builder.defaults.$entity_type_id.cancel",
+        'title' => $this->t('Cancel Layout'),
+        'weight' => 5,
+        'parent_id' => "layout_builder_ui:layout_builder.defaults.$entity_type_id.view",
+      ];
+    }
+    return $local_tasks;
+  }
+
   /**
    * Returns an array of relevant entity types.
    *
index 3a92ee63fab7fe21fbfd5f50243c25650f7da1ad..bba10ef67f39959df3f61592d18b25bfca836261 100644 (file)
@@ -30,7 +30,14 @@ use Symfony\Component\Routing\RouteCollection;
  *   experimental modules and development releases of contributed modules.
  *   See https://www.drupal.org/core/experimental for more information.
  */
-class OverridesSectionStorage extends SectionStorageBase implements ContainerFactoryPluginInterface, OverridesSectionStorageInterface {
+class OverridesSectionStorage extends SectionStorageBase implements ContainerFactoryPluginInterface, OverridesSectionStorageInterface, SectionStorageLocalTaskProviderInterface {
+
+  /**
+   * The field name used by this storage.
+   *
+   * @var string
+   */
+  const FIELD_NAME = 'layout_builder__layout';
 
   /**
    * The entity type manager.
@@ -127,8 +134,8 @@ class OverridesSectionStorage extends SectionStorageBase implements ContainerFac
     if (strpos($id, '.') !== FALSE) {
       list($entity_type_id, $entity_id) = explode('.', $id, 2);
       $entity = $this->entityTypeManager->getStorage($entity_type_id)->load($entity_id);
-      if ($entity instanceof FieldableEntityInterface && $entity->hasField('layout_builder__layout')) {
-        return $entity->get('layout_builder__layout');
+      if ($entity instanceof FieldableEntityInterface && $entity->hasField(static::FIELD_NAME)) {
+        return $entity->get(static::FIELD_NAME);
       }
     }
     throw new \InvalidArgumentException(sprintf('The "%s" ID for the "%s" section storage type is invalid', $id, $this->getStorageType()));
@@ -157,6 +164,45 @@ class OverridesSectionStorage extends SectionStorageBase implements ContainerFac
     }
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function buildLocalTasks($base_plugin_definition) {
+    $local_tasks = [];
+    foreach ($this->getEntityTypes() as $entity_type_id => $entity_type) {
+      $local_tasks["layout_builder.overrides.$entity_type_id.view"] = $base_plugin_definition + [
+        'route_name' => "layout_builder.overrides.$entity_type_id.view",
+        'weight' => 15,
+        'title' => $this->t('Layout'),
+        'base_route' => "entity.$entity_type_id.canonical",
+        'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
+      ];
+      $local_tasks["layout_builder.overrides.$entity_type_id.save"] = $base_plugin_definition + [
+        'route_name' => "layout_builder.overrides.$entity_type_id.save",
+        'title' => $this->t('Save Layout'),
+        'parent_id' => "layout_builder_ui:layout_builder.overrides.$entity_type_id.view",
+        'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
+      ];
+      $local_tasks["layout_builder.overrides.$entity_type_id.cancel"] = $base_plugin_definition + [
+        'route_name' => "layout_builder.overrides.$entity_type_id.cancel",
+        'title' => $this->t('Cancel Layout'),
+        'parent_id' => "layout_builder_ui:layout_builder.overrides.$entity_type_id.view",
+        'weight' => 5,
+        'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
+      ];
+      // @todo This link should be conditionally displayed, see
+      //   https://www.drupal.org/node/2917777.
+      $local_tasks["layout_builder.overrides.$entity_type_id.revert"] = $base_plugin_definition + [
+        'route_name' => "layout_builder.overrides.$entity_type_id.revert",
+        'title' => $this->t('Revert to defaults'),
+        'parent_id' => "layout_builder_ui:layout_builder.overrides.$entity_type_id.view",
+        'weight' => 10,
+        'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
+      ];
+    }
+    return $local_tasks;
+  }
+
   /**
    * Determines if this entity type's ID is stored as an integer.
    *
diff --git a/web/core/modules/layout_builder/src/Plugin/SectionStorage/SectionStorageLocalTaskProviderInterface.php b/web/core/modules/layout_builder/src/Plugin/SectionStorage/SectionStorageLocalTaskProviderInterface.php
new file mode 100644 (file)
index 0000000..a275d63
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+
+namespace Drupal\layout_builder\Plugin\SectionStorage;
+
+/**
+ * Allows section storage plugins to provide local tasks.
+ *
+ * @see \Drupal\layout_builder\Plugin\Derivative\LayoutBuilderLocalTaskDeriver
+ * @see \Drupal\layout_builder\SectionStorageInterface
+ *
+ * @internal
+ *   Layout Builder is currently experimental and should only be leveraged by
+ *   experimental modules and development releases of contributed modules.
+ *   See https://www.drupal.org/core/experimental for more information.
+ */
+interface SectionStorageLocalTaskProviderInterface {
+
+  /**
+   * Provides the local tasks dynamically for Layout Builder plugins.
+   *
+   * @param mixed $base_plugin_definition
+   *   The definition of the base plugin.
+   *
+   * @return array
+   *   An array of full derivative definitions keyed on derivative ID.
+   */
+  public function buildLocalTasks($base_plugin_definition);
+
+}
index 30d903a30ab8a8f15eb116389a41c385d833313c..1ec0b8af10aea6d433106e59d99758836cdc8b4d 100644 (file)
@@ -356,4 +356,13 @@ class Section {
     );
   }
 
+  /**
+   * Magic method: Implements a deep clone.
+   */
+  public function __clone() {
+    foreach ($this->components as $uuid => $component) {
+      $this->components[$uuid] = clone $component;
+    }
+  }
+
 }
index 9d942c7ad85982898e0480f3f7cac4d075b2f83e..36729d2ba6240d0377c9adf6cc7734d317c5c5fc 100644 (file)
@@ -111,4 +111,17 @@ trait SectionStorageTrait {
     return isset($this->getSections()[$delta]);
   }
 
+  /**
+   * Magic method: Implements a deep clone.
+   */
+  public function __clone() {
+    $sections = $this->getSections();
+
+    foreach ($sections as $delta => $item) {
+      $sections[$delta] = clone $item;
+    }
+
+    $this->setSections($sections);
+  }
+
 }
diff --git a/web/core/modules/layout_builder/tests/modules/layout_builder_fieldblock_test/config/schema/layout_builder_fieldblock_test.schema.yml b/web/core/modules/layout_builder/tests/modules/layout_builder_fieldblock_test/config/schema/layout_builder_fieldblock_test.schema.yml
new file mode 100644 (file)
index 0000000..92ce34d
--- /dev/null
@@ -0,0 +1,3 @@
+# See \Drupal\layout_builder_fieldblock_test\Plugin\Block\FieldBlock.
+block.settings.field_block_test:*:*:*:
+  type: block.settings.field_block:*:*:*
diff --git a/web/core/modules/layout_builder/tests/modules/layout_builder_fieldblock_test/layout_builder_fieldblock_test.info.yml b/web/core/modules/layout_builder/tests/modules/layout_builder_fieldblock_test/layout_builder_fieldblock_test.info.yml
new file mode 100644 (file)
index 0000000..607877e
--- /dev/null
@@ -0,0 +1,6 @@
+name: 'Layout Builder test'
+type: module
+description: 'Support module for testing layout building.'
+package: Testing
+version: VERSION
+core: 8.x
diff --git a/web/core/modules/layout_builder/tests/modules/layout_builder_fieldblock_test/src/Plugin/Block/FieldBlock.php b/web/core/modules/layout_builder/tests/modules/layout_builder_fieldblock_test/src/Plugin/Block/FieldBlock.php
new file mode 100644 (file)
index 0000000..07a00f5
--- /dev/null
@@ -0,0 +1,27 @@
+<?php
+
+namespace Drupal\layout_builder_fieldblock_test\Plugin\Block;
+
+use Drupal\layout_builder\Plugin\Block\FieldBlock as LayoutBuilderFieldBlock;
+
+/**
+ * Provides test field block to test with Block UI.
+ *
+ * \Drupal\Tests\layout_builder\FunctionalJavascript\FieldBlockTest provides
+ * test coverage of complex AJAX interactions within certain field blocks.
+ * layout_builder_plugin_filter_block__block_ui_alter() removes certain blocks
+ * with 'layout_builder' as the provider. To make these blocks available during
+ * testing, this plugin uses the same deriver but each derivative will have a
+ * different provider.
+ *
+ * @Block(
+ *   id = "field_block_test",
+ *   deriver = "\Drupal\layout_builder\Plugin\Derivative\FieldBlockDeriver",
+ * )
+ *
+ * @see \Drupal\Tests\layout_builder\FunctionalJavascript\FieldBlockTest
+ * @see layout_builder_plugin_filter_block__block_ui_alter()
+ */
+class FieldBlock extends LayoutBuilderFieldBlock {
+
+}
index 50e84f13d770412bfe21a6018ef6bdcc4d6e44c5..5f7df456ef5ef32be2e03188d13e201b5e408119 100644 (file)
@@ -22,6 +22,7 @@ class LayoutBuilderTest extends BrowserTestBase {
     'layout_builder_views_test',
     'layout_test',
     'block',
+    'block_test',
     'node',
     'layout_builder_test',
   ];
@@ -90,7 +91,7 @@ class LayoutBuilderTest extends BrowserTestBase {
     // The body field is only present once.
     $assert_session->elementsCount('css', '.field--name-body', 1);
     // The extra field is only present once.
-    $this->assertTextAppearsOnce('Placeholder for the "Extra label" field');
+    $assert_session->pageTextContainsOnce('Placeholder for the "Extra label" field');
     // Save the defaults.
     $assert_session->linkExists('Save Layout');
     $this->clickLink('Save Layout');
@@ -105,7 +106,7 @@ class LayoutBuilderTest extends BrowserTestBase {
     // The body field is only present once.
     $assert_session->elementsCount('css', '.field--name-body', 1);
     // The extra field is only present once.
-    $this->assertTextAppearsOnce('Placeholder for the "Extra label" field');
+    $assert_session->pageTextContainsOnce('Placeholder for the "Extra label" field');
 
     // Add a new block.
     $assert_session->linkExists('Add Block');
@@ -514,13 +515,68 @@ class LayoutBuilderTest extends BrowserTestBase {
   }
 
   /**
-   * Asserts that a text string only appears once on the page.
+   * Tests the usage of placeholders for empty blocks.
    *
-   * @param string $needle
-   *   The string to look for.
+   * @see \Drupal\Core\Block\BlockPluginInterface::getPlaceholderString()
+   * @see \Drupal\layout_builder\EventSubscriber\BlockComponentRenderArray::onBuildRender()
    */
-  protected function assertTextAppearsOnce($needle) {
-    $this->assertEquals(1, substr_count($this->getSession()->getPage()->getContent(), $needle), "'$needle' only appears once on the page.");
+  public function testBlockPlaceholder() {
+    $assert_session = $this->assertSession();
+    $page = $this->getSession()->getPage();
+
+    $this->drupalLogin($this->drupalCreateUser([
+      'configure any layout',
+      'administer node display',
+    ]));
+
+    $field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field';
+    $this->drupalPostForm("$field_ui_prefix/display/default", ['layout[enabled]' => TRUE], 'Save');
+
+    // Customize the default view mode.
+    $this->drupalGet("$field_ui_prefix/display-layout/default");
+
+    // Add a block whose content is controlled by state and is empty by default.
+    $this->clickLink('Add Block');
+    $this->clickLink('Test block caching');
+    $page->fillField('settings[label]', 'The block label');
+    $page->pressButton('Add Block');
+
+    $block_content = 'I am content';
+    $placeholder_content = 'Placeholder for the "The block label" block';
+
+    // The block placeholder is displayed and there is no content.
+    $assert_session->pageTextContains($placeholder_content);
+    $assert_session->pageTextNotContains($block_content);
+
+    // Set block content and reload the page.
+    \Drupal::state()->set('block_test.content', $block_content);
+    $this->getSession()->reload();
+
+    // The block placeholder is no longer displayed and the content is visible.
+    $assert_session->pageTextNotContains($placeholder_content);
+    $assert_session->pageTextContains($block_content);
+  }
+
+  /**
+   * Tests the Block UI when Layout Builder is installed.
+   */
+  public function testBlockUiListing() {
+    $assert_session = $this->assertSession();
+    $page = $this->getSession()->getPage();
+
+    $this->drupalLogin($this->drupalCreateUser([
+      'administer blocks',
+    ]));
+
+    $this->drupalGet('admin/structure/block');
+    $page->clickLink('Place block');
+
+    // Ensure that blocks expected to appear are available.
+    $assert_session->pageTextContains('Test HTML block');
+    $assert_session->pageTextContains('Block test');
+    // Ensure that blocks not expected to appear are not available.
+    $assert_session->pageTextNotContains('Body');
+    $assert_session->pageTextNotContains('Content fields');
   }
 
 }
index e4af3290c468475cbfda28acfe6490fe5041c08e..e29609b30e712a11fc7ad7b37f626a3089d2c159 100644 (file)
@@ -4,6 +4,7 @@ namespace Drupal\Tests\layout_builder\Functional;
 
 use Drupal\language\Entity\ConfigurableLanguage;
 use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
+use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
 use Drupal\layout_builder\Section;
 use Drupal\layout_builder\SectionComponent;
 use Drupal\Tests\BrowserTestBase;
@@ -20,13 +21,6 @@ class LayoutSectionTest extends BrowserTestBase {
    */
   public static $modules = ['field_ui', 'layout_builder', 'node', 'block_test'];
 
-  /**
-   * The name of the layout section field.
-   *
-   * @var string
-   */
-  protected $fieldName = 'layout_builder__layout';
-
   /**
    * {@inheritdoc}
    */
@@ -226,7 +220,7 @@ class LayoutSectionTest extends BrowserTestBase {
     ]);
     $entity->addTranslation('es', [
       'title' => 'Translated node title',
-      $this->fieldName => [
+      OverridesSectionStorage::FIELD_NAME => [
         [
           'section' => new Section('layout_twocol', [], [
             'foo' => new SectionComponent('foo', 'first', [
@@ -373,7 +367,7 @@ class LayoutSectionTest extends BrowserTestBase {
           'value' => 'The node body',
         ],
       ],
-      $this->fieldName => $section_values,
+      OverridesSectionStorage::FIELD_NAME => $section_values,
     ]);
   }
 
index f2ecd52b3bf47bdc853fd32f67f4f0ee9fa57846..bd4d1843c76fdb33ffae13a236c2f0494f900fd0 100644 (file)
@@ -16,7 +16,14 @@ class FieldBlockTest extends WebDriverTestBase {
   /**
    * {@inheritdoc}
    */
-  public static $modules = ['block', 'datetime', 'layout_builder', 'user'];
+  protected static $modules = [
+    'block',
+    'datetime',
+    'layout_builder',
+    'user',
+    // See \Drupal\layout_builder_fieldblock_test\Plugin\Block\FieldBlock.
+    'layout_builder_fieldblock_test',
+  ];
 
   /**
    * {@inheritdoc}
@@ -67,7 +74,7 @@ class FieldBlockTest extends WebDriverTestBase {
     $assert_session->pageTextNotContains('Initial email');
 
     $assert_session->pageTextContains('Date field');
-    $block_url = 'admin/structure/block/add/field_block%3Auser%3Auser%3Afield_date/classy';
+    $block_url = 'admin/structure/block/add/field_block_test%3Auser%3Auser%3Afield_date/classy';
     $assert_session->linkByHrefExists($block_url);
 
     $this->drupalGet($block_url);
index 05892bd314735b1436d54eddd43e660da70f630f..a94be8ba17db5229498125442c45e336c0fd91f8 100644 (file)
@@ -192,8 +192,8 @@ class InlineBlockPrivateFilesTest extends InlineBlockTestBase {
     $page = $this->getSession()->getPage();
     $page->clickLink('Add Block');
     $assert_session->assertWaitOnAjaxRequest();
-    $this->assertNotEmpty($assert_session->waitForElementVisible('css', '.block-categories details:contains(Create new block)'));
-    $this->clickLink('Basic block');
+    $this->assertNotEmpty($assert_session->waitForLink('Create custom block'));
+    $this->clickLink('Create custom block');
     $assert_session->assertWaitOnAjaxRequest();
     $assert_session->fieldValueEquals('Title', '');
     $page->findField('Title')->setValue($title);
index 9fdc8fdd3cca27a32e26fa6b7ac3dde17ed1fc4f..4f463192d150bde3a9b787b4ffcbddeba55e3509 100644 (file)
@@ -428,4 +428,74 @@ class InlineBlockTest extends InlineBlockTestBase {
     $assert_session->pageTextNotContains('You are not authorized to access this page');
   }
 
+  /**
+   * Tests the workflow for adding an inline block depending on number of types.
+   *
+   * @throws \Behat\Mink\Exception\ElementNotFoundException
+   * @throws \Behat\Mink\Exception\ExpectationException
+   */
+  public function testAddWorkFlow() {
+    $assert_session = $this->assertSession();
+    $page = $this->getSession()->getPage();
+    $type_storage = $this->container->get('entity_type.manager')->getStorage('block_content_type');
+    foreach ($type_storage->loadByProperties() as $type) {
+      $type->delete();
+    }
+
+    $this->drupalLogin($this->drupalCreateUser([
+      'access contextual links',
+      'configure any layout',
+      'administer node display',
+      'administer node fields',
+    ]));
+
+    // Enable layout builder and overrides.
+    $this->drupalPostForm(
+      static::FIELD_UI_PREFIX . '/display/default',
+      ['layout[enabled]' => TRUE, 'layout[allow_custom]' => TRUE],
+      'Save'
+    );
+
+    $layout_default_path = 'admin/structure/types/manage/bundle_with_section_field/display-layout/default';
+    $this->drupalGet($layout_default_path);
+    // Add a basic block with the body field set.
+    $page->clickLink('Add Block');
+    $assert_session->assertWaitOnAjaxRequest();
+    // Confirm that with no block content types the link does not appear.
+    $assert_session->linkNotExists('Create custom block');
+
+    $this->createBlockContentType('basic', 'Basic block');
+
+    $this->drupalGet($layout_default_path);
+    // Add a basic block with the body field set.
+    $page->clickLink('Add Block');
+    $assert_session->assertWaitOnAjaxRequest();
+    // Confirm with only 1 type the "Create custom block" link goes directly t
+    // block add form.
+    $assert_session->linkNotExists('Basic block');
+    $this->clickLink('Create custom block');
+    $assert_session->assertWaitOnAjaxRequest();
+    $assert_session->fieldExists('Title');
+
+    $this->createBlockContentType('advanced', 'Advanced block');
+
+    $this->drupalGet($layout_default_path);
+    // Add a basic block with the body field set.
+    $page->clickLink('Add Block');
+    // Confirm that, when more than 1 type exists, "Create custom block" shows a
+    // list of block types.
+    $assert_session->assertWaitOnAjaxRequest();
+    $assert_session->linkNotExists('Basic block');
+    $assert_session->linkNotExists('Advanced block');
+    $this->clickLink('Create custom block');
+    $assert_session->assertWaitOnAjaxRequest();
+    $assert_session->fieldNotExists('Title');
+    $assert_session->linkExists('Basic block');
+    $assert_session->linkExists('Advanced block');
+
+    $this->clickLink('Advanced block');
+    $assert_session->assertWaitOnAjaxRequest();
+    $assert_session->fieldExists('Title');
+  }
+
 }
index 6c99c6c39f107068d21fe5d46edf3d581d58e781..2b781766a03812ace2b4f649f2a9236788438734 100644 (file)
@@ -71,13 +71,7 @@ abstract class InlineBlockTestBase extends WebDriverTestBase {
         ],
       ],
     ]);
-    $bundle = BlockContentType::create([
-      'id' => 'basic',
-      'label' => 'Basic block',
-      'revision' => 1,
-    ]);
-    $bundle->save();
-    block_content_add_body_field($bundle->id());
+    $this->createBlockContentType('basic', 'Basic block');
 
     $this->blockStorage = $this->container->get('entity_type.manager')->getStorage('block_content');
   }
@@ -146,8 +140,8 @@ abstract class InlineBlockTestBase extends WebDriverTestBase {
     $page = $this->getSession()->getPage();
     $page->clickLink('Add Block');
     $assert_session->assertWaitOnAjaxRequest();
-    $this->assertNotEmpty($assert_session->waitForElementVisible('css', '.block-categories details:contains(Create new block)'));
-    $this->clickLink('Basic block');
+    $this->assertNotEmpty($assert_session->waitForLink('Create custom block'));
+    $this->clickLink('Create custom block');
     $assert_session->assertWaitOnAjaxRequest();
     $textarea = $assert_session->waitForElement('css', '[name="settings[block_form][body][0][value]"]');
     $this->assertNotEmpty($textarea);
@@ -219,4 +213,22 @@ abstract class InlineBlockTestBase extends WebDriverTestBase {
     }
   }
 
+  /**
+   * Creates a block content type.
+   *
+   * @param string $id
+   *   The block type id.
+   * @param string $label
+   *   The block type label.
+   */
+  protected function createBlockContentType($id, $label) {
+    $bundle = BlockContentType::create([
+      'id' => $id,
+      'label' => $label,
+      'revision' => 1,
+    ]);
+    $bundle->save();
+    block_content_add_body_field($bundle->id());
+  }
+
 }
diff --git a/web/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderUiTest.php b/web/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderUiTest.php
new file mode 100644 (file)
index 0000000..c5510e9
--- /dev/null
@@ -0,0 +1,94 @@
+<?php
+
+namespace Drupal\Tests\layout_builder\FunctionalJavascript;
+
+use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+
+/**
+ * Tests the Layout Builder UI.
+ *
+ * @group layout_builder
+ */
+class LayoutBuilderUiTest extends WebDriverTestBase {
+
+  /**
+   * Path prefix for the field UI for the test bundle.
+   *
+   * @var string
+   */
+  const FIELD_UI_PREFIX = 'admin/structure/types/manage/bundle_with_section_field';
+
+  public static $modules = [
+    'layout_builder',
+    'block',
+    'node',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    // @todo The Layout Builder UI relies on local tasks; fix in
+    //   https://www.drupal.org/project/drupal/issues/2917777.
+    $this->drupalPlaceBlock('local_tasks_block');
+
+    $this->createContentType(['type' => 'bundle_with_section_field']);
+
+    $this->drupalLogin($this->drupalCreateUser([
+      'configure any layout',
+      'administer node display',
+      'administer node fields',
+    ]));
+  }
+
+  /**
+   * Tests the message indicating unsaved changes.
+   */
+  public function testUnsavedChangesMessage() {
+    $assert_session = $this->assertSession();
+    $page = $this->getSession()->getPage();
+
+    // Enable layout builder.
+    $this->drupalPostForm(
+      static::FIELD_UI_PREFIX . '/display/default',
+      ['layout[enabled]' => TRUE],
+      'Save'
+    );
+
+    // Make and then cancel changes.
+    $this->assertModifiedLayout(static::FIELD_UI_PREFIX . '/display-layout/default');
+    $page->clickLink('Cancel Layout');
+    $assert_session->pageTextNotContains('You have unsaved changes.');
+
+    // Make and then save changes.
+    $this->assertModifiedLayout(static::FIELD_UI_PREFIX . '/display-layout/default');
+    $page->clickLink('Save Layout');
+    $assert_session->pageTextNotContains('You have unsaved changes.');
+  }
+
+  /**
+   * Asserts that modifying a layout works as expected.
+   *
+   * @param string $path
+   *   The path to a Layout Builder UI page.
+   */
+  protected function assertModifiedLayout($path) {
+    $assert_session = $this->assertSession();
+    $page = $this->getSession()->getPage();
+
+    $this->drupalGet($path);
+    $page->clickLink('Add Section');
+    $assert_session->assertWaitOnAjaxRequest();
+    $assert_session->pageTextNotContains('You have unsaved changes.');
+    $page->clickLink('One column');
+    $assert_session->assertWaitOnAjaxRequest();
+    $assert_session->pageTextContainsOnce('You have unsaved changes.');
+
+    // Reload the page.
+    $this->drupalGet($path);
+    $assert_session->pageTextContainsOnce('You have unsaved changes.');
+  }
+
+}
index d5246a0c2cd0342e65a1211f6f067b45f091009f..aaeb4ca8115e28c4f64162c9c73a989ea0c6cd4d 100644 (file)
@@ -230,11 +230,10 @@ class FieldBlockTest extends EntityKernelTestBase {
    * @covers ::build
    * @dataProvider providerTestBuild
    */
-  public function testBuild(PromiseInterface $promise, $in_preview, $expected_markup, $log_message = '', $log_arguments = []) {
+  public function testBuild(PromiseInterface $promise, $expected_markup, $log_message = '', $log_arguments = []) {
     $entity = $this->prophesize(FieldableEntityInterface::class);
     $field = $this->prophesize(FieldItemListInterface::class);
     $entity->get('the_field_name')->willReturn($field->reveal());
-    $entity->in_preview = $in_preview;
     $field->view(Argument::type('array'))->will($promise);
 
     $field_definition = $this->prophesize(FieldDefinitionInterface::class);
@@ -269,40 +268,20 @@ class FieldBlockTest extends EntityKernelTestBase {
    */
   public function providerTestBuild() {
     $data = [];
-    $data['array, no preview'] = [
+    $data['array'] = [
       new ReturnPromise([['content' => ['#markup' => 'The field value']]]),
-      FALSE,
-      'The field value',
-    ];
-    $data['array, preview'] = [
-      new ReturnPromise([['content' => ['#markup' => 'The field value']]]),
-      TRUE,
       'The field value',
     ];
-    $data['empty array, no preview'] = [
+    $data['empty array'] = [
       new ReturnPromise([[]]),
-      FALSE,
       '',
     ];
-    $data['empty array, preview'] = [
-      new ReturnPromise([[]]),
-      TRUE,
-      'Placeholder for the "The Field Label" field',
-    ];
-    $data['exception, no preview'] = [
+    $data['exception'] = [
       new ThrowPromise(new \Exception('The exception message')),
-      FALSE,
       '',
       'The field "%field" failed to render with the error of "%error".',
       ['%field' => 'the_field_name', '%error' => 'The exception message'],
     ];
-    $data['exception, preview'] = [
-      new ThrowPromise(new \Exception('The exception message')),
-      TRUE,
-      'Placeholder for the "The Field Label" field',
-      'The field "%field" failed to render with the error of "%error".',
-      ['%field' => 'the_field_name', '%error' => 'The exception message'],
-    ];
     return $data;
   }
 
index 57dacb5b18ce030d343e93d1fa58fc289616c8ba..12fd813f4fbf2db3a0e45f26cfffd9256052b492 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace Drupal\Tests\layout_builder\Kernel;
 
+use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
 use Drupal\layout_builder\Section;
 
 /**
@@ -56,7 +57,7 @@ class LayoutBuilderFieldLayoutCompatibilityTest extends LayoutBuilderCompatibili
     // Add a layout override.
     $this->enableOverrides();
     /** @var \Drupal\layout_builder\SectionStorageInterface $field_list */
-    $field_list = $this->entity->get('layout_builder__layout');
+    $field_list = $this->entity->get(OverridesSectionStorage::FIELD_NAME);
     $field_list->appendSection(new Section('layout_onecol'));
     $this->entity->save();
 
index aa1b9942c8538e19589e1b25ecdb5ee102d45497..a3ae736da1c84e86b13fc0696eae7cb145d80624 100644 (file)
@@ -4,6 +4,7 @@ namespace Drupal\Tests\layout_builder\Kernel;
 
 use Drupal\field\Entity\FieldConfig;
 use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
 use Drupal\layout_builder\Section;
 
 /**
@@ -35,7 +36,7 @@ class LayoutBuilderInstallTest extends LayoutBuilderCompatibilityTestBase {
     // Add a layout override.
     $this->enableOverrides();
     $this->entity = $this->reloadEntity($this->entity);
-    $this->entity->get('layout_builder__layout')->appendSection(new Section('layout_onecol'));
+    $this->entity->get(OverridesSectionStorage::FIELD_NAME)->appendSection(new Section('layout_onecol'));
     $this->entity->save();
 
     // The rendered entity has now changed. The non-configurable field is shown
@@ -50,7 +51,7 @@ class LayoutBuilderInstallTest extends LayoutBuilderCompatibilityTestBase {
     $this->assertNotEmpty($this->cssSelect('.layout--onecol'));
 
     // Removing the layout restores the original rendering of the entity.
-    $this->entity->get('layout_builder__layout')->removeSection(0);
+    $this->entity->get(OverridesSectionStorage::FIELD_NAME)->removeSection(0);
     $this->entity->save();
     $this->assertFieldAttributes($this->entity, $expected_fields);
 
index 5bd354675cb843e7293054ad25134c53ca5325a3..4231530fd7acd6f7afdc90c87a3ac2b5103806b6 100644 (file)
@@ -4,6 +4,7 @@ namespace Drupal\Tests\layout_builder\Kernel;
 
 use Drupal\entity_test\Entity\EntityTestBaseFieldDisplay;
 use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
+use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
 
 /**
  * Tests the field type for Layout Sections.
@@ -42,10 +43,10 @@ class LayoutSectionItemListTest extends SectionStorageTestBase {
     }, $section_data);
     $entity = EntityTestBaseFieldDisplay::create([
       'name' => 'The test entity',
-      'layout_builder__layout' => $section_data,
+      OverridesSectionStorage::FIELD_NAME => $section_data,
     ]);
     $entity->save();
-    return $entity->get('layout_builder__layout');
+    return $entity->get(OverridesSectionStorage::FIELD_NAME);
   }
 
 }
index 1879977633e3abc9b192d8fafc41c22f082c3311..6c54d6c9e4ff75fb48e628d533fa33d86e2e32a0 100644 (file)
@@ -137,6 +137,17 @@ abstract class SectionStorageTestBase extends EntityKernelTestBase {
     $this->assertSections($expected);
   }
 
+  /**
+   * Tests __clone().
+   */
+  public function testClone() {
+    $this->assertSame([], $this->sectionStorage->getSection(0)->getLayoutSettings());
+
+    $new_section_storage = clone $this->sectionStorage;
+    $new_section_storage->getSection(0)->setLayoutSettings(['asdf' => 'qwer']);
+    $this->assertSame([], $this->sectionStorage->getSection(0)->getLayoutSettings());
+  }
+
   /**
    * Asserts that the field list has the expected sections.
    *
index 0c74d01763290cc9d33daeda8ea5b2f8d4fc2894..fb46e608ae33b165db7ae8ddaf56f0e86d55d7ad 100644 (file)
@@ -16,6 +16,7 @@ class LayoutTempstoreRepositoryTest extends UnitTestCase {
 
   /**
    * @covers ::get
+   * @covers ::has
    */
   public function testGetEmptyTempstore() {
     $section_storage = $this->prophesize(SectionStorageInterface::class);
@@ -30,12 +31,15 @@ class LayoutTempstoreRepositoryTest extends UnitTestCase {
 
     $repository = new LayoutTempstoreRepository($tempstore_factory->reveal());
 
+    $this->assertFalse($repository->has($section_storage->reveal()));
+
     $result = $repository->get($section_storage->reveal());
     $this->assertSame($section_storage->reveal(), $result);
   }
 
   /**
    * @covers ::get
+   * @covers ::has
    */
   public function testGetLoadedTempstore() {
     $section_storage = $this->prophesize(SectionStorageInterface::class);
@@ -50,6 +54,8 @@ class LayoutTempstoreRepositoryTest extends UnitTestCase {
 
     $repository = new LayoutTempstoreRepository($tempstore_factory->reveal());
 
+    $this->assertTrue($repository->has($section_storage->reveal()));
+
     $result = $repository->get($section_storage->reveal());
     $this->assertSame($tempstore_section_storage->reveal(), $result);
     $this->assertNotSame($section_storage->reveal(), $result);
index 110ec2021e657d067791dd0f439e57d9dec07361..3691fb6c711fa5c5d54df5817ce8d747d0196641 100644 (file)
@@ -112,13 +112,13 @@ class OverridesSectionStorageTest extends UnitTestCase {
       $entity_storage = $this->prophesize(EntityStorageInterface::class);
 
       $entity_without_layout = $this->prophesize(FieldableEntityInterface::class);
-      $entity_without_layout->hasField('layout_builder__layout')->willReturn(FALSE);
-      $entity_without_layout->get('layout_builder__layout')->shouldNotBeCalled();
+      $entity_without_layout->hasField(OverridesSectionStorage::FIELD_NAME)->willReturn(FALSE);
+      $entity_without_layout->get(OverridesSectionStorage::FIELD_NAME)->shouldNotBeCalled();
       $entity_storage->load('entity_without_layout')->willReturn($entity_without_layout->reveal());
 
       $entity_with_layout = $this->prophesize(FieldableEntityInterface::class);
-      $entity_with_layout->hasField('layout_builder__layout')->willReturn(TRUE);
-      $entity_with_layout->get('layout_builder__layout')->willReturn('the_return_value');
+      $entity_with_layout->hasField(OverridesSectionStorage::FIELD_NAME)->willReturn(TRUE);
+      $entity_with_layout->get(OverridesSectionStorage::FIELD_NAME)->willReturn('the_return_value');
       $entity_storage->load('entity_with_layout')->willReturn($entity_with_layout->reveal());
 
       $this->entityTypeManager->getStorage($expected_entity_type_id)->willReturn($entity_storage->reveal());
index c21821077bcb23446f34ecaacccb236e8b90360d..d68a0a451c35b35c6e83f49a79ba4ab639de33f1 100644 (file)
@@ -185,20 +185,11 @@ class Media extends EditorialContentEntityBase implements MediaInterface {
     // Set the thumbnail alt.
     $media_source = $this->getSource();
     $plugin_definition = $media_source->getPluginDefinition();
+
+    $this->thumbnail->alt = '';
     if (!empty($plugin_definition['thumbnail_alt_metadata_attribute'])) {
       $this->thumbnail->alt = $media_source->getMetadata($this, $plugin_definition['thumbnail_alt_metadata_attribute']);
     }
-    else {
-      $this->thumbnail->alt = $this->t('Thumbnail', [], ['langcode' => $this->langcode->value]);
-    }
-
-    // Set the thumbnail title.
-    if (!empty($plugin_definition['thumbnail_title_metadata_attribute'])) {
-      $this->thumbnail->title = $media_source->getMetadata($this, $plugin_definition['thumbnail_title_metadata_attribute']);
-    }
-    else {
-      $this->thumbnail->title = $this->label();
-    }
 
     return $this;
   }
index 0c210878feaa823f24a1c90fc1cc9f266ffa05fc..e58e7e26adae0494d5925b09d20f83dee4c4d29d 100644 (file)
@@ -7,7 +7,6 @@ use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Cache\UseCacheBackendTrait;
 use GuzzleHttp\ClientInterface;
 use GuzzleHttp\Exception\RequestException;
-use Symfony\Component\Serializer\Encoder\XmlEncoder;
 
 /**
  * Fetches and caches oEmbed resources.
@@ -69,8 +68,7 @@ class ResourceFetcher implements ResourceFetcherInterface {
     $content = (string) $response->getBody();
 
     if (strstr($format, 'text/xml') || strstr($format, 'application/xml')) {
-      $encoder = new XmlEncoder();
-      $data = $encoder->decode($content, 'xml');
+      $data = $this->parseResourceXml($content, $url);
     }
     elseif (strstr($format, 'text/javascript') || strstr($format, 'application/json')) {
       $data = Json::decode($content);
@@ -194,4 +192,42 @@ class ResourceFetcher implements ResourceFetcherInterface {
     }
   }
 
+  /**
+   * Parses XML resource data.
+   *
+   * @param string $data
+   *   The raw XML for the resource.
+   * @param string $url
+   *   The resource URL.
+   *
+   * @return array
+   *   The parsed resource data.
+   *
+   * @throws \Drupal\media\OEmbed\ResourceException
+   *   If the resource data could not be parsed.
+   */
+  protected function parseResourceXml($data, $url) {
+    // Enable userspace error handling.
+    $was_using_internal_errors = libxml_use_internal_errors(TRUE);
+    libxml_clear_errors();
+
+    $content = simplexml_load_string($data, 'SimpleXMLElement', LIBXML_NOCDATA);
+    // Restore the previous error handling behavior.
+    libxml_use_internal_errors($was_using_internal_errors);
+
+    $error = libxml_get_last_error();
+    if ($error) {
+      libxml_clear_errors();
+      throw new ResourceException($error->message, $url);
+    }
+    elseif ($content === FALSE) {
+      throw new ResourceException('The fetched resource could not be parsed.', $url);
+    }
+
+    // Convert XML to JSON so that the parsed resource has a consistent array
+    // structure, regardless of any XML attributes or quirks of the XML parser.
+    $data = Json::encode($content);
+    return Json::decode($data);
+  }
+
 }
index a83a5144d24b7416c27320330aac45730e25ceb4..34b565c05449af1df78e2b31e606816e6e32c43b 100644 (file)
@@ -22,7 +22,8 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
  *   label = @Translation("Image"),
  *   description = @Translation("Use local images for reusable media."),
  *   allowed_field_types = {"image"},
- *   default_thumbnail_filename = "no-thumbnail.png"
+ *   default_thumbnail_filename = "no-thumbnail.png",
+ *   thumbnail_alt_metadata_attribute = "thumbnail_alt_value"
  * )
  */
 class Image extends File {
@@ -138,6 +139,9 @@ class Image extends File {
 
       case 'thumbnail_uri':
         return $uri;
+
+      case 'thumbnail_alt_value':
+        return $media->get($this->configuration['source_field'])->alt ?: parent::getMetadata($media, $name);
     }
 
     return parent::getMetadata($media, $name);
index 696b5bf84eeb271c05f766ac07863523a2b79ea9..9c0b08bfcc8e3de0e5731afe879bc00426a27a43 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8" standalone="yes"?>
 <oembed>
   <type>video</type>
-  <version>1.0</version>
+  <version type="float">1.0</version>
   <title>Let's Not Get a Drink Sometime</title>
   <https/>
   <author_name>CollegeHumor</author_name>
index 85e4e5d7314470375e77a91e67d34281be9ff7b3..de352e001ac2019d94b777f8666e7978e27ad49e 100644 (file)
@@ -167,13 +167,13 @@ abstract class MediaResourceTestBase extends EntityResourceTestBase {
       ],
       'thumbnail' => [
         [
-          'alt' => 'Thumbnail',
+          'alt' => '',
           'width' => 180,
           'height' => 180,
           'target_id' => (int) $thumbnail->id(),
           'target_type' => 'file',
           'target_uuid' => $thumbnail->uuid(),
-          'title' => 'Llama',
+          'title' => NULL,
           'url' => $thumbnail->url(),
         ],
       ],
index faf58c1a39b6d6753b61bc1ce16ee0d7c309bc26..cb013805ad278c528bbaa01385f806ff9fccc4e1 100644 (file)
@@ -59,6 +59,8 @@ class MediaSourceImageTest extends MediaSourceTestBase {
 
     // Make sure the thumbnail is displayed from uploaded image.
     $assert_session->elementAttributeContains('css', '.image-style-thumbnail', 'src', 'example_1.jpeg');
+    // Ensure the thumbnail has the correct alt attribute.
+    $assert_session->elementAttributeContains('css', '.image-style-thumbnail', 'alt', 'Image Alt Text 1');
 
     // Load the media and check that all fields are properly populated.
     $media = Media::load(1);
index 79cb7b41462f262c6f9ba295db946f7e9841a81b..ecf17785047e07786aa270aa5c031b2f6b68bc92 100644 (file)
@@ -40,7 +40,6 @@ class MediaSourceTest extends MediaKernelTestBase {
           'value' => 'Snowball',
         ],
         'thumbnail_uri' => [
-          'title' => 'Thumbnail',
           'value' => 'public://TheSisko.png',
         ],
       ]);
@@ -230,7 +229,7 @@ class MediaSourceTest extends MediaKernelTestBase {
 
     // Save a media item and make sure thumbnail was added.
     \Drupal::state()->set('media_source_test_attributes', [
-      'thumbnail_uri' => ['title' => 'Thumbnail', 'value' => 'public://thumbnail1.jpg'],
+      'thumbnail_uri' => ['value' => 'public://thumbnail1.jpg'],
     ]);
     /** @var \Drupal\media\MediaInterface $media */
     $media = Media::create([
@@ -242,45 +241,46 @@ class MediaSourceTest extends MediaKernelTestBase {
     $this->assertSame('public://thumbnail1.jpg', $media_source->getMetadata($media, 'thumbnail_uri'), 'Value of the thumbnail metadata attribute is not correct.');
     $media->save();
     $this->assertSame('public://thumbnail1.jpg', $media->thumbnail->entity->getFileUri(), 'Thumbnail was not added to the media item.');
-    $this->assertSame('Mr. Jones', $media->thumbnail->title, 'Title text was not set on the thumbnail.');
-    $this->assertEquals('Thumbnail', $media->thumbnail->alt, 'Alt text was not set on the thumbnail.');
+    // We expect the title not to be present on the Thumbnail.
+    $this->assertEmpty($media->thumbnail->title);
+    $this->assertSame('', $media->thumbnail->alt);
 
     // Now change the metadata attribute and make sure that the thumbnail stays
     // the same.
     \Drupal::state()->set('media_source_test_attributes', [
-      'thumbnail_uri' => ['title' => 'Thumbnail', 'value' => 'public://thumbnail2.jpg'],
+      'thumbnail_uri' => ['value' => 'public://thumbnail2.jpg'],
     ]);
     $this->assertSame('public://thumbnail2.jpg', $media_source->getMetadata($media, 'thumbnail_uri'), 'Value of the thumbnail metadata attribute is not correct.');
     $media->save();
     $this->assertSame('public://thumbnail1.jpg', $media->thumbnail->entity->getFileUri(), 'Thumbnail was not preserved.');
-    $this->assertSame('Mr. Jones', $media->thumbnail->title, 'Title text was not set on the thumbnail.');
-    $this->assertEquals('Thumbnail', $media->thumbnail->alt, 'Alt text was not set on the thumbnail.');
+    $this->assertEmpty($media->thumbnail->title);
+    $this->assertSame('', $media->thumbnail->alt);
 
     // Remove the thumbnail and make sure that it is auto-updated on save.
     $media->thumbnail->target_id = NULL;
     $this->assertSame('public://thumbnail2.jpg', $media_source->getMetadata($media, 'thumbnail_uri'), 'Value of the thumbnail metadata attribute is not correct.');
     $media->save();
     $this->assertSame('public://thumbnail2.jpg', $media->thumbnail->entity->getFileUri(), 'New thumbnail was not added to the media item.');
-    $this->assertSame('Mr. Jones', $media->thumbnail->title, 'Title text was not set on the thumbnail.');
-    $this->assertEquals('Thumbnail', $media->thumbnail->alt, 'Alt text was not set on the thumbnail.');
+    $this->assertEmpty($media->thumbnail->title);
+    $this->assertSame('', $media->thumbnail->alt);
 
     // Change the metadata attribute again, change the source field value too
     // and make sure that the thumbnail updates.
     \Drupal::state()->set('media_source_test_attributes', [
-      'thumbnail_uri' => ['title' => 'Thumbnail', 'value' => 'public://thumbnail1.jpg'],
+      'thumbnail_uri' => ['value' => 'public://thumbnail1.jpg'],
     ]);
     $media->field_media_test->value = 'some_new_value';
     $this->assertSame('public://thumbnail1.jpg', $media_source->getMetadata($media, 'thumbnail_uri'), 'Value of the thumbnail metadata attribute is not correct.');
     $media->save();
     $this->assertSame('public://thumbnail1.jpg', $media->thumbnail->entity->getFileUri(), 'New thumbnail was not added to the media item.');
-    $this->assertSame('Mr. Jones', $media->thumbnail->title, 'Title text was not set on the thumbnail.');
-    $this->assertEquals('Thumbnail', $media->thumbnail->alt, 'Alt text was not set on the thumbnail.');
+    $this->assertEmpty($media->thumbnail->title);
+    $this->assertSame('', $media->thumbnail->alt);
 
     // Change the thumbnail metadata attribute and make sure that the thumbnail
     // is set correctly.
     \Drupal::state()->set('media_source_test_attributes', [
-      'thumbnail_uri' => ['title' => 'Should not be used', 'value' => 'public://thumbnail1.jpg'],
-      'alternative_thumbnail_uri' => ['title' => 'Should be used', 'value' => 'public://thumbnail2.jpg'],
+      'thumbnail_uri' => ['value' => 'public://thumbnail1.jpg'],
+      'alternative_thumbnail_uri' => ['value' => 'public://thumbnail2.jpg'],
     ]);
     \Drupal::state()->set('media_source_test_definition', ['thumbnail_uri_metadata_attribute' => 'alternative_thumbnail_uri']);
     $media = Media::create([
@@ -293,14 +293,14 @@ class MediaSourceTest extends MediaKernelTestBase {
     $this->assertSame('public://thumbnail2.jpg', $media_source->getMetadata($media, 'alternative_thumbnail_uri'), 'Value of the thumbnail metadata attribute is not correct.');
     $media->save();
     $this->assertSame('public://thumbnail2.jpg', $media->thumbnail->entity->getFileUri(), 'Correct metadata attribute was not used for the thumbnail.');
-    $this->assertSame('Mr. Jones', $media->thumbnail->title, 'Title text was not set on the thumbnail.');
-    $this->assertEquals('Thumbnail', $media->thumbnail->alt, 'Alt text was not set on the thumbnail.');
+    $this->assertEmpty($media->thumbnail->title);
+    $this->assertSame('', $media->thumbnail->alt);
 
     // Enable queued thumbnails and make sure that the entity gets the default
     // thumbnail initially.
     \Drupal::state()->set('media_source_test_definition', []);
     \Drupal::state()->set('media_source_test_attributes', [
-      'thumbnail_uri' => ['title' => 'Should not be used', 'value' => 'public://thumbnail1.jpg'],
+      'thumbnail_uri' => ['value' => 'public://thumbnail1.jpg'],
     ]);
     $this->testMediaType->setQueueThumbnailDownloadsStatus(TRUE)->save();
     $media = Media::create([
@@ -311,8 +311,8 @@ class MediaSourceTest extends MediaKernelTestBase {
     $this->assertSame('public://thumbnail1.jpg', $media->getSource()->getMetadata($media, 'thumbnail_uri'), 'Value of the metadata attribute is not correct.');
     $media->save();
     $this->assertSame('public://media-icons/generic/generic.png', $media->thumbnail->entity->getFileUri(), 'Default thumbnail was not set initially.');
-    $this->assertSame('Mr. Jones', $media->thumbnail->title, 'Title text was not set on the thumbnail.');
-    $this->assertEquals('Thumbnail', $media->thumbnail->alt, 'Alt text was not set on the thumbnail.');
+    $this->assertEmpty($media->thumbnail->title);
+    $this->assertSame('', $media->thumbnail->alt);
 
     // Process the queue item and make sure that the thumbnail was updated too.
     $queue_name = 'media_entity_thumbnail';
@@ -330,18 +330,15 @@ class MediaSourceTest extends MediaKernelTestBase {
 
     $media = Media::load($media->id());
     $this->assertSame('public://thumbnail1.jpg', $media->thumbnail->entity->getFileUri(), 'Thumbnail was not updated by the queue.');
-    $this->assertSame('Mr. Jones', $media->thumbnail->title, 'Title text was not set on the thumbnail.');
-    $this->assertSame('Thumbnail', $media->thumbnail->alt, 'Alt text was not set on the thumbnail.');
+    $this->assertEmpty($media->thumbnail->title);
+    $this->assertSame('', $media->thumbnail->alt);
 
-    // Set alt and title metadata attributes and make sure they are used for the
-    // thumbnail.
+    // Set the alt metadata attribute and make sure it's used for the thumbnail.
     \Drupal::state()->set('media_source_test_definition', [
       'thumbnail_alt_metadata_attribute' => 'alt',
-      'thumbnail_title_metadata_attribute' => 'title',
     ]);
     \Drupal::state()->set('media_source_test_attributes', [
-      'alt' => ['title' => 'Alt text', 'value' => 'This will be alt.'],
-      'title' => ['title' => 'Title text', 'value' => 'This will be title.'],
+      'alt' => ['value' => 'This will be alt.'],
     ]);
     $media = Media::create([
       'bundle' => $this->testMediaType->id(),
@@ -350,8 +347,8 @@ class MediaSourceTest extends MediaKernelTestBase {
     ]);
     $media->save();
     $this->assertSame('Boxer', $media->getName(), 'Correct name was not set on the media item.');
-    $this->assertSame('This will be title.', $media->thumbnail->title, 'Title text was not set on the thumbnail.');
-    $this->assertSame('This will be alt.', $media->thumbnail->alt, 'Alt text was not set on the thumbnail.');
+    $this->assertEmpty($media->thumbnail->title);
+    $this->assertSame('This will be alt.', $media->thumbnail->alt);
   }
 
   /**
index 98e8cdfcb03c6591fb8bccad6d812e63f3fca294..1888eb12110c01226e49e3af1d2c92379ef151b5 100644 (file)
@@ -198,7 +198,7 @@ class MenuLinkContent extends MenuLinkBase implements ContainerFactoryPluginInte
    *   The menu link ID.
    */
   protected function getUuid() {
-    $this->getDerivativeId();
+    return $this->getDerivativeId();
   }
 
   /**
diff --git a/web/core/modules/menu_link_content/tests/src/Unit/MenuLinkPluginTest.php b/web/core/modules/menu_link_content/tests/src/Unit/MenuLinkPluginTest.php
new file mode 100644 (file)
index 0000000..a6af580
--- /dev/null
@@ -0,0 +1,31 @@
+<?php
+
+namespace Drupal\Tests\menu_link_content\Unit;
+
+use Drupal\menu_link_content\Plugin\Menu\MenuLinkContent;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\menu_link_content\Plugin\Menu\MenuLinkContent
+ *
+ * @group Menu
+ */
+class MenuLinkPluginTest extends UnitTestCase {
+
+  /**
+   * @covers ::getUuid
+   */
+  public function testGetInstanceReflection() {
+    /** @var \Drupal\menu_link_content\Plugin\Menu\MenuLinkContent $menu_link_content_plugin */
+    $menu_link_content_plugin = $this->prophesize(MenuLinkContent::class);
+    $menu_link_content_plugin->getDerivativeId()->willReturn('test_id');
+    $menu_link_content_plugin = $menu_link_content_plugin->reveal();
+
+    $class = new \ReflectionClass(MenuLinkContent::class);
+    $instance_method = $class->getMethod('getUuid');
+    $instance_method->setAccessible(TRUE);
+
+    $this->assertEquals('test_id', $instance_method->invoke($menu_link_content_plugin));
+  }
+
+}
index dde8a9f0a0b18fb6adf4adc7d5e45e0aa6b4b094..6d364e77fc565600dc6ed4a6d864cae19206efa8 100644 (file)
@@ -208,7 +208,14 @@ class MigrationPluginManager extends DefaultPluginManager implements MigrationPl
         $migration->set('requirements', $required_dependency_graph[$migration_id]['paths']);
       }
     }
-    array_multisort($weights, SORT_DESC, SORT_NUMERIC, $migrations);
+    // Sort weights, labels, and keys in the same order as each other.
+    array_multisort(
+      // Use the numerical weight as the primary sort.
+      $weights, SORT_DESC, SORT_NUMERIC,
+      // When migrations have the same weight, sort them alphabetically by ID.
+      array_keys($migrations), SORT_ASC, SORT_NATURAL,
+      $migrations
+    );
 
     return $migrations;
   }
index 9448ca2fff269dd7badaf11db7e9d91fad26d775..c9b75a9d7c23a74936da02349046bc732adae2b3 100644 (file)
@@ -82,7 +82,8 @@ use Drupal\migrate\Row;
  * @endcode
  *
  * If the source value was '2004-12-19T10:19:42-0600' the transformed value
- * would be 2004-12-19T10:19:42.
+ * would be 2004-12-19T10:19:42. Set validate_format to false if your source
+ * value is '0000-00-00 00:00:00'.
  *
  * @see \DateTime::createFromFormat()
  * @see \Drupal\Component\Datetime\DateTimePlus::__construct()
@@ -99,7 +100,7 @@ class FormatDate extends ProcessPluginBase {
    * {@inheritdoc}
    */
   public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
-    if (empty($value)) {
+    if (empty($value) && $value !== '0' && $value !== 0) {
       return '';
     }
 
index 3dd7144b8fc59a6c6412004806526bd0a0636794..8267bac783541046ee7b7aa63cf3a32f1aee34e8 100644 (file)
@@ -353,7 +353,7 @@ abstract class SourcePluginBase extends PluginBase implements MigrateSourceInter
 
       $row_data = $this->getIterator()->current() + $this->configuration;
       $this->fetchNextRow();
-      $row = new Row($row_data, $this->migration->getSourcePlugin()->getIds(), $this->migration->getDestinationIds());
+      $row = new Row($row_data, $this->getIds());
 
       // Populate the source key for this row.
       $this->currentSourceIds = $row->getSourceIdValues();
index b9e8e731ab6bdc123e14ba6b0a0a7e8f9ce5e2e4..4bf846e854eb0dd250be2d5eb6c8a3141736c5f7 100644 (file)
@@ -108,6 +108,11 @@ abstract class SqlBase extends SourcePluginBase implements ContainerFactoryPlugi
   public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, StateInterface $state) {
     parent::__construct($configuration, $plugin_id, $plugin_definition, $migration);
     $this->state = $state;
+    // If we are using high water, but haven't yet set a high water mark, skip
+    // joining the map table, as we want to get all available records.
+    if ($this->getHighWaterProperty() && $this->getHighWater() === NULL) {
+      $this->configuration['ignore_map'] = TRUE;
+    }
   }
 
   /**
@@ -322,7 +327,9 @@ abstract class SqlBase extends SourcePluginBase implements ContainerFactoryPlugi
       if ($this->getHighWaterProperty()) {
         $high_water_field = $this->getHighWaterField();
         $high_water = $this->getHighWater();
-        if ($high_water) {
+        // We check against NULL because 0 is an acceptable value for the high
+        // water mark.
+        if ($high_water !== NULL) {
           $conditions->condition($high_water_field, $high_water, '>');
           $condition_added = TRUE;
         }
index faffb663585cd8337481ab76fdb339eca63a06b3..27f826603c464988e9299986bf9aade87c95f836 100644 (file)
@@ -136,6 +136,72 @@ class HighWaterTest extends MigrateTestBase {
     $this->assertNodeDoesNotExist('Item 3');
   }
 
+  /**
+   * Tests that the high water value can be 0.
+   */
+  public function testZeroHighwater() {
+    // Assert all of the nodes have been imported.
+    $this->assertNodeExists('Item 1');
+    $this->assertNodeExists('Item 2');
+    $this->assertNodeExists('Item 3');
+    $migration = $this->container->get('plugin.manager.migration')->CreateInstance('high_water_test', []);
+    $source = $migration->getSourcePlugin();
+    $source->rewind();
+    $count = 0;
+    while ($source->valid()) {
+      $count++;
+      $source->next();
+    }
+
+    // Expect no rows as everything is below the high water mark.
+    $this->assertSame(0, $count);
+
+    // Test resetting the high water mark to 0.
+    $this->container->get('keyvalue')->get('migrate:high_water')->set('high_water_test', 0);
+    $migration = $this->container->get('plugin.manager.migration')->CreateInstance('high_water_test', []);
+    $source = $migration->getSourcePlugin();
+    $source->rewind();
+    $count = 0;
+    while ($source->valid()) {
+      $count++;
+      $source->next();
+    }
+    $this->assertSame(3, $count);
+  }
+
+  /**
+   * Tests that deleting the high water value causes all rows to be reimported.
+   */
+  public function testNullHighwater() {
+    // Assert all of the nodes have been imported.
+    $this->assertNodeExists('Item 1');
+    $this->assertNodeExists('Item 2');
+    $this->assertNodeExists('Item 3');
+    $migration = $this->container->get('plugin.manager.migration')->CreateInstance('high_water_test', []);
+    $source = $migration->getSourcePlugin();
+    $source->rewind();
+    $count = 0;
+    while ($source->valid()) {
+      $count++;
+      $source->next();
+    }
+
+    // Expect no rows as everything is below the high water mark.
+    $this->assertSame(0, $count);
+
+    // Test resetting the high water mark.
+    $this->container->get('keyvalue')->get('migrate:high_water')->delete('high_water_test');
+    $migration = $this->container->get('plugin.manager.migration')->CreateInstance('high_water_test', []);
+    $source = $migration->getSourcePlugin();
+    $source->rewind();
+    $count = 0;
+    while ($source->valid()) {
+      $count++;
+      $source->next();
+    }
+    $this->assertSame(3, $count);
+  }
+
   /**
    * Tests high water property of SqlBase when rows marked for update.
    */
index 019ac42ab9e7529e88ca1bc1cad5051bbfa57574..7be474e014e3384d96c0c040739709d08d17599b 100644 (file)
@@ -45,6 +45,11 @@ class MigrateEmbeddedDataTest extends KernelTestBase {
     $results = [];
     /** @var \Drupal\migrate\Row $row */
     foreach ($source as $row) {
+      // The plugin should not mark any rows as stubs. We need to use
+      // assertSame() here because assertFalse() will pass falsy values (e.g.,
+      // empty arrays).
+      $this->assertSame(FALSE, $row->isStub());
+
       $data_row = $row->getSource();
       // The "data" row returned by getSource() also includes all source
       // configuration - we remove it so we see only the data itself.
index 994de3855fbd9a8ae3c598a0206c4a15113de4d0..d11e95e4f11540915058e827f68b17dc0abd7bfd 100644 (file)
@@ -175,6 +175,42 @@ class FormatDateTest extends MigrateProcessTestCase {
         // converted from Australia/Sydney to America/Managua timezone.
         'expected' => '2004-12-18 17:19:42 America/Managua',
       ],
+      'integer_0' => [
+        'configuration' => [
+          'from_format' => 'U',
+          'to_format' => 'Y-m-d',
+        ],
+        'value' => 0,
+        'expected' => '1970-01-01',
+      ],
+      'string_0' => [
+        'configuration' => [
+          'from_format' => 'U',
+          'to_format' => 'Y-m-d',
+        ],
+        'value' => '0',
+        'expected' => '1970-01-01',
+      ],
+      'zeros' => [
+        'configuration' => [
+          'from_format' => 'Y-m-d H:i:s',
+          'to_format' => 'Y-m-d H:i:s e',
+          'settings' => ['validate_format' => FALSE],
+        ],
+        'value' => '0000-00-00 00:00:00',
+        'expected' => '-0001-11-30 00:00:00 Australia/Sydney',
+      ],
+      'zeros_same_timezone' => [
+        'configuration' => [
+          'from_format' => 'Y-m-d H:i:s',
+          'to_format' => 'Y-m-d H:i:s',
+          'settings' => ['validate_format' => FALSE],
+          'from_timezone' => 'UTC',
+          'to_timezone' => 'UTC',
+        ],
+        'value' => '0000-00-00 00:00:00',
+        'expected' => '-0001-11-30 00:00:00',
+      ],
     ];
   }
 
index c8b44f894e46951134b318beaa135c513b981064..15acb0213fb60130ed275cd451df9042314f9385 100644 (file)
@@ -102,6 +102,7 @@ abstract class DrupalSqlBase extends SqlBase implements ContainerFactoryPluginIn
    * {@inheritdoc}
    */
   public function checkRequirements() {
+    parent::checkRequirements();
     if ($this->pluginDefinition['requirements_met'] === TRUE) {
       if (isset($this->pluginDefinition['source_module'])) {
         if ($this->moduleExists($this->pluginDefinition['source_module'])) {
@@ -114,7 +115,6 @@ abstract class DrupalSqlBase extends SqlBase implements ContainerFactoryPluginIn
         }
       }
     }
-    parent::checkRequirements();
   }
 
   /**
index 185a7b6a78d9446bab1dfc09866f012778241229..389a4df56b2082abf9a801ce184847a2c352ab6a 100644 (file)
@@ -10071,6 +10071,30 @@ $connection->insert('i18n_strings')
   'objectindex' => '0',
   'format' => '0',
 ))
+->values(array(
+  'lid' => '1692',
+  'objectid' => '14',
+  'type' => 'term',
+  'property' => 'name',
+  'objectindex' => '14',
+  'format' => '0',
+))
+->values(array(
+  'lid' => '1693',
+  'objectid' => '15',
+  'type' => 'term',
+  'property' => 'name',
+  'objectindex' => '15',
+  'format' => '0',
+))
+->values(array(
+  'lid' => '1694',
+  'objectid' => '14',
+  'type' => 'term',
+  'property' => 'description',
+  'objectindex' => '14',
+  'format' => '0',
+))
 ->execute();
 
 $connection->schema()->createTable('i18n_variable', array(
@@ -22642,6 +22666,27 @@ $connection->insert('locales_source')
   'source' => 'White',
   'version' => '1',
 ))
+->values(array(
+  'lid' => '1692',
+  'location' => 'term:14:name',
+  'textgroup' => 'taxonomy',
+  'source' => 'Talos IV',
+  'version' => '1',
+))
+->values(array(
+  'lid' => '1693',
+  'location' => 'term:15:name',
+  'textgroup' => 'taxonomy',
+  'source' => 'Vulcan',
+  'version' => '1',
+))
+->values(array(
+  'lid' => '1694',
+  'location' => 'term:14:description',
+  'textgroup' => 'taxonomy',
+  'source' => 'The home of Captain Christopher Pike.',
+  'version' => '1',
+))
 ->execute();
 
 $connection->schema()->createTable('locales_target', array(
@@ -27718,6 +27763,30 @@ $connection->insert('locales_target')
   'plural' => '0',
   'i18n_status' => '0',
 ))
+->values(array(
+  'lid' => '1672',
+  'translation' => 'fr - Type',
+  'language' => 'fr',
+  'plid' => '0',
+  'plural' => '0',
+  'i18n_status' => '0',
+))
+->values(array(
+  'lid' => '1692',
+  'translation' => 'fr - Talos IV',
+  'language' => 'fr',
+  'plid' => '0',
+  'plural' => '0',
+  'i18n_status' => '0',
+))
+->values(array(
+  'lid' => '1694',
+  'translation' => 'fr - The home of Captain Christopher Pike.',
+  'language' => 'fr',
+  'plid' => '0',
+  'plural' => '0',
+  'i18n_status' => '0',
+))
 ->values(array(
   'lid' => '66',
   'translation' => 'zu - CCK - Aucune Intégration aux Vues',
@@ -27846,6 +27915,30 @@ $connection->insert('locales_target')
   'plural' => '0',
   'i18n_status' => '0',
 ))
+->values(array(
+  'lid' => '1672',
+  'translation' => 'zu - Type',
+  'language' => 'zu',
+  'plid' => '0',
+  'plural' => '0',
+  'i18n_status' => '0',
+))
+->values(array(
+  'lid' => '1693',
+  'translation' => 'zu - Vulcan',
+  'language' => 'zu',
+  'plid' => '0',
+  'plural' => '0',
+  'i18n_status' => '0',
+))
+->values(array(
+  'lid' => '1694',
+  'translation' => 'zu - The home of Captain Christopher Pike.',
+  'language' => 'zu',
+  'plid' => '0',
+  'plural' => '0',
+  'i18n_status' => '0',
+))
 ->execute();
 
 $connection->schema()->createTable('menu_custom', array(
@@ -40934,6 +41027,28 @@ $connection->insert('menu_router')
   'weight' => '0',
   'file' => 'sites/all/modules/i18n/i18n.admin.inc',
 ))
+->values(array(
+  'path' => 'admin/settings/language/i18n/variables',
+  'load_functions' => '',
+  'to_arg_functions' => '',
+  'access_callback' => 'user_access',
+  'access_arguments' => 'a:1:{i:0;s:29:"administer site configuration";}',
+  'page_callback' => 'drupal_get_form',
+  'page_arguments' => 'a:1:{i:0;s:25:"i18n_admin_variables_form";}',
+  'fit' => '31',
+  'number_parts' => '5',
+  'tab_parent' => 'admin/settings/language/i18n',
+  'tab_root' => 'admin/settings/language',
+  'title' => 'Variables',
+  'title_callback' => 't',
+  'title_arguments' => '',
+  'type' => '128',
+  'block_callback' => '',
+  'description' => 'Multilingual variables.',
+  'position' => '',
+  'weight' => '0',
+  'file' => 'sites/all/modules/i18n/i18n.admin.inc',
+))
 ->values(array(
   'path' => 'admin/settings/language/overview',
   'load_functions' => '',
@@ -47091,6 +47206,69 @@ $connection->insert('term_data')
   'language' => '',
   'trid' => '0',
 ))
+->values(array(
+  'tid' => '9',
+  'vid' => '3',
+  'name' => 'fr - term 4 of vocabulary 3',
+  'description' => '',
+  'weight' => '0',
+  'language' => 'fr',
+  'trid' => '1',
+))
+->values(array(
+  'tid' => '10',
+  'vid' => '3',
+  'name' => 'zu - term 4 of vocabulary 3',
+  'description' => '',
+  'weight' => '0',
+  'language' => 'zu',
+  'trid' => '1',
+))
+->values(array(
+  'tid' => '11',
+  'vid' => '3',
+  'name' => 'term 7 of vocabulary 3',
+  'description' => '',
+  'weight' => '0',
+  'language' => 'en',
+  'trid' => '2',
+))
+->values(array(
+  'tid' => '12',
+  'vid' => '3',
+  'name' => 'fr - term 7 of vocabulary 3',
+  'description' => '',
+  'weight' => '0',
+  'language' => 'fr',
+  'trid' => '2',
+))
+->values(array(
+  'tid' => '13',
+  'vid' => '3',
+  'name' => 'zu - term 7 of vocabulary 3',
+  'description' => '',
+  'weight' => '0',
+  'language' => 'zu',
+  'trid' => '2',
+))
+->values(array(
+  'tid' => '14',
+  'vid' => '5',
+  'name' => 'Talos IV',
+  'description' => 'The home of Captain Christopher Pike.',
+  'weight' => '0',
+  'language' => '',
+  'trid' => '0',
+))
+->values(array(
+  'tid' => '15',
+  'vid' => '5',
+  'name' => 'Vulcan',
+  'description' => '',
+  'weight' => '0',
+  'language' => '',
+  'trid' => '0',
+))
 ->execute();
 
 $connection->schema()->createTable('term_hierarchy', array(
@@ -47142,6 +47320,34 @@ $connection->insert('term_hierarchy')
   'tid' => '8',
   'parent' => '0',
 ))
+->values(array(
+  'tid' => '9',
+  'parent' => '0',
+))
+->values(array(
+  'tid' => '10',
+  'parent' => '0',
+))
+->values(array(
+  'tid' => '11',
+  'parent' => '0',
+))
+->values(array(
+  'tid' => '12',
+  'parent' => '0',
+))
+->values(array(
+  'tid' => '13',
+  'parent' => '0',
+))
+->values(array(
+  'tid' => '14',
+  'parent' => '0',
+))
+->values(array(
+  'tid' => '15',
+  'parent' => '0',
+))
 ->values(array(
   'tid' => '3',
   'parent' => '2',
@@ -48740,7 +48946,7 @@ $connection->insert('variable')
 ))
 ->values(array(
   'name' => 'i18ntaxonomy_vocabulary',
-  'value' => 'a:3:{i:1;s:1:"3";i:2;s:1:"2";i:3;s:1:"1";}',
+  'value' => 'a:4:{i:1;s:1:"3";i:2;s:1:"2";i:3;s:1:"3";i:5;s:1:"1";}',
 ))
 ->values(array(
   'name' => 'i18n_lock_node_article',
index 3f5242e3c8284c5a40ce6d22a41e492b682252fe..9be6c119eb82c9b371cb230ed18bf67eeaf3a11d 100644 (file)
@@ -11,286 +11,6 @@ use Drupal\Core\Database\Database;
 
 $connection = Database::getConnection();
 
-$connection->schema()->createTable('i18n_block_language', array(
-  'fields' => array(
-    'module' => array(
-      'type' => 'varchar',
-      'not null' => TRUE,
-      'length' => '64',
-    ),
-    'delta' => array(
-      'type' => 'varchar',
-      'not null' => TRUE,
-      'length' => '32',
-    ),
-    'language' => array(
-      'type' => 'varchar',
-      'not null' => TRUE,
-      'length' => '12',
-      'default' => '',
-    ),
-  ),
-  'primary key' => array(
-    'module',
-    'delta',
-    'language',
-  ),
-  'indexes' => array(
-    'language' => array(
-      'language',
-    ),
-  ),
-  'mysql_character_set' => 'utf8',
-));
-
-$connection->schema()->createTable('i18n_string', array(
-  'fields' => array(
-    'lid' => array(
-      'type' => 'int',
-      'not null' => TRUE,
-      'size' => 'normal',
-      'default' => '0',
-    ),
-    'textgroup' => array(
-      'type' => 'varchar',
-      'not null' => TRUE,
-      'length' => '50',
-      'default' => 'default',
-    ),
-    'context' => array(
-      'type' => 'varchar',
-      'not null' => TRUE,
-      'length' => '255',
-      'default' => '',
-    ),
-    'objectid' => array(
-      'type' => 'varchar',
-      'not null' => TRUE,
-      'length' => '255',
-      'default' => '',
-    ),
-    'type' => array(
-      'type' => 'varchar',
-      'not null' => TRUE,
-      'length' => '255',
-      'default' => '',
-    ),
-    'property' => array(
-      'type' => 'varchar',
-      'not null' => TRUE,
-      'length' => '255',
-      'default' => '',
-    ),
-    'objectindex' => array(
-      'type' => 'int',
-      'not null' => TRUE,
-      'size' => 'big',
-      'default' => '0',
-    ),
-    'format' => array(
-      'type' => 'varchar',
-      'not null' => FALSE,
-      'length' => '255',
-    ),
-  ),
-  'primary key' => array(
-    'lid',
-  ),
-  'indexes' => array(
-    'group_context' => array(
-      'textgroup',
-      array(
-        'context',
-        '50',
-      ),
-    ),
-  ),
-  'mysql_character_set' => 'utf8',
-));
-
-$connection->insert('i18n_string')
-->fields(array(
-  'lid',
-  'textgroup',
-  'context',
-  'objectid',
-  'type',
-  'property',
-  'objectindex',
-  'format',
-))
-->values(array(
-  'lid' => '57',
-  'textgroup' => 'blocks',
-  'context' => 'block:1:title',
-  'objectid' => '1',
-  'type' => 'block',
-  'property' => 'title',
-  'objectindex' => '1',
-  'format' => '',
-))
-->values(array(
-  'lid' => '60',
-  'textgroup' => 'blocks',
-  'context' => 'block:1:body',
-  'objectid' => '1',
-  'type' => 'block',
-  'property' => 'body',
-  'objectindex' => '1',
-  'format' => 'filtered_html',
-))
-->values(array(
-  'lid' => '61',
-  'textgroup' => 'node',
-  'context' => 'type:article:name',
-  'objectid' => 'article',
-  'type' => 'type',
-  'property' => 'name',
-  'objectindex' => '0',
-  'format' => '',
-))
-->values(array(
-  'lid' => '62',
-  'textgroup' => 'node',
-  'context' => 'type:article:title_label',
-  'objectid' => 'article',
-  'type' => 'type',
-  'property' => 'title_label',
-  'objectindex' => '0',
-  'format' => '',
-))
-->values(array(
-  'lid' => '63',
-  'textgroup' => 'node',
-  'context' => 'type:article:description',
-  'objectid' => 'article',
-  'type' => 'type',
-  'property' => 'description',
-  'objectindex' => '0',
-  'format' => '',
-))
-->values(array(
-  'lid' => '64',
-  'textgroup' => 'node',
-  'context' => 'type:article:help',
-  'objectid' => 'article',
-  'type' => 'type',
-  'property' => 'help',
-  'objectindex' => '0',
-  'format' => '',
-))
-->values(array(
-  'lid' => '65',
-  'textgroup' => 'node',
-  'context' => 'type:book:name',
-  'objectid' => 'book',
-  'type' => 'type',
-  'property' => 'name',
-  'objectindex' => '0',
-  'format' => '',
-))
-->values(array(
-  'lid' => '66',
-  'textgroup' => 'node',
-  'context' => 'type:book:title_label',
-  'objectid' => 'book',
-  'type' => 'type',
-  'property' => 'title_label',
-  'objectindex' => '0',
-  'format' => '',
-))
-->values(array(
-  'lid' => '67',
-  'textgroup' => 'node',
-  'context' => 'type:book:description',
-  'objectid' => 'book',
-  'type' => 'type',
-  'property' => 'description',
-  'objectindex' => '0',
-  'format' => '',
-))
-->values(array(
-  'lid' => '68',
-  'textgroup' => 'node',
-  'context' => 'type:page:name',
-  'objectid' => 'page',
-  'type' => 'type',
-  'property' => 'name',
-  'objectindex' => '0',
-  'format' => '',
-))
-->values(array(
-  'lid' => '69',
-  'textgroup' => 'node',
-  'context' => 'type:page:title_label',
-  'objectid' => 'page',
-  'type' => 'type',
-  'property' => 'title_label',
-  'objectindex' => '0',
-  'format' => '',
-))
-->values(array(
-  'lid' => '70',
-  'textgroup' => 'node',
-  'context' => 'type:page:description',
-  'objectid' => 'page',
-  'type' => 'type',
-  'property' => 'description',
-  'objectindex' => '0',
-  'format' => '',
-))
-->values(array(
-  'lid' => '71',
-  'textgroup' => 'node',
-  'context' => 'type:page:help',
-  'objectid' => 'page',
-  'type' => 'type',
-  'property' => 'help',
-  'objectindex' => '0',
-  'format' => '',
-))
-->values(array(
-  'lid' => '72',
-  'textgroup' => 'node',
-  'context' => 'type:test_content_type:name',
-  'objectid' => 'test_content_type',
-  'type' => 'type',
-  'property' => 'name',
-  'objectindex' => '0',
-  'format' => '',
-))
-->values(array(
-  'lid' => '73',
-  'textgroup' => 'node',
-  'context' => 'type:test_content_type:title_label',
-  'objectid' => 'test_content_type',
-  'type' => 'type',
-  'property' => 'title_label',
-  'objectindex' => '0',
-  'format' => '',
-))
-->values(array(
-  'lid' => '74',
-  'textgroup' => 'node',
-  'context' => 'type:test_content_type:description',
-  'objectid' => 'test_content_type',
-  'type' => 'type',
-  'property' => 'description',
-  'objectindex' => '0',
-  'format' => '',
-))
-->values(array(
-  'lid' => '75',
-  'textgroup' => 'node',
-  'context' => 'type:test_content_type:help',
-  'objectid' => 'test_content_type',
-  'type' => 'type',
-  'property' => 'help',
-  'objectindex' => '0',
-  'format' => '',
-))
-->execute();
-
 $connection->schema()->createTable('accesslog', array(
   'fields' => array(
     'aid' => array(
@@ -1316,6 +1036,12 @@ $connection->schema()->createTable('block', array(
       'size' => 'normal',
       'default' => '1',
     ),
+    'i18n_mode' => array(
+      'type' => 'int',
+      'not null' => TRUE,
+      'size' => 'normal',
+      'default' => '0',
+    ),
   ),
   'primary key' => array(
     'bid',
@@ -1337,6 +1063,7 @@ $connection->insert('block')
   'pages',
   'title',
   'cache',
+  'i18n_mode',
 ))
 ->values(array(
   'bid' => '1',
@@ -1351,6 +1078,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '-1',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '2',
@@ -1365,6 +1093,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '-1',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '3',
@@ -1379,6 +1108,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '-1',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '4',
@@ -1391,8 +1121,9 @@ $connection->insert('block')
   'custom' => '0',
   'visibility' => '0',
   'pages' => '',
-  'title' => '',
+  'title' => 'User login title',
   'cache' => '-1',
+  'i18n_mode' => '1',
 ))
 ->values(array(
   'bid' => '5',
@@ -1407,6 +1138,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '-1',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '6',
@@ -1421,6 +1153,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '-1',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '7',
@@ -1435,6 +1168,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '-1',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '8',
@@ -1449,6 +1183,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '-1',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '9',
@@ -1463,6 +1198,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '-1',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '10',
@@ -1475,8 +1211,9 @@ $connection->insert('block')
   'custom' => '0',
   'visibility' => '0',
   'pages' => '',
-  'title' => '',
+  'title' => 'User login title',
   'cache' => '-1',
+  'i18n_mode' => '1',
 ))
 ->values(array(
   'bid' => '11',
@@ -1491,6 +1228,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '-1',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '12',
@@ -1505,6 +1243,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '-1',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '13',
@@ -1519,6 +1258,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '1',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '14',
@@ -1533,6 +1273,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '-1',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '15',
@@ -1547,6 +1288,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '1',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '16',
@@ -1561,6 +1303,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '-1',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '17',
@@ -1575,6 +1318,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '-1',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '18',
@@ -1589,6 +1333,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '-1',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '19',
@@ -1603,6 +1348,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '-1',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '20',
@@ -1617,6 +1363,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '1',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '21',
@@ -1631,6 +1378,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '-1',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '22',
@@ -1645,6 +1393,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '1',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '23',
@@ -1659,6 +1408,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '-1',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '24',
@@ -1673,6 +1423,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '-1',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '25',
@@ -1687,6 +1438,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '-1',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '26',
@@ -1701,6 +1453,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '-1',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '27',
@@ -1715,6 +1468,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '-1',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '28',
@@ -1729,6 +1483,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '-1',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '29',
@@ -1743,6 +1498,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '-1',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '30',
@@ -1757,6 +1513,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '-1',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '31',
@@ -1771,6 +1528,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '1',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '32',
@@ -1785,6 +1543,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '5',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '33',
@@ -1799,6 +1558,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '-1',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '34',
@@ -1813,6 +1573,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '-2',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '35',
@@ -1827,6 +1588,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '-2',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '36',
@@ -1841,6 +1603,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '1',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '37',
@@ -1855,6 +1618,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '5',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '38',
@@ -1869,6 +1633,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '-1',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '39',
@@ -1883,6 +1648,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '-2',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '40',
@@ -1897,6 +1663,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '-2',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '41',
@@ -1911,6 +1678,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '-1',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '42',
@@ -1925,6 +1693,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '-1',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '43',
@@ -1939,6 +1708,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '-1',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '44',
@@ -1953,6 +1723,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '-1',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '45',
@@ -1967,6 +1738,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => 'Mildly amusing limerick of the day',
   'cache' => '-1',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '46',
@@ -1981,6 +1753,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => 'Mildly amusing limerick of the day',
   'cache' => '-1',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '47',
@@ -1995,6 +1768,7 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '1',
+  'i18n_mode' => '0',
 ))
 ->values(array(
   'bid' => '48',
@@ -2009,6 +1783,37 @@ $connection->insert('block')
   'pages' => '',
   'title' => '',
   'cache' => '1',
+  'i18n_mode' => '0',
+))
+->values(array(
+  'bid' => '49',
+  'module' => 'locale',
+  'delta' => 'language_content',
+  'theme' => 'bartik',
+  'status' => '0',
+  'weight' => '0',
+  'region' => '-1',
+  'custom' => '0',
+  'visibility' => '0',
+  'pages' => '',
+  'title' => '',
+  'cache' => '-1',
+  'i18n_mode' => '0',
+))
+->values(array(
+  'bid' => '50',
+  'module' => 'locale',
+  'delta' => 'language_content',
+  'theme' => 'seven',
+  'status' => '0',
+  'weight' => '0',
+  'region' => '-1',
+  'custom' => '0',
+  'visibility' => '0',
+  'pages' => '',
+  'title' => '',
+  'cache' => '-1',
+  'i18n_mode' => '0',
 ))
 ->execute();
 
@@ -2632,6 +2437,49 @@ $connection->schema()->createTable('cache_update', array(
   'mysql_character_set' => 'utf8',
 ));
 
+$connection->schema()->createTable('cache_variable', array(
+  'fields' => array(
+    'cid' => array(
+      'type' => 'varchar',
+      'not null' => TRUE,
+      'length' => '255',
+      'default' => '',
+    ),
+    'data' => array(
+      'type' => 'blob',
+      'not null' => FALSE,
+      'size' => 'big',
+    ),
+    'expire' => array(
+      'type' => 'int',
+      'not null' => TRUE,
+      'size' => 'normal',
+      'default' => '0',
+    ),
+    'created' => array(
+      'type' => 'int',
+      'not null' => TRUE,
+      'size' => 'normal',
+      'default' => '0',
+    ),
+    'serialized' => array(
+      'type' => 'int',
+      'not null' => TRUE,
+      'size' => 'small',
+      'default' => '0',
+    ),
+  ),
+  'primary key' => array(
+    'cid',
+  ),
+  'indexes' => array(
+    'expire' => array(
+      'expire',
+    ),
+  ),
+  'mysql_character_set' => 'utf8',
+));
+
 $connection->schema()->createTable('cache_views', array(
   'fields' => array(
     'cid' => array(
@@ -14702,6 +14550,296 @@ $connection->schema()->createTable('history', array(
   'mysql_character_set' => 'utf8',
 ));
 
+$connection->schema()->createTable('i18n_block_language', array(
+  'fields' => array(
+    'module' => array(
+      'type' => 'varchar',
+      'not null' => TRUE,
+      'length' => '64',
+    ),
+    'delta' => array(
+      'type' => 'varchar',
+      'not null' => TRUE,
+      'length' => '32',
+    ),
+    'language' => array(
+      'type' => 'varchar',
+      'not null' => TRUE,
+      'length' => '12',
+      'default' => '',
+    ),
+  ),
+  'primary key' => array(
+    'module',
+    'delta',
+    'language',
+  ),
+  'indexes' => array(
+    'language' => array(
+      'language',
+    ),
+  ),
+  'mysql_character_set' => 'utf8',
+));
+
+$connection->schema()->createTable('i18n_string', array(
+  'fields' => array(
+    'lid' => array(
+      'type' => 'int',
+      'not null' => TRUE,
+      'size' => 'normal',
+      'default' => '0',
+    ),
+    'textgroup' => array(
+      'type' => 'varchar',
+      'not null' => TRUE,
+      'length' => '50',
+      'default' => 'default',
+    ),
+    'context' => array(
+      'type' => 'varchar',
+      'not null' => TRUE,
+      'length' => '255',
+      'default' => '',
+    ),
+    'objectid' => array(
+      'type' => 'varchar',
+      'not null' => TRUE,
+      'length' => '255',
+      'default' => '',
+    ),
+    'type' => array(
+      'type' => 'varchar',
+      'not null' => TRUE,
+      'length' => '255',
+      'default' => '',
+    ),
+    'property' => array(
+      'type' => 'varchar',
+      'not null' => TRUE,
+      'length' => '255',
+      'default' => '',
+    ),
+    'objectindex' => array(
+      'type' => 'int',
+      'not null' => TRUE,
+      'size' => 'big',
+      'default' => '0',
+    ),
+    'format' => array(
+      'type' => 'varchar',
+      'not null' => FALSE,
+      'length' => '255',
+    ),
+  ),
+  'primary key' => array(
+    'lid',
+  ),
+  'indexes' => array(
+    'group_context' => array(
+      'textgroup',
+      array(
+        'context',
+        '50',
+      ),
+    ),
+  ),
+  'mysql_character_set' => 'utf8',
+));
+
+$connection->insert('i18n_string')
+->fields(array(
+  'lid',
+  'textgroup',
+  'context',
+  'objectid',
+  'type',
+  'property',
+  'objectindex',
+  'format',
+))
+->values(array(
+  'lid' => '57',
+  'textgroup' => 'blocks',
+  'context' => 'block:1:title',
+  'objectid' => '1',
+  'type' => 'block',
+  'property' => 'title',
+  'objectindex' => '1',
+  'format' => '',
+))
+->values(array(
+  'lid' => '60',
+  'textgroup' => 'blocks',
+  'context' => 'block:1:body',
+  'objectid' => '1',
+  'type' => 'block',
+  'property' => 'body',
+  'objectindex' => '1',
+  'format' => 'filtered_html',
+))
+->values(array(
+  'lid' => '61',
+  'textgroup' => 'node',
+  'context' => 'type:article:name',
+  'objectid' => 'article',
+  'type' => 'type',
+  'property' => 'name',
+  'objectindex' => '0',
+  'format' => '',
+))
+->values(array(
+  'lid' => '62',
+  'textgroup' => 'node',
+  'context' => 'type:article:title_label',
+  'objectid' => 'article',
+  'type' => 'type',
+  'property' => 'title_label',
+  'objectindex' => '0',
+  'format' => '',
+))
+->values(array(
+  'lid' => '63',
+  'textgroup' => 'node',
+  'context' => 'type:article:description',
+  'objectid' => 'article',
+  'type' => 'type',
+  'property' => 'description',
+  'objectindex' => '0',
+  'format' => '',
+))
+->values(array(
+  'lid' => '64',
+  'textgroup' => 'node',
+  'context' => 'type:article:help',
+  'objectid' => 'article',
+  'type' => 'type',
+  'property' => 'help',
+  'objectindex' => '0',
+  'format' => '',
+))
+->values(array(
+  'lid' => '65',
+  'textgroup' => 'node',
+  'context' => 'type:book:name',
+  'objectid' => 'book',
+  'type' => 'type',
+  'property' => 'name',
+  'objectindex' => '0',
+  'format' => '',
+))
+->values(array(
+  'lid' => '66',
+  'textgroup' => 'node',
+  'context' => 'type:book:title_label',
+  'objectid' => 'book',
+  'type' => 'type',
+  'property' => 'title_label',
+  'objectindex' => '0',
+  'format' => '',
+))
+->values(array(
+  'lid' => '67',
+  'textgroup' => 'node',
+  'context' => 'type:book:description',
+  'objectid' => 'book',
+  'type' => 'type',
+  'property' => 'description',
+  'objectindex' => '0',
+  'format' => '',
+))
+->values(array(
+  'lid' => '68',
+  'textgroup' => 'node',
+  'context' => 'type:page:name',
+  'objectid' => 'page',
+  'type' => 'type',
+  'property' => 'name',
+  'objectindex' => '0',
+  'format' => '',
+))
+->values(array(
+  'lid' => '69',
+  'textgroup' => 'node',
+  'context' => 'type:page:title_label',
+  'objectid' => 'page',
+  'type' => 'type',
+  'property' => 'title_label',
+  'objectindex' => '0',
+  'format' => '',
+))
+->values(array(
+  'lid' => '70',
+  'textgroup' => 'node',
+  'context' => 'type:page:description',
+  'objectid' => 'page',
+  'type' => 'type',
+  'property' => 'description',
+  'objectindex' => '0',
+  'format' => '',
+))
+->values(array(
+  'lid' => '71',
+  'textgroup' => 'node',
+  'context' => 'type:page:help',
+  'objectid' => 'page',
+  'type' => 'type',
+  'property' => 'help',
+  'objectindex' => '0',
+  'format' => '',
+))
+->values(array(
+  'lid' => '72',
+  'textgroup' => 'node',
+  'context' => 'type:test_content_type:name',
+  'objectid' => 'test_content_type',
+  'type' => 'type',
+  'property' => 'name',
+  'objectindex' => '0',
+  'format' => '',
+))
+->values(array(
+  'lid' => '73',
+  'textgroup' => 'node',
+  'context' => 'type:test_content_type:title_label',
+  'objectid' => 'test_content_type',
+  'type' => 'type',
+  'property' => 'title_label',
+  'objectindex' => '0',
+  'format' => '',
+))
+->values(array(
+  'lid' => '74',
+  'textgroup' => 'node',
+  'context' => 'type:test_content_type:description',
+  'objectid' => 'test_content_type',
+  'type' => 'type',
+  'property' => 'description',
+  'objectindex' => '0',
+  'format' => '',
+))
+->values(array(
+  'lid' => '75',
+  'textgroup' => 'node',
+  'context' => 'type:test_content_type:help',
+  'objectid' => 'test_content_type',
+  'type' => 'type',
+  'property' => 'help',
+  'objectindex' => '0',
+  'format' => '',
+))
+->values(array(
+  'lid' => '76',
+  'textgroup' => 'blocks',
+  'context' => 'user:login:title',
+  'objectid' => 'login',
+  'type' => 'user',
+  'property' => 'title',
+  'objectindex' => '0',
+  'format' => '',
+))
+->execute();
+
 $connection->schema()->createTable('image_effects', array(
   'fields' => array(
     'ieid' => array(
@@ -15010,6 +15148,15 @@ $connection->schema()->createTable('locales_source', array(
   'primary key' => array(
     'lid',
   ),
+  'indexes' => array(
+    'textgroup_context' => array(
+      'textgroup',
+      array(
+        'context',
+        '50',
+      ),
+    ),
+  ),
   'mysql_character_set' => 'utf8',
 ));
 
@@ -15622,6 +15769,14 @@ $connection->insert('locales_source')
   'context' => 'type:test_content_type:help',
   'version' => '1',
 ))
+->values(array(
+  'lid' => '76',
+  'location' => 'blocks:user:login:title',
+  'textgroup' => 'blocks',
+  'source' => 'User login title',
+  'context' => 'user:login:title',
+  'version' => '1',
+))
 ->execute();
 
 $connection->schema()->createTable('locales_target', array(
@@ -15695,7 +15850,7 @@ $connection->insert('locales_target')
   'plural' => '0',
   'i18n_status' => '0',
 ))
-->values(array(
+  ->values(array(
   'lid' => '57',
   'translation' => 'is - Mildly amusing limerick of the day',
   'language' => 'is',
@@ -15703,6 +15858,14 @@ $connection->insert('locales_target')
   'plural' => '0',
   'i18n_status' => '0',
 ))
+->values(array(
+  'lid' => '76',
+  'translation' => 'fr - User login title',
+  'language' => 'fr',
+  'plid' => '0',
+  'plural' => '0',
+  'i18n_status' => '0',
+))
 ->execute();
 
 $connection->schema()->createTable('menu_custom', array(
@@ -46256,6 +46419,16 @@ $connection->insert('role_permission')
   'permission' => 'switch shortcut sets',
   'module' => 'shortcut',
 ))
+->values(array(
+  'rid' => '3',
+  'permission' => 'translate admin strings',
+  'module' => 'i18n_string',
+))
+->values(array(
+  'rid' => '3',
+  'permission' => 'translate blocks',
+  'module' => 'i18n_block',
+))
 ->values(array(
   'rid' => '3',
   'permission' => 'translate content',
@@ -46266,6 +46439,11 @@ $connection->insert('role_permission')
   'permission' => 'translate interface',
   'module' => 'locale',
 ))
+->values(array(
+  'rid' => '3',
+  'permission' => 'translate user-defined strings',
+  'module' => 'i18n_string',
+))
 ->values(array(
   'rid' => '3',
   'permission' => 'use advanced search',
@@ -48917,11 +49095,11 @@ $connection->insert('system')
   'name' => 'i18n_block',
   'type' => 'module',
   'owner' => '',
-  'status' => '0',
+  'status' => '1',
   'bootstrap' => '0',
-  'schema_version' => '-1',
-  'weight' => '0',
-  'info' => 'a:12:{s:4:"name";s:15:"Block languages";s:11:"description";s:68:"Enables language selector for blocks and optional block translation.";s:12:"dependencies";a:2:{i:0;s:5:"block";i:1;s:11:"i18n_string";}s:7:"package";s:35:"Multilingual - Internationalization";s:4:"core";s:3:"7.x";s:5:"files";a:2:{i:0;s:14:"i18n_block.inc";i:1;s:15:"i18n_block.test";}s:7:"version";s:8:"7.x-1.26";s:7:"project";s:4:"i18n";s:9:"datestamp";s:10:"1534531985";s:5:"mtime";i:1534531985;s:3:"php";s:5:"5.2.4";s:9:"bootstrap";i:0;}',
+  'schema_version' => '7001',
+  'weight' => '100',
+  'info' => 'a:12:{s:4:"name";s:15:"Block languages";s:11:"description";s:68:"Enables language selector for blocks and optional block translation.";s:12:"dependencies";a:2:{i:0;s:5:"block";i:1;s:11:"i18n_string";}s:7:"package";s:35:"Multilingual - Internationalization";s:4:"core";s:3:"7.x";s:5:"files";a:2:{i:0;s:14:"i18n_block.inc";i:1;s:15:"i18n_block.test";}s:7:"version";s:8:"7.x-1.25";s:7:"project";s:4:"i18n";s:9:"datestamp";s:10:"1531342125";s:5:"mtime";i:1537747250;s:3:"php";s:5:"5.2.4";s:9:"bootstrap";i:0;}',
 ))
 ->values(array(
   'filename' => 'sites/all/modules/i18n/i18n_contact/i18n_contact.module',
index 33e0197e2f5412facc613852b1349132c0475baf..cf5d3da4f89f39d780a3f3dcda974dd8d4e9e1fb 100644 (file)
@@ -371,7 +371,7 @@ class ContentEntityTest extends KernelTestBase {
     $values = $media_source->current()->getSource();
     $this->assertEquals(1, $values['mid']);
     $this->assertEquals('Foo media', $values['name'][0]['value']);
-    $this->assertEquals('Foo media', $values['thumbnail'][0]['title']);
+    $this->assertNull($values['thumbnail'][0]['title']);
     $this->assertEquals(1, $values['uid'][0]['target_id']);
     $this->assertEquals('image', $values['bundle'][0]['target_id']);
   }
index ebd9334a2d4fe1feb062da020b7a3750246ff258..fdaf4b10af7ca99ea67c8be49abcb425e5725108 100644 (file)
@@ -63,6 +63,22 @@ class DrupalSqlBaseTest extends MigrateTestCase {
     }
   }
 
+  /**
+   * @covers ::checkRequirements
+   */
+  public function testSourceDatabaseError() {
+    $plugin_definition['requirements_met'] = TRUE;
+    $plugin_definition['source_module'] = 'module1';
+    /** @var \Drupal\Core\State\StateInterface $state */
+    $state = $this->getMock('Drupal\Core\State\StateInterface');
+    /** @var \Drupal\Core\Entity\EntityManagerInterface $entity_manager */
+    $entity_manager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface');
+    $plugin = new TestDrupalSqlBase([], 'test', $plugin_definition, $this->getMigration(), $state, $entity_manager);
+    $system_data = $plugin->getSystemData();
+    $this->setExpectedException(RequirementsException::class, 'No database connection configured for source plugin test');
+    $plugin->checkRequirements();
+  }
+
 }
 
 namespace Drupal\Tests\migrate_drupal\Unit\source;
index 54ea5747ad52e8c699c925c2dd7451b1599f73be..b37b45b638a97178c4315d86a450ad005c236be9 100644 (file)
@@ -84,7 +84,7 @@ class MigrateUpgrade6Test extends MigrateUpgradeExecuteTestBase {
       'shortcut_set' => 1,
       'action' => 23,
       'menu' => 8,
-      'taxonomy_term' => 8,
+      'taxonomy_term' => 15,
       'taxonomy_vocabulary' => 7,
       'tour' => 5,
       'user' => 7,
@@ -112,7 +112,7 @@ class MigrateUpgrade6Test extends MigrateUpgradeExecuteTestBase {
     $counts['file'] = 8;
     $counts['menu_link_content'] = 11;
     $counts['node'] = 19;
-    $counts['taxonomy_term'] = 9;
+    $counts['taxonomy_term'] = 16;
     $counts['user'] = 8;
     $counts['view'] = 16;
     return $counts;
index 66b24c8520861f41c59a8cfbd23bb90063121c0d..9e68d9dba15b84bfd2a93aed0eaefb92b496cec7 100644 (file)
@@ -65,6 +65,7 @@ class MigrateUpgrade7ReviewPageTest extends MigrateUpgradeReviewPageTestBase {
       'filter',
       'forum',
       'image',
+      'i18n_block',
       'language',
       'link',
       'list',
@@ -141,7 +142,6 @@ class MigrateUpgrade7ReviewPageTest extends MigrateUpgradeReviewPageTestBase {
       'entity_translation_i18n_menu',
       'entity_translation_upgrade',
       'i18n',
-      'i18n_block',
       'i18n_contact',
       'i18n_field',
       'i18n_forum',
index 43e3cd6acc32935142d61ea0109bd6f3b7ad8a99..913137757c2bddeaf66e43e58254242fa0ec61ac 100644 (file)
@@ -27,3 +27,10 @@ function node_post_update_configure_status_field_widget() {
     ])->save();
   }
 }
+
+/**
+ * Clear caches due to updated views data.
+ */
+function node_post_update_node_revision_views_data() {
+  // Empty post-update hook.
+}
index 2e095648c4d704801bd0f69967b977884970343e..35ef2978ea09eb55d02ea4380b959bd7b0ac028e 100644 (file)
@@ -216,6 +216,10 @@ class NodeViewsData extends EntityViewsData {
     $data['node_field_revision']['nid']['relationship']['base field'] = 'nid';
     $data['node_field_revision']['nid']['relationship']['title'] = $this->t('Content');
     $data['node_field_revision']['nid']['relationship']['label'] = $this->t('Get the actual content from a content revision.');
+    $data['node_field_revision']['nid']['relationship']['extra'][] = [
+      'field' => 'langcode',
+      'left_field' => 'langcode',
+    ];
 
     $data['node_field_revision']['vid'] = [
       'argument' => [
@@ -228,6 +232,12 @@ class NodeViewsData extends EntityViewsData {
         'base field' => 'vid',
         'title' => $this->t('Content'),
         'label' => $this->t('Get the actual content from a content revision.'),
+        'extra' => [
+          [
+            'field' => 'langcode',
+            'left_field' => 'langcode',
+          ],
+        ],
       ],
     ] + $data['node_field_revision']['vid'];
 
index 44f0b5e06031f8ec9cdc80b387117c6ed47c70e9..e64b98908898650c84548acda5bd148114965d1b 100644 (file)
@@ -40,7 +40,7 @@ class NodeType extends DrupalSqlBase {
    * {@inheritdoc}
    */
   public function fields() {
-    return [
+    $fields = [
       'type' => $this->t('Machine name of the node type.'),
       'name' => $this->t('Human name of the node type.'),
       'description' => $this->t('Description of the node type.'),
index 179e1401c1da775ccdeee2acad6b7054f181e02f..8c5d4cb4b2c769f3ebb31202106c11d4a4b097e0 100644 (file)
@@ -44,6 +44,13 @@ display:
           plugin_id: field
           entity_type: node
           entity_field: nid
+        langcode:
+          id: langcode
+          table: node_field_revision
+          field: langcode
+          plugin_id: field
+          entity_type: node
+          entity_field: langcode
       arguments:
         nid:
           id: nid
@@ -61,6 +68,21 @@ display:
           plugin_id: field
           entity_type: node
           entity_field: vid
+        langcode:
+          id: langcode
+          table: node_field_revision
+          field: langcode
+          relationship: none
+          group_type: group
+          admin_label: ''
+          order: DESC
+          exposed: false
+          expose:
+            label: ''
+          entity_type: node
+          entity_field: langcode
+          plugin_id: standard
+      display_extenders: {  }
     display_plugin: default
     display_title: Master
     id: default
index a2783a56d742997e03f87f0c66fc641141b965d0..3c9aac56f286ebf342779b62bd3c53e09d8c75f5 100644 (file)
@@ -44,6 +44,13 @@ display:
           plugin_id: field
           entity_type: node
           entity_field: nid
+        langcode:
+          id: langcode
+          table: node_field_revision
+          field: langcode
+          entity_type: node
+          entity_field: langcode
+          plugin_id: field
       arguments:
         nid:
           id: nid
@@ -52,6 +59,22 @@ display:
           plugin_id: node_nid
           entity_type: node
           entity_field: nid
+      display_extenders: {  }
+      sorts:
+        langcode:
+          id: langcode
+          table: node_field_revision
+          field: langcode
+          relationship: none
+          group_type: group
+          admin_label: ''
+          order: ASC
+          exposed: false
+          expose:
+            label: ''
+          entity_type: node
+          entity_field: langcode
+          plugin_id: standard
     display_plugin: default
     display_title: Master
     id: default
index e47bdd038b328123265d29007e4625ef59646b31..b21610c5fde7d9438ffe21ecfd95bf58550d891c 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace Drupal\Tests\node\Kernel\Views;
 
+use Drupal\language\Entity\ConfigurableLanguage;
 use Drupal\node\Entity\Node;
 use Drupal\node\Entity\NodeType;
 use Drupal\Tests\views\Kernel\ViewsKernelTestBase;
@@ -20,7 +21,12 @@ class RevisionRelationshipsTest extends ViewsKernelTestBase {
    *
    * @var array
    */
-  public static $modules = ['node' , 'node_test_views'];
+  public static $modules = [
+    'node',
+    'node_test_views',
+    'language',
+    'content_translation',
+  ];
 
   /**
    * {@inheritdoc}
@@ -33,6 +39,8 @@ class RevisionRelationshipsTest extends ViewsKernelTestBase {
     $this->installEntitySchema('user');
     $this->installEntitySchema('node');
 
+    ConfigurableLanguage::createFromLangcode('fr')->save();
+
     ViewTestData::createTestViews(get_class($this), ['node_test_views']);
   }
 
@@ -51,16 +59,22 @@ class RevisionRelationshipsTest extends ViewsKernelTestBase {
     $type->save();
     $node = Node::create(['type' => 'page', 'title' => 'test', 'uid' => 1]);
     $node->save();
+
+    // Add a translation.
+    $translation = $node->addTranslation('fr', $node->toArray());
+    $translation->save();
     // Create revision of the node.
     $node->setNewRevision(TRUE);
     $node->save();
+
     $column_map = [
       'vid' => 'vid',
       'node_field_data_node_field_revision_nid' => 'node_node_revision_nid',
       'nid_1' => 'nid_1',
+      'node_field_revision_langcode' => 'node_field_revision_langcode',
     ];
 
-    // Here should be two rows.
+    // Here should be two rows for each translation.
     $view_nid = Views::getView('test_node_revision_nid');
     $this->executeView($view_nid, [$node->id()]);
     $resultset_nid = [
@@ -68,17 +82,32 @@ class RevisionRelationshipsTest extends ViewsKernelTestBase {
         'vid' => '1',
         'node_node_revision_nid' => '1',
         'nid_1' => '1',
+        'node_field_revision_langcode' => 'fr',
+      ],
+      [
+        'vid' => '1',
+        'node_node_revision_nid' => '1',
+        'nid_1' => '1',
+        'node_field_revision_langcode' => 'en',
       ],
       [
         'vid' => '2',
         'node_revision_nid' => '1',
         'node_node_revision_nid' => '1',
         'nid_1' => '1',
+        'node_field_revision_langcode' => 'fr',
+      ],
+      [
+        'vid' => '2',
+        'node_revision_nid' => '1',
+        'node_node_revision_nid' => '1',
+        'nid_1' => '1',
+        'node_field_revision_langcode' => 'en',
       ],
     ];
     $this->assertIdenticalResultset($view_nid, $resultset_nid, $column_map);
 
-    // There should be only one row with active revision 2.
+    // There should be one row with active revision 2 for each translation.
     $view_vid = Views::getView('test_node_revision_vid');
     $this->executeView($view_vid, [$node->id()]);
     $resultset_vid = [
@@ -86,6 +115,13 @@ class RevisionRelationshipsTest extends ViewsKernelTestBase {
         'vid' => '2',
         'node_node_revision_nid' => '1',
         'nid_1' => '1',
+        'node_field_revision_langcode' => 'en',
+      ],
+      [
+        'vid' => '2',
+        'node_node_revision_nid' => '1',
+        'nid_1' => '1',
+        'node_field_revision_langcode' => 'fr',
       ],
     ];
     $this->assertIdenticalResultset($view_vid, $resultset_vid, $column_map);
index 41cc994412e2da6ca12a9a0ce161a3f4b0b03ddd..1985499dee5278a4db62b9921fb6a31b025bfb4c 100644 (file)
@@ -4,6 +4,7 @@ namespace Drupal\path\Plugin\Field\FieldType;
 
 use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Field\FieldItemList;
+use Drupal\Core\Language\LanguageInterface;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\TypedData\ComputedItemListTrait;
 
@@ -26,12 +27,18 @@ class PathFieldItemList extends FieldItemList {
 
     $entity = $this->getEntity();
     if (!$entity->isNew()) {
-      // @todo Support loading language neutral aliases in
-      //   https://www.drupal.org/node/2511968.
-      $alias = \Drupal::service('path.alias_storage')->load([
+      $conditions = [
         'source' => '/' . $entity->toUrl()->getInternalPath(),
         'langcode' => $this->getLangcode(),
-      ]);
+      ];
+      $alias = \Drupal::service('path.alias_storage')->load($conditions);
+      if ($alias === FALSE) {
+        // Fall back to non-specific language.
+        if ($this->getLangcode() !== LanguageInterface::LANGCODE_NOT_SPECIFIED) {
+          $conditions['langcode'] = LanguageInterface::LANGCODE_NOT_SPECIFIED;
+          $alias = \Drupal::service('path.alias_storage')->load($conditions);
+        }
+      }
 
       if ($alias) {
         $value = $alias;
index cd62bea662e6121be96bddaff95c7ae21a5c7c9b..fc263ce60afcdd573b6df721448c5507a619b776 100644 (file)
@@ -63,10 +63,15 @@ class PathItem extends FieldItemBase {
    * {@inheritdoc}
    */
   public function postSave($update) {
+    // If specified, rely on the langcode property for the language, so that the
+    // existing language of an alias can be kept. That could for example be
+    // unspecified even if the field/entity has a specific langcode.
+    $alias_langcode = ($this->langcode && $this->pid) ? $this->langcode : $this->getLangcode();
+
     if (!$update) {
       if ($this->alias) {
         $entity = $this->getEntity();
-        if ($path = \Drupal::service('path.alias_storage')->save('/' . $entity->urlInfo()->getInternalPath(), $this->alias, $this->getLangcode())) {
+        if ($path = \Drupal::service('path.alias_storage')->save('/' . $entity->urlInfo()->getInternalPath(), $this->alias, $alias_langcode)) {
           $this->pid = $path['pid'];
         }
       }
@@ -79,7 +84,7 @@ class PathItem extends FieldItemBase {
       // Only save a non-empty alias.
       elseif ($this->alias) {
         $entity = $this->getEntity();
-        \Drupal::service('path.alias_storage')->save('/' . $entity->urlInfo()->getInternalPath(), $this->alias, $this->getLangcode(), $this->pid);
+        \Drupal::service('path.alias_storage')->save('/' . $entity->urlInfo()->getInternalPath(), $this->alias, $alias_langcode, $this->pid);
       }
     }
   }
index e6771f76d2fab7d0e2875f407811a4af52f27aa3..be6350f75b24866cb8fcd01b44ec80a6772c77dd 100644 (file)
@@ -48,8 +48,14 @@ class PathAliasConstraintValidator extends ConstraintValidator implements Contai
     if ($entity && !$entity->isNew() && !$entity->isDefaultRevision()) {
       /** @var \Drupal\Core\Entity\ContentEntityInterface $original */
       $original = $this->entityTypeManager->getStorage($entity->getEntityTypeId())->loadUnchanged($entity->id());
-      if ($value->alias != $original->path->alias) {
-        $this->context->addViolation($constraint->message);
+      $entity_langcode = $entity->language()->getId();
+
+      // Only add the violation if the current translation does not have the
+      // same path alias.
+      if ($original->hasTranslation($entity_langcode)) {
+        if ($value->alias != $original->getTranslation($entity_langcode)->path->alias) {
+          $this->context->addViolation($constraint->message);
+        }
       }
     }
   }
index 953f1116bafdf208afc85a9c9eef46d5b4c6b703..b69a5dbe5b5dd95a32efb9e58356c08274ba8214 100644 (file)
@@ -2,7 +2,7 @@
 
 namespace Drupal\Tests\path\Functional;
 
-use Drupal\node\Entity\NodeType;
+use Drupal\language\Entity\ConfigurableLanguage;
 use Drupal\Tests\BrowserTestBase;
 use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
 
@@ -21,17 +21,26 @@ class PathContentModerationTest extends BrowserTestBase {
    *
    * @var array
    */
-  public static $modules = ['node', 'path', 'content_moderation'];
+  public static $modules = [
+    'node',
+    'path',
+    'content_moderation',
+    'content_translation',
+  ];
 
   /**
    * {@inheritdoc}
    */
   protected function setUp() {
     parent::setUp();
+    ConfigurableLanguage::createFromLangcode('fr')->save();
+    $this->rebuildContainer();
 
     // Created a content type.
-    $node_type = NodeType::create(['name' => 'moderated', 'type' => 'moderated']);
-    $node_type->save();
+    $this->drupalCreateContentType([
+      'name' => 'moderated',
+      'type' => 'moderated',
+    ]);
 
     // Set the content type as moderated.
     $workflow = $this->createEditorialWorkflow();
@@ -39,6 +48,21 @@ class PathContentModerationTest extends BrowserTestBase {
     $workflow->save();
 
     $this->drupalLogin($this->rootUser);
+
+    // Enable URL language detection and selection.
+    $edit = ['language_interface[enabled][language-url]' => 1];
+    $this->drupalPostForm('admin/config/regional/language/detection', $edit, 'Save settings');
+
+    // Enable translation for moderated node.
+    $edit = [
+      'entity_types[node]' => 1,
+      'settings[node][moderated][translatable]' => 1,
+      'settings[node][moderated][fields][path]' => 1,
+      'settings[node][moderated][fields][body]' => 1,
+      'settings[node][moderated][settings][language][language_alterable]' => 1,
+    ];
+    $this->drupalPostForm('admin/config/regional/content-language', $edit, 'Save configuration');
+    \Drupal::entityTypeManager()->clearCachedDefinitions();
   }
 
   /**
@@ -106,4 +130,104 @@ class PathContentModerationTest extends BrowserTestBase {
     $this->assertSession()->pageTextNotContains('You can only change the URL alias for the published version of this content.');
   }
 
+  /**
+   * Tests that translated and moderated node can get new draft revision.
+   */
+  public function testTranslatedModeratedNodeAlias() {
+    // Create one node with a random alias.
+    $default_node = $this->drupalCreateNode([
+      'type' => 'moderated',
+      'langcode' => 'en',
+      'moderation_state' => 'published',
+      'path' => '/' . $this->randomMachineName(),
+    ]);
+
+    // Add published translation with another alias.
+    $this->drupalGet('node/' . $default_node->id());
+    $this->drupalGet('node/' . $default_node->id() . '/translations');
+    $this->clickLink('Add');
+    $edit_translation = [
+      'body[0][value]' => $this->randomMachineName(),
+      'moderation_state[0][state]' => 'published',
+      'path[0][alias]' => '/' . $this->randomMachineName(),
+    ];
+    $this->drupalPostForm(NULL, $edit_translation, 'Save (this translation)');
+    // Confirm that the alias works.
+    $this->drupalGet('fr' . $edit_translation['path[0][alias]']);
+    $this->assertSession()->pageTextContains($edit_translation['body[0][value]']);
+
+    $default_path = $default_node->path->alias;
+    $translation_path = 'fr' . $edit_translation['path[0][alias]'];
+
+    $this->assertPathsAreAccessible([$default_path, $translation_path]);
+
+    // Try to create new draft revision for translation with a new path alias.
+    $edit_new_translation_draft_with_alias = [
+      'moderation_state[0][state]' => 'draft',
+      'path[0][alias]' => '/' . $this->randomMachineName(),
+    ];
+    $this->drupalPostForm('fr/node/' . $default_node->id() . '/edit', $edit_new_translation_draft_with_alias, 'Save (this translation)');
+    // Confirm the expected error.
+    $this->assertSession()->pageTextContains('You can only change the URL alias for the published version of this content.');
+
+    // Create new draft revision for translation without changing path alias.
+    $edit_new_translation_draft = [
+      'body[0][value]' => $this->randomMachineName(),
+      'moderation_state[0][state]' => 'draft',
+    ];
+    $this->drupalPostForm('fr/node/' . $default_node->id() . '/edit', $edit_new_translation_draft, t('Save (this translation)'));
+    // Confirm that the new draft revision was created.
+    $this->assertSession()->pageTextNotContains('You can only change the URL alias for the published version of this content.');
+    $this->assertSession()->pageTextContains($edit_new_translation_draft['body[0][value]']);
+    $this->assertPathsAreAccessible([$default_path, $translation_path]);
+
+    // Try to create a new draft revision for translation with path alias from
+    // the original language's default revision.
+    $edit_new_translation_draft_with_defaults_alias = [
+      'moderation_state[0][state]' => 'draft',
+      'path[0][alias]' => $default_node->path->alias,
+    ];
+    $this->drupalPostForm('fr/node/' . $default_node->id() . '/edit', $edit_new_translation_draft_with_defaults_alias, 'Save (this translation)');
+    // Verify the expected error.
+    $this->assertSession()->pageTextContains('You can only change the URL alias for the published version of this content.');
+
+    // Try to create new draft revision for translation with deleted (empty)
+    // path alias.
+    $edit_new_translation_draft_empty_alias = [
+      'body[0][value]' => $this->randomMachineName(),
+      'moderation_state[0][state]' => 'draft',
+      'path[0][alias]' => '',
+    ];
+    $this->drupalPostForm('fr/node/' . $default_node->id() . '/edit', $edit_new_translation_draft_empty_alias, 'Save (this translation)');
+    // Confirm the expected error.
+    $this->assertSession()->pageTextContains('You can only change the URL alias for the published version of this content.');
+
+    // Create new default (published) revision for translation with new path
+    // alias.
+    $edit_new_translation = [
+      'body[0][value]' => $this->randomMachineName(),
+      'moderation_state[0][state]' => 'published',
+      'path[0][alias]' => '/' . $this->randomMachineName(),
+    ];
+    $this->drupalPostForm('fr/node/' . $default_node->id() . '/edit', $edit_new_translation, 'Save (this translation)');
+    // Confirm that the new published revision was created.
+    $this->assertSession()->pageTextNotContains('You can only change the URL alias for the published version of this content.');
+    $this->assertSession()->pageTextContains($edit_new_translation['body[0][value]']);
+    $this->assertSession()->addressEquals('fr' . $edit_new_translation['path[0][alias]']);
+    $this->assertPathsAreAccessible([$default_path]);
+  }
+
+  /**
+   * Helper callback to verify paths are responding with status 200.
+   *
+   * @param string[] $paths
+   *   An array of paths to check for.
+   */
+  public function assertPathsAreAccessible(array $paths) {
+    foreach ($paths as $path) {
+      $this->drupalGet($path);
+      $this->assertSession()->statusCodeEquals(200);
+    }
+  }
+
 }
index c2e3276246fa1270448a7c88d10fd578824d7c40..c76284515b65e570c89f2255c1e8468b00d1f185 100644 (file)
@@ -2,6 +2,8 @@
 
 namespace Drupal\Tests\path\Functional;
 
+use Drupal\Core\Language\LanguageInterface;
+
 /**
  * Confirm that the Path module user interface works with languages.
  *
@@ -78,4 +80,36 @@ class PathLanguageUiTest extends PathTestBase {
     $this->assertText(t('Filter aliases'), 'Foreign URL alias works');
   }
 
+  /**
+   * Test that language unspecific aliases are shown and saved in the node form.
+   */
+  public function testNotSpecifiedNode() {
+    // Create test node.
+    $node = $this->drupalCreateNode();
+
+    // Create a language-unspecific alias in the admin UI, ensure that is
+    // displayed and the langcode is not changed when saving.
+    $edit = [
+      'source' => '/node/' . $node->id(),
+      'alias' => '/' . $this->getRandomGenerator()->word(8),
+      'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
+    ];
+    $this->drupalPostForm('admin/config/search/path/add', $edit, t('Save'));
+
+    $this->drupalGet($node->toUrl('edit-form'));
+    $this->assertSession()->fieldValueEquals('path[0][alias]', $edit['alias']);
+    $this->drupalPostForm(NULL, [], t('Save'));
+
+    $this->drupalGet('admin/config/search/path');
+    $this->assertSession()->pageTextContains('None');
+    $this->assertSession()->pageTextNotContains('English');
+
+    // Create another node, with no alias, to ensure non-language specific
+    // aliases are loaded correctly.
+    $node = $this->drupalCreateNode();
+    $this->drupalget($node->toUrl('edit-form'));
+    $this->drupalPostForm(NULL, [], t('Save'));
+    $this->assertSession()->pageTextNotContains(t('The alias is already in use.'));
+  }
+
 }
index 26a52629b2f7536bcfc66dd8ba73aef06c0566d0..f41ba253157afb6346195fdb5f12fae1da8b6578 100644 (file)
@@ -19,6 +19,16 @@ class ShortcutForm extends ContentEntityForm {
    */
   protected $entity;
 
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, FormStateInterface $form_state) {
+    $form = parent::form($form, $form_state);
+    $form['#attached']['library'][] = 'core/drupal.form';
+
+    return $form;
+  }
+
   /**
    * {@inheritdoc}
    */
index 0814d882d7101ed22a46130614e71bb4f3b35f0a..35fdd92cc19d1e4fb67ad894ca593cfcf7c37347 100644 (file)
@@ -855,6 +855,7 @@ function simpletest_phpunit_testcase_to_row($test_id, \SimpleXMLElement $testcas
 
   $attributes = $testcase->attributes();
 
+  $function = $attributes->class . '->' . $attributes->name . '()';
   $record = [
     'test_id' => $test_id,
     'test_class' => (string) $attributes->class,
@@ -862,9 +863,11 @@ function simpletest_phpunit_testcase_to_row($test_id, \SimpleXMLElement $testcas
     'message' => $message,
     // @todo: Check on the proper values for this.
     'message_group' => 'Other',
-    'function' => $attributes->class . '->' . $attributes->name . '()',
+    'function' => $function,
     'line' => $attributes->line ?: 0,
-    'file' => $attributes->file,
+    // There are situations when the file will not be present because a PHPUnit
+    // @requires has caused a test to be skipped.
+    'file' => $attributes->file ?: $function,
   ];
   return $record;
 }
diff --git a/web/core/modules/system/src/Tests/Ajax/ElementValidationTest.php b/web/core/modules/system/src/Tests/Ajax/ElementValidationTest.php
deleted file mode 100644 (file)
index 169a389..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-<?php
-
-namespace Drupal\system\Tests\Ajax;
-
-/**
- * Various tests of AJAX behavior.
- *
- * @group Ajax
- */
-class ElementValidationTest extends AjaxTestBase {
-
-  /**
-   * Tries to post an Ajax change to a form that has a validated element.
-   *
-   * The drivertext field is Ajax-enabled. An additional field is not, but
-   * is set to be a required field. In this test the required field is not
-   * filled in, and we want to see if the activation of the "drivertext"
-   * Ajax-enabled field fails due to the required field being empty.
-   */
-  public function testAjaxElementValidation() {
-    $edit = ['drivertext' => t('some dumb text')];
-
-    // Post with 'drivertext' as the triggering element.
-    $this->drupalPostAjaxForm('ajax_validation_test', $edit, 'drivertext');
-    // Look for a validation failure in the resultant JSON.
-    $this->assertNoText(t('Error message'), 'No error message in resultant JSON');
-    $this->assertText('ajax_forms_test_validation_form_callback invoked', 'The correct callback was invoked');
-
-    $this->drupalGet('ajax_validation_test');
-    $edit = ['drivernumber' => 12345];
-
-    // Post with 'drivernumber' as the triggering element.
-    $this->drupalPostAjaxForm('ajax_validation_test', $edit, 'drivernumber');
-    // Look for a validation failure in the resultant JSON.
-    $this->assertNoText(t('Error message'), 'No error message in resultant JSON');
-    $this->assertText('ajax_forms_test_validation_number_form_callback invoked', 'The correct callback was invoked');
-  }
-
-}
diff --git a/web/core/modules/system/src/Tests/Ajax/FormValuesTest.php b/web/core/modules/system/src/Tests/Ajax/FormValuesTest.php
deleted file mode 100644 (file)
index 358db89..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-<?php
-
-namespace Drupal\system\Tests\Ajax;
-
-use Drupal\Core\Ajax\DataCommand;
-
-/**
- * Tests that form values are properly delivered to AJAX callbacks.
- *
- * @group Ajax
- */
-class FormValuesTest extends AjaxTestBase {
-
-  protected function setUp() {
-    parent::setUp();
-
-    $this->drupalLogin($this->drupalCreateUser(['access content']));
-  }
-
-  /**
-   * Submits forms with select and checkbox elements via Ajax.
-   */
-  public function testSimpleAjaxFormValue() {
-    // Verify form values of a select element.
-    foreach (['red', 'green', 'blue'] as $item) {
-      $edit = [
-        'select' => $item,
-      ];
-      $commands = $this->drupalPostAjaxForm('ajax_forms_test_get_form', $edit, 'select');
-      $expected = new DataCommand('#ajax_selected_color', 'form_state_value_select', $item);
-      $this->assertCommand($commands, $expected->render(), 'Verification of AJAX form values from a selectbox issued with a correct value.');
-    }
-
-    // Verify form values of a checkbox element.
-    foreach ([FALSE, TRUE] as $item) {
-      $edit = [
-        'checkbox' => $item,
-      ];
-      $commands = $this->drupalPostAjaxForm('ajax_forms_test_get_form', $edit, 'checkbox');
-      $expected = new DataCommand('#ajax_checkbox_value', 'form_state_value_select', (int) $item);
-      $this->assertCommand($commands, $expected->render(), 'Verification of AJAX form values from a checkbox issued with a correct value.');
-    }
-
-    // Verify that AJAX elements with invalid callbacks return error code 500.
-    // Ensure the test error log is empty before these tests.
-    $this->assertNoErrorsLogged();
-    // We don't need to check for the X-Drupal-Ajax-Token header with these
-    // invalid requests.
-    $this->assertAjaxHeader = FALSE;
-    foreach (['null', 'empty', 'nonexistent'] as $key) {
-      $element_name = 'select_' . $key . '_callback';
-      $edit = [
-        $element_name => 'red',
-      ];
-      $commands = $this->drupalPostAjaxForm('ajax_forms_test_get_form', $edit, $element_name);
-      $this->assertResponse(500);
-    }
-    // Switch this back to the default.
-    $this->assertAjaxHeader = TRUE;
-    // The exceptions are expected. Do not interpret them as a test failure.
-    // Not using File API; a potential error must trigger a PHP warning.
-    unlink(\Drupal::root() . '/' . $this->siteDirectory . '/error.log');
-  }
-
-}
diff --git a/web/core/modules/system/src/Tests/Ajax/FrameworkTest.php b/web/core/modules/system/src/Tests/Ajax/FrameworkTest.php
deleted file mode 100644 (file)
index 507493b..0000000
+++ /dev/null
@@ -1,216 +0,0 @@
-<?php
-
-namespace Drupal\system\Tests\Ajax;
-
-use Drupal\Core\Ajax\AddCssCommand;
-use Drupal\Core\Ajax\AlertCommand;
-use Drupal\Core\Ajax\AppendCommand;
-use Drupal\Core\Ajax\HtmlCommand;
-use Drupal\Core\Ajax\PrependCommand;
-use Drupal\Core\Ajax\SettingsCommand;
-use Drupal\Core\Asset\AttachedAssets;
-
-/**
- * Performs tests on AJAX framework functions.
- *
- * @group Ajax
- */
-class FrameworkTest extends AjaxTestBase {
-
-  /**
-   * Verifies the Ajax rendering of a command in the settings.
-   */
-  public function testAJAXRender() {
-    // Verify that settings command is generated if JavaScript settings exist.
-    $commands = $this->drupalGetAjax('ajax-test/render');
-    $expected = new SettingsCommand(['ajax' => 'test'], TRUE);
-    $this->assertCommand($commands, $expected->render(), 'JavaScript settings command is present.');
-  }
-
-  /**
-   * Tests AjaxResponse::prepare() AJAX commands ordering.
-   */
-  public function testOrder() {
-    $expected_commands = [];
-
-    // Expected commands, in a very specific order.
-    $asset_resolver = \Drupal::service('asset.resolver');
-    $css_collection_renderer = \Drupal::service('asset.css.collection_renderer');
-    $js_collection_renderer = \Drupal::service('asset.js.collection_renderer');
-    $renderer = \Drupal::service('renderer');
-    $expected_commands[0] = new SettingsCommand(['ajax' => 'test'], TRUE);
-    $build['#attached']['library'][] = 'ajax_test/order-css-command';
-    $assets = AttachedAssets::createFromRenderArray($build);
-    $css_render_array = $css_collection_renderer->render($asset_resolver->getCssAssets($assets, FALSE));
-    $expected_commands[1] = new AddCssCommand($renderer->renderRoot($css_render_array));
-    $build['#attached']['library'][] = 'ajax_test/order-header-js-command';
-    $build['#attached']['library'][] = 'ajax_test/order-footer-js-command';
-    $assets = AttachedAssets::createFromRenderArray($build);
-    list($js_assets_header, $js_assets_footer) = $asset_resolver->getJsAssets($assets, FALSE);
-    $js_header_render_array = $js_collection_renderer->render($js_assets_header);
-    $js_footer_render_array = $js_collection_renderer->render($js_assets_footer);
-    $expected_commands[2] = new PrependCommand('head', $js_header_render_array);
-    $expected_commands[3] = new AppendCommand('body', $js_footer_render_array);
-    $expected_commands[4] = new HtmlCommand('body', 'Hello, world!');
-
-    // Load any page with at least one CSS file, at least one JavaScript file
-    // and at least one #ajax-powered element. The latter is an assumption of
-    // drupalPostAjaxForm(), the two former are assumptions of the Ajax
-    // renderer.
-    // @todo refactor AJAX Framework + tests to make less assumptions.
-    $this->drupalGet('ajax_forms_test_lazy_load_form');
-
-    // Verify AJAX command order — this should always be the order:
-    // 1. JavaScript settings
-    // 2. CSS files
-    // 3. JavaScript files in the header
-    // 4. JavaScript files in the footer
-    // 5. Any other AJAX commands, in whatever order they were added.
-    $commands = $this->drupalPostAjaxForm(NULL, [], NULL, 'ajax-test/order', [], [], NULL, []);
-    $this->assertCommand(array_slice($commands, 0, 1), $expected_commands[0]->render(), 'Settings command is first.');
-    $this->assertCommand(array_slice($commands, 1, 1), $expected_commands[1]->render(), 'CSS command is second (and CSS files are ordered correctly).');
-    $this->assertCommand(array_slice($commands, 2, 1), $expected_commands[2]->render(), 'Header JS command is third.');
-    $this->assertCommand(array_slice($commands, 3, 1), $expected_commands[3]->render(), 'Footer JS command is fourth.');
-    $this->assertCommand(array_slice($commands, 4, 1), $expected_commands[4]->render(), 'HTML command is fifth.');
-  }
-
-  /**
-   * Tests the behavior of an error alert command.
-   */
-  public function testAJAXRenderError() {
-    // Verify custom error message.
-    $edit = [
-      'message' => 'Custom error message.',
-    ];
-    $commands = $this->drupalGetAjax('ajax-test/render-error', ['query' => $edit]);
-    $expected = new AlertCommand($edit['message']);
-    $this->assertCommand($commands, $expected->render(), 'Custom error message is output.');
-  }
-
-  /**
-   * Tests that new JavaScript and CSS files are lazy-loaded on an AJAX request.
-   */
-  public function testLazyLoad() {
-    $asset_resolver = \Drupal::service('asset.resolver');
-    $css_collection_renderer = \Drupal::service('asset.css.collection_renderer');
-    $js_collection_renderer = \Drupal::service('asset.js.collection_renderer');
-    $renderer = \Drupal::service('renderer');
-
-    $expected = [
-      'setting_name' => 'ajax_forms_test_lazy_load_form_submit',
-      'setting_value' => 'executed',
-      'library_1' => 'system/admin',
-      'library_2' => 'system/drupal.system',
-    ];
-
-    // Get the base page.
-    $this->drupalGet('ajax_forms_test_lazy_load_form');
-    $original_settings = $this->getDrupalSettings();
-    $original_libraries = explode(',', $original_settings['ajaxPageState']['libraries']);
-
-    // Verify that the base page doesn't have the settings and files that are to
-    // be lazy loaded as part of the next requests.
-    $this->assertTrue(!isset($original_settings[$expected['setting_name']]), format_string('Page originally lacks the %setting, as expected.', ['%setting' => $expected['setting_name']]));
-    $this->assertTrue(!in_array($expected['library_1'], $original_libraries), format_string('Page originally lacks the %library library, as expected.', ['%library' => $expected['library_1']]));
-    $this->assertTrue(!in_array($expected['library_2'], $original_libraries), format_string('Page originally lacks the %library library, as expected.', ['%library' => $expected['library_2']]));
-
-    // Calculate the expected CSS and JS.
-    $assets = new AttachedAssets();
-    $assets->setLibraries([$expected['library_1']])
-      ->setAlreadyLoadedLibraries($original_libraries);
-    $css_render_array = $css_collection_renderer->render($asset_resolver->getCssAssets($assets, FALSE));
-    $expected_css_html = $renderer->renderRoot($css_render_array);
-
-    $assets->setLibraries([$expected['library_2']])
-      ->setAlreadyLoadedLibraries($original_libraries);
-    $js_assets = $asset_resolver->getJsAssets($assets, FALSE)[1];
-    unset($js_assets['drupalSettings']);
-    $js_render_array = $js_collection_renderer->render($js_assets);
-    $expected_js_html = $renderer->renderRoot($js_render_array);
-
-    // Submit the AJAX request without triggering files getting added.
-    $commands = $this->drupalPostAjaxForm(NULL, ['add_files' => FALSE], ['op' => t('Submit')]);
-    $new_settings = $this->getDrupalSettings();
-    $new_libraries = explode(',', $new_settings['ajaxPageState']['libraries']);
-
-    // Verify the setting was not added when not expected.
-    $this->assertTrue(!isset($new_settings[$expected['setting_name']]), format_string('Page still lacks the %setting, as expected.', ['%setting' => $expected['setting_name']]));
-    $this->assertTrue(!in_array($expected['library_1'], $new_libraries), format_string('Page still lacks the %library library, as expected.', ['%library' => $expected['library_1']]));
-    $this->assertTrue(!in_array($expected['library_2'], $new_libraries), format_string('Page still lacks the %library library, as expected.', ['%library' => $expected['library_2']]));
-    // Verify a settings command does not add CSS or scripts to drupalSettings
-    // and no command inserts the corresponding tags on the page.
-    $found_settings_command = FALSE;
-    $found_markup_command = FALSE;
-    foreach ($commands as $command) {
-      if ($command['command'] == 'settings' && (array_key_exists('css', $command['settings']['ajaxPageState']) || array_key_exists('js', $command['settings']['ajaxPageState']))) {
-        $found_settings_command = TRUE;
-      }
-      if (isset($command['data']) && ($command['data'] == $expected_js_html || $command['data'] == $expected_css_html)) {
-        $found_markup_command = TRUE;
-      }
-    }
-    $this->assertFalse($found_settings_command, format_string('Page state still lacks the %library_1 and %library_2 libraries, as expected.', ['%library_1' => $expected['library_1'], '%library_2' => $expected['library_2']]));
-    $this->assertFalse($found_markup_command, format_string('Page still lacks the %library_1 and %library_2 libraries, as expected.', ['%library_1' => $expected['library_1'], '%library_2' => $expected['library_2']]));
-
-    // Submit the AJAX request and trigger adding files.
-    $commands = $this->drupalPostAjaxForm(NULL, ['add_files' => TRUE], ['op' => t('Submit')]);
-    $new_settings = $this->getDrupalSettings();
-    $new_libraries = explode(',', $new_settings['ajaxPageState']['libraries']);
-
-    // Verify the expected setting was added, both to drupalSettings, and as
-    // the first AJAX command.
-    $this->assertIdentical($new_settings[$expected['setting_name']], $expected['setting_value'], format_string('Page now has the %setting.', ['%setting' => $expected['setting_name']]));
-    $expected_command = new SettingsCommand([$expected['setting_name'] => $expected['setting_value']], TRUE);
-    $this->assertCommand(array_slice($commands, 0, 1), $expected_command->render(), 'The settings command was first.');
-
-    // Verify the expected CSS file was added, both to drupalSettings, and as
-    // the second AJAX command for inclusion into the HTML.
-    $this->assertTrue(in_array($expected['library_1'], $new_libraries), format_string('Page state now has the %library library.', ['%library' => $expected['library_1']]));
-    $this->assertCommand(array_slice($commands, 1, 1), ['data' => $expected_css_html], format_string('Page now has the %library library.', ['%library' => $expected['library_1']]));
-
-    // Verify the expected JS file was added, both to drupalSettings, and as
-    // the third AJAX command for inclusion into the HTML. By testing for an
-    // exact HTML string containing the SCRIPT tag, we also ensure that
-    // unexpected JavaScript code, such as a jQuery.extend() that would
-    // potentially clobber rather than properly merge settings, didn't
-    // accidentally get added.
-    $this->assertTrue(in_array($expected['library_2'], $new_libraries), format_string('Page state now has the %library library.', ['%library' => $expected['library_2']]));
-    $this->assertCommand(array_slice($commands, 2, 1), ['data' => $expected_js_html], format_string('Page now has the %library library.', ['%library' => $expected['library_2']]));
-  }
-
-  /**
-   * Tests that drupalSettings.currentPath is not updated on AJAX requests.
-   */
-  public function testCurrentPathChange() {
-    $commands = $this->drupalPostAjaxForm('ajax_forms_test_lazy_load_form', ['add_files' => FALSE], ['op' => t('Submit')]);
-    foreach ($commands as $command) {
-      if ($command['command'] == 'settings') {
-        $this->assertFalse(isset($command['settings']['currentPath']), 'Value of drupalSettings.currentPath is not updated after an AJAX request.');
-      }
-    }
-  }
-
-  /**
-   * Tests that overridden CSS files are not added during lazy load.
-   */
-  public function testLazyLoadOverriddenCSS() {
-    // The test theme overrides js.module.css without an implementation,
-    // thereby removing it.
-    \Drupal::service('theme_handler')->install(['test_theme']);
-    $this->config('system.theme')
-      ->set('default', 'test_theme')
-      ->save();
-
-    // This gets the form, and emulates an Ajax submission on it, including
-    // adding markup to the HEAD and BODY for any lazy loaded JS/CSS files.
-    $this->drupalPostAjaxForm('ajax_forms_test_lazy_load_form', ['add_files' => TRUE], ['op' => t('Submit')]);
-
-    // Verify that the resulting HTML does not load the overridden CSS file.
-    // We add a "?" to the assertion, because drupalSettings may include
-    // information about the file; we only really care about whether it appears
-    // in a LINK or STYLE tag, for which Drupal always adds a query string for
-    // cache control.
-    $this->assertNoText('js.module.css?', 'Ajax lazy loading does not add overridden CSS files.');
-  }
-
-}
diff --git a/web/core/modules/system/src/Tests/Form/TriggeringElementTest.php b/web/core/modules/system/src/Tests/Form/TriggeringElementTest.php
deleted file mode 100644 (file)
index fb59cb5..0000000
+++ /dev/null
@@ -1,97 +0,0 @@
-<?php
-
-namespace Drupal\system\Tests\Form;
-
-use Drupal\simpletest\WebTestBase;
-
-/**
- * Tests that FAPI correctly determines the triggering element.
- *
- * @group Form
- */
-class TriggeringElementTest extends WebTestBase {
-
-  /**
-   * Modules to enable.
-   *
-   * @var array
-   */
-  public static $modules = ['form_test'];
-
-  /**
-   * Test the determination of the triggering element when no button
-   * information is included in the POST data, as is sometimes the case when
-   * the ENTER key is pressed in a textfield in Internet Explorer.
-   */
-  public function testNoButtonInfoInPost() {
-    $path = 'form-test/clicked-button';
-    $edit = [];
-    $form_html_id = 'form-test-clicked-button';
-
-    // Ensure submitting a form with no buttons results in no triggering element
-    // and the form submit handler not running.
-    $this->drupalPostForm($path, $edit, NULL, [], [], $form_html_id);
-    $this->assertText('There is no clicked button.', '$form_state->getTriggeringElement() set to NULL.');
-    $this->assertNoText('Submit handler for form_test_clicked_button executed.', 'Form submit handler did not execute.');
-
-    // Ensure submitting a form with one or more submit buttons results in the
-    // triggering element being set to the first one the user has access to. An
-    // argument with 'r' in it indicates a restricted (#access=FALSE) button.
-    $this->drupalPostForm($path . '/s', $edit, NULL, [], [], $form_html_id);
-    $this->assertText('The clicked button is button1.', '$form_state->getTriggeringElement() set to only button.');
-    $this->assertText('Submit handler for form_test_clicked_button executed.', 'Form submit handler executed.');
-
-    $this->drupalPostForm($path . '/s/s', $edit, NULL, [], [], $form_html_id);
-    $this->assertText('The clicked button is button1.', '$form_state->getTriggeringElement() set to first button.');
-    $this->assertText('Submit handler for form_test_clicked_button executed.', 'Form submit handler executed.');
-
-    $this->drupalPostForm($path . '/rs/s', $edit, NULL, [], [], $form_html_id);
-    $this->assertText('The clicked button is button2.', '$form_state->getTriggeringElement() set to first available button.');
-    $this->assertText('Submit handler for form_test_clicked_button executed.', 'Form submit handler executed.');
-
-    // Ensure submitting a form with buttons of different types results in the
-    // triggering element being set to the first button, regardless of type. For
-    // the FAPI 'button' type, this should result in the submit handler not
-    // executing. The types are 's'(ubmit), 'b'(utton), and 'i'(mage_button).
-    $this->drupalPostForm($path . '/s/b/i', $edit, NULL, [], [], $form_html_id);
-    $this->assertText('The clicked button is button1.', '$form_state->getTriggeringElement() set to first button.');
-    $this->assertText('Submit handler for form_test_clicked_button executed.', 'Form submit handler executed.');
-
-    $this->drupalPostForm($path . '/b/s/i', $edit, NULL, [], [], $form_html_id);
-    $this->assertText('The clicked button is button1.', '$form_state->getTriggeringElement() set to first button.');
-    $this->assertNoText('Submit handler for form_test_clicked_button executed.', 'Form submit handler did not execute.');
-
-    $this->drupalPostForm($path . '/i/s/b', $edit, NULL, [], [], $form_html_id);
-    $this->assertText('The clicked button is button1.', '$form_state->getTriggeringElement() set to first button.');
-    $this->assertText('Submit handler for form_test_clicked_button executed.', 'Form submit handler executed.');
-  }
-
-  /**
-   * Test that the triggering element does not get set to a button with
-   * #access=FALSE.
-   */
-  public function testAttemptAccessControlBypass() {
-    $path = 'form-test/clicked-button';
-    $form_html_id = 'form-test-clicked-button';
-
-    // Retrieve a form where 'button1' has #access=FALSE and 'button2' doesn't.
-    $this->drupalGet($path . '/rs/s');
-
-    // Submit the form with 'button1=button1' in the POST data, which someone
-    // trying to get around security safeguards could easily do. We have to do
-    // a little trickery here, to work around the safeguards in drupalPostForm(): by
-    // renaming the text field that is in the form to 'button1', we can get the
-    // data we want into \Drupal::request()->request.
-    $elements = $this->xpath('//form[@id="' . $form_html_id . '"]//input[@name="text"]');
-    $elements[0]['name'] = 'button1';
-    $this->drupalPostForm(NULL, ['button1' => 'button1'], NULL, [], [], $form_html_id);
-
-    // Ensure that the triggering element was not set to the restricted button.
-    // Do this with both a negative and positive assertion, because negative
-    // assertions alone can be brittle. See testNoButtonInfoInPost() for why the
-    // triggering element gets set to 'button2'.
-    $this->assertNoText('The clicked button is button1.', '$form_state->getTriggeringElement() not set to a restricted button.');
-    $this->assertText('The clicked button is button2.', '$form_state->getTriggeringElement() not set to a restricted button.');
-  }
-
-}
diff --git a/web/core/modules/system/src/Tests/Session/StackSessionHandlerIntegrationTest.php b/web/core/modules/system/src/Tests/Session/StackSessionHandlerIntegrationTest.php
deleted file mode 100644 (file)
index ac36868..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-<?php
-
-namespace Drupal\system\Tests\Session;
-
-use Drupal\simpletest\WebTestBase;
-
-/**
- * Tests the stacked session handler functionality.
- *
- * @group Session
- */
-class StackSessionHandlerIntegrationTest extends WebTestBase {
-
-  /**
-   * Modules to enable.
-   *
-   * @var array
-   */
-  public static $modules = ['session_test'];
-
-  /**
-   * Tests a request.
-   */
-  public function testRequest() {
-    $actual_trace = $this->drupalGetAjax('session-test/trace-handler');
-    $expect_trace = [
-      ['BEGIN', 'test_argument', 'open'],
-      ['BEGIN', NULL, 'open'],
-      ['END', NULL, 'open'],
-      ['END', 'test_argument', 'open'],
-      ['BEGIN', 'test_argument', 'read', $this->sessionId],
-      ['BEGIN', NULL, 'read', $this->sessionId],
-      ['END', NULL, 'read', $this->sessionId],
-      ['END', 'test_argument', 'read', $this->sessionId],
-      ['BEGIN', 'test_argument', 'write', $this->sessionId],
-      ['BEGIN', NULL, 'write', $this->sessionId],
-      ['END', NULL, 'write', $this->sessionId],
-      ['END', 'test_argument', 'write', $this->sessionId],
-      ['BEGIN', 'test_argument', 'close'],
-      ['BEGIN', NULL, 'close'],
-      ['END', NULL, 'close'],
-      ['END', 'test_argument', 'close'],
-    ];
-    $this->assertEqual($expect_trace, $actual_trace);
-  }
-
-}
index 71350305ffee40430a1dc791e911e20bbe1be09b..cf821515b2d724c9cdd5d7990ca7feb747321c04 100644 (file)
@@ -50,7 +50,7 @@ class Callbacks {
    */
   public function checkboxCallback($form, FormStateInterface $form_state) {
     $response = new AjaxResponse();
-    $response->addCommand(new HtmlCommand('#ajax_checkbox_value', (int) $form_state->getValue('checkbox')));
+    $response->addCommand(new HtmlCommand('#ajax_checkbox_value', $form_state->getValue('checkbox') ? 'checked' : 'unchecked'));
     $response->addCommand(new DataCommand('#ajax_checkbox_value', 'form_state_value_select', (int) $form_state->getValue('checkbox')));
     return $response;
   }
index 3cb8105315e157170666c156d40a2e291f55a88b..35e0b08e2020d0950ef02c406c43c36b63a6e8ab 100644 (file)
@@ -65,7 +65,7 @@ class AjaxFormsTestSimpleForm extends FormBase {
       $form['select_' . $key . '_callback'] = [
         '#type' => 'select',
         '#title' => $this->t('Test %key callbacks', ['%key' => $key]),
-        '#options' => ['red' => 'red'],
+        '#options' => ['red' => 'red', 'green' => 'green'],
         '#ajax' => ['callback' => $value],
       ];
     }
diff --git a/web/core/modules/system/tests/src/Functional/Ajax/FrameworkTest.php b/web/core/modules/system/tests/src/Functional/Ajax/FrameworkTest.php
new file mode 100644 (file)
index 0000000..cc7b782
--- /dev/null
@@ -0,0 +1,158 @@
+<?php
+
+namespace Drupal\Tests\system\Functional\Ajax;
+
+use Drupal\Component\Serialization\Json;
+use Drupal\Core\Ajax\AddCssCommand;
+use Drupal\Core\Ajax\AlertCommand;
+use Drupal\Core\Ajax\AppendCommand;
+use Drupal\Core\Ajax\HtmlCommand;
+use Drupal\Core\Ajax\PrependCommand;
+use Drupal\Core\Ajax\SettingsCommand;
+use Drupal\Core\Asset\AttachedAssets;
+use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Performs tests on AJAX framework functions.
+ *
+ * @group Ajax
+ */
+class FrameworkTest extends BrowserTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['node', 'ajax_test', 'ajax_forms_test'];
+
+  /**
+   * Verifies the Ajax rendering of a command in the settings.
+   */
+  public function testAJAXRender() {
+    // Verify that settings command is generated if JavaScript settings exist.
+    $commands = $this->drupalGetAjax('ajax-test/render');
+    $expected = new SettingsCommand(['ajax' => 'test'], TRUE);
+    $this->assertCommand($commands, $expected->render(), 'JavaScript settings command is present.');
+  }
+
+  /**
+   * Tests AjaxResponse::prepare() AJAX commands ordering.
+   */
+  public function testOrder() {
+    $expected_commands = [];
+
+    // Expected commands, in a very specific order.
+    $asset_resolver = \Drupal::service('asset.resolver');
+    $css_collection_renderer = \Drupal::service('asset.css.collection_renderer');
+    $js_collection_renderer = \Drupal::service('asset.js.collection_renderer');
+    $renderer = \Drupal::service('renderer');
+    $build['#attached']['library'][] = 'ajax_test/order-css-command';
+    $assets = AttachedAssets::createFromRenderArray($build);
+    $css_render_array = $css_collection_renderer->render($asset_resolver->getCssAssets($assets, FALSE));
+    $expected_commands[1] = new AddCssCommand($renderer->renderRoot($css_render_array));
+    $build['#attached']['library'][] = 'ajax_test/order-header-js-command';
+    $build['#attached']['library'][] = 'ajax_test/order-footer-js-command';
+    $assets = AttachedAssets::createFromRenderArray($build);
+    list($js_assets_header, $js_assets_footer) = $asset_resolver->getJsAssets($assets, FALSE);
+    $js_header_render_array = $js_collection_renderer->render($js_assets_header);
+    $js_footer_render_array = $js_collection_renderer->render($js_assets_footer);
+    $expected_commands[2] = new PrependCommand('head', $js_header_render_array);
+    $expected_commands[3] = new AppendCommand('body', $js_footer_render_array);
+    $expected_commands[4] = new HtmlCommand('body', 'Hello, world!');
+
+    // Load any page with at least one CSS file, at least one JavaScript file
+    // and at least one #ajax-powered element. The latter is an assumption of
+    // drupalPostAjaxForm(), the two former are assumptions of the Ajax
+    // renderer.
+    // @todo refactor AJAX Framework + tests to make less assumptions.
+    $this->drupalGet('ajax_forms_test_lazy_load_form');
+
+    // Verify AJAX command order — this should always be the order:
+    // 1. CSS files
+    // 2. JavaScript files in the header
+    // 3. JavaScript files in the footer
+    // 4. Any other AJAX commands, in whatever order they were added.
+    $commands = $this->drupalGetAjax('ajax-test/order');
+    $this->assertCommand(array_slice($commands, 0, 1), $expected_commands[1]->render());
+    $this->assertCommand(array_slice($commands, 1, 1), $expected_commands[2]->render());
+    $this->assertCommand(array_slice($commands, 2, 1), $expected_commands[3]->render());
+    $this->assertCommand(array_slice($commands, 3, 1), $expected_commands[4]->render());
+  }
+
+  /**
+   * Tests the behavior of an error alert command.
+   */
+  public function testAJAXRenderError() {
+    // Verify custom error message.
+    $edit = [
+      'message' => 'Custom error message.',
+    ];
+    $commands = $this->drupalGetAjax('ajax-test/render-error', ['query' => $edit]);
+    $expected = new AlertCommand($edit['message']);
+    $this->assertCommand($commands, $expected->render(), 'Custom error message is output.');
+  }
+
+  /**
+   * Asserts the array of Ajax commands contains the searched command.
+   *
+   * An AjaxResponse object stores an array of Ajax commands. This array
+   * sometimes includes commands automatically provided by the framework in
+   * addition to commands returned by a particular controller. During testing,
+   * we're usually interested that a particular command is present, and don't
+   * care whether other commands precede or follow the one we're interested in.
+   * Additionally, the command we're interested in may include additional data
+   * that we're not interested in. Therefore, this function simply asserts that
+   * one of the commands in $haystack contains all of the keys and values in
+   * $needle. Furthermore, if $needle contains a 'settings' key with an array
+   * value, we simply assert that all keys and values within that array are
+   * present in the command we're checking, and do not consider it a failure if
+   * the actual command contains additional settings that aren't part of
+   * $needle.
+   *
+   * @param $haystack
+   *   An array of rendered Ajax commands returned by the server.
+   * @param $needle
+   *   Array of info we're expecting in one of those commands.
+   */
+  protected function assertCommand($haystack, $needle) {
+    $found = FALSE;
+    foreach ($haystack as $command) {
+      // If the command has additional settings that we're not testing for, do
+      // not consider that a failure.
+      if (isset($command['settings']) && is_array($command['settings']) && isset($needle['settings']) && is_array($needle['settings'])) {
+        $command['settings'] = array_intersect_key($command['settings'], $needle['settings']);
+      }
+      // If the command has additional data that we're not testing for, do not
+      // consider that a failure. Also, == instead of ===, because we don't
+      // require the key/value pairs to be in any particular order
+      // (http://php.net/manual/language.operators.array.php).
+      if (array_intersect_key($command, $needle) == $needle) {
+        $found = TRUE;
+        break;
+      }
+    }
+    $this->assertTrue($found);
+  }
+
+  /**
+   * Requests a path or URL in drupal_ajax format and JSON-decodes the response.
+   *
+   * @param \Drupal\Core\Url|string $path
+   *   Drupal path or URL to request from.
+   * @param array $options
+   *   Array of URL options.
+   * @param array $headers
+   *   Array of headers.
+   *
+   * @return array
+   *   Decoded JSON.
+   */
+  protected function drupalGetAjax($path, array $options = [], array $headers = []) {
+    $headers[] = 'X-Requested-With: XMLHttpRequest';
+    if (!isset($options['query'][MainContentViewSubscriber::WRAPPER_FORMAT])) {
+      $options['query'][MainContentViewSubscriber::WRAPPER_FORMAT] = 'drupal_ajax';
+    }
+    return Json::decode($this->drupalGet($path, $options, $headers));
+  }
+
+}
similarity index 65%
rename from web/core/modules/system/src/Tests/Form/ElementsTableSelectTest.php
rename to web/core/modules/system/tests/src/Functional/Form/ElementsTableSelectTest.php
index 030526eef9fa42c2ecff07c9416fe28c9b7e641f..f377c0abf60ace5a0af955fc8e3e871415a92516 100644 (file)
@@ -1,17 +1,16 @@
 <?php
 
-namespace Drupal\system\Tests\Form;
+namespace Drupal\Tests\system\Functional\Form;
 
 use Drupal\Core\Form\FormState;
-use Drupal\simpletest\WebTestBase;
-use Drupal\Tests\system\Functional\Form\StubForm;
+use Drupal\Tests\BrowserTestBase;
 
 /**
  * Tests the tableselect form element for expected behavior.
  *
  * @group Form
  */
-class ElementsTableSelectTest extends WebTestBase {
+class ElementsTableSelectTest extends BrowserTestBase {
 
   /**
    * Modules to enable.
@@ -27,35 +26,14 @@ class ElementsTableSelectTest extends WebTestBase {
 
     $this->drupalGet('form_test/tableselect/multiple-true');
 
-    $this->assertNoText(t('Empty text.'), 'Empty text should not be displayed.');
+    $this->assertSession()->responseNotContains('Empty text.', 'Empty text should not be displayed.');
 
     // Test for the presence of the Select all rows tableheader.
-    $this->assertFieldByXPath('//th[@class="select-all"]', NULL, 'Presence of the "Select all" checkbox.');
+    $this->assertNotEmpty($this->xpath('//th[@class="select-all"]'), 'Presence of the "Select all" checkbox.');
 
     $rows = ['row1', 'row2', 'row3'];
     foreach ($rows as $row) {
-      $this->assertFieldByXPath('//input[@type="checkbox"]', $row, format_string('Checkbox for value @row.', ['@row' => $row]));
-    }
-  }
-
-  /**
-   * Test the presence of ajax functionality for all options.
-   */
-  public function testAjax() {
-    $rows = ['row1', 'row2', 'row3'];
-    // Test checkboxes (#multiple == TRUE).
-    foreach ($rows as $row) {
-      $element = 'tableselect[' . $row . ']';
-      $edit = [$element => TRUE];
-      $result = $this->drupalPostAjaxForm('form_test/tableselect/multiple-true', $edit, $element);
-      $this->assertFalse(empty($result), t('Ajax triggers on checkbox for @row.', ['@row' => $row]));
-    }
-    // Test radios (#multiple == FALSE).
-    $element = 'tableselect';
-    foreach ($rows as $row) {
-      $edit = [$element => $row];
-      $result = $this->drupalPostAjaxForm('form_test/tableselect/multiple-false', $edit, $element);
-      $this->assertFalse(empty($result), t('Ajax triggers on radio for @row.', ['@row' => $row]));
+      $this->assertNotEmpty($this->xpath('//input[@type="checkbox"]', [$row]), "Checkbox for the value $row.");
     }
   }
 
@@ -65,40 +43,39 @@ class ElementsTableSelectTest extends WebTestBase {
   public function testMultipleFalse() {
     $this->drupalGet('form_test/tableselect/multiple-false');
 
-    $this->assertNoText(t('Empty text.'), 'Empty text should not be displayed.');
+    $this->assertSession()->pageTextNotContains('Empty text.');
 
     // Test for the absence of the Select all rows tableheader.
-    $this->assertNoFieldByXPath('//th[@class="select-all"]', '', 'Absence of the "Select all" checkbox.');
+    $this->assertFalse($this->xpath('//th[@class="select-all"]'));
 
     $rows = ['row1', 'row2', 'row3'];
     foreach ($rows as $row) {
-      $this->assertFieldByXPath('//input[@type="radio"]', $row, format_string('Radio button for value @row.', ['@row' => $row]));
+      $this->assertNotEmpty($this->xpath('//input[@type="radio"]', [$row], "Radio button value: $row"));
     }
   }
 
   /**
    * Tests the display when #colspan is set.
    */
-  public function testTableselectColSpan() {
+  public function testTableSelectColSpan() {
     $this->drupalGet('form_test/tableselect/colspan');
 
-    $this->assertText(t('Three'), 'Presence of the third column');
-    $this->assertNoText(t('Four'), 'Absence of a fourth column');
+    $this->assertSession()->pageTextContains('Three', 'Presence of the third column');
+    $this->assertSession()->pageTextNotContains('Four', 'Absence of a fourth column');
 
     // There should be three labeled column headers and 1 for the input.
-    $table_head = $this->xpath('//thead');
-    $this->assertEqual(count($table_head[0]->tr->th), 4, 'There are four column headers');
+    $table_head = $this->xpath('//thead/tr/th');
+    $this->assertEquals(count($table_head), 4, 'There are four column headers');
 
-    $table_body = $this->xpath('//tbody');
     // The first two body rows should each have 5 table cells: One for the
     // radio, one cell in the first column, one cell in the second column,
     // and two cells in the third column which has colspan 2.
     for ($i = 0; $i <= 1; $i++) {
-      $this->assertEqual(count($table_body[0]->tr[$i]->td), 5, format_string('There are five cells in row @row.', ['@row' => $i]));
+      $this->assertEquals(count($this->xpath('//tbody/tr[' . ($i + 1) . ']/td')), 5, 'There are five cells in row ' . $i);
     }
     // The third row should have 3 cells, one for the radio, one spanning the
     // first and second column, and a third in column 3 (which has colspan 3).
-    $this->assertEqual(count($table_body[0]->tr[2]->td), 3, 'There are three cells in row 3.');
+    $this->assertEquals(count($this->xpath('//tbody/tr[3]/td')), 3, 'There are three cells in row 3.');
   }
 
   /**
@@ -106,7 +83,7 @@ class ElementsTableSelectTest extends WebTestBase {
    */
   public function testEmptyText() {
     $this->drupalGet('form_test/tableselect/empty-text');
-    $this->assertText(t('Empty text.'), 'Empty text should be displayed.');
+    $this->assertSession()->pageTextContains('Empty text.', 'Empty text should be displayed.');
   }
 
   /**
@@ -119,18 +96,19 @@ class ElementsTableSelectTest extends WebTestBase {
     $edit['tableselect[row1]'] = TRUE;
     $this->drupalPostForm('form_test/tableselect/multiple-true', $edit, 'Submit');
 
-    $this->assertText(t('Submitted: row1 = row1'), 'Checked checkbox row1');
-    $this->assertText(t('Submitted: row2 = 0'), 'Unchecked checkbox row2.');
-    $this->assertText(t('Submitted: row3 = 0'), 'Unchecked checkbox row3.');
+    $assert_session = $this->assertSession();
+    $assert_session->pageTextContains('Submitted: row1 = row1', 'Checked checkbox row1');
+    $assert_session->pageTextContains('Submitted: row2 = 0', 'Unchecked checkbox row2.');
+    $assert_session->pageTextContains('Submitted: row3 = 0', 'Unchecked checkbox row3.');
 
     // Test a submission with multiple checkboxes checked.
     $edit['tableselect[row1]'] = TRUE;
     $edit['tableselect[row3]'] = TRUE;
     $this->drupalPostForm('form_test/tableselect/multiple-true', $edit, 'Submit');
 
-    $this->assertText(t('Submitted: row1 = row1'), 'Checked checkbox row1.');
-    $this->assertText(t('Submitted: row2 = 0'), 'Unchecked checkbox row2.');
-    $this->assertText(t('Submitted: row3 = row3'), 'Checked checkbox row3.');
+    $assert_session->pageTextContains('Submitted: row1 = row1', 'Checked checkbox row1.');
+    $assert_session->pageTextContains('Submitted: row2 = 0', 'Unchecked checkbox row2.');
+    $assert_session->pageTextContains('Submitted: row3 = row3', 'Checked checkbox row3.');
 
   }
 
@@ -140,7 +118,7 @@ class ElementsTableSelectTest extends WebTestBase {
   public function testMultipleFalseSubmit() {
     $edit['tableselect'] = 'row1';
     $this->drupalPostForm('form_test/tableselect/multiple-false', $edit, 'Submit');
-    $this->assertText(t('Submitted: row1'), 'Selected radio button');
+    $this->assertSession()->pageTextContains('Submitted: row1', 'Selected radio button');
   }
 
   /**
@@ -149,18 +127,18 @@ class ElementsTableSelectTest extends WebTestBase {
   public function testAdvancedSelect() {
     // When #multiple = TRUE a Select all checkbox should be displayed by default.
     $this->drupalGet('form_test/tableselect/advanced-select/multiple-true-default');
-    $this->assertFieldByXPath('//th[@class="select-all"]', NULL, 'Display a "Select all" checkbox by default when #multiple is TRUE.');
+    $this->xpath('//th[@class="select-all"]');
 
     // When #js_select is set to FALSE, a "Select all" checkbox should not be displayed.
     $this->drupalGet('form_test/tableselect/advanced-select/multiple-true-no-advanced-select');
-    $this->assertNoFieldByXPath('//th[@class="select-all"]', NULL, 'Do not display a "Select all" checkbox when #js_select is FALSE.');
+    $this->assertFalse($this->xpath('//th[@class="select-all"]'));
 
     // A "Select all" checkbox never makes sense when #multiple = FALSE, regardless of the value of #js_select.
     $this->drupalGet('form_test/tableselect/advanced-select/multiple-false-default');
-    $this->assertNoFieldByXPath('//th[@class="select-all"]', NULL, 'Do not display a "Select all" checkbox when #multiple is FALSE.');
+    $this->assertFalse($this->xpath('//th[@class="select-all"]'));
 
     $this->drupalGet('form_test/tableselect/advanced-select/multiple-false-advanced-select');
-    $this->assertNoFieldByXPath('//th[@class="select-all"]', NULL, 'Do not display a "Select all" checkbox when #multiple is FALSE, even when #js_select is TRUE.');
+    $this->assertFalse($this->xpath('//th[@class="select-all"]'));
   }
 
   /**
diff --git a/web/core/modules/system/tests/src/Functional/Form/RebuildTest.php b/web/core/modules/system/tests/src/Functional/Form/RebuildTest.php
new file mode 100644 (file)
index 0000000..3e951e2
--- /dev/null
@@ -0,0 +1,61 @@
+<?php
+
+namespace Drupal\Tests\system\Functional\Form;
+
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Tests functionality of \Drupal\Core\Form\FormBuilderInterface::rebuildForm().
+ *
+ * @group Form
+ */
+class RebuildTest extends BrowserTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['node', 'form_test'];
+
+  /**
+   * A user for testing.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $webUser;
+
+  protected function setUp() {
+    parent::setUp();
+
+    $this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
+
+    $this->webUser = $this->drupalCreateUser(['access content']);
+    $this->drupalLogin($this->webUser);
+  }
+
+  /**
+   * Tests preservation of values.
+   */
+  public function testRebuildPreservesValues() {
+    $edit = [
+      'checkbox_1_default_off' => TRUE,
+      'checkbox_1_default_on' => FALSE,
+      'text_1' => 'foo',
+    ];
+    $this->drupalPostForm('form-test/form-rebuild-preserve-values', $edit, 'Add more');
+
+    $assert_session = $this->assertSession();
+
+    // Verify that initial elements retained their submitted values.
+    $assert_session->checkboxChecked('edit-checkbox-1-default-off');
+    $assert_session->checkboxNotChecked('edit-checkbox-1-default-on');
+    $assert_session->fieldValueEquals('edit-text-1', 'foo');
+
+    // Verify that newly added elements were initialized with their default values.
+    $assert_session->checkboxChecked('edit-checkbox-2-default-on');
+    $assert_session->checkboxNotChecked('edit-checkbox-2-default-off');
+    $assert_session->fieldValueEquals('edit-text-2', 'DEFAULT 2');
+  }
+
+}
similarity index 61%
rename from web/core/modules/system/src/Tests/Form/StorageTest.php
rename to web/core/modules/system/tests/src/Functional/Form/StorageTest.php
index a1f7fee3df5891bf0e5b376b1dafdf89fcdf5186..95969fde5a2c82af4a2e399b634fe882dbf1142e 100644 (file)
@@ -1,8 +1,10 @@
 <?php
 
-namespace Drupal\system\Tests\Form;
+namespace Drupal\Tests\system\Functional\Form;
 
-use Drupal\simpletest\WebTestBase;
+use Drupal\Core\Database\Database;
+use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
+use Drupal\Tests\BrowserTestBase;
 
 /**
  * Tests a multistep form using form storage and makes sure validation and
@@ -16,7 +18,7 @@ use Drupal\simpletest\WebTestBase;
  *
  * @group Form
  */
-class StorageTest extends WebTestBase {
+class StorageTest extends BrowserTestBase {
 
   /**
    * Modules to enable.
@@ -25,6 +27,9 @@ class StorageTest extends WebTestBase {
    */
   public static $modules = ['form_test', 'dblog'];
 
+  /**
+   * {@inheritdoc}
+   */
   protected function setUp() {
     parent::setUp();
 
@@ -36,25 +41,27 @@ class StorageTest extends WebTestBase {
    */
   public function testForm() {
     $this->drupalGet('form_test/form-storage');
-    $this->assertText('Form constructions: 1');
+
+    $assert_session = $this->assertSession();
+    $assert_session->pageTextContains('Form constructions: 1');
 
     $edit = ['title' => 'new', 'value' => 'value_is_set'];
 
     // Use form rebuilding triggered by a submit button.
     $this->drupalPostForm(NULL, $edit, 'Continue submit');
-    $this->assertText('Form constructions: 2');
-    $this->assertText('Form constructions: 3');
+    $assert_session->pageTextContains('Form constructions: 2');
+    $assert_session->pageTextContains('Form constructions: 3');
 
     // Reset the form to the values of the storage, using a form rebuild
     // triggered by button of type button.
     $this->drupalPostForm(NULL, ['title' => 'changed'], 'Reset');
-    $this->assertFieldByName('title', 'new', 'Values have been reset.');
+    $assert_session->fieldValueEquals('title', 'new');
     // After rebuilding, the form has been cached.
-    $this->assertText('Form constructions: 4');
+    $assert_session->pageTextContains('Form constructions: 4');
 
     $this->drupalPostForm(NULL, $edit, 'Save');
-    $this->assertText('Form constructions: 4');
-    $this->assertText('Title: new', 'The form storage has stored the values.');
+    $assert_session->pageTextContains('Form constructions: 4');
+    $assert_session->pageTextContains('Title: new', 'The form storage has stored the values.');
   }
 
   /**
@@ -62,26 +69,26 @@ class StorageTest extends WebTestBase {
    */
   public function testFormCached() {
     $this->drupalGet('form_test/form-storage', ['query' => ['cache' => 1]]);
-    $this->assertText('Form constructions: 1');
+    $this->assertSession()->pageTextContains('Form constructions: 1');
 
     $edit = ['title' => 'new', 'value' => 'value_is_set'];
 
     // Use form rebuilding triggered by a submit button.
     $this->drupalPostForm(NULL, $edit, 'Continue submit');
     // The first one is for the building of the form.
-    $this->assertText('Form constructions: 2');
+    $this->assertSession()->pageTextContains('Form constructions: 2');
     // The second one is for the rebuilding of the form.
-    $this->assertText('Form constructions: 3');
+    $this->assertSession()->pageTextContains('Form constructions: 3');
 
     // Reset the form to the values of the storage, using a form rebuild
     // triggered by button of type button.
     $this->drupalPostForm(NULL, ['title' => 'changed'], 'Reset');
-    $this->assertFieldByName('title', 'new', 'Values have been reset.');
-    $this->assertText('Form constructions: 4');
+    $this->assertSession()->fieldValueEquals('title', 'new');
+    $this->assertSession()->pageTextContains('Form constructions: 4');
 
     $this->drupalPostForm(NULL, $edit, 'Save');
-    $this->assertText('Form constructions: 4');
-    $this->assertText('Title: new', 'The form storage has stored the values.');
+    $this->assertSession()->pageTextContains('Form constructions: 4');
+    $this->assertSession()->pageTextContains('Title: new', 'The form storage has stored the values.');
   }
 
   /**
@@ -124,7 +131,7 @@ class StorageTest extends WebTestBase {
     // validation error. Post again and verify that the rebuilt form contains
     // the values of the updated form storage.
     $this->drupalPostForm(NULL, ['title' => 'foo', 'value' => 'bar'], 'Save');
-    $this->assertText("The thing has been changed.", 'The altered form storage value was updated in cache and taken over.');
+    $this->assertSession()->pageTextContains("The thing has been changed.", 'The altered form storage value was updated in cache and taken over.');
   }
 
   /**
@@ -135,27 +142,27 @@ class StorageTest extends WebTestBase {
     // Request the form with 'cache' query parameter to enable form caching.
     $this->drupalGet('form_test/form-storage', ['query' => ['cache' => 1, 'immutable' => 1]]);
     $buildIdFields = $this->xpath('//input[@name="form_build_id"]');
-    $this->assertEqual(count($buildIdFields), 1, 'One form build id field on the page');
-    $buildId = (string) $buildIdFields[0]['value'];
+    $this->assertEquals(count($buildIdFields), 1, 'One form build id field on the page');
+    $buildId = $buildIdFields[0]->getValue();
 
     // Trigger validation error by submitting an empty title.
     $edit = ['title' => ''];
     $this->drupalPostForm(NULL, $edit, 'Continue submit');
 
     // Verify that the build-id did change.
-    $this->assertNoFieldByName('form_build_id', $buildId, 'Build id changes when form validation fails');
+    $this->assertSession()->hiddenFieldValueNotEquals('form_build_id', $buildId);
 
     // Retrieve the new build-id.
     $buildIdFields = $this->xpath('//input[@name="form_build_id"]');
-    $this->assertEqual(count($buildIdFields), 1, 'One form build id field on the page');
-    $buildId = (string) $buildIdFields[0]['value'];
+    $this->assertEquals(count($buildIdFields), 1, 'One form build id field on the page');
+    $buildId = (string) $buildIdFields[0]->getValue();
 
     // Trigger validation error by again submitting an empty title.
     $edit = ['title' => ''];
     $this->drupalPostForm(NULL, $edit, 'Continue submit');
 
     // Verify that the build-id does not change the second time.
-    $this->assertFieldByName('form_build_id', $buildId, 'Build id remains the same when form validation fails subsequently');
+    $this->assertSession()->hiddenFieldValueEquals('form_build_id', $buildId);
   }
 
   /**
@@ -164,25 +171,28 @@ class StorageTest extends WebTestBase {
   public function testImmutableFormLegacyProtection() {
     $this->drupalGet('form_test/form-storage', ['query' => ['cache' => 1, 'immutable' => 1]]);
     $build_id_fields = $this->xpath('//input[@name="form_build_id"]');
-    $this->assertEqual(count($build_id_fields), 1, 'One form build id field on the page');
-    $build_id = (string) $build_id_fields[0]['value'];
+    $this->assertEquals(count($build_id_fields), 1, 'One form build id field on the page');
+    $build_id = $build_id_fields[0]->getValue();
 
     // Try to poison the form cache.
-    $original = $this->drupalGetAjax('form-test/form-storage-legacy/' . $build_id);
-    $this->assertEqual($original['form']['#build_id_old'], $build_id, 'Original build_id was recorded');
-    $this->assertNotEqual($original['form']['#build_id'], $build_id, 'New build_id was generated');
+    $response = $this->drupalGet('form-test/form-storage-legacy/' . $build_id, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']], ['X-Requested-With: XMLHttpRequest']);
+    $original = json_decode($response, TRUE);
+
+    $this->assertEquals($original['form']['#build_id_old'], $build_id, 'Original build_id was recorded');
+    $this->assertNotEquals($original['form']['#build_id'], $build_id, 'New build_id was generated');
 
     // Assert that a watchdog message was logged by
     // \Drupal::formBuilder()->setCache().
-    $status = (bool) db_query_range('SELECT 1 FROM {watchdog} WHERE message = :message', 0, 1, [':message' => 'Form build-id mismatch detected while attempting to store a form in the cache.']);
-    $this->assert($status, 'A watchdog message was logged by \Drupal::formBuilder()->setCache');
+    $status = (bool) Database::getConnection()->queryRange('SELECT 1 FROM {watchdog} WHERE message = :message', 0, 1, [':message' => 'Form build-id mismatch detected while attempting to store a form in the cache.']);
+    $this->assertTrue($status, 'A watchdog message was logged by \Drupal::formBuilder()->setCache');
 
     // Ensure that the form state was not poisoned by the preceding call.
-    $original = $this->drupalGetAjax('form-test/form-storage-legacy/' . $build_id);
-    $this->assertEqual($original['form']['#build_id_old'], $build_id, 'Original build_id was recorded');
-    $this->assertNotEqual($original['form']['#build_id'], $build_id, 'New build_id was generated');
-    $this->assert(empty($original['form']['#poisoned']), 'Original form structure was preserved');
-    $this->assert(empty($original['form_state']['poisoned']), 'Original form state was preserved');
+    $response = $this->drupalGet('form-test/form-storage-legacy/' . $build_id, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']], ['X-Requested-With: XMLHttpRequest']);
+    $original = json_decode($response, TRUE);
+    $this->assertEquals($original['form']['#build_id_old'], $build_id, 'Original build_id was recorded');
+    $this->assertNotEquals($original['form']['#build_id'], $build_id, 'New build_id was generated');
+    $this->assertTrue(empty($original['form']['#poisoned']), 'Original form structure was preserved');
+    $this->assertTrue(empty($original['form_state']['poisoned']), 'Original form state was preserved');
   }
 
 }
index 6f32539243f5042b57f66a9cdf1e6ecf77c3983a..ef59d0128d142f08dbeff9859654c697376787e0 100644 (file)
@@ -107,6 +107,32 @@ class MailTest extends BrowserTestBase {
     $this->assertEquals('Drépal this is a very long test sentence to te <simpletest@example.com>', Unicode::mimeHeaderDecode($sent_message['headers']['From']), 'From header is correctly encoded.');
     $this->assertFalse(isset($sent_message['headers']['Reply-to']), 'Message reply-to is not set if not specified.');
     $this->assertFalse(isset($sent_message['headers']['Errors-To']), 'Errors-to header must not be set, it is deprecated.');
+
+    // Test RFC-2822 rules are respected for 'display-name' component of
+    // 'From:' header. Specials characters are not allowed, so randomly add one
+    // of them to the site name and check the string is wrapped in quotes. Also
+    // hardcode some double-quotes and backslash to validate these are escaped
+    // properly too.
+    $specials = '()<>[]:;@\,."';
+    $site_name = 'Drupal' . $specials[rand(0, strlen($specials) - 1)] . ' "si\te"';
+    $this->config('system.site')->set('name', $site_name)->save();
+    // Send an email and check that the From-header contains the site name
+    // within double-quotes. Also make sure double-quotes and "\" are escaped.
+    \Drupal::service('plugin.manager.mail')->mail('simpletest', 'from_test', 'from_test@example.com', $language);
+    $captured_emails = \Drupal::state()->get('system.test_mail_collector');
+    $sent_message = end($captured_emails);
+    $escaped_site_name = str_replace(['\\', '"'], ['\\\\', '\\"'], $site_name);
+    $this->assertEquals('"' . $escaped_site_name . '" <simpletest@example.com>', $sent_message['headers']['From'], 'From header is correctly quoted.');
+
+    // Make sure display-name is not quoted nor escaped if part on an encoding.
+    $site_name = 'Drépal, "si\te"';
+    $this->config('system.site')->set('name', $site_name)->save();
+    // Send an email and check that the From-header contains the site name.
+    \Drupal::service('plugin.manager.mail')->mail('simpletest', 'from_test', 'from_test@example.com', $language);
+    $captured_emails = \Drupal::state()->get('system.test_mail_collector');
+    $sent_message = end($captured_emails);
+    $this->assertEquals('=?UTF-8?B?RHLDqXBhbCwgInNpXHRlIg==?= <simpletest@example.com>', $sent_message['headers']['From'], 'From header is correctly encoded.');
+    $this->assertEquals($site_name . ' <simpletest@example.com>', Unicode::mimeHeaderDecode($sent_message['headers']['From']), 'From header is correctly encoded.');
   }
 
   /**
similarity index 79%
rename from web/core/modules/system/src/Tests/Routing/RouterTest.php
rename to web/core/modules/system/tests/src/Functional/Routing/RouterTest.php
index 8d7c43e86a883a2b06167a3a9d27dacb615ce771..0cb10bd80f39e894a89337e84e9a6803334bd228 100644 (file)
@@ -1,11 +1,11 @@
 <?php
 
-namespace Drupal\system\Tests\Routing;
+namespace Drupal\Tests\system\Functional\Routing;
 
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
 use Drupal\Core\Language\LanguageInterface;
-use Drupal\simpletest\WebTestBase;
+use Drupal\Tests\BrowserTestBase;
 use Symfony\Component\Routing\Exception\RouteNotFoundException;
 use Drupal\Core\Url;
 
@@ -14,7 +14,7 @@ use Drupal\Core\Url;
  *
  * @group Routing
  */
-class RouterTest extends WebTestBase {
+class RouterTest extends BrowserTestBase {
 
   /**
    * Modules to enable.
@@ -34,69 +34,72 @@ class RouterTest extends WebTestBase {
     $this->drupalGet('router_test/test1');
     $this->assertRaw('test1', 'The correct string was returned because the route was successful.');
     // Check expected headers from FinishResponseSubscriber.
-    $headers = $this->drupalGetHeaders();
-    $this->assertEqual($headers['x-ua-compatible'], 'IE=edge');
-    $this->assertEqual($headers['content-language'], 'en');
-    $this->assertEqual($headers['x-content-type-options'], 'nosniff');
-    $this->assertEqual($headers['x-frame-options'], 'SAMEORIGIN');
+    $headers = $this->getSession()->getResponseHeaders();
+
+    $this->assertEquals($headers['X-UA-Compatible'], ['IE=edge']);
+    $this->assertEquals($headers['Content-language'], ['en']);
+    $this->assertEquals($headers['X-Content-Type-Options'], ['nosniff']);
+    $this->assertEquals($headers['X-Frame-Options'], ['SAMEORIGIN']);
 
     $this->drupalGet('router_test/test2');
     $this->assertRaw('test2', 'The correct string was returned because the route was successful.');
     // Check expected headers from FinishResponseSubscriber.
     $headers = $this->drupalGetHeaders();
-    $this->assertEqual($headers['x-drupal-cache-contexts'], implode(' ', $expected_cache_contexts));
-    $this->assertEqual($headers['x-drupal-cache-tags'], 'config:user.role.anonymous http_response rendered');
+    $this->assertEqual($headers['X-Drupal-Cache-Contexts'], [implode(' ', $expected_cache_contexts)]);
+    $this->assertEqual($headers['X-Drupal-Cache-Tags'], ['config:user.role.anonymous http_response rendered']);
     // Confirm that the page wrapping is being added, so we're not getting a
     // raw body returned.
     $this->assertRaw('</html>', 'Page markup was found.');
     // In some instances, the subrequest handling may get confused and render
     // a page inception style.  This test verifies that is not happening.
-    $this->assertNoPattern('#</body>.*</body>#s', 'There was no double-page effect from a misrendered subrequest.');
+    $this->assertSession()->responseNotMatches('#</body>.*</body>#s', 'There was no double-page effect from a misrendered subrequest.');
 
     // Confirm that route-level access check's cacheability is applied to the
     // X-Drupal-Cache-Contexts and X-Drupal-Cache-Tags headers.
     // 1. controller result: render array, globally cacheable route access.
     $this->drupalGet('router_test/test18');
     $headers = $this->drupalGetHeaders();
-    $this->assertEqual($headers['x-drupal-cache-contexts'], implode(' ', Cache::mergeContexts($renderer_required_cache_contexts, ['url'])));
-    $this->assertEqual($headers['x-drupal-cache-tags'], 'config:user.role.anonymous foo http_response rendered');
+    $this->assertEqual($headers['X-Drupal-Cache-Contexts'], [implode(' ', Cache::mergeContexts($renderer_required_cache_contexts, ['url']))]);
+    $this->assertEqual($headers['X-Drupal-Cache-Tags'], ['config:user.role.anonymous foo http_response rendered']);
     // 2. controller result: render array, per-role cacheable route access.
     $this->drupalGet('router_test/test19');
     $headers = $this->drupalGetHeaders();
-    $this->assertEqual($headers['x-drupal-cache-contexts'], implode(' ', Cache::mergeContexts($renderer_required_cache_contexts, ['url', 'user.roles'])));
-    $this->assertEqual($headers['x-drupal-cache-tags'], 'config:user.role.anonymous foo http_response rendered');
+    $this->assertEqual($headers['X-Drupal-Cache-Contexts'], [implode(' ', Cache::mergeContexts($renderer_required_cache_contexts, ['url', 'user.roles']))]);
+    $this->assertEqual($headers['X-Drupal-Cache-Tags'], ['config:user.role.anonymous foo http_response rendered']);
     // 3. controller result: Response object, globally cacheable route access.
     $this->drupalGet('router_test/test1');
     $headers = $this->drupalGetHeaders();
-    $this->assertFalse(isset($headers['x-drupal-cache-contexts']));
-    $this->assertFalse(isset($headers['x-drupal-cache-tags']));
+    $this->assertFalse(isset($headers['X-Drupal-Cache-Contexts']));
+    $this->assertFalse(isset($headers['X-Drupal-Cache-Tags']));
     // 4. controller result: Response object, per-role cacheable route access.
     $this->drupalGet('router_test/test20');
     $headers = $this->drupalGetHeaders();
-    $this->assertFalse(isset($headers['x-drupal-cache-contexts']));
-    $this->assertFalse(isset($headers['x-drupal-cache-tags']));
+    $this->assertFalse(isset($headers['X-Drupal-Cache-Contexts']));
+    $this->assertFalse(isset($headers['X-Drupal-Cache-Tags']));
     // 5. controller result: CacheableResponse object, globally cacheable route access.
     $this->drupalGet('router_test/test21');
     $headers = $this->drupalGetHeaders();
-    $this->assertEqual($headers['x-drupal-cache-contexts'], '');
-    $this->assertEqual($headers['x-drupal-cache-tags'], 'http_response');
+    $this->assertEqual($headers['X-Drupal-Cache-Contexts'], ['']);
+    $this->assertEqual($headers['X-Drupal-Cache-Tags'], ['http_response']);
     // 6. controller result: CacheableResponse object, per-role cacheable route access.
     $this->drupalGet('router_test/test22');
     $headers = $this->drupalGetHeaders();
-    $this->assertEqual($headers['x-drupal-cache-contexts'], 'user.roles');
-    $this->assertEqual($headers['x-drupal-cache-tags'], 'http_response');
+    $this->assertEqual($headers['X-Drupal-Cache-Contexts'], ['user.roles']);
+    $this->assertEqual($headers['X-Drupal-Cache-Tags'], ['http_response']);
 
     // Finally, verify that the X-Drupal-Cache-Contexts and X-Drupal-Cache-Tags
     // headers are not sent when their container parameter is set to FALSE.
     $this->drupalGet('router_test/test18');
     $headers = $this->drupalGetHeaders();
-    $this->assertTrue(isset($headers['x-drupal-cache-contexts']));
-    $this->assertTrue(isset($headers['x-drupal-cache-tags']));
-    $this->setHttpResponseDebugCacheabilityHeaders(FALSE);
+    $this->assertTrue(isset($headers['X-Drupal-Cache-Contexts']));
+    $this->assertTrue(isset($headers['X-Drupal-Cache-Tags']));
+    $this->setContainerParameter('http.response.debug_cacheability_headers', FALSE);
+    $this->rebuildContainer();
+    $this->resetAll();
     $this->drupalGet('router_test/test18');
     $headers = $this->drupalGetHeaders();
-    $this->assertFalse(isset($headers['x-drupal-cache-contexts']));
-    $this->assertFalse(isset($headers['x-drupal-cache-tags']));
+    $this->assertFalse(isset($headers['X-Drupal-Cache-Contexts']));
+    $this->assertFalse(isset($headers['X-Drupal-Cache-Tags']));
   }
 
   /**
@@ -145,7 +148,7 @@ class RouterTest extends WebTestBase {
 
     // In some instances, the subrequest handling may get confused and render
     // a page inception style.  This test verifies that is not happening.
-    $this->assertNoPattern('#</body>.*</body>#s', 'There was no double-page effect from a misrendered subrequest.');
+    $this->assertSession()->responseNotMatches('#</body>.*</body>#s', 'There was no double-page effect from a misrendered subrequest.');
   }
 
   /**
@@ -162,7 +165,7 @@ class RouterTest extends WebTestBase {
 
     // In some instances, the subrequest handling may get confused and render
     // a page inception style.  This test verifies that is not happening.
-    $this->assertNoPattern('#</body>.*</body>#s', 'There was no double-page effect from a misrendered subrequest.');
+    $this->assertSession()->responseNotMatches('#</body>.*</body>#s', 'There was no double-page effect from a misrendered subrequest.');
   }
 
   /**
@@ -179,7 +182,7 @@ class RouterTest extends WebTestBase {
 
     // In some instances, the subrequest handling may get confused and render
     // a page inception style.  This test verifies that is not happening.
-    $this->assertNoPattern('#</body>.*</body>#s', 'There was no double-page effect from a misrendered subrequest.');
+    $this->assertSession()->responseNotMatches('#</body>.*</body>#s', 'There was no double-page effect from a misrendered subrequest.');
   }
 
   /**
@@ -208,7 +211,7 @@ class RouterTest extends WebTestBase {
 
     // In some instances, the subrequest handling may get confused and render
     // a page inception style. This test verifies that is not happening.
-    $this->assertNoPattern('#</body>.*</body>#s', 'There was no double-page effect from a misrendered subrequest.');
+    $this->assertSession()->responseNotMatches('#</body>.*</body>#s', 'There was no double-page effect from a misrendered subrequest.');
   }
 
   /**
@@ -277,7 +280,9 @@ class RouterTest extends WebTestBase {
   public function testControllerResolutionAjax() {
     // This will fail with a JSON parse error if the request is not routed to
     // The correct controller.
-    $this->drupalGetAjax('/router_test/test10');
+    $options['query'][MainContentViewSubscriber::WRAPPER_FORMAT] = 'drupal_ajax';
+    $headers[] = 'X-Requested-With: XMLHttpRequest';
+    $this->drupalGet('/router_test/test10', $options, $headers);
 
     $this->assertEqual($this->drupalGetHeader('Content-Type'), 'application/json', 'Correct mime content type was returned');
 
@@ -311,21 +316,18 @@ class RouterTest extends WebTestBase {
     $request = $this->container->get('request_stack')->getCurrentRequest();
     $url = $request->getUriForPath('//router_test/test1');
     $this->drupalGet($url);
-    $this->assertEqual(1, $this->redirectCount, $url . " redirected to " . $this->url);
     $this->assertUrl($request->getUriForPath('/router_test/test1'));
 
     // It should not matter how many leading slashes are used and query strings
     // should be preserved.
     $url = $request->getUriForPath('/////////////////////////////////////////////////router_test/test1') . '?qs=test';
     $this->drupalGet($url);
-    $this->assertEqual(1, $this->redirectCount, $url . " redirected to " . $this->url);
     $this->assertUrl($request->getUriForPath('/router_test/test1') . '?qs=test');
 
     // Ensure that external URLs in destination query params are not redirected
     // to.
     $url = $request->getUriForPath('/////////////////////////////////////////////////router_test/test1') . '?qs=test&destination=http://www.example.com%5c@drupal8alt.test';
     $this->drupalGet($url);
-    $this->assertEqual(1, $this->redirectCount, $url . " redirected to " . $this->url);
     $this->assertUrl($request->getUriForPath('/router_test/test1') . '?qs=test');
   }
 
similarity index 77%
rename from web/core/modules/system/src/Tests/Session/SessionAuthenticationTest.php
rename to web/core/modules/system/tests/src/Functional/Session/SessionAuthenticationTest.php
index ffdc08d1928974add2e740f055f51fcae9627615..10a35020e23eb78d770acfb4ddd9887b1eb5515d 100644 (file)
@@ -1,17 +1,17 @@
 <?php
 
-namespace Drupal\system\Tests\Session;
+namespace Drupal\Tests\system\Functional\Session;
 
 use Drupal\Core\Url;
-use Drupal\basic_auth\Tests\BasicAuthTestTrait;
-use Drupal\simpletest\WebTestBase;
+use Drupal\Tests\basic_auth\Traits\BasicAuthTestTrait;
+use Drupal\Tests\BrowserTestBase;
 
 /**
  * Tests if sessions are correctly handled when a user authenticates.
  *
  * @group Session
  */
-class SessionAuthenticationTest extends WebTestBase {
+class SessionAuthenticationTest extends BrowserTestBase {
 
   use BasicAuthTestTrait;
 
@@ -52,20 +52,22 @@ class SessionAuthenticationTest extends WebTestBase {
 
     // Test that the route is not accessible as an anonymous user.
     $this->drupalGet($protected_url);
+    $session = $this->getSession();
     $this->assertResponse(401, 'An anonymous user cannot access a route protected with basic authentication.');
 
     // We should be able to access the route with basic authentication.
-    $this->basicAuthGet($protected_url, $this->user->getUsername(), $this->user->pass_raw);
+    $this->basicAuthGet($protected_url, $this->user->getAccountName(), $this->user->passRaw);
     $this->assertResponse(200, 'A route protected with basic authentication can be accessed by an authenticated user.');
 
     // Check that the correct user is logged in.
-    $this->assertEqual($this->user->id(), json_decode($this->getRawContent())->user, 'The correct user is authenticated on a route with basic authentication.');
+    $this->assertEqual($this->user->id(), json_decode($session->getPage()->getContent())->user, 'The correct user is authenticated on a route with basic authentication.');
+    $session->restart();
 
     // If we now try to access a page without basic authentication then we
     // should no longer be logged in.
     $this->drupalGet($unprotected_url);
     $this->assertResponse(200, 'An unprotected route can be accessed without basic authentication.');
-    $this->assertFalse(json_decode($this->getRawContent())->user, 'The user is no longer authenticated after visiting a page without basic authentication.');
+    $this->assertFalse(json_decode($session->getPage()->getContent())->user, 'The user is no longer authenticated after visiting a page without basic authentication.');
 
     // If we access the protected page again without basic authentication we
     // should get 401 Unauthorized.
@@ -113,20 +115,24 @@ class SessionAuthenticationTest extends WebTestBase {
     $no_cookie_url = Url::fromRoute('session_test.get_session_basic_auth');
 
     // A route that is authorized with standard cookie authentication.
-    $cookie_url = '<front>';
+    $cookie_url = 'user/login';
 
     // If we authenticate with a third party authentication system then no
     // session cookie should be set, the third party system is responsible for
     // sustaining the session.
-    $this->basicAuthGet($no_cookie_url, $this->user->getUsername(), $this->user->pass_raw);
+    $this->basicAuthGet($no_cookie_url, $this->user->getAccountName(), $this->user->passRaw);
     $this->assertResponse(200, 'The user is successfully authenticated using basic authentication.');
-    $this->assertFalse($this->drupalGetHeader('set-cookie', TRUE), 'No cookie is set on a route protected with basic authentication.');
+    $this->assertEmpty($this->getSessionCookies());
+    // Mink stores some information in the session that breaks the next check if
+    // not reset.
+    $this->getSession()->restart();
 
     // On the other hand, authenticating using Cookie sets a cookie.
-    $edit = ['name' => $this->user->getUsername(), 'pass' => $this->user->pass_raw];
+    $this->drupalGet($cookie_url);
+    $this->assertEmpty($this->getSessionCookies());
+    $edit = ['name' => $this->user->getAccountName(), 'pass' => $this->user->passRaw];
     $this->drupalPostForm($cookie_url, $edit, t('Log in'));
-    $this->assertResponse(200, 'The user is successfully authenticated using cookie authentication.');
-    $this->assertTrue($this->drupalGetHeader('set-cookie', TRUE), 'A cookie is set on a route protected with cookie authentication.');
+    $this->assertNotEmpty($this->getSessionCookies());
   }
 
 }
similarity index 89%
rename from web/core/modules/system/src/Tests/Session/SessionTest.php
rename to web/core/modules/system/tests/src/Functional/Session/SessionTest.php
index 0f219c5036b5c1c162008e0ff8f603cfb70536aa..f0ce98cef5eb7880aaed0b9998325629a1e7f793 100644 (file)
@@ -1,15 +1,15 @@
 <?php
 
-namespace Drupal\system\Tests\Session;
+namespace Drupal\Tests\system\Functional\Session;
 
-use Drupal\simpletest\WebTestBase;
+use Drupal\Tests\BrowserTestBase;
 
 /**
  * Drupal session handling tests.
  *
  * @group Session
  */
-class SessionTest extends WebTestBase {
+class SessionTest extends BrowserTestBase {
 
   /**
    * Modules to enable.
@@ -36,12 +36,15 @@ class SessionTest extends WebTestBase {
     $user = $this->drupalCreateUser();
 
     // Enable sessions.
-    $this->sessionReset($user->id());
+    $this->sessionReset();
 
-    // Make sure the session cookie is set as HttpOnly.
-    $this->drupalLogin($user);
+    // Make sure the session cookie is set as HttpOnly. We can only test this in
+    // the header, with the test setup
+    // \GuzzleHttp\Cookie\SetCookie::getHttpOnly() always returns FALSE.
+    // Start a new session by setting a message.
+    $this->drupalGet('session-test/set-message');
+    $this->assertSessionCookie(TRUE);
     $this->assertTrue(preg_match('/HttpOnly/i', $this->drupalGetHeader('Set-Cookie', TRUE)), 'Session cookie is set as HttpOnly.');
-    $this->drupalLogout();
 
     // Verify that the session is regenerated if a module calls exit
     // in hook_user_login().
@@ -49,15 +52,15 @@ class SessionTest extends WebTestBase {
     $user->save();
     $this->drupalGet('session-test/id');
     $matches = [];
-    preg_match('/\s*session_id:(.*)\n/', $this->getRawContent(), $matches);
+    preg_match('/\s*session_id:(.*)\n/', $this->getSession()->getPage()->getContent(), $matches);
     $this->assertTrue(!empty($matches[1]), 'Found session ID before logging in.');
     $original_session = $matches[1];
 
     // We cannot use $this->drupalLogin($user); because we exit in
     // session_test_user_login() which breaks a normal assertion.
     $edit = [
-      'name' => $user->getUsername(),
-      'pass' => $user->pass_raw,
+      'name' => $user->getAccountName(),
+      'pass' => $user->passRaw,
     ];
     $this->drupalPostForm('user/login', $edit, t('Log in'));
     $this->drupalGet('user');
@@ -66,7 +69,7 @@ class SessionTest extends WebTestBase {
 
     $this->drupalGet('session-test/id');
     $matches = [];
-    preg_match('/\s*session_id:(.*)\n/', $this->getRawContent(), $matches);
+    preg_match('/\s*session_id:(.*)\n/', $this->getSession()->getPage()->getContent(), $matches);
     $this->assertTrue(!empty($matches[1]), 'Found session ID after logging in.');
     $this->assertTrue($matches[1] != $original_session, 'Session ID changed after login.');
   }
@@ -91,14 +94,22 @@ class SessionTest extends WebTestBase {
     // properly, val_1 will still be set.
     $value_2 = $this->randomMachineName();
     $this->drupalGet('session-test/no-set/' . $value_2);
+    $session = $this->getSession();
     $this->assertText($value_2, 'The session value was correctly passed to session-test/no-set.', 'Session');
     $this->drupalGet('session-test/get');
     $this->assertText($value_1, 'Session data is not saved for drupal_save_session(FALSE).', 'Session');
 
     // Switch browser cookie to anonymous user, then back to user 1.
-    $this->sessionReset();
-    $this->sessionReset($user->id());
+    $session_cookie_name = $this->getSessionName();
+    $session_cookie_value = $session->getCookie($session_cookie_name);
+    $session->restart();
+    $this->initFrontPage();
+    // Session restart always resets all the cookies by design, so we need to
+    // add the old session cookie again.
+    $session->setCookie($session_cookie_name, $session_cookie_value);
+    $this->drupalGet('session-test/get');
     $this->assertText($value_1, 'Session data persists through browser close.', 'Session');
+    $this->mink->setDefaultSessionName('default');
 
     // Logout the user and make sure the stored value no longer persists.
     $this->drupalLogout();
@@ -242,8 +253,6 @@ class SessionTest extends WebTestBase {
     $this->assertEqual($times4->timestamp, $times3->timestamp, 'Sessions table was not updated.');
 
     // Force updating of users and sessions table once per second.
-    $this->settingsSet('session_write_interval', 0);
-    // Write that value also into the test settings.php file.
     $settings['settings']['session_write_interval'] = (object) [
       'value' => 0,
       'required' => TRUE,
@@ -270,8 +279,7 @@ class SessionTest extends WebTestBase {
     // Send a blank sid in the session cookie, and the session should no longer
     // be valid. Closing the curl handler will stop the previous session ID
     // from persisting.
-    $this->curlClose();
-    $this->additionalCurlOptions[CURLOPT_COOKIE] = rawurlencode($this->getSessionName()) . '=;';
+    $this->mink->resetSessions();
     $this->drupalGet('session-test/id-from-cookie');
     $this->assertRaw("session_id:\n", 'Session ID is blank as sent from cookie header.');
     // Assert that we have an anonymous session now.
@@ -281,19 +289,13 @@ class SessionTest extends WebTestBase {
 
   /**
    * Reset the cookie file so that it refers to the specified user.
-   *
-   * @param $uid
-   *   User id to set as the active session.
    */
-  public function sessionReset($uid = 0) {
+  public function sessionReset() {
     // Close the internal browser.
-    $this->curlClose();
+    $this->mink->resetSessions();
     $this->loggedInUser = FALSE;
 
     // Change cookie file for user.
-    $this->cookieFile = \Drupal::service('stream_wrapper_manager')->getViaScheme('temporary')->getDirectoryPath() . '/cookie.' . $uid . '.txt';
-    $this->additionalCurlOptions[CURLOPT_COOKIEFILE] = $this->cookieFile;
-    $this->additionalCurlOptions[CURLOPT_COOKIESESSION] = TRUE;
     $this->drupalGet('session-test/get');
     $this->assertResponse(200, 'Session test module is correctly enabled.', 'Session');
   }
@@ -303,10 +305,10 @@ class SessionTest extends WebTestBase {
    */
   public function assertSessionCookie($sent) {
     if ($sent) {
-      $this->assertNotNull($this->sessionId, 'Session cookie was sent.');
+      $this->assertNotEmpty($this->getSessionCookies()->count(), 'Session cookie was sent.');
     }
     else {
-      $this->assertNull($this->sessionId, 'Session cookie was not sent.');
+      $this->assertEmpty($this->getSessionCookies()->count(), 'Session cookie was not sent.');
     }
   }
 
diff --git a/web/core/modules/system/tests/src/Functional/Session/StackSessionHandlerIntegrationTest.php b/web/core/modules/system/tests/src/Functional/Session/StackSessionHandlerIntegrationTest.php
new file mode 100644 (file)
index 0000000..d65ba42
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+
+namespace Drupal\Tests\system\Functional\Session;
+
+use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Tests the stacked session handler functionality.
+ *
+ * @group Session
+ */
+class StackSessionHandlerIntegrationTest extends BrowserTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['session_test'];
+
+  /**
+   * Tests a request.
+   */
+  public function testRequest() {
+    $options['query'][MainContentViewSubscriber::WRAPPER_FORMAT] = 'drupal_ajax';
+    $headers[] = 'X-Requested-With: XMLHttpRequest';
+    $actual_trace = json_decode($this->drupalGet('session-test/trace-handler', $options, $headers));
+    $sessionId = $this->getSessionCookies()->getCookieByName($this->getSessionName())->getValue();
+    $expect_trace = [
+      ['BEGIN', 'test_argument', 'open'],
+      ['BEGIN', NULL, 'open'],
+      ['END', NULL, 'open'],
+      ['END', 'test_argument', 'open'],
+      ['BEGIN', 'test_argument', 'read', $sessionId],
+      ['BEGIN', NULL, 'read', $sessionId],
+      ['END', NULL, 'read', $sessionId],
+      ['END', 'test_argument', 'read', $sessionId],
+      ['BEGIN', 'test_argument', 'write', $sessionId],
+      ['BEGIN', NULL, 'write', $sessionId],
+      ['END', NULL, 'write', $sessionId],
+      ['END', 'test_argument', 'write', $sessionId],
+      ['BEGIN', 'test_argument', 'close'],
+      ['BEGIN', NULL, 'close'],
+      ['END', NULL, 'close'],
+      ['END', 'test_argument', 'close'],
+    ];
+    $this->assertEqual($expect_trace, $actual_trace);
+  }
+
+}
index 3d534d8e750337515910f9d3f81144eb54186c90..b70ee859c65044e6bf32b010a4d245d06c55ed4c 100644 (file)
@@ -94,7 +94,11 @@ class ThemeTest extends BrowserTestBase {
     $config->set('css.preprocess', 0);
     $config->save();
     $this->drupalGet('theme-test/suggestion');
-    $this->assertNoText('js.module.css', 'The theme\'s .info.yml file is able to override a module CSS file from being added to the page.');
+    // We add a "?" to the assertion, because drupalSettings may include
+    // information about the file; we only really care about whether it appears
+    // in a LINK or STYLE tag, for which Drupal always adds a query string for
+    // cache control.
+    $this->assertSession()->responseNotContains('js.module.css?');
 
     // Also test with aggregation enabled, simply ensuring no PHP errors are
     // triggered during drupal_build_css_cache() when a source file doesn't
diff --git a/web/core/modules/system/tests/src/FunctionalJavascript/Form/ElementsTableSelectTest.php b/web/core/modules/system/tests/src/FunctionalJavascript/Form/ElementsTableSelectTest.php
new file mode 100644 (file)
index 0000000..b565bfd
--- /dev/null
@@ -0,0 +1,58 @@
+<?php
+
+namespace Drupal\Tests\system\FunctionalJavascript\Form;
+
+use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+
+/**
+ * Tests the tableselect form element for expected behavior.
+ *
+ * @group Form
+ */
+class ElementsTableSelectTest extends WebDriverTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['form_test'];
+
+  /**
+   * Test the presence of ajax functionality for all options.
+   */
+  public function testAjax() {
+    // Test checkboxes (#multiple == TRUE).
+    $this->drupalGet('form_test/tableselect/multiple-true');
+    $session = $this->getSession();
+    $page = $session->getPage();
+    for ($i = 1; $i <= 3; $i++) {
+      $row = 'row' . $i;
+      $page->hasUncheckedField($row);
+      $page->checkField($row);
+      $this->assertSession()->assertWaitOnAjaxRequest();
+      // Check current row and previous rows are checked.
+      for ($j = 1; $j <= $i; $j++) {
+        $other_row = 'row' . $j;
+        $page->hasCheckedField($other_row);
+      }
+    }
+
+    // Test radios (#multiple == FALSE).
+    $this->drupalGet('form_test/tableselect/multiple-false');
+    for ($i = 1; $i <= 3; $i++) {
+      $row = 'input[value="row' . $i . '"]';
+      $page->hasUncheckedField($row);
+      $this->click($row);
+      $this->assertSession()->assertWaitOnAjaxRequest();
+      $page->hasCheckedField($row);
+      // Check other rows are not checked
+      for ($j = 1; $j <= 3; $j++) {
+        if ($j == $i) {
+          continue;
+        }
+        $other_row = 'edit-tableselect-row' . $j;
+        $page->hasUncheckedField($other_row);
+      }
+    }
+  }
+
+}
similarity index 53%
rename from web/core/modules/system/src/Tests/Form/RebuildTest.php
rename to web/core/modules/system/tests/src/FunctionalJavascript/Form/RebuildTest.php
index 2e138bdfb322ce200baf9a1a431321569fc04eeb..caf52094d574c9f12d402da7b40dc11dbbd58646 100644 (file)
@@ -1,12 +1,12 @@
 <?php
 
-namespace Drupal\system\Tests\Form;
+namespace Drupal\Tests\system\FunctionalJavascript\Form;
 
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
 use Drupal\Core\Url;
 use Drupal\field\Entity\FieldConfig;
-use Drupal\simpletest\WebTestBase;
 use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
 
 /**
  * Tests functionality of \Drupal\Core\Form\FormBuilderInterface::rebuildForm().
@@ -14,14 +14,12 @@ use Drupal\field\Entity\FieldStorageConfig;
  * @group Form
  * @todo Add tests for other aspects of form rebuilding.
  */
-class RebuildTest extends WebTestBase {
+class RebuildTest extends WebDriverTestBase {
 
   /**
-   * Modules to enable.
-   *
-   * @var array
+   * {@inheritdoc}
    */
-  public static $modules = ['node', 'form_test'];
+  protected static $modules = ['node', 'form_test'];
 
   /**
    * A user for testing.
@@ -30,6 +28,9 @@ class RebuildTest extends WebTestBase {
    */
   protected $webUser;
 
+  /**
+   * {@inheritdoc}
+   */
   protected function setUp() {
     parent::setUp();
 
@@ -39,28 +40,6 @@ class RebuildTest extends WebTestBase {
     $this->drupalLogin($this->webUser);
   }
 
-  /**
-   * Tests preservation of values.
-   */
-  public function testRebuildPreservesValues() {
-    $edit = [
-      'checkbox_1_default_off' => TRUE,
-      'checkbox_1_default_on' => FALSE,
-      'text_1' => 'foo',
-    ];
-    $this->drupalPostForm('form-test/form-rebuild-preserve-values', $edit, 'Add more');
-
-    // Verify that initial elements retained their submitted values.
-    $this->assertFieldChecked('edit-checkbox-1-default-off', 'A submitted checked checkbox retained its checked state during a rebuild.');
-    $this->assertNoFieldChecked('edit-checkbox-1-default-on', 'A submitted unchecked checkbox retained its unchecked state during a rebuild.');
-    $this->assertFieldById('edit-text-1', 'foo', 'A textfield retained its submitted value during a rebuild.');
-
-    // Verify that newly added elements were initialized with their default values.
-    $this->assertFieldChecked('edit-checkbox-2-default-on', 'A newly added checkbox was initialized with a default checked state.');
-    $this->assertNoFieldChecked('edit-checkbox-2-default-off', 'A newly added checkbox was initialized with a default unchecked state.');
-    $this->assertFieldById('edit-text-2', 'DEFAULT 2', 'A newly added textfield was initialized with its default value.');
-  }
-
   /**
    * Tests that a form's action is retained after an Ajax submission.
    *
@@ -68,6 +47,7 @@ class RebuildTest extends WebTestBase {
    * followed by a non-Ajax submission, which triggers a validation error.
    */
   public function testPreserveFormActionAfterAJAX() {
+    $page = $this->getSession()->getPage();
     // Create a multi-valued field for 'page' nodes to use for Ajax testing.
     $field_name = 'field_ajax_test';
     FieldStorageConfig::create([
@@ -81,8 +61,26 @@ class RebuildTest extends WebTestBase {
       'entity_type' => 'node',
       'bundle' => 'page',
     ])->save();
+
+    // Also create a file field to test server side validation error.
+    $field_file_name = 'field_file_test';
+    FieldStorageConfig::create([
+      'field_name' => $field_file_name,
+      'entity_type' => 'node',
+      'type' => 'file',
+      'cardinality' => 1,
+    ])->save();
+    FieldConfig::create([
+      'field_name' => $field_file_name,
+      'entity_type' => 'node',
+      'bundle' => 'page',
+      'label' => 'Test file',
+      'required' => TRUE,
+    ])->save();
+
     entity_get_form_display('node', 'page', 'default')
       ->setComponent($field_name, ['type' => 'text_textfield'])
+      ->setComponent($field_file_name, ['type' => 'file_generic'])
       ->save();
 
     // Log in a user who can create 'page' nodes.
@@ -93,27 +91,31 @@ class RebuildTest extends WebTestBase {
     // submission and verify it worked by ensuring the updated page has two text
     // field items in the field for which we just added an item.
     $this->drupalGet('node/add/page');
-    $this->drupalPostAjaxForm(NULL, [], ['field_ajax_test_add_more' => t('Add another item')], NULL, [], [], 'node-page-form');
-    $this->assert(count($this->xpath('//div[contains(@class, "field--name-field-ajax-test")]//input[@type="text"]')) == 2, 'AJAX submission succeeded.');
+    $page->find('css', '[value="Add another item"]')->click();
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->assertTrue(count($this->xpath('//div[contains(@class, "field--name-field-ajax-test")]//input[@type="text"]')) == 2, 'AJAX submission succeeded.');
 
-    // Submit the form with the non-Ajax "Save" button, leaving the title field
+    // Submit the form with the non-Ajax "Save" button, leaving the file field
     // blank to trigger a validation error, and ensure that a validation error
     // occurred, because this test is for testing what happens when a form is
     // re-rendered without being re-built, which is what happens when there's
-    // a validation error.
-    $this->drupalPostForm(NULL, [], t('Save'));
-    $this->assertText('Title field is required.', 'Non-AJAX submission correctly triggered a validation error.');
+    // a server side validation error.
+    $edit = [
+      'title[0][value]' => $this->randomString(),
+    ];
+    $this->drupalPostForm(NULL, $edit, 'Save');
+    $this->assertSession()->pageTextContains('Test file field is required.', 'Non-AJAX submission correctly triggered a validation error.');
 
     // Ensure that the form contains two items in the multi-valued field, so we
     // know we're testing a form that was correctly retrieved from cache.
-    $this->assert(count($this->xpath('//form[contains(@id, "node-page-form")]//div[contains(@class, "js-form-item-field-ajax-test")]//input[@type="text"]')) == 2, 'Form retained its state from cache.');
+    $this->assertTrue(count($this->xpath('//form[contains(@id, "node-page-form")]//div[contains(@class, "js-form-item-field-ajax-test")]//input[@type="text"]')) == 2, 'Form retained its state from cache.');
 
     // Ensure that the form's action is correct.
     $forms = $this->xpath('//form[contains(@class, "node-page-form")]');
-    $this->assertEqual(1, count($forms));
+    $this->assertEquals(1, count($forms));
     // Strip query params off the action before asserting.
-    $url = parse_url($forms[0]['action'])['path'];
-    $this->assertEqual(Url::fromRoute('node.add', ['node_type' => 'page'])->toString(), $url);
+    $url = parse_url($forms[0]->getAttribute('action'))['path'];
+    $this->assertEquals(Url::fromRoute('node.add', ['node_type' => 'page'])->toString(), $url);
   }
 
 }
diff --git a/web/core/modules/system/tests/src/FunctionalJavascript/Form/TriggeringElementTest.php b/web/core/modules/system/tests/src/FunctionalJavascript/Form/TriggeringElementTest.php
new file mode 100644 (file)
index 0000000..41733ec
--- /dev/null
@@ -0,0 +1,111 @@
+<?php
+
+namespace Drupal\Tests\system\FunctionalJavascript\Form;
+
+use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+
+/**
+ * Tests that FAPI correctly determines the triggering element.
+ *
+ * @group Form
+ */
+class TriggeringElementTest extends WebDriverTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['form_test'];
+
+  /**
+   * Tests the the triggering element when no button information is included.
+   *
+   * Test the determination of the triggering element when no button
+   * information is included in the POST data, as is sometimes the case when
+   * the ENTER key is pressed in a textfield in Internet Explorer.
+   */
+  public function testNoButtonInfoInPost() {
+    $path = '/form-test/clicked-button';
+    $form_html_id = 'form-test-clicked-button';
+
+    // Ensure submitting a form with no buttons results in no triggering element
+    // and the form submit handler not running.
+    $this->drupalGet($path);
+
+    $assert_session = $this->assertSession();
+    $this->getSession()->getDriver()->submitForm('//form[@id="' . $form_html_id . '"]');
+    $assert_session->pageTextContains('There is no clicked button.');
+    $assert_session->pageTextNotContains('Submit handler for form_test_clicked_button executed.');
+
+    // Ensure submitting a form with one or more submit buttons results in the
+    // triggering element being set to the first one the user has access to. An
+    // argument with 'r' in it indicates a restricted (#access=FALSE) button.
+    $this->drupalGet($path . '/s');
+    $this->getSession()->getDriver()->submitForm('//form[@id="' . $form_html_id . '"]');
+    $assert_session->pageTextContains('The clicked button is button1.');
+    $assert_session->pageTextContains('Submit handler for form_test_clicked_button executed.');
+
+    $this->drupalGet($path . '/s/s');
+    $this->getSession()->getDriver()->submitForm('//form[@id="' . $form_html_id . '"]');
+    $assert_session->pageTextContains('The clicked button is button1.');
+    $assert_session->pageTextContains('Submit handler for form_test_clicked_button executed.');
+
+    $this->drupalGet($path . '/rs/s');
+    $this->getSession()->getDriver()->submitForm('//form[@id="' . $form_html_id . '"]');
+    $assert_session->pageTextContains('The clicked button is button2.');
+    $assert_session->pageTextContains('Submit handler for form_test_clicked_button executed.');
+
+    // Ensure submitting a form with buttons of different types results in the
+    // triggering element being set to the first button, regardless of type. For
+    // the FAPI 'button' type, this should result in the submit handler not
+    // executing. The types are 's'(ubmit), 'b'(utton), and 'i'(mage_button).
+    $this->drupalGet($path . '/s/b/i');
+    $this->getSession()->getDriver()->submitForm('//form[@id="' . $form_html_id . '"]');
+    $assert_session->pageTextContains('The clicked button is button1.');
+    $assert_session->pageTextContains('Submit handler for form_test_clicked_button executed.');
+
+    $this->drupalGet($path . '/b/s/i');
+    $this->getSession()->getDriver()->submitForm('//form[@id="' . $form_html_id . '"]');
+    $assert_session->pageTextContains('The clicked button is button1.');
+    $assert_session->pageTextNotContains('Submit handler for form_test_clicked_button executed.');
+
+    $this->drupalGet($path . '/i/s/b');
+    $this->getSession()->getDriver()->submitForm('//form[@id="' . $form_html_id . '"]');
+    $assert_session->pageTextContains('The clicked button is button1.');
+    $assert_session->pageTextContains('Submit handler for form_test_clicked_button executed.');
+  }
+
+  /**
+   * Tests attempts to bypass access control.
+   *
+   * Test that the triggering element does not get set to a button with
+   * #access=FALSE.
+   */
+  public function testAttemptAccessControlBypass() {
+    $path = 'form-test/clicked-button';
+    $form_html_id = 'form-test-clicked-button';
+
+    // Retrieve a form where 'button1' has #access=FALSE and 'button2' doesn't.
+    $this->drupalGet($path . '/rs/s');
+
+    // Submit the form with 'button1=button1' in the POST data, which someone
+    // trying to get around security safeguards could easily do. We have to do
+    // a little trickery here, to work around the safeguards in drupalPostForm()
+    // by renaming the text field and value that is in the form to 'button1',
+    // we can get the data we want into \Drupal::request()->request.
+    $page = $this->getSession()->getPage();
+    $input = $page->find('css', 'input[name="text"]');
+    $this->assertNotNull($input, 'text input located.');
+
+    $input->setValue('name', 'button1');
+    $input->setValue('value', 'button1');
+    $this->xpath('//form[@id="' . $form_html_id . '"]//input[@type="submit"]')[0]->click();
+
+    // Ensure that the triggering element was not set to the restricted button.
+    // Do this with both a negative and positive assertion, because negative
+    // assertions alone can be brittle. See testNoButtonInfoInPost() for why the
+    // triggering element gets set to 'button2'.
+    $this->assertSession()->pageTextNotContains('The clicked button is button1.');
+    $this->assertSession()->pageTextContains('The clicked button is button2.');
+  }
+
+}
diff --git a/web/core/modules/system/tests/src/FunctionalJavascript/FrameworkTest.php b/web/core/modules/system/tests/src/FunctionalJavascript/FrameworkTest.php
new file mode 100644 (file)
index 0000000..60f10b3
--- /dev/null
@@ -0,0 +1,122 @@
+<?php
+
+namespace Drupal\Tests\system\FunctionalJavascript;
+
+use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+
+/**
+ * Tests the off-canvas dialog functionality.
+ *
+ * @group system
+ */
+class FrameworkTest extends WebDriverTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['node', 'ajax_test', 'ajax_forms_test'];
+
+  /**
+   * Tests that new JavaScript and CSS files are lazy-loaded on an AJAX request.
+   */
+  public function testLazyLoad() {
+    $expected = [
+      'setting_name' => 'ajax_forms_test_lazy_load_form_submit',
+      'setting_value' => 'executed',
+      'library_1' => 'system/admin',
+      'library_2' => 'system/drupal.system',
+    ];
+
+    // Get the base page.
+    $this->drupalGet('ajax_forms_test_lazy_load_form');
+    $page = $this->getSession()->getPage();
+    $assert = $this->assertSession();
+
+    $original_settings = $this->getDrupalSettings();
+    $original_libraries = explode(',', $original_settings['ajaxPageState']['libraries']);
+
+    // Verify that the base page doesn't have the settings and files that are to
+    // be lazy loaded as part of the next requests.
+    $this->assertTrue(!isset($original_settings[$expected['setting_name']]), format_string('Page originally lacks the %setting, as expected.', ['%setting' => $expected['setting_name']]));
+    $this->assertTrue(!in_array($expected['library_1'], $original_libraries), format_string('Page originally lacks the %library library, as expected.', ['%library' => $expected['library_1']]));
+    $this->assertTrue(!in_array($expected['library_2'], $original_libraries), format_string('Page originally lacks the %library library, as expected.', ['%library' => $expected['library_2']]));
+
+    // Submit the AJAX request without triggering files getting added.
+    $page->pressButton('Submit');
+    $assert->assertWaitOnAjaxRequest();
+    $new_settings = $this->getDrupalSettings();
+    $new_libraries = explode(',', $new_settings['ajaxPageState']['libraries']);
+
+    // Verify the setting was not added when not expected.
+    $this->assertTrue(!isset($new_settings[$expected['setting_name']]), format_string('Page still lacks the %setting, as expected.', ['%setting' => $expected['setting_name']]));
+    $this->assertTrue(!in_array($expected['library_1'], $new_libraries), format_string('Page still lacks the %library library, as expected.', ['%library' => $expected['library_1']]));
+    $this->assertTrue(!in_array($expected['library_2'], $new_libraries), format_string('Page still lacks the %library library, as expected.', ['%library' => $expected['library_2']]));
+
+    // Submit the AJAX request and trigger adding files.
+    $page->checkField('add_files');
+    $page->pressButton('Submit');
+    $assert->assertWaitOnAjaxRequest();
+    $new_settings = $this->getDrupalSettings();
+    $new_libraries = explode(',', $new_settings['ajaxPageState']['libraries']);
+
+    // Verify the expected setting was added, both to drupalSettings, and as
+    // the first AJAX command.
+    $this->assertIdentical($new_settings[$expected['setting_name']], $expected['setting_value'], format_string('Page now has the %setting.', ['%setting' => $expected['setting_name']]));
+
+    // Verify the expected CSS file was added, both to drupalSettings, and as
+    // the second AJAX command for inclusion into the HTML.
+    $this->assertTrue(in_array($expected['library_1'], $new_libraries), format_string('Page state now has the %library library.', ['%library' => $expected['library_1']]));
+
+    // Verify the expected JS file was added, both to drupalSettings, and as
+    // the third AJAX command for inclusion into the HTML. By testing for an
+    // exact HTML string containing the SCRIPT tag, we also ensure that
+    // unexpected JavaScript code, such as a jQuery.extend() that would
+    // potentially clobber rather than properly merge settings, didn't
+    // accidentally get added.
+    $this->assertTrue(in_array($expected['library_2'], $new_libraries), format_string('Page state now has the %library library.', ['%library' => $expected['library_2']]));
+  }
+
+  /**
+   * Tests that drupalSettings.currentPath is not updated on AJAX requests.
+   */
+  public function testCurrentPathChange() {
+    $this->drupalGet('ajax_forms_test_lazy_load_form');
+    $page = $this->getSession()->getPage();
+    $assert = $this->assertSession();
+
+    $old_settings = $this->getDrupalSettings();
+    $page->pressButton('Submit');
+    $assert->assertWaitOnAjaxRequest();
+    $new_settings = $this->getDrupalSettings();
+    $this->assertEquals($old_settings['path']['currentPath'], $new_settings['path']['currentPath']);
+  }
+
+  /**
+   * Tests that overridden CSS files are not added during lazy load.
+   */
+  public function testLazyLoadOverriddenCSS() {
+    // The test theme overrides js.module.css without an implementation,
+    // thereby removing it.
+    \Drupal::service('theme_handler')->install(['test_theme']);
+    $this->config('system.theme')
+      ->set('default', 'test_theme')
+      ->save();
+
+    // This gets the form, and does an Ajax submission on it.
+    $this->drupalGet('ajax_forms_test_lazy_load_form');
+    $page = $this->getSession()->getPage();
+    $assert = $this->assertSession();
+
+    $page->checkField('add_files');
+    $page->pressButton('Submit');
+    $assert->assertWaitOnAjaxRequest();
+
+    // Verify that the resulting HTML does not load the overridden CSS file.
+    // We add a "?" to the assertion, because drupalSettings may include
+    // information about the file; we only really care about whether it appears
+    // in a LINK or STYLE tag, for which Drupal always adds a query string for
+    // cache control.
+    $assert->responseNotContains('js.module.css?', 'Ajax lazy loading does not add overridden CSS files.');
+  }
+
+}
index 708d517a321200d83c28e1d1bd5790918fc820c7..fd22de13977cf0f8184c8c0a711bfa3ba85af401 100644 (file)
@@ -233,11 +233,11 @@ class ModuleHandlerTest extends KernelTestBase {
     $result = $this->moduleInstaller()->uninstall([$non_dependency]);
     $this->assertTrue($result, 'ModuleInstaller::uninstall() returns TRUE.');
     $this->assertFalse($this->moduleHandler()->moduleExists($non_dependency));
-    $this->assertEquals(drupal_get_installed_schema_version($non_dependency), SCHEMA_UNINSTALLED, "$dependency module was uninstalled.");
+    $this->assertEquals(drupal_get_installed_schema_version($non_dependency), SCHEMA_UNINSTALLED, "$non_dependency module was uninstalled.");
 
     // Verify that the installation profile itself was not uninstalled.
     $uninstalled_modules = \Drupal::state()->get('module_test.uninstall_order') ?: [];
-    $this->assertContains($non_dependency, $uninstalled_modules, "$dependency module is in the list of uninstalled modules.");
+    $this->assertContains($non_dependency, $uninstalled_modules, "$non_dependency module is in the list of uninstalled modules.");
     $this->assertNotContains($profile, $uninstalled_modules, 'The installation profile is not in the list of uninstalled modules.');
 
     // Try uninstalling the required module.
index b73a45d7c6221855c78aea1dfc6d309f2604a049..b9660a45f52a654a2be8c2f159ab59318529e1c5 100644 (file)
@@ -16,7 +16,7 @@ base theme: classy
 core: 8.x
 logo: images/logo2.svg
 stylesheets-remove:
-  - '@system/css/js.module.css'
+  - '@stable/css/system/components/js.module.css'
 libraries:
   - test_theme/global-styling
 libraries-override:
index a53e94a0c563afb02f3c7cbd2e734b92d02cb2c9..482577f2d1e236d0841b9666fb4b35de57bbaaa7 100644 (file)
@@ -10,3 +10,9 @@ test_theme_settings.settings:
       sequence:
         type: integer
         label: 'fids'
+    multi_file:
+      type: sequence
+      label: 'Multiple file field with all file extensions'
+      sequence:
+        type: integer
+        label: 'fids'
index 7f3c1395f9fb5b199831f2c1d254c7dc34176ce9..1255c65379a054e64edc47a79db873a52079bacf 100644 (file)
@@ -24,6 +24,17 @@ function test_theme_settings_form_system_theme_settings_alter(&$form, FormStateI
     ],
   ];
 
+  $form['multi_file'] = [
+    '#type' => 'managed_file',
+    '#title' => t('Multiple file field with all file extensions'),
+    '#multiple' => TRUE,
+    '#default_value' => theme_get_setting('multi_file'),
+    '#upload_location' => 'public://test',
+    '#upload_validators'  => [
+      'file_validate_extensions' => [],
+    ],
+  ];
+
   $form['#submit'][] = 'test_theme_settings_form_system_theme_settings_submit';
 }
 
diff --git a/web/core/modules/taxonomy/src/Plugin/migrate/source/d6/TermLocalizedTranslation.php b/web/core/modules/taxonomy/src/Plugin/migrate/source/d6/TermLocalizedTranslation.php
new file mode 100644 (file)
index 0000000..5348ee2
--- /dev/null
@@ -0,0 +1,100 @@
+<?php
+
+namespace Drupal\taxonomy\Plugin\migrate\source\d6;
+
+use Drupal\migrate\Row;
+
+/**
+ * Gets i18n taxonomy terms from source database.
+ *
+ * @MigrateSource(
+ *   id = "d6_term_localized_translation",
+ *   source_module = "i18ntaxonomy"
+ * )
+ */
+class TermLocalizedTranslation extends Term {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function query() {
+    // Ideally, the query would return rows for each language for each taxonomy
+    // term with the translations for both the name and description or just the
+    // name translation or just the description translation. That query quickly
+    // became complex and would be difficult to maintain.
+    // Therefore, build a query based on i18nstrings table where each row has
+    // the translation for only one property, either name or description. The
+    // method prepareRow() is then used to obtain the translation for the other
+    // property.
+    $query = parent::query();
+    $query->addField('td', 'language', 'td.language');
+
+    // Add in the property, which is either name or description.
+    // Cast td.tid as char for PostgreSQL compatibility.
+    $query->leftJoin('i18n_strings', 'i18n', 'CAST(td.tid AS CHAR(255)) = i18n.objectid');
+    $query->isNotNull('i18n.lid');
+    $query->addField('i18n', 'lid');
+    $query->addField('i18n', 'property');
+
+    // Add in the translation for the property.
+    $query->innerJoin('locales_target', 'lt', 'i18n.lid = lt.lid');
+    $query->addField('lt', 'language', 'lt.language');
+    $query->addField('lt', 'translation');
+    return $query;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function prepareRow(Row $row) {
+    $language = $row->getSourceProperty('ltlanguage');
+    $row->setSourceProperty('language', $language);
+    $tid = $row->getSourceProperty('tid');
+
+    // If this row has been migrated it is a duplicate then skip it.
+    if ($this->idMap->lookupDestinationIds(['tid' => $tid, 'language' => $language])) {
+      return FALSE;
+    }
+
+    // Save the translation for this property.
+    $property = $row->getSourceProperty('property');
+    $row->setSourceProperty($property . '_translated', $row->getSourceProperty('translation'));
+
+    // Get the translation, if one exists, for the property not already in the
+    // row.
+    $other_property = ($property == 'name') ? 'description' : 'name';
+    $query = $this->select('i18n_strings', 'i18n')
+      ->fields('i18n', ['lid'])
+      ->condition('i18n.property', $other_property)
+      ->condition('i18n.objectid', $tid);
+    $query->leftJoin('locales_target', 'lt', 'i18n.lid = lt.lid');
+    $query->condition('lt.language', $language);
+    $query->addField('lt', 'translation');
+    $results = $query->execute()->fetchAssoc();
+    $row->setSourceProperty($other_property . '_translated', $results['translation']);
+
+    parent::prepareRow($row);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function fields() {
+    $fields = [
+      'language' => $this->t('Language for this term.'),
+      'name_translated' => $this->t('Term name translation.'),
+      'description_translated' => $this->t('Term description translation.'),
+    ];
+    return parent::fields() + $fields;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getIds() {
+    $ids['language']['type'] = 'string';
+    $ids['language']['alias'] = 'lt';
+    return parent::getIds() + $ids;
+  }
+
+}
index 75c79241e0c8eeed939f7cc60dcf9cebbb39be58..20ecb6390346241c7f29b610c7b5e4aaf8d8f840 100644 (file)
@@ -104,7 +104,14 @@ class MigrateTaxonomyTermTest extends MigrateDrupal6TestBase {
 
       $this->assertArrayHasKey($tid, $tree_terms, "Term $tid exists in vocabulary tree");
       $tree_term = $tree_terms[$tid];
-      $this->assertEquals($values['parent'], $tree_term->parents, "Term $tid has correct parents in vocabulary tree");
+
+      // PostgreSQL, MySQL and SQLite may not return the parent terms in the
+      // same order so sort before testing.
+      $expected_parents = $values['parent'];
+      sort($expected_parents);
+      $actual_parents = $tree_term->parents;
+      sort($actual_parents);
+      $this->assertEquals($expected_parents, $actual_parents, "Term $tid has correct parents in vocabulary tree");
     }
   }
 
diff --git a/web/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermLocalizedTranslationTest.php b/web/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermLocalizedTranslationTest.php
new file mode 100644 (file)
index 0000000..b051ce6
--- /dev/null
@@ -0,0 +1,142 @@
+<?php
+
+namespace Drupal\Tests\taxonomy\Kernel\Migrate\d6;
+
+use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
+use Drupal\taxonomy\Entity\Term;
+use Drupal\taxonomy\TermInterface;
+
+/**
+ * Tests migration of localized translated taxonomy terms.
+ *
+ * @group migrate_drupal_6
+ */
+class MigrateTermLocalizedTranslationTest extends MigrateDrupal6TestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'content_translation',
+    'language',
+    'menu_ui',
+    'node',
+    'taxonomy',
+    // Required for translation migrations.
+    'migrate_drupal_multilingual',
+  ];
+
+  /**
+   * The cached taxonomy tree items, keyed by vid and tid.
+   *
+   * @var array
+   */
+  protected $treeData = [];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->installEntitySchema('taxonomy_term');
+    $this->installConfig(static::$modules);
+    $this->executeMigrations([
+      'language',
+      'd6_node_type',
+      'd6_field',
+      'd6_taxonomy_vocabulary',
+      'd6_field_instance',
+      'd6_taxonomy_term',
+      'd6_taxonomy_term_localized_translation',
+    ]);
+  }
+
+  /**
+   * Validates a migrated term contains the expected values.
+   *
+   * @param int $id
+   *   Entity ID to load and check.
+   * @param string $expected_language
+   *   The language code for this term.
+   * @param string $expected_label
+   *   The label the migrated entity should have.
+   * @param string $expected_vid
+   *   The parent vocabulary the migrated entity should have.
+   * @param string $expected_description
+   *   The description the migrated entity should have.
+   * @param string $expected_format
+   *   The format the migrated entity should have.
+   * @param int $expected_weight
+   *   The weight the migrated entity should have.
+   * @param array $expected_parents
+   *   The parent terms the migrated entity should have.
+   * @param int $expected_field_integer_value
+   *   The value the migrated entity field should have.
+   * @param int $expected_term_reference_tid
+   *   The term reference ID the migrated entity field should have.
+   */
+  protected function assertEntity($id, $expected_language, $expected_label, $expected_vid, $expected_description = '', $expected_format = NULL, $expected_weight = 0, array $expected_parents = [], $expected_field_integer_value = NULL, $expected_term_reference_tid = NULL) {
+    /** @var \Drupal\taxonomy\TermInterface $entity */
+    $entity = Term::load($id);
+    $this->assertInstanceOf(TermInterface::class, $entity);
+    $this->assertSame($expected_language, $entity->language()->getId());
+    $this->assertSame($expected_label, $entity->label());
+    $this->assertSame($expected_vid, $entity->bundle());
+    $this->assertSame($expected_description, $entity->getDescription());
+    $this->assertSame($expected_format, $entity->getFormat());
+    $this->assertSame($expected_weight, $entity->getWeight());
+    $this->assertHierarchy($expected_vid, $id, $expected_parents);
+  }
+
+  /**
+   * Asserts that a term is present in the tree storage, with the right parents.
+   *
+   * @param string $vid
+   *   Vocabulary ID.
+   * @param int $tid
+   *   ID of the term to check.
+   * @param array $parent_ids
+   *   The expected parent term IDs.
+   */
+  protected function assertHierarchy($vid, $tid, array $parent_ids) {
+    if (!isset($this->treeData[$vid])) {
+      $tree = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->loadTree($vid);
+      $this->treeData[$vid] = [];
+      foreach ($tree as $item) {
+        $this->treeData[$vid][$item->tid] = $item;
+      }
+    }
+
+    $this->assertArrayHasKey($tid, $this->treeData[$vid], "Term $tid exists in taxonomy tree");
+    $term = $this->treeData[$vid][$tid];
+    $this->assertEquals($parent_ids, array_filter($term->parents), "Term $tid has correct parents in taxonomy tree");
+  }
+
+  /**
+   * Tests the Drupal 6 i18n localized taxonomy term to Drupal 8 migration.
+   */
+  public function testTranslatedLocalizedTaxonomyTerms() {
+    $this->assertEntity(14, 'en', 'Talos IV', 'vocabulary_name_much_longer_than', 'The home of Captain Christopher Pike.', NULL, '0', []);
+    $this->assertEntity(15, 'en', 'Vulcan', 'vocabulary_name_much_longer_than', NULL, NULL, '0', []);
+
+    /** @var \Drupal\taxonomy\TermInterface $entity */
+    $entity = Term::load(14);
+    $this->assertTrue($entity->hasTranslation('fr'));
+    $translation = $entity->getTranslation('fr');
+    $this->assertSame('fr - Talos IV', $translation->label());
+    $this->assertSame('fr - The home of Captain Christopher Pike.', $translation->getDescription());
+
+    $this->assertTrue($entity->hasTranslation('zu'));
+    $translation = $entity->getTranslation('zu');
+    $this->assertSame('Talos IV', $translation->label());
+    $this->assertSame('zu - The home of Captain Christopher Pike.', $translation->getDescription());
+
+    $entity = Term::load(15);
+    $this->assertFalse($entity->hasTranslation('fr'));
+    $this->assertTrue($entity->hasTranslation('zu'));
+    $translation = $entity->getTranslation('zu');
+    $this->assertSame('zu - Vulcan', $translation->label());
+    $this->assertSame('', $translation->getDescription());
+  }
+
+}
index c29e3d1e816f9ce62a0c0b6f619db2fe828acead..46d60a6398e1fc61f1afbac3514975e9b413cae5 100644 (file)
@@ -81,13 +81,13 @@ class MigrateVocabularyFieldInstanceTest extends MigrateDrupal6TestBase {
     $field_id = 'node.story.field_vocabulary_3_i_2_';
     $field = FieldConfig::load($field_id);
     $this->assertFalse($field->isRequired(), 'Field is not required');
-    $this->assertFalse($field->isTranslatable());
+    $this->assertTrue($field->isTranslatable());
 
     // Tests that a vocabulary named like a D8 base field will be migrated and
     // prefixed with 'field_' to avoid conflicts.
     $field_type = FieldConfig::load('node.sponsor.field_type');
     $this->assertInstanceOf(FieldConfig::class, $field_type);
-    $this->assertFalse($field->isTranslatable());
+    $this->assertTrue($field->isTranslatable());
   }
 
   /**
diff --git a/web/core/modules/taxonomy/tests/src/Kernel/Plugin/migrate/source/d6/TermLocalizedTranslationTest.php b/web/core/modules/taxonomy/tests/src/Kernel/Plugin/migrate/source/d6/TermLocalizedTranslationTest.php
new file mode 100644 (file)
index 0000000..c2fea26
--- /dev/null
@@ -0,0 +1,179 @@
+<?php
+
+namespace Drupal\Tests\taxonomy\Kernel\Plugin\migrate\source\d6;
+
+use Drupal\Tests\migrate\Kernel\MigrateSqlSourceTestBase;
+
+/**
+ * Tests D6 i18n term localized source plugin.
+ *
+ * @covers \Drupal\taxonomy\Plugin\migrate\source\d6\TermLocalizedTranslation
+ * @group taxonomy
+ */
+class TermLocalizedTranslationTest extends MigrateSqlSourceTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['taxonomy', 'migrate_drupal'];
+
+  /**
+   * {@inheritdoc}
+   */
+  public function providerSource() {
+    $tests = [];
+
+    // The source data.
+    $tests[0]['source_data']['term_data'] = [
+      [
+        'tid' => 1,
+        'vid' => 5,
+        'name' => 'name value 1',
+        'description' => 'description value 1',
+        'weight' => 0,
+        'language' => NULL,
+      ],
+      [
+        'tid' => 2,
+        'vid' => 6,
+        'name' => 'name value 2',
+        'description' => 'description value 2',
+        'weight' => 0,
+        'language' => NULL,
+      ],
+      [
+        'tid' => 3,
+        'vid' => 6,
+        'name' => 'name value 3',
+        'description' => 'description value 3',
+        'weight' => 0,
+        'language' => NULL,
+      ],
+      [
+        'tid' => 4,
+        'vid' => 5,
+        'name' => 'name value 4',
+        'description' => 'description value 4',
+        'weight' => 1,
+        'language' => NULL,
+      ],
+    ];
+    $tests[0]['source_data']['term_hierarchy'] = [
+      [
+        'tid' => 1,
+        'parent' => 0,
+      ],
+      [
+        'tid' => 2,
+        'parent' => 0,
+      ],
+      [
+        'tid' => 3,
+        'parent' => 0,
+      ],
+      [
+        'tid' => 4,
+        'parent' => 1,
+      ],
+    ];
+    $tests[0]['source_data']['i18n_strings'] = [
+      [
+        'lid' => 6,
+        'objectid' => 1,
+        'type' => 'term',
+        'property' => 'name',
+        'objectindex' => '1',
+        'format' => 0,
+      ],
+      [
+        'lid' => 7,
+        'objectid' => 1,
+        'type' => 'term',
+        'property' => 'description',
+        'objectindex' => '1',
+        'format' => 0,
+      ],
+      [
+        'lid' => 8,
+        'objectid' => 3,
+        'type' => 'term',
+        'property' => 'name',
+        'objectindex' => '3',
+        'format' => 0,
+      ],
+    ];
+    $tests[0]['source_data']['locales_target'] = [
+      [
+        'lid' => 6,
+        'language' => 'fr',
+        'translation' => 'fr - name value 1 translation',
+        'plid' => 0,
+        'plural' => 0,
+        'i18n_status' => 0,
+      ],
+      [
+        'lid' => 7,
+        'language' => 'fr',
+        'translation' => 'fr - description value 1 translation',
+        'plid' => 0,
+        'plural' => 0,
+        'i18n_status' => 0,
+      ],
+      [
+        'lid' => 8,
+        'language' => 'zu',
+        'translation' => 'zu - description value 2 translation',
+        'plid' => 0,
+        'plural' => 0,
+        'i18n_status' => 0,
+      ],
+    ];
+
+    // The expected results.
+    $tests[0]['expected_data'] = [
+      [
+        'tid' => 1,
+        'vid' => 5,
+        'name' => 'name value 1',
+        'description' => 'description value 1',
+        'weight' => 0,
+        'parent' => [0],
+        'property' => 'name',
+        'language' => 'fr',
+        'name_translated' => 'fr - name value 1 translation',
+        'description_translated' => 'fr - description value 1 translation',
+      ],
+      [
+        'tid' => 1,
+        'vid' => 5,
+        'name' => 'name value 1',
+        'description' => 'description value 1',
+        'weight' => 0,
+        'parent' => [0],
+        'property' => 'description',
+        'language' => 'fr',
+        'name_translated' => 'fr - name value 1 translation',
+        'description_translated' => 'fr - description value 1 translation',
+      ],
+      [
+        'tid' => 3,
+        'vid' => 6,
+        'name' => 'name value 3',
+        'description' => 'description value 3',
+        'weight' => 0,
+        'parent' => [0],
+        'property' => 'name',
+        'language' => 'zu',
+        'name_translated' => 'zu - description value 2 translation',
+        'description_translated' => NULL,
+      ],
+    ];
+
+    $tests[0]['expected_count'] = NULL;
+    // Empty configuration will return terms for all vocabularies.
+    $tests[0]['configuration'] = [];
+
+    return $tests;
+  }
+
+}
index 1fc0140fe8b26de0effd1bd15b7e2845b9d5df02..01e460233f0c6199a85c6aee9ae56e3af31bc395 100644 (file)
@@ -110,7 +110,8 @@ function hook_user_cancel_methods_alter(&$methods) {
  *
  * Called by $account->getDisplayName() to allow modules to alter the username
  * that is displayed. Can be used to ensure user privacy in situations where
- * $account->getDisplayName() is too revealing.
+ * $account->getDisplayName() is too revealing. This hook is invoked both for
+ * user entities and the anonymous user session object.
  *
  * @param string|Drupal\Component\Render\MarkupInterface $name
  *   The username that is displayed for a user. If a hook implementation changes
@@ -118,7 +119,14 @@ function hook_user_cancel_methods_alter(&$methods) {
  *   the implementation to ensure the user's name is escaped properly. String
  *   values will be autoescaped.
  * @param \Drupal\Core\Session\AccountInterface $account
- *   The user object on which the operation is being performed.
+ *   The object on which the operation is being performed. This object may be a
+ *   user entity. If the object is an implementation of UserInterface you can
+ *   use instanceof operator before accessing user entity methods. For example:
+ *   @code
+ *   if ($account instanceof UserInterface) {
+ *      // Access user entity methods.
+ *   }
+ *   @endcode
  *
  * @see \Drupal\Core\Session\AccountInterface::getDisplayName()
  * @see sanitization
index 4e3020ca0ab09d9f8d9829e3c6690e3cf8d8ee18..9ed369c045dc8a680f730bb7758f49624f0d6e10 100644 (file)
@@ -105,6 +105,13 @@ abstract class ViewsBlockBase extends BlockBase implements ContainerFactoryPlugi
     return ['views_label' => ''];
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getPreviewFallbackString() {
+    return $this->t('Placeholder for the "@view" views block', ['@view' => $this->view->storage->label()]);
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/web/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_click_sort_ajax.yml b/web/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_click_sort_ajax.yml
new file mode 100644 (file)
index 0000000..514655f
--- /dev/null
@@ -0,0 +1,65 @@
+langcode: en
+status: true
+dependencies: {  }
+id: test_click_sort_ajax
+module: views
+description: ''
+tag: ''
+base_table: views_test_data
+base_field: nid
+core: '8'
+display:
+  default:
+    display_options:
+      use_ajax: true
+      fields:
+        id:
+          id: id
+          table: views_test_data
+          field: id
+          label: ID
+          plugin_id: numeric
+        name:
+          id: name
+          table: views_test_data
+          field: name
+          label: Name
+          plugin_id: string
+        created:
+          id: created
+          table: views_test_data
+          field: created
+          label: created
+          plugin_id: field
+          type: timestamp
+          settings:
+            date_format: medium
+            custom_date_format: ''
+            timezone: ''
+      access:
+        type: none
+      cache:
+        type: tag
+      style:
+        type: table
+        options:
+          info:
+            id:
+              sortable: true
+              default_sort_order: asc
+            name:
+              sortable: true
+              default_sort_order: desc
+            created:
+              sortable: false
+    display_plugin: default
+    display_title: Master
+    id: default
+    position: 0
+  page_1:
+    display_options:
+      path: test_click_sort
+    display_plugin: page
+    display_title: Page
+    id: page_1
+    position: 0
diff --git a/web/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_mini_pager_ajax.yml b/web/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_mini_pager_ajax.yml
new file mode 100644 (file)
index 0000000..c49ef34
--- /dev/null
@@ -0,0 +1,88 @@
+langcode: en
+status: true
+dependencies:
+  module:
+    - node
+    - user
+id: test_mini_pager_ajax
+label: test_mini_pager
+module: views
+description: ''
+tag: ''
+base_table: node_field_data
+base_field: nid
+core: 8.x
+display:
+  default:
+    display_plugin: default
+    id: default
+    display_title: Master
+    position: null
+    display_options:
+      use_ajax: true
+      access:
+        type: perm
+      cache:
+        type: tag
+      query:
+        type: views_query
+      exposed_form:
+        type: basic
+      pager:
+        type: mini
+        options:
+          items_per_page: 3
+          offset: 0
+          id: 0
+          total_pages: null
+          tags:
+            previous: '‹‹ test'
+            next: '›› test'
+          expose:
+            items_per_page: false
+            items_per_page_label: 'Items per page'
+            items_per_page_options: '5, 10, 25, 50'
+            items_per_page_options_all: false
+            items_per_page_options_all_label: '- All -'
+            offset: false
+            offset_label: Offset
+      style:
+        type: default
+      row:
+        type: 'entity:node'
+        options:
+          view_mode: teaser
+      fields:
+        title:
+          id: title
+          table: node_field_data
+          field: title
+          label: ''
+          alter:
+            alter_text: false
+            make_link: false
+            absolute: false
+            trim: false
+            word_boundary: false
+            ellipsis: false
+            strip_tags: false
+            html: false
+          hide_empty: false
+          empty_zero: false
+          plugin_id: field
+          entity_type: node
+          entity_field: title
+      filters: {  }
+      sorts:
+        nid:
+          id: nid
+          table: node_field_data
+          field: nid
+          plugin_id: standard
+          order: ASC
+          entity_type: node
+          entity_field: nid
+      title: test_mini_pager
+      filter_groups:
+        operator: AND
+        groups: {  }
diff --git a/web/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_pager_full_ajax.yml b/web/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_pager_full_ajax.yml
new file mode 100644 (file)
index 0000000..31b4a26
--- /dev/null
@@ -0,0 +1,37 @@
+langcode: en
+status: true
+dependencies:
+  module:
+    - node
+id: test_pager_full_ajax
+label: ''
+module: views
+description: ''
+tag: ''
+base_table: node_field_data
+base_field: nid
+core: '8'
+display:
+  default:
+    display_options:
+      use_ajax: true
+      access:
+        type: none
+      cache:
+        type: tag
+      exposed_form:
+        type: basic
+      pager:
+        options:
+          id: 0
+          items_per_page: 5
+          offset: 0
+        type: full
+      style:
+        type: default
+      row:
+        type: 'entity:node'
+    display_plugin: default
+    display_title: Master
+    id: default
+    position: 0
diff --git a/web/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_user_path.yml b/web/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_user_path.yml
new file mode 100644 (file)
index 0000000..a5e629f
--- /dev/null
@@ -0,0 +1,158 @@
+langcode: en
+status: true
+dependencies:
+  module:
+    - user
+id: test_user_path
+label: 'user break'
+module: views
+description: ''
+tag: ''
+base_table: users_field_data
+base_field: uid
+core: 8.x
+display:
+  default:
+    display_plugin: default
+    id: default
+    display_title: Master
+    position: 0
+    display_options:
+      access:
+        type: perm
+        options:
+          perm: 'access user profiles'
+      cache:
+        type: tag
+        options: {  }
+      query:
+        type: views_query
+        options:
+          disable_sql_rewrite: false
+          distinct: false
+          replica: false
+          query_comment: ''
+          query_tags: {  }
+      exposed_form:
+        type: basic
+        options:
+          submit_button: Toepassen
+          reset_button: false
+          reset_button_label: Reset
+          exposed_sorts_label: 'Sorteren op'
+          expose_sort_order: true
+          sort_asc_label: Oplopend
+          sort_desc_label: Aflopend
+      pager:
+        type: mini
+        options:
+          items_per_page: 10
+          offset: 0
+          id: 0
+          total_pages: null
+          expose:
+            items_per_page: false
+            items_per_page_label: 'Items per pagina'
+            items_per_page_options: '5, 10, 25, 50'
+            items_per_page_options_all: false
+            items_per_page_options_all_label: '- Alle -'
+            offset: false
+            offset_label: Startpunt
+          tags:
+            previous: ‹‹
+            next: ››
+      style:
+        type: default
+      row:
+        type: fields
+      fields:
+        name:
+          id: name
+          table: users_field_data
+          field: name
+          entity_type: user
+          entity_field: name
+          label: ''
+          alter:
+            alter_text: false
+            make_link: false
+            absolute: false
+            trim: false
+            word_boundary: false
+            ellipsis: false
+            strip_tags: false
+            html: false
+          hide_empty: false
+          empty_zero: false
+          plugin_id: field
+          relationship: none
+          group_type: group
+          admin_label: ''
+          exclude: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_alter_empty: true
+          click_sort_column: value
+          type: user_name
+          settings: {  }
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+      filters:
+        status:
+          value: '1'
+          table: users_field_data
+          field: status
+          plugin_id: boolean
+          entity_type: user
+          entity_field: status
+          id: status
+          expose:
+            operator: ''
+          group: 1
+      sorts: {  }
+      title: 'user break'
+      header: {  }
+      footer: {  }
+      empty: {  }
+      relationships: {  }
+      arguments: {  }
+      display_extenders: {  }
+    cache_metadata:
+      max-age: -1
+      contexts:
+        - 'languages:language_content'
+        - 'languages:language_interface'
+        - url.query_args
+        - user.permissions
+      tags: {  }
+  page_1:
+    display_plugin: page
+    id: page_1
+    display_title: Page
+    position: 1
+    display_options:
+      display_extenders: {  }
+      path: user/%
+    cache_metadata:
+      max-age: -1
+      contexts:
+        - 'languages:language_content'
+        - 'languages:language_interface'
+        - url.query_args
+        - user.permissions
+      tags: {  }
diff --git a/web/core/modules/views/tests/src/Functional/UserPathTest.php b/web/core/modules/views/tests/src/Functional/UserPathTest.php
new file mode 100644 (file)
index 0000000..27a0f27
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+
+namespace Drupal\Tests\views\Functional;
+
+/**
+ * Tests overriding user paths using wildcards.
+ *
+ * @group views
+ */
+class UserPathTest extends ViewTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['views', 'user'];
+
+  /**
+   * The test views to use.
+   *
+   * @var array
+   */
+  public static $testViews = ['test_user_path'];
+
+  /**
+   * Tests if the login page is still available when using a wildcard path.
+   */
+  public function testUserLoginPage() {
+    $this->drupalGet('user/login');
+    $this->assertSession()->statusCodeEquals(200);
+  }
+
+}
diff --git a/web/core/modules/views_ui/src/Tests/PreviewTest.php b/web/core/modules/views_ui/src/Tests/PreviewTest.php
deleted file mode 100644 (file)
index c8bd7bd..0000000
+++ /dev/null
@@ -1,386 +0,0 @@
-<?php
-
-namespace Drupal\views_ui\Tests;
-
-use Drupal\Component\Serialization\Json;
-use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
-
-/**
- * Tests the UI preview functionality.
- *
- * @group views_ui
- */
-class PreviewTest extends UITestBase {
-
-  /**
-   * Views used by this test.
-   *
-   * @var array
-   */
-  public static $testViews = ['test_preview', 'test_preview_error', 'test_pager_full', 'test_mini_pager', 'test_click_sort'];
-
-  /**
-   * Tests contextual links in the preview form.
-   */
-  public function testPreviewContextual() {
-    \Drupal::service('module_installer')->install(['contextual']);
-    $this->resetAll();
-
-    $this->drupalGet('admin/structure/views/view/test_preview/edit');
-    $this->assertResponse(200);
-    $this->drupalPostForm(NULL, $edit = [], t('Update preview'));
-
-    $elements = $this->xpath('//div[@id="views-live-preview"]//ul[contains(@class, :ul-class)]/li[contains(@class, :li-class)]', [':ul-class' => 'contextual-links', ':li-class' => 'filter-add']);
-    $this->assertEqual(count($elements), 1, 'The contextual link to add a new field is shown.');
-
-    $this->drupalPostForm(NULL, $edit = ['view_args' => '100'], t('Update preview'));
-
-    // Test that area text and exposed filters are present and rendered.
-    $this->assertFieldByName('id', NULL, 'ID exposed filter field found.');
-    $this->assertText('Test header text', 'Rendered header text found');
-    $this->assertText('Test footer text', 'Rendered footer text found.');
-    $this->assertText('Test empty text', 'Rendered empty text found.');
-  }
-
-  /**
-   * Tests arguments in the preview form.
-   */
-  public function testPreviewUI() {
-    $this->drupalGet('admin/structure/views/view/test_preview/edit');
-    $this->assertResponse(200);
-
-    $this->drupalPostForm(NULL, $edit = [], t('Update preview'));
-
-    $elements = $this->xpath('//div[@class = "view-content"]/div[contains(@class, views-row)]');
-    $this->assertEqual(count($elements), 5);
-
-    // Filter just the first result.
-    $this->drupalPostForm(NULL, $edit = ['view_args' => '1'], t('Update preview'));
-
-    $elements = $this->xpath('//div[@class = "view-content"]/div[contains(@class, views-row)]');
-    $this->assertEqual(count($elements), 1);
-
-    // Filter for no results.
-    $this->drupalPostForm(NULL, $edit = ['view_args' => '100'], t('Update preview'));
-
-    $elements = $this->xpath('//div[@class = "view-content"]/div[contains(@class, views-row)]');
-    $this->assertEqual(count($elements), 0);
-
-    // Test that area text and exposed filters are present and rendered.
-    $this->assertFieldByName('id', NULL, 'ID exposed filter field found.');
-    $this->assertText('Test header text', 'Rendered header text found');
-    $this->assertText('Test footer text', 'Rendered footer text found.');
-    $this->assertText('Test empty text', 'Rendered empty text found.');
-
-    // Test feed preview.
-    $view = [];
-    $view['label'] = $this->randomMachineName(16);
-    $view['id'] = strtolower($this->randomMachineName(16));
-    $view['page[create]'] = 1;
-    $view['page[title]'] = $this->randomMachineName(16);
-    $view['page[path]'] = $this->randomMachineName(16);
-    $view['page[feed]'] = 1;
-    $view['page[feed_properties][path]'] = $this->randomMachineName(16);
-    $this->drupalPostForm('admin/structure/views/add', $view, t('Save and edit'));
-    $this->clickLink(t('Feed'));
-    $this->drupalPostForm(NULL, [], t('Update preview'));
-    $result = $this->xpath('//div[@id="views-live-preview"]/pre');
-    $this->assertTrue(strpos($result[0], '<title>' . $view['page[title]'] . '</title>'), 'The Feed RSS preview was rendered.');
-
-    // Test the non-default UI display options.
-    // Statistics only, no query.
-    $settings = \Drupal::configFactory()->getEditable('views.settings');
-    $settings->set('ui.show.performance_statistics', TRUE)->save();
-    $this->drupalGet('admin/structure/views/view/test_preview/edit');
-    $this->drupalPostForm(NULL, $edit = ['view_args' => '100'], t('Update preview'));
-    $this->assertText(t('Query build time'));
-    $this->assertText(t('Query execute time'));
-    $this->assertText(t('View render time'));
-    $this->assertNoRaw('<strong>Query</strong>');
-
-    // Statistics and query.
-    $settings->set('ui.show.sql_query.enabled', TRUE)->save();
-    $this->drupalPostForm(NULL, $edit = ['view_args' => '100'], t('Update preview'));
-    $this->assertText(t('Query build time'));
-    $this->assertText(t('Query execute time'));
-    $this->assertText(t('View render time'));
-    $this->assertRaw('<strong>Query</strong>');
-    $query_string = <<<SQL
-SELECT views_test_data.name AS views_test_data_name
-FROM
-{views_test_data} views_test_data
-WHERE (views_test_data.id = '100')
-SQL;
-    $this->assertEscaped($query_string);
-
-    // Test that the statistics and query are rendered above the preview.
-    $this->assertTrue(strpos($this->getRawContent(), 'views-query-info') < strpos($this->getRawContent(), 'view-test-preview'), 'Statistics shown above the preview.');
-
-    // Test that statistics and query rendered below the preview.
-    $settings->set('ui.show.sql_query.where', 'below')->save();
-    $this->drupalPostForm(NULL, $edit = ['view_args' => '100'], t('Update preview'));
-    $this->assertTrue(strpos($this->getRawContent(), 'view-test-preview') < strpos($this->getRawContent(), 'views-query-info'), 'Statistics shown below the preview.');
-
-    // Test that the preview title isn't double escaped.
-    $this->drupalPostForm("admin/structure/views/nojs/display/test_preview/default/title", $edit = ['title' => 'Double & escaped'], t('Apply'));
-    $this->drupalPostForm(NULL, [], t('Update preview'));
-    $elements = $this->xpath('//div[@id="views-live-preview"]/div[contains(@class, views-query-info)]//td[text()=:text]', [':text' => t('Double & escaped')]);
-    $this->assertEqual(1, count($elements));
-  }
-
-  /**
-   * Tests the taxonomy term preview AJAX.
-   *
-   * This tests a specific regression in the taxonomy term view preview.
-   *
-   * @see https://www.drupal.org/node/2452659
-   */
-  public function testTaxonomyAJAX() {
-    \Drupal::service('module_installer')->install(['taxonomy']);
-    $this->getPreviewAJAX('taxonomy_term', 'page_1', 0);
-  }
-
-  /**
-   * Tests pagers in the preview form.
-   */
-  public function testPreviewWithPagersUI() {
-
-    // Create 11 nodes and make sure that everyone is returned.
-    $this->drupalCreateContentType(['type' => 'page']);
-    for ($i = 0; $i < 11; $i++) {
-      $this->drupalCreateNode();
-    }
-
-    // Test Full Pager.
-    $this->getPreviewAJAX('test_pager_full', 'default', 5);
-
-    // Test that the pager is present and rendered.
-    $elements = $this->xpath('//ul[contains(@class, :class)]/li', [':class' => 'pager__items']);
-    $this->assertTrue(!empty($elements), 'Full pager found.');
-
-    // Verify elements and links to pages.
-    // We expect to find 5 elements: current page == 1, links to pages 2 and
-    // and 3, links to 'next >' and 'last >>' pages.
-    $this->assertClass($elements[0], 'is-active', 'Element for current page has .is-active class.');
-    $this->assertTrue($elements[0]->a, 'Element for current page has link.');
-
-    $this->assertClass($elements[1], 'pager__item', 'Element for page 2 has .pager__item class.');
-    $this->assertTrue($elements[1]->a, 'Link to page 2 found.');
-
-    $this->assertClass($elements[2], 'pager__item', 'Element for page 3 has .pager__item class.');
-    $this->assertTrue($elements[2]->a, 'Link to page 3 found.');
-
-    $this->assertClass($elements[3], 'pager__item--next', 'Element for next page has .pager__item--next class.');
-    $this->assertTrue($elements[3]->a, 'Link to next page found.');
-
-    $this->assertClass($elements[4], 'pager__item--last', 'Element for last page has .pager__item--last class.');
-    $this->assertTrue($elements[4]->a, 'Link to last page found.');
-
-    // Navigate to next page.
-    $elements = $this->xpath('//li[contains(@class, :class)]/a', [':class' => 'pager__item--next']);
-    $this->clickPreviewLinkAJAX($elements[0]['href'], 5);
-
-    // Test that the pager is present and rendered.
-    $elements = $this->xpath('//ul[contains(@class, :class)]/li', [':class' => 'pager__items']);
-    $this->assertTrue(!empty($elements), 'Full pager found.');
-
-    // Verify elements and links to pages.
-    // We expect to find 7 elements: links to '<< first' and '< previous'
-    // pages, link to page 1, current page == 2, link to page 3 and links
-    // to 'next >' and 'last >>' pages.
-    $this->assertClass($elements[0], 'pager__item--first', 'Element for first page has .pager__item--first class.');
-    $this->assertTrue($elements[0]->a, 'Link to first page found.');
-
-    $this->assertClass($elements[1], 'pager__item--previous', 'Element for previous page has .pager__item--previous class.');
-    $this->assertTrue($elements[1]->a, 'Link to previous page found.');
-
-    $this->assertClass($elements[2], 'pager__item', 'Element for page 1 has .pager__item class.');
-    $this->assertTrue($elements[2]->a, 'Link to page 1 found.');
-
-    $this->assertClass($elements[3], 'is-active', 'Element for current page has .is-active class.');
-    $this->assertTrue($elements[3]->a, 'Element for current page has link.');
-
-    $this->assertClass($elements[4], 'pager__item', 'Element for page 3 has .pager__item class.');
-    $this->assertTrue($elements[4]->a, 'Link to page 3 found.');
-
-    $this->assertClass($elements[5], 'pager__item--next', 'Element for next page has .pager__item--next class.');
-    $this->assertTrue($elements[5]->a, 'Link to next page found.');
-
-    $this->assertClass($elements[6], 'pager__item--last', 'Element for last page has .pager__item--last class.');
-    $this->assertTrue($elements[6]->a, 'Link to last page found.');
-
-    // Test Mini Pager.
-    $this->getPreviewAJAX('test_mini_pager', 'default', 3);
-
-    // Test that the pager is present and rendered.
-    $elements = $this->xpath('//ul[contains(@class, :class)]/li', [':class' => 'pager__items']);
-    $this->assertTrue(!empty($elements), 'Mini pager found.');
-
-    // Verify elements and links to pages.
-    // We expect to find current pages element with no link, next page element
-    // with a link, and not to find previous page element.
-    $this->assertClass($elements[0], 'is-active', 'Element for current page has .is-active class.');
-
-    $this->assertClass($elements[1], 'pager__item--next', 'Element for next page has .pager__item--next class.');
-    $this->assertTrue($elements[1]->a, 'Link to next page found.');
-
-    // Navigate to next page.
-    $elements = $this->xpath('//li[contains(@class, :class)]/a', [':class' => 'pager__item--next']);
-    $this->clickPreviewLinkAJAX($elements[0]['href'], 3);
-
-    // Test that the pager is present and rendered.
-    $elements = $this->xpath('//ul[contains(@class, :class)]/li', [':class' => 'pager__items']);
-    $this->assertTrue(!empty($elements), 'Mini pager found.');
-
-    // Verify elements and links to pages.
-    // We expect to find 3 elements: previous page with a link, current
-    // page with no link, and next page with a link.
-    $this->assertClass($elements[0], 'pager__item--previous', 'Element for previous page has .pager__item--previous class.');
-    $this->assertTrue($elements[0]->a, 'Link to previous page found.');
-
-    $this->assertClass($elements[1], 'is-active', 'Element for current page has .is-active class.');
-    $this->assertFalse(isset($elements[1]->a), 'Element for current page has no link.');
-
-    $this->assertClass($elements[2], 'pager__item--next', 'Element for next page has .pager__item--next class.');
-    $this->assertTrue($elements[2]->a, 'Link to next page found.');
-  }
-
-  /**
-   * Tests the additional information query info area.
-   */
-  public function testPreviewAdditionalInfo() {
-    \Drupal::service('module_installer')->install(['views_ui_test']);
-    $this->resetAll();
-
-    $this->drupalGet('admin/structure/views/view/test_preview/edit');
-    $this->assertResponse(200);
-
-    $this->drupalPostForm(NULL, $edit = [], t('Update preview'));
-
-    // Check for implementation of hook_views_preview_info_alter().
-    // @see views_ui_test.module
-    $elements = $this->xpath('//div[@id="views-live-preview"]/div[contains(@class, views-query-info)]//td[text()=:text]', [':text' => t('Test row count')]);
-    $this->assertEqual(count($elements), 1, 'Views Query Preview Info area altered.');
-    // Check that additional assets are attached.
-    $this->assertTrue(strpos($this->getDrupalSettings()['ajaxPageState']['libraries'], 'views_ui_test/views_ui_test.test') !== FALSE, 'Attached library found.');
-    $this->assertRaw('css/views_ui_test.test.css', 'Attached CSS asset found.');
-  }
-
-  /**
-   * Tests view validation error messages in the preview.
-   */
-  public function testPreviewError() {
-    $this->drupalGet('admin/structure/views/view/test_preview_error/edit');
-    $this->assertResponse(200);
-
-    $this->drupalPostForm(NULL, $edit = [], t('Update preview'));
-
-    $this->assertText('Unable to preview due to validation errors.', 'Preview error text found.');
-  }
-
-  /**
-   * Tests the link to sort in the preview form.
-   */
-  public function testPreviewSortLink() {
-
-    // Get the preview.
-    $this->getPreviewAJAX('test_click_sort', 'page_1', 0);
-
-    // Test that the header label is present.
-    $elements = $this->xpath('//th[contains(@class, :class)]/a', [':class' => 'views-field views-field-name']);
-    $this->assertTrue(!empty($elements), 'The header label is present.');
-
-    // Verify link.
-    $this->assertLinkByHref('preview/page_1?_wrapper_format=drupal_ajax&order=name&sort=desc', 0, 'The output URL is as expected.');
-
-    // Click link to sort.
-    $this->clickPreviewLinkAJAX($elements[0]['href'], 0);
-
-    // Test that the header label is present.
-    $elements = $this->xpath('//th[contains(@class, :class)]/a', [':class' => 'views-field views-field-name is-active']);
-    $this->assertTrue(!empty($elements), 'The header label is present.');
-
-    // Verify link.
-    $this->assertLinkByHref('preview/page_1?_wrapper_format=drupal_ajax&order=name&sort=asc', 0, 'The output URL is as expected.');
-  }
-
-  /**
-   * Get the preview form and force an AJAX preview update.
-   *
-   * @param string $view_name
-   *   The view to test.
-   * @param string $panel_id
-   *   The view panel to test.
-   * @param int $row_count
-   *   The expected number of rows in the preview.
-   */
-  protected function getPreviewAJAX($view_name, $panel_id, $row_count) {
-    $this->drupalGet('admin/structure/views/view/' . $view_name . '/preview/' . $panel_id);
-    $result = $this->drupalPostAjaxForm(NULL, [], ['op' => t('Update preview')]);
-    $this->assertPreviewAJAX($result, $row_count);
-  }
-
-  /**
-   * Mimic clicking on a preview link.
-   *
-   * @param string $url
-   *   The url to navigate to.
-   * @param int $row_count
-   *   The expected number of rows in the preview.
-   */
-  protected function clickPreviewLinkAJAX($url, $row_count) {
-    $content = $this->content;
-    $drupal_settings = $this->drupalSettings;
-    $ajax_settings = [
-      'wrapper' => 'views-preview-wrapper',
-      'method' => 'replaceWith',
-    ];
-    $url = $this->getAbsoluteUrl($url);
-    $post = ['js' => 'true'] + $this->getAjaxPageStatePostData();
-    $result = Json::decode($this->drupalPost($url, '', $post, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]));
-    if (!empty($result)) {
-      $this->drupalProcessAjaxResponse($content, $result, $ajax_settings, $drupal_settings);
-    }
-    $this->assertPreviewAJAX($result, $row_count);
-  }
-
-  /**
-   * Assert that the AJAX response contains expected data.
-   *
-   * @param array $result
-   *   An array of AJAX commands.
-   * @param int $row_count
-   *   The expected number of rows in the preview.
-   */
-  protected function assertPreviewAJAX($result, $row_count) {
-    // Has AJAX callback replied with an insert command? If so, we can
-    // assume that the page content was updated with AJAX returned data.
-    $result_commands = [];
-    foreach ($result as $command) {
-      $result_commands[$command['command']] = $command;
-    }
-    $this->assertTrue(isset($result_commands['insert']), 'AJAX insert command received.');
-
-    // Test if preview contains the expected number of rows.
-    $elements = $this->xpath('//div[@class = "view-content"]/div[contains(@class, views-row)]');
-    $this->assertEqual(count($elements), $row_count, 'Expected items found on page.');
-  }
-
-  /**
-   * Asserts that an element has a given class.
-   *
-   * @param \SimpleXMLElement $element
-   *   The element to test.
-   * @param string $class
-   *   The class to assert.
-   * @param string $message
-   *   (optional) A verbose message to output.
-   */
-  protected function assertClass(\SimpleXMLElement $element, $class, $message = NULL) {
-    if (!isset($message)) {
-      $message = "Class .$class found.";
-    }
-    $this->assertTrue(strpos($element['class'], $class) !== FALSE, $message);
-  }
-
-}
diff --git a/web/core/modules/views_ui/tests/src/Functional/PreviewTest.php b/web/core/modules/views_ui/tests/src/Functional/PreviewTest.php
new file mode 100644 (file)
index 0000000..b76e464
--- /dev/null
@@ -0,0 +1,161 @@
+<?php
+
+namespace Drupal\Tests\views_ui\Functional;
+
+/**
+ * Tests the UI preview functionality.
+ *
+ * @group views_ui
+ */
+class PreviewTest extends UITestBase {
+
+  /**
+   * Views used by this test.
+   *
+   * @var array
+   */
+  public static $testViews = ['test_preview', 'test_preview_error', 'test_pager_full', 'test_mini_pager', 'test_click_sort'];
+
+  /**
+   * Tests contextual links in the preview form.
+   */
+  public function testPreviewContextual() {
+    \Drupal::service('module_installer')->install(['contextual']);
+    $this->resetAll();
+
+    $this->drupalGet('admin/structure/views/view/test_preview/edit');
+    $this->assertResponse(200);
+    $this->drupalPostForm(NULL, $edit = [], t('Update preview'));
+
+    $elements = $this->xpath('//div[@id="views-live-preview"]//ul[contains(@class, :ul-class)]/li[contains(@class, :li-class)]', [':ul-class' => 'contextual-links', ':li-class' => 'filter-add']);
+    $this->assertEqual(count($elements), 1, 'The contextual link to add a new field is shown.');
+
+    $this->drupalPostForm(NULL, $edit = ['view_args' => '100'], t('Update preview'));
+
+    // Test that area text and exposed filters are present and rendered.
+    $this->assertFieldByName('id', NULL, 'ID exposed filter field found.');
+    $this->assertText('Test header text', 'Rendered header text found');
+    $this->assertText('Test footer text', 'Rendered footer text found.');
+    $this->assertText('Test empty text', 'Rendered empty text found.');
+  }
+
+  /**
+   * Tests arguments in the preview form.
+   */
+  public function testPreviewUI() {
+    $this->drupalGet('admin/structure/views/view/test_preview/edit');
+    $this->assertResponse(200);
+
+    $this->drupalPostForm(NULL, $edit = [], t('Update preview'));
+
+    $elements = $this->xpath('//div[@class = "view-content"]/div[contains(@class, views-row)]');
+    $this->assertEqual(count($elements), 5);
+
+    // Filter just the first result.
+    $this->drupalPostForm(NULL, $edit = ['view_args' => '1'], t('Update preview'));
+
+    $elements = $this->xpath('//div[@class = "view-content"]/div[contains(@class, views-row)]');
+    $this->assertEqual(count($elements), 1);
+
+    // Filter for no results.
+    $this->drupalPostForm(NULL, $edit = ['view_args' => '100'], t('Update preview'));
+
+    $elements = $this->xpath('//div[@class = "view-content"]/div[contains(@class, views-row)]');
+    $this->assertEqual(count($elements), 0);
+
+    // Test that area text and exposed filters are present and rendered.
+    $this->assertFieldByName('id', NULL, 'ID exposed filter field found.');
+    $this->assertText('Test header text', 'Rendered header text found');
+    $this->assertText('Test footer text', 'Rendered footer text found.');
+    $this->assertText('Test empty text', 'Rendered empty text found.');
+
+    // Test feed preview.
+    $view = [];
+    $view['label'] = $this->randomMachineName(16);
+    $view['id'] = strtolower($this->randomMachineName(16));
+    $view['page[create]'] = 1;
+    $view['page[title]'] = $this->randomMachineName(16);
+    $view['page[path]'] = $this->randomMachineName(16);
+    $view['page[feed]'] = 1;
+    $view['page[feed_properties][path]'] = $this->randomMachineName(16);
+    $this->drupalPostForm('admin/structure/views/add', $view, t('Save and edit'));
+    $this->clickLink(t('Feed'));
+    $this->drupalPostForm(NULL, [], t('Update preview'));
+    $result = $this->xpath('//div[@id="views-live-preview"]/pre');
+    $this->assertTrue(strpos($result[0]->getText(), '<title>' . $view['page[title]'] . '</title>'), 'The Feed RSS preview was rendered.');
+
+    // Test the non-default UI display options.
+    // Statistics only, no query.
+    $settings = \Drupal::configFactory()->getEditable('views.settings');
+    $settings->set('ui.show.performance_statistics', TRUE)->save();
+    $this->drupalGet('admin/structure/views/view/test_preview/edit');
+    $this->drupalPostForm(NULL, $edit = ['view_args' => '100'], t('Update preview'));
+    $this->assertText(t('Query build time'));
+    $this->assertText(t('Query execute time'));
+    $this->assertText(t('View render time'));
+    $this->assertNoRaw('<strong>Query</strong>');
+
+    // Statistics and query.
+    $settings->set('ui.show.sql_query.enabled', TRUE)->save();
+    $this->drupalPostForm(NULL, $edit = ['view_args' => '100'], t('Update preview'));
+    $this->assertText(t('Query build time'));
+    $this->assertText(t('Query execute time'));
+    $this->assertText(t('View render time'));
+    $this->assertRaw('<strong>Query</strong>');
+    $query_string = <<<SQL
+SELECT views_test_data.name AS views_test_data_name
+FROM
+{views_test_data} views_test_data
+WHERE (views_test_data.id = '100')
+SQL;
+    $this->assertEscaped($query_string);
+
+    // Test that the statistics and query are rendered above the preview.
+    $this->assertTrue(strpos($this->getSession()->getPage()->getContent(), 'views-query-info') < strpos($this->getSession()->getPage()->getContent(), 'view-test-preview'), 'Statistics shown above the preview.');
+
+    // Test that statistics and query rendered below the preview.
+    $settings->set('ui.show.sql_query.where', 'below')->save();
+    $this->drupalPostForm(NULL, $edit = ['view_args' => '100'], t('Update preview'));
+    $this->assertTrue(strpos($this->getSession()->getPage()->getContent(), 'view-test-preview') < strpos($this->getSession()->getPage()->getContent(), 'views-query-info'), 'Statistics shown below the preview.');
+
+    // Test that the preview title isn't double escaped.
+    $this->drupalPostForm("admin/structure/views/nojs/display/test_preview/default/title", $edit = ['title' => 'Double & escaped'], t('Apply'));
+    $this->drupalPostForm(NULL, [], t('Update preview'));
+    $elements = $this->xpath('//div[@id="views-live-preview"]/div[contains(@class, views-query-info)]//td[text()=:text]', [':text' => 'Double & escaped']);
+    $this->assertEqual(1, count($elements));
+  }
+
+  /**
+   * Tests the additional information query info area.
+   */
+  public function testPreviewAdditionalInfo() {
+    \Drupal::service('module_installer')->install(['views_ui_test']);
+    $this->resetAll();
+
+    $this->drupalGet('admin/structure/views/view/test_preview/edit');
+    $this->assertResponse(200);
+
+    $this->drupalPostForm(NULL, $edit = [], t('Update preview'));
+
+    // Check for implementation of hook_views_preview_info_alter().
+    // @see views_ui_test.module
+    $elements = $this->xpath('//div[@id="views-live-preview"]/div[contains(@class, views-query-info)]//td[text()=:text]', [':text' => 'Test row count']);
+    $this->assertEqual(count($elements), 1, 'Views Query Preview Info area altered.');
+    // Check that additional assets are attached.
+    $this->assertTrue(strpos($this->getDrupalSettings()['ajaxPageState']['libraries'], 'views_ui_test/views_ui_test.test') !== FALSE, 'Attached library found.');
+    $this->assertRaw('css/views_ui_test.test.css', 'Attached CSS asset found.');
+  }
+
+  /**
+   * Tests view validation error messages in the preview.
+   */
+  public function testPreviewError() {
+    $this->drupalGet('admin/structure/views/view/test_preview_error/edit');
+    $this->assertResponse(200);
+
+    $this->drupalPostForm(NULL, $edit = [], t('Update preview'));
+
+    $this->assertText('Unable to preview due to validation errors.', 'Preview error text found.');
+  }
+
+}
diff --git a/web/core/modules/views_ui/tests/src/FunctionalJavascript/PreviewTest.php b/web/core/modules/views_ui/tests/src/FunctionalJavascript/PreviewTest.php
new file mode 100644 (file)
index 0000000..489a374
--- /dev/null
@@ -0,0 +1,304 @@
+<?php
+
+namespace Drupal\Tests\views_ui\FunctionalJavascript;
+
+use Behat\Mink\Element\NodeElement;
+use Drupal\Core\Database\Database;
+use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use Drupal\views\Tests\ViewTestData;
+
+/**
+ * Tests the UI preview functionality.
+ *
+ * @group views_ui
+ */
+class PreviewTest extends WebDriverTestBase {
+
+  /**
+   * Views used by this test.
+   *
+   * @var array
+   */
+  public static $testViews = ['test_preview', 'test_pager_full_ajax', 'test_mini_pager_ajax', 'test_click_sort_ajax'];
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'node',
+    'views',
+    'views_ui',
+    'views_test_config',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    ViewTestData::createTestViews(self::class, ['views_test_config']);
+
+    $this->enableViewsTestModule();
+
+    $admin_user = $this->drupalCreateUser([
+      'administer site configuration',
+      'administer views',
+      'administer nodes',
+      'access content overview',
+    ]);
+
+    // Disable automatic live preview to make the sequence of calls clearer.
+    \Drupal::configFactory()->getEditable('views.settings')->set('ui.always_live_preview', FALSE)->save();
+    $this->drupalLogin($admin_user);
+  }
+
+  /**
+   * Sets up the views_test_data.module.
+   *
+   * Because the schema of views_test_data.module is dependent on the test
+   * using it, it cannot be enabled normally.
+   */
+  protected function enableViewsTestModule() {
+    // Define the schema and views data variable before enabling the test module.
+    \Drupal::state()->set('views_test_data_schema', $this->schemaDefinition());
+    \Drupal::state()->set('views_test_data_views_data', $this->viewsData());
+
+    \Drupal::service('module_installer')->install(['views_test_data']);
+    $this->resetAll();
+    $this->rebuildContainer();
+    $this->container->get('module_handler')->reload();
+
+    // Load the test dataset.
+    $data_set = $this->dataSet();
+    $query = Database::getConnection()->insert('views_test_data')
+      ->fields(array_keys($data_set[0]));
+    foreach ($data_set as $record) {
+      $query->values($record);
+    }
+    $query->execute();
+  }
+
+  /**
+   * Returns the schema definition.
+   *
+   * @internal
+   */
+  protected function schemaDefinition() {
+    return ViewTestData::schemaDefinition();
+  }
+
+  /**
+   * Returns the views data definition.
+   */
+  protected function viewsData() {
+    return ViewTestData::viewsData();
+  }
+
+  /**
+   * Returns a very simple test dataset.
+   */
+  protected function dataSet() {
+    return ViewTestData::dataSet();
+  }
+
+  /**
+   * Tests the taxonomy term preview AJAX.
+   *
+   * This tests a specific regression in the taxonomy term view preview.
+   *
+   * @see https://www.drupal.org/node/2452659
+   */
+  public function testTaxonomyAJAX() {
+    \Drupal::service('module_installer')->install(['taxonomy']);
+    $this->getPreviewAJAX('taxonomy_term', 'page_1', 0);
+  }
+
+  /**
+   * Tests pagers in the preview form.
+   */
+  public function testPreviewWithPagersUI() {
+    // Create 11 nodes and make sure that everyone is returned.
+    $this->drupalCreateContentType(['type' => 'page']);
+    for ($i = 0; $i < 11; $i++) {
+      $this->drupalCreateNode();
+    }
+
+    // Test Full Pager.
+    $this->getPreviewAJAX('test_pager_full_ajax', 'default', 5);
+
+    // Test that the pager is present and rendered.
+    $elements = $this->xpath('//ul[contains(@class, :class)]/li', [':class' => 'pager__items']);
+    $this->assertTrue(!empty($elements), 'Full pager found.');
+
+    // Verify elements and links to pages.
+    // We expect to find 5 elements: current page == 1, links to pages 2 and
+    // and 3, links to 'next >' and 'last >>' pages.
+    $this->assertClass($elements[0], 'is-active', 'Element for current page has .is-active class.');
+    $this->assertTrue($elements[0]->find('css', 'a'), 'Element for current page has link.');
+
+    $this->assertClass($elements[1], 'pager__item', 'Element for page 2 has .pager__item class.');
+    $this->assertTrue($elements[1]->find('css', 'a'), 'Link to page 2 found.');
+
+    $this->assertClass($elements[2], 'pager__item', 'Element for page 3 has .pager__item class.');
+    $this->assertTrue($elements[2]->find('css', 'a'), 'Link to page 3 found.');
+
+    $this->assertClass($elements[3], 'pager__item--next', 'Element for next page has .pager__item--next class.');
+    $this->assertTrue($elements[3]->find('css', 'a'), 'Link to next page found.');
+
+    $this->assertClass($elements[4], 'pager__item--last', 'Element for last page has .pager__item--last class.');
+    $this->assertTrue($elements[4]->find('css', 'a'), 'Link to last page found.');
+
+    // Navigate to next page.
+    $elements = $this->xpath('//li[contains(@class, :class)]/a', [':class' => 'pager__item--next']);
+    $this->clickPreviewLinkAJAX($elements[0], 5);
+
+    // Test that the pager is present and rendered.
+    $elements = $this->xpath('//ul[contains(@class, :class)]/li', [':class' => 'pager__items']);
+    $this->assertTrue(!empty($elements), 'Full pager found.');
+
+    // Verify elements and links to pages.
+    // We expect to find 7 elements: links to '<< first' and '< previous'
+    // pages, link to page 1, current page == 2, link to page 3 and links
+    // to 'next >' and 'last >>' pages.
+    $this->assertClass($elements[0], 'pager__item--first', 'Element for first page has .pager__item--first class.');
+    $this->assertTrue($elements[0]->find('css', 'a'), 'Link to first page found.');
+
+    $this->assertClass($elements[1], 'pager__item--previous', 'Element for previous page has .pager__item--previous class.');
+    $this->assertTrue($elements[1]->find('css', 'a'), 'Link to previous page found.');
+
+    $this->assertClass($elements[2], 'pager__item', 'Element for page 1 has .pager__item class.');
+    $this->assertTrue($elements[2]->find('css', 'a'), 'Link to page 1 found.');
+
+    $this->assertClass($elements[3], 'is-active', 'Element for current page has .is-active class.');
+    $this->assertTrue($elements[3]->find('css', 'a'), 'Element for current page has link.');
+
+    $this->assertClass($elements[4], 'pager__item', 'Element for page 3 has .pager__item class.');
+    $this->assertTrue($elements[4]->find('css', 'a'), 'Link to page 3 found.');
+
+    $this->assertClass($elements[5], 'pager__item--next', 'Element for next page has .pager__item--next class.');
+    $this->assertTrue($elements[5]->find('css', 'a'), 'Link to next page found.');
+
+    $this->assertClass($elements[6], 'pager__item--last', 'Element for last page has .pager__item--last class.');
+    $this->assertTrue($elements[6]->find('css', 'a'), 'Link to last page found.');
+
+    // Test Mini Pager.
+    $this->getPreviewAJAX('test_mini_pager_ajax', 'default', 3);
+
+    // Test that the pager is present and rendered.
+    $elements = $this->xpath('//ul[contains(@class, :class)]/li', [':class' => 'pager__items']);
+    $this->assertTrue(!empty($elements), 'Mini pager found.');
+
+    // Verify elements and links to pages.
+    // We expect to find current pages element with no link, next page element
+    // with a link, and not to find previous page element.
+    $this->assertClass($elements[0], 'is-active', 'Element for current page has .is-active class.');
+
+    $this->assertClass($elements[1], 'pager__item--next', 'Element for next page has .pager__item--next class.');
+    $this->assertTrue($elements[1]->find('css', 'a'), 'Link to next page found.');
+
+    // Navigate to next page.
+    $elements = $this->xpath('//li[contains(@class, :class)]/a', [':class' => 'pager__item--next']);
+    $this->clickPreviewLinkAJAX($elements[0], 3);
+
+    // Test that the pager is present and rendered.
+    $elements = $this->xpath('//ul[contains(@class, :class)]/li', [':class' => 'pager__items']);
+    $this->assertTrue(!empty($elements), 'Mini pager found.');
+
+    // Verify elements and links to pages.
+    // We expect to find 3 elements: previous page with a link, current
+    // page with no link, and next page with a link.
+    $this->assertClass($elements[0], 'pager__item--previous', 'Element for previous page has .pager__item--previous class.');
+    $this->assertTrue($elements[0]->find('css', 'a'), 'Link to previous page found.');
+
+    $this->assertClass($elements[1], 'is-active', 'Element for current page has .is-active class.');
+    $this->assertEmpty($elements[1]->find('css', 'a'), 'Element for current page has no link.');
+
+    $this->assertClass($elements[2], 'pager__item--next', 'Element for next page has .pager__item--next class.');
+    $this->assertTrue($elements[2]->find('css', 'a'), 'Link to next page found.');
+  }
+
+  /**
+   * Tests the link to sort in the preview form.
+   */
+  public function testPreviewSortLink() {
+    // Get the preview.
+    $this->getPreviewAJAX('test_click_sort_ajax', 'page_1', 0);
+
+    // Test that the header label is present.
+    $elements = $this->xpath('//th[contains(@class, :class)]/a', [':class' => 'views-field views-field-name']);
+    $this->assertTrue(!empty($elements), 'The header label is present.');
+
+    // Verify link.
+    $this->assertLinkByHref('preview/page_1?_wrapper_format=drupal_ajax&order=name&sort=desc', 0, 'The output URL is as expected.');
+
+    // Click link to sort.
+    $elements[0]->click();
+    $sort_link = $this->assertSession()->waitForElement('xpath', '//th[contains(@class, \'views-field views-field-name is-active\')]/a');
+
+    $this->assertNotEmpty($sort_link);
+
+    // Verify link.
+    $this->assertLinkByHref('preview/page_1?_wrapper_format=drupal_ajax&order=name&sort=asc', 0, 'The output URL is as expected.');
+  }
+
+  /**
+   * Get the preview form and force an AJAX preview update.
+   *
+   * @param string $view_name
+   *   The view to test.
+   * @param string $panel_id
+   *   The view panel to test.
+   * @param int $row_count
+   *   The expected number of rows in the preview.
+   */
+  protected function getPreviewAJAX($view_name, $panel_id, $row_count) {
+    $this->drupalGet('admin/structure/views/view/' . $view_name . '/edit/' . $panel_id);
+    $this->getSession()->getPage()->pressButton('Update preview');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->assertPreviewAJAX($row_count);
+  }
+
+  /**
+   * Click on a preview link.
+   *
+   * @param \Behat\Mink\Element\NodeElement $element
+   *   The element to click.
+   * @param int $row_count
+   *   The expected number of rows in the preview.
+   */
+  protected function clickPreviewLinkAJAX(NodeElement $element, $row_count) {
+    $element->click();
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->assertPreviewAJAX($row_count);
+  }
+
+  /**
+   * Assert that the preview contains expected data.
+   *
+   * @param int $row_count
+   *   The expected number of rows in the preview.
+   */
+  protected function assertPreviewAJAX($row_count) {
+    $elements = $this->getSession()->getPage()->findAll('css', '.view-content .views-row');
+    $this->assertCount($row_count, $elements, 'Expected items found on page.');
+  }
+
+  /**
+   * Asserts that an element has a given class.
+   *
+   * @param \Behat\Mink\Element\NodeElement $element
+   *   The element to test.
+   * @param string $class
+   *   The class to assert.
+   * @param string $message
+   *   (optional) A verbose message to output.
+   */
+  protected function assertClass(NodeElement $element, $class, $message = NULL) {
+    if (!isset($message)) {
+      $message = "Class .$class found.";
+    }
+    $this->assertTrue(strpos($element->getAttribute('class'), $class) !== FALSE, $message);
+  }
+
+}
index 3b91f84e519ef75a99c4462b7cfca51a7dd16a48..5495c7fa4e7c09c31db6484bc881ad7126279341 100644 (file)
@@ -70,4 +70,18 @@ class EntityTypeInfo implements ContainerInjectionInterface {
     }
   }
 
+  /**
+   * Alters field plugin definitions.
+   *
+   * @param array[] $definitions
+   *   An array of field plugin definitions.
+   *
+   * @see hook_field_info_alter()
+   */
+  public function fieldInfoAlter(&$definitions) {
+    if (isset($definitions['entity_reference'])) {
+      $definitions['entity_reference']['constraints']['EntityReferenceSupportedNewEntities'] = [];
+    }
+  }
+
 }
diff --git a/web/core/modules/workspaces/src/Plugin/Validation/Constraint/EntityReferenceSupportedNewEntitiesConstraint.php b/web/core/modules/workspaces/src/Plugin/Validation/Constraint/EntityReferenceSupportedNewEntitiesConstraint.php
new file mode 100644 (file)
index 0000000..894e343
--- /dev/null
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\workspaces\Plugin\Validation\Constraint;
+
+use Symfony\Component\Validator\Constraint;
+
+/**
+ * The entity reference supported new entities constraint.
+ *
+ * @Constraint(
+ *   id = "EntityReferenceSupportedNewEntities",
+ *   label = @Translation("Entity Reference Supported New Entities", context = "Validation"),
+ * )
+ */
+class EntityReferenceSupportedNewEntitiesConstraint extends Constraint {
+
+  /**
+   * The default violation message.
+   *
+   * @var string
+   */
+  public $message = '%collection_label can only be created in the default workspace.';
+
+}
diff --git a/web/core/modules/workspaces/src/Plugin/Validation/Constraint/EntityReferenceSupportedNewEntitiesConstraintValidator.php b/web/core/modules/workspaces/src/Plugin/Validation/Constraint/EntityReferenceSupportedNewEntitiesConstraintValidator.php
new file mode 100644 (file)
index 0000000..11083df
--- /dev/null
@@ -0,0 +1,65 @@
+<?php
+
+namespace Drupal\workspaces\Plugin\Validation\Constraint;
+
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\workspaces\WorkspaceManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+
+/**
+ * Checks if new entities created for entity reference fields are supported.
+ */
+class EntityReferenceSupportedNewEntitiesConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
+
+  /**
+   * The workspace manager.
+   *
+   * @var \Drupal\workspaces\WorkspaceManagerInterface
+   */
+  protected $workspaceManager;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Creates a new EntityReferenceSupportedNewEntitiesConstraintValidator instance.
+   */
+  public function __construct(WorkspaceManagerInterface $workspaceManager, EntityTypeManagerInterface $entityTypeManager) {
+    $this->workspaceManager = $workspaceManager;
+    $this->entityTypeManager = $entityTypeManager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('workspaces.manager'),
+      $container->get('entity_type.manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate($value, Constraint $constraint) {
+    if ($this->workspaceManager->getActiveWorkspace()->isDefaultWorkspace()) {
+      return;
+    }
+
+    $target_entity_type_id = $value->getFieldDefinition()->getFieldStorageDefinition()->getSetting('target_type');
+    $target_entity_type = $this->entityTypeManager->getDefinition($target_entity_type_id);
+
+    if ($value->hasNewEntity() && !$this->workspaceManager->isEntityTypeSupported($target_entity_type)) {
+      $this->context->addViolation($constraint->message, ['%collection_label' => $target_entity_type->getCollectionLabel()]);
+    }
+  }
+
+}
index faa5f8fface75f52454aedbfe06f0c5bc1aa7ebf..837edefe8493e821da464a4dc392be3a26303bb4 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace Drupal\workspaces;
 
+use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface;
 use Drupal\Core\DependencyInjection\ClassResolverInterface;
 use Drupal\Core\Entity\EntityPublishedInterface;
 use Drupal\Core\Entity\EntityTypeInterface;
@@ -47,6 +48,13 @@ class WorkspaceManager implements WorkspaceManagerInterface {
    */
   protected $entityTypeManager;
 
+  /**
+   * The entity memory cache service.
+   *
+   * @var \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface
+   */
+  protected $entityMemoryCache;
+
   /**
    * The current user.
    *
@@ -96,6 +104,8 @@ class WorkspaceManager implements WorkspaceManagerInterface {
    *   The request stack.
    * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
    *   The entity type manager.
+   * @param \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface $entity_memory_cache
+   *   The entity memory cache service.
    * @param \Drupal\Core\Session\AccountProxyInterface $current_user
    *   The current user.
    * @param \Drupal\Core\State\StateInterface $state
@@ -107,9 +117,10 @@ class WorkspaceManager implements WorkspaceManagerInterface {
    * @param array $negotiator_ids
    *   The workspace negotiator service IDs.
    */
-  public function __construct(RequestStack $request_stack, EntityTypeManagerInterface $entity_type_manager, AccountProxyInterface $current_user, StateInterface $state, LoggerInterface $logger, ClassResolverInterface $class_resolver, array $negotiator_ids) {
+  public function __construct(RequestStack $request_stack, EntityTypeManagerInterface $entity_type_manager, MemoryCacheInterface $entity_memory_cache, AccountProxyInterface $current_user, StateInterface $state, LoggerInterface $logger, ClassResolverInterface $class_resolver, array $negotiator_ids) {
     $this->requestStack = $request_stack;
     $this->entityTypeManager = $entity_type_manager;
+    $this->entityMemoryCache = $entity_memory_cache;
     $this->currentUser = $current_user;
     $this->state = $state;
     $this->logger = $logger;
@@ -167,6 +178,31 @@ class WorkspaceManager implements WorkspaceManagerInterface {
    * {@inheritdoc}
    */
   public function setActiveWorkspace(WorkspaceInterface $workspace) {
+    $this->doSwitchWorkspace($workspace);
+
+    // Set the workspace on the proper negotiator.
+    $request = $this->requestStack->getCurrentRequest();
+    foreach ($this->negotiatorIds as $negotiator_id) {
+      $negotiator = $this->classResolver->getInstanceFromDefinition($negotiator_id);
+      if ($negotiator->applies($request)) {
+        $negotiator->setActiveWorkspace($workspace);
+        break;
+      }
+    }
+
+    return $this;
+  }
+
+  /**
+   * Switches the current workspace.
+   *
+   * @param \Drupal\workspaces\WorkspaceInterface $workspace
+   *   The workspace to set as active.
+   *
+   * @throws \Drupal\workspaces\WorkspaceAccessException
+   *   Thrown when the current user doesn't have access to view the workspace.
+   */
+  protected function doSwitchWorkspace(WorkspaceInterface $workspace) {
     // If the current user doesn't have access to view the workspace, they
     // shouldn't be allowed to switch to it.
     if (!$workspace->access('view') && !$workspace->isDefaultWorkspace()) {
@@ -179,22 +215,30 @@ class WorkspaceManager implements WorkspaceManagerInterface {
 
     $this->activeWorkspace = $workspace;
 
-    // Set the workspace on the proper negotiator.
-    $request = $this->requestStack->getCurrentRequest();
-    foreach ($this->negotiatorIds as $negotiator_id) {
-      $negotiator = $this->classResolver->getInstanceFromDefinition($negotiator_id);
-      if ($negotiator->applies($request)) {
-        $negotiator->setActiveWorkspace($workspace);
-        break;
-      }
-    }
+    // Clear the static entity cache for the supported entity types.
+    $cache_tags_to_invalidate = array_map(function ($entity_type_id) {
+      return 'entity.memory_cache:' . $entity_type_id;
+    }, array_keys($this->getSupportedEntityTypes()));
+    $this->entityMemoryCache->invalidateTags($cache_tags_to_invalidate);
+  }
 
-    $supported_entity_types = $this->getSupportedEntityTypes();
-    foreach ($supported_entity_types as $supported_entity_type) {
-      $this->entityTypeManager->getStorage($supported_entity_type->id())->resetCache();
+  /**
+   * {@inheritdoc}
+   */
+  public function executeInWorkspace($workspace_id, callable $function) {
+    /** @var \Drupal\workspaces\WorkspaceInterface $workspace */
+    $workspace = $this->entityTypeManager->getStorage('workspace')->load($workspace_id);
+
+    if (!$workspace) {
+      throw new \InvalidArgumentException('The ' . $workspace_id . ' workspace does not exist.');
     }
 
-    return $this;
+    $previous_active_workspace = $this->getActiveWorkspace();
+    $this->doSwitchWorkspace($workspace);
+    $result = $function();
+    $this->doSwitchWorkspace($previous_active_workspace);
+
+    return $result;
   }
 
   /**
index 9ce720b3f148e6536a39ff1fe74c38307015114c..006a2bd86d8a1486539c07452108fb0030204f5d 100644 (file)
@@ -49,6 +49,19 @@ interface WorkspaceManagerInterface {
    */
   public function setActiveWorkspace(WorkspaceInterface $workspace);
 
+  /**
+   * Executes the given callback function in the context of a workspace.
+   *
+   * @param string $workspace_id
+   *   The ID of a workspace.
+   * @param callable $function
+   *   The callback to be executed.
+   *
+   * @return mixed
+   *   The callable's return value.
+   */
+  public function executeInWorkspace($workspace_id, callable $function);
+
   /**
    * Determines whether runtime entity operations should be altered.
    *
index 5c7bea6e6a60f4655d262f5859b2c470d909b634..00276ce0b63725b983e850c8cbb8d7255a82310e 100644 (file)
@@ -16,7 +16,7 @@ class WorkspaceTest extends BrowserTestBase {
   /**
    * {@inheritdoc}
    */
-  public static $modules = ['workspaces'];
+  public static $modules = ['workspaces', 'toolbar'];
 
   /**
    * A test user.
@@ -43,6 +43,8 @@ class WorkspaceTest extends BrowserTestBase {
       'create workspace',
       'edit own workspace',
       'edit any workspace',
+      'view own workspace',
+      'access toolbar',
     ];
 
     $this->editor1 = $this->drupalCreateUser($permissions);
@@ -69,6 +71,36 @@ class WorkspaceTest extends BrowserTestBase {
     $page->hasContent("This value is not valid");
   }
 
+  /**
+   * Test that the toolbar correctly shows the active workspace.
+   */
+  public function testWorkspaceToolbar() {
+    $this->drupalLogin($this->editor1);
+
+    $this->drupalPostForm('/admin/config/workflow/workspaces/add', [
+      'id' => 'test_workspace',
+      'label' => 'Test workspace',
+    ], 'Save');
+
+    // Activate the test workspace.
+    $this->drupalPostForm('/admin/config/workflow/workspaces/manage/test_workspace/activate', [], 'Confirm');
+
+    $this->drupalGet('<front>');
+    $page = $this->getSession()->getPage();
+    // Toolbar should show the correct label.
+    $this->assertTrue($page->hasLink('Test workspace'));
+
+    // Change the workspace label.
+    $this->drupalPostForm('/admin/config/workflow/workspaces/manage/test_workspace/edit', [
+      'label' => 'New name',
+    ], 'Save');
+
+    $this->drupalGet('<front>');
+    $page = $this->getSession()->getPage();
+    // Toolbar should show the new label.
+    $this->assertTrue($page->hasLink('New name'));
+  }
+
   /**
    * Test changing the owner of a workspace.
    */
diff --git a/web/core/modules/workspaces/tests/src/Kernel/EntityReferenceSupportedNewEntitiesConstraintValidatorTest.php b/web/core/modules/workspaces/tests/src/Kernel/EntityReferenceSupportedNewEntitiesConstraintValidatorTest.php
new file mode 100644 (file)
index 0000000..5cef79d
--- /dev/null
@@ -0,0 +1,81 @@
+<?php
+
+namespace Drupal\Tests\workspaces\Kernel;
+
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\entity_test\Entity\EntityTest;
+use Drupal\entity_test\Entity\EntityTestMulRevPub;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\Tests\user\Traits\UserCreationTrait;
+
+/**
+ * @coversDefaultClass \Drupal\workspaces\Plugin\Validation\Constraint\EntityReferenceSupportedNewEntitiesConstraintValidator
+ * @group workspaces
+ */
+class EntityReferenceSupportedNewEntitiesConstraintValidatorTest extends KernelTestBase {
+
+  use UserCreationTrait;
+  use WorkspaceTestTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'system',
+    'user',
+    'workspaces',
+    'entity_test',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->installEntitySchema('user');
+    $this->installSchema('system', ['sequences']);
+    $this->createUser();
+
+    $fields['supported_reference'] = BaseFieldDefinition::create('entity_reference')->setSetting('target_type', 'entity_test_mulrevpub');
+    $fields['unsupported_reference'] = BaseFieldDefinition::create('entity_reference')->setSetting('target_type', 'entity_test');
+    $this->container->get('state')->set('entity_test_mulrevpub.additional_base_field_definitions', $fields);
+
+    $this->installEntitySchema('entity_test_mulrevpub');
+    $this->initializeWorkspacesModule();
+  }
+
+  /**
+   * @covers ::validate
+   */
+  public function testNewEntitiesAllowedInDefaultWorkspace() {
+    $entity = EntityTestMulRevPub::create([
+      'unsupported_reference' => [
+        'entity' => EntityTest::create([]),
+      ],
+      'supported_reference' => [
+        'entity' => EntityTest::create([]),
+      ],
+    ]);
+    $this->assertCount(0, $entity->validate());
+  }
+
+  /**
+   * @covers ::validate
+   */
+  public function testNewEntitiesForbiddenInNonDefaultWorkspace() {
+    $this->switchToWorkspace('stage');
+    $entity = EntityTestMulRevPub::create([
+      'unsupported_reference' => [
+        'entity' => EntityTest::create([]),
+      ],
+      'supported_reference' => [
+        'entity' => EntityTestMulRevPub::create([]),
+      ],
+    ]);
+    $violations = $entity->validate();
+    $this->assertCount(1, $violations);
+    $this->assertEquals('<em class="placeholder">Test entity entities</em> can only be created in the default workspace.', $violations[0]->getMessage());
+  }
+
+}
index 9d77cdf001f1a2c524c73c6a522d10c2d4efc846..39c365b9468f42aa1ce0a73af51336a68352bee5 100644 (file)
@@ -14,7 +14,6 @@ use Drupal\Tests\node\Traits\NodeCreationTrait;
 use Drupal\Tests\user\Traits\UserCreationTrait;
 use Drupal\views\Tests\ViewResultAssertionTrait;
 use Drupal\views\Views;
-use Drupal\workspaces\Entity\Workspace;
 
 /**
  * Tests a complete deployment scenario across different workspaces.
@@ -28,6 +27,7 @@ class WorkspaceIntegrationTest extends KernelTestBase {
   use NodeCreationTrait;
   use UserCreationTrait;
   use ViewResultAssertionTrait;
+  use WorkspaceTestTrait;
 
   /**
    * The entity type manager.
@@ -36,13 +36,6 @@ class WorkspaceIntegrationTest extends KernelTestBase {
    */
   protected $entityTypeManager;
 
-  /**
-   * An array of test workspaces, keyed by workspace ID.
-   *
-   * @var \Drupal\workspaces\WorkspaceInterface[]
-   */
-  protected $workspaces = [];
-
   /**
    * Creation timestamp that should be incremented for each new entity.
    *
@@ -93,34 +86,6 @@ class WorkspaceIntegrationTest extends KernelTestBase {
     $this->createNode(['title' => 'live - 2 - r2 - unpublished', 'created' => $this->createdTimestamp++, 'status' => FALSE]);
   }
 
-  /**
-   * Enables the Workspaces module and creates two workspaces.
-   */
-  protected function initializeWorkspacesModule() {
-    // Enable the Workspaces module here instead of the static::$modules array
-    // so we can test it with default content.
-    $this->enableModules(['workspaces']);
-    $this->container = \Drupal::getContainer();
-    $this->entityTypeManager = \Drupal::entityTypeManager();
-
-    $this->installEntitySchema('workspace');
-    $this->installEntitySchema('workspace_association');
-
-    // Create two workspaces by default, 'live' and 'stage'.
-    $this->workspaces['live'] = Workspace::create(['id' => 'live']);
-    $this->workspaces['live']->save();
-    $this->workspaces['stage'] = Workspace::create(['id' => 'stage']);
-    $this->workspaces['stage']->save();
-
-    $permissions = [
-      'administer nodes',
-      'create workspace',
-      'edit any workspace',
-      'view any workspace',
-    ];
-    $this->setCurrentUser($this->createUser($permissions));
-  }
-
   /**
    * Tests various scenarios for creating and deploying content in workspaces.
    */
@@ -492,6 +457,57 @@ class WorkspaceIntegrationTest extends KernelTestBase {
     $entity_test->delete();
   }
 
+  /**
+   * @covers \Drupal\workspaces\WorkspaceManager::executeInWorkspace
+   */
+  public function testExecuteInWorkspaceContext() {
+    $this->initializeWorkspacesModule();
+
+    // Create an entity in the default workspace.
+    $this->switchToWorkspace('live');
+    $node = $this->createNode([
+      'title' => 'live node 1',
+    ]);
+    $node->save();
+
+    // Switch to the 'stage' workspace and change some values for the referenced
+    // entities.
+    $this->switchToWorkspace('stage');
+    $node->title->value = 'stage node 1';
+    $node->save();
+
+    // Switch back to the default workspace and run the baseline assertions.
+    $this->switchToWorkspace('live');
+    $storage = $this->entityTypeManager->getStorage('node');
+
+    $this->assertEquals('live', $this->workspaceManager->getActiveWorkspace()->id());
+
+    $live_node = $storage->loadUnchanged($node->id());
+    $this->assertEquals('live node 1', $live_node->title->value);
+
+    $result = $storage->getQuery()
+      ->condition('title', 'live node 1')
+      ->execute();
+    $this->assertEquals([$live_node->getRevisionId() => $node->id()], $result);
+
+    // Try the same assertions in the context of the 'stage' workspace.
+    $this->workspaceManager->executeInWorkspace('stage', function () use ($node, $storage) {
+      $this->assertEquals('stage', $this->workspaceManager->getActiveWorkspace()->id());
+
+      $stage_node = $storage->loadUnchanged($node->id());
+      $this->assertEquals('stage node 1', $stage_node->title->value);
+
+      $result = $storage->getQuery()
+        ->condition('title', 'stage node 1')
+        ->execute();
+      $this->assertEquals([$stage_node->getRevisionId() => $stage_node->id()], $result);
+    });
+
+    // Check that the 'stage' workspace was not persisted by the workspace
+    // manager.
+    $this->assertEquals('live', $this->workspaceManager->getActiveWorkspace()->id());
+  }
+
   /**
    * Checks entity load, entity queries and views results for a test scenario.
    *
@@ -681,18 +697,6 @@ class WorkspaceIntegrationTest extends KernelTestBase {
     }
   }
 
-  /**
-   * Sets a given workspace as active.
-   *
-   * @param string $workspace_id
-   *   The ID of the workspace to switch to.
-   */
-  protected function switchToWorkspace($workspace_id) {
-    // Switch the test runner's context to the specified workspace.
-    $workspace = $this->entityTypeManager->getStorage('workspace')->load($workspace_id);
-    \Drupal::service('workspaces.manager')->setActiveWorkspace($workspace);
-  }
-
   /**
    * Flattens the expectations array defined by testWorkspaces().
    *
diff --git a/web/core/modules/workspaces/tests/src/Kernel/WorkspaceTestTrait.php b/web/core/modules/workspaces/tests/src/Kernel/WorkspaceTestTrait.php
new file mode 100644 (file)
index 0000000..13cff6a
--- /dev/null
@@ -0,0 +1,67 @@
+<?php
+
+namespace Drupal\Tests\workspaces\Kernel;
+
+use Drupal\workspaces\Entity\Workspace;
+
+/**
+ * A trait with common workspaces testing functionality.
+ */
+trait WorkspaceTestTrait {
+
+  /**
+   * The workspaces manager.
+   *
+   * @var \Drupal\workspaces\WorkspaceManagerInterface
+   */
+  protected $workspaceManager;
+
+  /**
+   * An array of test workspaces, keyed by workspace ID.
+   *
+   * @var \Drupal\workspaces\WorkspaceInterface[]
+   */
+  protected $workspaces = [];
+
+  /**
+   * Enables the Workspaces module and creates two workspaces.
+   */
+  protected function initializeWorkspacesModule() {
+    // Enable the Workspaces module here instead of the static::$modules array
+    // so we can test it with default content.
+    $this->enableModules(['workspaces']);
+    $this->container = \Drupal::getContainer();
+    $this->entityTypeManager = \Drupal::entityTypeManager();
+    $this->workspaceManager = \Drupal::service('workspaces.manager');
+
+    $this->installEntitySchema('workspace');
+    $this->installEntitySchema('workspace_association');
+
+    // Create two workspaces by default, 'live' and 'stage'.
+    $this->workspaces['live'] = Workspace::create(['id' => 'live']);
+    $this->workspaces['live']->save();
+    $this->workspaces['stage'] = Workspace::create(['id' => 'stage']);
+    $this->workspaces['stage']->save();
+
+    $permissions = array_intersect([
+      'administer nodes',
+      'create workspace',
+      'edit any workspace',
+      'view any workspace',
+    ], array_keys($this->container->get('user.permissions')->getPermissions()));
+    $this->setCurrentUser($this->createUser($permissions));
+  }
+
+  /**
+   * Sets a given workspace as active.
+   *
+   * @param string $workspace_id
+   *   The ID of the workspace to switch to.
+   */
+  protected function switchToWorkspace($workspace_id) {
+    // Switch the test runner's context to the specified workspace.
+    $workspace = $this->entityTypeManager->getStorage('workspace')->load($workspace_id);
+    \Drupal::service('workspaces.manager')->setActiveWorkspace($workspace);
+  }
+
+}
index f279c4389375284519a6d2f52ce0d5d15bda3331..8f4f2fa2e281a6ae0ca6353a2463c3cb2471e2dc 100644 (file)
@@ -56,6 +56,15 @@ function workspaces_form_alter(&$form, FormStateInterface $form_state, $form_id)
     ->formAlter($form, $form_state, $form_id);
 }
 
+/**
+ * Implements hook_field_info_alter().
+ */
+function workspaces_field_info_alter(&$definitions) {
+  \Drupal::service('class_resolver')
+    ->getInstanceFromDefinition(EntityTypeInfo::class)
+    ->fieldInfoAlter($definitions);
+}
+
 /**
  * Implements hook_entity_load().
  */
@@ -154,15 +163,15 @@ function workspaces_toolbar() {
 
   $current_user = \Drupal::currentUser();
   if (!$current_user->hasPermission('administer workspaces')
-    || !$current_user->hasPermission('view own workspace')
-    || !$current_user->hasPermission('view any workspace')) {
+    && !$current_user->hasPermission('view own workspace')
+    && !$current_user->hasPermission('view any workspace')) {
     return $items;
   }
 
   /** @var \Drupal\workspaces\WorkspaceInterface $active_workspace */
   $active_workspace = \Drupal::service('workspaces.manager')->getActiveWorkspace();
 
-  $items['workspace'] = [
+  $items['workspace'] += [
     '#type' => 'toolbar_item',
     'tab' => [
       '#type' => 'link',
@@ -180,6 +189,7 @@ function workspaces_toolbar() {
           ],
         ]),
       ],
+      '#cache' => ['tags' => $active_workspace->getCacheTags()],
     ],
     '#wrapper_attributes' => [
       'class' => ['workspaces-toolbar-tab'],
index a238473d111eae878ff00d483305710fcc6f12ed..ab9e8bf89f71d0bd572a691a7d123f355c526284 100644 (file)
@@ -1,7 +1,7 @@
 services:
   workspaces.manager:
     class: Drupal\workspaces\WorkspaceManager
-    arguments: ['@request_stack', '@entity_type.manager', '@current_user', '@state', '@logger.channel.workspaces', '@class_resolver']
+    arguments: ['@request_stack', '@entity_type.manager', '@entity.memory_cache', '@current_user', '@state', '@logger.channel.workspaces', '@class_resolver']
     tags:
       - { name: service_id_collector, tag: workspace_negotiator }
   workspaces.operation_factory:
index 8a4357f84fbd77718ef81bf70a567f1e548ba962..d65688686f8d86fc99f32ba51f31fed0ff9ad325 100644 (file)
@@ -144,6 +144,20 @@ display:
           expose:
             label: ''
           granularity: second
+        nid:
+          id: nid
+          table: node_field_data
+          field: nid
+          relationship: none
+          group_type: group
+          admin_label: ''
+          order: ASC
+          exposed: false
+          expose:
+            label: ''
+          entity_type: node
+          entity_field: nid
+          plugin_id: standard
       title: 'More featured articles'
       header: {  }
       footer: {  }
index 3ddc61bb755a9934c17cbd145be857e6ebc5e15a..9ce6c4cab1716f81079880a06dd9a8e79c68802f 100644 (file)
@@ -158,6 +158,20 @@ display:
           expose:
             label: ''
           granularity: second
+        nid:
+          id: nid
+          table: node_field_data
+          field: nid
+          relationship: none
+          group_type: group
+          admin_label: ''
+          order: ASC
+          exposed: false
+          expose:
+            label: ''
+          entity_type: node
+          entity_field: nid
+          plugin_id: standard
       title: Articles
       header: {  }
       footer: {  }
index 7f4f0db3f116c1c9d96db734d71d7fd4190e3192..a97299283bec7dedc1647c9b2c8e61f8ea1cca6b 100644 (file)
@@ -249,6 +249,20 @@ display:
           granularity: second
           entity_type: node
           entity_field: created
+        nid:
+          id: nid
+          table: node_field_data
+          field: nid
+          relationship: none
+          group_type: group
+          admin_label: ''
+          order: ASC
+          exposed: false
+          expose:
+            label: ''
+          entity_type: node
+          entity_field: nid
+          plugin_id: standard
       style:
         type: default
         options:
index e7cf9fd6f97c569387da2df5eed24ae9120a72be..7e273c970dedffff3980abcc6a8231fd148ddcba 100644 (file)
@@ -158,6 +158,20 @@ display:
           expose:
             label: ''
           granularity: second
+        nid:
+          id: nid
+          table: node_field_data
+          field: nid
+          relationship: none
+          group_type: group
+          admin_label: ''
+          order: ASC
+          exposed: false
+          expose:
+            label: ''
+          entity_type: node
+          entity_field: nid
+          plugin_id: standard
       title: Recipes
       header: {  }
       footer: {  }
index 8476f1a6a8362330b56bfe29dcb92674dac8a3d8..7486d488f3b7a1fbeea22bc05e08b6e181c7effa 100644 (file)
@@ -36,6 +36,9 @@
   background: url('../../../../images/svg/pointer--white.svg') no-repeat center center;
   vertical-align: middle;
 }
+[dir=rtl] .block-type-footer-promo-block .footer-promo-content a:after {
+  transform: rotate(180deg);
+}
 
 @media screen and (min-width: 60rem) {
   .block-type-footer-promo-block {
index 06c196381216f8cd68fd0244f9f750058db048e0..b140da4adce6c6f12162923c87a74510e45f2252 100644 (file)
@@ -19,6 +19,7 @@
 .search-form #edit-basic {
   display: flex;
   align-items: flex-end;
+  flex: 1 1;
 }
 .search-form .form-type-search {
   margin: 0;
@@ -43,6 +44,7 @@
 .search-form .search-help-link {
   padding: 1.28rem;
   margin: 0 1rem 1rem 0;
+  flex: 1 1;
 }
 
 .search-form #edit-advanced {
   background: #fff;
   border: 1px solid #fcece7;
   padding: 1.28rem;
-  margin: 0 0 1rem 0;
+  margin: 0 0 1rem 0; /* LTR */
+}
+/* Apply right margin to keep aligned with title and exposed filter. */
+[dir=rtl] .search-results li {
+  margin: 0 1rem 1rem 0;
 }
 
 .search-results .search-result__snippet {
index fe3684298847e56c9b3bbd95714beb6dd1f2a3fe..176a09c44b59769d671fb53ed562862be5c470ff 100644 (file)
@@ -4,7 +4,7 @@
  */
 
 .search-form + h2 {
-  margin: 0 14px 1rem;
+  margin: 0 1rem 1rem;
 }
 
 .search-block-form {
   width: 20em;
   max-width: calc(100vw - 6.25em);
   height: auto;
-  margin: 0 -2px 0 0;
+  margin: 0 -2px 0 0; /* LTR */
   padding: 7px 8px 7px 32px;
   color: #464646;
-  border: 1px solid #dbdbdb;
-  border-right: none;
+  border: 1px solid #dbdbdb; /* LTR */
+  border-right: none; /* LTR */
   border-radius: 3px;
   background: url(../../../../images/svg/search.svg) no-repeat 0.5em center #fff;
   font-size: 0.875rem;
   line-height: normal;
 }
+[dir=rtl] .form-search {
+  margin: 0 0 0 -2px;
+  border-left: none;
+  border-right: 1px solid #dbdbdb;
+}
 
 .form-search:focus {
-  margin: 0 0 -2px -2px;
+  margin: 0 0 -2px -2px; /* LTR */
   padding: 5px 8px 5px 32px;
   outline: none;
 }
+[dir=rtl] .form-search:focus {
+  margin: 0 -2px -2px 0;
+  background-position: 0.35em;
+  border: 3px solid #00836d;
+}
 
 .form-search::placeholder {
   opacity: 1;
 .search-block-form .form-submit,
 .search-form .form-submit {
   /* Take off the border radius on the left side as it bumps into the search field */
-  border-top-left-radius: 0;
-  border-bottom-left-radius: 0;
+  border-top-left-radius: 0; /* LTR */
+  border-bottom-left-radius: 0; /* LTR */
+}
+[dir=rtl] .search-block-form .form-submit,
+[dir=rtl] .search-form .form-submit {
+  /* Take off the border radius on the left side as it bumps into the search field */
+  border-top-left-radius: 4px;
+  border-bottom-left-radius: 4px;
+  border-top-right-radius: 0;
+  border-bottom-right-radius: 0;
 }
 
 .search-block-form .form-submit:focus,
 .search-form .form-submit:focus,
 .search-form .form-submit:hover {
   margin: 0;
-  border-top-left-radius: 4px;
-  border-bottom-left-radius: 4px;
+  border-top-left-radius: 4px; /* LTR */
+  border-bottom-left-radius: 4px; /* LTR */
+}
+/* Apply border radius to all corners to override LTR and RTL (normal state) changes. */
+[dir=rtl] .search-block-form .form-submit:focus,
+[dir=rtl] .search-block-form .form-submit:hover,
+[dir=rtl] .search-form .form-submit:focus,
+[dir=rtl] .search-form .form-submit:hover {
+  margin: 0;
+  border-radius: 4px;
 }
index 453d8b24feb7da82072cd3a58b17f5a99948fa72..ac1aff73159960f892b2c5422218858dee2bbcc1 100644 (file)
   align-items: center;
   min-height: 40px;
   margin-bottom: 0.96em;
-  padding-left: 48px;
+  padding-left: 48px; /* LTR */
   background-repeat: no-repeat;
-  background-position: left center;
+  background-position: left center; /* LTR */
   background-size: 40px 40px;
 }
+[dir=rtl] .node--type-recipe.node--view-mode-full .field--name-field-preparation-time,
+[dir=rtl] .node--type-recipe.node--view-mode-full .field--name-field-cooking-time,
+[dir=rtl] .node--type-recipe.node--view-mode-full .field--name-field-number-of-servings,
+[dir=rtl] .node--type-recipe.node--view-mode-full .field--name-field-difficulty {
+  background-position: right center;
+  padding-left: 0;
+  padding-right: 48px;
+}
 .node--type-recipe.node--view-mode-full .field--name-field-preparation-time {
   background-image: url(../../../../images/svg/knife.svg);
 }
index af9ee7ee3069fccbb57edbbbe6340a2b47ab6824..6b3797e7179fe2af432ee676d569f13dffbd16f9 100644 (file)
 .field--name-field-recipe-instruction ol > li {
   position: relative;
   min-height: 1.5em;
-  padding: 0 0 0.6em 2.5em;
+  padding: 0 0 0.6em 2.5em; /* LTR */
   list-style: none;
   counter-increment: step-counter;
 }
+[dir=rtl] .field--name-field-recipe-instruction ol > li {
+  padding: 0 2.5em 0.6em 0;
+}
 .field--name-field-recipe-instruction ol > li::before {
   position: absolute;
   top: 0;
-  left: 0;
+  left: 0; /* LTR */
   content: counter(step-counter);
   color: #cc2a00;
   font-size: 1.5rem;
 }
+[dir=rtl] .field--name-field-recipe-instruction ol > li::before {
+  right: 0;
+  left: auto;
+}
index 5ba7810ce644c319437fd9514247df16076d3f90..b1d265f31fc7f0918f6eb1892159f87728c18ef6 100644 (file)
 
 @media screen and (min-width: 30rem) { /* 480px */
   .contact-form .form-actions .button {
-    margin-left: 1em;
+    margin-left: 1em; /* LTR */
+  }
+  [dir=rtl] .contact-form .form-actions .button + .button {
+    margin-right: 1em;
+    margin-left: 0;
   }
 }
index d7a4f049879f89bc6e3d0af6bd32c5264d914024..b49bc8e48cf5f3a8cac9f8d0f9f3f47522065c0d 100644 (file)
   margin-top: 1.538em;
 }
 .messages__content {
-  background: no-repeat 0 center;
+  background: no-repeat 0 center; /* LTR */
+}
+[dir=rtl] .messages__content {
+  background-position: right;
 }
 .messages--status {
   background-color: #e6eee0;
   margin: 0;
 }
 .messages__item {
-  margin-left: 24px;
+  margin-left: 24px; /* LTR */
+}
+[dir=rtl] .messages__item {
+  margin-left: 0;
+  margin-right: 24px;
 }
 .messages__item + .messages__item {
   margin-top: 0.769em;
index f04578f0ea8a8cd34889bb5278f60b1174d66fd9..6cfde97e995244aadca3af2f658b937ca724c434 100644 (file)
@@ -5,6 +5,9 @@
 .breadcrumb {
   padding: 0.79rem 1.266rem;
 }
+.breadcrumb li {
+  display: inline-block;
+}
 /* Large */
 @media screen and (min-width: 60rem) { /* 960px */
   .breadcrumb {
index ac4f3918525827373bf1e4a6d81bfa58eef1e9f9..563a902cca7bd8c4138976804f9df7b9f32bb4d1 100644 (file)
@@ -7,7 +7,10 @@
   .menu--account {
     display: block;
     flex: 0 1 50%;
-    text-align: right;
+    text-align: right; /* LTR */
+  }
+  [dir="rtl"] .menu--account {
+    text-align: left;
   }
 }
 
   line-height: 1.5;
 }
 .menu-account__item + .menu-account__item {
-  margin-left: 1em;
+  margin-left: 1em; /* LTR */
+}
+[dir="rtl"] .menu-account__item + .menu-account__item {
+  margin-left: 0;
+  margin-right: 1em;
 }
 .menu-account__link,
 .menu-account__link:hover {
index 44b715b182438ce61476db248b2bf073423c6299..54c54d9a2957c4745142214cd419406e48d108b4 100644 (file)
     margin-bottom: 0;
   }
   .menu-main__item + .menu-main__item {
-    margin-left: 2.5em;
+    margin-left: 2.5em; /* LTR */
+  }
+  [dir="rtl"] .menu-main__item + .menu-main__item {
+    margin-left: 0;
+    margin-right: 2.5em;
   }
 }
 
index 79cc1194cdb4f34f10b97a1fd5f68c4d28aa30bf..84e0b097185a7d59115ec236d538b2c5cacb6492 100644 (file)
   padding: 0;
 }
 .tabs .tab {
-  margin: 0;
+  margin: 0; /* LTR */
   background-color: #e6eee0;
 }
+[dir=rtl] .tabs .tab {
+  margin: 0;
+}
 .tabs .tab.is-active {
   background-color: #fff;
 }
index b81adae5926b636df21a9d6a72d2a46139e7b438..0ad4107c25fdf25a5496fb8d1f7e6174f9c18cdd 100644 (file)
 @media screen and (min-width: 60rem) { /* 960px */
   .view-promoted-items--single > .view-content {
     flex: 0 0 50%;
-    margin-right: 14px;
+    margin-right: 14px; /* LTR */
     display: flex;
   }
+  [dir=rtl] .view-promoted-items--single > .view-content {
+    margin-right: 0;
+    margin-left: 14px;
+  }
 }
 
 .view-promoted-items--single > .view-content .views-row {
 /* Large */
 @media screen and (min-width: 60rem) { /* 960px */
   .view-promoted-items--single > .attachment-after {
-    margin-left: 14px;
+    margin-left: 14px; /* LTR */
     display: flex;
   }
+  [dir=rtl] .view-promoted-items--single > .attachment-after {
+    margin-left: 0;
+    margin-right: 14px;
+  }
 }
 
 /* Large */
diff --git a/web/core/tests/Drupal/FunctionalJavascriptTests/Ajax/ElementValidationTest.php b/web/core/tests/Drupal/FunctionalJavascriptTests/Ajax/ElementValidationTest.php
new file mode 100644 (file)
index 0000000..48bda56
--- /dev/null
@@ -0,0 +1,53 @@
+<?php
+
+namespace Drupal\FunctionalJavascriptTests\Ajax;
+
+use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+
+/**
+ * Various tests of AJAX behavior.
+ *
+ * @group Ajax
+ */
+class ElementValidationTest extends WebDriverTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['ajax_test', 'ajax_forms_test'];
+
+  /**
+   * Tries to post an Ajax change to a form that has a validated element.
+   *
+   * Drupal AJAX commands update the DOM echoing back the validated values in
+   * the form of messages that appear on the page.
+   */
+  public function testAjaxElementValidation() {
+    $this->drupalGet('ajax_validation_test');
+    $page = $this->getSession()->getPage();
+    $assert = $this->assertSession();
+
+    // Partially complete the form with a string.
+    $page->fillField('drivertext', 'some dumb text');
+    // Move focus away from this field to trigger AJAX.
+    $page->findField('spare_required_field')->focus();
+
+    // When the AJAX command updates the DOM a <ul> unsorted list
+    // "message__list" structure will appear on the page echoing back the
+    // "some dumb text" message.
+    $placeholder_text = $assert->waitForElement('css', "ul.messages__list li.messages__item em:contains('some dumb text')");
+    $this->assertNotNull($placeholder_text, 'A callback successfully echoed back a string.');
+
+    $this->drupalGet('ajax_validation_test');
+    // Partialy complete the form with a number.
+    $page->fillField('drivernumber', '12345');
+    $page->findField('spare_required_field')->focus();
+
+    // The AJAX request/resonse will complete successfully when a InsertCommand
+    // injects a message with a placeholder element into the DOM with the
+    // submitted number.
+    $placeholder_number = $assert->waitForElement('css', "ul.messages__list li.messages__item em:contains('12345')");
+    $this->assertNotNull($placeholder_number, 'A callback successfully echoed back a number.');
+  }
+
+}
diff --git a/web/core/tests/Drupal/FunctionalJavascriptTests/Ajax/FormValuesTest.php b/web/core/tests/Drupal/FunctionalJavascriptTests/Ajax/FormValuesTest.php
new file mode 100644 (file)
index 0000000..d1a3e5c
--- /dev/null
@@ -0,0 +1,84 @@
+<?php
+
+namespace Drupal\FunctionalJavascriptTests\Ajax;
+
+use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+
+/**
+ * Tests that form values are properly delivered to AJAX callbacks.
+ *
+ * @group Ajax
+ */
+class FormValuesTest extends WebDriverTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['node', 'ajax_test', 'ajax_forms_test'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->drupalLogin($this->drupalCreateUser(['access content']));
+  }
+
+  /**
+   * Submits forms with select and checkbox elements via Ajax.
+   */
+  public function testSimpleAjaxFormValue() {
+
+    $this->drupalGet('ajax_forms_test_get_form');
+
+    $session = $this->getSession();
+    $assertSession = $this->assertSession();
+
+    // Verify form values of a select element.
+    foreach (['green', 'blue', 'red'] as $item) {
+      // Updating the field will trigger a AJAX request/response.
+      $session->getPage()->selectFieldOption('select', $item);
+
+      // The AJAX command in the response will update the DOM
+      $select = $assertSession->waitForElement('css', "div#ajax_selected_color:contains('$item')");
+      $this->assertNotNull($select, "DataCommand has updated the page with a value of $item.");
+    }
+
+    // Verify form values of a checkbox element.
+    $session->getPage()->checkField('checkbox');
+    $div0 = $this->assertSession()->waitForElement('css', "div#ajax_checkbox_value:contains('checked')");
+    $this->assertNotNull($div0, 'DataCommand updates the DOM as expected when a checkbox is selected');
+
+    $session->getPage()->uncheckField('checkbox');
+    $div1 = $this->assertSession()->waitForElement('css', "div#ajax_checkbox_value:contains('unchecked')");
+    $this->assertNotNull($div1, 'DataCommand updates the DOM as expected when a checkbox is de-selected');
+
+    // Verify that AJAX elements with invalid callbacks return error code 500.
+    // Ensure the test error log is empty before these tests.
+    $this->assertFalse(file_exists(DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log'), 'PHP error.log is empty.');
+    // We don't need to check for the X-Drupal-Ajax-Token header with these
+    // invalid requests.
+    $this->assertAjaxHeader = FALSE;
+    foreach (['null', 'empty', 'nonexistent'] as $key) {
+      $element_name = 'select_' . $key . '_callback';
+      // Updating the field will trigger a AJAX request/response.
+      $session->getPage()->selectFieldOption($element_name, 'green');
+
+      // The select element is disabled as the AJAX request is issued.
+      $this->assertSession()->waitForElement('css', "select[name=\"$element_name\"]:disabled");
+
+      // The select element is enabled as the response is receieved.
+      $this->assertSession()->waitForElement('css', "select[name=\"$element_name\"]:enabled");
+      $this->assertTrue(file_exists(DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log'), 'PHP error.log is not empty.');
+      $this->assertContains('"The specified #ajax callback is empty or not callable."', file_get_contents(DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log'));
+      // The exceptions are expected. Do not interpret them as a test failure.
+      // Not using File API; a potential error must trigger a PHP warning.
+      unlink(\Drupal::root() . '/' . $this->siteDirectory . '/error.log');
+    }
+    // We need to reload the page to kill any unfinished AJAX calls before
+    // tearDown() is called.
+    $this->drupalGet('ajax_forms_test_get_form');
+  }
+
+}
index fb564450b966650400259f8a5c061052e555c647..eb70f68ceae078b116671e6eca992ca255011f57 100644 (file)
@@ -114,6 +114,7 @@ class BrowserTestBaseTest extends BrowserTestBase {
 
     // Test drupalPostForm().
     $edit = ['bananas' => 'red'];
+    // Submit the form using the button label.
     $result = $this->drupalPostForm('form-test/object-builder', $edit, 'Save');
     $this->assertSame($this->getSession()->getPage()->getContent(), $result);
     $value = $config_factory->get('form_test.object')->get('bananas');
@@ -123,6 +124,20 @@ class BrowserTestBaseTest extends BrowserTestBase {
     $value = $config_factory->get('form_test.object')->get('bananas');
     $this->assertSame('', $value);
 
+    // Submit the form using the button id.
+    $edit = ['bananas' => 'blue'];
+    $result = $this->drupalPostForm('form-test/object-builder', $edit, 'edit-submit');
+    $this->assertSame($this->getSession()->getPage()->getContent(), $result);
+    $value = $config_factory->get('form_test.object')->get('bananas');
+    $this->assertSame('blue', $value);
+
+    // Submit the form using the button name.
+    $edit = ['bananas' => 'purple'];
+    $result = $this->drupalPostForm('form-test/object-builder', $edit, 'op');
+    $this->assertSame($this->getSession()->getPage()->getContent(), $result);
+    $value = $config_factory->get('form_test.object')->get('bananas');
+    $this->assertSame('purple', $value);
+
     // Test drupalPostForm() with no-html response.
     $values = Json::decode($this->drupalPostForm('form_test/form-state-values-clean', [], t('Submit')));
     $this->assertTrue(1000, $values['beer']);
index b6ad437677dde07201e96643b829641f940e43c2..69c209070389721255719335813e273556fbc7da 100644 (file)
@@ -19,7 +19,7 @@ class InstallerDatabaseErrorMessagesTest extends InstallerTestBase {
     // it will try and create the drupal_install_test table as this is part of
     // the standard database tests performed by the installer in
     // Drupal\Core\Database\Install\Tasks.
-    Database::getConnection('default')->query('CREATE TABLE {drupal_install_test} (id int NULL)');
+    Database::getConnection('default')->query('CREATE TABLE {drupal_install_test} (id int NOT NULL PRIMARY KEY)');
     parent::setUpSettings();
   }
 
index ecf7d8867a15038340a0e41d9678d64e6508a098..5fa84aba08da17eef0cedfab0edbfa9a0762a3ff 100644 (file)
@@ -48,12 +48,12 @@ class InstallerTranslationTest extends InstallerTestBase {
     // it will try and create the drupal_install_test table as this is part of
     // the standard database tests performed by the installer in
     // Drupal\Core\Database\Install\Tasks.
-    Database::getConnection('default')->query('CREATE TABLE {drupal_install_test} (id int NULL)');
+    Database::getConnection('default')->query('CREATE TABLE {drupal_install_test} (id int NOT NULL PRIMARY KEY)');
     parent::setUpSettings();
 
     // Ensure that the error message translation is working.
     $this->assertRaw('Beheben Sie alle Probleme unten, um die Installation fortzusetzen. Informationen zur Konfiguration der Datenbankserver finden Sie in der <a href="https://www.drupal.org/getting-started/install">Installationshandbuch</a>, oder kontaktieren Sie Ihren Hosting-Anbieter.');
-    $this->assertRaw('<strong>CREATE</strong> ein Test-Tabelle auf Ihrem Datenbankserver mit dem Befehl <em class="placeholder">CREATE TABLE {drupal_install_test} (id int NULL)</em> fehlgeschlagen.');
+    $this->assertRaw('<strong>CREATE</strong> ein Test-Tabelle auf Ihrem Datenbankserver mit dem Befehl <em class="placeholder">CREATE TABLE {drupal_install_test} (id int NOT NULL PRIMARY KEY)</em> fehlgeschlagen.');
 
     // Now do it successfully.
     Database::getConnection('default')->query('DROP TABLE {drupal_install_test}');
index eb1588f18e8302f96dfa1bd47acf3ee5931d3c59..7acab53b06636c7493e178e92f1274609547e29f 100644 (file)
@@ -40,7 +40,7 @@ trait AssertConfigTrait {
 
           // Allow to skip entire config files.
           if ($skipped_config[$config_name] === TRUE) {
-            continue;
+            break;
           }
 
           // Allow to skip some specific lines of imported config files.
@@ -71,12 +71,12 @@ trait AssertConfigTrait {
         case 'Drupal\Component\Diff\Engine\DiffOpAdd':
           // The _core property does not exist in the default config.
           if ($op->closing[0] === '_core:') {
-            continue;
+            break;
           }
           foreach ($op->closing as $closing) {
             // The UUIDs don't exist in the default config.
             if (strpos($closing, 'uuid: ') === 0) {
-              continue;
+              break;
             }
             throw new \Exception($config_name . ': ' . var_export($op, TRUE));
           }
index 82ff53086ba3a006c40fc874fccb3f352c629331..f595cfcab5ec8dbfa2bf5aed11099fe62acae4ab 100644 (file)
@@ -29,8 +29,12 @@ class SizeTest extends KernelTestBase {
   public function providerTestCommonFormatSize() {
     $kb = Bytes::KILOBYTE;
     return [
+      ['0 bytes', 0],
       ['1 byte', 1],
+      ['-1 bytes', -1],
       ['2 bytes', 2],
+      ['-2 bytes', -2],
+      ['1023 bytes', $kb - 1],
       ['1 KB', $kb],
       ['1 MB', pow($kb, 2)],
       ['1 GB', pow($kb, 3)],
@@ -39,10 +43,13 @@ class SizeTest extends KernelTestBase {
       ['1 EB', pow($kb, 6)],
       ['1 ZB', pow($kb, 7)],
       ['1 YB', pow($kb, 8)],
-      // Rounded to 1 MB - not 1000 or 1024 kilobyte
+      ['1024 YB', pow($kb, 9)],
+      // Rounded to 1 MB - not 1000 or 1024 kilobytes
       ['1 MB', ($kb * $kb) - 1],
+      ['-1 MB', -(($kb * $kb) - 1)],
       // Decimal Megabytes
       ['3.46 MB', 3623651],
+      ['3.77 GB', 4053371676],
       // Decimal Petabytes
       ['59.72 PB', 67234178751368124],
       // Decimal Yottabytes
index 3fd17d86b47798da09b4aa795e4a3663c7b66b10..1695d9378c9f0da75a96bae97e24383210c0114f 100644 (file)
@@ -3,6 +3,7 @@
 namespace Drupal\KernelTests\Core\Entity;
 
 use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityStorageException;
 use Drupal\Core\Entity\RevisionLogInterface;
 use Drupal\Core\Entity\TypedData\EntityDataDefinition;
 use Drupal\Core\Entity\TypedData\EntityDataDefinitionInterface;
@@ -919,4 +920,33 @@ class EntityFieldTest extends EntityKernelTestBase {
     $this->assertEqual($entity->field_test_text->processed, $target, format_string('%entity_type: Text is processed with the default filter.', ['%entity_type' => $entity_type]));
   }
 
+  /**
+   * Tests explicit entity ID assignment.
+   */
+  public function testEntityIdAssignment() {
+    $entity_type = 'entity_test';
+    /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
+    $storage = $this->container->get('entity_type.manager')->getStorage($entity_type);
+
+    // Check that an ID can be explicitly assigned on creation.
+    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+    $entity = $this->createTestEntity($entity_type);
+    $entity_id = 3;
+    $entity->set('id', $entity_id);
+    $this->assertSame($entity_id, $entity->id());
+    $storage->save($entity);
+    $entity = $storage->loadUnchanged($entity->id());
+    $this->assertTrue($entity);
+
+    // Check that an explicitly-assigned ID is preserved on update.
+    $storage->save($entity);
+    $entity = $storage->loadUnchanged($entity->id());
+    $this->assertTrue($entity);
+
+    // Check that an ID cannot be explicitly assigned on update.
+    $this->setExpectedException(EntityStorageException::class);
+    $entity->set('id', $entity_id + 1);
+    $storage->save($entity);
+  }
+
 }
index e69260489809aaea3c879b7574b099ca2c650abd..a6819f01905084c3b19557897dc70ad9b706f8fa 100644 (file)
@@ -138,8 +138,11 @@ class EntityQueryTest extends EntityKernelTestBase {
       }
       foreach (array_reverse(str_split(decbin($i))) as $key => $bit) {
         if ($bit) {
-          list($field_name, $langcode, $values) = $units[$key];
-          $entity->getTranslation($langcode)->{$field_name}[] = $values;
+          // @todo https://www.drupal.org/project/drupal/issues/3001920 Doing
+          //   list($field_name, $langcode, $values) = $units[$key]; causes
+          //   problems in PHP 7.3. Revert to better variable names once
+          //   https://bugs.php.net/bug.php?id=76937 is fixed.
+          $entity->getTranslation($units[$key][1])->{$units[$key][0]}[] = $units[$key][2];
         }
       }
       $entity->save();
@@ -1160,4 +1163,42 @@ class EntityQueryTest extends EntityKernelTestBase {
     $this->assertEquals($entity->id(), reset($result));
   }
 
+  /**
+   * Tests entity queries with condition on the revision metadata keys.
+   */
+  public function testConditionOnRevisionMetadataKeys() {
+    $this->installModule('entity_test_revlog');
+    $this->installEntitySchema('entity_test_revlog');
+
+    /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
+    $entity_type_manager = $this->container->get('entity_type.manager');
+    /** @var \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type */
+    $entity_type = $entity_type_manager->getDefinition('entity_test_revlog');
+    /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
+    $storage = $entity_type_manager->getStorage('entity_test_revlog');
+
+    $revision_created_timestamp = time();
+    $revision_created_field_name = $entity_type->getRevisionMetadataKey('revision_created');
+    $entity = $storage->create([
+      'type' => 'entity_test',
+      $revision_created_field_name => $revision_created_timestamp,
+    ]);
+    $entity->save();
+
+    // Query only the default revision.
+    $result = $storage->getQuery()
+      ->condition($revision_created_field_name, $revision_created_timestamp)
+      ->execute();
+    $this->assertCount(1, $result);
+    $this->assertEquals($entity->id(), reset($result));
+
+    // Query all revisions.
+    $result = $storage->getQuery()
+      ->condition($revision_created_field_name, $revision_created_timestamp)
+      ->allRevisions()
+      ->execute();
+    $this->assertCount(1, $result);
+    $this->assertEquals($entity->id(), reset($result));
+  }
+
 }
diff --git a/web/core/tests/Drupal/KernelTests/Core/Plugin/EntityContextTypedDataTest.php b/web/core/tests/Drupal/KernelTests/Core/Plugin/EntityContextTypedDataTest.php
new file mode 100644 (file)
index 0000000..c26a3f8
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Plugin;
+
+use Drupal\Core\Entity\Entity\EntityViewDisplay;
+use Drupal\Core\Plugin\Context\EntityContext;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests the interaction between entity context and typed data.
+ *
+ * @group Context
+ */
+class EntityContextTypedDataTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['entity_test'];
+
+  /**
+   * Tests that entity contexts wrapping a config entity can be validated.
+   */
+  public function testValidateConfigEntityContext() {
+    $display = EntityViewDisplay::create([
+      'targetEntityType' => 'entity_test',
+      'bundle' => 'entity_test',
+      'mode' => 'default',
+      'status' => TRUE,
+    ]);
+    $display->save();
+
+    $violations = EntityContext::fromEntity($display)->validate();
+    $this->assertCount(0, $violations);
+  }
+
+}
index 1b3796eff43132ffae02b937e3a339c6bbc38544..5552591ed1a422753ae5014b87ff07b583551058 100644 (file)
@@ -32,6 +32,14 @@ class MessageTest extends KernelTestBase {
     $this->render($messages);
     $this->assertRaw('messages messages--error');
     $this->assertRaw('messages messages--status');
+    // Tests display of only one type of messages.
+    \Drupal::messenger()->addError('An error occurred');
+    $messages = [
+      '#type' => 'status_messages',
+      '#display' => 'error',
+    ];
+    $this->render($messages);
+    $this->assertRaw('messages messages--error');
   }
 
 }
index 6443b5a38493519f588e8c38f23718bfc92924a8..5417edc042358cc48e3e2bbfd776ecab448c8384 100644 (file)
@@ -676,7 +676,7 @@ abstract class BrowserTestBase extends TestCase {
    *   The JSON decoded drupalSettings value from the current page.
    */
   protected function getDrupalSettings() {
-    $html = $this->getSession()->getPage()->getHtml();
+    $html = $this->getSession()->getPage()->getContent();
     if (preg_match('@<script type="application/json" data-drupal-selector="drupal-settings-json">([^<]*)</script>@', $html, $matches)) {
       return Json::decode($matches[1]);
     }
diff --git a/web/core/tests/Drupal/Tests/Component/Utility/MailTest.php b/web/core/tests/Drupal/Tests/Component/Utility/MailTest.php
new file mode 100644 (file)
index 0000000..5f30328
--- /dev/null
@@ -0,0 +1,60 @@
+<?php
+
+namespace Drupal\Tests\Component\Utility;
+
+use Drupal\Component\Utility\Mail;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * Test mail helpers implemented in Mail component.
+ *
+ * @group Utility
+ *
+ * @coversDefaultClass \Drupal\Component\Utility\Mail
+ */
+class MailTest extends TestCase {
+
+  /**
+   * Tests RFC-2822 'display-name' formatter.
+   *
+   * @dataProvider providerTestDisplayName
+   * @covers ::formatDisplayName
+   */
+  public function testFormatDisplayName($string, $safe_display_name) {
+    $this->assertEquals($safe_display_name, Mail::formatDisplayName($string));
+  }
+
+  /**
+   * Data provider for testFormatDisplayName().
+   *
+   * @see testFormatDisplayName()
+   *
+   * @return array
+   *   An array containing a string and its 'display-name' safe value.
+   */
+  public function providerTestDisplayName() {
+    return [
+      // Simple ASCII characters.
+      ['Test site', 'Test site'],
+      // ASCII with html entity.
+      ['Test &amp; site', 'Test & site'],
+      // Non-ASCII characters.
+      ['Tést site', '=?UTF-8?B?VMOpc3Qgc2l0ZQ==?='],
+      // Non-ASCII with special characters.
+      ['Tést; site', '=?UTF-8?B?VMOpc3Q7IHNpdGU=?='],
+      // Non-ASCII with html entity.
+      ['T&eacute;st; site', '=?UTF-8?B?VMOpc3Q7IHNpdGU=?='],
+      // ASCII with special characters.
+      ['Test; site', '"Test; site"'],
+      // ASCII with special characters as html entity.
+      ['Test &lt; site', '"Test < site"'],
+      // ASCII with special characters and '\'.
+      ['Test; \ "site"', '"Test; \\\\ \"site\""'],
+      // String already RFC-2822 compliant.
+      ['"Test; site"', '"Test; site"'],
+      // String already RFC-2822 compliant.
+      ['"Test; \\\\ \"site\""', '"Test; \\\\ \"site\""'],
+    ];
+  }
+
+}
index 2d898591482370a889896207206c8ab755514b3b..139200cabeabfdb6243164171282cc30c9ebcf4e 100644 (file)
@@ -12,6 +12,7 @@ use Drupal\Core\Extension\ThemeHandlerInterface;
 use Drupal\Core\Layout\LayoutDefault;
 use Drupal\Core\Layout\LayoutDefinition;
 use Drupal\Core\Layout\LayoutPluginManager;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Tests\UnitTestCase;
 use org\bovigo\vfs\vfsStream;
 use Prophecy\Argument;
@@ -114,8 +115,12 @@ class LayoutPluginManagerTest extends UnitTestCase {
     $theme_a_path = vfsStream::url('root/themes/theme_a');
     $layout_definition = $this->layoutPluginManager->getDefinition('theme_a_provided_layout');
     $this->assertSame('theme_a_provided_layout', $layout_definition->id());
-    $this->assertSame('2 column layout', $layout_definition->getLabel());
-    $this->assertSame('Columns: 2', $layout_definition->getCategory());
+    $this->assertSame('2 column layout', (string) $layout_definition->getLabel());
+    $this->assertSame('Columns: 2', (string) $layout_definition->getCategory());
+    $this->assertSame('A theme provided layout', (string) $layout_definition->getDescription());
+    $this->assertTrue($layout_definition->getLabel() instanceof TranslatableMarkup);
+    $this->assertTrue($layout_definition->getCategory() instanceof TranslatableMarkup);
+    $this->assertTrue($layout_definition->getDescription() instanceof TranslatableMarkup);
     $this->assertSame('twocol', $layout_definition->getTemplate());
     $this->assertSame("$theme_a_path/templates", $layout_definition->getPath());
     $this->assertSame('theme_a/twocol', $layout_definition->getLibrary());
@@ -126,19 +131,26 @@ class LayoutPluginManagerTest extends UnitTestCase {
     $this->assertSame(LayoutDefault::class, $layout_definition->getClass());
     $expected_regions = [
       'left' => [
-        'label' => 'Left region',
+        'label' => new TranslatableMarkup('Left region', [], ['context' => 'layout_region']),
       ],
       'right' => [
-        'label' => 'Right region',
+        'label' => new TranslatableMarkup('Right region', [], ['context' => 'layout_region']),
       ],
     ];
-    $this->assertSame($expected_regions, $layout_definition->getRegions());
+    $regions = $layout_definition->getRegions();
+    $this->assertEquals($expected_regions, $regions);
+    $this->assertTrue($regions['left']['label'] instanceof TranslatableMarkup);
+    $this->assertTrue($regions['right']['label'] instanceof TranslatableMarkup);
 
     $module_a_path = vfsStream::url('root/modules/module_a');
     $layout_definition = $this->layoutPluginManager->getDefinition('module_a_provided_layout');
     $this->assertSame('module_a_provided_layout', $layout_definition->id());
-    $this->assertSame('1 column layout', $layout_definition->getLabel());
-    $this->assertSame('Columns: 1', $layout_definition->getCategory());
+    $this->assertSame('1 column layout', (string) $layout_definition->getLabel());
+    $this->assertSame('Columns: 1', (string) $layout_definition->getCategory());
+    $this->assertSame('A module provided layout', (string) $layout_definition->getDescription());
+    $this->assertTrue($layout_definition->getLabel() instanceof TranslatableMarkup);
+    $this->assertTrue($layout_definition->getCategory() instanceof TranslatableMarkup);
+    $this->assertTrue($layout_definition->getDescription() instanceof TranslatableMarkup);
     $this->assertSame(NULL, $layout_definition->getTemplate());
     $this->assertSame("$module_a_path/layouts", $layout_definition->getPath());
     $this->assertSame('module_a/onecol', $layout_definition->getLibrary());
@@ -149,19 +161,26 @@ class LayoutPluginManagerTest extends UnitTestCase {
     $this->assertSame(LayoutDefault::class, $layout_definition->getClass());
     $expected_regions = [
       'top' => [
-        'label' => 'Top region',
+        'label' => new TranslatableMarkup('Top region', [], ['context' => 'layout_region']),
       ],
       'bottom' => [
-        'label' => 'Bottom region',
+        'label' => new TranslatableMarkup('Bottom region', [], ['context' => 'layout_region']),
       ],
     ];
-    $this->assertSame($expected_regions, $layout_definition->getRegions());
+    $regions = $layout_definition->getRegions();
+    $this->assertEquals($expected_regions, $regions);
+    $this->assertTrue($regions['top']['label'] instanceof TranslatableMarkup);
+    $this->assertTrue($regions['bottom']['label'] instanceof TranslatableMarkup);
 
     $core_path = '/core/lib/Drupal/Core';
     $layout_definition = $this->layoutPluginManager->getDefinition('plugin_provided_layout');
     $this->assertSame('plugin_provided_layout', $layout_definition->id());
     $this->assertEquals('Layout plugin', $layout_definition->getLabel());
     $this->assertEquals('Columns: 1', $layout_definition->getCategory());
+    $this->assertEquals('Test layout', $layout_definition->getDescription());
+    $this->assertTrue($layout_definition->getLabel() instanceof TranslatableMarkup);
+    $this->assertTrue($layout_definition->getCategory() instanceof TranslatableMarkup);
+    $this->assertTrue($layout_definition->getDescription() instanceof TranslatableMarkup);
     $this->assertSame('plugin-provided-layout', $layout_definition->getTemplate());
     $this->assertSame($core_path, $layout_definition->getPath());
     $this->assertSame(NULL, $layout_definition->getLibrary());
@@ -172,10 +191,12 @@ class LayoutPluginManagerTest extends UnitTestCase {
     $this->assertSame('Drupal\Core\Plugin\Layout\TestLayout', $layout_definition->getClass());
     $expected_regions = [
       'main' => [
-        'label' => 'Main Region',
+        'label' => new TranslatableMarkup('Main Region', [], ['context' => 'layout_region']),
       ],
     ];
-    $this->assertEquals($expected_regions, $layout_definition->getRegions());
+    $regions = $layout_definition->getRegions();
+    $this->assertEquals($expected_regions, $regions);
+    $this->assertTrue($regions['main']['label'] instanceof TranslatableMarkup);
   }
 
   /**
@@ -284,6 +305,7 @@ EOS;
 module_a_provided_layout:
   label: 1 column layout
   category: 'Columns: 1'
+  description: 'A module provided layout'
   theme_hook: onecol
   path: layouts
   library: module_a/onecol
@@ -301,6 +323,7 @@ theme_a_provided_layout:
   class: '\Drupal\Core\Layout\LayoutDefault'
   label: 2 column layout
   category: 'Columns: 2'
+  description: 'A theme provided layout'
   template: twocol
   path: templates
   library: theme_a/twocol
@@ -325,7 +348,7 @@ use Drupal\Core\Layout\LayoutDefault;
  *   template = "templates/plugin-provided-layout",
  *   regions = {
  *     "main" = {
- *       "label" = @Translation("Main Region")
+ *       "label" = @Translation("Main Region", context = "layout_region")
  *     }
  *   }
  * )
diff --git a/web/core/tests/Drupal/Tests/Core/Plugin/Context/ContextAwarePluginBaseTest.php b/web/core/tests/Drupal/Tests/Core/Plugin/Context/ContextAwarePluginBaseTest.php
new file mode 100644 (file)
index 0000000..66e54b2
--- /dev/null
@@ -0,0 +1,96 @@
+<?php
+
+namespace Drupal\Tests\Core\Plugin\Context;
+
+use Drupal\Component\Plugin\Context\ContextInterface as ComponentContextInterface;
+use Drupal\Component\Plugin\Definition\ContextAwarePluginDefinitionInterface;
+use Drupal\Component\Plugin\Definition\ContextAwarePluginDefinitionTrait;
+use Drupal\Component\Plugin\Definition\PluginDefinition;
+use Drupal\Component\Plugin\Exception\ContextException;
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Core\Plugin\Context\ContextDefinition;
+use Drupal\Core\Plugin\ContextAwarePluginBase;
+use Drupal\Core\TypedData\DataDefinition;
+use Drupal\Core\TypedData\Plugin\DataType\StringData;
+use Drupal\Core\TypedData\TypedDataManagerInterface;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Plugin\ContextAwarePluginBase
+ * @group Plugin
+ */
+class ContextAwarePluginBaseTest extends UnitTestCase {
+
+  /**
+   * The plugin instance under test.
+   *
+   * @var \Drupal\Core\Plugin\ContextAwarePluginBase
+   */
+  private $plugin;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+    $this->plugin = new TestContextAwarePlugin([], 'the_sisko', new TestPluginDefinition());
+  }
+
+  /**
+   * @covers ::getContextDefinitions
+   */
+  public function testGetContextDefinitions() {
+    $this->assertInternalType('array', $this->plugin->getContextDefinitions());
+  }
+
+  /**
+   * @covers ::getContextDefinition
+   */
+  public function testGetContextDefinition() {
+    // The context is not defined, so an exception will be thrown.
+    $this->setExpectedException(ContextException::class, 'The person context is not a valid context.');
+    $this->plugin->getContextDefinition('person');
+  }
+
+  /**
+   * @covers ::setContextValue
+   */
+  public function testSetContextValue() {
+    $typed_data_manager = $this->prophesize(TypedDataManagerInterface::class);
+    $container = new ContainerBuilder();
+    $container->set('typed_data_manager', $typed_data_manager->reveal());
+    \Drupal::setContainer($container);
+
+    $this->plugin->getPluginDefinition()->addContextDefinition('foo', new ContextDefinition('string'));
+
+    $this->assertFalse($this->plugin->setContextCalled);
+    $this->plugin->setContextValue('foo', new StringData(new DataDefinition(), 'bar'));
+    $this->assertTrue($this->plugin->setContextCalled);
+  }
+
+}
+
+class TestPluginDefinition extends PluginDefinition implements ContextAwarePluginDefinitionInterface {
+
+  use ContextAwarePluginDefinitionTrait;
+
+}
+
+class TestContextAwarePlugin extends ContextAwarePluginBase {
+
+  /**
+   * Indicates if ::setContext() has been called or not.
+   *
+   * @var bool
+   */
+  public $setContextCalled = FALSE;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setContext($name, ComponentContextInterface $context) {
+    parent::setContext($name, $context);
+    $this->setContextCalled = TRUE;
+  }
+
+}
diff --git a/web/core/tests/Drupal/Tests/Core/Routing/RouterTest.php b/web/core/tests/Drupal/Tests/Core/Routing/RouterTest.php
new file mode 100644 (file)
index 0000000..d6ba1d4
--- /dev/null
@@ -0,0 +1,60 @@
+<?php
+
+namespace Drupal\Tests\Core\Routing;
+
+use Drupal\Core\Path\CurrentPathStack;
+use Drupal\Core\Routing\RequestContext;
+use Drupal\Core\Routing\RouteCompiler;
+use Drupal\Core\Routing\RouteProviderInterface;
+use Drupal\Core\Routing\Router;
+use Drupal\Core\Routing\UrlGeneratorInterface;
+use Drupal\Tests\UnitTestCase;
+use Prophecy\Argument;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Routing\Router
+ * @group Routing
+ */
+class RouterTest extends UnitTestCase {
+
+  /**
+   * @covers ::applyFitOrder
+   */
+  public function testMatchesWithDifferentFitOrder() {
+    $route_provider = $this->prophesize(RouteProviderInterface::class);
+
+    $route_collection = new RouteCollection();
+
+    $route = new Route('/user/{user}');
+    $route->setOption('compiler_class', RouteCompiler::class);
+    $route_collection->add('user_view', $route);
+
+    $route = new Route('/user/login');
+    $route->setOption('compiler_class', RouteCompiler::class);
+    $route_collection->add('user_login', $route);
+
+    $route_provider->getRouteCollectionForRequest(Argument::any())
+      ->willReturn($route_collection);
+
+    $url_generator = $this->prophesize(UrlGeneratorInterface::class);
+    $current_path_stack = $this->prophesize(CurrentPathStack::class);
+    $router = new Router($route_provider->reveal(), $current_path_stack->reveal(), $url_generator->reveal());
+
+    $request_context = $this->prophesize(RequestContext::class);
+    $request_context->getScheme()->willReturn('http');
+    $router->setContext($request_context->reveal());
+
+    $current_path_stack->getPath(Argument::any())->willReturn('/user/1');
+    $result = $router->match('/user/1');
+
+    $this->assertEquals('user_view', $result['_route']);
+
+    $current_path_stack->getPath(Argument::any())->willReturn('/user/login');
+    $result = $router->match('/user/login');
+
+    $this->assertEquals('user_login', $result['_route']);
+  }
+
+}
index d0817f6076f93522f19e4844f854b74404393402..d315433ac923965538a7f6ef466b7a9aae1bf824 100644 (file)
@@ -167,27 +167,12 @@ trait UiHelperTrait {
    *   @todo change $edit to disallow NULL as a value for Drupal 9.
    *     https://www.drupal.org/node/2802401
    * @param string $submit
-   *   Value of the submit button whose click is to be emulated. For example,
-   *   'Save'. The processing of the request depends on this value. For example,
-   *   a form may have one button with the value 'Save' and another button with
-   *   the value 'Delete', and execute different code depending on which one is
-   *   clicked.
-   *
-   *   This function can also be called to emulate an Ajax submission. In this
-   *   case, this value needs to be an array with the following keys:
-   *   - path: A path to submit the form values to for Ajax-specific processing.
-   *   - triggering_element: If the value for the 'path' key is a generic Ajax
-   *     processing path, this needs to be set to the name of the element. If
-   *     the name doesn't identify the element uniquely, then this should
-   *     instead be an array with a single key/value pair, corresponding to the
-   *     element name and value. The \Drupal\Core\Form\FormAjaxResponseBuilder
-   *     uses this to find the #ajax information for the element, including
-   *     which specific callback to use for processing the request.
-   *
-   *   This can also be set to NULL in order to emulate an Internet Explorer
-   *   submission of a form with a single text field, and pressing ENTER in that
-   *   textfield: under these conditions, no button information is added to the
-   *   POST data.
+   *   The id, name, label or value of the submit button which is to be clicked.
+   *   For example, 'Save'. The first element matched by
+   *   \Drupal\Tests\WebAssert::buttonExists() will be used. The processing of
+   *   the request depends on this value. For example, a form may have one
+   *   button with the value 'Save' and another button with the value 'Delete',
+   *   and execute different code depending on which one is clicked.
    * @param array $options
    *   Options to be forwarded to the url generator.
    * @param string|null $form_html_id
@@ -202,6 +187,8 @@ trait UiHelperTrait {
    *   (deprecated) The response content after submit form. It is necessary for
    *   backwards compatibility and will be removed before Drupal 9.0. You should
    *   just use the webAssert object for your assertions.
+   *
+   * @see \Drupal\Tests\WebAssert::buttonExists()
    */
   protected function drupalPostForm($path, $edit, $submit, array $options = [], $form_html_id = NULL) {
     if (is_object($submit)) {
index 19d9c2cc65e8ee1e470bc4e35472bceaa934d4cc..fa7700ecedb67137d3df1ec90bbc214be7f37753 100644 (file)
@@ -3,6 +3,7 @@
 namespace Drupal\Tests;
 
 use Behat\Mink\Exception\ExpectationException;
+use Behat\Mink\Exception\ResponseTextException;
 use Behat\Mink\WebAssert as MinkWebAssert;
 use Behat\Mink\Element\TraversableElement;
 use Behat\Mink\Exception\ElementNotFoundException;
@@ -545,4 +546,31 @@ class WebAssert extends MinkWebAssert {
     $this->assert(!preg_match($regex, $actual), $message);
   }
 
+  /**
+   * Checks that current page contains text only once.
+   *
+   * @param string $text
+   *   The string to look for.
+   *
+   * @see \Behat\Mink\WebAssert::pageTextContains()
+   */
+  public function pageTextContainsOnce($text) {
+    $actual = $this->session->getPage()->getText();
+    $actual = preg_replace('/\s+/u', ' ', $actual);
+    $regex = '/' . preg_quote($text, '/') . '/ui';
+    $count = preg_match_all($regex, $actual);
+    if ($count === 1) {
+      return;
+    }
+
+    if ($count > 1) {
+      $message = sprintf('The text "%s" appears in the text of this page more than once, but it should not.', $text);
+    }
+    else {
+      $message = sprintf('The text "%s" was not found anywhere in the text of the current page.', $text);
+    }
+
+    throw new ResponseTextException($message, $this->session->getDriver());
+  }
+
 }
index 1c408cf3cc5cb95bee1deb881c68e0b15698b053..76394c16ae22886bdf07ab236467a7cbc5baf131 100644 (file)
@@ -37,7 +37,7 @@ function bartik_preprocess_html(&$variables) {
 }
 
 /**
- * Implements hook_preprocess_HOOK() for page templates.
+ * Implements hook_preprocess_HOOK() for page title templates.
  */
 function bartik_preprocess_page_title(&$variables) {
   // Since the title and the shortcut link are both block level elements,
index 13483a81ce23c2736864d70d009072e00a72e521..54da16277ade77495e7b227288c62f7d8abfe9a1 100644 (file)
@@ -42,7 +42,7 @@ Disallow: /web.config
 # Paths (clean URLs)
 Disallow: /admin/
 Disallow: /comment/reply/
-Disallow: /filter/tips/
+Disallow: /filter/tips
 Disallow: /node/add/
 Disallow: /search/
 Disallow: /user/register/
@@ -52,7 +52,7 @@ Disallow: /user/logout/
 # Paths (no clean URLs)
 Disallow: /index.php/admin/
 Disallow: /index.php/comment/reply/
-Disallow: /index.php/filter/tips/
+Disallow: /index.php/filter/tips
 Disallow: /index.php/node/add/
 Disallow: /index.php/search/
 Disallow: /index.php/user/password/
index e4de67eb99183a5e79da979085bb687b188d0fee..daaf68272a50ba90845ccda2eed1c3c405f3739f 100644 (file)
@@ -26,9 +26,9 @@
  * example, to map https://www.drupal.org:8080/mysite/test to the configuration
  * directory sites/example.com, the array should be defined as:
  * @code
- * $sites = array(
+ * $sites = [
  *   '8080.www.drupal.org.mysite.test' => 'example.com',
- * );
+ * ];
  * @endcode
  * The URL, https://www.drupal.org:8080/mysite/test/, could be a symbolic link
  * or an Apache Alias directive that points to the Drupal root containing