diff --git a/composer.json b/composer.json index 1a392f6d643da2540665116d6f5efcc47e43f586..4c099b5ea7921ba48556fd4e6580fb862d7dfd99 100644 --- a/composer.json +++ b/composer.json @@ -88,7 +88,7 @@ "drupal-composer/drupal-scaffold": "2.5.4", "drupal/address": "1.1", "drupal/addtocalendar": "3.1", - "drupal/admin_toolbar": "2.0", + "drupal/admin_toolbar": "2.2", "drupal/administerusersbyrole": "2.0-beta1", "drupal/allowed_formats": "1.2", "drupal/anchor_link": "1.6", @@ -117,7 +117,7 @@ "drupal/entity_browser": "1.4", "drupal/entity_clone": "1.0.0-beta3", "drupal/entity_embed": "1.0-beta2", - "drupal/entity_reference_revisions": "1.3", + "drupal/entity_reference_revisions": "1.8", "drupal/externalauth": "1.1", "drupal/features": "3.8", "drupal/field_group": "3.0", @@ -127,7 +127,7 @@ "drupal/geolocation": "1.10", "drupal/google_analytics": "2.4", "drupal/google_tag": "1.3", - "drupal/honeypot": "1.28", + "drupal/honeypot": "1.30", "drupal/image_popup": "1.1", "drupal/inline_entity_form": "1.0-rc1", "drupal/libraries": "3.0.0-alpha1", @@ -136,7 +136,7 @@ "drupal/magnific_popup": "1.3", "drupal/mathjax": "2.7", "drupal/media_entity_browser": "2.0-alpha2", - "drupal/media_entity_twitter": "2.0-alpha2", + "drupal/media_entity_twitter": "2.3", "drupal/menu_block": "1.4", "drupal/menu_block_title": "1.1", "drupal/menu_breadcrumb": "1.12", @@ -318,4 +318,4 @@ "php": "7.0.8" } } -} \ No newline at end of file +} diff --git a/composer.lock b/composer.lock index fd58f4396839813c4a9dd5201d7ab9e636712b47..6e8ee99d0c75a54d4341f30f94dbf443417018b0 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f3f9a51e6c09cc6554fa012dbf1ad64d", + "content-hash": "e4746447f1d486ac63e393e17d8faba2", "packages": [ { "name": "alchemy/zippy", @@ -2113,20 +2113,20 @@ }, { "name": "drupal/admin_toolbar", - "version": "2.0.0", + "version": "2.2.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/admin_toolbar.git", - "reference": "8.x-2.0" + "reference": "8.x-2.2" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/admin_toolbar-8.x-2.0.zip", - "reference": "8.x-2.0", - "shasum": "568de63dbaa8046a82d327dbd0b892ab79fb87aa" + "url": "https://ftp.drupal.org/files/projects/admin_toolbar-8.x-2.2.zip", + "reference": "8.x-2.2", + "shasum": "41ea0e3321e6d1e190c486be49a99e60446d8dd7" }, "require": { - "drupal/core": "*" + "drupal/core": "^8.8.0 || ^9.0" }, "type": "drupal-module", "extra": { @@ -2134,8 +2134,8 @@ "dev-2.x": "2.x-dev" }, "drupal": { - "version": "8.x-2.0", - "datestamp": "1573751237", + "version": "8.x-2.2", + "datestamp": "1585017179", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -4612,23 +4612,23 @@ }, { "name": "drupal/entity_reference_revisions", - "version": "1.3.0", + "version": "1.8.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/entity_reference_revisions.git", - "reference": "8.x-1.3" + "reference": "8.x-1.8" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/entity_reference_revisions-8.x-1.3.zip", - "reference": "8.x-1.3", - "shasum": "78aebb58efbbfcbb2faa40a1afc0830312b32631" + "url": "https://ftp.drupal.org/files/projects/entity_reference_revisions-8.x-1.8.zip", + "reference": "8.x-1.8", + "shasum": "c1279e6c683edc2dbccedba8de1505340c8a62b6" }, "require": { - "drupal/core": "~8.0" + "drupal/core": "^8.7.7 || ^9" }, "require-dev": { - "drupal/diff": "*" + "drupal/diff": "1.x-dev" }, "type": "drupal-module", "extra": { @@ -4636,8 +4636,8 @@ "dev-1.x": "1.x-dev" }, "drupal": { - "version": "8.x-1.3", - "datestamp": "1515143885", + "version": "8.x-1.8", + "datestamp": "1583961846", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -4646,9 +4646,13 @@ }, "notification-url": "https://packages.drupal.org/8/downloads", "license": [ - "GPL-2.0-or-later" + "GPL-2.0" ], "authors": [ + { + "name": "Berdir", + "homepage": "https://www.drupal.org/user/214652" + }, { "name": "Frans", "homepage": "https://www.drupal.org/user/514222" @@ -4662,10 +4666,10 @@ "homepage": "https://www.drupal.org/user/227761" } ], - "description": "Adds a Entity Reference field type with revision support.", + "description": "Entity Reference Revisions", "homepage": "https://www.drupal.org/project/entity_reference_revisions", "support": { - "source": "http://cgit.drupalcode.org/entity_reference_revisions" + "source": "https://git.drupalcode.org/project/entity_reference_revisions" } }, { @@ -5187,17 +5191,17 @@ }, { "name": "drupal/honeypot", - "version": "1.28.0", + "version": "1.30.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/honeypot.git", - "reference": "8.x-1.28" + "reference": "8.x-1.30" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/honeypot-8.x-1.28.zip", - "reference": "8.x-1.28", - "shasum": "bbfea8791cee7d88be705e0cbf28bd6a22a54c60" + "url": "https://ftp.drupal.org/files/projects/honeypot-8.x-1.30.zip", + "reference": "8.x-1.30", + "shasum": "1d7983e8e07feee4f13e4b05c9a10db15ae2097e" }, "require": { "drupal/core": "~8.0" @@ -5208,8 +5212,8 @@ "dev-1.x": "1.x-dev" }, "drupal": { - "version": "8.x-1.28", - "datestamp": "1533849180", + "version": "8.x-1.30", + "datestamp": "1576274288", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -5223,8 +5227,16 @@ "authors": [ { "name": "Jeff Geerling", - "homepage": "https://www.drupal.org/user/389011", + "homepage": "https://www.drupal.org/user/213194", "email": "geerlingguy@mac.com" + }, + { + "name": "geerlingguy", + "homepage": "https://www.drupal.org/user/389011" + }, + { + "name": "vijaycs85", + "homepage": "https://www.drupal.org/user/93488" } ], "description": "Mitigates spam form submissions using the honeypot method.", @@ -5238,7 +5250,7 @@ "spam" ], "support": { - "source": "http://cgit.drupalcode.org/honeypot" + "source": "https://git.drupalcode.org/project/honeypot" } }, { @@ -5711,17 +5723,17 @@ }, { "name": "drupal/media_entity_twitter", - "version": "2.0.0-alpha2", + "version": "2.3.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/media_entity_twitter.git", - "reference": "8.x-2.0-alpha2" + "reference": "8.x-2.3" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/media_entity_twitter-8.x-2.0-alpha2.zip", - "reference": "8.x-2.0-alpha2", - "shasum": "21925e1e1b02bbbcd6d8e9730bc587669edc0e5c" + "url": "https://ftp.drupal.org/files/projects/media_entity_twitter-8.x-2.3.zip", + "reference": "8.x-2.3", + "shasum": "de0f25deaa97e09c6851c09ee01c1e2f3f505f48" }, "require": { "drupal/core": "^8.4", @@ -5733,11 +5745,11 @@ "dev-2.x": "2.x-dev" }, "drupal": { - "version": "8.x-2.0-alpha2", - "datestamp": "1507907344", + "version": "8.x-2.3", + "datestamp": "1579597383", "security-coverage": { - "status": "not-covered", - "message": "Alpha releases are not covered by Drupal security advisories." + "status": "covered", + "message": "Covered by Drupal's security advisory policy" } } }, @@ -5758,15 +5770,19 @@ "name": "chr.fritsch", "homepage": "https://www.drupal.org/user/2103716" }, + { + "name": "phenaproxima", + "homepage": "https://www.drupal.org/user/205645" + }, { "name": "slashrsm", "homepage": "https://www.drupal.org/user/744628" } ], - "description": "Media entity Twitter provider.", + "description": "Media Entity Twitter provider.", "homepage": "https://www.drupal.org/project/media_entity_twitter", "support": { - "source": "http://cgit.drupalcode.org/media_entity_twitter" + "source": "https://git.drupalcode.org/project/media_entity_twitter" } }, { @@ -6064,8 +6080,7 @@ "homepage": "https://www.drupal.org/project/migrate_devel", "support": { "source": "http://cgit.drupalcode.org/migrate_devel" - }, - "time": "2017-06-25T23:46:13+00:00" + } }, { "name": "drupal/migrate_plus", diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index 1dcf950ce4129566464e6f27ae1a5efed7f4661c..c55698ce71041d090964b9b4b503c0c2c98c7506 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -2178,21 +2178,21 @@ }, { "name": "drupal/admin_toolbar", - "version": "2.0.0", - "version_normalized": "2.0.0.0", + "version": "2.2.0", + "version_normalized": "2.2.0.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/admin_toolbar.git", - "reference": "8.x-2.0" + "reference": "8.x-2.2" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/admin_toolbar-8.x-2.0.zip", - "reference": "8.x-2.0", - "shasum": "568de63dbaa8046a82d327dbd0b892ab79fb87aa" + "url": "https://ftp.drupal.org/files/projects/admin_toolbar-8.x-2.2.zip", + "reference": "8.x-2.2", + "shasum": "41ea0e3321e6d1e190c486be49a99e60446d8dd7" }, "require": { - "drupal/core": "*" + "drupal/core": "^8.8.0 || ^9.0" }, "type": "drupal-module", "extra": { @@ -2200,8 +2200,8 @@ "dev-2.x": "2.x-dev" }, "drupal": { - "version": "8.x-2.0", - "datestamp": "1573751237", + "version": "8.x-2.2", + "datestamp": "1585017179", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -4748,24 +4748,24 @@ }, { "name": "drupal/entity_reference_revisions", - "version": "1.3.0", - "version_normalized": "1.3.0.0", + "version": "1.8.0", + "version_normalized": "1.8.0.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/entity_reference_revisions.git", - "reference": "8.x-1.3" + "reference": "8.x-1.8" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/entity_reference_revisions-8.x-1.3.zip", - "reference": "8.x-1.3", - "shasum": "78aebb58efbbfcbb2faa40a1afc0830312b32631" + "url": "https://ftp.drupal.org/files/projects/entity_reference_revisions-8.x-1.8.zip", + "reference": "8.x-1.8", + "shasum": "c1279e6c683edc2dbccedba8de1505340c8a62b6" }, "require": { - "drupal/core": "~8.0" + "drupal/core": "^8.7.7 || ^9" }, "require-dev": { - "drupal/diff": "*" + "drupal/diff": "1.x-dev" }, "type": "drupal-module", "extra": { @@ -4773,8 +4773,8 @@ "dev-1.x": "1.x-dev" }, "drupal": { - "version": "8.x-1.3", - "datestamp": "1515143885", + "version": "8.x-1.8", + "datestamp": "1583961846", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -4784,9 +4784,13 @@ "installation-source": "dist", "notification-url": "https://packages.drupal.org/8/downloads", "license": [ - "GPL-2.0-or-later" + "GPL-2.0" ], "authors": [ + { + "name": "Berdir", + "homepage": "https://www.drupal.org/user/214652" + }, { "name": "Frans", "homepage": "https://www.drupal.org/user/514222" @@ -4800,10 +4804,10 @@ "homepage": "https://www.drupal.org/user/227761" } ], - "description": "Adds a Entity Reference field type with revision support.", + "description": "Entity Reference Revisions", "homepage": "https://www.drupal.org/project/entity_reference_revisions", "support": { - "source": "http://cgit.drupalcode.org/entity_reference_revisions" + "source": "https://git.drupalcode.org/project/entity_reference_revisions" } }, { @@ -5343,18 +5347,18 @@ }, { "name": "drupal/honeypot", - "version": "1.28.0", - "version_normalized": "1.28.0.0", + "version": "1.30.0", + "version_normalized": "1.30.0.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/honeypot.git", - "reference": "8.x-1.28" + "reference": "8.x-1.30" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/honeypot-8.x-1.28.zip", - "reference": "8.x-1.28", - "shasum": "bbfea8791cee7d88be705e0cbf28bd6a22a54c60" + "url": "https://ftp.drupal.org/files/projects/honeypot-8.x-1.30.zip", + "reference": "8.x-1.30", + "shasum": "1d7983e8e07feee4f13e4b05c9a10db15ae2097e" }, "require": { "drupal/core": "~8.0" @@ -5365,8 +5369,8 @@ "dev-1.x": "1.x-dev" }, "drupal": { - "version": "8.x-1.28", - "datestamp": "1533849180", + "version": "8.x-1.30", + "datestamp": "1576274288", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -5381,8 +5385,16 @@ "authors": [ { "name": "Jeff Geerling", - "homepage": "https://www.drupal.org/user/389011", + "homepage": "https://www.drupal.org/user/213194", "email": "geerlingguy@mac.com" + }, + { + "name": "geerlingguy", + "homepage": "https://www.drupal.org/user/389011" + }, + { + "name": "vijaycs85", + "homepage": "https://www.drupal.org/user/93488" } ], "description": "Mitigates spam form submissions using the honeypot method.", @@ -5396,7 +5408,7 @@ "spam" ], "support": { - "source": "http://cgit.drupalcode.org/honeypot" + "source": "https://git.drupalcode.org/project/honeypot" } }, { @@ -5885,18 +5897,18 @@ }, { "name": "drupal/media_entity_twitter", - "version": "2.0.0-alpha2", - "version_normalized": "2.0.0.0-alpha2", + "version": "2.3.0", + "version_normalized": "2.3.0.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/media_entity_twitter.git", - "reference": "8.x-2.0-alpha2" + "reference": "8.x-2.3" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/media_entity_twitter-8.x-2.0-alpha2.zip", - "reference": "8.x-2.0-alpha2", - "shasum": "21925e1e1b02bbbcd6d8e9730bc587669edc0e5c" + "url": "https://ftp.drupal.org/files/projects/media_entity_twitter-8.x-2.3.zip", + "reference": "8.x-2.3", + "shasum": "de0f25deaa97e09c6851c09ee01c1e2f3f505f48" }, "require": { "drupal/core": "^8.4", @@ -5908,11 +5920,11 @@ "dev-2.x": "2.x-dev" }, "drupal": { - "version": "8.x-2.0-alpha2", - "datestamp": "1507907344", + "version": "8.x-2.3", + "datestamp": "1579597383", "security-coverage": { - "status": "not-covered", - "message": "Alpha releases are not covered by Drupal security advisories." + "status": "covered", + "message": "Covered by Drupal's security advisory policy" } } }, @@ -5934,15 +5946,19 @@ "name": "chr.fritsch", "homepage": "https://www.drupal.org/user/2103716" }, + { + "name": "phenaproxima", + "homepage": "https://www.drupal.org/user/205645" + }, { "name": "slashrsm", "homepage": "https://www.drupal.org/user/744628" } ], - "description": "Media entity Twitter provider.", + "description": "Media Entity Twitter provider.", "homepage": "https://www.drupal.org/project/media_entity_twitter", "support": { - "source": "http://cgit.drupalcode.org/media_entity_twitter" + "source": "https://git.drupalcode.org/project/media_entity_twitter" } }, { diff --git a/web/modules/admin_toolbar/admin_toolbar.info.yml b/web/modules/admin_toolbar/admin_toolbar.info.yml index b55a509cffc796765208f3105fcca7b074c03c77..382e37ffb632cbf53b8a88d3ddf18e953a7b7a4b 100644 --- a/web/modules/admin_toolbar/admin_toolbar.info.yml +++ b/web/modules/admin_toolbar/admin_toolbar.info.yml @@ -3,12 +3,12 @@ description: Provides an improved drop-down menu interface to the site Toolbar. package: Administration type: module -core: 8.x +core_version_requirement: ^8.8.0 || ^9.0 dependencies: - drupal:toolbar -# Information added by Drupal.org packaging script on 2019-10-29 -version: '8.x-2.0' +# Information added by Drupal.org packaging script on 2020-03-24 +version: '8.x-2.2' project: 'admin_toolbar' -datestamp: 1572370993 +datestamp: 1585017182 diff --git a/web/modules/admin_toolbar/admin_toolbar.libraries.yml b/web/modules/admin_toolbar/admin_toolbar.libraries.yml index 34306becdb84df10b5c3eb40e0aac95051aaa295..85b322a4e6f1bf851f781529e6140a7e66acd86b 100755 --- a/web/modules/admin_toolbar/admin_toolbar.libraries.yml +++ b/web/modules/admin_toolbar/admin_toolbar.libraries.yml @@ -8,14 +8,3 @@ toolbar.tree: dependencies: - core/jquery - core/drupal -search: - css: - theme: - css/admin.toolbar_search.css: {} - js: - js/admin_toolbar_search.js: {} - dependencies: - - core/jquery - - core/drupal - - core/jquery.once - - core/jquery.ui.autocomplete diff --git a/web/modules/admin_toolbar/admin_toolbar.module b/web/modules/admin_toolbar/admin_toolbar.module index 3c5e81bda093621abd53be5d6507e8c576e97a49..c02c3f4868df2827ffd830eb170de560b15bab62 100755 --- a/web/modules/admin_toolbar/admin_toolbar.module +++ b/web/modules/admin_toolbar/admin_toolbar.module @@ -5,57 +5,17 @@ * This is the module to create a drop-down menu for the core toolbar. */ -use Drupal\Core\Menu\MenuTreeParameters; +use Drupal\admin_toolbar\Render\Element\AdminToolbar; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Url; use Drupal\Component\Utility\Html; -use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Implements hook_toolbar_alter(). */ function admin_toolbar_toolbar_alter(&$items) { - $items['administration']['tray']['toolbar_administration']['#pre_render'] = ['admin_toolbar_prerender_toolbar_administration_tray']; + $items['administration']['tray']['toolbar_administration']['#pre_render'] = [[AdminToolbar::class, 'preRenderTray']]; $items['administration']['#attached']['library'][] = 'admin_toolbar/toolbar.tree'; - $admin_toolbar_tools = \Drupal::service('module_handler') - ->moduleExists('admin_toolbar_tools'); - $items['administration_search'] = [ - "#type" => "toolbar_item", - 'tab' => [ - '#type' => 'link', - '#title' => new TranslatableMarkup('Search'), - '#url' => URL::fromRoute('system.admin'), - '#attributes' => [ - 'class' => [ - 'toolbar-icon', - ], - ], - ], - 'tray' => [ - 'search' => [ - '#title' => 'Search', - '#type' => 'textfield', - '#size' => 60, - '#attributes' => [ - 'id' => 'admin-toolbar-search-input', - 'aria-labelledby' => 'toolbar-item-administration-search', - ], - ], - ], - '#attached' => [ - 'library' => [ - 'admin_toolbar/search', - ], - 'drupalSettings' => [ - 'adminToolbarSearch' => [ - 'loadExtraLinks' => $admin_toolbar_tools, - ], - ], - ], - '#wrapper_attributes' => [ - "id" => "admin-toolbar-search-tab", - ], - ]; } /** @@ -77,35 +37,6 @@ function admin_toolbar_help($route_name, RouteMatchInterface $route_match) { } } -/** - * Renders the toolbar's administration tray. - * - * This is a clone of core's toolbar_prerender_toolbar_administration_tray() - * function, which uses setMaxDepth(4) instead of setTopLevelOnly(). - * - * @param array $element - * A renderable array. - * - * @return array - * The updated renderable array. - * - * @see toolbar_prerender_toolbar_administration_tray() - */ -function admin_toolbar_prerender_toolbar_administration_tray(array $element) { - $menu_tree = \Drupal::service('toolbar.menu_tree'); - $parameters = new MenuTreeParameters(); - $parameters->setRoot('system.admin')->excludeRoot()->setMaxDepth(4)->onlyEnabledLinks(); - $tree = $menu_tree->load(NULL, $parameters); - $manipulators = [ - ['callable' => 'menu.default_tree_manipulators:checkAccess'], - ['callable' => 'menu.default_tree_manipulators:generateIndexAndSort'], - ['callable' => 'toolbar_tools_menu_navigation_links'], - ]; - $tree = $menu_tree->transform($tree, $manipulators); - $element['administration_menu'] = $menu_tree->build($tree); - return $element; -} - /** * Adds toolbar-specific attributes to the menu link tree. * diff --git a/web/modules/admin_toolbar/admin_toolbar_links_access_filter/admin_toolbar_links_access_filter.info.yml b/web/modules/admin_toolbar/admin_toolbar_links_access_filter/admin_toolbar_links_access_filter.info.yml index 9ac6cdb5a387e84a74bbd69b663f63277aa8c0ae..1b8b648077189fd2a7441a268e4a2abec439768f 100644 --- a/web/modules/admin_toolbar/admin_toolbar_links_access_filter/admin_toolbar_links_access_filter.info.yml +++ b/web/modules/admin_toolbar/admin_toolbar_links_access_filter/admin_toolbar_links_access_filter.info.yml @@ -3,12 +3,12 @@ description: Provides a workaround for the common problem that users with 'Use t package: Administration type: module -core: 8.x +core_version_requirement: ^8.8.0 || ^9.0 dependencies: - admin_toolbar:admin_toolbar -# Information added by Drupal.org packaging script on 2019-10-29 -version: '8.x-2.0' +# Information added by Drupal.org packaging script on 2020-03-24 +version: '8.x-2.2' project: 'admin_toolbar' -datestamp: 1572370993 +datestamp: 1585017182 diff --git a/web/modules/admin_toolbar/admin_toolbar_links_access_filter/composer.json b/web/modules/admin_toolbar/admin_toolbar_links_access_filter/composer.json index d75f3b0c68ce40945ebd60764c3b2bd98e30b89c..b7a007d7be8a5f115b3167e675baad91560418a8 100755 --- a/web/modules/admin_toolbar/admin_toolbar_links_access_filter/composer.json +++ b/web/modules/admin_toolbar/admin_toolbar_links_access_filter/composer.json @@ -26,13 +26,14 @@ "name": "Mohamed Anis Taktak (matio89)", "homepage": "https://www.drupal.org/u/matio89", "role": "Maintainer" - } + } ], "support": { "issues": "https://www.drupal.org/project/issues/admin_toolbar", "source": "http://cgit.drupalcode.org/admin_toolbar" }, "require": { - "drupal/admin_toolbar": "^1" + "drupal/admin_toolbar": "^2", + "drupal/core": "^8.8.0 || ^9.0" } } diff --git a/web/modules/admin_toolbar/admin_toolbar_search/admin_toolbar_search.info.yml b/web/modules/admin_toolbar/admin_toolbar_search/admin_toolbar_search.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..3dcad3ac3cfa7958994009a3a2f72a533a0eb6e8 --- /dev/null +++ b/web/modules/admin_toolbar/admin_toolbar_search/admin_toolbar_search.info.yml @@ -0,0 +1,14 @@ +name: Admin Toolbar Search +description: Provides search of admin toolbar items. +package: Administration + +type: module +core_version_requirement: ^8.8.0 || ^9.0 + +dependencies: + - admin_toolbar:admin_toolbar + +# Information added by Drupal.org packaging script on 2020-03-24 +version: '8.x-2.2' +project: 'admin_toolbar' +datestamp: 1585017182 diff --git a/web/modules/admin_toolbar/admin_toolbar_search/admin_toolbar_search.libraries.yml b/web/modules/admin_toolbar/admin_toolbar_search/admin_toolbar_search.libraries.yml new file mode 100644 index 0000000000000000000000000000000000000000..4f3bedac46babd32003f1b15844bf0ad29640869 --- /dev/null +++ b/web/modules/admin_toolbar/admin_toolbar_search/admin_toolbar_search.libraries.yml @@ -0,0 +1,11 @@ +search: + css: + theme: + css/admin.toolbar_search.css: {} + js: + js/admin_toolbar_search.js: {} + dependencies: + - core/jquery + - core/drupal + - core/jquery.once + - core/jquery.ui.autocomplete diff --git a/web/modules/admin_toolbar/admin_toolbar_search/admin_toolbar_search.module b/web/modules/admin_toolbar/admin_toolbar_search/admin_toolbar_search.module new file mode 100755 index 0000000000000000000000000000000000000000..acad2f23cf0384bf4a413ed76a3c1268c182f31a --- /dev/null +++ b/web/modules/admin_toolbar/admin_toolbar_search/admin_toolbar_search.module @@ -0,0 +1,79 @@ +<?php + +/** + * @file + * Functionality for search of Admin Toolbar. + */ + +use Drupal\Core\StringTranslation\TranslatableMarkup; +use Drupal\Core\Url; +use Drupal\Core\Routing\RouteMatchInterface; + +/** + * Implements hook_help(). + */ +function admin_toolbar_search_help($route_name, RouteMatchInterface $route_match) { + switch ($route_name) { + // Main module help. + case 'help.page.admin_toolbar_search': + $output = ''; + $output .= '<h3>' . t('About') . '</h3>'; + $output .= '<p>' . t('The Admin Toolbar Search module add a search option to the toolbar for site administrative tasks.') . '</p>'; + + return $output; + } +} + +/** + * Implements hook_toolbar_alter(). + */ +function admin_toolbar_search_toolbar_alter(&$items) { + $access = \Drupal::currentUser()->hasPermission('use admin toolbar search'); + $admin_toolbar_tools_enabled = \Drupal::service('module_handler') + ->moduleExists('admin_toolbar_tools'); + + $items['administration_search'] = [ + "#type" => "toolbar_item", + '#access' => $access, + 'tab' => [ + '#type' => 'link', + '#title' => new TranslatableMarkup('Search'), + '#url' => URL::fromRoute('system.admin'), + '#attributes' => [ + 'class' => [ + 'toolbar-icon', + ], + ], + ], + 'tray' => [ + 'search' => [ + '#title' => 'Search', + '#type' => 'textfield', + '#size' => 60, + '#attributes' => [ + 'id' => 'admin-toolbar-search-input', + 'aria-labelledby' => 'toolbar-item-administration-search', + ], + ], + ], + '#attached' => [ + 'library' => [ + 'admin_toolbar_search/search', + ], + 'drupalSettings' => [ + 'adminToolbarSearch' => [ + 'loadExtraLinks' => $admin_toolbar_tools_enabled, + ], + ], + ], + '#wrapper_attributes' => [ + 'id' => 'admin-toolbar-search-tab', + ], + '#cache' => [ + 'contexts' => [ + 'user.permissions', + ], + ], + ]; + +} diff --git a/web/modules/admin_toolbar/admin_toolbar_search/admin_toolbar_search.permissions.yml b/web/modules/admin_toolbar/admin_toolbar_search/admin_toolbar_search.permissions.yml new file mode 100644 index 0000000000000000000000000000000000000000..b0d7ec73c5ad73a0e6a864c75e01dab020f7abba --- /dev/null +++ b/web/modules/admin_toolbar/admin_toolbar_search/admin_toolbar_search.permissions.yml @@ -0,0 +1,2 @@ +use admin toolbar search: + title: 'Use Admin Toolbar search' diff --git a/web/modules/admin_toolbar/admin_toolbar_search/admin_toolbar_search.routing.yml b/web/modules/admin_toolbar/admin_toolbar_search/admin_toolbar_search.routing.yml new file mode 100755 index 0000000000000000000000000000000000000000..14adc551599cc40ab387c0ee68b7efadd407f115 --- /dev/null +++ b/web/modules/admin_toolbar/admin_toolbar_search/admin_toolbar_search.routing.yml @@ -0,0 +1,6 @@ +admin_toolbar.search: + path: '/admin/admin-toolbar-search' + defaults: + _controller: '\Drupal\admin_toolbar_search\Controller\AdminToolbarSearchController::search' + requirements: + _permission: 'use admin toolbar search' diff --git a/web/modules/admin_toolbar/admin_toolbar_search/admin_toolbar_search.services.yml b/web/modules/admin_toolbar/admin_toolbar_search/admin_toolbar_search.services.yml new file mode 100644 index 0000000000000000000000000000000000000000..977e6ce2c3aba9cdccca8f7065c799847a953bf5 --- /dev/null +++ b/web/modules/admin_toolbar/admin_toolbar_search/admin_toolbar_search.services.yml @@ -0,0 +1,9 @@ +services: + admin_toolbar_search.search_links: + class: Drupal\admin_toolbar_search\SearchLinks + arguments: + - '@entity_type.manager' + - '@module_handler' + - '@router.route_provider' + - '@cache_contexts_manager' + - '@cache.toolbar' diff --git a/web/modules/admin_toolbar/admin_toolbar_search/composer.json b/web/modules/admin_toolbar/admin_toolbar_search/composer.json new file mode 100755 index 0000000000000000000000000000000000000000..919f43f82baf791b5aaa52662deea3bc382f2447 --- /dev/null +++ b/web/modules/admin_toolbar/admin_toolbar_search/composer.json @@ -0,0 +1,39 @@ +{ + "name": "drupal/admin_toolbar_search", + "description": "Provides search of admin toolbar items.", + "type": "drupal-module", + "keywords": ["Drupal", "Toolbar", "Search"], + "homepage": "http://drupal.org/project/admin_toolbar", + "license": "GPL-2.0+", + "authors": [ + { + "name": "Wilfrid Roze (eme)", + "homepage": "https://www.drupal.org/u/eme", + "role": "Maintainer" + }, + { + "name": "Romain Jarraud (romainj)", + "homepage": "https://www.drupal.org/u/romainj", + "role": "Maintainer" + }, + { + "name": "Adrian Cid Almaguer (adriancid)", + "email": "adriancid@gmail.com", + "homepage": "https://www.drupal.org/u/adriancid", + "role": "Maintainer" + }, + { + "name": "Mohamed Anis Taktak (matio89)", + "homepage": "https://www.drupal.org/u/matio89", + "role": "Maintainer" + } + ], + "support": { + "issues": "https://www.drupal.org/project/issues/admin_toolbar", + "source": "http://cgit.drupalcode.org/admin_toolbar" + }, + "require": { + "drupal/admin_toolbar": "^2", + "drupal/core": "^8.8.0 || ^9.0" + } +} diff --git a/web/modules/admin_toolbar/admin_toolbar_search/css/admin.toolbar_search.css b/web/modules/admin_toolbar/admin_toolbar_search/css/admin.toolbar_search.css new file mode 100755 index 0000000000000000000000000000000000000000..6215d9b625ea6d9ce16cee0caee2155734314d9d --- /dev/null +++ b/web/modules/admin_toolbar/admin_toolbar_search/css/admin.toolbar_search.css @@ -0,0 +1,33 @@ +#toolbar-item-administration-search-tray { + padding-left: 1em; +} + +#admin-toolbar-search-tab .toolbar-item:before { + background-image: url('../../misc/icons/bebebe/loupe.svg'); +} + +#admin-toolbar-search-tab .toolbar-item:active:before, +#admin-toolbar-search-tab .toolbar-item.is-active:before { + background-image: url('../../misc/icons/ffffff/loupe.svg'); +} + +#toolbar-item-administration-search-tray label { + display: inline-block; + color: #000000; + margin-right: .5em; + font-weight: bold; +} + +#toolbar-item-administration-search-tray div.form-item { + margin: 0.75em 0; +} + +#toolbar-item-administration-search-tray input { + display: inline-block; + padding: 0.3em 0.4em 0.3em 0.5em; + font-size: 1em; +} + +.ui-autocomplete .ui-menu-item span.admin-toolbar-search-url { + color: rgba(0, 0, 0, 0.50); +} diff --git a/web/modules/admin_toolbar/admin_toolbar_search/js/admin_toolbar_search.js b/web/modules/admin_toolbar/admin_toolbar_search/js/admin_toolbar_search.js new file mode 100755 index 0000000000000000000000000000000000000000..701d76fb6c2caf698eb1f4c3310e904d9368e5e9 --- /dev/null +++ b/web/modules/admin_toolbar/admin_toolbar_search/js/admin_toolbar_search.js @@ -0,0 +1,188 @@ +/** + * @file + * Behaviors for the search widget in the admin toolbar. + */ + +(function ($, Drupal) { + + 'use strict'; + + Drupal.behaviors.adminToolbarSearch = { + + // If extra links have been fetched. + extraFetched: false, + + attach: function (context) { + if (context != document) { + return; + } + + var getUrl = window.location; + var baseUrl = getUrl.protocol + "//" + getUrl.host + "/"; + var $self = this; + this.links = []; + $('.toolbar-tray a[data-drupal-link-system-path]').each(function () { + if (this.href != baseUrl) { + var label = $self.getItemLabel(this); + $self.links.push({ + 'value': $(this).attr('href'), + 'label': label + ' ' + $(this).attr('href'), + 'labelRaw': label + }); + } + }); + + $("#admin-toolbar-search-input").autocomplete({ + minLength: 2, + source: function (request, response) { + var data = $self.handleAutocomplete(request.term); + if (!$self.extraFetched && drupalSettings.adminToolbarSearch.loadExtraLinks) { + $.getJSON( "/admin/admin-toolbar-search", function( data ) { + $(data).each(function() { + var item = this; + item.label = this.labelRaw + ' ' + this.value; + $self.links.push(item); + }); + + $self.extraFetched = true; + + var results = $self.handleAutocomplete(request.term); + response(results); + }); + } + else { + response(data); + } + }, + open: function () { + var zIndex = $('#toolbar-item-administration-search-tray') + .css("z-index") + 1; + $(this).autocomplete('widget').css('z-index', zIndex); + + return false; + }, + select: function (event, ui) { + if (ui.item.value) { + location.href = ui.item.value; + return false; + } + } + }).data("ui-autocomplete")._renderItem = (function (ul, item) { + return $("<li>") + .append('<div>' + item.labelRaw + ' <span class="admin-toolbar-search-url">' + item.value + '</span></div>') + .appendTo(ul); + }); + + // Focus on search field when tab is clicked, or enter is pressed. + $(context).find('#toolbar-item-administration-search') + .once('admin_toolbar_search') + .each(function () { + if (Drupal.behaviors.adminToolbarSearch.isSearchVisible()) { + $('#admin-toolbar-search-input').focus(); + } + $(this).on('click', function () { + $self.focusOnSearchElement(); + }); + }); + + // Initialize hotkey / keyboard shortcut. + this.initHotkey(); + }, + focusOnSearchElement: function () { + var waitforVisible = function () { + if ($('#toolbar-item-administration-search-tray:visible').length) { + $('#admin-toolbar-search-input').focus(); + } + else { + setTimeout(function () { + waitforVisible(); + }, 1); + } + }; + waitforVisible(); + }, + getItemLabel: function (item) { + var breadcrumbs = []; + $(item).parents().each(function () { + if ($(this).hasClass('menu-item')) { + var $link = $(this).find('a:first'); + if ($link.length && !$link.hasClass('admin-toolbar-search-ignore')) { + breadcrumbs.unshift($link.text()); + } + } + }); + return breadcrumbs.join(' > '); + }, + handleAutocomplete: function (term) { + var $self = this; + var keywords = term.split(" "); // Split search terms into list. + + var suggestions = []; + $self.links.forEach(function (element) { + var label = element.label.toLowerCase(); + + // Add exact matches. + if (label.indexOf(term.toLowerCase()) >= 0) { + suggestions.push(element); + } + else { + // Add suggestions where it matches all search terms. + var matchCount = 0; + keywords.forEach(function (keyword) { + if (label.indexOf(keyword.toLowerCase()) >= 0) { + matchCount++; + } + }); + if (matchCount == keywords.length) { + suggestions.push(element); + } + } + }); + return suggestions; + }, + /** + * Whether the search is visible or not. + * + * @returns {boolean} + * True if visible, false otherwise. + */ + isSearchVisible: function () { + return $('#toolbar-item-administration-search-tray').is(':visible'); + }, + /** + * Toggles the toolbar search tray. + */ + toggleSearch: function () { + $('#toolbar-item-administration-search').trigger('click'); + }, + /** + * Binds a keyboard shortcut to toggle the search. + */ + initHotkey: function () { + $(document) + .once('admin_toolbar_search') + .keydown(function (event) { + // Show the form with alt + S. + if (!Drupal.behaviors.adminToolbarSearch.isSearchVisible()) { + // 83 = s. + if (event.altKey === true && event.keyCode === 83) { + Drupal.behaviors.adminToolbarSearch.toggleSearch(); + event.preventDefault(); + } + } + // Hide the search with alt + S or ESC. + else { + // 83 = s. + if ( + (event.altKey === true && event.keyCode === 83) || + event.key === 'Escape' + ) { + Drupal.behaviors.adminToolbarSearch.toggleSearch(); + event.preventDefault(); + } + } + }); + } + }; + +})(jQuery, Drupal); diff --git a/web/modules/admin_toolbar/admin_toolbar_search/src/Controller/AdminToolbarSearchController.php b/web/modules/admin_toolbar/admin_toolbar_search/src/Controller/AdminToolbarSearchController.php new file mode 100755 index 0000000000000000000000000000000000000000..d3ae3325b72811057ed7f351b51208ba3e689e2c --- /dev/null +++ b/web/modules/admin_toolbar/admin_toolbar_search/src/Controller/AdminToolbarSearchController.php @@ -0,0 +1,50 @@ +<?php + +namespace Drupal\admin_toolbar_search\Controller; + +use Drupal\admin_toolbar_search\SearchLinks; +use Drupal\Core\Controller\ControllerBase; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpFoundation\JsonResponse; + +/** + * Class AdminToolbarSearchController. + * + * @package Drupal\admin_toolbar_tools\Controller + */ +class AdminToolbarSearchController extends ControllerBase { + + /** + * The search links service. + * + * @var \Drupal\admin_toolbar_search\SearchLinks + */ + protected $links; + + /** + * Constructs an AdminToolbarSearchController object. + * + * @param \Drupal\admin_toolbar_search\SearchLinks $links + * The search links service. + */ + public function __construct(SearchLinks $links) { + $this->links = $links; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('admin_toolbar_search.search_links') + ); + } + + /** + * Return additional search links. + */ + public function search() { + return new JsonResponse($this->links->getLinks()); + } + +} diff --git a/web/modules/admin_toolbar/admin_toolbar_tools/src/SearchLinks.php b/web/modules/admin_toolbar/admin_toolbar_search/src/SearchLinks.php similarity index 92% rename from web/modules/admin_toolbar/admin_toolbar_tools/src/SearchLinks.php rename to web/modules/admin_toolbar/admin_toolbar_search/src/SearchLinks.php index 703edbee5311bebbf5389d3a7cf27874e9c13401..e36dba37a9617b3de26c7728fa4dc198f9dc3b9c 100644 --- a/web/modules/admin_toolbar/admin_toolbar_tools/src/SearchLinks.php +++ b/web/modules/admin_toolbar/admin_toolbar_search/src/SearchLinks.php @@ -1,6 +1,6 @@ <?php -namespace Drupal\admin_toolbar_tools; +namespace Drupal\admin_toolbar_search; use Drupal\admin_toolbar_tools\Plugin\Derivative\ExtraLinks; use Drupal\Core\Cache\Cache; @@ -57,7 +57,18 @@ class SearchLinks { protected $toolbarCache; /** - * {@inheritdoc} + * Constructs a SearchLinks object. + * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler. + * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider + * The route provider. + * @param \Drupal\Core\Cache\Context\CacheContextsManager $cache_context_manager + * The cache contexts manager. + * @param \Drupal\Core\Cache\CacheBackendInterface $toolbar_cache + * Cache backend instance to use. */ public function __construct(EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, RouteProviderInterface $route_provider, CacheContextsManager $cache_context_manager, CacheBackendInterface $toolbar_cache) { $this->entityTypeManager = $entity_type_manager; @@ -68,10 +79,10 @@ public function __construct(EntityTypeManagerInterface $entity_type_manager, Mod } /** - * Get extra links for admin toolbar search feature. + * Gets extra links for admin toolbar search feature. * * @return array - * An array of link data. + * An array of link data for the JSON used for search. * * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException @@ -239,7 +250,7 @@ public function getLinks() { } /** - * Get a list of content entities. + * Gets a list of content entities. * * @return array * An array of metadata about content entities. @@ -258,19 +269,6 @@ protected function getBundleableEntitiesList() { return $content_entities; } - /** - * Get an array of entity types that should trigger a menu rebuild. - * - * @return array - * An array of entity machine names. - */ - public function getRebuildEntityTypes() { - $types = ['menu']; - $content_entities = $this->getBundleableEntitiesList(); - $types = array_merge($types, array_column($content_entities, 'content_entity_bundle')); - return $types; - } - /** * Determine if a route exists by name. * diff --git a/web/modules/admin_toolbar/admin_toolbar_search/tests/src/FunctionalJavascript/AdminToolbarSearchTest.php b/web/modules/admin_toolbar/admin_toolbar_search/tests/src/FunctionalJavascript/AdminToolbarSearchTest.php new file mode 100755 index 0000000000000000000000000000000000000000..f12b8f8543c77c0fa73aba069a55195646199c96 --- /dev/null +++ b/web/modules/admin_toolbar/admin_toolbar_search/tests/src/FunctionalJavascript/AdminToolbarSearchTest.php @@ -0,0 +1,47 @@ +<?php + +namespace Drupal\Tests\admin_toolbar_search\FunctionalJavascript; + +/** + * Test the functionality of admin toolbar search. + * + * @group admin_toolbar + * @group admin_toolbar_search + */ +class AdminToolbarSearchTest extends AdminToolbarSearchTestBase { + + /** + * Tests search functionality without admin_toolbar_tools enabled. + */ + public function testToolbarSearch() { + $search_tab = '#toolbar-item-administration-search'; + $search_tray = '#toolbar-item-administration-search-tray'; + + $this->drupalLogin($this->userWithAccess); + $assert_session = $this->assertSession(); + $assert_session->responseContains('admin.toolbar_search.css'); + $assert_session->responseContains('admin_toolbar_search.js'); + $assert_session->waitForElementVisible('css', $search_tab)->click(); + $assert_session->waitForElementVisible('css', $search_tray); + + $this->assertSuggestionContains('perfor', 'admin/config/development/performance'); + $this->assertSuggestionContains('develop', 'admin/config/development/maintenance'); + $this->assertSuggestionContains('types', 'admin/structure/types'); + } + + /** + * Tests a user without the search permission can't use search. + */ + public function testNoAccess() { + $search_tab = '#toolbar-item-administration-search'; + $search_tray = '#toolbar-item-administration-search-tray'; + + $this->drupalLogin($this->noAccessUser); + $assert_session = $this->assertSession(); + $assert_session->responseNotContains('admin.toolbar_search.css'); + $assert_session->responseNotContains('admin_toolbar_search.js'); + $assert_session->elementNotExists('css', $search_tab); + $assert_session->elementNotExists('css', $search_tray); + } + +} diff --git a/web/modules/admin_toolbar/admin_toolbar_search/tests/src/FunctionalJavascript/AdminToolbarSearchTestBase.php b/web/modules/admin_toolbar/admin_toolbar_search/tests/src/FunctionalJavascript/AdminToolbarSearchTestBase.php new file mode 100755 index 0000000000000000000000000000000000000000..6a16c5882e0f863a4a4ca078a6f163fc3e911859 --- /dev/null +++ b/web/modules/admin_toolbar/admin_toolbar_search/tests/src/FunctionalJavascript/AdminToolbarSearchTestBase.php @@ -0,0 +1,192 @@ +<?php + +namespace Drupal\Tests\admin_toolbar_search\FunctionalJavascript; + +use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use Drupal\system\Entity\Menu; + +/** + * Base class for testing the functionality of admin toolbar search. + * + * @group admin_toolbar + * @group admin_toolbar_search + */ +abstract class AdminToolbarSearchTestBase extends WebDriverTestBase { + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * {@inheritdoc} + */ + public static $modules = [ + 'admin_toolbar_search', + 'node', + 'media', + 'field_ui', + 'menu_ui', + 'block', + ]; + + /** + * A user with the 'Use Admin Toolbar search' permission. + * + * @var \Drupal\user\UserInterface + */ + protected $userWithAccess; + + /** + * A test user without the 'Use Admin Toolbar search' permission.. + * + * @var \Drupal\user\UserInterface + */ + protected $noAccessUser; + + /** + * {@inheritdoc} + */ + public function setUp() { + parent::setUp(); + + $baby_names = [ + 'ada' => 'Ada', + 'amara' => 'Amara', + 'amelia' => 'Amelia', + 'arabella' => 'Arabella', + 'asher' => 'Asher', + 'astrid' => 'Astrid', + 'atticus' => 'Atticus', + 'aurora' => 'Aurora', + 'ava' => 'Ava', + 'cora' => 'Cora', + 'eleanor' => 'Eleanor', + 'eloise' => 'Eloise', + 'felix' => 'Felix', + 'freya' => 'Freya', + 'genevieve' => 'Genevieve', + 'isla' => 'Isla', + 'jasper' => 'Jasper', + 'luna' => 'Luna', + 'maeve' => 'Maeve', + 'milo' => 'Milo', + 'nora' => 'Nora', + 'olivia' => 'Olivia', + 'ophelia' => 'Ophelia', + 'posie' => 'Posie', + 'rose' => 'Rose', + 'silas' => 'Silas', + 'soren' => 'Soren', + ]; + + foreach ($baby_names as $id => $label) { + $menu = Menu::create([ + 'id' => $id, + 'label' => $label, + ]); + $menu->save(); + } + + $this->drupalPlaceBlock('local_tasks_block'); + + $permissions = [ + 'access toolbar', + 'administer menu', + 'access administration pages', + 'administer site configuration', + 'administer content types', + ]; + $this->noAccessUser = $this->drupalCreateUser($permissions); + $permissions[] = 'use admin toolbar search'; + $this->userWithAccess = $this->drupalCreateUser($permissions); + } + + /** + * Assert that the search suggestions contain a given string with given input. + * + * @param string $search + * The string to search for. + * @param string $contains + * Some HTML that is expected to be within the suggestions element. + */ + protected function assertSuggestionContains($search, $contains) { + $this->resetSearch(); + $page = $this->getSession()->getPage(); + $page->fillField('admin-toolbar-search-input', $search); + $this->getSession()->getDriver()->keyDown('//input[@id="admin-toolbar-search-input"]', ' '); + $page->waitFor(3, function () use ($page) { + return ($page->find('css', 'ul.ui-autocomplete')->isVisible() === TRUE); + }); + $suggestions_markup = $page->find('css', 'ul.ui-autocomplete')->getHtml(); + $this->assertContains($contains, $suggestions_markup); + } + + /** + * Assert that the search suggestions does not contain a given string. + * + * Assert that the search suggestions does not contain a given string with a + * given input. + * + * @param string $search + * The string to search for. + * @param string $contains + * Some HTML that is not expected to be within the suggestions element. + */ + protected function assertSuggestionNotContains($search, $contains) { + $this->resetSearch(); + $page = $this->getSession()->getPage(); + $page->fillField('admin-toolbar-search-input', $search); + $this->getSession()->getDriver()->keyDown('//input[@id="admin-toolbar-search-input"]', ' '); + $page->waitFor(3, function () use ($page) { + return ($page->find('css', 'ul.ui-autocomplete')->isVisible() === TRUE); + }); + if ($page->find('css', 'ul.ui-autocomplete')->isVisible() === FALSE) { + return; + } + else { + $suggestions_markup = $page->find('css', 'ul.ui-autocomplete')->getHtml(); + $this->assertNotContains($contains, $suggestions_markup); + } + } + + /** + * Search for an empty string to clear out the autocomplete suggestions. + */ + protected function resetSearch() { + $page = $this->getSession()->getPage(); + // Empty out the suggestions. + $page->fillField('admin-toolbar-search-input', ''); + $this->getSession()->getDriver()->keyDown('//input[@id="admin-toolbar-search-input"]', ' '); + $page->waitFor(3, function () use ($page) { + return ($page->find('css', 'ul.ui-autocomplete')->isVisible() === FALSE); + }); + } + + /** + * Checks that there is a link with the specified url in the admin toolbar. + * + * @param string $url + * The url to assert exists in the admin menu. + * + * @throws \Behat\Mink\Exception\ElementNotFoundException + */ + protected function assertMenuHasHref($url) { + $this->assertSession() + ->elementExists('xpath', '//div[@id="toolbar-item-administration-tray"]//a[contains(@href, "' . $url . '")]'); + } + + /** + * Checks that there is no link with the specified url in the admin toolbar. + * + * @param string $url + * The url to assert exists in the admin menu. + * + * @throws \Behat\Mink\Exception\ExpectationException + */ + protected function assertMenuDoesNotHaveHref($url) { + $this->assertSession() + ->elementNotExists('xpath', '//div[@id="toolbar-item-administration-tray"]//a[contains(@href, "' . $url . '")]'); + } + +} diff --git a/web/modules/admin_toolbar/admin_toolbar_search/tests/src/FunctionalJavascript/AdminToolbarToolsSearchTest.php b/web/modules/admin_toolbar/admin_toolbar_search/tests/src/FunctionalJavascript/AdminToolbarToolsSearchTest.php new file mode 100755 index 0000000000000000000000000000000000000000..239b6ef332fdf4643ddb53c29ce272bb34cc0345 --- /dev/null +++ b/web/modules/admin_toolbar/admin_toolbar_search/tests/src/FunctionalJavascript/AdminToolbarToolsSearchTest.php @@ -0,0 +1,213 @@ +<?php + +namespace Drupal\Tests\admin_toolbar_search\FunctionalJavascript; + +use Drupal\media\Entity\MediaType; +use Drupal\Tests\media\Traits\MediaTypeCreationTrait; + +/** + * Test the functionality of admin toolbar search. + * + * @group admin_toolbar + * @group admin_toolbar_search + */ +class AdminToolbarToolsSearchTest extends AdminToolbarSearchTestBase { + + use MediaTypeCreationTrait; + + /** + * {@inheritdoc} + */ + public static $modules = [ + 'admin_toolbar_tools', + 'admin_toolbar_search', + 'node', + 'media', + 'field_ui', + 'menu_ui', + 'block', + ]; + + /** + * The admin user for tests. + * + * @var \Drupal\user\UserInterface + */ + protected $adminUser; + + /** + * {@inheritdoc} + */ + public function setUp() { + parent::setUp(); + + $this->drupalCreateContentType([ + 'type' => 'article', + 'name' => 'Article', + ]); + + $dog_names = [ + 'archie' => 'Archie', + 'bailey' => 'Bailey', + 'bella' => 'Bella', + 'buddy' => 'Buddy', + 'charlie' => 'Charlie', + 'coco' => 'Coco', + 'daisy' => 'Daisy', + 'frankie' => 'Frankie', + 'jack' => 'Jack', + 'lola' => 'Lola', + 'lucy' => 'Lucy', + 'max' => 'Max', + 'milo' => 'Milo', + 'molly' => 'Molly', + 'ollie' => 'Ollie', + 'oscar' => 'Oscar', + 'rosie' => 'Rosie', + 'ruby' => 'Ruby', + 'teddy' => 'Teddy', + 'toby' => 'Toby', + ]; + + foreach ($dog_names as $machine_name => $label) { + $this->createMediaType('image', [ + 'id' => $machine_name, + 'label' => $label, + ]); + } + + $this->adminUser = $this->drupalCreateUser([ + 'access toolbar', + 'administer menu', + 'access administration pages', + 'administer site configuration', + 'administer content types', + 'administer node fields', + 'access media overview', + 'administer media', + 'administer media fields', + 'administer media form display', + 'administer media display', + 'administer media types', + 'use admin toolbar search', + ]); + } + + /** + * Tests search functionality with admin_toolbar_tools enabled. + */ + public function testToolbarSearch() { + $search_tab = '#toolbar-item-administration-search'; + $search_tray = '#toolbar-item-administration-search-tray'; + + $this->drupalLogin($this->adminUser); + $assert_session = $this->assertSession(); + $assert_session->responseContains('admin.toolbar_search.css'); + $assert_session->responseContains('admin_toolbar_search.js'); + $assert_session->waitForElementVisible('css', $search_tab)->click(); + $assert_session->waitForElementVisible('css', $search_tray); + + $this->assertSuggestionContains('basic', 'admin/config/system/site-information'); + + // Rebuild menu items. + drupal_flush_all_caches(); + + // Test that the route admin_toolbar.search returns expected json. + $this->drupalGet('/admin/admin-toolbar-search'); + + $search_menus = [ + 'cora', + 'eleanor', + 'eloise', + 'felix', + 'freya', + 'genevieve', + 'isla', + 'jasper', + 'luna', + 'maeve', + 'milo', + 'nora', + 'olivia', + 'ophelia', + 'posie', + 'rose', + 'silas', + 'soren', + ]; + + $toolbar_menus = [ + 'ada', + 'amara', + 'amelia', + 'arabella', + 'asher', + 'astrid', + 'atticus', + 'aurora', + 'ava', + ]; + + foreach ($search_menus as $menu_id) { + $assert_session->responseContains('\/admin\/structure\/menu\/manage\/' . $menu_id); + } + + foreach ($toolbar_menus as $menu_id) { + $assert_session->responseNotContains('\/admin\/structure\/menu\/manage\/' . $menu_id); + } + + $this->drupalGet('/admin'); + + foreach ($search_menus as $menu_id) { + $this->assertMenuDoesNotHaveHref('/admin/structure/menu/manage/' . $menu_id); + } + + foreach ($toolbar_menus as $menu_id) { + $this->assertMenuHasHref('/admin/structure/menu/manage/' . $menu_id); + } + + $this->drupalGet('admin/structure/types/manage/article/fields'); + $assert_session->waitForElementVisible('css', $search_tray); + + $this->assertSuggestionContains('article manage fields', '/admin/structure/types/manage/article/fields'); + + $suggestions = $assert_session + ->waitForElementVisible('css', 'ul.ui-autocomplete'); + + // Assert there is only one suggestion with a link to + // /admin/structure/types/manage/article/fields. + $count = count($suggestions->findAll('xpath', '//span[contains(text(), "/admin/structure/types/manage/article/fields")]')); + $this->assertEquals(1, $count); + + // Test that bundle within admin toolbar appears in search. + $this->assertSuggestionContains('lola', 'admin/structure/media/manage/lola/fields'); + + // Assert that a link after the limit (10) doesn't appear in admin toolbar. + $toby_url = '/admin/structure/media/manage/toby/fields'; + $assert_session->elementNotContains('css', '#toolbar-administration', $toby_url); + + // Assert that a link excluded from admin toolbar appears in search. + $this->assertSuggestionContains('toby', $toby_url); + + // Test that adding a new bundle updates the extra links loaded from + // admin_toolbar.search route. + $this->createMediaType('image', [ + 'id' => 'zuzu', + 'label' => 'Zuzu', + ]); + + $this->drupalGet('admin'); + $assert_session->waitForElementVisible('css', $search_tray); + $this->assertSuggestionContains('zuzu', '/admin/structure/media/manage/zuzu/fields'); + + // Test that deleting a bundle updates the extra links loaded from + // admin_toolbar.search route. + $toby = MediaType::load('toby'); + $toby->delete(); + + $this->getSession()->reload(); + $assert_session->waitForElementVisible('css', $search_tray); + $this->assertSuggestionNotContains('toby', $toby_url); + } + +} diff --git a/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.info.yml b/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.info.yml index 7b7728100e0203152229f058b6017ff81db1e8fa..89eedeafa06375c3937d5a6155f15eec2a9d7535 100644 --- a/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.info.yml +++ b/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.info.yml @@ -3,13 +3,12 @@ description: Adds menu links to the Admin Toolbar. package: Administration type: module -core: 8.x +core_version_requirement: ^8.8.0 || ^9.0 dependencies: - admin_toolbar:admin_toolbar - - drupal:system (>=8.6) -# Information added by Drupal.org packaging script on 2019-10-29 -version: '8.x-2.0' +# Information added by Drupal.org packaging script on 2020-03-24 +version: '8.x-2.2' project: 'admin_toolbar' -datestamp: 1572370993 +datestamp: 1585017182 diff --git a/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.install b/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.install new file mode 100644 index 0000000000000000000000000000000000000000..1cc14c73d3d9d84970e7f3beefcb04087de69cbf --- /dev/null +++ b/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.install @@ -0,0 +1,14 @@ +<?php + +/** + * @file + * Install, update and uninstall functions for the Admin Toolbar Tools module. + */ + +/** + * Install the Admin Toolbar Search module. + */ +function admin_toolbar_tools_update_8001() { + // Installing the Admin Toolbar Search module. + \Drupal::service('module_installer')->install(['admin_toolbar_search']); +} diff --git a/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.links.menu.yml b/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.links.menu.yml index 567e32d16ec24eb534d728fd7c9a5f2462a343a3..670a4f76c6219fbffba857c4047554a17dfdf197 100755 --- a/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.links.menu.yml +++ b/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.links.menu.yml @@ -75,6 +75,12 @@ admin_toolbar_tools.flush_rendercache: parent: admin_toolbar_tools.flush menu_name: admin +admin_toolbar_tools.theme_rebuild: + title: 'Rebuild theme registry' + route_name: admin_toolbar_tools.theme_rebuild + parent: admin_toolbar_tools.flush + menu_name: admin + admin_toolbar_tools.extra_links: deriver: \Drupal\admin_toolbar_tools\Plugin\Derivative\ExtraLinks menu_name: admin diff --git a/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.module b/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.module index ec4e682fa2e917414223954a78dcadf08317f470..6958a862975878a06152f06af3a6503a9cce48b4 100755 --- a/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.module +++ b/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.module @@ -48,7 +48,7 @@ function admin_toolbar_tools_help($route_name, RouteMatchInterface $route_match) * Implements hook_entity_insert(). */ function admin_toolbar_tools_entity_insert(EntityInterface $entity) { - $entities = \Drupal::service('admin_toolbar_tools.search_links')->getRebuildEntityTypes(); + $entities = \Drupal::service('admin_toolbar_tools.helper')->getRebuildEntityTypes(); if (in_array($entity->getEntityTypeId(), $entities)) { \Drupal::service('plugin.manager.menu.link')->rebuild(); } @@ -58,7 +58,7 @@ function admin_toolbar_tools_entity_insert(EntityInterface $entity) { * Implements hook_entity_update(). */ function admin_toolbar_tools_entity_update(EntityInterface $entity) { - $entities = \Drupal::service('admin_toolbar_tools.search_links')->getRebuildEntityTypes(); + $entities = \Drupal::service('admin_toolbar_tools.helper')->getRebuildEntityTypes(); if (in_array($entity->getEntityTypeId(), $entities)) { \Drupal::service('plugin.manager.menu.link')->rebuild(); } @@ -68,7 +68,7 @@ function admin_toolbar_tools_entity_update(EntityInterface $entity) { * Implements hook_entity_delete(). */ function admin_toolbar_tools_entity_delete(EntityInterface $entity) { - $entities = \Drupal::service('admin_toolbar_tools.search_links')->getRebuildEntityTypes(); + $entities = \Drupal::service('admin_toolbar_tools.helper')->getRebuildEntityTypes(); if (in_array($entity->getEntityTypeId(), $entities)) { \Drupal::service('plugin.manager.menu.link')->rebuild(); } diff --git a/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.routing.yml b/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.routing.yml index ee4eb23a2ccd04b88fe42ab9ab49b125701e365f..bcae9db1b7b37b7fa0eaf8c3d7b94c5f30c74bca 100755 --- a/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.routing.yml +++ b/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.routing.yml @@ -70,18 +70,20 @@ admin_toolbar_tools.flush_twig: _permission: 'administer site configuration' _csrf_token: 'TRUE' -admin_toolbar.run.cron: - path: '/run-cron' +admin_toolbar_tools.theme_rebuild: + path: '/admin/flush/theme_rebuild' defaults: - _controller: '\Drupal\admin_toolbar_tools\Controller\ToolbarController::runCron' - _title: 'Run cron' + _controller: '\Drupal\admin_toolbar_tools\Controller\ToolbarController::themeRebuild' + _title: 'Theme Rebuild' requirements: _permission: 'administer site configuration' _csrf_token: 'TRUE' -admin_toolbar.search: - path: '/admin/admin-toolbar-search' +admin_toolbar.run.cron: + path: '/run-cron' defaults: - _controller: '\Drupal\admin_toolbar_tools\Controller\ToolbarController::search' + _controller: '\Drupal\admin_toolbar_tools\Controller\ToolbarController::runCron' + _title: 'Run cron' requirements: _permission: 'administer site configuration' + _csrf_token: 'TRUE' diff --git a/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.services.yml b/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.services.yml index f772cffaae91fdab57c4f91726e8beba179c5b03..aa533c7871e4299d3f1b2aefe7941afe1a6eaca5 100644 --- a/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.services.yml +++ b/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.services.yml @@ -1,9 +1,5 @@ services: - admin_toolbar_tools.search_links: - class: Drupal\admin_toolbar_tools\SearchLinks + admin_toolbar_tools.helper: + class: Drupal\admin_toolbar_tools\AdminToolbarToolsHelper arguments: - '@entity_type.manager' - - '@module_handler' - - '@router.route_provider' - - '@cache_contexts_manager' - - '@cache.toolbar' diff --git a/web/modules/admin_toolbar/admin_toolbar_tools/composer.json b/web/modules/admin_toolbar/admin_toolbar_tools/composer.json index bb568793a3b735117d18e746c5e007830ed869da..fad275ff539e67a174f90e92ce76cc8c3dffe08c 100755 --- a/web/modules/admin_toolbar/admin_toolbar_tools/composer.json +++ b/web/modules/admin_toolbar/admin_toolbar_tools/composer.json @@ -26,7 +26,7 @@ "name": "Mohamed Anis Taktak (matio89)", "homepage": "https://www.drupal.org/u/matio89", "role": "Maintainer" - } + } ], "support": { "issues": "https://www.drupal.org/project/issues/admin_toolbar", @@ -34,6 +34,6 @@ }, "require": { "drupal/admin_toolbar": "^2", - "drupal/core": "~8.7" + "drupal/core": "^8.8.0 || ^9.0" } } diff --git a/web/modules/admin_toolbar/admin_toolbar_tools/src/AdminToolbarToolsHelper.php b/web/modules/admin_toolbar/admin_toolbar_tools/src/AdminToolbarToolsHelper.php new file mode 100644 index 0000000000000000000000000000000000000000..b270e679e9da56f4932030d70c0e253a40f096d9 --- /dev/null +++ b/web/modules/admin_toolbar/admin_toolbar_tools/src/AdminToolbarToolsHelper.php @@ -0,0 +1,62 @@ +<?php + +namespace Drupal\admin_toolbar_tools; + +use Drupal\Core\Entity\EntityTypeManagerInterface; + +/** + * Admin Toolbar Tools helper service. + */ +class AdminToolbarToolsHelper { + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * Create an AdminToolbarToolsHelper object. + * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + */ + public function __construct(EntityTypeManagerInterface $entity_type_manager) { + $this->entityTypeManager = $entity_type_manager; + } + + /** + * Gets a list of content entities. + * + * @return array + * An array of metadata about content entities. + */ + public function getBundleableEntitiesList() { + $entity_types = $this->entityTypeManager->getDefinitions(); + $content_entities = []; + foreach ($entity_types as $key => $entity_type) { + if ($entity_type->getBundleEntityType() && ($entity_type->get('field_ui_base_route') != '')) { + $content_entities[$key] = [ + 'content_entity' => $key, + 'content_entity_bundle' => $entity_type->getBundleEntityType(), + ]; + } + } + return $content_entities; + } + + /** + * Gets an array of entity types that should trigger a menu rebuild. + * + * @return array + * An array of entity machine names. + */ + public function getRebuildEntityTypes() { + $types = ['menu']; + $content_entities = $this->getBundleableEntitiesList(); + $types = array_merge($types, array_column($content_entities, 'content_entity_bundle')); + return $types; + } + +} diff --git a/web/modules/admin_toolbar/admin_toolbar_tools/src/Controller/ToolbarController.php b/web/modules/admin_toolbar/admin_toolbar_tools/src/Controller/ToolbarController.php index 7677f2af67604bb8359089c50e52ca4c2cb12d5b..22ccded1fcf6600e3bd414f562d92effa89a61e7 100755 --- a/web/modules/admin_toolbar/admin_toolbar_tools/src/Controller/ToolbarController.php +++ b/web/modules/admin_toolbar/admin_toolbar_tools/src/Controller/ToolbarController.php @@ -2,21 +2,20 @@ namespace Drupal\admin_toolbar_tools\Controller; -use Drupal\admin_toolbar_tools\SearchLinks; use Drupal\Component\Datetime\TimeInterface; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\CronInterface; -use Drupal\Core\Menu\ContextualLinkManagerInterface; -use Drupal\Core\Menu\LocalActionManagerInterface; -use Drupal\Core\Menu\LocalTaskManagerInterface; +use Drupal\Core\Menu\ContextualLinkManager; +use Drupal\Core\Menu\LocalActionManager; +use Drupal\Core\Menu\LocalTaskManager; use Drupal\Core\Menu\MenuLinkManagerInterface; use Drupal\Core\Plugin\CachedDiscoveryClearerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; -use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\RequestStack; use Drupal\Core\Template\TwigEnvironment; +use Drupal\Core\Theme\Registry; /** * Class ToolbarController. @@ -42,21 +41,21 @@ class ToolbarController extends ControllerBase { /** * A context link manager instance. * - * @var \Drupal\Core\Menu\ContextualLinkManagerInterface + * @var \Drupal\Core\Menu\ContextualLinkManager */ protected $contextualLinkManager; /** * A local task manager instance. * - * @var \Drupal\Core\Menu\LocalTaskManagerInterface + * @var \Drupal\Core\Menu\LocalTaskManager */ protected $localTaskLinkManager; /** * A local action manager instance. * - * @var \Drupal\Core\Menu\LocalActionManagerInterface + * @var \Drupal\Core\Menu\LocalActionManager */ protected $localActionLinkManager; @@ -103,11 +102,11 @@ class ToolbarController extends ControllerBase { protected $twig; /** - * The search links service. + * The search theme.registry service. * - * @var \Drupal\admin_toolbar_tools\SearchLinks + * @var \Drupal\Core\Theme\Registry */ - protected $links; + protected $themeRegistry; /** * Constructs a ToolbarController object. @@ -116,11 +115,11 @@ class ToolbarController extends ControllerBase { * A cron instance. * @param \Drupal\Core\Menu\MenuLinkManagerInterface $menuLinkManager * A menu link manager instance. - * @param \Drupal\Core\Menu\ContextualLinkManagerInterface $contextualLinkManager + * @param \Drupal\Core\Menu\ContextualLinkManager $contextualLinkManager * A context link manager instance. - * @param \Drupal\Core\Menu\LocalTaskManagerInterface $localTaskLinkManager + * @param \Drupal\Core\Menu\LocalTaskManager $localTaskLinkManager * A local task manager instance. - * @param \Drupal\Core\Menu\LocalActionManagerInterface $localActionLinkManager + * @param \Drupal\Core\Menu\LocalActionManager $localActionLinkManager * A local action manager instance. * @param \Drupal\Core\Cache\CacheBackendInterface $cacheRender * A cache backend interface instance. @@ -134,21 +133,23 @@ class ToolbarController extends ControllerBase { * A cache menu instance. * @param \Drupal\Core\Template\TwigEnvironment $twig * A TwigEnvironment instance. - * @param \Drupal\admin_toolbar_tools\SearchLinks $links - * The search links service. + * @param \Drupal\Core\Theme\Registry $theme_registry + * The theme.registry service. */ - public function __construct(CronInterface $cron, - MenuLinkManagerInterface $menuLinkManager, - ContextualLinkManagerInterface $contextualLinkManager, - LocalTaskManagerInterface $localTaskLinkManager, - LocalActionManagerInterface $localActionLinkManager, - CacheBackendInterface $cacheRender, - TimeInterface $time, - RequestStack $request_stack, - CachedDiscoveryClearerInterface $plugin_cache_clearer, - CacheBackendInterface $cache_menu, - TwigEnvironment $twig, - SearchLinks $links) { + public function __construct( + CronInterface $cron, + MenuLinkManagerInterface $menuLinkManager, + ContextualLinkManager $contextualLinkManager, + LocalTaskManager $localTaskLinkManager, + LocalActionManager $localActionLinkManager, + CacheBackendInterface $cacheRender, + TimeInterface $time, + RequestStack $request_stack, + CachedDiscoveryClearerInterface $plugin_cache_clearer, + CacheBackendInterface $cache_menu, + TwigEnvironment $twig, + Registry $theme_registry + ) { $this->cron = $cron; $this->menuLinkManager = $menuLinkManager; $this->contextualLinkManager = $contextualLinkManager; @@ -160,7 +161,7 @@ public function __construct(CronInterface $cron, $this->pluginCacheClearer = $plugin_cache_clearer; $this->cacheMenu = $cache_menu; $this->twig = $twig; - $this->links = $links; + $this->themeRegistry = $theme_registry; } /** @@ -179,7 +180,7 @@ public static function create(ContainerInterface $container) { $container->get('plugin.cache_clearer'), $container->get('cache.menu'), $container->get('twig'), - $container->get('admin_toolbar_tools.search_links') + $container->get('theme.registry') ); } @@ -283,10 +284,12 @@ public function cacheRender() { } /** - * Return additional search links. + * Rebuild the theme registry. */ - public function search() { - return new JsonResponse($this->links->getLinks()); + public function themeRebuild() { + $this->themeRegistry->reset(); + $this->messenger()->addMessage($this->t('Theme registry rebuilded.')); + return new RedirectResponse($this->reloadPage()); } } diff --git a/web/modules/admin_toolbar/admin_toolbar_tools/src/Plugin/Derivative/ExtraLinks.php b/web/modules/admin_toolbar/admin_toolbar_tools/src/Plugin/Derivative/ExtraLinks.php index c667b6b38b1ef36ce7f1867821db8fae037f7849..e3d62177c44774edb0f35929cdd86ee1189901cf 100755 --- a/web/modules/admin_toolbar/admin_toolbar_tools/src/Plugin/Derivative/ExtraLinks.php +++ b/web/modules/admin_toolbar/admin_toolbar_tools/src/Plugin/Derivative/ExtraLinks.php @@ -52,7 +52,7 @@ class ExtraLinks extends DeriverBase implements ContainerDeriverInterface { /** * {@inheritdoc} */ - public function __construct($base_plugin_id, EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, RouteProviderInterface $route_provider, ThemeHandlerInterface $theme_handler) { + public function __construct(EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, RouteProviderInterface $route_provider, ThemeHandlerInterface $theme_handler) { $this->entityTypeManager = $entity_type_manager; $this->moduleHandler = $module_handler; $this->routeProvider = $route_provider; @@ -64,7 +64,6 @@ public function __construct($base_plugin_id, EntityTypeManagerInterface $entity_ */ public static function create(ContainerInterface $container, $base_plugin_id) { return new static( - $base_plugin_id, $container->get('entity_type.manager'), $container->get('module_handler'), $container->get('router.route_provider'), @@ -96,7 +95,7 @@ public function getDerivativeDefinitions($base_plugin_definition) { $content_entity_bundle_storage = $this->entityTypeManager->getStorage($content_entity_bundle); $bundles_ids = $content_entity_bundle_storage->getQuery()->pager(self::MAX_BUNDLE_NUMBER)->execute(); $bundles = $this->entityTypeManager->getStorage($content_entity_bundle)->loadMultiple($bundles_ids); - if (count($bundles) == self::MAX_BUNDLE_NUMBER) { + if (count($bundles) == self::MAX_BUNDLE_NUMBER && $this->routeExists('entity.' . $content_entity_bundle . '.collection')) { $links[$content_entity_bundle . '.collection'] = [ 'title' => $this->t('All types'), 'route_name' => 'entity.' . $content_entity_bundle . '.collection', @@ -130,8 +129,8 @@ public function getDerivativeDefinitions($base_plugin_definition) { $content_entity_bundle_root = $key; } else { - $links[$key]['parent'] = $content_entity_bundle_root; - $links[$key]['title'] = t('Edit'); + $links[$key]['parent'] = $base_plugin_definition['id'] . ':' . $content_entity_bundle_root; + $links[$key]['title'] = $this->t('Edit'); } } if ($this->moduleHandler->moduleExists('field_ui')) { @@ -191,23 +190,23 @@ public function getDerivativeDefinitions($base_plugin_definition) { 'parent' => 'entity.user.collection', ] + $base_plugin_definition; $links['user.admin_permissions'] = [ - 'title' => t('Permissions'), + 'title' => $this->t('Permissions'), 'route_name' => 'user.admin_permissions', 'parent' => 'entity.user.collection', ] + $base_plugin_definition; $links['entity.user_role.collection'] = [ - 'title' => t('Roles'), + 'title' => $this->t('Roles'), 'route_name' => 'entity.user_role.collection', 'parent' => 'entity.user.collection', ] + $base_plugin_definition; $links['user.logout'] = [ - 'title' => t('Logout'), + 'title' => $this->t('Logout'), 'route_name' => 'user.logout', 'parent' => 'admin_toolbar_tools.help', 'weight' => 10, ] + $base_plugin_definition; $links['user.role_add'] = [ - 'title' => t('Add role'), + 'title' => $this->t('Add role'), 'route_name' => 'user.role_add', 'parent' => $base_plugin_definition['id'] . ':entity.user_role.collection', 'weight' => -5, diff --git a/web/modules/admin_toolbar/admin_toolbar_tools/tests/src/Functional/AdminToolbarToolsAlterTest.php b/web/modules/admin_toolbar/admin_toolbar_tools/tests/src/Functional/AdminToolbarToolsAlterTest.php index 94c27cd59e9758c412a14e283fc1145fe14e0619..9669a49a5f06eaed8aad6ef1dd6481a1914b5836 100755 --- a/web/modules/admin_toolbar/admin_toolbar_tools/tests/src/Functional/AdminToolbarToolsAlterTest.php +++ b/web/modules/admin_toolbar/admin_toolbar_tools/tests/src/Functional/AdminToolbarToolsAlterTest.php @@ -22,6 +22,11 @@ class AdminToolbarToolsAlterTest extends BrowserTestBase { 'admin_toolbar_tools', ]; + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + /** * A test user with permission to access the administrative toolbar. * diff --git a/web/modules/admin_toolbar/composer.json b/web/modules/admin_toolbar/composer.json index a97caac9b0186101e747fab92f10ea2a0b3bf115..650ed769fbab1240e04874011ee85945d5b61948 100755 --- a/web/modules/admin_toolbar/composer.json +++ b/web/modules/admin_toolbar/composer.json @@ -26,10 +26,13 @@ "name": "Mohamed Anis Taktak (matio89)", "homepage": "https://www.drupal.org/u/matio89", "role": "Maintainer" - } + } ], "support": { "issues": "https://www.drupal.org/project/issues/admin_toolbar", "source": "http://cgit.drupalcode.org/admin_toolbar" + }, + "require": { + "drupal/core": "^8.8.0 || ^9.0" } } diff --git a/web/modules/admin_toolbar/src/Render/Element/AdminToolbar.php b/web/modules/admin_toolbar/src/Render/Element/AdminToolbar.php new file mode 100644 index 0000000000000000000000000000000000000000..5d0000011f2c8c88469c1ecb6405496a3c364bf4 --- /dev/null +++ b/web/modules/admin_toolbar/src/Render/Element/AdminToolbar.php @@ -0,0 +1,51 @@ +<?php + +namespace Drupal\admin_toolbar\Render\Element; + +use Drupal\Core\Menu\MenuTreeParameters; +use Drupal\Core\Security\TrustedCallbackInterface; + +/** + * Class AdminToolbar. + * + * @package Drupal\admin_toolbar\Render\Element + */ +class AdminToolbar implements TrustedCallbackInterface { + + /** + * {@inheritdoc} + */ + public static function trustedCallbacks() { + return ['preRenderTray']; + } + + /** + * Renders the toolbar's administration tray. + * + * This is a clone of core's toolbar_prerender_toolbar_administration_tray() + * function, which uses setMaxDepth(4) instead of setTopLevelOnly(). + * + * @param array $build + * A renderable array. + * + * @return array + * The updated renderable array. + * + * @see toolbar_prerender_toolbar_administration_tray() + */ + public static function preRenderTray(array $build) { + $menu_tree = \Drupal::service('toolbar.menu_tree'); + $parameters = new MenuTreeParameters(); + $parameters->setRoot('system.admin')->excludeRoot()->setMaxDepth(4)->onlyEnabledLinks(); + $tree = $menu_tree->load(NULL, $parameters); + $manipulators = [ + ['callable' => 'menu.default_tree_manipulators:checkAccess'], + ['callable' => 'menu.default_tree_manipulators:generateIndexAndSort'], + ['callable' => 'toolbar_tools_menu_navigation_links'], + ]; + $tree = $menu_tree->transform($tree, $manipulators); + $build['administration_menu'] = $menu_tree->build($tree); + return $build; + } + +} diff --git a/web/modules/admin_toolbar/tests/src/Functional/AdminToolbarAlterTest.php b/web/modules/admin_toolbar/tests/src/Functional/AdminToolbarAlterTest.php index bd57d502b3be60bddd39cb75a1eef9c308a3e1cf..c17f575ee5135a42040e5336a096d0a855a500d3 100755 --- a/web/modules/admin_toolbar/tests/src/Functional/AdminToolbarAlterTest.php +++ b/web/modules/admin_toolbar/tests/src/Functional/AdminToolbarAlterTest.php @@ -22,6 +22,11 @@ class AdminToolbarAlterTest extends BrowserTestBase { 'admin_toolbar', ]; + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + /** * A test user with permission to access the administrative toolbar. * diff --git a/web/modules/admin_toolbar/tests/src/Functional/AdminToolbarToolsSortTest.php b/web/modules/admin_toolbar/tests/src/Functional/AdminToolbarToolsSortTest.php index 88a6261b1c300ab6f9c0a00a31e37061072acdaa..8da6ca9ae27324dc9341d4152bc94910b2ff1af4 100644 --- a/web/modules/admin_toolbar/tests/src/Functional/AdminToolbarToolsSortTest.php +++ b/web/modules/admin_toolbar/tests/src/Functional/AdminToolbarToolsSortTest.php @@ -28,6 +28,11 @@ class AdminToolbarToolsSortTest extends BrowserTestBase { 'field_ui', ]; + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + /** * A test user with permission to access the administrative toolbar. * @@ -35,13 +40,6 @@ class AdminToolbarToolsSortTest extends BrowserTestBase { */ protected $adminUser; - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - } - /** * Tests that menu updates on entity add/update/delete. */ diff --git a/web/modules/admin_toolbar/tests/src/FunctionalJavascript/AdminToolbarSearchTest.php b/web/modules/admin_toolbar/tests/src/FunctionalJavascript/AdminToolbarSearchTest.php deleted file mode 100755 index 5bd143f47050769db35c0a25dbf34f2faababe99..0000000000000000000000000000000000000000 --- a/web/modules/admin_toolbar/tests/src/FunctionalJavascript/AdminToolbarSearchTest.php +++ /dev/null @@ -1,339 +0,0 @@ -<?php - -namespace Drupal\Tests\admin_toolbar\FunctionalJavascript; - -use Drupal\FunctionalJavascriptTests\WebDriverTestBase; -use Drupal\media\Entity\MediaType; -use Drupal\system\Entity\Menu; -use Drupal\Tests\media\Traits\MediaTypeCreationTrait; - -/** - * Test the functionality of admin toolbar search. - * - * @group admin_toolbar - */ -class AdminToolbarSearchTest extends WebDriverTestBase { - - use MediaTypeCreationTrait; - - /** - * {@inheritdoc} - */ - public static $modules = [ - 'admin_toolbar', - 'admin_toolbar_tools', - 'node', - 'media', - 'field_ui', - 'menu_ui', - 'block', - ]; - - /** - * The admin user for tests. - * - * @var \Drupal\user\UserInterface - */ - protected $adminUser; - - /** - * {@inheritdoc} - */ - public function setUp() { - parent::setUp(); - - $this->drupalCreateContentType([ - 'type' => 'article', - 'name' => 'Article', - ]); - - $dog_names = [ - 'archie' => 'Archie', - 'bailey' => 'Bailey', - 'bella' => 'Bella', - 'buddy' => 'Buddy', - 'charlie' => 'Charlie', - 'coco' => 'Coco', - 'daisy' => 'Daisy', - 'frankie' => 'Frankie', - 'jack' => 'Jack', - 'lola' => 'Lola', - 'lucy' => 'Lucy', - 'max' => 'Max', - 'milo' => 'Milo', - 'molly' => 'Molly', - 'ollie' => 'Ollie', - 'oscar' => 'Oscar', - 'rosie' => 'Rosie', - 'ruby' => 'Ruby', - 'teddy' => 'Teddy', - 'toby' => 'Toby', - ]; - - foreach ($dog_names as $machine_name => $label) { - $this->createMediaType('image', [ - 'id' => $machine_name, - 'label' => $label, - ]); - } - - $baby_names = [ - 'ada' => 'Ada', - 'amara' => 'Amara', - 'amelia' => 'Amelia', - 'arabella' => 'Arabella', - 'asher' => 'Asher', - 'astrid' => 'Astrid', - 'atticus' => 'Atticus', - 'aurora' => 'Aurora', - 'ava' => 'Ava', - 'cora' => 'Cora', - 'eleanor' => 'Eleanor', - 'eloise' => 'Eloise', - 'felix' => 'Felix', - 'freya' => 'Freya', - 'genevieve' => 'Genevieve', - 'isla' => 'Isla', - 'jasper' => 'Jasper', - 'luna' => 'Luna', - 'maeve' => 'Maeve', - 'milo' => 'Milo', - 'nora' => 'Nora', - 'olivia' => 'Olivia', - 'ophelia' => 'Ophelia', - 'posie' => 'Posie', - 'rose' => 'Rose', - 'silas' => 'Silas', - 'soren' => 'Soren', - ]; - - foreach ($baby_names as $id => $label) { - $menu = Menu::create([ - 'id' => $id, - 'label' => $label, - ]); - $menu->save(); - } - - $this->drupalPlaceBlock('local_tasks_block'); - - $this->adminUser = $this->drupalCreateUser([ - 'access toolbar', - 'administer menu', - 'access administration pages', - 'administer site configuration', - 'administer content types', - 'administer node fields', - 'access media overview', - 'administer media', - 'administer media fields', - 'administer media form display', - 'administer media display', - 'administer media types', - ]); - } - - /** - * Tests search functionality. - */ - public function testSearchFunctionality() { - - $search_tab = '#toolbar-item-administration-search'; - $search_tray = '#toolbar-item-administration-search-tray'; - - $this->drupalLogin($this->adminUser); - $this->assertSession()->responseContains('admin.toolbar_search.css'); - $this->assertSession()->responseContains('admin_toolbar_search.js'); - $this->assertSession()->waitForElementVisible('css', $search_tab)->click(); - $this->assertSession()->waitForElementVisible('css', $search_tray); - - $this->assertSuggestionContains('basic', 'admin/config/system/site-information'); - - // Rebuild menu items. - drupal_flush_all_caches(); - - // Test that the route admin_toolbar.search returns expected json. - $this->drupalGet('/admin/admin-toolbar-search'); - - $search_menus = [ - 'cora', - 'eleanor', - 'eloise', - 'felix', - 'freya', - 'genevieve', - 'isla', - 'jasper', - 'luna', - 'maeve', - 'milo', - 'nora', - 'olivia', - 'ophelia', - 'posie', - 'rose', - 'silas', - 'soren', - ]; - - $toolbar_menus = [ - 'ada', - 'amara', - 'amelia', - 'arabella', - 'asher', - 'astrid', - 'atticus', - 'aurora', - 'ava', - ]; - - foreach ($search_menus as $menu_id) { - $this->assertSession()->responseContains('\/admin\/structure\/menu\/manage\/' . $menu_id); - } - - foreach ($toolbar_menus as $menu_id) { - $this->assertSession()->responseNotContains('\/admin\/structure\/menu\/manage\/' . $menu_id); - } - - $this->drupalGet('/admin'); - - foreach ($search_menus as $menu_id) { - $this->assertMenuDoesNotHaveHref('/admin/structure/menu/manage/' . $menu_id); - } - - foreach ($toolbar_menus as $menu_id) { - $this->assertMenuHasHref('/admin/structure/menu/manage/' . $menu_id); - } - - $this->drupalGet('admin/structure/types/manage/article/fields'); - $this->assertSession()->waitForElementVisible('css', $search_tray); - - $this->assertSuggestionContains('article manage fields', '/admin/structure/types/manage/article/fields'); - - $suggestions = $this->assertSession() - ->waitForElementVisible('css', 'ul.ui-autocomplete'); - - // Assert there is only one suggestion with a link to - // /admin/structure/types/manage/article/fields. - $count = count($suggestions->findAll('xpath', '//span[contains(text(), "/admin/structure/types/manage/article/fields")]')); - $this->assertEquals(1, $count); - - // Test that bundle within admin toolbar appears in search. - $this->assertSuggestionContains('lola', 'admin/structure/media/manage/lola/fields'); - - // Assert that a link after the limit (10) doesn't appear in admin toolbar. - $toby_url = '/admin/structure/media/manage/toby/fields'; - $this->assertSession() - ->elementNotContains('css', '#toolbar-administration', $toby_url); - - // Assert that a link excluded from admin toolbar appears in search. - $this->assertSuggestionContains('toby', $toby_url); - - // Test that adding a new bundle updates the extra links loaded from - // admin_toolbar.search route. - $this->createMediaType('image', [ - 'id' => 'zuzu', - 'label' => 'Zuzu', - ]); - - $this->drupalGet('admin'); - $this->assertSession()->waitForElementVisible('css', $search_tray); - $this->assertSuggestionContains('zuzu', '/admin/structure/media/manage/zuzu/fields'); - - // Test that deleting a bundle updates the extra links loaded from - // admin_toolbar.search route. - $toby = MediaType::load('toby'); - $toby->delete(); - - $this->getSession()->reload(); - $this->assertSession()->waitForElementVisible('css', $search_tray); - $this->assertSuggestionNotContains('toby', $toby_url); - - } - - /** - * Assert that the search suggestions contain a given string with given input. - * - * @param string $search - * The string to search for. - * @param string $contains - * Some HTML that is expected to be within the suggestions element. - */ - protected function assertSuggestionContains($search, $contains) { - $this->resetSearch(); - $page = $this->getSession()->getPage(); - $page->fillField('admin-toolbar-search-input', $search); - $page->waitFor(3, function () use ($page) { - return ($page->find('css', 'ul.ui-autocomplete')->isVisible() === TRUE); - }); - $suggestions_markup = $page->find('css', 'ul.ui-autocomplete')->getHtml(); - $this->assertContains($contains, $suggestions_markup); - } - - /** - * Assert that the search suggestions does not contain a given string. - * - * Assert that the search suggestions does not contain a given string with a - * given input. - * - * @param string $search - * The string to search for. - * @param string $contains - * Some HTML that is not expected to be within the suggestions element. - */ - protected function assertSuggestionNotContains($search, $contains) { - $this->resetSearch(); - $page = $this->getSession()->getPage(); - $page->fillField('admin-toolbar-search-input', $search); - $page->waitFor(3, function () use ($page) { - return ($page->find('css', 'ul.ui-autocomplete')->isVisible() === TRUE); - }); - if ($page->find('css', 'ul.ui-autocomplete')->isVisible() === FALSE) { - return; - } - else { - $suggestions_markup = $page->find('css', 'ul.ui-autocomplete')->getHtml(); - $this->assertNotContains($contains, $suggestions_markup); - } - } - - /** - * Search for an empty string to clear out the autocomplete suggestions. - */ - protected function resetSearch() { - $page = $this->getSession()->getPage(); - // Empty out the suggestions. - $page->fillField('admin-toolbar-search-input', ''); - $page->waitFor(3, function () use ($page) { - return ($page->find('css', 'ul.ui-autocomplete')->isVisible() === FALSE); - }); - } - - /** - * Checks that there is a link with the specified url in the admin toolbar. - * - * @param string $url - * The url to assert exists in the admin menu. - * - * @throws \Behat\Mink\Exception\ElementNotFoundException - */ - protected function assertMenuHasHref($url) { - $this->assertSession() - ->elementExists('xpath', '//div[@id="toolbar-item-administration-tray"]//a[contains(@href, "' . $url . '")]'); - } - - /** - * Checks that there is no link with the specified url in the admin toolbar. - * - * @param string $url - * The url to assert exists in the admin menu. - * - * @throws \Behat\Mink\Exception\ExpectationException - */ - protected function assertMenuDoesNotHaveHref($url) { - $this->assertSession() - ->elementNotExists('xpath', '//div[@id="toolbar-item-administration-tray"]//a[contains(@href, "' . $url . '")]'); - } - -} diff --git a/web/modules/entity_reference_revisions/composer.json b/web/modules/entity_reference_revisions/composer.json new file mode 100644 index 0000000000000000000000000000000000000000..7fdfb2e91999c1c74d5a33474ad0cc79a209f845 --- /dev/null +++ b/web/modules/entity_reference_revisions/composer.json @@ -0,0 +1,12 @@ +{ + "name": "drupal/entity_reference_revisions", + "description": "Entity Reference Revisions", + "type": "drupal-module", + "license": "GPL-2.0", + "require": { + "drupal/core": "^8.7.7 || ^9" + }, + "require-dev": { + "drupal/diff": "1.x-dev" + } +} diff --git a/web/modules/entity_reference_revisions/entity_reference_revisions.info.yml b/web/modules/entity_reference_revisions/entity_reference_revisions.info.yml index 6c425b7f118b1ddc550a5275aa8a6ac8c8975f8e..55807d43482d557361720824c7805204112f28af 100644 --- a/web/modules/entity_reference_revisions/entity_reference_revisions.info.yml +++ b/web/modules/entity_reference_revisions/entity_reference_revisions.info.yml @@ -1,14 +1,10 @@ name: Entity Reference Revisions type: module description: Adds a Entity Reference field type with revision support. -# core: 8.x +core_version_requirement: ^8.7.7 || ^9 package: Field types -test_dependencies: - - diff:diff - -# Information added by Drupal.org packaging script on 2017-05-26 -version: '8.x-1.3' -core: '8.x' +# Information added by Drupal.org packaging script on 2020-03-11 +version: '8.x-1.8' project: 'entity_reference_revisions' -datestamp: 1495814304 +datestamp: 1583961849 diff --git a/web/modules/entity_reference_revisions/entity_reference_revisions.links.menu.yml b/web/modules/entity_reference_revisions/entity_reference_revisions.links.menu.yml new file mode 100644 index 0000000000000000000000000000000000000000..beb8161771ec246b11b1d3c12f3db09d65377197 --- /dev/null +++ b/web/modules/entity_reference_revisions/entity_reference_revisions.links.menu.yml @@ -0,0 +1,6 @@ +entity_reference_revisions.delete_orphans: + title: 'Delete orphaned composite entities' + parent: system.admin_config_system + description: 'Delete revisions of entities that are no longer used in Entity Reference Revisions fields.' + route_name: entity_reference_revisions.delete_orphans + weight: 30 diff --git a/web/modules/entity_reference_revisions/entity_reference_revisions.module b/web/modules/entity_reference_revisions/entity_reference_revisions.module index ba9ac496f96fb8eb7cf439eb0c618205f66b0aea..fa24f77eab5fe6092919d721786c36778a016370 100644 --- a/web/modules/entity_reference_revisions/entity_reference_revisions.module +++ b/web/modules/entity_reference_revisions/entity_reference_revisions.module @@ -6,9 +6,14 @@ */ use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\FieldableEntityInterface; +use Drupal\Core\Entity\TranslatableRevisionableStorageInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\Element; use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\entity_reference_revisions\Plugin\Field\FieldType\EntityReferenceRevisionsItem; use Drupal\field\Entity\FieldStorageConfig; use Drupal\field\Entity\FieldConfig; use Drupal\field\FieldStorageConfigInterface; @@ -218,3 +223,129 @@ function entity_reference_revisions_form_field_ui_field_storage_add_form_alter(a unset($form['add']['new_storage_type']['#options'][(string) t('Reference revisions')]['entity_reference_revisions']); $form['add']['new_storage_type']['#options'][(string) t('Reference revisions')]['entity_reference_revisions'] = t('Other…'); } + +/** + * Implements hook_entity_revision_create(). + */ +function entity_reference_revisions_entity_revision_create(ContentEntityInterface $new_revision, ContentEntityInterface $entity, $keep_untranslatable_fields) { + $entity_type_manager = \Drupal::entityTypeManager(); + $storage = \Drupal::entityTypeManager()->getStorage($entity->getEntityTypeId()); + foreach ($entity->getFieldDefinitions() as $field_name => $field_definition) { + if ($field_definition->getType() == 'entity_reference_revisions' && !$field_definition->isTranslatable()) { + $target_entity_type_id = $field_definition->getSetting('target_type'); + if ($entity_type_manager->getDefinition($target_entity_type_id)->get('entity_revision_parent_id_field')) { + + // The default implementation copied the values from the current + // default revision into the field since it is not translatable. + // Take the originally referenced entity, create a new revision + // of it and set that instead on the new entity revision. + $active_langcode = $entity->language()->getId(); + $target_storage = \Drupal::entityTypeManager()->getStorage($target_entity_type_id); + if ($target_storage instanceof TranslatableRevisionableStorageInterface) { + + $items = $entity->get($field_name); + $translation_items = NULL; + if (!$new_revision->isDefaultTranslation() && $storage instanceof TranslatableRevisionableStorageInterface) { + $translation_items = $items; + $items = $storage->load($new_revision->id())->get($field_name); + } + + $values = []; + foreach ($items as $delta => $item) { + // If the target entity is missing, let's skip it. + if (empty($item->entity)) { + continue; + } + + // Use the item from the translation if it exists. + // If we have translation items, use that if one with the matching + // target id exists. + if ($translation_items) { + foreach ($translation_items as $translation_item) { + if ($item->target_id == $translation_item->target_id) { + $item = $translation_item; + break; + } + } + } + + /** @var \Drupal\Core\Entity\ContentEntityInterface $target_entity */ + $target_entity = $item->entity; + if (!$target_entity->hasTranslation($active_langcode)) { + $target_entity->addTranslation($active_langcode, $target_entity->toArray()); + } + $target_entity = $item->entity->getTranslation($active_langcode); + $revised_entity = $target_storage->createRevision($target_entity, $new_revision->isDefaultRevision(), $keep_untranslatable_fields); + + // Restore the revision ID. + $revision_key = $revised_entity->getEntityType()->getKey('revision'); + $revised_entity->set($revision_key, $revised_entity->getLoadedRevisionId()); + $values[$delta] = $revised_entity; + } + $new_revision->set($field_name, $values); + } + } + } + } +} + +/** + * Batch callback to dispatch the orphan composite batch operation to a service. + */ +function _entity_reference_revisions_orphan_purger_batch_dispatcher() { + $args = func_get_args(); + list($service, $method) = explode(':', array_shift($args)); + // The second argument (context) is passed by reference. + $values = $args[1]; + $args[1] = &$values; + call_user_func_array([\Drupal::service($service), $method], $args); +} + +/** + * Implements hook_entity_delete(). + * + * Performs garbage collection for composite entities that were not removed + * by EntityReferenceRevisionsItem. + */ +function entity_reference_revisions_entity_delete(EntityInterface $entity) { + if (!$entity instanceof FieldableEntityInterface) { + return; + } + + $entity_type_manager = \Drupal::entityTypeManager(); + /** @var \Drupal\Core\Field\FieldTypePluginManager $field_type_manager */ + $field_type_manager = \Drupal::service('plugin.manager.field.field_type'); + foreach ($entity->getFieldDefinitions() as $field_name => $field_definition) { + $field_class = $field_type_manager->getPluginClass($field_definition->getType()); + if ($field_class == EntityReferenceRevisionsItem::class || is_subclass_of($field_class, EntityReferenceRevisionsItem::class)) { + $target_entity_type_id = $field_definition->getSetting('target_type'); + $target_entity_storage = $entity_type_manager->getStorage($target_entity_type_id); + $target_entity_type = $target_entity_storage->getEntityType(); + + $parent_type_field = $target_entity_type->get('entity_revision_parent_type_field'); + $parent_id_field = $target_entity_type->get('entity_revision_parent_id_field'); + $parent_name_field = $target_entity_type->get('entity_revision_parent_field_name_field'); + + if ($parent_type_field && $parent_id_field && $parent_name_field) { + $entity_ids = $target_entity_storage + ->getQuery() + ->allRevisions() + ->condition($parent_type_field, $entity->getEntityTypeId()) + ->condition($parent_id_field, $entity->id()) + ->condition($parent_name_field, $field_name) + ->execute(); + + if (empty($entity_ids)) { + continue; + } + $entity_ids = array_unique($entity_ids); + foreach ($entity_ids as $revision_id => $entity_id) { + \Drupal::queue('entity_reference_revisions_orphan_purger')->createItem([ + 'entity_id' => $entity_id, + 'entity_type_id' => $target_entity_type_id, + ]); + } + } + } + } +} diff --git a/web/modules/entity_reference_revisions/entity_reference_revisions.permissions.yml b/web/modules/entity_reference_revisions/entity_reference_revisions.permissions.yml new file mode 100644 index 0000000000000000000000000000000000000000..bcd64c16d34741ee2c09057aadbc4e5e5b8349cf --- /dev/null +++ b/web/modules/entity_reference_revisions/entity_reference_revisions.permissions.yml @@ -0,0 +1,3 @@ +delete orphan revisions: + title: 'Delete orphan revisions' + description: 'Allow to access to the Entity Reference Revisions orphan deletion form.' diff --git a/web/modules/entity_reference_revisions/entity_reference_revisions.routing.yml b/web/modules/entity_reference_revisions/entity_reference_revisions.routing.yml new file mode 100644 index 0000000000000000000000000000000000000000..cd90db817dfa6418cefe7cb5c2dbfb8f5eea3062 --- /dev/null +++ b/web/modules/entity_reference_revisions/entity_reference_revisions.routing.yml @@ -0,0 +1,7 @@ +entity_reference_revisions.delete_orphans: + path: '/admin/config/system/delete-orphans' + defaults: + _form: 'Drupal\entity_reference_revisions\Form\OrphanedCompositeEntitiesDeleteForm' + _title: 'Delete orphaned composite entities' + requirements: + _permission: 'delete orphan revisions' diff --git a/web/modules/entity_reference_revisions/entity_reference_revisions.services.yml b/web/modules/entity_reference_revisions/entity_reference_revisions.services.yml new file mode 100755 index 0000000000000000000000000000000000000000..d112df982e4c4830e49666f570e9819ffadb4056 --- /dev/null +++ b/web/modules/entity_reference_revisions/entity_reference_revisions.services.yml @@ -0,0 +1,4 @@ +services: + entity_reference_revisions.orphan_purger: + class: Drupal\entity_reference_revisions\EntityReferenceRevisionsOrphanPurger + arguments: ['@entity_type.manager', '@date.formatter', '@datetime.time', '@database', '@messenger'] diff --git a/web/modules/entity_reference_revisions/entity_reference_revisions.views.inc b/web/modules/entity_reference_revisions/entity_reference_revisions.views.inc index 9d351b3765b5ccf7b1483aaa26d9941eba52af5d..6dd8ec66f71ade9143dc80931a407ab0ec505680 100644 --- a/web/modules/entity_reference_revisions/entity_reference_revisions.views.inc +++ b/web/modules/entity_reference_revisions/entity_reference_revisions.views.inc @@ -43,7 +43,7 @@ function entity_reference_revisions_field_views_data(FieldStorageConfigInterface // Provide a reverse relationship for the entity type that is referenced by // the field. $args['@entity'] = $entity_type->getLabel(); - $args['@label'] = $target_entity_type->getLowercaseLabel(); + $args['@label'] = $target_entity_type->getSingularLabel(); $pseudo_field_name = 'reverse__' . $entity_type_id . '__' . $field_name; /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ $table_mapping = $entity_manager->getStorage($entity_type_id)->getTableMapping(); diff --git a/web/modules/entity_reference_revisions/src/EntityReferenceRevisionsFieldItemList.php b/web/modules/entity_reference_revisions/src/EntityReferenceRevisionsFieldItemList.php index ba907277add9b397cae852212a1535035c1508e1..983e7be2697e036570f1756cf5a9795bedb3d12a 100644 --- a/web/modules/entity_reference_revisions/src/EntityReferenceRevisionsFieldItemList.php +++ b/web/modules/entity_reference_revisions/src/EntityReferenceRevisionsFieldItemList.php @@ -3,6 +3,8 @@ namespace Drupal\entity_reference_revisions; use Drupal\Core\Entity\FieldableEntityInterface; +use Drupal\Core\Field\FieldItemListInterface; +use Drupal\Core\Field\FieldItemListTranslationChangesInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\EntityReferenceFieldItemList; @@ -122,4 +124,28 @@ public function defaultValuesFormSubmit(array $element, array &$form, FormStateI return $default_value; } + /** + * {@inheritdoc} + */ + public function hasAffectingChanges(FieldItemListInterface $original_items, $langcode) { + // If there are fewer items, then it is a change. + if (count($this) < count($original_items)) { + return TRUE; + } + + foreach ($this as $delta => $item) { + // If this is a different entity, then it is an affecting change. + if (!$original_items->offsetExists($delta) || $item->target_id != $original_items[$delta]->target_id) { + return TRUE; + } + // If it is the same entity, only consider it as having affecting changes + // if the target entity itself has changes. + if ($item->entity && $item->entity->hasTranslation($langcode) && $item->entity->getTranslation($langcode)->hasTranslationChanges()) { + return TRUE; + } + } + + return FALSE; + } + } diff --git a/web/modules/entity_reference_revisions/src/EntityReferenceRevisionsOrphanPurger.php b/web/modules/entity_reference_revisions/src/EntityReferenceRevisionsOrphanPurger.php new file mode 100644 index 0000000000000000000000000000000000000000..8735817fc013be61a37c740895dfaa743b836d46 --- /dev/null +++ b/web/modules/entity_reference_revisions/src/EntityReferenceRevisionsOrphanPurger.php @@ -0,0 +1,370 @@ +<?php + +namespace Drupal\entity_reference_revisions; + +use Drupal\Component\Datetime\TimeInterface; +use Drupal\Core\Database\Connection; +use Drupal\Core\Datetime\DateFormatterInterface; +use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Messenger\MessengerInterface; +use Drupal\Core\Site\Settings; +use Drupal\Core\StringTranslation\StringTranslationTrait; + +/** + * Manages orphan composite revision deletion. + */ +class EntityReferenceRevisionsOrphanPurger { + + use StringTranslationTrait; + + /** + * Parent is valid. + */ + const PARENT_VALID = 0; + + /** + * Parent is invalid and usage can not be verified. + */ + const PARENT_INVALID_SKIP = 1; + + /** + * Parent is invalid and paragraph is safe to delete. + */ + const PARENT_INVALID_DELETE = 2; + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The date formatter service. + * + * @var \Drupal\Core\Datetime\DateFormatterInterface + */ + protected $dateFormatter; + + /** + * The time service. + * + * @var \Drupal\Component\Datetime\TimeInterface + */ + protected $time; + + /** + * The database service. + * + * @var \Drupal\Core\Database\Connection + */ + protected $database; + + /** + * The messenger. + * + * @var \Drupal\Core\Messenger\MessengerInterface + */ + protected $messenger; + + /** + * List of already checked parents. + * + * @var bool[][] + */ + protected $validParents = []; + + /** + * Constructs a EntityReferenceRevisionsOrphanManager object. + * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter + * The date formatter service. + * @param \Drupal\Component\Datetime\TimeInterface $time + * The time service. + * @param \Drupal\Core\Database\Connection $database + * The database service. + * @param \Drupal\Core\Messenger\MessengerInterface $messenger + * The messenger service. + */ + public function __construct(EntityTypeManagerInterface $entity_type_manager, DateFormatterInterface $date_formatter, TimeInterface $time, Connection $database, MessengerInterface $messenger) { + $this->entityTypeManager = $entity_type_manager; + $this->dateFormatter = $date_formatter; + $this->time = $time; + $this->database = $database; + $this->messenger = $messenger; + } + + /** + * Deletes unused revision or an entity if there are no revisions remaining. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $composite_revision + * The composite revision. + * + * @return bool + * TRUE if an entity revision was deleted. Otherwise, FALSE. + */ + public function deleteUnusedRevision(ContentEntityInterface $composite_revision) { + // If this is the default revision of the composite entity, check if there + // are other revisions. If there are not, delete the composite entity. + $composite_storage = $this->entityTypeManager->getStorage($composite_revision->getEntityTypeId()); + + if ($composite_revision->isDefaultRevision()) { + $count = $composite_storage + ->getQuery() + ->accessCheck(FALSE) + ->allRevisions() + ->condition($composite_storage->getEntityType()->getKey('id'), $composite_revision->id()) + ->count() + ->execute(); + if ($count <= 1) { + $composite_revision->delete(); + return TRUE; + } + } + else { + // Delete the revision if this is not the default one. + $composite_storage->deleteRevision($composite_revision->getRevisionId()); + return TRUE; + } + + return FALSE; + } + + /** + * Batch operation for checking orphans for a given entity type. + * + * @param string $entity_type_id + * The entity type id, for example 'paragraph'. + * @param array $context + * The context array. + */ + public function deleteOrphansBatchOperation($entity_type_id, array &$context) { + $composite_type = $this->entityTypeManager->getDefinition($entity_type_id); + $composite_revision_key = $composite_type->getKey('revision'); + /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $composite_storage */ + $composite_storage = $this->entityTypeManager->getStorage($entity_type_id); + $batch_size = Settings::get('entity_update_batch_size', 50); + + if (empty($context['sandbox']['total'])) { + $context['sandbox']['progress'] = 0; + $context['sandbox']['current_revision_id'] = -1; + $context['sandbox']['total'] = (int) $composite_storage->getQuery() + ->allRevisions() + ->accessCheck(FALSE) + ->count() + ->execute(); + } + + if (!isset($context['results'][$entity_type_id])) { + $context['results'][$entity_type_id]['entity_count'] = 0; + $context['results'][$entity_type_id]['revision_count'] = 0; + $context['results'][$entity_type_id]['start'] = $this->time->getRequestTime(); + } + + // Get the next batch of revision ids from the selected entity type. + // @todo Replace with an entity query on all revisions with a revision ID + // condition after https://www.drupal.org/project/drupal/issues/2766135. + $revision_table = $composite_type->getRevisionTable(); + $entity_revision_ids = $this->database->select($revision_table, 'r') + ->fields('r', [$composite_revision_key]) + ->range(0, $batch_size) + ->orderBy($composite_revision_key) + ->condition($composite_revision_key, $context['sandbox']['current_revision_id'], '>') + ->execute() + ->fetchCol(); + + /** @var \Drupal\Core\Entity\ContentEntityInterface $composite_revision */ + foreach ($composite_storage->loadMultipleRevisions($entity_revision_ids) as $composite_revision) { + $context['sandbox']['progress']++; + $context['sandbox']['current_revision_id'] = $composite_revision->getRevisionId(); + + if ($this->isUsed($composite_revision)) { + continue; + } + + if ($this->deleteUnusedRevision($composite_revision)) { + $context['results'][$entity_type_id]['revision_count']++; + if ($composite_revision->isDefaultRevision()) { + $context['results'][$entity_type_id]['entity_count']++; + } + } + } + + // This entity type is completed if no new revision ids were found or the + // total is reached. + if ($entity_revision_ids && $context['sandbox']['progress'] < $context['sandbox']['total']) { + $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['total']; + } + else { + $context['finished'] = 1; + $context['results'][$entity_type_id]['end'] = $this->time->getRequestTime(); + } + + $interval = $this->dateFormatter->formatInterval($this->time->getRequestTime() - $context['results'][$entity_type_id]['start']); + $context['message'] = t('Checked @entity_type revisions for orphans: @current of @total in @interval (@deletions deleted)', [ + '@entity_type' => $composite_type->getLabel(), + '@current' => $context['sandbox']['progress'], + '@total' => $context['sandbox']['total'], + '@interval' => $interval, + '@deletions' => $context['results'][$entity_type_id]['revision_count'], + ]); + } + + /** + * Batch dispatch submission finished callback. + */ + public static function batchSubmitFinished($success, $results, $operations) { + return \Drupal::service('entity_reference_revisions.orphan_purger')->doBatchSubmitFinished($success, $results, $operations); + } + + /** + * Sets a batch for executing deletion of the orphaned composite entities. + * + * @param array $composite_entity_type_ids + * An array of composite entity type IDs to remove orphaned items for. + */ + public function setBatch(array $composite_entity_type_ids) { + if (empty($composite_entity_type_ids)) { + return; + } + + $operations = []; + foreach ($composite_entity_type_ids as $entity_type_id) { + $operations[] = ['_entity_reference_revisions_orphan_purger_batch_dispatcher', + [ + 'entity_reference_revisions.orphan_purger:deleteOrphansBatchOperation', + $entity_type_id, + ], + ]; + } + + $batch = [ + 'operations' => $operations, + 'finished' => [EntityReferenceRevisionsOrphanPurger::class, 'batchSubmitFinished'], + 'title' => $this->t('Removing orphaned entities.'), + 'progress_message' => $this->t('Processed @current of @total entity types.'), + 'error_message' => $this->t('This batch encountered an error.'), + ]; + batch_set($batch); + } + + /** + * Finished callback for the batch process. + * + * @param bool $success + * Whether the batch completed successfully. + * @param array $results + * The results array. + * @param array $operations + * The operations array. + */ + public function doBatchSubmitFinished($success, $results, $operations) { + if ($success) { + foreach ($results as $entity_type_id => $result) { + if ($this->entityTypeManager->hasDefinition($entity_type_id)) { + $entity_type = $this->entityTypeManager->getDefinition($entity_type_id); + $interval = $this->dateFormatter->formatInterval($result['end'] - $result['start']); + $this->messenger->addMessage($this->t('@label: Deleted @revision_count revisions (@entity_count entities) in @interval.', [ + '@label' => $entity_type->getLabel(), + '@revision_count' => $result['revision_count'], + '@entity_count' => $result['entity_count'], + '@interval' => $interval, + ])); + } + } + } + else { + // $operations contains the operations that remained unprocessed. + $error_operation = reset($operations); + $this->messenger->addError($this->t('An error occurred while processing @operation with arguments : @args', [ + '@operation' => $error_operation[0], + '@args' => print_r($error_operation[0], TRUE), + ])); + } + } + + /** + * Checks if the composite entity is used. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $composite_revision + * The composite revision. + * + * @return bool + * Whether the composite entity is used, FALSE if it is safe to delete. + */ + public function isUsed(ContentEntityInterface $composite_revision) { + $composite_type = $this->entityTypeManager->getDefinition($composite_revision->getEntityTypeId()); + + $parent_type_field = $composite_type->get('entity_revision_parent_type_field'); + $parent_type = $composite_revision->get($parent_type_field)->value; + $parent_field_name_field = $composite_type->get('entity_revision_parent_field_name_field'); + $parent_field_name = $composite_revision->get($parent_field_name_field)->value; + + $status = $this->isValidParent($parent_type, $parent_field_name); + if ($status !== static::PARENT_VALID) { + return $status == static::PARENT_INVALID_SKIP ? TRUE : FALSE; + } + + // Check if the revision is used in any revision of the parent, if that + // entity type supports revisions. + $query = $this->entityTypeManager->getStorage($parent_type) + ->getQuery() + ->condition("$parent_field_name.target_revision_id", $composite_revision->getRevisionId()) + ->range(0, 1) + ->accessCheck(FALSE); + + if ($this->entityTypeManager->getDefinition($parent_type)->isRevisionable()) { + $query = $query->allRevisions(); + } + + $revisions = $query->execute(); + // If there are parent revisions where this revision is used, skip it. + return !empty($revisions); + } + + /** + * Checks if the parent type/field is a valid combination that can be queried. + * + * @param string $parent_type + * Parent entity type ID. + * @param string $parent_field_name + * Parent field name. + * + * @return int + * static::PARENT_VALID, static::PARENT_INVALID_SKIP or + * static::PARENT_INVALID_DELETE. + */ + protected function isValidParent($parent_type, $parent_field_name) { + // There is not certainty that this revision is not used because we do not + // know what to query for if the parent fields are empty. + if ($parent_type == NULL) { + return static::PARENT_INVALID_SKIP; + } + + if (isset($this->validParents[$parent_type][$parent_field_name])) { + return $this->validParents[$parent_type][$parent_field_name]; + } + + $status = static::PARENT_VALID; + // If the parent type does not exist anymore, the composite is not used. + if (!$this->entityTypeManager->hasDefinition($parent_type)) { + $status = static::PARENT_INVALID_DELETE; + } + // Check if the parent field is valid. + elseif (!($parent_field_config = $this->entityTypeManager->getStorage('field_storage_config')->load("$parent_type.$parent_field_name"))) { + $status = static::PARENT_INVALID_DELETE; + } + // In case the parent field has no target revision ID key we can not be sure + // that this revision is not used anymore. + elseif (empty($parent_field_config->getSchema()['columns']['target_revision_id'])) { + $status = static::PARENT_INVALID_SKIP; + } + $this->validParents[$parent_type][$parent_field_name] = $status; + + return $status; + } + +} diff --git a/web/modules/entity_reference_revisions/src/Form/OrphanedCompositeEntitiesDeleteForm.php b/web/modules/entity_reference_revisions/src/Form/OrphanedCompositeEntitiesDeleteForm.php new file mode 100644 index 0000000000000000000000000000000000000000..b3da36da6bfd8fdadf272c8543b3eba134f0bdee --- /dev/null +++ b/web/modules/entity_reference_revisions/src/Form/OrphanedCompositeEntitiesDeleteForm.php @@ -0,0 +1,130 @@ +<?php + +namespace Drupal\entity_reference_revisions\Form; + +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Form\FormBase; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Messenger\MessengerInterface; +use Drupal\entity_reference_revisions\EntityReferenceRevisionsOrphanPurger; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Class OrphanedCompositeEntitiesDeleteForm. + * + * @package Drupal\entity_reference_revisions\Form + */ +class OrphanedCompositeEntitiesDeleteForm extends FormBase { + + /** + * The Entity type manager service. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The messenger. + * + * @var \Drupal\Core\Messenger\MessengerInterface + */ + protected $messenger; + + /** + * The entity reference revisions orphan purger service. + * + * @var \Drupal\entity_reference_revisions\EntityReferenceRevisionsOrphanPurger + */ + protected $purger; + + /** + * OrphanedCompositeEntitiesDeleteForm constructor. + * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_manager + * The EntityTypeManager service. + * @param \Drupal\Core\Messenger\MessengerInterface $messenger + * The messenger. + * @param \Drupal\entity_reference_revisions\EntityReferenceRevisionsOrphanPurger $purger + * The entity reference revisions orphan purger. + */ + public function __construct(EntityTypeManagerInterface $entity_manager, MessengerInterface $messenger, EntityReferenceRevisionsOrphanPurger $purger) { + $this->entityTypeManager = $entity_manager; + $this->messenger = $messenger; + $this->purger = $purger; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager'), + $container->get('messenger'), + $container->get('entity_reference_revisions.orphan_purger') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'orphaned_composite_entities_delete_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $this->messenger->addWarning($this->t('The submission of the current form can cause the deletion of entities that are still used, backup all data first.'), 'warning'); + $form['description'] = [ + '#markup' => $this->t('Delete orphaned composite entities revisions that are no longer referenced. If there are no revisions left, the entity will be deleted as long as it is not used.'), + ]; + $options = []; + foreach ($this->getCompositeEntityTypes() as $entity_type) { + $options[$entity_type->id()] = $entity_type->getLabel(); + } + $form['composite_entity_types'] = [ + '#type' => 'checkboxes', + '#required' => TRUE, + '#title' => $this->t('Select the entity types to check for orphans'), + '#options' => $options, + '#default_value' => array_keys($options), + ]; + $form['actions'] = ['#type' => 'actions']; + $form['actions']['submit'] = [ + '#type' => 'submit', + '#button_type' => 'primary', + '#value' => $this->t('Delete orphaned composite revisions'), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $this->purger->setBatch(array_filter($form_state->getValue('composite_entity_types'))); + } + + /** + * Returns a list of composite entity types. + * + * @return \Drupal\Core\Entity\EntityTypeInterface[] + * An array of composite entity types. + */ + public function getCompositeEntityTypes() { + $composite_entity_types = []; + $entity_types = $this->entityTypeManager->getDefinitions(); + foreach ($entity_types as $entity_type) { + $has_parent_type_field = $entity_type->get('entity_revision_parent_type_field'); + $has_parent_id_field = $entity_type->get('entity_revision_parent_id_field'); + $has_parent_field_name_field = $entity_type->get('entity_revision_parent_field_name_field'); + if ($has_parent_type_field && $has_parent_id_field && $has_parent_field_name_field) { + $composite_entity_types[] = $entity_type; + } + } + return $composite_entity_types; + } + +} diff --git a/web/modules/entity_reference_revisions/src/Plugin/DataType/EntityReferenceRevisions.php b/web/modules/entity_reference_revisions/src/Plugin/DataType/EntityReferenceRevisions.php index c9e66ac6656263e0517264bd43baff8285cd3528..b24b6b10765f89cfe2c6ac420dd37f82f46721db 100644 --- a/web/modules/entity_reference_revisions/src/Plugin/DataType/EntityReferenceRevisions.php +++ b/web/modules/entity_reference_revisions/src/Plugin/DataType/EntityReferenceRevisions.php @@ -73,8 +73,13 @@ public function isTargetNew() { */ public function getTarget() { if (!isset($this->target) && isset($this->revision_id)) { - // If we have a valid reference, return the entity's TypedData adapter. - $entity = \Drupal::entityTypeManager()->getStorage($this->getTargetDefinition()->getEntityTypeId())->loadRevision($this->revision_id); + $storage = \Drupal::entityTypeManager()->getStorage($this->getTargetDefinition()->getEntityTypeId()); + // By default always load the default revision, so caches get used. + $entity = $storage->load($this->id); + if ($entity !== NULL && $entity->getRevisionId() != $this->revision_id) { + // A non-default revision is a referenced, so load this one. + $entity = $storage->loadRevision($this->revision_id); + } $this->target = isset($entity) ? $entity->getTypedData() : NULL; } return $this->target; diff --git a/web/modules/entity_reference_revisions/src/Plugin/Field/FieldType/EntityReferenceRevisionsItem.php b/web/modules/entity_reference_revisions/src/Plugin/Field/FieldType/EntityReferenceRevisionsItem.php index cdf2867b1b389046f0c2ea54a20fdfadda0b875e..16496dbc7e47247060fba0fa32077359b520d337 100644 --- a/web/modules/entity_reference_revisions/src/Plugin/Field/FieldType/EntityReferenceRevisionsItem.php +++ b/web/modules/entity_reference_revisions/src/Plugin/Field/FieldType/EntityReferenceRevisionsItem.php @@ -2,8 +2,10 @@ namespace Drupal\entity_reference_revisions\Plugin\Field\FieldType; +use Drupal\Component\Utility\Random; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Entity\TranslatableRevisionableInterface; use Drupal\Core\Entity\TypedData\EntityDataDefinition; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldStorageDefinitionInterface; @@ -179,7 +181,7 @@ public function setValue($values, $notify = TRUE) { // If the entity has been saved and we're trying to set both the // target_id and the entity values with a non-null target ID, then the // value for target_id should match the ID of the entity value. - if (!$this->entity->isNew() && $values['target_id'] !== NULL && ($entity_id !== $values['target_id'])) { + if (!$this->entity->isNew() && $values['target_id'] !== NULL && ($entity_id != $values['target_id'])) { throw new \InvalidArgumentException('The target id and entity passed to the entity reference item do not match.'); } } @@ -253,24 +255,55 @@ public function preSave() { // If it is a new entity, parent will save it. parent::preSave(); + $is_affected = TRUE; if (!$has_new) { // Create a new revision if it is a composite entity in a host with a new // revision. $host = $this->getEntity(); $needs_save = $this->entity instanceof EntityNeedsSaveInterface && $this->entity->needsSave(); - if (!$host->isNew() && $host->isNewRevision() && $this->entity && $this->entity->getEntityType()->get('entity_revision_parent_id_field')) { - $this->entity->setNewRevision(); - if ($host->isDefaultRevision()) { - $this->entity->isDefaultRevision(TRUE); + + // The item is considered to be affected if the field is either + // untranslatable or there are translation changes. This ensures that for + // translatable fields, a new revision of the referenced entity is only + // created for the affected translations and that the revision ID does not + // change on the unaffected translations. In turn, the host entity is not + // marked as affected for these translations. + $is_affected = !$this->getFieldDefinition()->isTranslatable() || ($host instanceof TranslatableRevisionableInterface && $host->hasTranslationChanges()); + if ($is_affected && !$host->isNew() && $this->entity && $this->entity->getEntityType()->get('entity_revision_parent_id_field')) { + if ($host->isNewRevision()) { + $this->entity->setNewRevision(); + $needs_save = TRUE; + } + // Additionally ensure that the default revision state is kept in sync. + if ($this->entity && $host->isDefaultRevision() != $this->entity->isDefaultRevision()) { + $this->entity->isDefaultRevision($host->isDefaultRevision()); + $needs_save = TRUE; } - $needs_save = TRUE; } if ($needs_save) { + + // Because ContentEntityBase::hasTranslationChanges() does not check for + // EntityReferenceRevisionsFieldItemList::hasAffectingChanges() on field + // items that are not translatable, hidden on translation forms and not + // in the default translation, this has to be handled here by setting + // setRevisionTranslationAffected on host translations that holds a + // reference that has been changed. + if ($is_affected && $host instanceof TranslatableRevisionableInterface) { + $languages = $host->getTranslationLanguages(); + foreach ($languages as $langcode => $language) { + $translation = $host->getTranslation($langcode); + if ($this->entity->hasTranslation($langcode) && $this->entity->getTranslation($langcode)->hasTranslationChanges() && $this->target_revision_id != $this->entity->getRevisionId()) { + $translation->setRevisionTranslationAffected(TRUE); + $translation->setRevisionTranslationAffectedEnforced(TRUE); + } + } + } + $this->entity->save(); } } - if ($this->entity) { + if ($this->entity && $is_affected) { $this->target_revision_id = $this->entity->getRevisionId(); } } @@ -301,6 +334,22 @@ public function postSave($update) { } } + // Keep in sync the translation languages between the parent and the child. + // For non translatable fields we have to do this in ::preSave but for + // translatable fields we have all the information we need in ::delete. + if (isset($parent_entity->original) && !$this->getFieldDefinition()->isTranslatable()) { + $langcodes = array_keys($parent_entity->getTranslationLanguages()); + $original_langcodes = array_keys($parent_entity->original->getTranslationLanguages()); + if ($removed_langcodes = array_diff($original_langcodes, $langcodes)) { + foreach ($removed_langcodes as $removed_langcode) { + if ($entity->hasTranslation($removed_langcode) && $entity->getUntranslated()->language()->getId() != $removed_langcode) { + $entity->removeTranslation($removed_langcode); + } + } + $needs_save = TRUE; + } + } + $parent_type = $entity->getEntityType()->get('entity_revision_parent_type_field'); $parent_id = $entity->getEntityType()->get('entity_revision_parent_id_field'); @@ -328,8 +377,11 @@ public function postSave($update) { */ public function deleteRevision() { $child = $this->entity; - if ($child->isDefaultRevision()) { - // Do not delete if it is the default revision. + // Return early, and do not delete the child revision, when the child + // revision is either: + // 1: Missing. + // 2: A default revision. + if (!$child || $child->isDefaultRevision()) { return; } @@ -338,6 +390,7 @@ public function deleteRevision() { $all_revisions = \Drupal::entityQuery($host->getEntityTypeId()) ->condition($field_name, $child->getRevisionId()) ->allRevisions() + ->accessCheck(FALSE) ->execute(); if (count($all_revisions) > 1) { @@ -357,15 +410,59 @@ public function delete() { if ($this->entity && $this->entity->getEntityType()->get('entity_revision_parent_type_field') && $this->entity->getEntityType()->get('entity_revision_parent_id_field')) { // Only delete composite entities if the host field is not translatable. if (!$this->getFieldDefinition()->isTranslatable()) { - $this->entity->delete(); + \Drupal::queue('entity_reference_revisions_orphan_purger')->createItem([ + 'entity_id' => $this->entity->id(), + 'entity_type_id' => $this->entity->getEntityTypeId(), + ]); } } } - /** - * {@inheritdoc} - */ + + /** + * {@inheritdoc} + */ public static function onDependencyRemoval(FieldDefinitionInterface $field_definition, array $dependencies) { - return FALSE; + $changed = FALSE; + $entity_type_manager = \Drupal::entityTypeManager(); + $target_entity_type = $entity_type_manager->getDefinition($field_definition->getFieldStorageDefinition() + ->getSetting('target_type')); + $handler_settings = $field_definition->getSetting('handler_settings'); + + // Update the 'target_bundles' handler setting if a bundle config dependency + // has been removed. + if (!empty($handler_settings['target_bundles'])) { + if ($bundle_entity_type_id = $target_entity_type->getBundleEntityType()) { + if ($storage = $entity_type_manager->getStorage($bundle_entity_type_id)) { + foreach ($storage->loadMultiple($handler_settings['target_bundles']) as $bundle) { + if (isset($dependencies[$bundle->getConfigDependencyKey()][$bundle->getConfigDependencyName()])) { + unset($handler_settings['target_bundles'][$bundle->id()]); + $changed = TRUE; + + // In case we deleted the only target bundle allowed by the field + // we can log a message because the behaviour of the field will + // have changed. + if ($handler_settings['target_bundles'] === []) { + \Drupal::logger('entity_reference_revisions') + ->notice('The %target_bundle bundle (entity type: %target_entity_type) was deleted. As a result, the %field_name entity reference revisions field (entity_type: %entity_type, bundle: %bundle) no longer specifies a specific target bundle. The field will now accept any bundle and may need to be adjusted.', [ + '%target_bundle' => $bundle->label(), + '%target_entity_type' => $bundle->getEntityType() + ->getBundleOf(), + '%field_name' => $field_definition->getName(), + '%entity_type' => $field_definition->getTargetEntityTypeId(), + '%bundle' => $field_definition->getTargetBundle() + ]); + } + } + } + } + } + } + + if ($changed) { + $field_definition->setSetting('handler_settings', $handler_settings); + } + + return $changed; } /** diff --git a/web/modules/entity_reference_revisions/src/Plugin/Field/FieldWidget/EntityReferenceRevisionsAutocompleteWidget.php b/web/modules/entity_reference_revisions/src/Plugin/Field/FieldWidget/EntityReferenceRevisionsAutocompleteWidget.php index faad139aba5389e3aa58ecd1e9559ce655d7f81c..c1d5ee51d96b4e2c4ad80f1c4afbec8d80dc8d04 100644 --- a/web/modules/entity_reference_revisions/src/Plugin/Field/FieldWidget/EntityReferenceRevisionsAutocompleteWidget.php +++ b/web/modules/entity_reference_revisions/src/Plugin/Field/FieldWidget/EntityReferenceRevisionsAutocompleteWidget.php @@ -2,7 +2,6 @@ namespace Drupal\entity_reference_revisions\Plugin\Field\FieldWidget; -use Drupal\Core\Entity\Entity; use Drupal\Core\Field\Plugin\Field\FieldWidget\EntityReferenceAutocompleteWidget; use Drupal\Core\Form\FormStateInterface; diff --git a/web/modules/entity_reference_revisions/src/Plugin/QueueWorker/OrphanPurger.php b/web/modules/entity_reference_revisions/src/Plugin/QueueWorker/OrphanPurger.php new file mode 100644 index 0000000000000000000000000000000000000000..a2bf051f1badda6050c5aade5d5a461fb90a8c57 --- /dev/null +++ b/web/modules/entity_reference_revisions/src/Plugin/QueueWorker/OrphanPurger.php @@ -0,0 +1,113 @@ +<?php + +namespace Drupal\entity_reference_revisions\Plugin\QueueWorker; + +use Drupal\Core\Database\Connection; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\Queue\QueueWorkerBase; +use Drupal\entity_reference_revisions\EntityReferenceRevisionsOrphanPurger; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Removes composite revisions that are no longer used. + * + * @QueueWorker( + * id = "entity_reference_revisions_orphan_purger", + * title = @Translation("Entity Reference Revisions Orphan Purger"), + * cron = {"time" = 60} + * ) + */ +class OrphanPurger extends QueueWorkerBase implements ContainerFactoryPluginInterface { + + /** + * The entity type manager service. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The purger. + * + * @var \Drupal\entity_reference_revisions\EntityReferenceRevisionsOrphanPurger + */ + protected $purger; + + /** + * The database. + * + * @var \Drupal\Core\Database\Connection + */ + protected $database; + + /** + * Constructs a new OrphanPurger instance. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin_id for the plugin instance. + * @param mixed $plugin_definition + * The plugin implementation definition. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager service. + * @param \Drupal\entity_reference_revisions\EntityReferenceRevisionsOrphanPurger $purger + * The purger service. + * @param \Drupal\Core\Database\Connection $database + * The database service. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityReferenceRevisionsOrphanPurger $purger, Connection $database) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->entityTypeManager = $entity_type_manager; + $this->purger = $purger; + $this->database = $database; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager'), + $container->get('entity_reference_revisions.orphan_purger'), + $container->get('database') + ); + } + + /** + * {@inheritdoc} + */ + public function processItem($data) { + $entity_type_id = $data['entity_type_id']; + if (!$this->entityTypeManager->hasDefinition($entity_type_id)) { + return; + } + + // Check the usage of data item and remove if not used. + $composite_storage = $this->entityTypeManager->getStorage($entity_type_id); + $composite_type = $this->entityTypeManager->getDefinition($entity_type_id); + $composite_revision_key = $composite_type->getKey('revision'); + + // Load all revisions of the composite type. + // @todo Replace with an entity query on all revisions with a revision ID + // condition after https://www.drupal.org/project/drupal/issues/2766135. + $entity_revision_ids = $this->database->select($composite_type->getRevisionTable(), 'r') + ->fields('r', [$composite_revision_key]) + ->condition($composite_type->getKey('id'), $data['entity_id']) + ->orderBy($composite_revision_key) + ->execute() + ->fetchCol(); + + /** @var \Drupal\Core\Entity\ContentEntityInterface $composite_revision */ + foreach ($composite_storage->loadMultipleRevisions($entity_revision_ids) as $composite_revision) { + if (!$this->purger->isUsed($composite_revision)) { + $this->purger->deleteUnusedRevision($composite_revision); + } + } + } + +} diff --git a/web/modules/entity_reference_revisions/src/Plugin/migrate/destination/EntityReferenceRevisions.php b/web/modules/entity_reference_revisions/src/Plugin/migrate/destination/EntityReferenceRevisions.php index a50f5a76750cf719b1e5473a56b0eb7730ae65cb..d4baf31c65e7076e773e56993ba73ec2a8c0523e 100644 --- a/web/modules/entity_reference_revisions/src/Plugin/migrate/destination/EntityReferenceRevisions.php +++ b/web/modules/entity_reference_revisions/src/Plugin/migrate/destination/EntityReferenceRevisions.php @@ -2,6 +2,7 @@ namespace Drupal\entity_reference_revisions\Plugin\migrate\destination; +use Drupal\Component\Plugin\ConfigurableInterface; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\TypedData\TranslatableInterface; use Drupal\migrate\MigrateException; @@ -12,12 +13,40 @@ /** * Provides entity_reference_revisions destination plugin. * + * Available configuration keys: + * - new_revisions: (optional) Flag to indicate if a new revision should be + * created instead of updating a previous default record. Only applicable when + * providing an entity id without a revision_id. + * * @MigrateDestination( * id = "entity_reference_revisions", * deriver = "Drupal\entity_reference_revisions\Plugin\Derivative\MigrateEntityReferenceRevisions" * ) */ -class EntityReferenceRevisions extends EntityRevision { +class EntityReferenceRevisions extends EntityRevision implements ConfigurableInterface { + + /** + * {@inheritdoc} + */ + public function setConfiguration(array $configuration) { + $this->configuration = $configuration; + } + + /** + * {@inheritdoc} + */ + public function getConfiguration() { + return $this->configuration; + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return [ + 'new_revisions' => FALSE, + ]; + } /** * {@inheritdoc} @@ -32,7 +61,7 @@ protected static function getEntityTypeId($pluginId) { /** * {@inheritdoc} */ - protected function save(ContentEntityInterface $entity, array $oldDestinationIdValues = array()) { + protected function save(ContentEntityInterface $entity, array $oldDestinationIdValues = []) { $entity->save(); return [ @@ -70,12 +99,33 @@ public function getIds() { * {@inheritdoc} */ protected function getEntity(Row $row, array $oldDestinationIdValues) { + $entity_id = $oldDestinationIdValues ? + array_shift($oldDestinationIdValues) : + $this->getEntityId($row); $revision_id = $oldDestinationIdValues ? array_pop($oldDestinationIdValues) : $row->getDestinationProperty($this->getKey('revision')); + + // If a specific revision_id is supplied and exists, assert the entity_id + // matches (if supplied), and update the revision. + /** @var \Drupal\Core\Entity\RevisionableInterface|\Drupal\Core\Entity\EntityInterface $entity */ if (!empty($revision_id) && ($entity = $this->storage->loadRevision($revision_id))) { - $entity->setNewRevision(FALSE); + if (!empty($entity_id) && ($entity->id() != $entity_id)) { + throw new MigrateException("The revision_id exists for this entity type, but does not belong to the given entity id"); + } + $entity = $this->updateEntity($entity, $row) ?: $entity; + } + // If there is no revision_id supplied, but there is an entity_id + // supplied that exists, update it. + elseif (!empty($entity_id) && ($entity = $this->storage->load($entity_id))) { + // If so configured, create a new revision while updating. + if (!empty($this->configuration['new_revisions'])) { + $entity->setNewRevision(TRUE); + } + $entity = $this->updateEntity($entity, $row) ?: $entity; } + + // Otherwise, create a new (possibly stub) entity. else { // Attempt to ensure we always have a bundle. if ($bundle = $this->getBundle($row)) { @@ -90,7 +140,6 @@ protected function getEntity(Row $row, array $oldDestinationIdValues) { ->enforceIsNew(TRUE); $entity->setNewRevision(TRUE); } - $entity = $this->updateEntity($entity, $row) ?: $entity; $this->rollbackAction = MigrateIdMapInterface::ROLLBACK_DELETE; return $entity; } @@ -117,8 +166,8 @@ protected function rollbackTranslation(array $destination_identifiers) { $entity = $this->storage->loadRevision(array_pop($destination_identifiers)); if ($entity && $entity instanceof TranslatableInterface) { if ($key = $this->getKey('langcode')) { - if (isset($destination_identifier[$key])) { - $langcode = $destination_identifier[$key]; + if (isset($destination_identifiers[$key])) { + $langcode = $destination_identifiers[$key]; if ($entity->hasTranslation($langcode)) { // Make sure we don't remove the default translation. $translation = $entity->getTranslation($langcode); @@ -150,4 +199,5 @@ protected function rollbackNonTranslation(array $destination_identifiers) { } } } + } diff --git a/web/modules/entity_reference_revisions/src/Plugin/views/display/EntityReferenceRevisions.php b/web/modules/entity_reference_revisions/src/Plugin/views/display/EntityReferenceRevisions.php index 3c738564d2f4174dea61020c5770f686a7333565..65ad121f9bb692ccc0e6df8529a3857d787446a2 100644 --- a/web/modules/entity_reference_revisions/src/Plugin/views/display/EntityReferenceRevisions.php +++ b/web/modules/entity_reference_revisions/src/Plugin/views/display/EntityReferenceRevisions.php @@ -2,6 +2,7 @@ namespace Drupal\entity_reference_revisions\Plugin\views\display; +use Drupal\Core\Database\Query\Condition; use Drupal\views\Plugin\views\display\DisplayPluginBase; /** @@ -123,13 +124,13 @@ public function query() { // Restrict the autocomplete options based on what's been typed already. if (isset($options['match'])) { $style_options = $this->getOption('style'); - $value = db_like($options['match']) . '%'; + $value = \Drupal::database()->escapeLike($options['match']) . '%'; if ($options['match_operator'] != 'STARTS_WITH') { $value = '%' . $value; } // Multiple search fields are OR'd together. - $conditions = db_or(); + $conditions = new Condition('OR'); // Build the condition using the selected search fields. foreach ($style_options['options']['search_fields'] as $field_alias) { diff --git a/web/modules/entity_reference_revisions/tests/modules/entity_composite_relationship_test/entity_composite_relationship_test.info.yml b/web/modules/entity_reference_revisions/tests/modules/entity_composite_relationship_test/entity_composite_relationship_test.info.yml index d81f3c3672c4ee1be2daaed0b8b46c91c5f1a6d0..7b906fc29d371869a923883fddcde58671fc1faf 100644 --- a/web/modules/entity_reference_revisions/tests/modules/entity_composite_relationship_test/entity_composite_relationship_test.info.yml +++ b/web/modules/entity_reference_revisions/tests/modules/entity_composite_relationship_test/entity_composite_relationship_test.info.yml @@ -2,13 +2,13 @@ name: 'ERR Composite relationship test' type: module description: 'Entity with parent type and ID.' package: Testing -# core: 8.x +core_version_requirement: ^8.7.7 || ^9 dependencies: - - entity_reference_revisions - - entity_test -# Information added by Drupal.org packaging script on 2017-05-26 -version: '8.x-1.3' -core: '8.x' + - entity_reference_revisions:entity_reference_revisions + - drupal:entity_test + +# Information added by Drupal.org packaging script on 2020-03-11 +version: '8.x-1.8' project: 'entity_reference_revisions' -datestamp: 1495814304 +datestamp: 1583961849 diff --git a/web/modules/entity_reference_revisions/tests/modules/entity_composite_relationship_test/src/Entity/EntityTestCompositeRelationship.php b/web/modules/entity_reference_revisions/tests/modules/entity_composite_relationship_test/src/Entity/EntityTestCompositeRelationship.php index fb61ad15940d4ac80a6458a7b10a51b5be932f1e..9ef825c409854c2d8bedfcffd347e8c303d080d1 100644 --- a/web/modules/entity_reference_revisions/tests/modules/entity_composite_relationship_test/src/Entity/EntityTestCompositeRelationship.php +++ b/web/modules/entity_reference_revisions/tests/modules/entity_composite_relationship_test/src/Entity/EntityTestCompositeRelationship.php @@ -18,6 +18,7 @@ * revision_table = "entity_test_composite_revision", * data_table = "entity_test_composite_field_data", * revision_data_table = "entity_test_composite_field_revision", + * content_translation_ui_skip = TRUE, * translatable = TRUE, * entity_revision_parent_type_field = "parent_type", * entity_revision_parent_id_field = "parent_id", @@ -44,15 +45,18 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { $fields = parent::baseFieldDefinitions($entity_type); $fields['parent_id'] = BaseFieldDefinition::create('string') ->setLabel(t('Parent ID')) - ->setDescription(t('The ID of the parent entity of which this entity is referenced.')); + ->setDescription(t('The ID of the parent entity of which this entity is referenced.')) + ->setRevisionable(TRUE); $fields['parent_type'] = BaseFieldDefinition::create('string') ->setLabel(t('Parent type')) - ->setDescription(t('The entity parent type to which this entity is referenced.')); + ->setDescription(t('The entity parent type to which this entity is referenced.')) + ->setRevisionable(TRUE); $fields['parent_field_name'] = BaseFieldDefinition::create('string') ->setLabel(t('Parent field name')) - ->setDescription(t('The entity parent field name to which this entity is referenced.')); + ->setDescription(t('The entity parent field name to which this entity is referenced.')) + ->setRevisionable(TRUE); return $fields; } diff --git a/web/modules/entity_reference_revisions/src/Tests/EntityReferenceRevisionsAdminTest.php b/web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsAdminTest.php similarity index 74% rename from web/modules/entity_reference_revisions/src/Tests/EntityReferenceRevisionsAdminTest.php rename to web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsAdminTest.php index e0a4c592740a2ce3871045364112aedc8b072deb..f2618b914f8337a868ec65d627c1d37a85e734a3 100644 --- a/web/modules/entity_reference_revisions/src/Tests/EntityReferenceRevisionsAdminTest.php +++ b/web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsAdminTest.php @@ -1,17 +1,17 @@ <?php -namespace Drupal\entity_reference_revisions\Tests; +namespace Drupal\Tests\entity_reference_revisions\Functional; -use Drupal\field_ui\Tests\FieldUiTestTrait; use Drupal\node\Entity\Node; -use Drupal\simpletest\WebTestBase; +use Drupal\Tests\BrowserTestBase; +use Drupal\Tests\field_ui\Traits\FieldUiTestTrait; /** * Tests the entity_reference_revisions configuration. * * @group entity_reference_revisions */ -class EntityReferenceRevisionsAdminTest extends WebTestBase { +class EntityReferenceRevisionsAdminTest extends BrowserTestBase { use FieldUiTestTrait; @@ -28,6 +28,11 @@ class EntityReferenceRevisionsAdminTest extends WebTestBase { 'block', ); + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + /** * {@inheritdoc} */ @@ -38,12 +43,6 @@ protected function setUp() { $this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article')); // Place the breadcrumb, tested in fieldUIAddNewField(). $this->drupalPlaceBlock('system_breadcrumb_block'); - } - - /** - * Tests the entity reference revisions configuration. - */ - public function testEntityReferenceRevisions() { $admin_user = $this->drupalCreateUser(array( 'administer site configuration', 'administer nodes', @@ -55,7 +54,12 @@ public function testEntityReferenceRevisions() { 'edit any article content', )); $this->drupalLogin($admin_user); + } + /** + * Tests the entity reference revisions configuration. + */ + public function testEntityReferenceRevisions() { // Create a test target node used as entity reference by another test node. $node_target = Node::create([ 'title' => 'Target node', @@ -90,7 +94,7 @@ public function testEntityReferenceRevisions() { 'title[0][value]' => $title, 'body[0][value]' => 'Revision 1', ); - $this->drupalPostForm('node/add/article', $edit, t('Save and publish')); + $this->drupalPostForm('node/add/article', $edit, t('Save')); $this->assertText($title); $this->assertText('Revision 1'); $node = $this->drupalGetNodeByTitle($title); @@ -103,7 +107,7 @@ public function testEntityReferenceRevisions() { 'title[0][value]' => 'Entity reference revision content', 'field_entity_reference_revisions[1][target_id]' => $node->label() . ' (' . $node->id() . ')', ]; - $this->drupalPostForm(NULL, $edit, t('Save and publish')); + $this->drupalPostForm(NULL, $edit, t('Save')); $this->assertLinkByHref('node/' . $node_target->id()); $this->assertText('Entity revisions Entity reference revision content has been created.'); $this->assertText('Entity reference revision content'); @@ -115,7 +119,7 @@ public function testEntityReferenceRevisions() { 'body[0][value]' => 'Revision 2', 'revision' => TRUE, ); - $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); + $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save')); $this->assertText($title); $this->assertText('Revision 2'); @@ -148,4 +152,37 @@ public function testEntityReferenceRevisions() { $this->assertEqual((string) $properties['entity']->getLabel(), 'Content'); } + /** + * Tests target bundle settings for an entity reference revisions field. + */ + public function testMultipleTargetBundles() { + // Create a couple of content types for the ERR field to point to. + $target_types = []; + for ($i = 0; $i < 2; $i++) { + $target_types[$i] = $this->drupalCreateContentType([ + 'type' => strtolower($this->randomMachineName()), + 'name' => 'Test type ' . $i + ]); + } + + // Create a new field that can point to either target content type. + $node_type_path = 'admin/structure/types/manage/entity_revisions'; + + // Generate a random field name, must be only lowercase characters. + $field_name = strtolower($this->randomMachineName()); + + $field_edit = []; + $storage_edit = ['settings[target_type]' => 'node', 'cardinality' => '-1']; + $field_edit['settings[handler_settings][target_bundles][' . $target_types[0]->id() . ']'] = TRUE; + $field_edit['settings[handler_settings][target_bundles][' . $target_types[1]->id() . ']'] = TRUE; + + $this->fieldUIAddNewField($node_type_path, $field_name, 'Entity reference revisions', 'entity_reference_revisions', $storage_edit, $field_edit); + + // Deleting one of these content bundles at this point should only delete + // that bundle's body field. Test that there is no second field that will + // be deleted. + $this->drupalGet('/admin/structure/types/manage/' . $target_types[0]->id() . '/delete'); + $this->assertNoFieldByXPath('(//details[@id="edit-entity-deletes"]//ul[@data-drupal-selector="edit-field-config"]/li)[2]'); + } + } diff --git a/web/modules/entity_reference_revisions/src/Tests/EntityReferenceRevisionsAutocompleteTest.php b/web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsAutocompleteTest.php similarity index 92% rename from web/modules/entity_reference_revisions/src/Tests/EntityReferenceRevisionsAutocompleteTest.php rename to web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsAutocompleteTest.php index 4998a41cee51c9164182098c824f4068bd422b88..95cff09154cc2b9a6778faa3ea025dc94999182e 100644 --- a/web/modules/entity_reference_revisions/src/Tests/EntityReferenceRevisionsAutocompleteTest.php +++ b/web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsAutocompleteTest.php @@ -1,19 +1,19 @@ <?php -namespace Drupal\entity_reference_revisions\Tests; +namespace Drupal\Tests\entity_reference_revisions\Functional; use Drupal\block_content\Entity\BlockContent; use Drupal\Component\Utility\Html; -use Drupal\field_ui\Tests\FieldUiTestTrait; use Drupal\node\Entity\Node; -use Drupal\simpletest\WebTestBase; +use Drupal\Tests\BrowserTestBase; +use Drupal\Tests\field_ui\Traits\FieldUiTestTrait; /** * Tests the entity_reference_revisions autocomplete. * * @group entity_reference_revisions */ -class EntityReferenceRevisionsAutocompleteTest extends WebTestBase { +class EntityReferenceRevisionsAutocompleteTest extends BrowserTestBase { use FieldUiTestTrait; @@ -30,6 +30,11 @@ class EntityReferenceRevisionsAutocompleteTest extends WebTestBase { 'field_ui', ); + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + /** * {@inheritdoc} */ @@ -91,7 +96,7 @@ public function testEntityReferenceRevisionsAutocompleteProcessing() { 'body[0][value]' => 'Revision 1', 'field_entity_reference_revisions[0][target_id]' => $block_label . ' (' . $block->id() . ')', ); - $this->drupalPostForm('node/add/article', $edit, t('Save and publish')); + $this->drupalPostForm('node/add/article', $edit, t('Save')); $this->assertText($title); $this->assertText(Html::escape($block_content)); diff --git a/web/modules/entity_reference_revisions/src/Tests/EntityReferenceRevisionsDiffTest.php b/web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsDiffTest.php similarity index 83% rename from web/modules/entity_reference_revisions/src/Tests/EntityReferenceRevisionsDiffTest.php rename to web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsDiffTest.php index e8ea4ad0c66cc56770ee8a3e9e048903d16f508d..a79d4d94dbbb8bf5029faa00458b05d10f46a2b5 100644 --- a/web/modules/entity_reference_revisions/src/Tests/EntityReferenceRevisionsDiffTest.php +++ b/web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsDiffTest.php @@ -1,9 +1,9 @@ <?php -namespace Drupal\entity_reference_revisions\Tests; +namespace Drupal\Tests\entity_reference_revisions\Functional; -use Drupal\field_ui\Tests\FieldUiTestTrait; -use Drupal\simpletest\WebTestBase; +use Drupal\Tests\BrowserTestBase; +use Drupal\Tests\field_ui\Traits\FieldUiTestTrait; /** * Tests the entity_reference_revisions diff plugin. @@ -12,9 +12,10 @@ * * @dependencies diff */ -class EntityReferenceRevisionsDiffTest extends WebTestBase { +class EntityReferenceRevisionsDiffTest extends BrowserTestBase { use FieldUiTestTrait; + /** * Modules to enable. * @@ -29,6 +30,11 @@ class EntityReferenceRevisionsDiffTest extends WebTestBase { 'diff', ]; + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + /** * {@inheritdoc} */ @@ -76,7 +82,7 @@ public function testEntityReferenceRevisionsDiff() { 'title[0][value]' => $title_node_1, 'body[0][value]' => 'body_node_1', ]; - $this->drupalPostForm('node/add/article', $edit, t('Save and publish')); + $this->drupalPostForm('node/add/article', $edit, t('Save')); // Create second referenced node. $title_node_2 = 'referenced_node_2'; @@ -84,7 +90,7 @@ public function testEntityReferenceRevisionsDiff() { 'title[0][value]' => $title_node_2, 'body[0][value]' => 'body_node_2', ]; - $this->drupalPostForm('node/add/article', $edit, t('Save and publish')); + $this->drupalPostForm('node/add/article', $edit, t('Save')); // Create referencing node. $title = 'referencing_node'; @@ -93,11 +99,11 @@ public function testEntityReferenceRevisionsDiff() { 'title[0][value]' => $title, 'field_err_field[0][target_id]' => $title_node_1 . ' (' . $node->id() . ')', ]; - $this->drupalPostForm('node/add/article', $edit, t('Save and publish')); + $this->drupalPostForm('node/add/article', $edit, t('Save')); // Check the plugin is set. $this->drupalGet('admin/config/content/diff/fields'); - $this->drupalPostForm(NULL, ['fields[node.field_err_field][plugin][type]' => 'entity_reference_revisions_field_diff_builder'], t('Save')); + $this->drupalPostForm(NULL, ['fields[node__field_err_field][plugin][type]' => 'entity_reference_revisions_field_diff_builder'], t('Save')); // Update the referenced node of the err field and create a new revision. $node = $this->drupalGetNodeByTitle($title); @@ -106,7 +112,7 @@ public function testEntityReferenceRevisionsDiff() { 'field_err_field[0][target_id]' => $title_node_2 . ' (' . $referenced_node_new->id() . ')', 'revision' => TRUE, ]; - $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); + $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save')); // Compare the revisions of the referencing node. $this->drupalPostForm('node/' . $node->id() . '/revisions', [], t('Compare selected revisions')); diff --git a/web/modules/entity_reference_revisions/src/Tests/EntityReferenceRevisionsNormalizerTest.php b/web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsNormalizerTest.php similarity index 90% rename from web/modules/entity_reference_revisions/src/Tests/EntityReferenceRevisionsNormalizerTest.php rename to web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsNormalizerTest.php index 3faf8fb41a044234014364b2f41e2cd28f88b375..740fe8b1927ca8a430b70b0acb8a6a163cd211fb 100644 --- a/web/modules/entity_reference_revisions/src/Tests/EntityReferenceRevisionsNormalizerTest.php +++ b/web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsNormalizerTest.php @@ -1,17 +1,17 @@ <?php -namespace Drupal\entity_reference_revisions\Tests; +namespace Drupal\Tests\entity_reference_revisions\Functional; -use Drupal\field_ui\Tests\FieldUiTestTrait; use Drupal\node\Entity\Node; -use Drupal\simpletest\WebTestBase; +use Drupal\Tests\BrowserTestBase; +use Drupal\Tests\field_ui\Traits\FieldUiTestTrait; /** * Tests the entity_reference_revisions configuration. * * @group entity_reference_revisions */ -class EntityReferenceRevisionsNormalizerTest extends WebTestBase { +class EntityReferenceRevisionsNormalizerTest extends BrowserTestBase { use FieldUiTestTrait; @@ -31,6 +31,11 @@ class EntityReferenceRevisionsNormalizerTest extends WebTestBase { 'rest', ); + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + /** * {@inheritdoc} */ @@ -68,7 +73,7 @@ public function testEntityReferenceRevisions() { 'title[0][value]' => $title, 'body[0][value]' => 'Revision 1', ); - $this->drupalPostForm('node/add/article', $edit, t('Save and publish')); + $this->drupalPostForm('node/add/article', $edit, t('Save')); $this->assertText($title); $this->assertText('Revision 1'); $node = $this->drupalGetNodeByTitle($title); @@ -79,7 +84,7 @@ public function testEntityReferenceRevisions() { 'title[0][value]' => $err_title, 'field_entity_reference_revisions[0][target_id]' => $node->label() . ' (' . $node->id() . ')', ); - $this->drupalPostForm('node/add/entity_revisions', $edit, t('Save and publish')); + $this->drupalPostForm('node/add/entity_revisions', $edit, t('Save')); $this->assertText('Entity revisions Entity reference revision content has been created.'); $err_node = $this->drupalGetNodeByTitle($err_title); @@ -92,7 +97,7 @@ public function testEntityReferenceRevisions() { 'body[0][value]' => 'Revision 2', 'revision' => TRUE, ); - $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); + $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save')); $serializer = $this->container->get('serializer'); $normalized = $serializer->normalize($err_node, 'hal_json'); $request = \Drupal::request(); diff --git a/web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsOrphanRemovalTest.php b/web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsOrphanRemovalTest.php new file mode 100644 index 0000000000000000000000000000000000000000..3fa8798747c4e1aa92bbe1bfcfb685c0a48a0a90 --- /dev/null +++ b/web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsOrphanRemovalTest.php @@ -0,0 +1,371 @@ +<?php + +namespace Drupal\Tests\entity_reference_revisions\Functional; + +use Drupal\Core\Site\Settings; +use Drupal\entity_composite_relationship_test\Entity\EntityTestCompositeRelationship; +use Drupal\field\Entity\FieldConfig; +use Drupal\field\Entity\FieldStorageConfig; +use Drupal\node\Entity\NodeType; +use Drupal\Tests\BrowserTestBase; + +/** + * Tests orphan composite revisions are properly removed. + * + * @group entity_reference_revisions + */ +class EntityReferenceRevisionsOrphanRemovalTest extends BrowserTestBase { + + /** + * A user with administration access. + * + * @var \Drupal\user\UserInterface + */ + protected $adminUser; + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = [ + 'node', + 'field', + 'entity_reference_revisions', + 'entity_composite_relationship_test' + ]; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * {@inheritdoc} + */ + public function setUp() { + parent::setUp(); + $this->adminUser = $this->drupalCreateUser([ + 'delete orphan revisions', + ]); + $this->drupalLogin($this->adminUser); + $this->insertRevisionableData(); + $this->insertNonRevisionableData(); + } + + /** + * Tests that revisions that are no longer used are properly deleted. + */ + public function testNotUsedRevisionDeletion() { + $entity_test_composite_storage = \Drupal::entityTypeManager()->getStorage('entity_test_composite'); + + $composite_entity_first = $entity_test_composite_storage->loadByProperties(['name' => 'first not used, second used']); + $composite_entity_first = reset($composite_entity_first); + $this->assertRevisionCount(2, 'entity_test_composite', $composite_entity_first->id()); + + $composite_entity_second = $entity_test_composite_storage->loadByProperties(['name' => 'first used, second not used']); + $composite_entity_second = reset($composite_entity_second); + $this->assertRevisionCount(2, 'entity_test_composite', $composite_entity_second->id()); + + $composite_entity_third = $entity_test_composite_storage->loadByProperties(['name' => 'first not used, second not used']); + $composite_entity_third = reset($composite_entity_third); + $this->assertRevisionCount(2, 'entity_test_composite', $composite_entity_third->id()); + + $composite_entity_fourth = $entity_test_composite_storage->loadByProperties(['name' => '1st filled not, 2nd filled not']); + $composite_entity_fourth = reset($composite_entity_fourth); + $this->assertRevisionCount(2, 'entity_test_composite', $composite_entity_fourth->id()); + + $composite_entity_fifth = $entity_test_composite_storage->loadByProperties(['name' => '1st not, 2nd used, 3rd not, 4th']); + $composite_entity_fifth = reset($composite_entity_fifth); + $this->assertRevisionCount(4, 'entity_test_composite', $composite_entity_fifth->id()); + + $composite_entity_sixth = $entity_test_composite_storage->loadByProperties(['name' => 'wrong parent fields']); + $composite_entity_sixth = reset($composite_entity_sixth); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_entity_sixth->id()); + + // Test non revisionable parent entities. + $composite_entity_seventh = $entity_test_composite_storage->loadByProperties(['name' => 'NR first not used, second used']); + $composite_entity_seventh = reset($composite_entity_seventh); + $this->assertRevisionCount(2, 'entity_test_composite', $composite_entity_seventh->id()); + + $composite_entity_eighth = $entity_test_composite_storage->loadByProperties(['name' => 'NR first used, second not used']); + $composite_entity_eighth = reset($composite_entity_eighth); + $this->assertRevisionCount(2, 'entity_test_composite', $composite_entity_eighth->id()); + + $composite_entity_ninth = $entity_test_composite_storage->loadByProperties(['name' => 'NR 1st not, 2nd, 3rd not, 4th']); + $composite_entity_ninth = reset($composite_entity_ninth); + $this->assertRevisionCount(3, 'entity_test_composite', $composite_entity_ninth->id()); + + // Set the batch size to 1. + $settings = Settings::getInstance() ? Settings::getAll() : []; + $settings['entity_update_batch_size'] = 1; + new Settings($settings); + + // Run the delete process through the form. + $this->runDeleteForm(); + $this->assertSession()->pageTextContains('Test entity - composite relationship: Deleted 8 revisions (1 entities)'); + + $this->assertRevisionCount(1, 'entity_test_composite', $composite_entity_first->id()); + $this->assertRevisionCount(2, 'entity_test_composite', $composite_entity_second->id()); + $this->assertRevisionCount(2, 'entity_test_composite', $composite_entity_third->id()); + $this->assertRevisionCount(0, 'entity_test_composite', $composite_entity_fourth->id()); + $this->assertRevisionCount(2, 'entity_test_composite', $composite_entity_fifth->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_entity_sixth->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_entity_seventh->id()); + $this->assertRevisionCount(2, 'entity_test_composite', $composite_entity_eighth->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_entity_ninth->id()); + } + + /** + * Programmatically runs the 'Delete orphaned composite entities' form. + */ + public function runDeleteForm() { + $this->drupalGet('admin/config/system/delete-orphans'); + $this->submitForm([], t('Delete orphaned composite revisions')); + $this->checkForMetaRefresh(); + } + + /** + * Asserts the revision count of a certain entity. + * + * @param int $expected + * The expected count. + * @param string $entity_type_id + * The entity type ID, e.g. node. + * @param int $entity_id + * The entity ID. + */ + protected function assertRevisionCount($expected, $entity_type_id, $entity_id) { + $id_field = \Drupal::entityTypeManager()->getDefinition($entity_type_id)->getKey('id'); + $revision_count = \Drupal::entityQuery($entity_type_id) + ->condition($id_field, $entity_id) + ->allRevisions() + ->count() + ->execute(); + $this->assertEquals($expected, $revision_count); + } + + /** + * Inserts revisionable entities needed for testing. + */ + public function insertRevisionableData() { + /** @var \Drupal\node\NodeStorageInterface $node_storage */ + $node_storage = \Drupal::entityTypeManager()->getStorage('node'); + NodeType::create(['type' => 'revisionable', 'new_revision' => TRUE])->save(); + // Add a translatable field and a not translatable field to both content + // types. + $field_storage = FieldStorageConfig::create([ + 'field_name' => 'field_composite_entity', + 'entity_type' => 'node', + 'type' => 'entity_reference_revisions', + 'settings' => [ + 'target_type' => 'entity_test_composite' + ], + ]); + $field_storage->save(); + $field = FieldConfig::create([ + 'field_storage' => $field_storage, + 'bundle' => 'revisionable', + 'translatable' => FALSE, + ]); + $field->save(); + + // Scenario 1: A composite with a default revision that is referenced and an + // old revision that is not. Result: Only the old revision is deleted. + $composite_entity_first = EntityTestCompositeRelationship::create([ + 'name' => 'first not used, second used', + 'parent_id' => 1000, + 'parent_type' => 'node', + 'parent_field_name' => 'field_composite_entity', + ]); + $composite_entity_first->save(); + $composite_entity_first = EntityTestCompositeRelationship::load($composite_entity_first->id()); + $composite_entity_first->setNewRevision(TRUE); + $composite_entity_first->save(); + $node = $this->drupalCreateNode([ + 'type' => 'revisionable', + 'title' => 'First composite', + 'field_composite_entity' => $composite_entity_first, + ]); + $node->save(); + + // Scenario 2: A composite with an old revision that is used and a default + // revision that is not. Result: Nothing should be deleted. + $composite_entity_second = EntityTestCompositeRelationship::create([ + 'name' => 'first used, second not used', + ]); + $composite_entity_second->save(); + $node = $this->drupalCreateNode([ + 'type' => 'revisionable', + 'title' => 'Second composite', + 'field_composite_entity' => $composite_entity_second, + ]); + $node->save(); + $node = $this->getNodeByTitle('Second composite'); + $node = $node_storage->createRevision($node); + $node->set('field_composite_entity', NULL); + $node->save(); + $composite_entity_second = EntityTestCompositeRelationship::load($composite_entity_second->id()); + $composite_entity_second->setNewRevision(TRUE); + $composite_entity_second->save(); + + // Scenario 3: A composite with an old revision and a default revision both + // that are not used with empty parent fields. Result: Nothing should be + // deleted since we do not know if it is still used. + $composite_entity_third = EntityTestCompositeRelationship::create([ + 'name' => 'first not used, second not used', + ]); + $composite_entity_third->save(); + $composite_entity_third = EntityTestCompositeRelationship::load($composite_entity_third->id()); + $composite_entity_third->setNewRevision(TRUE); + $composite_entity_third->save(); + + // Scenario 4: A composite with an old revision and a default revision both + // that are not used with filled parent fields. Result: Should first delete + // the old revision and then the default revision. Delete the entity too. + $composite_entity_fourth = EntityTestCompositeRelationship::create([ + 'name' => '1st filled not, 2nd filled not', + 'parent_id' => 1001, + 'parent_type' => 'node', + 'parent_field_name' => 'field_composite_entity', + ]); + $composite_entity_fourth->save(); + $composite_entity_fourth = EntityTestCompositeRelationship::load($composite_entity_fourth->id()); + $composite_entity_fourth->setNewRevision(TRUE); + $composite_entity_fourth->set('parent_id', 1001); + $composite_entity_fourth->save(); + + // Scenario 5: A composite with many revisions and 2 at least used. Result: + // Delete all unused revisions. + $composite_entity_fifth = EntityTestCompositeRelationship::create([ + 'name' => '1st not, 2nd used, 3rd not, 4th', + 'parent_id' => 1001, + 'parent_type' => 'node', + 'parent_field_name' => 'field_composite_entity', + ]); + $composite_entity_fifth->save(); + $composite_entity_fifth = EntityTestCompositeRelationship::load($composite_entity_fifth->id()); + $composite_entity_fifth->setNewRevision(TRUE); + $composite_entity_fifth->save(); + $node = $this->drupalCreateNode([ + 'type' => 'revisionable', + 'title' => 'Third composite', + 'field_composite_entity' => $composite_entity_fifth, + ]); + $node->save(); + $node = $this->getNodeByTitle('Third composite'); + $node = $node_storage->createRevision($node); + $node->set('field_composite_entity', NULL); + $node->save(); + $composite_entity_fifth = EntityTestCompositeRelationship::load($composite_entity_fifth->id()); + $composite_entity_fifth->setNewRevision(TRUE); + $composite_entity_fifth->save(); + $node = $this->getNodeByTitle('Third composite'); + $node = $node_storage->createRevision($node); + $node->set('field_composite_entity', $composite_entity_fifth); + $node->save(); + + // Scenario 6: A composite with wrong parent fields filled pointing to a non + // existent parent (Parent 1). However, Parent 2 references it. Result: Must + // not be deleted. + $node = $this->drupalCreateNode([ + 'type' => 'revisionable', + 'title' => 'DELETED composite', + ]); + $node->save(); + $composite_entity_sixth = EntityTestCompositeRelationship::create([ + 'name' => 'wrong parent fields', + 'parent_id' => $node->id(), + 'parent_type' => 'node', + 'parent_field_name' => 'field_composite_entity', + ]); + $composite_entity_sixth->save(); + $node->delete(); + $node = $this->drupalCreateNode([ + 'type' => 'revisionable', + 'title' => 'Fourth composite', + 'field_composite_entity' => $composite_entity_sixth, + ]); + $node->save(); + } + + /** + * Inserts non revisionable entities needed for testing. + */ + public function insertNonRevisionableData() { + /** @var \Drupal\node\NodeStorageInterface $node_storage */ + NodeType::create(['type' => 'non_revisionable', 'new_revision' => FALSE])->save(); + // Add a translatable field and a not translatable field to both content + // types. + $field_storage = FieldStorageConfig::loadByName('node', 'field_composite_entity'); + $field = FieldConfig::create([ + 'field_storage' => $field_storage, + 'bundle' => 'non_revisionable', + 'translatable' => FALSE, + ]); + $field->save(); + + // Scenario 1: A composite with a default revision that is referenced and an + // old revision that is not. Result: Only the old revision is deleted. + $composite_entity_first = EntityTestCompositeRelationship::create([ + 'name' => 'NR first not used, second used', + 'parent_id' => 1001, + 'parent_type' => 'node', + 'parent_field_name' => 'field_composite_entity', + ]); + $composite_entity_first->save(); + $composite_entity_first = EntityTestCompositeRelationship::load($composite_entity_first->id()); + $composite_entity_first->setNewRevision(TRUE); + $composite_entity_first->save(); + $node = $this->drupalCreateNode([ + 'type' => 'non_revisionable', + 'title' => 'First composite', + 'field_composite_entity' => $composite_entity_first, + ]); + $node->save(); + + // Scenario 2: A composite with an old revision that is used and a default + // revision that is not. Result: Nothing should be deleted. + $composite_entity_second = EntityTestCompositeRelationship::create([ + 'name' => 'NR first used, second not used', + ]); + $composite_entity_second->save(); + $node = $this->drupalCreateNode([ + 'type' => 'non_revisionable', + 'title' => 'Second composite', + 'field_composite_entity' => $composite_entity_second, + ]); + $node->save(); + $composite_entity_second = EntityTestCompositeRelationship::load($composite_entity_second->id()); + $composite_entity_second->setNewRevision(TRUE); + $composite_entity_second->save(); + + // Scenario 3: A composite with many revisions and 2 at least used. Result: + // Delete all unused revisions. + $composite_entity_third = EntityTestCompositeRelationship::create([ + 'name' => 'NR 1st not, 2nd, 3rd not, 4th', + 'parent_id' => 1001, + 'parent_type' => 'node', + 'parent_field_name' => 'field_composite_entity', + ]); + $composite_entity_third->save(); + $composite_entity_third = EntityTestCompositeRelationship::load($composite_entity_third->id()); + $composite_entity_third->setNewRevision(TRUE); + $composite_entity_third->save(); + $node = $this->drupalCreateNode([ + 'type' => 'non_revisionable', + 'title' => 'Third composite', + 'field_composite_entity' => $composite_entity_third, + ]); + $node->save(); + $node = $this->getNodeByTitle('Third composite'); + $node->set('field_composite_entity', NULL); + $node->save(); + $composite_entity_third = EntityTestCompositeRelationship::load($composite_entity_third->id()); + $composite_entity_third->setNewRevision(TRUE); + $composite_entity_third->save(); + $node = $this->getNodeByTitle('Third composite'); + $node->set('field_composite_entity', $composite_entity_third); + $node->save(); + } +} diff --git a/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTest.php b/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTest.php index e92bfbe2b236bfb6d901c7ff333cd0f99bf890a5..ac226fa193e83e102522b04907517d2e336f683d 100644 --- a/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTest.php +++ b/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTest.php @@ -9,8 +9,8 @@ use Drupal\language\Entity\ConfigurableLanguage; use Drupal\node\Entity\Node; use Drupal\node\Entity\NodeType; -use Drupal\simpletest\ContentTypeCreationTrait; -use Drupal\simpletest\NodeCreationTrait; +use Drupal\Tests\node\Traits\ContentTypeCreationTrait; +use Drupal\Tests\node\Traits\NodeCreationTrait; /** * Tests the entity_reference_revisions composite relationship. @@ -50,6 +50,13 @@ class EntityReferenceRevisionsCompositeTest extends EntityKernelTestBase { */ protected $entityTypeManager; + /** + * The cron service. + * + * @var \Drupal\Core\Cron + */ + protected $cron; + /** * {@inheritdoc} */ @@ -79,9 +86,10 @@ protected function setUp() { )); $field->save(); - // Inject database connection and entity type manager for the tests. + // Inject database connection, entity type manager and cron for the tests. $this->database = \Drupal::database(); $this->entityTypeManager = \Drupal::entityTypeManager(); + $this->cron = \Drupal::service('cron'); } /** @@ -102,53 +110,116 @@ public function testEntityReferenceRevisionsCompositeRelationship() { $this->assertEquals(1, $composite_revisions_count); // Create a node with a reference to the test composite entity. + /** @var \Drupal\node\NodeInterface $node */ $node = Node::create(array( 'title' => $this->randomMachineName(), 'type' => 'article', - 'composite_reference' => $composite, )); $node->save(); + $node->set('composite_reference', $composite); + $this->assertTrue($node->hasTranslationChanges()); + $node->save(); // Assert that there is only 1 revision when creating a node. $node_revisions_count = \Drupal::entityQuery('node')->condition('nid', $node->id())->allRevisions()->count()->execute(); - $this->assertEqual($node_revisions_count, 1); + $this->assertEquals(1, $node_revisions_count); // Assert there is no new composite revision after creating a host entity. $composite_revisions_count = \Drupal::entityQuery('entity_test_composite')->condition('uuid', $composite->uuid())->allRevisions()->count()->execute(); $this->assertEquals(1, $composite_revisions_count); // Verify the value of parent type and id after create a node. $composite = EntityTestCompositeRelationship::load($composite->id()); - $this->assertEqual($composite->parent_type->value, $node->getEntityTypeId()); - $this->assertEqual($composite->parent_id->value, $node->id()); - $this->assertEqual($composite->parent_field_name->value, 'composite_reference'); + $this->assertEquals($node->getEntityTypeId(), $composite->parent_type->value); + $this->assertEquals($node->id(), $composite->parent_id->value); + $this->assertEquals('composite_reference', $composite->parent_field_name->value); // Create second revision of the node. $original_composite_revision = $node->composite_reference[0]->target_revision_id; $original_node_revision = $node->getRevisionId(); $node->setTitle('2nd revision'); $node->setNewRevision(); $node->save(); - $node = node_load($node->id(), TRUE); + $node = Node::load($node->id()); // Check the revision of the node. - $this->assertEqual('2nd revision', $node->getTitle(), 'New node revision has changed data.'); - $this->assertNotEqual($original_composite_revision, $node->composite_reference[0]->target_revision_id, 'Composite entity got new revision when its host did.'); + $this->assertEquals('2nd revision', $node->getTitle(), 'New node revision has changed data.'); + $this->assertNotEquals($original_composite_revision, $node->composite_reference[0]->target_revision_id, 'Composite entity got new revision when its host did.'); // Make sure that there are only 2 revisions. $node_revisions_count = \Drupal::entityQuery('node')->condition('nid', $node->id())->allRevisions()->count()->execute(); - $this->assertEqual($node_revisions_count, 2); + $this->assertEquals(2,$node_revisions_count); // Revert to first revision of the node. $node = $this->entityTypeManager->getStorage('node')->loadRevision($original_node_revision); $node->setNewRevision(); $node->isDefaultRevision(TRUE); $node->save(); - $node = node_load($node->id(), TRUE); + $node = Node::load($node->id()); // Check the revision of the node. - $this->assertNotEqual('2nd revision', $node->getTitle(), 'Node did not keep changed title after reversion.'); - $this->assertNotEqual($original_composite_revision, $node->composite_reference[0]->target_revision_id, 'Composite entity got new revision when its host reverted to an old revision.'); + $this->assertNotEquals('2nd revision', $node->getTitle(), 'Node did not keep changed title after reversion.'); + $this->assertNotEquals($original_composite_revision, $node->composite_reference[0]->target_revision_id, 'Composite entity got new revision when its host reverted to an old revision.'); + + $node_storage = $this->entityTypeManager->getStorage('node'); + // Test that removing composite references results in translation changes. + $node->set('composite_reference', []); + $this->assertTrue($node->hasTranslationChanges()); + + // Test that changing composite reference results in translation changes. + $changed_composite_reference = $composite; + $changed_composite_reference->set('name', 'Changing composite reference'); + $this->assertTrue((bool) $changed_composite_reference->isRevisionTranslationAffected()); + + $node->set('composite_reference', $changed_composite_reference); + $node->setNewRevision(); + $this->assertTrue($node->hasTranslationChanges()); + $node->save(); + $nid = $node->id(); + $node_storage->resetCache([$nid]); + /** @var \Drupal\node\NodeInterface $node */ + $node = $node_storage->load($nid); + + // Check the composite has changed. + $this->assertEquals('Changing composite reference', $node->get('composite_reference')->entity->getName()); + + // Make sure the node has 4 revisions. + $node_revisions_count = $node_storage->getQuery()->condition('nid', $nid)->allRevisions()->count()->execute(); + $this->assertEqual($node_revisions_count, 4); + + // Make sure the node has no revision with revision translation affected + // flag set to NULL. + $node_revisions_count = $node_storage->getQuery()->condition('nid', $nid)->allRevisions()->condition('revision_translation_affected', NULL, 'IS NULL')->count()->execute(); + $this->assertEqual($node_revisions_count, 0, 'Node has a revision with revision translation affected set to NULL'); + + // Revert the changes to avoid interfering with the delete test. + $node->set('composite_reference', $composite); // Test that the composite entity is deleted when its parent is deleted. $node->delete(); + $this->assertNotNull(EntityTestCompositeRelationship::load($composite->id())); + + $this->cron->run(); $this->assertNull(EntityTestCompositeRelationship::load($composite->id())); + + // Test that the deleting composite entity does not break the parent entity + // when creating a new revision. + $composite = EntityTestCompositeRelationship::create([ + 'name' => $this->randomMachineName(), + ]); + $composite->save(); + // Create a node with a reference to the test composite entity. + /** @var \Drupal\node\NodeInterface $node */ + $node = Node::create([ + 'title' => $this->randomMachineName(), + 'type' => 'article', + 'composite_reference' => $composite, + ]); + $node->save(); + // Delete the composite entity. + $composite->delete(); + // Re-apply the field item values to unset the computed "entity" property. + $field_item = $node->get('composite_reference')->get(0); + $field_item->setValue($field_item->getValue(), FALSE); + + $new_revision = $this->entityTypeManager->getStorage('node')->createRevision($node); + $this->assertTrue($new_revision->get('composite_reference')->isEmpty()); } /** @@ -178,23 +249,29 @@ function testCompositeRelationshipWithTranslationNonTranslatableField() { // Verify the value of parent type and id after create a node. $composite = EntityTestCompositeRelationship::load($composite->id()); - $this->assertEqual($composite->parent_type->value, $node->getEntityTypeId()); - $this->assertEqual($composite->parent_id->value, $node->id()); - $this->assertEqual($composite->parent_field_name->value, 'composite_reference'); + $this->assertEquals($node->getEntityTypeId(), $composite->parent_type->value); + $this->assertEquals($node->id(), $composite->parent_id->value); + $this->assertEquals('composite_reference', $composite->parent_field_name->value); $this->assertTrue($composite->hasTranslation('de')); - // Test that the composite entity is not when the german translation of the - // parent is deleted. + // Test that the composite entity is not deleted when the german translation + // of the parent is deleted. $node->removeTranslation('de'); $node->save(); $composite = EntityTestCompositeRelationship::load($composite->id()); $this->assertNotNull($composite); - // @todo Support deleting translations of a composite reference. - // @see https://www.drupal.org/node/2834314. - //$this->assertFalse($composite->hasTranslation('de')); + $this->assertFalse($composite->hasTranslation('de')); + + // Change the language of the entity, ensure that doesn't try to delete + // the default translation. + $node->set('langcode', 'de'); + $node->save(); + $composite = EntityTestCompositeRelationship::load($composite->id()); + $this->assertNotNull($composite); // Test that the composite entity is deleted when its parent is deleted. $node->delete(); + $this->cron->run(); $composite = EntityTestCompositeRelationship::load($composite->id()); $this->assertNull($composite); } @@ -228,23 +305,23 @@ function testCompositeRelationshipWithTranslationTranslatableField() { // Verify the value of parent type and id after create a node. $composite = EntityTestCompositeRelationship::load($composite->id()); - $this->assertEqual($composite->parent_type->value, $node->getEntityTypeId()); - $this->assertEqual($composite->parent_id->value, $node->id()); - $this->assertEqual($composite->parent_field_name->value, 'composite_reference'); + $this->assertEquals($node->getEntityTypeId(), $composite->parent_type->value); + $this->assertEquals($node->id(), $composite->parent_id->value); + $this->assertEquals('composite_reference', $composite->parent_field_name->value); - // Test that the composite entity is not when the german translation of the parent is deleted. + // Test that the composite entity is not deleted when the German parent + // translation is removed. $node->removeTranslation('de'); $node->save(); - //$this->entityTypeManager->getStorage('entity_test_composite')->resetCache(); + $this->cron->run(); $composite = EntityTestCompositeRelationship::load($composite->id()); $this->assertNotNull($composite); // Test that the composite entity is deleted when its parent is deleted. $node->delete(); + $this->cron->run(); $composite = EntityTestCompositeRelationship::load($composite->id()); - // @todo Support deletions for translatable fields. - // @see https://www.drupal.org/node/2834374 - // $this->assertNull($composite); + $this->assertNull($composite); } /** @@ -272,15 +349,15 @@ function testCompositeRelationshipWithRevisions() { $composite = EntityTestCompositeRelationship::load($composite->id()); $composite_original_revision_id = $composite->getRevisionId(); $node_original_revision_id = $node->getRevisionId(); - $this->assertEqual($composite->parent_type->value, $node->getEntityTypeId()); - $this->assertEqual($composite->parent_id->value, $node->id()); - $this->assertEqual($composite->parent_field_name->value, 'composite_reference'); + $this->assertEquals($node->getEntityTypeId(), $composite->parent_type->value); + $this->assertEquals($node->id(), $composite->parent_id->value); + $this->assertEquals('composite_reference', $composite->parent_field_name->value); $node->setNewRevision(TRUE); $node->save(); // Ensure that we saved a new revision ID. $composite = EntityTestCompositeRelationship::load($composite->id()); - $this->assertNotEqual($composite->getRevisionId(), $composite_original_revision_id); + $this->assertNotEquals($composite_original_revision_id, $composite->getRevisionId()); // Test that deleting the first revision does not delete the composite. $this->entityTypeManager->getStorage('node')->deleteRevision($node_original_revision_id); @@ -293,6 +370,7 @@ function testCompositeRelationshipWithRevisions() { // Test that the composite entity is deleted when its parent is deleted. $node->delete(); + $this->cron->run(); $composite = EntityTestCompositeRelationship::load($composite->id()); $this->assertNull($composite); } @@ -400,8 +478,235 @@ function testCompositeRelationshipDuplicatedRevisions() { // Test that the composite entity is deleted when its parent is deleted. $node->delete(); + $this->cron->run(); $composite = EntityTestCompositeRelationship::load($composite2->id()); $this->assertNull($composite); } + /** + * Tests the composite entity is deleted after removing its reference. + */ + public function testCompositeDeleteAfterRemovingReference() { + list($composite, $node) = $this->assignCompositeToNode(); + + // Remove reference to the composite entity from the node. + $node->set('composite_reference', NULL); + $node->save(); + + // Verify that the composite entity is not yet removed after deleting the + // parent. + $node->delete(); + $composite = EntityTestCompositeRelationship::load($composite->id()); + $this->assertNotNull($composite); + + // Verify that the composite entity is removed after running cron. + $this->cron->run(); + $composite = EntityTestCompositeRelationship::load($composite->id()); + $this->assertNull($composite); + } + + /** + * Tests the composite entity is deleted after removing its reference. + * + * Includes revisions on the host entity. + */ + public function testCompositeDeleteAfterRemovingReferenceWithRevisions() { + list($composite, $node) = $this->assignCompositeToNode(); + + // Remove reference to the composite entity from the node in a new revision. + $node->set('composite_reference', NULL); + $node->setNewRevision(); + $node->save(); + $composite = EntityTestCompositeRelationship::load($composite->id()); + // Verify the composite entity is not removed on nodes with revisions. + $this->assertNotNull($composite); + + // Verify that the composite entity is not yet removed after deleting the + // parent. + $node->delete(); + $composite = EntityTestCompositeRelationship::load($composite->id()); + $this->assertNotNull($composite); + + // Verify that the composite entity is removed after running cron. + $this->cron->run(); + $composite = EntityTestCompositeRelationship::load($composite->id()); + $this->assertNull($composite); + } + + /** + * Tests the composite entity is not deleted when changing parents. + * + * Includes revisions on the host entity. + */ + public function testCompositeDeleteAfterChangingParent() { + list($composite, $node) = $this->assignCompositeToNode(); + // Remove reference to the composite entity from the node. + $node->set('composite_reference', NULL); + $node->setNewRevision(); + $node->save(); + + // Setting a new revision of the composite entity in the second node. + $composite = EntityTestCompositeRelationship::load($composite->id()); + $composite->setNewRevision(TRUE); + $composite->save(); + $second_node = Node::create([ + 'title' => 'Second node', + 'type' => 'article', + 'composite_reference' => $composite, + ]); + $second_node->save(); + // Remove reference to the composite entity from the node. + $second_node->set('composite_reference', NULL); + $second_node->setNewRevision(TRUE); + $second_node->save(); + // Verify the composite entity is not removed on nodes with revisions. + $composite = EntityTestCompositeRelationship::load($composite->id()); + $this->assertNotNull($composite); + // Verify the amount of revisions of each entity. + $this->assertRevisionCount(2, 'entity_test_composite', $composite->id()); + $this->assertRevisionCount(2, 'node', $node->id()); + $this->assertRevisionCount(2, 'node', $second_node->id()); + // Test that the composite entity is not deleted when its new parent is + // deleted, since it is still being used in a previous revision with a + // different parent. + $second_node->delete(); + $this->cron->run(); + $composite = EntityTestCompositeRelationship::load($composite->id()); + $this->assertNotNull($composite); + + // Delete the parent of the previous revision. + $node->delete(); + + // Verify that the composite entity is removed after running cron. + $this->cron->run(); + $composite = EntityTestCompositeRelationship::load($composite->id()); + $this->assertNull($composite); + } + + /** + * Composite entity with revisions isn't deleted when changing parents. + * + * Includes revisions on the host entity. + */ + public function testCompositeDeleteRevisionAfterChangingParent() { + list($composite, $node) = $this->assignCompositeToNode(); + // Remove reference to the composite entity from the node. + $node->set('composite_reference', NULL); + $node->setNewRevision(); + $node->save(); + + // Setting a new revision of the composite entity in the second node. + $composite = EntityTestCompositeRelationship::load($composite->id()); + $composite->setNewRevision(TRUE); + $composite->save(); + $composite = EntityTestCompositeRelationship::load($composite->id()); + $second_node = Node::create([ + 'title' => 'Second node', + 'type' => 'article', + 'composite_reference' => $composite, + ]); + $second_node->save(); + // Remove reference to the composite entity from the node. + $second_node->set('composite_reference', NULL); + $second_node->setNewRevision(TRUE); + $second_node->save(); + // Verify the composite entity is not removed on nodes with revisions. + $composite = EntityTestCompositeRelationship::load($composite->id()); + $this->assertNotNull($composite); + // Verify the amount of revisions of each entity. + $this->assertRevisionCount(2, 'entity_test_composite', $composite->id()); + $this->assertRevisionCount(2, 'node', $node->id()); + $this->assertRevisionCount(2, 'node', $second_node->id()); + // Test that the composite entity is not deleted when its old parent is + // deleted. + $node->delete(); + $composite = EntityTestCompositeRelationship::load($composite->id()); + $this->assertNotNull($composite); + + // Verify that the composite entity is not removed after running cron but + // the previous unused revision is deleted. + $this->cron->run(); + $composite = EntityTestCompositeRelationship::load($composite->id()); + $this->assertNotNull($composite); + $this->assertRevisionCount(1, 'entity_test_composite', $composite->id()); + } + + /** + * Tests the composite entity is not deleted when duplicating host entity. + * + * Includes revisions on the host entity. + */ + public function testCompositeDeleteAfterDuplicatingParent() { + list($composite, $node) = $this->assignCompositeToNode(); + $node->setNewRevision(TRUE); + $node->save(); + + // Create a duplicate of the node. + $duplicate_node = $node->createDuplicate(); + $duplicate_node->save(); + $duplicate_node->setNewRevision(TRUE); + $duplicate_node->save(); + + // Verify the amount of revisions of each entity. + $this->assertRevisionCount(3, 'entity_test_composite', $composite->id()); + $this->assertRevisionCount(2, 'node', $node->id()); + $this->assertRevisionCount(2, 'node', $duplicate_node->id()); + // Test that the composite entity is not deleted when the duplicate is + // deleted. + $duplicate_node->delete(); + $composite = EntityTestCompositeRelationship::load($composite->id()); + $this->assertNotNull($composite); + + $this->cron->run(); + $composite = EntityTestCompositeRelationship::load($composite->id()); + $this->assertNotNull($composite); + } + + /** + * Asserts the revision count of a certain entity. + * + * @param int $expected + * The expected count. + * @param string $entity_type_id + * The entity type ID, e.g. node. + * @param int $entity_id + * The entity ID. + */ + protected function assertRevisionCount($expected, $entity_type_id, $entity_id) { + $id_field = \Drupal::entityTypeManager() + ->getDefinition($entity_type_id) + ->getKey('id'); + $revision_count = \Drupal::entityQuery($entity_type_id) + ->condition($id_field, $entity_id) + ->allRevisions() + ->count() + ->execute(); + $this->assertEquals($expected, $revision_count); + } + + /** + * Creates and assigns the composite entity to a node. + * + * @param string $node_type + * The node type. + * + * @return array + * An array containing a composite and a node entity. + */ + protected function assignCompositeToNode($node_type = 'article') { + $composite = EntityTestCompositeRelationship::create([ + 'uuid' => $this->randomMachineName(), + 'name' => $this->randomMachineName(), + ]); + $composite->save(); + $node = Node::create([ + 'title' => $this->randomMachineName(), + 'type' => $node_type, + 'composite_reference' => $composite, + ]); + $node->save(); + + return [$composite, $node]; + } + } diff --git a/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTranslatableFieldTest.php b/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTranslatableFieldTest.php new file mode 100644 index 0000000000000000000000000000000000000000..4e261bfcf4385ea6fc5314eb529df414f70304a1 --- /dev/null +++ b/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTranslatableFieldTest.php @@ -0,0 +1,351 @@ +<?php + +namespace Drupal\Tests\entity_reference_revisions\Kernel; + +use Drupal\entity_composite_relationship_test\Entity\EntityTestCompositeRelationship; +use Drupal\field\Entity\FieldConfig; +use Drupal\field\Entity\FieldStorageConfig; +use Drupal\KernelTests\Core\Entity\EntityKernelTestBase; +use Drupal\language\Entity\ConfigurableLanguage; +use Drupal\node\Entity\Node; +use Drupal\node\Entity\NodeType; +use Drupal\Tests\node\Traits\ContentTypeCreationTrait; +use Drupal\Tests\node\Traits\NodeCreationTrait; + +/** + * Tests entity_reference_revisions composites with a translatable field. + * + * @group entity_reference_revisions + */ +class EntityReferenceRevisionsCompositeTranslatableFieldTest extends EntityKernelTestBase { + + use ContentTypeCreationTrait; + use NodeCreationTrait; + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array( + 'node', + 'field', + 'entity_reference_revisions', + 'entity_composite_relationship_test', + 'language', + 'content_translation' + ); + + /** + * The current database connection. + * + * @var \Drupal\Core\Database\Connection + */ + protected $database; + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + * + */ + protected $entityTypeManager; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + ConfigurableLanguage::createFromLangcode('de')->save(); + ConfigurableLanguage::createFromLangcode('fr')->save(); + + $this->installEntitySchema('entity_test_composite'); + $this->installSchema('node', ['node_access']); + + // Create article content type. + NodeType::create(['type' => 'article', 'name' => 'Article'])->save(); + + // Create the reference to the composite entity test. + $field_storage = FieldStorageConfig::create(array( + 'field_name' => 'composite_reference', + 'entity_type' => 'node', + 'type' => 'entity_reference_revisions', + 'settings' => array( + 'target_type' => 'entity_test_composite' + ), + )); + $field_storage->save(); + $field = FieldConfig::create(array( + 'field_storage' => $field_storage, + 'bundle' => 'article', + 'translatable' => TRUE, + )); + $field->save(); + + // Inject database connection and entity type manager for the tests. + $this->database = \Drupal::database(); + $this->entityTypeManager = \Drupal::entityTypeManager(); + + // @todo content_translation should not be needed for a storage test, but + // \Drupal\Core\Entity\ContentEntityBase::isTranslatable() only returns + // TRUE if the bundle is explicitly translatable. + \Drupal::service('content_translation.manager')->setEnabled('node', 'article', TRUE); + \Drupal::service('content_translation.manager')->setEnabled('entity_test_composite', 'entity_test_composite', TRUE); + \Drupal::service('content_translation.manager')->setBundleTranslationSettings('node', 'article', [ + 'untranslatable_fields_hide' => TRUE, + ]); + \Drupal::service('entity_type.bundle.info')->clearCachedBundles(); + } + + /** + * Test the storage for handling pending revisions with translations. + */ + public function testCompositePendingRevisionTranslation() { + /** @var \Drupal\node\NodeStorageInterface $node_storage */ + $node_storage = \Drupal::entityTypeManager()->getStorage('node'); + + // Create the test composite entity. + $composite = EntityTestCompositeRelationship::create([ + 'langcode' => 'en', + 'name' => 'Initial Source Composite', + ]); + $composite->save(); + + // Create a node with a reference to the test composite entity. + $node = Node::create([ + 'langcode' => 'en', + 'title' => 'Initial Source Node', + 'type' => 'article', + 'composite_reference' => $composite, + ]); + $node->save(); + + /** @var \Drupal\node\NodeInterface $node */ + $node = $node_storage->load($node->id()); + + // Assert the revision count. + $this->assertRevisionCount(1, 'node', $node->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite->id()); + + // Create a translation as a pending revision for both the composite and the + // node. While technically, the referenced composite could be the same + // entity, for translatable fields, it makes more sense if each translation + // points to a separate entity, each only with a single language. + $composite_de = $node->get('composite_reference')->entity->createDuplicate(); + $composite_de->set('langcode', 'de'); + $composite_de->set('name', 'Pending Revision Composite #1 DE'); + /** @var \Drupal\node\NodeInterface $node_de */ + $node_de = $node->addTranslation('de', ['title' => 'Pending Revision Node #1 DE', 'composite_reference' => $composite_de] + $node->toArray()); + $node_de->setNewRevision(TRUE); + $node_de->isDefaultRevision(FALSE); + $node_de->save(); + + // Assert the revision count. + $this->assertRevisionCount(2, 'node', $node->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_de->id()); + + // The DE translation will now reference to a pending revision of the + // composite entity but the en translation will reference the existing, + // unchanged revision. + /** @var \Drupal\node\NodeInterface $node_revision */ + $node_revision = $node_storage->loadRevision($node_de->getRevisionId()); + $this->assertFalse($node_revision->isDefaultRevision()); + $this->assertFalse((bool) $node_revision->isRevisionTranslationAffected()); + $this->assertEquals('Initial Source Node', $node_revision->label()); + $this->assertTrue($node_revision->get('composite_reference')->entity->isDefaultRevision()); + $this->assertEquals('Initial Source Composite', $node_revision->get('composite_reference')->entity->label()); + $this->assertFalse($node_revision->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals($node->get('composite_reference')->target_revision_id, $node_revision->get('composite_reference')->target_revision_id); + + $node_de = $node_revision->getTranslation('de'); + $this->assertTrue((bool) $node_de->isRevisionTranslationAffected()); + $this->assertEquals('Pending Revision Node #1 DE', $node_de->label()); + // The composite is the default revision because it is a new entity. + $this->assertTrue($node_de->get('composite_reference')->entity->isDefaultRevision()); + $this->assertEquals('Pending Revision Composite #1 DE', $node_de->get('composite_reference')->entity->label()); + $this->assertNotEquals($node->get('composite_reference')->target_revision_id, $node_de->get('composite_reference')->target_revision_id); + + // Reload the default revision of the node, make sure that the composite + // there is unchanged. + $node = $node_storage->load($node->id()); + $this->assertFalse($node->hasTranslation('de')); + $this->assertEquals('Initial Source Node', $node->label()); + $this->assertFalse($node->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals('Initial Source Composite', $node->get('composite_reference')->entity->label()); + + // Create a second translation revision for FR. + $composite_fr = $node->get('composite_reference')->entity->createDuplicate(); + $composite_fr->set('langcode', 'fr'); + $composite_fr->set('name', 'Pending Revision Composite #1 FR'); + $node_fr = $node->addTranslation('fr', ['title' => 'Pending Revision Node #1 FR', 'composite_reference' => $composite_fr] + $node->toArray()); + $node_fr->setNewRevision(TRUE); + $node_fr->isDefaultRevision(FALSE); + $node_fr->save(); + + // Assert the revision count. + $this->assertRevisionCount(3, 'node', $node->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_de->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_fr->id()); + + // Now assert that all 3 revisions exist as expected. Two translation + // pending revisions, each has the original revision as parent without + // any existing translation. + /** @var \Drupal\node\NodeInterface $node_fr */ + $node_revision = $node_storage->loadRevision($node_fr->getRevisionId()); + $this->assertFalse($node_revision->isDefaultRevision()); + $this->assertFalse((bool) $node_revision->isRevisionTranslationAffected()); + $this->assertEquals('Initial Source Node', $node_revision->label()); + $this->assertTrue($node_revision->get('composite_reference')->entity->isDefaultRevision()); + $this->assertEquals('Initial Source Composite', $node_revision->get('composite_reference')->entity->label()); + $this->assertFalse($node_revision->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals($node->get('composite_reference')->target_revision_id, $node_revision->get('composite_reference')->target_revision_id); + + $node_fr = $node_revision->getTranslation('fr'); + $this->assertTrue((bool) $node_fr->isRevisionTranslationAffected()); + $this->assertEquals('Pending Revision Node #1 FR', $node_fr->label()); + $this->assertTrue($node_fr->get('composite_reference')->entity->isDefaultRevision()); + $this->assertEquals('Pending Revision Composite #1 FR', $node_fr->get('composite_reference')->entity->label()); + $this->assertNotEquals($node->get('composite_reference')->target_revision_id, $node_fr->get('composite_reference')->target_revision_id); + + $node_de = $node_storage->loadRevision($node_de->getRevisionId())->getTranslation('de'); + $this->assertTrue((bool) $node_de->isRevisionTranslationAffected()); + $this->assertEquals('Pending Revision Node #1 DE', $node_de->label()); + $this->assertTrue($node_de->get('composite_reference')->entity->isDefaultRevision()); + $this->assertEquals('Pending Revision Composite #1 DE', $node_de->get('composite_reference')->entity->label()); + $this->assertNotEquals($node->get('composite_reference')->target_revision_id, $node_de->get('composite_reference')->target_revision_id); + + // Reload the default revision of the node, make sure that the composite + // there is unchanged. + $node = $node_storage->load($node->id()); + $this->assertFalse($node->hasTranslation('de')); + $this->assertEquals('Initial Source Node', $node->label()); + $this->assertFalse($node->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals('Initial Source Composite', $node->get('composite_reference')->entity->label()); + + // Now make a change to the initial source revision, save as a new default + // revision. + $initial_revision_id = $node->getRevisionId(); + $node->get('composite_reference')->entity->set('name', 'Updated Source Composite'); + $node->setTitle('Updated Source Node'); + $node->setNewRevision(TRUE); + $node->save(); + + // Assert the revision count. + $this->assertRevisionCount(4, 'node', $node->id()); + $this->assertRevisionCount(2, 'entity_test_composite', $composite->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_de->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_fr->id()); + + // Assert the two english revisions. + // Reload the default revision of the node, make sure that the composite + // there is unchanged. + $node = $node_storage->load($node->id()); + $this->assertTrue($node->isDefaultRevision()); + $this->assertFalse($node->hasTranslation('de')); + $this->assertFalse($node->hasTranslation('fr')); + $this->assertTrue((bool) $node->isRevisionTranslationAffected()); + $this->assertEquals('Updated Source Node', $node->label()); + $this->assertTrue($node->get('composite_reference')->entity->isDefaultRevision()); + $this->assertFalse($node->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals('Updated Source Composite', $node->get('composite_reference')->entity->label()); + + $node_initial = $node_storage->loadRevision($initial_revision_id); + $this->assertFalse($node_initial->isDefaultRevision()); + $this->assertFalse($node_initial->hasTranslation('de')); + $this->assertFalse($node_initial->hasTranslation('fr')); + $this->assertEquals('Initial Source Node', $node_initial->label()); + $this->assertFalse($node_initial->get('composite_reference')->entity->isDefaultRevision()); + $this->assertFalse($node_initial->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals('Initial Source Composite', $node_initial->get('composite_reference')->entity->label()); + + // Now publish the FR pending revision. + $node_storage->createRevision($node_fr->getTranslation('fr'))->save(); + + // Assert the revision count. + $this->assertRevisionCount(5, 'node', $node->id()); + $this->assertRevisionCount(2, 'entity_test_composite', $composite->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_de->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_fr->id()); + + // The new default revision should now have the updated english source and + // the french pending revision. + $node = $node_storage->load($node->id()); + $this->assertTrue($node->isDefaultRevision()); + $this->assertFalse($node->hasTranslation('de')); + $this->assertTrue($node->hasTranslation('fr')); + $node_fr = $node->getTranslation('fr'); + $this->assertFalse((bool) $node->isRevisionTranslationAffected()); + $this->assertTrue((bool) $node->getTranslation('fr')->isRevisionTranslationAffected()); + $this->assertEquals('Updated Source Node', $node->label()); + $this->assertTrue($node->get('composite_reference')->entity->isDefaultRevision()); + $this->assertFalse($node->get('composite_reference')->entity->hasTranslation('de')); + $this->assertTrue($node_fr->get('composite_reference')->entity->hasTranslation('fr')); + $this->assertEquals('Pending Revision Node #1 FR', $node_fr->label()); + $this->assertEquals('Pending Revision Composite #1 FR', $node_fr->get('composite_reference')->entity->getTranslation('fr')->label()); + $this->assertEquals('Updated Source Composite', $node->get('composite_reference')->entity->label()); + + // Now publish the DE pending revision as well. + $node_storage->createRevision($node_de->getTranslation('de'))->save(); + + // Assert the revision count. + $this->assertRevisionCount(6, 'node', $node->id()); + $this->assertRevisionCount(2, 'entity_test_composite', $composite->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_de->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_fr->id()); + + // The new default revision should now have the updated source and both + // translations. + $node = $node_storage->load($node->id()); + $this->assertTrue($node->isDefaultRevision()); + $this->assertTrue($node->hasTranslation('de')); + $this->assertTrue($node->hasTranslation('fr')); + $node_fr = $node->getTranslation('fr'); + $node_de = $node->getTranslation('de'); + $this->assertFalse((bool) $node->isRevisionTranslationAffected()); + $this->assertFalse((bool) $node->getTranslation('fr')->isRevisionTranslationAffected()); + $this->assertTrue((bool) $node->getTranslation('de')->isRevisionTranslationAffected()); + $this->assertEquals('Updated Source Node', $node->label()); + + // Each translation only has the composite in its translation. + $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('en')); + $this->assertFalse($node->get('composite_reference')->entity->hasTranslation('de')); + $this->assertFalse($node->get('composite_reference')->entity->hasTranslation('fr')); + $this->assertFalse($node_fr->get('composite_reference')->entity->hasTranslation('en')); + $this->assertTrue($node_fr->get('composite_reference')->entity->hasTranslation('fr')); + $this->assertFalse($node_fr->get('composite_reference')->entity->hasTranslation('de')); + $this->assertFalse($node_de->get('composite_reference')->entity->hasTranslation('en')); + $this->assertTrue($node_de->get('composite_reference')->entity->hasTranslation('de')); + $this->assertFalse($node_de->get('composite_reference')->entity->hasTranslation('fr')); + + $this->assertEquals('Pending Revision Node #1 FR', $node_fr->label()); + $this->assertEquals('Pending Revision Composite #1 FR', $node_fr->get('composite_reference')->entity->getTranslation('fr')->label()); + $this->assertEquals('Pending Revision Node #1 DE', $node_de->label()); + $this->assertEquals('Pending Revision Composite #1 DE', $node_de->get('composite_reference')->entity->getTranslation('de')->label()); + $this->assertEquals('Updated Source Composite', $node->get('composite_reference')->entity->label()); + } + + /** + * Asserts the revision count of a certain entity. + * + * @param int $expected + * The expected count. + * @param string $entity_type_id + * The entity type ID, e.g. node. + * @param int $entity_id + * The entity ID. + */ + protected function assertRevisionCount($expected, $entity_type_id, $entity_id) { + $id_field = \Drupal::entityTypeManager()->getDefinition($entity_type_id)->getKey('id'); + + $revision_count = \Drupal::entityQuery($entity_type_id) + ->condition($id_field, $entity_id) + ->allRevisions() + ->count() + ->execute(); + $this->assertEquals($expected, $revision_count); + } + +} diff --git a/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTranslationTest.php b/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTranslationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..2a406176ae35ef6e75ddaee7838a1ce2bf0d05b9 --- /dev/null +++ b/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTranslationTest.php @@ -0,0 +1,641 @@ +<?php + +namespace Drupal\Tests\entity_reference_revisions\Kernel; + +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Field\FieldStorageDefinitionInterface; +use Drupal\entity_composite_relationship_test\Entity\EntityTestCompositeRelationship; +use Drupal\field\Entity\FieldConfig; +use Drupal\field\Entity\FieldStorageConfig; +use Drupal\KernelTests\Core\Entity\EntityKernelTestBase; +use Drupal\language\Entity\ConfigurableLanguage; +use Drupal\node\Entity\Node; +use Drupal\node\Entity\NodeType; +use Drupal\Tests\node\Traits\ContentTypeCreationTrait; +use Drupal\Tests\node\Traits\NodeCreationTrait; + +/** + * Tests the entity_reference_revisions composite relationship. + * + * @group entity_reference_revisions + */ +class EntityReferenceRevisionsCompositeTranslationTest extends EntityKernelTestBase { + + use ContentTypeCreationTrait; + use NodeCreationTrait; + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = [ + 'node', + 'field', + 'entity_reference_revisions', + 'entity_composite_relationship_test', + 'language', + 'content_translation' + ]; + + /** + * The current database connection. + * + * @var \Drupal\Core\Database\Connection + */ + protected $database; + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + * + */ + protected $entityTypeManager; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + ConfigurableLanguage::createFromLangcode('de')->save(); + ConfigurableLanguage::createFromLangcode('fr')->save(); + + $this->installEntitySchema('entity_test_composite'); + $this->installSchema('node', ['node_access']); + + // Create article content type. + NodeType::create(['type' => 'article', 'name' => 'Article'])->save(); + + // Create the reference to the composite entity test. + $field_storage = FieldStorageConfig::create([ + 'field_name' => 'composite_reference', + 'entity_type' => 'node', + 'type' => 'entity_reference_revisions', + 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, + 'settings' => [ + 'target_type' => 'entity_test_composite' + ], + ]); + $field_storage->save(); + $field = FieldConfig::create([ + 'field_storage' => $field_storage, + 'bundle' => 'article', + 'translatable' => FALSE, + ]); + $field->save(); + + // Create an untranslatable field on the composite entity. + $text_field_storage = FieldStorageConfig::create([ + 'field_name' => 'field_untranslatable', + 'entity_type' => 'entity_test_composite', + 'type' => 'string', + ]); + $text_field_storage->save(); + $text_field = FieldConfig::create([ + 'field_storage' => $text_field_storage, + 'bundle' => 'entity_test_composite', + 'translatable' => FALSE, + ]); + $text_field->save(); + + // Add a nested composite field. + $field_storage = FieldStorageConfig::create([ + 'field_name' => 'composite_reference', + 'entity_type' => 'entity_test_composite', + 'type' => 'entity_reference_revisions', + 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, + 'settings' => [ + 'target_type' => 'entity_test_composite' + ], + ]); + $field_storage->save(); + $field = FieldConfig::create([ + 'field_storage' => $field_storage, + 'bundle' => 'entity_test_composite', + 'translatable' => FALSE, + ]); + $field->save(); + + // Inject database connection and entity type manager for the tests. + $this->database = \Drupal::database(); + $this->entityTypeManager = \Drupal::entityTypeManager(); + + // @todo content_translation should not be needed for a storage test, but + // \Drupal\Core\Entity\ContentEntityBase::isTranslatable() only returns + // TRUE if the bundle is explicitly translatable. + \Drupal::service('content_translation.manager')->setEnabled('node', 'article', TRUE); + \Drupal::service('content_translation.manager')->setEnabled('entity_test_composite', 'entity_test_composite', TRUE); + \Drupal::service('content_translation.manager')->setBundleTranslationSettings('node', 'article', [ + 'untranslatable_fields_hide' => TRUE, + ]); + \Drupal::service('entity_type.bundle.info')->clearCachedBundles(); + } + + /** + * Test the storage for handling pending revisions with translations. + */ + public function testCompositePendingRevisionTranslation() { + /** @var \Drupal\node\NodeStorageInterface $node_storage */ + $node_storage = \Drupal::entityTypeManager()->getStorage('node'); + + // Create a nested composite entity. + $nested_composite = EntityTestCompositeRelationship::create([ + 'langcode' => 'en', + 'name' => 'Initial Nested Source Composite', + ]); + $nested_composite->save(); + + // Create a composite entity. + $composite = EntityTestCompositeRelationship::create([ + 'langcode' => 'en', + 'name' => 'Initial Source Composite', + 'field_untranslatable' => 'Initial untranslatable field', + 'composite_reference' => $nested_composite, + ]); + $composite->save(); + + // Create a node with a reference to the test composite entity. + $node = Node::create([ + 'langcode' => 'en', + 'title' => 'Initial Source Node', + 'type' => 'article', + 'composite_reference' => $composite, + ]); + $node->save(); + $initial_revision_id = $node->getRevisionId(); + + /** @var \Drupal\node\NodeInterface $node */ + $node = $node_storage->load($node->id()); + + // Assert that there is only 1 revision when creating a node. + $this->assertRevisionCount(1, $node); + // Assert there is no new composite revision after creating a host entity. + $this->assertRevisionCount(1, $composite); + // Assert there is no new composite revision after creating a host entity. + $this->assertRevisionCount(1, $nested_composite); + + // Create a second nested composite entity. + $second_nested_composite = EntityTestCompositeRelationship::create([ + 'langcode' => 'en', + 'name' => 'Initial Nested Composite #2', + ]); + + // Add a pending revision. + $node = $node_storage->createRevision($node, FALSE); + $node->get('composite_reference')->entity->get('composite_reference')->appendItem($second_nested_composite); + $node->save(); + $pending_en_revision_id = $node->getRevisionId(); + + $this->assertRevisionCount(2, $node); + $this->assertRevisionCount(2, $composite); + $this->assertRevisionCount(2, $nested_composite); + $this->assertRevisionCount(1, $second_nested_composite); + + // Create a DE translation, start as a draft to replicate the behavior of + // the UI. + $node_de = $node->addTranslation('de', ['title' => 'New Node #1 DE'] + $node->toArray()); + $node_de = $node_storage->createRevision($node_de, FALSE); + + // Despite starting of the draft revision, creating draft of the translation + // uses the paragraphs of the default revision. + $this->assertCount(1, $node_de->get('composite_reference')->entity->get('composite_reference')); + + $node_de->get('composite_reference')->entity->getTranslation('de')->set('name', 'New Composite #1 DE'); + $node_de->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('de')->set('name', 'New Nested Composite #1 DE'); + $node_de->isDefaultRevision(TRUE); + $violations = $node_de->validate(); + foreach ($violations as $violation) { + $this->fail($violation->getPropertyPath() . ': ' . $violation->getMessage()); + } + $this->assertEquals(0, count($violations)); + $node_de->save(); + + $this->assertRevisionCount(3, $node); + $this->assertRevisionCount(3, $composite); + $this->assertRevisionCount(3, $nested_composite); + $this->assertRevisionCount(1, $second_nested_composite); + + // Update the translation as a pending revision for both the composite and + // the node. + $node_de->get('composite_reference')->entity->getTranslation('de')->set('name', 'Pending Revision Composite #1 DE'); + $node_de->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('de')->set('name', 'Pending Nested Composite #1 DE'); + $node_de->set('title', 'Pending Revision Node #1 DE'); + $node_de->setNewRevision(TRUE); + $node_de->isDefaultRevision(FALSE); + $violations = $node_de->validate(); + foreach ($violations as $violation) { + $this->fail($violation->getMessage()); + } + $this->assertEquals(0, count($violations)); + $node_de->save(); + + $this->assertRevisionCount(4, $node); + $this->assertRevisionCount(4, $composite); + $this->assertRevisionCount(4, $nested_composite); + $this->assertRevisionCount(1, $second_nested_composite); + + /** @var \Drupal\node\NodeInterface $node_de */ + $node_de = $node_storage->loadRevision($node_de->getRevisionId()); + $this->assertFalse($node_de->isDefaultRevision()); + $this->assertFalse((bool) $node_de->isRevisionTranslationAffected()); + $this->assertTrue((bool) $node_de->getTranslation('de')->isRevisionTranslationAffected()); + $this->assertEquals('Pending Revision Node #1 DE', $node_de->getTranslation('de')->label()); + $this->assertEquals('Initial Source Node', $node_de->label()); + $this->assertFalse($node_de->get('composite_reference')->entity->isDefaultRevision()); + $this->assertEquals('Pending Revision Composite #1 DE', $node_de->get('composite_reference')->entity->getTranslation('de')->label()); + $this->assertEquals('Pending Nested Composite #1 DE', $node_de->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('de')->label()); + $this->assertEquals('Initial untranslatable field', $node_de->get('composite_reference')->entity->getTranslation('de')->get('field_untranslatable')->value); + $this->assertEquals('Initial Source Composite', $node_de->get('composite_reference')->entity->label()); + + // Reload the default revision of the node, make sure that the composite + // there is unchanged. + $node = $node_storage->load($node->id()); + $this->assertTrue($node->hasTranslation('de')); + $this->assertEquals('Initial Source Node', $node->label()); + $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals('Initial Source Composite', $node->get('composite_reference')->entity->label()); + + // Create a FR translation, start as a draft to replicate the behavior of + // the UI. + $node_fr = $node->addTranslation('fr', ['title' => 'Pending Revision Node #1 FR'] + $node->toArray()); + $node_fr = $node_storage->createRevision($node_fr, FALSE); + $node_fr->get('composite_reference')->entity->getTranslation('fr')->set('name', 'Pending Revision Composite #1 FR'); + $node_fr->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('fr')->set('name', 'Pending Nested Composite #1 FR'); + $violations = $node_fr->validate(); + $this->assertEquals(0, count($violations)); + $node_fr->save(); + + // Now assert that all 3 revisions exist as expected. Two translation + // pending revisions, each composite has the original revision as parent + // without any existing translation. + /** @var \Drupal\node\NodeInterface $node_fr */ + $node_fr = $node_storage->loadRevision($node_fr->getRevisionId()); + $this->assertFalse($node_fr->isDefaultRevision()); + $this->assertTrue($node_fr->hasTranslation('de')); + $this->assertFalse((bool) $node_fr->isRevisionTranslationAffected()); + $this->assertTrue((bool) $node_fr->getTranslation('fr')->isRevisionTranslationAffected()); + $this->assertEquals('Pending Revision Node #1 FR', $node_fr->getTranslation('fr')->label()); + $this->assertEquals('Initial Source Node', $node_fr->label()); + $this->assertFalse($node_fr->get('composite_reference')->entity->isDefaultRevision()); + $this->assertTrue($node_fr->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals('Pending Revision Composite #1 FR', $node_fr->get('composite_reference')->entity->getTranslation('fr')->label()); + $this->assertEquals('Pending Nested Composite #1 FR', $node_fr->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('fr')->label()); + $this->assertEquals('Initial untranslatable field', $node_fr->get('composite_reference')->entity->getTranslation('fr')->get('field_untranslatable')->value); + $this->assertEquals('Initial Source Composite', $node_fr->get('composite_reference')->entity->label()); + + $node_de = $node_storage->loadRevision($node_de->getRevisionId()); + $this->assertFalse($node_de->isDefaultRevision()); + $this->assertFalse($node_de->hasTranslation('fr')); + $this->assertEquals('Pending Revision Node #1 DE', $node_de->getTranslation('de')->label()); + $this->assertEquals('Initial Source Node', $node_de->label()); + $this->assertFalse($node_de->get('composite_reference')->entity->isDefaultRevision()); + $this->assertFalse($node_de->get('composite_reference')->entity->hasTranslation('fr')); + $this->assertEquals('Pending Revision Composite #1 DE', $node_de->get('composite_reference')->entity->getTranslation('de')->label()); + $this->assertEquals('Pending Nested Composite #1 DE', $node_de->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('de')->label()); + $this->assertEquals('Initial untranslatable field', $node_de->get('composite_reference')->entity->getTranslation('de')->get('field_untranslatable')->value); + $this->assertEquals('Initial Source Composite', $node_de->get('composite_reference')->entity->label()); + + // Reload the default revision of the node, make sure that the composite + // there is unchanged. + $node = $node_storage->load($node->id()); + $this->assertTrue($node->hasTranslation('de')); + $this->assertEquals('Initial Source Node', $node->label()); + $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals('Initial Source Composite', $node->get('composite_reference')->entity->label()); + + // Create another pending EN revision and make that the default. + $node = $node_storage->loadRevision($pending_en_revision_id); + $new_revision = $node_storage->createRevision($node); + $new_revision->get('composite_reference')->entity->set('name', 'Updated Source Composite'); + $new_revision->get('composite_reference')->entity->set('field_untranslatable', 'Updated untranslatable field'); + $new_revision->setTitle('Updated Source Node'); + $new_revision->get('composite_reference')->entity->get('composite_reference')[1]->entity->set('name', 'Draft Nested Source Composite #2'); + $violations = $new_revision->validate(); + $this->assertEquals(0, count($violations)); + $new_revision->save(); + + // Assert the two english revisions. + // Reload the default revision of the node, make sure that the composite + // there is unchanged. + $node = $node_storage->load($node->id()); + $this->assertTrue($node->isDefaultRevision()); + $this->assertTrue($node->hasTranslation('de')); + $this->assertFalse($node->hasTranslation('fr')); + $this->assertTrue((bool) $node->isRevisionTranslationAffected()); + $this->assertEquals('Updated Source Node', $node->label()); + $this->assertTrue($node->get('composite_reference')->entity->isDefaultRevision()); + $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('de')); + $this->assertFalse($node->get('composite_reference')->entity->hasTranslation('fr')); + $this->assertEquals('Updated Source Composite', $node->get('composite_reference')->entity->label()); + $this->assertEquals('Initial Nested Source Composite', $node->get('composite_reference')->entity->get('composite_reference')->entity->label()); + $this->assertEquals('Draft Nested Source Composite #2', $node->get('composite_reference')->entity->get('composite_reference')[1]->entity->label()); + $this->assertEquals('Updated untranslatable field', $node->get('composite_reference')->entity->get('field_untranslatable')->value); + + $node_initial = $node_storage->loadRevision($initial_revision_id); + $this->assertFalse($node_initial->isDefaultRevision()); + $this->assertFalse($node_initial->hasTranslation('de')); + $this->assertFalse($node_initial->hasTranslation('fr')); + $this->assertEquals('Initial Source Node', $node_initial->label()); + $this->assertFalse($node_initial->get('composite_reference')->entity->isDefaultRevision()); + $this->assertFalse($node_initial->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals('Initial Source Composite', $node_initial->get('composite_reference')->entity->label()); + $this->assertEquals('Initial Nested Source Composite', $node_initial->get('composite_reference')->entity->get('composite_reference')->entity->label()); + $this->assertEquals('Initial untranslatable field', $node_initial->get('composite_reference')->entity->get('field_untranslatable')->value); + $this->assertCount(1, $node_initial->get('composite_reference')->entity->get('composite_reference')); + + // The current node_fr pending revision still has the initial value before + // "merging" it, but it will get the new value for the untranslatable field + // in the new revision. + $node_fr = $node_storage->loadRevision($node_fr->getRevisionId()); + $this->assertEquals('Initial untranslatable field', $node_fr->get('composite_reference')->entity->get('field_untranslatable')->value); + $this->assertCount(1, $node_fr->get('composite_reference')->entity->get('composite_reference')); + + // Now publish the FR pending revision and also add a translation for + // the second composite that it now has. + $new_revision = $node_storage->createRevision($node_fr->getTranslation('fr')); + $this->assertCount(2, $new_revision->get('composite_reference')->entity->get('composite_reference')); + $new_revision->get('composite_reference')->entity->get('composite_reference')[1]->entity->getTranslation('fr')->set('name', 'FR Nested Composite #2'); + + $violations = $new_revision->validate(); + $this->assertEquals(0, count($violations)); + $new_revision->save(); + + $this->assertRevisionCount(7, $node); + $this->assertRevisionCount(7, $composite); + $this->assertRevisionCount(7, $nested_composite); + $this->assertRevisionCount(3, $second_nested_composite); + + // The new default revision should now have the updated english source, + // original german translation and the french pending revision. + $node = $node_storage->load($node->id()); + $this->assertTrue($node->isDefaultRevision()); + $this->assertTrue($node->hasTranslation('de')); + $this->assertTrue($node->hasTranslation('fr')); + $this->assertFalse((bool) $node->isRevisionTranslationAffected()); + $this->assertTrue((bool) $node->getTranslation('fr')->isRevisionTranslationAffected()); + $this->assertEquals('Updated Source Node', $node->label()); + $this->assertTrue($node->get('composite_reference')->entity->isDefaultRevision()); + $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('de')); + $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('fr')); + $this->assertEquals('Pending Revision Node #1 FR', $node->getTranslation('fr')->label()); + $this->assertEquals('Pending Revision Composite #1 FR', $node->get('composite_reference')->entity->getTranslation('fr')->label()); + $this->assertEquals('Pending Nested Composite #1 FR', $node->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('fr')->label()); + $this->assertEquals('New Node #1 DE', $node->getTranslation('de')->label()); + $this->assertEquals('New Composite #1 DE', $node->get('composite_reference')->entity->getTranslation('de')->label()); + $this->assertEquals('New Nested Composite #1 DE', $node->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('de')->label()); + $this->assertEquals('Updated Source Composite', $node->get('composite_reference')->entity->label()); + $this->assertEquals('Updated untranslatable field', $node->get('composite_reference')->entity->get('field_untranslatable')->value); + $this->assertEquals('Draft Nested Source Composite #2', $node->get('composite_reference')->entity->get('composite_reference')[1]->entity->label()); + $this->assertEquals('FR Nested Composite #2', $node->get('composite_reference')->entity->get('composite_reference')[1]->entity->getTranslation('fr')->label()); + + // Now publish the DE pending revision as well. + $new_revision = $node_storage->createRevision($node_de->getTranslation('de')); + $violations = $new_revision->validate(); + $this->assertCount(2, $new_revision->get('composite_reference')->entity->get('composite_reference')); + $this->assertEquals(0, count($violations)); + $new_revision->save(); + + $this->assertRevisionCount(8, $node); + $this->assertRevisionCount(8, $composite); + $this->assertRevisionCount(8, $nested_composite); + $this->assertRevisionCount(4, $second_nested_composite); + + // The new default revision should now have the updated source and both + // translations. + $node = $node_storage->load($node->id()); + $this->assertTrue($node->isDefaultRevision()); + $this->assertTrue($node->hasTranslation('de')); + $this->assertTrue($node->hasTranslation('fr')); + $this->assertFalse((bool) $node->isRevisionTranslationAffected()); + $this->assertFalse((bool) $node->getTranslation('fr')->isRevisionTranslationAffected()); + $this->assertTrue((bool) $node->getTranslation('de')->isRevisionTranslationAffected()); + $this->assertEquals('Updated Source Node', $node->label()); + $this->assertTrue($node->get('composite_reference')->entity->isDefaultRevision()); + $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('de')); + $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('fr')); + $this->assertEquals('Pending Revision Node #1 FR', $node->getTranslation('fr')->label()); + $this->assertEquals('Pending Revision Composite #1 FR', $node->get('composite_reference')->entity->getTranslation('fr')->label()); + $this->assertEquals('Pending Revision Node #1 DE', $node->getTranslation('de')->label()); + $this->assertEquals('Pending Revision Composite #1 DE', $node->get('composite_reference')->entity->getTranslation('de')->label()); + $this->assertEquals('Pending Nested Composite #1 DE', $node->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('de')->label()); + $this->assertEquals('Updated Source Composite', $node->get('composite_reference')->entity->label()); + $this->assertEquals('Updated untranslatable field', $node->get('composite_reference')->entity->get('field_untranslatable')->value); + $this->assertEquals('Draft Nested Source Composite #2', $node->get('composite_reference')->entity->get('composite_reference')[1]->entity->label()); + $this->assertEquals('FR Nested Composite #2', $node->get('composite_reference')->entity->get('composite_reference')[1]->entity->getTranslation('fr')->label()); + + // The second nested composite of DE inherited the default values for its + // translation. + $this->assertEquals('Draft Nested Source Composite #2', $node->get('composite_reference')->entity->get('composite_reference')[1]->entity->getTranslation('de')->label()); + + // Simulate creating a new pending revision like + // \Drupal\content_moderation\EntityTypeInfo::entityPrepareForm(). + $new_revision = $node_storage->createRevision($node); + $revision_key = $new_revision->getEntityType()->getKey('revision'); + $new_revision->set($revision_key, $new_revision->getLoadedRevisionId()); + $new_revision->save(); + $this->assertEquals('Pending Nested Composite #1 DE', $new_revision->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('de')->label()); + + } + + /** + * Tests that composite translations affects the host entity's translations. + */ + public function testCompositeTranslation() { + /** @var \Drupal\node\NodeStorageInterface $node_storage */ + $node_storage = $this->entityTypeManager->getStorage('node'); + + // Create a composite entity. + $composite = EntityTestCompositeRelationship::create([ + 'langcode' => 'en', + 'name' => 'Initial Source Composite', + ]); + $composite->save(); + + // Create a node with a reference to the test composite entity. + $node = Node::create([ + 'langcode' => 'en', + 'title' => 'Initial Source Node', + 'type' => 'article', + 'composite_reference' => $composite, + ]); + $node->save(); + + /** @var \Drupal\node\NodeInterface $node */ + $node = $node_storage->load($node->id()); + + // Assert that there is only 1 revision when creating a node. + $this->assertRevisionCount(1, $node); + // Assert that there is only 1 affected revision when creating a node. + $this->assertAffectedRevisionCount(1, $node); + // Assert there is no new composite revision after creating a host entity. + $this->assertRevisionCount(1, $composite); + + $node_de = $node->addTranslation('de', ['title' => 'New Node #1 DE'] + $node->toArray()); + $node_de = $node_storage->createRevision($node_de, FALSE); + + $node_de->get('composite_reference')->entity->getTranslation('de')->set('name', 'New Composite #1 DE'); + $node_de->isDefaultRevision(TRUE); + $violations = $node_de->validate(); + foreach ($violations as $violation) { + $this->fail($violation->getPropertyPath() . ': ' . $violation->getMessage()); + } + $this->assertEquals(0, count($violations)); + $node_de->save(); + $this->assertAffectedRevisionCount(1, $node_de); + $this->assertAffectedRevisionCount(1, $node); + + // Test that changing composite non default language (DE) reference results + // in translation changes for this language but not for the default + // language. + $node_de->get('composite_reference')->entity->getTranslation('de')->set('name', 'Change Composite #1 DE'); + $node_de->setNewRevision(); + $node_de->save(); + + $this->assertEquals('Change Composite #1 DE', $node_de->get('composite_reference')->entity->getTranslation('de')->getName()); + + // Make sure the node DE has one more affected translation revision. + $this->assertAffectedRevisionCount(2, $node_de); + // Make sure the node EN has only one 1 affected translation revision. + $this->assertAffectedRevisionCount(1, $node); + + // Test that changing composite in default language (EN) results in + // translation changes for this language but not for the DE language. + $node = $node_storage->load($node->id()); + $node->get('composite_reference')->entity->set('name', 'Update Source #1'); + $node->setNewRevision(); + $node->save(); + + $this->assertEquals('Update Source #1', $node->get('composite_reference')->entity->getTranslation('en')->getName()); + + // The node EN now has 2 affected translation revision. + $this->assertAffectedRevisionCount(2, $node); + // The node DE still has 2 affected translation revisions. + $this->assertAffectedRevisionCount(2, $node_de); + } + + /** + * Tests that nested composite translations affects the host translations. + */ + public function testNestedCompositeTranslation() { + /** @var \Drupal\node\NodeStorageInterface $node_storage */ + $node_storage = \Drupal::entityTypeManager()->getStorage('node'); + + // Create a nested composite entity. + $nested_composite = EntityTestCompositeRelationship::create([ + 'langcode' => 'en', + 'name' => 'Initial Nested Source Composite', + ]); + $nested_composite->addTranslation('de', ['name' => 'Nested Source Composite DE'] + $nested_composite->toArray()); + $nested_composite->save(); + + // Create a composite entity. + $composite = EntityTestCompositeRelationship::create([ + 'langcode' => 'en', + 'name' => 'Initial Source Composite', + 'field_untranslatable' => 'Initial untranslatable field', + 'composite_reference' => $nested_composite, + ]); + $composite->addTranslation('de', ['name' => 'Source Composite DE'] + $composite->toArray()); + $composite->save(); + + // Create a node with a reference to the test composite entity. + $node = Node::create([ + 'langcode' => 'en', + 'title' => 'Initial Source Node', + 'type' => 'article', + 'composite_reference' => $composite, + ]); + $node->save(); + /** @var \Drupal\node\NodeInterface $node */ + $node = $node_storage->load($node->id()); + + // Assert that there is only 1 revision when creating a node. + $this->assertRevisionCount(1, $node); + // Assert that there is only 1 affected revision when creating a node. + $this->assertAffectedRevisionCount(1, $node); + // Assert there is no new composite revision after creating a host entity. + $this->assertRevisionCount(1, $composite); + // Assert there is no new nested composite revision after creating a host + // entity. + $this->assertRevisionCount(1, $nested_composite); + + $node_de = $node->addTranslation('de', ['title' => 'New Node #1 DE'] + $node->toArray()); + $node_de = $node_storage->createRevision($node_de, FALSE); + + $node_de->get('composite_reference')->entity->getTranslation('de')->get('composite_reference')->entity->getTranslation('de')->set('name', 'New Nested Composite #1 DE'); + $node_de->isDefaultRevision(TRUE); + $node_de->save(); + $this->assertAffectedRevisionCount(1, $node_de); + $this->assertAffectedRevisionCount(1, $node); + + // Test that changing nested composite non default language (DE) reference + // results in translation changes for this language but not for the default + // language. + $node_de->get('composite_reference')->entity->getTranslation('de')->get('composite_reference')->entity->getTranslation('de')->set('name', 'Change Nested Composite #1 DE'); + $node_de->setNewRevision(); + $node_de->save(); + + $this->assertEquals('Change Nested Composite #1 DE', $node_de->get('composite_reference')->entity->getTranslation('de')->get('composite_reference')->entity->getTranslation('de')->getName()); + + // Make sure the node DE has one more affected translation revision. + $this->assertAffectedRevisionCount(2, $node_de); + // Make sure the node EN has only one 1 affected translation revision. + $this->assertAffectedRevisionCount(1, $node); + + // Test that changing nested composite in default language (EN) results in + // translation changes for this language but not for the DE language. + $node = $node_storage->load($node->id()); + $node->get('composite_reference')->entity->get('composite_reference')->entity->set('name', 'Update Nested Source #1'); + $node->setNewRevision(); + $node->save(); + + $this->assertEquals('Update Nested Source #1', $node->get('composite_reference')->entity->getTranslation('en')->get('composite_reference')->entity->getTranslation('en')->getName()); + + // The node EN now has 2 affected translation revision. + $this->assertAffectedRevisionCount(2, $node); + // The node DE still has 2 affected translation revisions. + $this->assertAffectedRevisionCount(2, $node_de); + } + + /** + * Asserts the affected revision count of a certain entity. + * + * @param int $expected + * The expected count. + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity. + */ + protected function assertAffectedRevisionCount($expected, EntityInterface $entity) { + $entity_type = $entity->getEntityType(); + $affected_revisions_count = $this->entityTypeManager->getStorage($entity_type->id()) + ->getQuery() + ->condition($entity_type->getKey('id'), $entity->id()) + ->condition($entity_type->getKey('langcode'), $entity->language()->getId()) + ->condition($entity_type->getKey('revision_translation_affected'), 1) + ->allRevisions() + ->count() + ->execute(); + + $this->assertEquals($expected, $affected_revisions_count); + } + + /** + * Asserts the revision count of an entity. + * + * @param int $expected + * The expected amount of revisions. + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity. + */ + protected function assertRevisionCount($expected, EntityInterface $entity) { + $node_revisions_count = \Drupal::entityQuery($entity->getEntityTypeId()) + ->condition($entity->getEntityType()->getKey('id'), $entity->id()) + ->allRevisions() + ->count() + ->execute(); + $this->assertEquals($expected, $node_revisions_count); + } + +} diff --git a/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsFormatterTest.php b/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsFormatterTest.php index 430d06e6209eea49a6acda88403ae28bda24208f..f65e0745d8339bd2f355cdfd95591d95256cad86 100644 --- a/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsFormatterTest.php +++ b/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsFormatterTest.php @@ -8,7 +8,7 @@ use Drupal\KernelTests\KernelTestBase; use Drupal\node\Entity\Node; use Drupal\node\Entity\NodeType; -use Drupal\simpletest\UserCreationTrait; +use Drupal\Tests\user\Traits\UserCreationTrait; /** * @coversDefaultClass \Drupal\entity_reference_revisions\Plugin\Field\FieldFormatter\EntityReferenceRevisionsEntityFormatter diff --git a/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsSaveTest.php b/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsSaveTest.php index e73a2dddc1d1eff63a71a2510ce927d177fc09b7..2b66a63058d9197152193f37a3226d8dc107ff80 100644 --- a/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsSaveTest.php +++ b/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsSaveTest.php @@ -88,7 +88,8 @@ public function testNeedsSave() { 'type' => 'article', 'composite_reference' => $entity_test, ]); - // Check the name is properly set. + // Check the name is properly set and that getValue() returns the entity + // when it is marked as needs save." $values = $node->composite_reference->getValue(); $this->assertTrue(isset($values[0]['entity'])); static::assertEquals($values[0]['entity']->name->value, $text); @@ -102,20 +103,22 @@ public function testNeedsSave() { static::assertEquals($entity_test_after->name->value, $text); $new_text = 'Dummy text again'; - // Set the name again. - $entity_test->name = $new_text; - $entity_test->setNeedsSave(FALSE); + // Set another name and save the node without marking it as needs saving. + $entity_test_after->name = $new_text; + $entity_test_after->setNeedsSave(FALSE); - // Load the Node and check the composite reference field is not set. + // Load the Node and check the composite reference entity is not returned + // from getValue() if it is not marked as needs saving. $node = Node::load($node->id()); $values = $node->composite_reference->getValue(); $this->assertFalse(isset($values[0]['entity'])); - $node->composite_reference = $entity_test; + $node->composite_reference = $entity_test_after; $node->save(); // Check the name is not updated. + \Drupal::entityTypeManager()->getStorage('entity_test_composite')->resetCache(); $entity_test_after = EntityTestCompositeRelationship::load($entity_test->id()); - static::assertEquals($entity_test_after->name->value, $text); + static::assertEquals($text, $entity_test_after->name->value); // Test if after delete the referenced entity there are no problems setting // the referencing values to the parent. @@ -263,4 +266,56 @@ public function testEntityReferenceRevisionsDefaultValue() { $this->assertEquals($dependencies['config'][1], 'node.type.article'); $this->assertEquals($dependencies['module'][0], 'entity_reference_revisions'); } + + /** + * Tests FieldType\EntityReferenceRevisionsItem::deleteRevision + */ + public function testEntityReferenceRevisionsDeleteHandleDeletedChild() { + $field_storage = FieldStorageConfig::create([ + 'field_name' => 'field_reference', + 'entity_type' => 'node', + 'type' => 'entity_reference_revisions', + 'settings' => [ + 'target_type' => 'node', + ], + ]); + $field_storage->save(); + $field = FieldConfig::create([ + 'field_storage' => $field_storage, + 'bundle' => 'article', + ]); + $field->save(); + + $child = Node::create([ + 'type' => 'article', + 'title' => 'Child node', + ]); + $child->save(); + + $node = Node::create([ + 'type' => 'article', + 'title' => 'Parent node', + 'field_reference' => [ + [ + 'target_id' => $child->id(), + 'target_revision_id' => $child->getRevisionId(), + ] + ], + ]); + + // Create two revisions. + $node->save(); + $revisionId = $node->getRevisionId(); + $node->setNewRevision(TRUE); + $node->save(); + + // Force delete the child Paragraph. + // Core APIs allow this although it is an inconsistent storage situation + // for Paragraphs. + $child->delete(); + + // Previously deleting a revision with a lost child failed fatal. + \Drupal::entityTypeManager()->getStorage('node')->deleteRevision($revisionId); + } + } diff --git a/web/modules/entity_reference_revisions/tests/src/Kernel/Plugin/Derivative/EntityReferenceRevisionsDeriverTest.php b/web/modules/entity_reference_revisions/tests/src/Kernel/Plugin/Derivative/EntityReferenceRevisionsDeriverTest.php index e68cf058caeb1693c85d9c0e83f65f784c0d2557..86c49a1ffb431f3cef56023cbf6799002124f112 100644 --- a/web/modules/entity_reference_revisions/tests/src/Kernel/Plugin/Derivative/EntityReferenceRevisionsDeriverTest.php +++ b/web/modules/entity_reference_revisions/tests/src/Kernel/Plugin/Derivative/EntityReferenceRevisionsDeriverTest.php @@ -4,7 +4,6 @@ use Drupal\entity_reference_revisions\Plugin\migrate\destination\EntityReferenceRevisions; use Drupal\KernelTests\KernelTestBase; -use Drupal\migrate\Plugin\MigrationPluginManager; use Drupal\migrate\Plugin\MigrateDestinationPluginManager; /** @@ -25,7 +24,7 @@ class EntityReferenceRevisionsDeriverTest extends KernelTestBase { */ protected function setUp() { parent::setUp(); - $this->installConfig($this->modules); + $this->installConfig(static::$modules); } /** diff --git a/web/modules/entity_reference_revisions/tests/src/Kernel/Plugin/migrate/destination/EntityReferenceRevisionsDestinationTest.php b/web/modules/entity_reference_revisions/tests/src/Kernel/Plugin/migrate/destination/EntityReferenceRevisionsDestinationTest.php index 02e02ec58760a932de36022c457a54506822d0d3..05f781f544756c6ac30373bc1972294fe0a34be4 100644 --- a/web/modules/entity_reference_revisions/tests/src/Kernel/Plugin/migrate/destination/EntityReferenceRevisionsDestinationTest.php +++ b/web/modules/entity_reference_revisions/tests/src/Kernel/Plugin/migrate/destination/EntityReferenceRevisionsDestinationTest.php @@ -2,8 +2,6 @@ namespace Drupal\Tests\entity_reference_revisions\Kernel\Plugin\migrate\destination; -use Drupal\Core\Entity\EntityStorageBase; -use Drupal\entity_reference_revisions\Plugin\migrate\destination\EntityReferenceRevisions; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\KernelTests\KernelTestBase; @@ -21,9 +19,9 @@ class EntityReferenceRevisionsDestinationTest extends KernelTestBase implements MigrateMessageInterface { /** - * @var \Drupal\migrate\Plugin\MigrationPluginManager $migrationManager - * * The migration plugin manager. + * + * @var \Drupal\migrate\Plugin\MigrationPluginManager */ protected $migrationPluginManager; @@ -46,7 +44,7 @@ protected function setUp() { parent::setUp(); $this->installEntitySchema('entity_test_composite'); $this->installSchema('system', ['sequences']); - $this->installConfig($this->modules); + $this->installConfig(static::$modules); $this->migrationPluginManager = \Drupal::service('plugin.manager.migration'); } @@ -59,12 +57,12 @@ protected function setUp() { * @covers ::getEntityTypeId */ public function testGetEntityTypeId(array $definition, $expected) { - /** @var Migration $migration */ + /** @var \Drupal\migrate\Plugin\Migration $migration */ $migration = $this->migrationPluginManager->createStubMigration($definition); - /** @var EntityReferenceRevisions $destination */ + /** @var \Drupal\entity_reference_revisions\Plugin\migrate\destination\EntityReferenceRevisions $destination */ $destination = $migration->getDestinationPlugin(); - /** @var EntityStorageBase $storage */ + /** @var \Drupal\Core\Entity\EntityStorageBase $storage */ $storage = $this->readAttribute($destination, 'storage'); $actual = $this->readAttribute($storage, 'entityTypeId'); @@ -75,13 +73,13 @@ public function testGetEntityTypeId(array $definition, $expected) { * Provides multiple migration definitions for "getEntityTypeId" test. */ public function getEntityTypeIdDataProvider() { - $datas = $this->getEntityDataProvider(); + $data = $this->getEntityDataProvider(); - foreach ($datas as &$data) { - $data['expected'] = 'entity_test_composite'; + foreach ($data as &$datum) { + $datum['expected'] = 'entity_test_composite'; } - return $datas; + return $data; } /** @@ -94,18 +92,19 @@ public function getEntityTypeIdDataProvider() { * @covers ::rollbackNonTranslation */ public function testGetEntity(array $definition, array $expected) { - /** @var Migration $migration */ + /** @var \Drupal\migrate\Plugin\Migration $migration */ $migration = $this->migrationPluginManager->createStubMigration($definition); $migrationExecutable = (new MigrateExecutable($migration, $this)); - /** @var EntityStorageBase $storage */ + /** @var \Drupal\Core\Entity\EntityStorageBase $storage */ $storage = $this->readAttribute($migration->getDestinationPlugin(), 'storage'); // Test inserting and updating by looping twice. for ($i = 0; $i < 2; $i++) { $migrationExecutable->import(); $migration->getIdMap()->prepareUpdate(); foreach ($expected as $data) { - $entity = $storage->loadRevision($data['id']); + $entity = $storage->loadRevision($data['revision_id']); $this->assertEquals($data['label'], $entity->label()); + $this->assertEquals($data['id'], $entity->id()); } } $migrationExecutable->rollback(); @@ -142,12 +141,70 @@ public function getEntityDataProvider() { ], ], 'expected' => [ - ['id' => 1, 'label' => 'content item 1a'], - ['id' => 2, 'label' => 'content item 1b'], - ['id' => 3, 'label' => 'content item 2'], + ['id' => 1, 'revision_id' => 1, 'label' => 'content item 1a'], + ['id' => 2, 'revision_id' => 2, 'label' => 'content item 1b'], + ['id' => 3, 'revision_id' => 3, 'label' => 'content item 2'], + ], + ], + 'with ids' => [ + 'definition' => [ + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => [ + ['id' => 1, 'name' => 'content item 1a'], + ['id' => 1, 'name' => 'content item 1b'], + ['id' => 2, 'name' => 'content item 2'], + ['id' => 3, 'name' => 'content item 3'], + ], + 'ids' => [ + 'id' => ['type' => 'integer'], + 'name' => ['type' => 'text'], + ], + ], + 'process' => [ + 'name' => 'name', + 'id' => 'id', + ], + 'destination' => [ + 'plugin' => 'entity_reference_revisions:entity_test_composite', + ], + ], + 'expected' => [ + ['id' => 1, 'revision_id' => 1, 'label' => 'content item 1b'], + ['id' => 2, 'revision_id' => 2, 'label' => 'content item 2'], + ['id' => 3, 'revision_id' => 3, 'label' => 'content item 3'], ], ], - 'with keys' => [ + 'with ids and new revisions' => [ + 'definition' => [ + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => [ + ['id' => 1, 'name' => 'content item 1a'], + ['id' => 1, 'name' => 'content item 1b'], + ['id' => 2, 'name' => 'content item 2'], + ], + 'ids' => [ + 'id' => ['type' => 'integer'], + 'name' => ['type' => 'text'], + ], + ], + 'process' => [ + 'name' => 'name', + 'id' => 'id', + ], + 'destination' => [ + 'plugin' => 'entity_reference_revisions:entity_test_composite', + 'new_revisions' => TRUE, + ], + ], + 'expected' => [ + ['id' => 1, 'revision_id' => 1, 'label' => 'content item 1a'], + ['id' => 1, 'revision_id' => 2, 'label' => 'content item 1b'], + ['id' => 2, 'revision_id' => 3, 'label' => 'content item 2'], + ], + ], + 'with ids and revisions' => [ 'definition' => [ 'source' => [ 'plugin' => 'embedded_data', @@ -171,9 +228,9 @@ public function getEntityDataProvider() { ], ], 'expected' => [ - ['id' => 1, 'label' => 'content item 1'], - ['id' => 2, 'label' => 'content item 2'], - ['id' => 3, 'label' => 'content item 3'], + ['id' => 1, 'revision_id' => 1, 'label' => 'content item 1'], + ['id' => 2, 'revision_id' => 2, 'label' => 'content item 2'], + ['id' => 3, 'revision_id' => 3, 'label' => 'content item 3'], ], ], ]; @@ -183,9 +240,8 @@ public function getEntityDataProvider() { * Tests multi-value and single-value destination field linkage. * * @dataProvider destinationFieldMappingDataProvider - * */ - public function testDestinationFieldMapping(array $datas) { + public function testDestinationFieldMapping(array $data) { $this->enableModules(['node', 'field']); $this->installEntitySchema('node'); $this->installEntitySchema('user'); @@ -202,7 +258,7 @@ public function testDestinationFieldMapping(array $datas) { 'entity_type' => 'node', 'type' => 'entity_reference_revisions', 'settings' => [ - 'target_type' => 'entity_test_composite' + 'target_type' => 'entity_test_composite', ], 'cardinality' => 1, ]); @@ -219,7 +275,7 @@ public function testDestinationFieldMapping(array $datas) { 'entity_type' => 'node', 'type' => 'entity_reference_revisions', 'settings' => [ - 'target_type' => 'entity_test_composite' + 'target_type' => 'entity_test_composite', ], 'cardinality' => -1, ]); @@ -232,9 +288,9 @@ public function testDestinationFieldMapping(array $datas) { $definitions = []; $instances = []; - foreach ($datas as $data) { - $definitions[$data['definition']['id']] = $data['definition']; - $instances[$data['definition']['id']] = $this->migrationPluginManager->createStubMigration($data['definition']); + foreach ($data as $datum) { + $definitions[$datum['definition']['id']] = $datum['definition']; + $instances[$datum['definition']['id']] = $this->migrationPluginManager->createStubMigration($datum['definition']); } // Reflection is easier than mocking. We need to use createInstance for @@ -245,13 +301,13 @@ public function testDestinationFieldMapping(array $datas) { $property->setValue($this->migrationPluginManager, $definitions); $this->container->set('plugin.manager.migration', $this->migrationPluginManager); - foreach ($datas as $data) { - $migration = $this->migrationPluginManager->createInstance($data['definition']['id']); + foreach ($data as $datum) { + $migration = $this->migrationPluginManager->createInstance($datum['definition']['id']); $migrationExecutable = (new MigrateExecutable($migration, $this)); - /** @var EntityStorageBase $storage */ + /** @var \Drupal\Core\Entity\EntityStorageBase $storage */ $storage = $this->readAttribute($migration->getDestinationPlugin(), 'storage'); $migrationExecutable->import(); - foreach ($data['expected'] as $expected) { + foreach ($datum['expected'] as $expected) { $entity = $storage->loadRevision($expected['id']); $properties = array_diff_key($expected, array_flip(['id'])); foreach ($properties as $property => $value) { @@ -262,7 +318,7 @@ public function testDestinationFieldMapping(array $datas) { } } else { - $this->assertNotEmpty($entity, 'Entity with label ' . $expected[$property] .' is empty'); + $this->assertNotEmpty($entity, 'Entity with label ' . $expected[$property] . ' is empty'); $this->assertEquals($expected[$property], $entity->label()); } } @@ -405,7 +461,7 @@ public function destinationFieldMappingDataProvider() { ], 'field_err_single/target_id' => [ [ - 'plugin' => 'migration', + 'plugin' => 'migration_lookup', 'migration' => ['single_err'], 'no_stub' => TRUE, 'source' => 'id', @@ -419,7 +475,7 @@ public function destinationFieldMappingDataProvider() { ], 'field_err_single/target_revision_id' => [ [ - 'plugin' => 'migration', + 'plugin' => 'migration_lookup', 'migration' => ['single_err'], 'no_stub' => TRUE, 'source' => 'id', @@ -433,7 +489,7 @@ public function destinationFieldMappingDataProvider() { ], 'field_err_multiple' => [ [ - 'plugin' => 'migration', + 'plugin' => 'migration_lookup', 'migration' => [ 'multiple_err_author1', 'multiple_err_author2', @@ -442,7 +498,7 @@ public function destinationFieldMappingDataProvider() { 'source' => 'author', ], [ - 'plugin' => 'iterator', + 'plugin' => 'sub_process', 'process' => [ 'target_id' => '0', 'target_revision_id' => '1', diff --git a/web/modules/honeypot/.travis.yml b/web/modules/honeypot/.travis.yml index 784af29cf4d690ed488f69b3bc20ed672c04ef01..843a0e32958d133a1185fb0fe81607b570c41a9c 100644 --- a/web/modules/honeypot/.travis.yml +++ b/web/modules/honeypot/.travis.yml @@ -1,20 +1,13 @@ --- language: php -php: '7.1' +php: '7.2' services: docker env: - DOCKER_COMPOSE_VERSION: 1.13.0 + DOCKER_COMPOSE_VERSION: 1.23.2 before_install: - # List available docker-engine versions. - - apt-cache madison docker-engine - - # Upgrade docker. - - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - - - sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" - - sudo apt-get update - - sudo apt-get -y install docker-ce + - sudo service mysql stop # Upgrade docker-compose. - sudo rm /usr/local/bin/docker-compose @@ -22,19 +15,33 @@ before_install: - chmod +x docker-compose - sudo mv docker-compose /usr/local/bin - # Pull container. - - docker pull geerlingguy/drupal-vm:latest - -script: - # Build environment and install Honeypot. +install: + # Build environment. - docker-compose up -d - - docker exec honeypot install-drupal - - docker exec honeypot ln -s /opt/honeypot/ /var/www/drupalvm/drupal/web/modules/honeypot - - docker exec honeypot bash -c 'cd /var/www/drupalvm/drupal/web; drush en -y honeypot simpletest' - # Fix permissions on the simpletest directories. - - docker exec honeypot chown -R www-data:www-data /var/www/drupalvm/drupal/web/sites/simpletest - - docker exec honeypot chown -R www-data:www-data /var/www/drupalvm/drupal/web/sites/default/files + # Wait for composer create-project to complete. + - sleep 300 + + # Structure the codebase and install necessary dependencies. + - docker-compose exec drupal bash -c 'apt-get update && apt-get install -y sudo' + - docker-compose exec drupal bash -c 'composer config platform --unset' + - docker-compose exec drupal bash -c 'composer require --dev drush/drush' + - docker-compose exec drupal bash -c 'composer update phpunit/phpunit symfony/phpunit-bridge phpspec/prophecy symfony/yaml --with-dependencies' + - docker-compose exec drupal ln -s /opt/honeypot/ /var/www/html/web/modules/honeypot + # Install Drupal and Honeypot/Testing. + - docker-compose exec drupal bash -c 'sudo -u www-data vendor/bin/drush site:install standard --site-name="Honeypot Test" --account-pass admin -y' + - docker-compose exec drupal bash -c 'vendor/bin/drush en -y honeypot simpletest' + +before_script: + # Adjust permissions on the simpletest directories. + - docker exec honeypot mkdir -p /var/www/html/web/sites/simpletest + - docker exec honeypot chown -R www-data:www-data /var/www/html/web/sites/simpletest + +script: # Run module tests. - - docker exec honeypot bash -c 'sudo -u www-data php /var/www/drupalvm/drupal/web/core/scripts/run-tests.sh --verbose --module honeypot --url http://local.drupalhoneypot.com/' + - docker-compose exec drupal bash -c 'sudo -u www-data php web/core/scripts/run-tests.sh --module honeypot --url http://localhost/' + +after_failure: + # Re-run tests with verbose output for debugging. + - docker-compose exec drupal bash -c 'sudo -u www-data php web/core/scripts/run-tests.sh --verbose --module honeypot --url http://localhost/' diff --git a/web/modules/honeypot/README.md b/web/modules/honeypot/README.md index 7930749f96f423f6f03f8a0bdd463c400835b5b3..c096502ad351f2e5e2a758c8cf07942c50710fe6 100644 --- a/web/modules/honeypot/README.md +++ b/web/modules/honeypot/README.md @@ -43,16 +43,29 @@ restriction on the form by including or not including the option in the array. Honeypot includes a `docker-compose.yml` file that can be used for testing purposes. To build a Drupal 8 environment for local testing, do the following: 1. Make sure you have Docker for Mac (or for whatever OS you're using) installed. - 2. Add the following entry to your `/etc/hosts` file: `192.168.22.33 local.drupalhoneypot.com` - 3. Run `docker-compose up -d` in this directory. - 4. Install Drupal: `docker exec honeypot install-drupal` (optionally provide a version after `install-drupal`). - 5. Link the honeypot module directory into the Drupal modules directory: `docker exec honeypot ln -s /opt/honeypot/ /var/www/drupalvm/drupal/web/modules/honeypot` - 6. Visit `http://local.drupalhoneypot.com/user` and log in using the admin credentials Drush displayed. + 1. Run the following commands in this directory to start the environment and install Drush: -> Note: If you're using a Mac, you may also need to perform additional steps to get the hostname working; see [Managing your hosts file](http://docs.drupalvm.com/en/latest/other/docker/#managing-your-hosts-file) in the Drupal VM documentation. + ``` + docker-compose up -d + # Wait a couple minutes for the container to build the Drupal codebase. + docker-compose exec drupal bash -c 'composer require drush/drush' + ``` + 1. Link the honeypot module directory into the Drupal modules directory: + + ``` + docker-compose exec drupal ln -s /opt/honeypot/ /var/www/html/web/modules/honeypot + ``` + + 1. Install Drupal with Drush: + + ``` + docker-compose exec drupal bash -c 'vendor/bin/drush site:install standard --site-name="Honeypot Test" --account-pass admin -y && chown -R www-data:www-data web/sites/default/files' + ``` + + 1. Log into `http://localhost/` with `admin`/`admin` and enable Honeypot (and the Testing module, if desired). ## Credit -The Honeypot module was originally developed by Jeff Geerling of Midwestern Mac, -LLC (midwesternmac.com), and sponsored by Flocknote (flocknote.com). +The Honeypot module was originally developed by Jeff Geerling of [Midwestern Mac, +LLC](https://www.midwesternmac.com/), and sponsored by [Flocknote](https://flocknote.com). diff --git a/web/modules/honeypot/docker-compose.yml b/web/modules/honeypot/docker-compose.yml index 1104254154475189bf0ccaca42e6654175067d93..1b07b5c0366df106c0a6870c7e378c6dfbf6cef6 100644 --- a/web/modules/honeypot/docker-compose.yml +++ b/web/modules/honeypot/docker-compose.yml @@ -1,34 +1,36 @@ version: "3" services: - - honeypot: - image: geerlingguy/drupal-vm + drupal: + image: geerlingguy/drupal container_name: honeypot + environment: + DRUPAL_DATABASE_HOST: 'mysql' + DRUPAL_DATABASE_PORT: '3306' + DRUPAL_DATABASE_NAME: 'drupal' + DRUPAL_DATABASE_USERNAME: 'drupal' + DRUPAL_DATABASE_PASSWORD: 'drupal' + DRUPAL_HASH_SALT: 'fe918c992fb1bcfa01f32303c8b21f3d0a0' + DRUPAL_DOWNLOAD_IF_NOT_PRESENT: 'true' + DRUPAL_DOWNLOAD_METHOD: 'composer' + DRUPAL_PROJECT_ROOT: /var/www/html + APACHE_DOCUMENT_ROOT: /var/www/html/web ports: - - 80:80 - - 443:443 - privileged: true - extra_hosts: - local.drupalhoneypot.com: 127.0.0.1 - dns: - - 8.8.8.8 - - 8.8.4.4 + - "80:80" + restart: always volumes: - # Switch to the commented line once Docker CE stable has the feature. - - ./:/opt/honeypot/:rw - # - ./:/opt/honeypot/:rw,delegated - command: /lib/systemd/systemd - networks: - honeypot: - ipv4_address: 192.168.22.33 - -networks: + - ./:/opt/honeypot/:rw,delegated - honeypot: - driver: bridge - driver_opts: - ip: 192.168.22.1 - ipam: - config: - - subnet: "192.168.22.0/16" + mysql: + image: mysql:5.7 + container_name: drupal-mysql + command: ['--max_allowed_packet=32505856'] + environment: + MYSQL_RANDOM_ROOT_PASSWORD: 'yes' + MYSQL_DATABASE: drupal + MYSQL_USER: drupal + MYSQL_PASSWORD: drupal + ports: + - "3306:3306" + volumes: + - /var/lib/mysql diff --git a/web/modules/honeypot/drupalci.yml b/web/modules/honeypot/drupalci.yml new file mode 100644 index 0000000000000000000000000000000000000000..9adfb33938e5b3d45c3f6c0dea51a36b19e00e84 --- /dev/null +++ b/web/modules/honeypot/drupalci.yml @@ -0,0 +1,25 @@ +# Learn to make one for your own drupal.org project: +# https://www.drupal.org/drupalorg/docs/drupal-ci/customizing-drupalci-testing +build: + assessment: + validate_codebase: + phplint: + container_composer: + phpcs: + # phpcs will use core's specified version of Coder. + sniff-all-files: true + halt-on-fail: false + testing: + # run_tests task is executed several times in order of performance speeds. + # halt-on-fail can be set on the run_tests tasks in order to fail fast. + # suppress-deprecations is false in order to be alerted to usages of + # deprecated code. + run_tests.standard: + types: 'Simpletest,PHPUnit-Unit,PHPUnit-Kernel,PHPUnit-Functional' + testgroups: '--all' + suppress-deprecations: false + run_tests.js: + types: 'PHPUnit-FunctionalJavascript' + testgroups: '--all' + suppress-deprecations: false + nightwatchjs: { } diff --git a/web/modules/honeypot/honeypot.info.yml b/web/modules/honeypot/honeypot.info.yml index e7df0fbe9f4711fa732ebd81acef46ad1a427107..177d30a6119df7d3c913476e2fab8a8f8a621b84 100644 --- a/web/modules/honeypot/honeypot.info.yml +++ b/web/modules/honeypot/honeypot.info.yml @@ -2,12 +2,11 @@ name: Honeypot type: module description: 'Mitigates spam form submissions using the honeypot method.' package: "Spam control" -# core: 8.x +core: 8.x configure: honeypot.config hidden: false -# Information added by Drupal.org packaging script on 2018-08-09 -version: '8.x-1.28' -core: '8.x' +# Information added by Drupal.org packaging script on 2019-12-13 +version: '8.x-1.30' project: 'honeypot' -datestamp: 1533849185 +datestamp: 1576274291 diff --git a/web/modules/honeypot/honeypot.install b/web/modules/honeypot/honeypot.install index a151b8bb40ea64142a617a9efb1f8214abe8ccfd..dab3049138b0e7037fa79611ede5d5fbae35a3e9 100644 --- a/web/modules/honeypot/honeypot.install +++ b/web/modules/honeypot/honeypot.install @@ -48,7 +48,7 @@ function honeypot_schema() { function honeypot_install() { if (PHP_SAPI !== 'cli') { $config_url = Url::fromUri('base://admin/config/content/honeypot'); - drupal_set_message(t( + \Drupal::messenger()->addMessage(t( 'Honeypot installed successfully. Please <a href=":url">configure Honeypot</a> to protect your forms from spam bots.', [':url' => $config_url->toString()] )); diff --git a/web/modules/honeypot/honeypot.module b/web/modules/honeypot/honeypot.module index 276b834a6628d9e1dc2d9593d9ea0f38b4051830..1da86a6609e083a1d0c0f743cb18c024db29eadc 100644 --- a/web/modules/honeypot/honeypot.module +++ b/web/modules/honeypot/honeypot.module @@ -140,6 +140,7 @@ function honeypot_add_form_protection(&$form, FormStateInterface $form_state, ar $honeypot_class = $honeypot_element . '-textfield'; $form[$honeypot_element] = [ '#theme_wrappers' => [ + 'form_element', 'container' => [ '#id' => NULL, '#attributes' => [ @@ -168,7 +169,7 @@ function honeypot_add_form_protection(&$form, FormStateInterface $form_state, ar $input = $form_state->getUserInput(); if (empty($input['honeypot_time'])) { $identifier = Crypt::randomBytesBase64(); - \Drupal::service('keyvalue.expirable')->get('honeypot_time_restriction')->set($identifier, time(), 3600*24); + \Drupal::service('keyvalue.expirable')->get('honeypot_time_restriction')->setWithExpire($identifier, time(), 3600*24); } else { $identifier = $input['honeypot_time']; @@ -206,7 +207,7 @@ function _honeypot_honeypot_validate($element, FormStateInterface $form_state) { $honeypot_value = $element['#value']; // Make sure it's empty. - if (!empty($honeypot_value)) { + if (!empty($honeypot_value) || $honeypot_value == '0') { _honeypot_log($form_state->getValue('form_id'), 'honeypot'); $form_state->setErrorByName('', t('There was a problem with your form submission. Please refresh the page and try again.')); } @@ -239,7 +240,7 @@ function _honeypot_time_restriction_validate($element, FormStateInterface $form_ if (!$honeypot_time || \Drupal::time()->getRequestTime() < ($honeypot_time + $time_limit)) { _honeypot_log($form_state->getValue('form_id'), 'honeypot_time'); $time_limit = honeypot_get_time_limit(); - \Drupal::service('keyvalue.expirable')->get('honeypot_time_restriction')->set($identifier, \Drupal::time()->getRequestTime(), 3600*24); + \Drupal::service('keyvalue.expirable')->get('honeypot_time_restriction')->setWithExpire($identifier, \Drupal::time()->getRequestTime(), 3600*24); $form_state->setErrorByName('', t('There was a problem with your form submission. Please wait @limit seconds and try again.', ['@limit' => $time_limit])); } } @@ -294,7 +295,7 @@ function honeypot_get_time_limit(array $form_values = []) { $number = $query->countQuery()->execute()->fetchField(); // Don't add more than 30 days' worth of extra time. - $honeypot_time_limit = (int) min($honeypot_time_limit + exp($number) - 1, 2592000); + $honeypot_time_limit = (int) min($honeypot_time_limit + exp($number) - 1, $expire_time); // TODO - Only accepts two args. $additions = \Drupal::moduleHandler()->invokeAll('honeypot_time_limit', [ $honeypot_time_limit, diff --git a/web/modules/honeypot/src/Controller/HoneypotSettingsController.php b/web/modules/honeypot/src/Controller/HoneypotSettingsController.php index 72e6a4b3761f4bc7e12c3e9c3137cba38ab49b2e..18794d5a2acd857c5d935aa01bddbea6e8e9866d 100644 --- a/web/modules/honeypot/src/Controller/HoneypotSettingsController.php +++ b/web/modules/honeypot/src/Controller/HoneypotSettingsController.php @@ -12,6 +12,7 @@ use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Entity\EntityTypeBundleInfoInterface; use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Messenger\MessengerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -47,6 +48,13 @@ class HoneypotSettingsController extends ConfigFormBase { */ protected $cache; + /** + * The Messenger service. + * + * @var \Drupal\Core\Messenger\MessengerInterface + */ + protected $messenger; + /** * Constructs a settings controller. * @@ -60,13 +68,16 @@ class HoneypotSettingsController extends ConfigFormBase { * The entity type bundle info service. * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend * The cache backend interface. + * @param \Drupal\Core\Messenger\MessengerInterface $messenger + * The messenger service. */ - public function __construct(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info, CacheBackendInterface $cache_backend) { + public function __construct(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info, CacheBackendInterface $cache_backend, MessengerInterface $messenger) { parent::__construct($config_factory); $this->moduleHandler = $module_handler; $this->entityTypeManager = $entity_type_manager; $this->entityTypeBundleInfo = $entity_type_bundle_info; $this->cache = $cache_backend; + $this->messenger = $messenger; } /** @@ -78,7 +89,8 @@ public static function create(ContainerInterface $container) { $container->get('module_handler'), $container->get('entity_type.manager'), $container->get('entity_type.bundle.info'), - $container->get('cache.default') + $container->get('cache.default'), + $container->get('messenger') ); } @@ -323,7 +335,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $this->cache->delete('honeypot_protected_forms'); // Tell the user the settings have been saved. - drupal_set_message($this->t('The configuration options have been saved.')); + $this->messenger->addMessage($this->t('The configuration options have been saved.')); } } diff --git a/web/modules/honeypot/tests/modules/honeypot_test/honeypot_test.info.yml b/web/modules/honeypot/tests/modules/honeypot_test/honeypot_test.info.yml index c2db19d16ef0bc84ce158a143bf637f4e0951e31..e4c0669576f4c3a2a6774c06bcfdfdbbdbe30c89 100644 --- a/web/modules/honeypot/tests/modules/honeypot_test/honeypot_test.info.yml +++ b/web/modules/honeypot/tests/modules/honeypot_test/honeypot_test.info.yml @@ -1,12 +1,11 @@ name: honeypot_test type: module description: Support module for Honeypot internal testing purposes. -# core: 8.x +core: 8.x package: Testing hidden: true -# Information added by Drupal.org packaging script on 2018-08-09 -version: '8.x-1.28' -core: '8.x' +# Information added by Drupal.org packaging script on 2019-12-13 +version: '8.x-1.30' project: 'honeypot' -datestamp: 1533849185 +datestamp: 1576274291 diff --git a/web/modules/honeypot/src/Tests/HoneypotAdminFormTest.php b/web/modules/honeypot/tests/src/Functional/HoneypotAdminFormTest.php similarity index 71% rename from web/modules/honeypot/src/Tests/HoneypotAdminFormTest.php rename to web/modules/honeypot/tests/src/Functional/HoneypotAdminFormTest.php index 1347d46eff09cafa945b18738f78401d050a7c87..9ae49479ff0498ad1afc9a4f738aa3e6ae01b4e3 100644 --- a/web/modules/honeypot/src/Tests/HoneypotAdminFormTest.php +++ b/web/modules/honeypot/tests/src/Functional/HoneypotAdminFormTest.php @@ -1,18 +1,30 @@ <?php -namespace Drupal\honeypot\Tests; +namespace Drupal\Tests\honeypot\Functional; -use Drupal\simpletest\WebTestBase; +use Drupal\Tests\BrowserTestBase; /** * Test Honeypot spam protection admin form functionality. * * @group honeypot */ -class HoneypotAdminFormTest extends WebTestBase { +class HoneypotAdminFormTest extends BrowserTestBase { + /** + * Admin user. + * + * @var \Drupal\user\UserInterface + */ protected $adminUser; + /** + * Default theme. + * + * @var string + */ + protected $defaultTheme = 'stark'; + /** * Modules to enable. * @@ -46,14 +58,14 @@ public function testElementNameUpdateSuccess() { $this->drupalPostForm('admin/config/content/honeypot', $edit, t('Save configuration')); // Form should have been submitted successfully. - $this->assertText(t('The configuration options have been saved.'), 'Honeypot element name assertion works for valid names.'); + $this->assertSession()->pageTextContains('The configuration options have been saved.'); // Set up form and submit it. $edit['element_name'] = "test-1"; $this->drupalPostForm('admin/config/content/honeypot', $edit, t('Save configuration')); // Form should have been submitted successfully. - $this->assertText(t('The configuration options have been saved.'), 'Honeypot element name assertion works for valid names with dashes and numbers.'); + $this->assertSession()->pageTextContains('The configuration options have been saved.'); } /** @@ -68,7 +80,7 @@ public function testElementNameUpdateFirstCharacterFail() { $this->drupalPostForm('admin/config/content/honeypot', $edit, t('Save configuration')); // Form submission should fail. - $this->assertText(t('The element name must start with a letter.'), 'Honeypot element name assertion works for invalid names.'); + $this->assertSession()->pageTextContains('The element name must start with a letter.'); } /** @@ -83,14 +95,14 @@ public function testElementNameUpdateInvalidCharacterFail() { $this->drupalPostForm('admin/config/content/honeypot', $edit, t('Save configuration')); // Form submission should fail. - $this->assertText(t('The element name cannot contain spaces or other special characters.'), 'Honeypot element name assertion works for invalid names with special characters.'); + $this->assertSession()->pageTextContains('The element name cannot contain spaces or other special characters.'); // Set up form and submit it. $edit['element_name'] = "space in name"; $this->drupalPostForm('admin/config/content/honeypot', $edit, t('Save configuration')); // Form submission should fail. - $this->assertText(t('The element name cannot contain spaces or other special characters.'), 'Honeypot element name assertion works for invalid names with spaces.'); + $this->assertSession()->pageTextContains('The element name cannot contain spaces or other special characters.'); } } diff --git a/web/modules/honeypot/src/Tests/HoneypotFormCacheTest.php b/web/modules/honeypot/tests/src/Functional/HoneypotFormCacheTest.php similarity index 81% rename from web/modules/honeypot/src/Tests/HoneypotFormCacheTest.php rename to web/modules/honeypot/tests/src/Functional/HoneypotFormCacheTest.php index d899facbac615e8b8c22ed5cd69d97f54f39f73b..69ccb5d1a943957158c908cb557c2f9aed5d0380 100644 --- a/web/modules/honeypot/src/Tests/HoneypotFormCacheTest.php +++ b/web/modules/honeypot/tests/src/Functional/HoneypotFormCacheTest.php @@ -1,22 +1,38 @@ <?php -namespace Drupal\honeypot\Tests; +namespace Drupal\Tests\honeypot\Functional; use Drupal\comment\Tests\CommentTestTrait; use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface; use Drupal\contact\Entity\ContactForm; -use Drupal\simpletest\WebTestBase; +use Drupal\Tests\BrowserTestBase; use Drupal\user\Entity\Role; use Drupal\user\RoleInterface; +use Drupal\user\UserInterface; /** * Tests page caching on Honeypot protected forms. * * @group honeypot */ -class HoneypotFormCacheTest extends WebTestBase { +class HoneypotFormCacheTest extends BrowserTestBase { use CommentTestTrait; + + /** + * Default theme. + * + * @var string + */ + protected $defaultTheme = 'stark'; + + /** + * Node object. + * + * @var \Drupal\node\NodeInterface + */ + protected $node; + /** * Modules to enable. * @@ -24,8 +40,6 @@ class HoneypotFormCacheTest extends WebTestBase { */ public static $modules = ['honeypot', 'node', 'comment', 'contact']; - protected $node; - /** * {@inheritdoc} */ @@ -45,7 +59,7 @@ protected function setUp() { // Set up other required configuration. $user_config = \Drupal::configFactory()->getEditable('user.settings'); $user_config->set('verify_mail', TRUE); - $user_config->set('register', USER_REGISTER_VISITORS); + $user_config->set('register', UserInterface::REGISTER_VISITORS); $user_config->save(); // Create an Article node type. @@ -82,7 +96,7 @@ public function testCacheContactForm() { // Test on cache header with time limit enabled, cache should miss. $this->drupalGet('contact/feedback'); - $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), '', 'Page was not cached.'); + $this->assertEquals('', $this->drupalGetHeader('X-Drupal-Cache'), 'Page was not cached.'); // Disable time limit. \Drupal::configFactory()->getEditable('honeypot.settings')->set('time_limit', 0)->save(); @@ -91,12 +105,12 @@ public function testCacheContactForm() { $this->drupalGet('contact/feedback'); // Test on cache header with time limit disabled, cache should hit. $this->drupalGet('contact/feedback'); - $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', 'Page was cached.'); + $this->assertEquals('HIT', $this->drupalGetHeader('X-Drupal-Cache'), 'Page was cached.'); // Re-enable the time limit, we should not be seeing the cached version. \Drupal::configFactory()->getEditable('honeypot.settings')->set('time_limit', 5)->save(); $this->drupalGet('contact/feedback'); - $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), '', 'Page was not cached.'); + $this->assertEquals('', $this->drupalGetHeader('X-Drupal-Cache'), 'Page was not cached.'); } /** @@ -120,7 +134,7 @@ public function testCacheCommentForm() { // Test on cache header with time limit enabled, cache should miss. $this->drupalGet('node/' . $this->node->id()); - $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), '', 'Page was not cached.'); + $this->assertEquals('', $this->drupalGetHeader('X-Drupal-Cache'), 'Page was not cached.'); // Disable time limit. \Drupal::configFactory()->getEditable('honeypot.settings')->set('time_limit', 0)->save(); @@ -130,7 +144,7 @@ public function testCacheCommentForm() { // Test on cache header with time limit disabled, cache should hit. $this->drupalGet('node/' . $this->node->id()); - $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', 'Page was cached.'); + $this->assertEquals('HIT', $this->drupalGetHeader('X-Drupal-Cache'), 'Page was cached.'); } diff --git a/web/modules/honeypot/src/Tests/HoneypotFormProgrammaticSubmissionTest.php b/web/modules/honeypot/tests/src/Functional/HoneypotFormProgrammaticSubmissionTest.php similarity index 64% rename from web/modules/honeypot/src/Tests/HoneypotFormProgrammaticSubmissionTest.php rename to web/modules/honeypot/tests/src/Functional/HoneypotFormProgrammaticSubmissionTest.php index f1d39fb8daa87f75adf1d04aa9f9b91c7dd81232..50996ecc3cedef548223aa3a34848198cc9062c5 100644 --- a/web/modules/honeypot/src/Tests/HoneypotFormProgrammaticSubmissionTest.php +++ b/web/modules/honeypot/tests/src/Functional/HoneypotFormProgrammaticSubmissionTest.php @@ -1,15 +1,23 @@ <?php -namespace Drupal\honeypot\Tests; +namespace Drupal\Tests\honeypot\Functional; -use Drupal\simpletest\WebTestBase; +use Drupal\Component\Serialization\Json; +use Drupal\Tests\BrowserTestBase; /** * Test programmatic submission of forms protected by Honeypot. * * @group honeypot */ -class HoneypotFormProgrammaticSubmissionTest extends WebTestBase { +class HoneypotFormProgrammaticSubmissionTest extends BrowserTestBase { + + /** + * Default theme. + * + * @var string + */ + protected $defaultTheme = 'stark'; /** * Modules to enable. @@ -40,9 +48,9 @@ protected function setUp() { */ public function testProgrammaticFormSubmission() { $result = $this->drupalGet('/honeypot_test/submit_form'); - $form_errors = (array) json_decode($result); - $this->assertNoRaw('There was a problem with your form submission. Please wait 6 seconds and try again.'); - $this->assertFalse($form_errors, 'The were no validation errors when submitting the form.'); + $form_errors = (array) Json::decode($result); + $this->assertSession()->responseNotContains('There was a problem with your form submission. Please wait 6 seconds and try again.'); + $this->assertEmpty($form_errors, 'The were no validation errors when submitting the form.'); } } diff --git a/web/modules/honeypot/src/Tests/HoneypotFormTest.php b/web/modules/honeypot/tests/src/Functional/HoneypotFormTest.php similarity index 74% rename from web/modules/honeypot/src/Tests/HoneypotFormTest.php rename to web/modules/honeypot/tests/src/Functional/HoneypotFormTest.php index 20ea198375f4a258372713128ef3183c239bb855..e96ccaab9d1cb725da01bb01921664961c4d3863 100644 --- a/web/modules/honeypot/src/Tests/HoneypotFormTest.php +++ b/web/modules/honeypot/tests/src/Functional/HoneypotFormTest.php @@ -1,25 +1,50 @@ <?php -namespace Drupal\honeypot\Tests; +namespace Drupal\Tests\honeypot\Functional; -use Drupal\simpletest\WebTestBase; use Drupal\comment\Tests\CommentTestTrait; use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface; use Drupal\contact\Entity\ContactForm; +use Drupal\Tests\BrowserTestBase; +use Drupal\user\UserInterface; /** * Test Honeypot spam protection functionality. * * @group honeypot */ -class HoneypotFormTest extends WebTestBase { +class HoneypotFormTest extends BrowserTestBase { use CommentTestTrait; + /** + * Admin user. + * + * @var \Drupal\user\UserInterface + */ protected $adminUser; + + /** + * Site visitor. + * + * @var \Drupal\user\UserInterface + */ protected $webUser; + + /** + * Node object. + * + * @var \Drupal\node\NodeInterface + */ protected $node; + /** + * Default theme. + * + * @var string + */ + protected $defaultTheme = 'stark'; + /** * Modules to enable. * @@ -28,7 +53,7 @@ class HoneypotFormTest extends WebTestBase { public static $modules = ['honeypot', 'node', 'comment', 'contact']; /** - * Setup before test. + * {@inheritdoc} */ public function setUp() { // Enable modules required for this test. @@ -47,7 +72,7 @@ public function setUp() { // Set up other required configuration. $user_config = \Drupal::configFactory()->getEditable('user.settings'); $user_config->set('verify_mail', TRUE); - $user_config->set('register', USER_REGISTER_VISITORS); + $user_config->set('register', UserInterface::REGISTER_VISITORS); $user_config->save(); // Create an Article node type. @@ -89,7 +114,7 @@ public function setUp() { */ public function testUserLoginNotProtected() { $this->drupalGet('user'); - $this->assertNoText('id="edit-url" name="url"', 'Honeypot not enabled on user login form.'); + $this->assertSession()->responseNotContains('id="edit-url" name="url"'); } /** @@ -102,7 +127,7 @@ public function testProtectRegisterUserNormal() { $this->drupalPostForm('user/register', $edit, t('Create new account')); // Form should have been submitted successfully. - $this->assertText(t('A welcome message with further instructions has been sent to your email address.'), 'User registered successfully.'); + $this->assertSession()->pageTextContains('A welcome message with further instructions has been sent to your email address.'); } /** @@ -116,7 +141,7 @@ public function testProtectUserRegisterHoneypotFilled() { $this->drupalPostForm('user/register', $edit, t('Create new account')); // Form should have error message. - $this->assertText(t('There was a problem with your form submission. Please refresh the page and try again.'), 'Registration form protected by honeypot.'); + $this->assertSession()->pageTextContains('There was a problem with your form submission. Please refresh the page and try again.'); } /** @@ -142,7 +167,23 @@ public function testProtectRegisterUserTooFast() { $this->drupalPostForm('user/register', $edit, t('Create new account')); // Form should have error message. - $this->assertText(t('There was a problem with your form submission. Please wait 6 seconds and try again.'), 'Registration form protected by time limit.'); + $this->assertSession()->pageTextContains('There was a problem with your form submission. Please wait 6 seconds and try again.'); + } + + /** + * Test that any (not-strict-empty) value triggers protection. + */ + public function testStrictEmptinessOnHoneypotField() { + // Initialise the form values. + $edit['name'] = $this->randomMachineName(); + $edit['mail'] = $edit['name'] . '@example.com'; + + // Any value that is not strictly empty should trigger Honeypot. + foreach (['0', ' '] as $value) { + $edit['url'] = $value; + $this->drupalPostForm('user/register', $edit, t('Create new account')); + $this->assertText(t('There was a problem with your form submission. Please refresh the page and try again.'), "Honeypot protection is triggered when the honeypot field contains '{$value}'."); + } } /** @@ -160,7 +201,7 @@ public function testProtectCommentFormNormal() { // Set up form and submit it. $edit["comment_body[0][value]"] = $comment; $this->drupalPostForm('comment/reply/node/' . $this->node->id() . '/comment', $edit, t('Save')); - $this->assertText(t('Your comment has been queued for review'), 'Comment posted successfully.'); + $this->assertSession()->pageTextContains('Your comment has been queued for review'); } /** @@ -176,7 +217,7 @@ public function testProtectCommentFormHoneypotFilled() { $edit["comment_body[0][value]"] = $comment; $edit['url'] = 'http://www.example.com/'; $this->drupalPostForm('comment/reply/node/' . $this->node->id() . '/comment', $edit, t('Save')); - $this->assertText(t('There was a problem with your form submission. Please refresh the page and try again.'), 'Comment posted successfully.'); + $this->assertSession()->pageTextContains('There was a problem with your form submission. Please refresh the page and try again.'); } /** @@ -188,7 +229,7 @@ public function testProtectCommentFormHoneypotBypass() { // Get the comment reply form and ensure there's no 'url' field. $this->drupalGet('comment/reply/node/' . $this->node->id() . '/comment'); - $this->assertNoText('id="edit-url" name="url"', 'Honeypot home page field not shown.'); + $this->assertSession()->responseNotContains('id="edit-url" name="url"'); } /** @@ -204,7 +245,7 @@ public function testProtectNodeFormTooFast() { // Set up the form and submit it. $edit["title[0][value]"] = 'Test Page'; $this->drupalPostForm('node/add/article', $edit, t('Save')); - $this->assertText(t('There was a problem with your form submission.'), 'Honeypot node form timestamp protection works.'); + $this->assertSession()->pageTextContains('There was a problem with your form submission.'); } /** @@ -217,7 +258,7 @@ public function testProtectNodeFormPreviewPassthru() { // Post a node form using the 'Preview' button and make sure it's allowed. $edit["title[0][value]"] = 'Test Page'; $this->drupalPostForm('node/add/article', $edit, t('Preview')); - $this->assertNoText(t('There was a problem with your form submission.'), 'Honeypot not blocking node form previews.'); + $this->assertSession()->pageTextNotContains('There was a problem with your form submission.'); } /** @@ -227,7 +268,10 @@ public function testProtectContactForm() { $this->drupalLogin($this->adminUser); // Disable 'protect_all_forms'. - \Drupal::configFactory()->getEditable('honeypot.settings')->set('protect_all_forms', FALSE)->save(); + \Drupal::configFactory() + ->getEditable('honeypot.settings') + ->set('protect_all_forms', FALSE) + ->save(); // Create a Website feedback contact form. $feedback_form = ContactForm::create([ @@ -248,7 +292,7 @@ public function testProtectContactForm() { $this->drupalLogin($this->webUser); $this->drupalGet('contact/feedback'); - $this->assertField('url', 'Honeypot field is added to Contact form.'); + $this->assertSession()->fieldExists('url'); } } diff --git a/web/modules/media_entity_twitter/js/twitter.js b/web/modules/media_entity_twitter/js/twitter.js index 92297172c01cb31a26edb79392c36b2367b61fde..3734e0b9a2968e174c7f3d7060ec1d57252d55e5 100644 --- a/web/modules/media_entity_twitter/js/twitter.js +++ b/web/modules/media_entity_twitter/js/twitter.js @@ -2,24 +2,24 @@ * @file */ -(function ($, Drupal) { +(function (Drupal) { "use strict"; Drupal.behaviors.twitterMediaEntity = { attach: function (context) { - function _init () { - twttr.widgets.load(context); + function _init() { + twttr.widgets.load((context && context.nodeType === 1) ? context : null); } // If the tweet is being embedded in a CKEditor's iFrame the widgets // library might not have been loaded yet. if (typeof twttr == 'undefined') { - $.getScript('//platform.twitter.com/widgets.js', _init); - } - else { - _init(); + var script = document.createElement("script"); + script.src = '//platform.twitter.com/widgets.js'; + document.head.appendChild(script); } + _init(); } }; -})(jQuery, Drupal); +})(Drupal); diff --git a/web/modules/media_entity_twitter/media_entity_twitter.info.yml b/web/modules/media_entity_twitter/media_entity_twitter.info.yml index 0fcd6afec420de6d327c26b020c29c50857dd2cd..718e5679d6563122ac39abcc80e823e9b2d2f17f 100644 --- a/web/modules/media_entity_twitter/media_entity_twitter.info.yml +++ b/web/modules/media_entity_twitter/media_entity_twitter.info.yml @@ -1,13 +1,12 @@ -name: Media entity Twitter -description: 'Media entity Twitter provider.' +name: Media Entity Twitter +description: 'Media Entity Twitter provider.' type: module package: Media -# core: 8.x +core: 8.x dependencies: - drupal:media (>= 8.4) -# Information added by Drupal.org packaging script on 2017-10-13 -version: '8.x-2.0-alpha2' -core: '8.x' +# Information added by Drupal.org packaging script on 2020-01-21 +version: '8.x-2.3' project: 'media_entity_twitter' -datestamp: 1507907346 +datestamp: 1579597387 diff --git a/web/modules/media_entity_twitter/media_entity_twitter.install b/web/modules/media_entity_twitter/media_entity_twitter.install index 50be0d8d931e1c4fbf01c94cacfb2d2115bdc288..daa65ab75b33d0f42a3c2f6f3f0796787b98ab61 100644 --- a/web/modules/media_entity_twitter/media_entity_twitter.install +++ b/web/modules/media_entity_twitter/media_entity_twitter.install @@ -5,13 +5,15 @@ * Install, uninstall and update hooks for Media entity Twitter module. */ +use Drupal\Core\File\FileSystemInterface; + /** * Implements hook_install(). */ function media_entity_twitter_install() { $source = drupal_get_path('module', 'media_entity_twitter') . '/images/icons'; $destination = \Drupal::config('media.settings')->get('icon_base_uri'); - file_prepare_directory($destination, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); + \Drupal::service('file_system')->prepareDirectory($destination, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS); $files = file_scan_directory($source, '/.*\.(svg|png|jpg|jpeg|gif)$/'); foreach ($files as $file) { @@ -22,7 +24,7 @@ function media_entity_twitter_install() { // referenced somewhere else. Since showing an error that it was not // possible to copy the files is also confusing, we silently do nothing. if (!file_exists($destination . DIRECTORY_SEPARATOR . $file->filename)) { - file_unmanaged_copy($file->uri, $destination, FILE_EXISTS_ERROR); + \Drupal::service('file_system')->copy($file->uri, $destination, FileSystemInterface::EXISTS_ERROR); } } } @@ -34,7 +36,7 @@ function media_entity_twitter_requirements($phase) { $requirements = []; if ($phase == 'install') { $destination = \Drupal::config('media.settings')->get('icon_base_uri'); - file_prepare_directory($destination, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); + \Drupal::service('file_system')->prepareDirectory($destination, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS); $is_writable = is_writable($destination); $is_directory = is_dir($destination); if (!$is_writable || !$is_directory) { diff --git a/web/modules/media_entity_twitter/media_entity_twitter.libraries.yml b/web/modules/media_entity_twitter/media_entity_twitter.libraries.yml index d22dc483c5589d18815d61d902adde807789e324..e55869159a377234d8515e3d9ef823c57cb643d0 100644 --- a/web/modules/media_entity_twitter/media_entity_twitter.libraries.yml +++ b/web/modules/media_entity_twitter/media_entity_twitter.libraries.yml @@ -4,7 +4,6 @@ integration: 'js/twitter.js': {} dependencies: - core/drupal - - core/jquery - media_entity_twitter/twttr.widgets twttr.widgets: remote: //platform.twitter.com/widgets.js diff --git a/web/modules/media_entity_twitter/media_entity_twitter.post_update.php b/web/modules/media_entity_twitter/media_entity_twitter.post_update.php new file mode 100644 index 0000000000000000000000000000000000000000..d61cb74eda23bc5f464a74c368c53c3cbf9ac138 --- /dev/null +++ b/web/modules/media_entity_twitter/media_entity_twitter.post_update.php @@ -0,0 +1,14 @@ +<?php + +/** + * @file + * Post update functions for Media entity Twitter module. + */ + +/** + * Rename the cache bin. + */ +function media_entity_twitter_post_update_rename_cache_bin() { + // An empty update will force service definitions to be cleared and create a + // new bin with new name. +} diff --git a/web/modules/media_entity_twitter/media_entity_twitter.services.yml b/web/modules/media_entity_twitter/media_entity_twitter.services.yml index a520e35817bd62b47114cd77e672e945c2f2e00e..1072dfa0ab3ee287f4afd91094be9c46d7c5cdfe 100644 --- a/web/modules/media_entity_twitter/media_entity_twitter.services.yml +++ b/web/modules/media_entity_twitter/media_entity_twitter.services.yml @@ -1,13 +1,13 @@ services: media_entity_twitter.tweet_fetcher: - class: '\Drupal\media_entity_twitter\TweetFetcher' + class: Drupal\media_entity_twitter\TweetFetcher arguments: - - '@media_entity_twitter.cache.tweets' + - '@cache.tweets' - media_entity_twitter.cache.tweets: - class: '\Drupal\Core\Cache\CacheBackendInterface' + cache.tweets: + class: Drupal\Core\Cache\CacheBackendInterface tags: - - { name: cache.bin, default_backend: cache.backend.chainedfast } + - { name: cache.bin } factory: cache_factory:get arguments: - tweets diff --git a/web/modules/media_entity_twitter/src/Plugin/Validation/Constraint/TweetVisibleConstraintValidator.php b/web/modules/media_entity_twitter/src/Plugin/Validation/Constraint/TweetVisibleConstraintValidator.php index ccf13fe12776896fb36dd334167babe37efba014..81967dab07e16aee442b0743cb16c62a3d90904d 100644 --- a/web/modules/media_entity_twitter/src/Plugin/Validation/Constraint/TweetVisibleConstraintValidator.php +++ b/web/modules/media_entity_twitter/src/Plugin/Validation/Constraint/TweetVisibleConstraintValidator.php @@ -3,9 +3,11 @@ namespace Drupal\media_entity_twitter\Plugin\Validation\Constraint; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; +use Drupal\Core\Field\FieldItemListInterface; use Drupal\media_entity_twitter\Plugin\media\Source\Twitter; use Drupal\Core\Field\FieldItemInterface; use GuzzleHttp\Client; +use GuzzleHttp\Exception\ClientException; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; @@ -47,7 +49,7 @@ public function validate($value, Constraint $constraint) { if (is_string($value)) { $data = $value; } - elseif ($value instanceof FieldItemList) { + elseif ($value instanceof FieldItemListInterface) { $fieldtype = $value->getFieldDefinition()->getType(); $field_value = $value->getValue(); if ($fieldtype == 'link') { @@ -76,7 +78,13 @@ public function validate($value, Constraint $constraint) { } // Fetch content from the given url. - $response = $this->httpClient->get($matches[0][0], ['allow_redirects' => FALSE]); + try { + $response = $this->httpClient->get($matches[0][0], ['allow_redirects' => FALSE]); + } + catch (ClientException $e) { + $this->context->addViolation($constraint->message); + return; + } if ($response->getStatusCode() == 302 && ($location = $response->getHeader('location'))) { $effective_url_parts = parse_url($location[0]); diff --git a/web/modules/media_entity_twitter/src/Plugin/media/Source/Twitter.php b/web/modules/media_entity_twitter/src/Plugin/media/Source/Twitter.php index 38fdf83324aeae75f710b7acd779e59ef929a90e..a1d994039d8d9d33126b5ab42b7691f4a67bd492 100644 --- a/web/modules/media_entity_twitter/src/Plugin/media/Source/Twitter.php +++ b/web/modules/media_entity_twitter/src/Plugin/media/Source/Twitter.php @@ -3,10 +3,13 @@ namespace Drupal\media_entity_twitter\Plugin\media\Source; use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Datetime\DrupalDateTime; use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\File\FileSystemInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Logger\LoggerChannelInterface; +use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface; use Drupal\Core\Render\RendererInterface; use Drupal\media\MediaInterface; use Drupal\media\MediaSourceBase; @@ -50,6 +53,13 @@ class Twitter extends MediaSourceBase implements MediaSourceFieldConstraintsInte */ protected $logger; + /** + * The file system service. + * + * @var \Drupal\Core\File\FileSystemInterface + */ + protected $fileSystem; + /** * {@inheritdoc} */ @@ -64,7 +74,8 @@ public static function create(ContainerInterface $container, array $configuratio $container->get('config.factory'), $container->get('renderer'), $container->get('media_entity_twitter.tweet_fetcher'), - $container->get('logger.factory')->get('media_entity_twitter') + $container->get('logger.factory')->get('media_entity_twitter'), + $container->get('file_system') ); } @@ -101,11 +112,12 @@ public static function create(ContainerInterface $container, array $configuratio * @param \Drupal\Core\Logger\LoggerChannelInterface $logger * The logger channel. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, FieldTypePluginManagerInterface $field_type_manager, ConfigFactoryInterface $config_factory, RendererInterface $renderer, TweetFetcherInterface $tweet_fetcher, LoggerChannelInterface $logger) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, FieldTypePluginManagerInterface $field_type_manager, ConfigFactoryInterface $config_factory, RendererInterface $renderer, TweetFetcherInterface $tweet_fetcher, LoggerChannelInterface $logger, FileSystemInterface $file_system) { parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager, $entity_field_manager, $field_type_manager, $config_factory); $this->renderer = $renderer; $this->tweetFetcher = $tweet_fetcher; $this->logger = $logger; + $this->fileSystem = $file_system; } /** @@ -140,6 +152,8 @@ public function getMetadataAttributes() { 'content' => $this->t('This tweet content'), 'retweet_count' => $this->t('Retweet count for this tweet'), 'profile_image_url_https' => $this->t('Link to profile image'), + 'created_time' => $this->t('Date/time created'), + 'user_name' => $this->t('User name'), ]; } @@ -193,7 +207,8 @@ public function getMetadata(MediaInterface $media, $attribute_name) { ]; $svg = $this->renderer->renderRoot($thumbnail); - return file_unmanaged_save_data($svg, $thumbnail_uri, FILE_EXISTS_ERROR) ?: parent::getMetadata($media, $attribute_name); + + return $this->fileSystem->saveData($svg, $thumbnail_uri, FileSystemInterface::EXISTS_ERROR) ?: parent::getMetadata($media, $attribute_name); } // If we have auth settings return the other fields. @@ -217,7 +232,7 @@ public function getMetadata(MediaInterface $media, $attribute_name) { // @TODO: Use Guzzle, possibly in a service, for this. $image_data = file_get_contents($image_url); if ($image_data) { - return file_unmanaged_save_data($image_data, $local_uri, FILE_EXISTS_REPLACE); + return $this->fileSystem->saveData($image_data, $local_uri, FileSystemInterface::EXISTS_REPLACE); } } } @@ -231,8 +246,8 @@ public function getMetadata(MediaInterface $media, $attribute_name) { return NULL; case 'content': - if (isset($tweet['text'])) { - return $tweet['text']; + if (isset($tweet['full_text'])) { + return $tweet['full_text']; } return NULL; @@ -248,6 +263,20 @@ public function getMetadata(MediaInterface $media, $attribute_name) { } return NULL; + case 'created_time': + if (isset($tweet['created_at'])) { + if ($datetime = DrupalDateTime::createFromFormat('D M d H:i:s O Y', $tweet['created_at'])) { + return $datetime->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT); + } + } + return NULL; + + case 'user_name': + if (isset($tweet['user']['name'])) { + return $tweet['user']['name']; + } + return NULL; + case 'default_name': $user = $this->getMetadata($media, 'user'); $id = $this->getMetadata($media, 'id'); @@ -380,7 +409,7 @@ protected function getLocalImageUri($id, MediaInterface $media, $media_url = NUL // Ensure that the destination directory is writable. If not, log a warning // and return the default thumbnail. - $ready = file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); + $ready = $this->fileSystem->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS); if (!$ready) { $this->logger->warning('Could not prepare thumbnail destination directory @dir', [ '@dir' => $directory, diff --git a/web/modules/media_entity_twitter/src/TweetFetcher.php b/web/modules/media_entity_twitter/src/TweetFetcher.php index 88b442bd7eca6584640b5770aade238430022374..6c72f99a55fc0acb321f75b7189aa52760c76cdf 100644 --- a/web/modules/media_entity_twitter/src/TweetFetcher.php +++ b/web/modules/media_entity_twitter/src/TweetFetcher.php @@ -59,7 +59,7 @@ public function fetchTweet($id) { // Query Twitter's API. $response = $this->twitter - ->setGetfield('?id=' . $id) + ->setGetfield('?id=' . $id . '&tweet_mode=extended') ->buildOAuth('https://api.twitter.com/1.1/statuses/show.json', 'GET') ->performRequest(); diff --git a/web/modules/media_entity_twitter/tests/src/Functional/TweetEmbedFormatterTest.php b/web/modules/media_entity_twitter/tests/src/Functional/TweetEmbedFormatterTest.php index bfdeca2b6ce569801e5fb537eb1a8f323a5ee42c..8470b3a36b17f59a071ed571e2a4b8004d543511 100644 --- a/web/modules/media_entity_twitter/tests/src/Functional/TweetEmbedFormatterTest.php +++ b/web/modules/media_entity_twitter/tests/src/Functional/TweetEmbedFormatterTest.php @@ -21,19 +21,12 @@ class TweetEmbedFormatterTest extends MediaFunctionalTestBase { 'link', ]; - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - } - /** * Tests adding and editing a twitter embed formatter. */ public function testManageEmbedFormatter() { // Test and create one media type. - $bundle = $this->createMediaType(['bundle' => 'twitter'], 'twitter'); + $bundle = $this->createMediaType('twitter', ['id' => 'twitter']); // We need to fix widget and formatter config for the default field. $source = $bundle->getSource(); @@ -42,6 +35,13 @@ public function testManageEmbedFormatter() { $component = \Drupal::service('plugin.manager.field.widget') ->prepareConfiguration('string', []); + // Enable the conical URL. + \Drupal::configFactory() + ->getEditable('media.settings') + ->set('standalone_url', TRUE) + ->save(TRUE); + $this->container->get('router.builder')->rebuild(); + // @todo Replace entity_get_form_display() when #2367933 is done. // https://www.drupal.org/node/2872159. entity_get_form_display('media', $bundle->id(), 'default') @@ -50,8 +50,8 @@ public function testManageEmbedFormatter() { // Assert that the media type has the expected values before proceeding. $this->drupalGet('admin/structure/media/manage/' . $bundle->id()); - $this->assertFieldByName('label', $bundle->label()); - $this->assertFieldByName('source', 'twitter'); + $this->assertSession()->fieldValueEquals('label', $bundle->label()); + $this->assertSession()->fieldValueEquals('source', 'twitter'); // Add and save string_long field type settings (Embed code). $this->drupalGet('admin/structure/media/manage/' . $bundle->id() . '/fields/add-field'); @@ -61,33 +61,36 @@ public function testManageEmbedFormatter() { 'field_name' => 'embed_code', ]; $this->drupalPostForm(NULL, $edit_conf, t('Save and continue')); - $this->assertText('These settings apply to the ' . $edit_conf['label'] . ' field everywhere it is used.'); + $this->assertSession() + ->responseContains('These settings apply to the <em class="placeholder">' . $edit_conf['label'] . '</em> field everywhere it is used.'); $edit = [ 'cardinality' => 'number', 'cardinality_number' => '1', ]; $this->drupalPostForm(NULL, $edit, t('Save field settings')); - $this->assertText('Updated field ' . $edit_conf['label'] . ' field settings.'); + $this->assertSession() + ->responseContains('Updated field <em class="placeholder">' . $edit_conf['label'] . '</em> field settings.'); // Set the new string_long field type as required. $edit = [ 'required' => TRUE, ]; $this->drupalPostForm(NULL, $edit, t('Save settings')); - $this->assertText('Saved ' . $edit_conf['label'] . ' configuration.'); + $this->assertSession() + ->responseContains('Saved <em class="placeholder">' . $edit_conf['label'] . '</em> configuration.'); // Assert that the new field types configurations have been successfully // saved. $this->drupalGet('admin/structure/media/manage/' . $bundle->id() . '/fields'); $xpath = $this->xpath('//*[@id=:id]/td', [':id' => 'field-media-twitter']); - $this->assertEqual((string) $xpath[0]->getText(), 'Tweet Url'); - $this->assertEqual((string) $xpath[1]->getText(), 'field_media_twitter'); - $this->assertEqual((string) $xpath[2]->find('css', 'a')->getText(), 'Text (plain)'); + $this->assertEquals((string) $xpath[0]->getText(), 'Tweet URL'); + $this->assertEquals((string) $xpath[1]->getText(), 'field_media_twitter'); + $this->assertEquals((string) $xpath[2]->find('css', 'a')->getText(), 'Text (plain)'); $xpath = $this->xpath('//*[@id=:id]/td', [':id' => 'field-embed-code']); - $this->assertEqual((string) $xpath[0]->getText(), 'Embed code'); - $this->assertEqual((string) $xpath[1]->getText(), 'field_embed_code'); - $this->assertEqual((string) $xpath[2]->find('css', 'a')->getText(), 'Text (plain, long)'); + $this->assertEquals((string) $xpath[0]->getText(), 'Embed code'); + $this->assertEquals((string) $xpath[1]->getText(), 'field_embed_code'); + $this->assertEquals((string) $xpath[2]->find('css', 'a')->getText(), 'Text (plain, long)'); $this->drupalGet('admin/structure/media/manage/' . $bundle->id() . '/display'); @@ -101,13 +104,13 @@ public function testManageEmbedFormatter() { 'fields[field_embed_code][type]' => 'twitter_embed', ]; $this->drupalPostForm(NULL, $edit, t('Save')); - $this->assertText('Your settings have been saved.'); + $this->assertSession()->responseContains('Your settings have been saved.'); // Create and save the media with a twitter media code. $this->drupalGet('media/add/' . $bundle->id()); // Random image url from twitter. - $tweet_url = 'https://twitter.com/RamzyStinson/status/670650348319576064'; + $tweet_url = 'https://twitter.com/DrupalConEur/status/1176518741208817664'; // Random image from twitter. $tweet = '<blockquote class="twitter-tweet" lang="it"><p lang="en" dir="ltr">' . @@ -123,17 +126,19 @@ public function testManageEmbedFormatter() { 'field_embed_code[0][value]' => $tweet, ]; $this->drupalPostForm(NULL, $edit, t('Save')); + $this->drupalGet('media/1'); // Assert that the media has been successfully saved. - $this->assertText('Title'); + $this->assertSession()->pageTextContains('Title'); // Assert that the link url formatter exists on this page. - $this->assertText('Tweet Url'); - $this->assertRaw('<a href="https://twitter.com/RamzyStinson/statuses/670650348319576064">', 'Link in embedded Tweet found.'); + $this->assertSession()->pageTextContains('Tweet URL'); + $this->assertSession() + ->responseContains('<a href="https://twitter.com/RamzyStinson/statuses/670650348319576064">', 'Link in embedded Tweet found.'); // Assert that the string_long code formatter exists on this page. - $this->assertText('Embed code'); - $this->assertRaw('<blockquote class="twitter-tweet', 'Embedded Tweet found.'); + $this->assertSession()->pageTextContains('Embed code'); + $this->assertSession()->responseContains('<blockquote class="twitter-tweet', 'Embedded Tweet found.'); } } diff --git a/web/modules/media_entity_twitter/tests/src/Kernel/ThumbnailTest.php b/web/modules/media_entity_twitter/tests/src/Kernel/ThumbnailTest.php index a39cda295b4bb436238d40243ab889b954949d90..4c43d24c816980b5e9db62234a46c6837ff1075d 100644 --- a/web/modules/media_entity_twitter/tests/src/Kernel/ThumbnailTest.php +++ b/web/modules/media_entity_twitter/tests/src/Kernel/ThumbnailTest.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\media_entity_twitter\Kernel; +use Drupal\Core\File\FileSystemInterface; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\KernelTests\KernelTestBase; @@ -61,7 +62,7 @@ protected function setUp() { $this->installEntitySchema('media'); $this->installConfig(['media_entity_twitter', 'system']); - $this->tweetFetcher = $this->getMock(TweetFetcherInterface::class); + $this->tweetFetcher = $this->createMock(TweetFetcherInterface::class); $this->container->set('media_entity_twitter.tweet_fetcher', $this->tweetFetcher); MediaType::create([ @@ -106,7 +107,7 @@ protected function setUp() { ->get('media_entity_twitter.settings') ->get('local_images'); - file_prepare_directory($dir, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); + \Drupal::service('file_system')->prepareDirectory($dir, FileSystemInterface::CREATE_DIRECTORY| FileSystemInterface::MODIFY_PERMISSIONS); } /** diff --git a/web/modules/media_entity_twitter/tests/src/Unit/ConstraintsTest.php b/web/modules/media_entity_twitter/tests/src/Unit/ConstraintsTest.php index fdbc1148ce025f1f7b31dabc832b634ff532f831..69e4a1bb0f641ec187b72a584615b4c626625720 100644 --- a/web/modules/media_entity_twitter/tests/src/Unit/ConstraintsTest.php +++ b/web/modules/media_entity_twitter/tests/src/Unit/ConstraintsTest.php @@ -96,7 +96,7 @@ public function testTweetVisibleConstraint($embed_code, $mocked_response, $viola $constraint = new TweetVisibleConstraint(); $this->assertEquals('Referenced tweet is not publicly visible.', $constraint->message, 'Correct constraint message found.'); - $http_client = $this->getMock('\GuzzleHttp\Client'); + $http_client = $this->createMock('\GuzzleHttp\Client'); $http_client->expects($this->once()) ->method('__call') ->with('get', [$embed_code, ['allow_redirects' => FALSE]]) @@ -127,12 +127,12 @@ public function testTweetVisibleConstraint($embed_code, $mocked_response, $viola * Provides test data for testTweetVisibleConstraint(). */ public function visibleProvider() { - $visible_response = $this->getMock('\GuzzleHttp\Psr7\Response'); + $visible_response = $this->createMock('\GuzzleHttp\Psr7\Response'); $visible_response->expects($this->any()) ->method('getStatusCode') ->will($this->returnValue(200)); - $invisible_response = $this->getMock('\GuzzleHttp\Psr7\Response'); + $invisible_response = $this->createMock('\GuzzleHttp\Psr7\Response'); $invisible_response->expects($this->once()) ->method('getStatusCode') ->will($this->returnValue(302)); @@ -165,8 +165,10 @@ public function visibleProvider() { */ public function testBadUrlsOnVisibleConstraint($embed_code) { - $http_client = $this->getMock('\GuzzleHttp\Client'); - $http_client->expects($this->never())->method('get'); + $http_client = $this->createMock('\GuzzleHttp\Client'); + $http_client->expects($this->never()) + ->method('__call') + ->with('get', [$embed_code, ['allow_redirects' => FALSE]]); $execution_context = $this->getMockBuilder('\Drupal\Core\TypedData\Validation\ExecutionContext') ->disableOriginalConstructor()