diff --git a/composer.json b/composer.json index b87ed6814cb5d6e37fd7ecbb64379de1a6f82a9f..04965fbaf9a161b6b9ba9db4d726bbbfdf15c603 100644 --- a/composer.json +++ b/composer.json @@ -111,7 +111,7 @@ "drupal/ctools": "3.4", "drupal/devel": "2.0", "drupal/draggableviews": "1.0", - "drupal/dropzonejs": "2.1", + "drupal/dropzonejs": "2.3", "drupal/editor_advanced_link": "1.8", "drupal/embed": "1.4", "drupal/entity": "1.0-beta1", @@ -131,7 +131,7 @@ "drupal/honeypot": "1.30", "drupal/image_popup": "1.1", "drupal/inline_entity_form": "1.0-rc7", - "drupal/libraries": "3.0.0-alpha1", + "drupal/libraries": "3.0.0-alpha6", "drupal/link_attributes": "1.11", "drupal/linkit": "5.0-beta11", "drupal/magnific_popup": "1.3", @@ -141,7 +141,7 @@ "drupal/menu_block": "1.6", "drupal/menu_block_title": "1.1", "drupal/menu_breadcrumb": "1.14", - "drupal/metatag": "1.14", + "drupal/metatag": "1.15", "drupal/migrate_devel": "2.0-alpha2", "drupal/migrate_plus": "5.1", "drupal/migrate_tools": "5.0", @@ -159,11 +159,11 @@ "drupal/redis": "1.0", "drupal/roleassign": "1.0.0-beta1", "drupal/scheduler": "1.3", - "drupal/search_api": "1.17", - "drupal/search_api_db": "1.17", + "drupal/search_api": "1.18", + "drupal/search_api_db": "1.18", "drupal/simple_gmap": "3.0", "drupal/simple_megamenu": "1.0-beta3", - "drupal/simple_sitemap": "3.7", + "drupal/simple_sitemap": "3.8", "drupal/simplesamlphp_auth": "3.1", "drupal/smtp": "1.0-rc4", "drupal/social_media": "1.8", diff --git a/composer.lock b/composer.lock index f65fb16877f63aff279842d02dd5ca4ab7279f9c..791123dec412ca8714d648b63d6ae51d34a55a01 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": "04322a544764f633495fd1d9c5c319f8", + "content-hash": "9909b71b39861fcde2b53c5d327ee3ed", "packages": [ { "name": "alchemy/zippy", @@ -4028,35 +4028,32 @@ }, { "name": "drupal/dropzonejs", - "version": "2.1.0", + "version": "2.3.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/dropzonejs.git", - "reference": "8.x-2.1" + "reference": "8.x-2.3" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/dropzonejs-8.x-2.1.zip", - "reference": "8.x-2.1", - "shasum": "a165b70070c320933fdce0c02be3577abce5e938" + "url": "https://ftp.drupal.org/files/projects/dropzonejs-8.x-2.3.zip", + "reference": "8.x-2.3", + "shasum": "c145f2560ed31de32ea8938b0c0378765f70e36a" }, "require": { "drupal/core": "^8.8 || ^9" }, "require-dev": { - "drupal/entity_browser": "*" + "drupal/entity_browser": "^2.5" }, "suggest": { - "enyo/dropzone": "Required to user drupal/dropzonejs. Dropzone is an easy to use drag'n'drop library." + "enyo/dropzone": "Required to use drupal/dropzonejs. DropzoneJS is an open source library that provides drag’n’drop file uploads with image previews." }, "type": "drupal-module", "extra": { - "branch-alias": { - "dev-2.x": "2.x-dev" - }, "drupal": { - "version": "8.x-2.1", - "datestamp": "1585662324", + "version": "8.x-2.3", + "datestamp": "1600311980", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -4065,7 +4062,7 @@ }, "notification-url": "https://packages.drupal.org/8/downloads", "license": [ - "GPL-2.0+" + "GPL-2.0-or-later" ], "authors": [ { @@ -4083,6 +4080,11 @@ "homepage": "https://drupal.org/u/Primsi", "role": "Maintainer" }, + { + "name": "Qiangjun Ran", + "homepage": "https://drupal.org/u/jungle", + "role": "Maintainer" + }, { "name": "See other contributors", "homepage": "https://www.drupal.org/node/1998478/committers", @@ -4110,7 +4112,7 @@ "support": { "source": "https://www.drupal.org/project/dropzonejs", "issues": "https://www.drupal.org/project/issues/dropzonejs", - "irc": "irc://irc.freenode.org/drupal-contribute" + "#media": "http://drupal.slack.com" } }, { @@ -4118,14 +4120,11 @@ "version": "2.1.0", "require": { "drupal/core": "^8.8 || ^9", - "drupal/dropzonejs": "self.version", + "drupal/dropzonejs": "*", "drupal/entity_browser": "*" }, "type": "metapackage", "extra": { - "branch-alias": { - "dev-2.x": "2.x-dev" - }, "drupal": { "version": "8.x-2.1", "datestamp": "1585662324", @@ -4156,6 +4155,10 @@ "name": "chr.fritsch", "homepage": "https://www.drupal.org/user/2103716" }, + { + "name": "jungle", + "homepage": "https://www.drupal.org/user/2919723" + }, { "name": "slashrsm", "homepage": "https://www.drupal.org/user/744628" @@ -5432,29 +5435,26 @@ }, { "name": "drupal/libraries", - "version": "3.0.0-alpha1", + "version": "3.0.0-alpha6", "source": { "type": "git", "url": "https://git.drupalcode.org/project/libraries.git", - "reference": "8.x-3.0-alpha1" + "reference": "8.x-3.0-alpha6" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/libraries-8.x-3.0-alpha1.zip", - "reference": "8.x-3.0-alpha1", - "shasum": "bb07036b1eaeea7d736fc7e72416238830cd8d67" + "url": "https://ftp.drupal.org/files/projects/libraries-8.x-3.0-alpha6.zip", + "reference": "8.x-3.0-alpha6", + "shasum": "d2aaf7f0968a1864457a7741b7e38ab11ef83e2c" }, "require": { - "drupal/core": "~8.0" + "drupal/core": "^8 || ^9" }, "type": "drupal-module", "extra": { - "branch-alias": { - "dev-3.x": "3.x-dev" - }, "drupal": { - "version": "8.x-3.0-alpha1", - "datestamp": "1517046484", + "version": "8.x-3.0-alpha6", + "datestamp": "1608099124", "security-coverage": { "status": "not-covered", "message": "Alpha releases are not covered by Drupal security advisories." @@ -5470,6 +5470,10 @@ "name": "Pol", "homepage": "https://www.drupal.org/user/47194" }, + { + "name": "joseph.olstad", + "homepage": "https://www.drupal.org/user/1321830" + }, { "name": "rjacobs", "homepage": "https://www.drupal.org/user/422459" @@ -6020,17 +6024,17 @@ }, { "name": "drupal/metatag", - "version": "1.14.0", + "version": "1.15.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/metatag.git", - "reference": "8.x-1.14" + "reference": "8.x-1.15" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/metatag-8.x-1.14.zip", - "reference": "8.x-1.14", - "shasum": "9bf9f1517ad015d0c93ca1460e284c557624aa90" + "url": "https://ftp.drupal.org/files/projects/metatag-8.x-1.15.zip", + "reference": "8.x-1.15", + "shasum": "7658d7286fdc075ea72a6ec36aea737b1182b5d8" }, "require": { "drupal/core": "^8 || ^9", @@ -6046,8 +6050,8 @@ "type": "drupal-module", "extra": { "drupal": { - "version": "8.x-1.14", - "datestamp": "1597183852", + "version": "8.x-1.15", + "datestamp": "1607188979", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -7120,17 +7124,17 @@ }, { "name": "drupal/search_api", - "version": "1.17.0", + "version": "1.18.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/search_api.git", - "reference": "8.x-1.17" + "reference": "8.x-1.18" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/search_api-8.x-1.17.zip", - "reference": "8.x-1.17", - "shasum": "a9f3352f1c8c893c7032c11f00a23f76b9733b56" + "url": "https://ftp.drupal.org/files/projects/search_api-8.x-1.18.zip", + "reference": "8.x-1.18", + "shasum": "6cf1d6820ba55891e204bac40b6031ed15db482a" }, "require": { "drupal/core": "^8.8 || ^9" @@ -7151,8 +7155,8 @@ "type": "drupal-module", "extra": { "drupal": { - "version": "8.x-1.17", - "datestamp": "1591128369", + "version": "8.x-1.18", + "datestamp": "1605204423", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -7166,7 +7170,7 @@ }, "notification-url": "https://packages.drupal.org/8/downloads", "license": [ - "GPL-2.0+" + "GPL-2.0-or-later" ], "authors": [ { @@ -7192,16 +7196,16 @@ }, { "name": "drupal/search_api_db", - "version": "1.17.0", + "version": "1.18.0", "require": { "drupal/core": "^8.8 || ^9", - "drupal/search_api": "self.version" + "drupal/search_api": "*" }, "type": "metapackage", "extra": { "drupal": { - "version": "8.x-1.17", - "datestamp": "1591128369", + "version": "8.x-1.18", + "datestamp": "1605204423", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -7336,17 +7340,17 @@ }, { "name": "drupal/simple_sitemap", - "version": "3.7.0", + "version": "3.8.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/simple_sitemap.git", - "reference": "8.x-3.7" + "reference": "8.x-3.8" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/simple_sitemap-8.x-3.7.zip", - "reference": "8.x-3.7", - "shasum": "7b23930cc71d37f332c1e836bc18c29ed2ae8cde" + "url": "https://ftp.drupal.org/files/projects/simple_sitemap-8.x-3.8.zip", + "reference": "8.x-3.8", + "shasum": "55252af261fbd7b18c230ab429eb7d8b92f4c66a" }, "require": { "drupal/core": "^8 || ^9", @@ -7355,8 +7359,8 @@ "type": "drupal-module", "extra": { "drupal": { - "version": "8.x-3.7", - "datestamp": "1592298918", + "version": "8.x-3.8", + "datestamp": "1605141357", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -7378,6 +7382,10 @@ "homepage": "https://www.drupal.org/u/gbyte.co", "email": "contact@gbyte.co", "role": "Maintainer" + }, + { + "name": "gbyte", + "homepage": "https://www.drupal.org/user/2381352" } ], "description": "Creates a standard conform hreflang XML sitemap of the site content and provides a framework for developing other sitemap types.", diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index 20a73a20d6f7e9fd33474595cfb998b859b01012..35095aa591ae875c568b78a4895a53534b852cdb 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -4148,36 +4148,33 @@ }, { "name": "drupal/dropzonejs", - "version": "2.1.0", - "version_normalized": "2.1.0.0", + "version": "2.3.0", + "version_normalized": "2.3.0.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/dropzonejs.git", - "reference": "8.x-2.1" + "reference": "8.x-2.3" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/dropzonejs-8.x-2.1.zip", - "reference": "8.x-2.1", - "shasum": "a165b70070c320933fdce0c02be3577abce5e938" + "url": "https://ftp.drupal.org/files/projects/dropzonejs-8.x-2.3.zip", + "reference": "8.x-2.3", + "shasum": "c145f2560ed31de32ea8938b0c0378765f70e36a" }, "require": { "drupal/core": "^8.8 || ^9" }, "require-dev": { - "drupal/entity_browser": "*" + "drupal/entity_browser": "^2.5" }, "suggest": { - "enyo/dropzone": "Required to user drupal/dropzonejs. Dropzone is an easy to use drag'n'drop library." + "enyo/dropzone": "Required to use drupal/dropzonejs. DropzoneJS is an open source library that provides drag’n’drop file uploads with image previews." }, "type": "drupal-module", "extra": { - "branch-alias": { - "dev-2.x": "2.x-dev" - }, "drupal": { - "version": "8.x-2.1", - "datestamp": "1585662324", + "version": "8.x-2.3", + "datestamp": "1600311980", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -4187,7 +4184,7 @@ "installation-source": "dist", "notification-url": "https://packages.drupal.org/8/downloads", "license": [ - "GPL-2.0+" + "GPL-2.0-or-later" ], "authors": [ { @@ -4205,6 +4202,11 @@ "homepage": "https://drupal.org/u/Primsi", "role": "Maintainer" }, + { + "name": "Qiangjun Ran", + "homepage": "https://drupal.org/u/jungle", + "role": "Maintainer" + }, { "name": "See other contributors", "homepage": "https://www.drupal.org/node/1998478/committers", @@ -4232,7 +4234,7 @@ "support": { "source": "https://www.drupal.org/project/dropzonejs", "issues": "https://www.drupal.org/project/issues/dropzonejs", - "irc": "irc://irc.freenode.org/drupal-contribute" + "#media": "http://drupal.slack.com" } }, { @@ -4241,14 +4243,11 @@ "version_normalized": "2.1.0.0", "require": { "drupal/core": "^8.8 || ^9", - "drupal/dropzonejs": "self.version", + "drupal/dropzonejs": "*", "drupal/entity_browser": "*" }, "type": "metapackage", "extra": { - "branch-alias": { - "dev-2.x": "2.x-dev" - }, "drupal": { "version": "8.x-2.1", "datestamp": "1585662324", @@ -4279,6 +4278,10 @@ "name": "chr.fritsch", "homepage": "https://www.drupal.org/user/2103716" }, + { + "name": "jungle", + "homepage": "https://www.drupal.org/user/2919723" + }, { "name": "slashrsm", "homepage": "https://www.drupal.org/user/744628" @@ -5594,30 +5597,27 @@ }, { "name": "drupal/libraries", - "version": "3.0.0-alpha1", - "version_normalized": "3.0.0.0-alpha1", + "version": "3.0.0-alpha6", + "version_normalized": "3.0.0.0-alpha6", "source": { "type": "git", "url": "https://git.drupalcode.org/project/libraries.git", - "reference": "8.x-3.0-alpha1" + "reference": "8.x-3.0-alpha6" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/libraries-8.x-3.0-alpha1.zip", - "reference": "8.x-3.0-alpha1", - "shasum": "bb07036b1eaeea7d736fc7e72416238830cd8d67" + "url": "https://ftp.drupal.org/files/projects/libraries-8.x-3.0-alpha6.zip", + "reference": "8.x-3.0-alpha6", + "shasum": "d2aaf7f0968a1864457a7741b7e38ab11ef83e2c" }, "require": { - "drupal/core": "~8.0" + "drupal/core": "^8 || ^9" }, "type": "drupal-module", "extra": { - "branch-alias": { - "dev-3.x": "3.x-dev" - }, "drupal": { - "version": "8.x-3.0-alpha1", - "datestamp": "1517046484", + "version": "8.x-3.0-alpha6", + "datestamp": "1608099124", "security-coverage": { "status": "not-covered", "message": "Alpha releases are not covered by Drupal security advisories." @@ -5634,6 +5634,10 @@ "name": "Pol", "homepage": "https://www.drupal.org/user/47194" }, + { + "name": "joseph.olstad", + "homepage": "https://www.drupal.org/user/1321830" + }, { "name": "rjacobs", "homepage": "https://www.drupal.org/user/422459" @@ -6202,18 +6206,18 @@ }, { "name": "drupal/metatag", - "version": "1.14.0", - "version_normalized": "1.14.0.0", + "version": "1.15.0", + "version_normalized": "1.15.0.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/metatag.git", - "reference": "8.x-1.14" + "reference": "8.x-1.15" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/metatag-8.x-1.14.zip", - "reference": "8.x-1.14", - "shasum": "9bf9f1517ad015d0c93ca1460e284c557624aa90" + "url": "https://ftp.drupal.org/files/projects/metatag-8.x-1.15.zip", + "reference": "8.x-1.15", + "shasum": "7658d7286fdc075ea72a6ec36aea737b1182b5d8" }, "require": { "drupal/core": "^8 || ^9", @@ -6229,8 +6233,8 @@ "type": "drupal-module", "extra": { "drupal": { - "version": "8.x-1.14", - "datestamp": "1597183852", + "version": "8.x-1.15", + "datestamp": "1607188979", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -7338,18 +7342,18 @@ }, { "name": "drupal/search_api", - "version": "1.17.0", - "version_normalized": "1.17.0.0", + "version": "1.18.0", + "version_normalized": "1.18.0.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/search_api.git", - "reference": "8.x-1.17" + "reference": "8.x-1.18" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/search_api-8.x-1.17.zip", - "reference": "8.x-1.17", - "shasum": "a9f3352f1c8c893c7032c11f00a23f76b9733b56" + "url": "https://ftp.drupal.org/files/projects/search_api-8.x-1.18.zip", + "reference": "8.x-1.18", + "shasum": "6cf1d6820ba55891e204bac40b6031ed15db482a" }, "require": { "drupal/core": "^8.8 || ^9" @@ -7370,8 +7374,8 @@ "type": "drupal-module", "extra": { "drupal": { - "version": "8.x-1.17", - "datestamp": "1591128369", + "version": "8.x-1.18", + "datestamp": "1605204423", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -7386,7 +7390,7 @@ "installation-source": "dist", "notification-url": "https://packages.drupal.org/8/downloads", "license": [ - "GPL-2.0+" + "GPL-2.0-or-later" ], "authors": [ { @@ -7412,17 +7416,17 @@ }, { "name": "drupal/search_api_db", - "version": "1.17.0", - "version_normalized": "1.17.0.0", + "version": "1.18.0", + "version_normalized": "1.18.0.0", "require": { "drupal/core": "^8.8 || ^9", - "drupal/search_api": "self.version" + "drupal/search_api": "*" }, "type": "metapackage", "extra": { "drupal": { - "version": "8.x-1.17", - "datestamp": "1591128369", + "version": "8.x-1.18", + "datestamp": "1605204423", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -7561,18 +7565,18 @@ }, { "name": "drupal/simple_sitemap", - "version": "3.7.0", - "version_normalized": "3.7.0.0", + "version": "3.8.0", + "version_normalized": "3.8.0.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/simple_sitemap.git", - "reference": "8.x-3.7" + "reference": "8.x-3.8" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/simple_sitemap-8.x-3.7.zip", - "reference": "8.x-3.7", - "shasum": "7b23930cc71d37f332c1e836bc18c29ed2ae8cde" + "url": "https://ftp.drupal.org/files/projects/simple_sitemap-8.x-3.8.zip", + "reference": "8.x-3.8", + "shasum": "55252af261fbd7b18c230ab429eb7d8b92f4c66a" }, "require": { "drupal/core": "^8 || ^9", @@ -7581,8 +7585,8 @@ "type": "drupal-module", "extra": { "drupal": { - "version": "8.x-3.7", - "datestamp": "1592298918", + "version": "8.x-3.8", + "datestamp": "1605141357", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -7605,6 +7609,10 @@ "homepage": "https://www.drupal.org/u/gbyte.co", "email": "contact@gbyte.co", "role": "Maintainer" + }, + { + "name": "gbyte", + "homepage": "https://www.drupal.org/user/2381352" } ], "description": "Creates a standard conform hreflang XML sitemap of the site content and provides a framework for developing other sitemap types.", diff --git a/web/modules/dropzonejs/.editorconfig b/web/modules/dropzonejs/.editorconfig new file mode 100644 index 0000000000000000000000000000000000000000..7fc6757614dad0faca00ffb9a8e13bde89441cdd --- /dev/null +++ b/web/modules/dropzonejs/.editorconfig @@ -0,0 +1,5 @@ +# @see http://editorconfig.org/ + +[composer.libraries.json] +indent_size = 4 +indent_style = space diff --git a/web/modules/dropzonejs/README.md b/web/modules/dropzonejs/README.md index 1bc517ef7919e80d8256c18c9d0de0e95ab76f4e..7af1a12dc59bfee3612fa6a40f7d9ade092cf7fe 100644 --- a/web/modules/dropzonejs/README.md +++ b/web/modules/dropzonejs/README.md @@ -1,35 +1,82 @@ -## About DropzoneJS +# About DropzoneJS -This is the Drupal 8 integration for [DropzoneJS](http://www.dropzonejs.com/). +This is the Drupal integration for [DropzoneJS](http://www.dropzonejs.com/). + +### How to install + +#### The non-composer way -###How to install: 1. Download this module 2. [Download DropzoneJS](https://github.com/enyo/dropzone) and place it in the libraries folder -3. Install dropzonejs the [usual way](https://www.drupal.org/documentation/install/modules-themes/modules-8) +3. Install dropzonejs the [usual way](https://www.drupal.org/docs/extending-drupal/installing-drupal-modules) 4. Remove "test" folder from libraries folder as it could constitute a security risk to your site. See http://drupal.org/node/1189632 for more info. You will now have a dropzonejs element at your disposal. -###Future plans: +#### The composer way 1 + +Run `composer require wikimedia/composer-merge-plugin` + +Update the root `composer.json` file. For example: + +``` + "extra": { + "merge-plugin": { + "include": [ + "web/modules/contrib/dropzonejs/composer.libraries.json" + ] + } + } +``` + +Run `composer require drupal/dropzonejs enyo/dropzone`, the DropzoneJS library will be +installed to the `libraries` folder automatically. + +#### The composer way 2 + +Copy the following into the root `composer.json` file's `repository` key + +``` + "repositories": [ + { + "type": "package", + "package": { + "name": "enyo/dropzone", + "version": "5.7.1", + "type": "drupal-library", + "dist": { + "url": "https://github.com/enyo/dropzone/archive/v5.7.1.zip", + "type": "zip" + } + } + } + ] +``` + +Run `composer require drupal/dropzonejs enyo/dropzone`, the DropzoneJS library +will be installed to the `libraries` folder automatically as well. + +### Future plans: - A dropzonejs field widget. - Handling already uploaded files. - Handling other types of upload validations (min/max resolution, min size,...) - Removing files that were removed by the user on first upload from temp storage. -###Project page: +### Project page: [drupal.org project page](https://www.drupal.org/project/dropzonejs) -###Maintainers: +### Maintainers: + Janez Urevc (@slashrsm) drupal.org/u/slashrsm + John McCormick (@neardark) drupal.org/u/neardark + Primoz Hmeljak (@primsi) drupal.org/u/Primsi ++ Qiangjun Ran (@jungle) drupal.org/u/jungle -###Get in touch: +### Get in touch: - http://groups.drupal.org/media - - IRC: #drupal-media @ Freenode - -###Thanks: + - **#media**: http://drupal.slack.com + +### Thanks: The development of this module is sponsored by [Examiner.com](http://www.examiner.com) Thanks also to [NYC CAMP](http://nyccamp.org/) that hosted media sprints. diff --git a/web/modules/dropzonejs/composer.json b/web/modules/dropzonejs/composer.json index 975a82d2dc95dbb49194ec8009c85cc2e73cc268..36025f3af3af02f9dfd91a737dd3d65c85dda5bd 100644 --- a/web/modules/dropzonejs/composer.json +++ b/web/modules/dropzonejs/composer.json @@ -1,41 +1,49 @@ { - "name": "drupal/dropzonejs", - "description": "Drupal integration for DropzoneJS - An open source library that provides drag’n’drop file uploads with image previews.", - "type": "drupal-module", - "homepage": "https://www.drupal.org/project/dropzonejs", - "keywords": ["Drupal", "DropzoneJS"], - "license": "GPL-2.0+", - "minimum-stability": "dev", - "prefer-stable": true, - "support": { - "issues": "https://www.drupal.org/project/issues/dropzonejs", - "irc": "irc://irc.freenode.org/drupal-contribute", - "source": "https://www.drupal.org/project/dropzonejs" - }, - "authors": [ - { - "name": "Janez Urevc", - "homepage": "https://drupal.org/u/slashrsm", - "role": "Maintainer" + "name": "drupal/dropzonejs", + "description": "Drupal integration for DropzoneJS - An open source library that provides drag’n’drop file uploads with image previews.", + "type": "drupal-module", + "homepage": "https://www.drupal.org/project/dropzonejs", + "keywords": ["Drupal", "DropzoneJS"], + "license": "GPL-2.0-or-later", + "minimum-stability": "dev", + "prefer-stable": true, + "support": { + "issues": "https://www.drupal.org/project/issues/dropzonejs", + "#media": "http://drupal.slack.com", + "source": "https://www.drupal.org/project/dropzonejs" }, - { - "name": "Christian Fritsch", - "homepage": "https://drupal.org/u/chrfritsch", - "role": "Maintainer" + "authors": [ + { + "name": "Janez Urevc", + "homepage": "https://drupal.org/u/slashrsm", + "role": "Maintainer" + }, + { + "name": "Christian Fritsch", + "homepage": "https://drupal.org/u/chrfritsch", + "role": "Maintainer" + }, + { + "name": "Primoz Hmeljak", + "homepage": "https://drupal.org/u/Primsi", + "role": "Maintainer" + }, + { + "name": "Qiangjun Ran", + "homepage": "https://drupal.org/u/jungle", + "role": "Maintainer" + }, + { + "name": "See other contributors", + "homepage": "https://www.drupal.org/node/1998478/committers", + "role": "contributor" + } + ], + "require": {}, + "require-dev": { + "drupal/entity_browser": "^2.5" }, - { - "name": "Primoz Hmeljak", - "homepage": "https://drupal.org/u/Primsi", - "role": "Maintainer" - }, - { - "name": "See other contributors", - "homepage": "https://www.drupal.org/node/1998478/committers", - "role": "contributor" + "suggest": { + "enyo/dropzone": "Required to use drupal/dropzonejs. DropzoneJS is an open source library that provides drag’n’drop file uploads with image previews." } - ], - "require": {}, - "suggest": { - "enyo/dropzone": "Required to user drupal/dropzonejs. Dropzone is an easy to use drag'n'drop library." - } } diff --git a/web/modules/dropzonejs/composer.libraries.json b/web/modules/dropzonejs/composer.libraries.json new file mode 100644 index 0000000000000000000000000000000000000000..7f82b5e1d07fe76f04edf51669b7ad80c2092646 --- /dev/null +++ b/web/modules/dropzonejs/composer.libraries.json @@ -0,0 +1,19 @@ +{ + "repositories": { + "enyo/dropzone": { + "type": "package", + "package": { + "name": "enyo/dropzone", + "version": "5.7.1", + "type": "drupal-library", + "dist": { + "url": "https://github.com/enyo/dropzone/archive/v5.7.1.zip", + "type": "zip" + } + } + } + }, + "require": { + "enyo/dropzone": "5.7.1" + } +} diff --git a/web/modules/dropzonejs/config/install/dropzonejs.settings.yml b/web/modules/dropzonejs/config/install/dropzonejs.settings.yml index 940ac0d681af049e47bf5f0a4b8c14769a69df46..5a2b111f042e5109c08c64ab5ed298c28d1b1410 100644 --- a/web/modules/dropzonejs/config/install/dropzonejs.settings.yml +++ b/web/modules/dropzonejs/config/install/dropzonejs.settings.yml @@ -1,2 +1,3 @@ tmp_upload_scheme: temporary filename_transliteration: true +upload_timeout_ms: 0 diff --git a/web/modules/dropzonejs/config/schema/dropzonejs.schema.yml b/web/modules/dropzonejs/config/schema/dropzonejs.schema.yml index ab7542039897c73659591b9d2d5c48e1a7f0f66e..587ccae177a06562fc01b43f2f24e5dc81dc55bc 100644 --- a/web/modules/dropzonejs/config/schema/dropzonejs.schema.yml +++ b/web/modules/dropzonejs/config/schema/dropzonejs.schema.yml @@ -8,3 +8,6 @@ dropzonejs.settings: filename_transliteration: type: boolean label: 'Transliterate names of uploaded files' + upload_timeout_ms: + type: integer + label: 'Upload timeout' diff --git a/web/modules/dropzonejs/dropzonejs.info.yml b/web/modules/dropzonejs/dropzonejs.info.yml index e246dac93d992a737a7d5255869faaedf32263e4..25f528f95958870969901534b81b748629ccb2ba 100644 --- a/web/modules/dropzonejs/dropzonejs.info.yml +++ b/web/modules/dropzonejs/dropzonejs.info.yml @@ -6,7 +6,7 @@ package: Media dependencies: - drupal:file -# Information added by Drupal.org packaging script on 2020-03-31 -version: '8.x-2.1' +# Information added by Drupal.org packaging script on 2020-09-17 +version: '8.x-2.3' project: 'dropzonejs' -datestamp: 1585662326 +datestamp: 1600310563 diff --git a/web/modules/dropzonejs/dropzonejs.install b/web/modules/dropzonejs/dropzonejs.install index 679b46c94174eed465a23568f451c8125d653b18..17f9f8839d644cec14de42df17f2023e555cc905 100644 --- a/web/modules/dropzonejs/dropzonejs.install +++ b/web/modules/dropzonejs/dropzonejs.install @@ -72,3 +72,14 @@ function dropzonejs_update_8002() { $config->set('filename_transliteration', TRUE); $config->save(TRUE); } + +/** + * Set default value for upload timeout. + */ +function dropzonejs_update_8003() { + $config_factory = \Drupal::configFactory(); + $config = $config_factory->getEditable('dropzonejs.settings'); + $config->clear('upload_timeout_ms'); + $config->set('upload_timeout_ms', 0); + $config->save(TRUE); +} diff --git a/web/modules/dropzonejs/modules/eb_widget/dropzonejs_eb_widget.info.yml b/web/modules/dropzonejs/modules/eb_widget/dropzonejs_eb_widget.info.yml index c691dda5ffed3263991e1be8493d99544cc3b8f6..f803a34dc837b6a60c49a51c2b6564f45cb383f7 100644 --- a/web/modules/dropzonejs/modules/eb_widget/dropzonejs_eb_widget.info.yml +++ b/web/modules/dropzonejs/modules/eb_widget/dropzonejs_eb_widget.info.yml @@ -9,7 +9,7 @@ dependencies: - entity_browser:entity_browser -# Information added by Drupal.org packaging script on 2020-03-31 -version: '8.x-2.1' +# Information added by Drupal.org packaging script on 2020-09-17 +version: '8.x-2.3' project: 'dropzonejs' -datestamp: 1585662326 +datestamp: 1600310563 diff --git a/web/modules/dropzonejs/modules/eb_widget/src/Plugin/EntityBrowser/Widget/DropzoneJsEbWidget.php b/web/modules/dropzonejs/modules/eb_widget/src/Plugin/EntityBrowser/Widget/DropzoneJsEbWidget.php index 1b4b49b7912dc03df770c5dfb2586e27f354a223..0c18472d48718d56e0d2f9ae4f68440b03b5005c 100644 --- a/web/modules/dropzonejs/modules/eb_widget/src/Plugin/EntityBrowser/Widget/DropzoneJsEbWidget.php +++ b/web/modules/dropzonejs/modules/eb_widget/src/Plugin/EntityBrowser/Widget/DropzoneJsEbWidget.php @@ -603,4 +603,18 @@ public static function handleAjaxCommand(array $form, FormStateInterface $form_s return $ajax; } + /** + * {@inheritdoc} + */ + protected function handleWidgetContext($widget_context) { + parent::handleWidgetContext($widget_context); + $validators = isset($widget_context['upload_validators']) ? $widget_context['upload_validators'] : []; + if (isset($validators['file_validate_size'])) { + $this->configuration['max_filesize'] = $validators['file_validate_size'][0]; + } + if (isset($validators['file_validate_extensions'])) { + $this->configuration['extensions'] = $validators['file_validate_extensions'][0]; + } + } + } diff --git a/web/modules/dropzonejs/modules/eb_widget/src/Plugin/EntityBrowser/Widget/MediaEntityDropzoneJsEbWidget.php b/web/modules/dropzonejs/modules/eb_widget/src/Plugin/EntityBrowser/Widget/MediaEntityDropzoneJsEbWidget.php index 34a12b09d0cee273a36b67db4bf4619a97727b53..c50c568992af110a09d1d767912dfedee175b095 100644 --- a/web/modules/dropzonejs/modules/eb_widget/src/Plugin/EntityBrowser/Widget/MediaEntityDropzoneJsEbWidget.php +++ b/web/modules/dropzonejs/modules/eb_widget/src/Plugin/EntityBrowser/Widget/MediaEntityDropzoneJsEbWidget.php @@ -104,6 +104,11 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta ]); } + // Remove these config options as these are propagated from the field. + $form['max_filesize']['#access'] = FALSE; + $form['extensions']['#access'] = FALSE; + $form['upload_location']['#access'] = FALSE; + return $form; } @@ -167,4 +172,18 @@ public function submit(array &$element, array &$form, FormStateInterface $form_s $this->clearFormValues($element, $form_state); } + /** + * {@inheritdoc} + */ + protected function handleWidgetContext($widget_context) { + parent::handleWidgetContext($widget_context); + $bundle = $this->getType(); + $source = $bundle->getSource(); + $field = $source->getSourceFieldDefinition($bundle); + $field_storage = $field->getFieldStorageDefinition(); + $this->configuration['upload_location'] = $field_storage->getSettings()['uri_scheme'] . '://' . $field->getSettings()['file_directory']; + $this->configuration['max_filesize'] = $field->getSettings()['max_filesize']; + $this->configuration['extensions'] = $field->getSettings()['file_extensions']; + } + } diff --git a/web/modules/dropzonejs/src/Controller/UploadController.php b/web/modules/dropzonejs/src/Controller/UploadController.php index fb08090c047f5d1adfd2bb358fa07de9d2c8a5fe..6e9b588defaa82ea9a1f57475fda56e5150e4eb3 100644 --- a/web/modules/dropzonejs/src/Controller/UploadController.php +++ b/web/modules/dropzonejs/src/Controller/UploadController.php @@ -55,7 +55,7 @@ public static function create(ContainerInterface $container) { } /** - * Handles DropzoneJs uploads. + * Handles DropzoneJS uploads. */ public function handleUploads() { $file = $this->request->files->get('file'); @@ -65,10 +65,12 @@ public function handleUploads() { // @todo: Implement file_validate_size(); try { + /* @var \Drupal\Core\File\FileSystem $file_system */ + $file_system = \Drupal::service('file_system'); // Return JSON-RPC response. return new AjaxResponse([ 'jsonrpc' => '2.0', - 'result' => basename($this->uploadHandler->handleUpload($file)), + 'result' => $file_system->basename($this->uploadHandler->handleUpload($file)), 'id' => 'id', ]); } diff --git a/web/modules/dropzonejs/src/DropzoneJsUploadSaveInterface.php b/web/modules/dropzonejs/src/DropzoneJsUploadSaveInterface.php index 9d5672c0fe895dfa40df43385dfd9dadf31c381c..2a1f1a7c1c7b9ecf768b6c16b0abcc75b05d2736 100644 --- a/web/modules/dropzonejs/src/DropzoneJsUploadSaveInterface.php +++ b/web/modules/dropzonejs/src/DropzoneJsUploadSaveInterface.php @@ -6,7 +6,7 @@ use Drupal\file\FileInterface; /** - * Provides an interface for classes that save DropzoneJs uploads. + * Provides an interface for classes that save DropzoneJS uploads. */ interface DropzoneJsUploadSaveInterface { diff --git a/web/modules/dropzonejs/src/Element/DropzoneJs.php b/web/modules/dropzonejs/src/Element/DropzoneJs.php index 11aedb1c3b12b8ea00c89676a29d8d1cff42955f..48d24c3c4e749c6c632707790fccd641fa8146fd 100644 --- a/web/modules/dropzonejs/src/Element/DropzoneJs.php +++ b/web/modules/dropzonejs/src/Element/DropzoneJs.php @@ -23,7 +23,7 @@ * Will be visible inside the upload area. * - #max_filesize (string) * Used by dropzonejs and expressed in number + unit (i.e. 1.1M) This will be - * converted to a form that DropzoneJs understands. See: + * converted to a form that DropzoneJS understands. See: * http://www.dropzonejs.com/#config-maxFilesize * - #extensions (string) * A string of valid extensions separated by a space. @@ -138,6 +138,7 @@ public static function preRenderDropzoneJs(array $element) { 'dictDefaultMessage' => Html::escape($element['#dropzone_description']), 'acceptedFiles' => '.' . str_replace(' ', ',.', self::getValidExtensions($element)), 'maxFiles' => $element['#max_files'], + 'timeout' => \Drupal::configFactory()->get('dropzonejs.settings')->get('upload_timeout_ms'), ], ], ]; diff --git a/web/modules/dropzonejs/tests/modules/dropzonejs_test/dropzonejs_test.info.yml b/web/modules/dropzonejs/tests/modules/dropzonejs_test/dropzonejs_test.info.yml index ba9f5b3259dc9e0edc2c7fbbe8a96047962ef9a0..ebe917b3029bc8c804dc8d219341b8f0720179c6 100644 --- a/web/modules/dropzonejs/tests/modules/dropzonejs_test/dropzonejs_test.info.yml +++ b/web/modules/dropzonejs/tests/modules/dropzonejs_test/dropzonejs_test.info.yml @@ -1,6 +1,7 @@ -name: 'DropzoneJs Test' +name: 'DropzoneJS Test' type: module -description: 'Support module for DropzoneJs tests.' +description: 'Support module for DropzoneJS tests.' +core_version_requirement: ^8.8 || ^9 package: Testing # version: VERSION dependencies: @@ -8,7 +9,7 @@ dependencies: - dropzonejs:dropzonejs - dropzonejs:dropzonejs_eb_widget -# Information added by Drupal.org packaging script on 2020-03-31 -version: '8.x-2.1' +# Information added by Drupal.org packaging script on 2020-09-17 +version: '8.x-2.3' project: 'dropzonejs' -datestamp: 1585662326 +datestamp: 1600310563 diff --git a/web/modules/dropzonejs/tests/modules/dropzonejs_test/src/Form/DropzoneJsTestForm.php b/web/modules/dropzonejs/tests/modules/dropzonejs_test/src/Form/DropzoneJsTestForm.php index 841d6b718b2f80d76c3418cc9071a385c2361e19..867f9c8a5938e5b5387a5a5299c6fa2f8a535ea0 100644 --- a/web/modules/dropzonejs/tests/modules/dropzonejs_test/src/Form/DropzoneJsTestForm.php +++ b/web/modules/dropzonejs/tests/modules/dropzonejs_test/src/Form/DropzoneJsTestForm.php @@ -22,10 +22,10 @@ public function getFormId() { */ public function buildForm(array $form, FormStateInterface $form_state) { $form['dropzonejs'] = [ - '#title' => $this->t('DropzoneJs element'), + '#title' => $this->t('DropzoneJS element'), '#type' => 'dropzonejs', '#required' => TRUE, - '#dropzone_description' => 'DropzoneJs description', + '#dropzone_description' => 'DropzoneJS description', '#max_filesize' => '1M', '#extensions' => 'jpg png', ]; diff --git a/web/modules/dropzonejs/tests/src/Kernel/DropzoneJsElementTest.php b/web/modules/dropzonejs/tests/src/Kernel/DropzoneJsElementTest.php index 7a8e55482ccbf8c8585d643c43325e326b12ee77..98ac71d2a612fa0b52800a39193acf7e9348594d 100644 --- a/web/modules/dropzonejs/tests/src/Kernel/DropzoneJsElementTest.php +++ b/web/modules/dropzonejs/tests/src/Kernel/DropzoneJsElementTest.php @@ -7,9 +7,9 @@ use Drupal\user\RoleInterface; /** - * Tests related to the dropzoneJs element. + * Tests related to the DropzoneJS element. * - * @group DropzoneJs + * @group dropzonejs */ class DropzoneJsElementTest extends KernelTestBase { @@ -49,8 +49,8 @@ public function testDropzoneJsElement() { $xpath_base = "//div[contains(@class, 'form-item-dropzonejs')]"; // Label. - $this->assertEmpty($this->xpath("$xpath_base/label[text()='Not DropzoneJs element']")); - $this->assertNotEmpty($this->xpath("$xpath_base/label[text()='DropzoneJs element']")); + $this->assertEmpty($this->xpath("$xpath_base/label[text()='Not DropzoneJS element']")); + $this->assertNotEmpty($this->xpath("$xpath_base/label[text()='DropzoneJS element']")); // Element where dropzonejs is attached to. $this->assertNotEmpty($this->xpath("$xpath_base/div[contains(@class, 'dropzone-enable')]")); // Uploaded files input. diff --git a/web/modules/dropzonejs/tests/src/Kernel/DropzoneJsUploadControllerTest.php b/web/modules/dropzonejs/tests/src/Kernel/DropzoneJsUploadControllerTest.php index f1bc6bc5a4b73119d5718a1545e874b56293ef03..27cf67eab307bfedbe108a2e899eb38d927ce0b5 100644 --- a/web/modules/dropzonejs/tests/src/Kernel/DropzoneJsUploadControllerTest.php +++ b/web/modules/dropzonejs/tests/src/Kernel/DropzoneJsUploadControllerTest.php @@ -11,9 +11,9 @@ use Drupal\language\Entity\ConfigurableLanguage; /** - * Tests dropzoneJs upload controller. + * Tests DropzoneJS upload controller. * - * @group DropzoneJs + * @group dropzonejs */ class DropzoneJsUploadControllerTest extends KernelTestBase { @@ -43,7 +43,7 @@ class DropzoneJsUploadControllerTest extends KernelTestBase { * * @var string */ - protected $testfileData = 'DropzoneJs test file data'; + protected $testfileData = 'DropzoneJS test file data'; /** * Modules to enable. @@ -68,7 +68,7 @@ protected function setUp() { } /** - * Test that dropzoneJs correctly handles uploads. + * Test that DropzoneJS correctly handles uploads. */ public function testDropzoneJsUploadController() { $this->container->get('router.builder')->rebuild(); @@ -99,7 +99,7 @@ public function testDropzoneJsUploadController() { } /** - * Tests that dropzoneJs ignores filename transliteration. + * Tests that DropzoneJS ignores filename transliteration. */ public function testIgnoreTransliteration() { $this->container->get('router.builder')->rebuild(); diff --git a/web/modules/libraries/CHANGELOG.txt b/web/modules/libraries/CHANGELOG.txt index 6d1daa8351781ca92e24b008ece84bc2a2e27539..e0785a691d43621b75f2434513e4b01ef8e47058 100644 --- a/web/modules/libraries/CHANGELOG.txt +++ b/web/modules/libraries/CHANGELOG.txt @@ -1,6 +1,7 @@ Libraries 8.x-3.x, xxxx-xx-xx ----------------------------- +#2882709 by Kingdutch: Fix "LibraryIdAccessorInterface" name #2833756 by 20th: Check that public://library-definitions directory does not exist #2825940 by tstoeckler, rjacobs: Remove LibrariesServiceProvider #2816115 by rjacobs: Remove ExtensionHandler service diff --git a/web/modules/libraries/libraries.drush.inc b/web/modules/libraries/libraries.drush.inc index 22b7d62da06a06d4e827d689126d42c61a5e25f1..3d9f7cd429584aee94f5843f5311fa9408ca87c7 100644 --- a/web/modules/libraries/libraries.drush.inc +++ b/web/modules/libraries/libraries.drush.inc @@ -62,8 +62,8 @@ function libraries_drush_invalidate_cache() { */ function libraries_drush_list() { $libraries = array(); - foreach (libraries_info() as $name => $info) { - $libraries[$name] = libraries_detect($name); + foreach (\Drupal::service('libraries.manager')->info() as $name => $info) { + $libraries[$name] = \Drupal::service('libraries.manager')->getLibrary($name); } ksort($libraries); diff --git a/web/modules/libraries/libraries.info.yml b/web/modules/libraries/libraries.info.yml index 02136222c77638e32a78040d0087cc6853c16d30..68ea958a3ffe6b11eabfa45db73c68f393b68355 100644 --- a/web/modules/libraries/libraries.info.yml +++ b/web/modules/libraries/libraries.info.yml @@ -1,10 +1,10 @@ name: Libraries type: module description: Allows version-dependent and shared usage of external libraries. -# core: 8.x +core: 8.x +core_version_requirement: ^8 || ^9 -# Information added by Drupal.org packaging script on 2018-01-27 -version: '8.x-3.0-alpha1' -core: '8.x' +# Information added by Drupal.org packaging script on 2020-12-16 +version: '8.x-3.0-alpha6' project: 'libraries' -datestamp: 1517046488 +datestamp: 1608099126 diff --git a/web/modules/libraries/libraries.install b/web/modules/libraries/libraries.install index a2edea4928282ff1d92f9450917fe12d6f584911..666f84ecea70285d621e3e36dd7d245ab7d3b5db 100644 --- a/web/modules/libraries/libraries.install +++ b/web/modules/libraries/libraries.install @@ -23,6 +23,6 @@ function libraries_install() { */ function libraries_uninstall() { if (is_dir('public://library-definitions')) { - file_unmanaged_delete_recursive('public://library-definitions'); + \Drupal::service('file_system')->deleteRecursive('public://library-definitions'); } } diff --git a/web/modules/libraries/libraries.module b/web/modules/libraries/libraries.module index 3006c345ceb1716bce03c98045f108430fb6a39f..7028d2f9777d097f0814f2a75730a931c5b145db 100644 --- a/web/modules/libraries/libraries.module +++ b/web/modules/libraries/libraries.module @@ -11,6 +11,7 @@ use Drupal\libraries\ExternalLibrary\Utility\LibraryAccessorInterface; use Drupal\libraries\ExternalLibrary\Utility\LibraryIdAccessorInterface; use Symfony\Component\Yaml\Parser; +use Drupal\Core\Extension\Dependency; /** * Implements hook_library_info_build(). @@ -115,7 +116,7 @@ function libraries_get_libraries() { // Similar to 'modules' and 'themes' directories inside an installation // profile, installation profiles may want to place libraries into a // 'libraries' directory. - if ($profile = drupal_get_profile()) { + if ($profile = \Drupal::installProfile()) { $profile_path = drupal_get_path('profile', $profile); $searchdir[] = "$profile_path/libraries"; }; @@ -167,7 +168,7 @@ function libraries_get_libraries() { * https://www.drupal.org/node/2170763 */ function libraries_scan_info_files() { - $profile = drupal_get_path('profile', drupal_get_profile()); + $profile = drupal_get_path('profile', \Drupal::installProfile()); $config = DrupalKernel::findSitePath(\Drupal::request()); // Build a list of directories. @@ -181,7 +182,7 @@ function libraries_scan_info_files() { $files = array(); foreach ($directories as $dir) { if (file_exists($dir)) { - $files = array_merge($files, file_scan_directory($dir, '@^[a-z0-9._-]+\.libraries\.info\.yml$@', array( + $files = array_merge($files, \Drupal::service('file_system')->scanDirectory($dir, '@^[a-z0-9._-]+\.libraries\.info\.yml$@', array( 'key' => 'name', 'recurse' => FALSE, ))); @@ -345,9 +346,9 @@ function libraries_prepare_files(&$library, $version = NULL, $variant = NULL) { function libraries_detect_dependencies(&$library, $version = NULL, $variant = NULL) { if (isset($library['dependencies'])) { foreach ($library['dependencies'] as &$dependency_string) { - $dependency_info = ModuleHandler::parseDependency($dependency_string); - $dependency = libraries_detect($dependency_info['name']); - if (!$dependency['installed']) { + $dependency = Dependency::createFromString($dependency_string); + $info = libraries_detect($dependency->getName()); + if (!$info['installed']) { $library['installed'] = FALSE; $library['error'] = 'missing dependency'; $library['error message'] = t('The %dependency library, which the %library library depends on, is not installed.', array( @@ -355,19 +356,19 @@ function libraries_detect_dependencies(&$library, $version = NULL, $variant = NU '%library' => $library['name'], )); } - elseif (drupal_check_incompatibility($dependency_info, $dependency['version'])) { + elseif (!$dependency->isCompatible($info['version'])) { $library['installed'] = FALSE; $library['error'] = 'incompatible dependency'; $library['error message'] = t('The version %dependency_version of the %dependency library is not compatible with the %library library.', array( - '%dependency_version' => $dependency['version'], - '%dependency' => $dependency['name'], + '%dependency_version' => $info['version'], + '%dependency' => $info['name'], '%library' => $library['name'], )); } // Remove the version string from the dependency, so libraries_load() can // load the libraries directly. - $dependency_string = $dependency_info['name']; + $dependency_string = $info['name']; } } } diff --git a/web/modules/libraries/src/ExternalLibrary/Exception/LibraryTypeNotFoundException.php b/web/modules/libraries/src/ExternalLibrary/Exception/LibraryTypeNotFoundException.php index b5655bba09a626a1d7b73a60b2971a1fc19c5a83..4b382837f0ca308a3557b445d762b37a76776155 100644 --- a/web/modules/libraries/src/ExternalLibrary/Exception/LibraryTypeNotFoundException.php +++ b/web/modules/libraries/src/ExternalLibrary/Exception/LibraryTypeNotFoundException.php @@ -8,7 +8,7 @@ /** * Provides an exception for a library definition without a type declaration. */ -class LibraryTypeNotFoundException extends \RuntimeException implements LibraryAccessorInterface { +class LibraryTypeNotFoundException extends \RuntimeException implements LibraryIdAccessorInterface { use LibraryIdAccessorTrait; diff --git a/web/modules/libraries/src/ExternalLibrary/LibraryManager.php b/web/modules/libraries/src/ExternalLibrary/LibraryManager.php index 17a62236e292b2ddf0b2a71e3914bf97c080bb9e..6b92d9dc9c1d01ab62f6784214c99102515db4b9 100644 --- a/web/modules/libraries/src/ExternalLibrary/LibraryManager.php +++ b/web/modules/libraries/src/ExternalLibrary/LibraryManager.php @@ -61,7 +61,9 @@ public function getLibrary($id) { public function getRequiredLibraryIds() { $library_ids = []; foreach (['module', 'theme'] as $type) { - foreach (system_get_info($type) as $info) { + $service_id = 'extension.list.' . $type; + $extension_list = \Drupal::service($service_id); + foreach ($extension_list->getAllInstalledInfo() as $info) { if (isset($info['library_dependencies'])) { $library_ids = array_merge($library_ids, $info['library_dependencies']); } diff --git a/web/modules/libraries/src/ExternalLibrary/Utility/LibraryIdAccessorInterface.php b/web/modules/libraries/src/ExternalLibrary/Utility/LibraryIdAccessorInterface.php index 98928e9deccd67a28b7734199456885bf43087c0..1634382bce9605f1ba2ebaca29d1b50406da9ea9 100644 --- a/web/modules/libraries/src/ExternalLibrary/Utility/LibraryIdAccessorInterface.php +++ b/web/modules/libraries/src/ExternalLibrary/Utility/LibraryIdAccessorInterface.php @@ -5,7 +5,7 @@ /** * Provides an interface for classes giving access to a library ID. */ -interface LibraryAccessorIdInterface { +interface LibraryIdAccessorInterface { /** * Returns the ID of the library. diff --git a/web/modules/libraries/tests/example/example_info_file.libraries.info.yml b/web/modules/libraries/tests/example/example_info_file.libraries.info.yml index 529af045cf3d5cfce7fb74a9ce74dc98aaec9c88..d0ffc711a33096f3b68c6c0e2de3bf1186e9be38 100644 --- a/web/modules/libraries/tests/example/example_info_file.libraries.info.yml +++ b/web/modules/libraries/tests/example/example_info_file.libraries.info.yml @@ -1,8 +1,7 @@ # This is an example info file of a library used for testing purposes. name: Example info file -# Information added by Drupal.org packaging script on 2018-01-27 -version: '8.x-3.0-alpha1' -core: '8.x' +# Information added by Drupal.org packaging script on 2020-12-16 +version: '8.x-3.0-alpha6' project: 'libraries' -datestamp: 1517046488 +datestamp: 1608099126 diff --git a/web/modules/libraries/tests/modules/libraries_test/libraries_test.info.yml b/web/modules/libraries/tests/modules/libraries_test/libraries_test.info.yml index be54ba548ffeeb66d574293940fbcbdb06524972..f2bb897e8a092fcafba1d595314f8f8a9a62c0c5 100644 --- a/web/modules/libraries/tests/modules/libraries_test/libraries_test.info.yml +++ b/web/modules/libraries/tests/modules/libraries_test/libraries_test.info.yml @@ -1,7 +1,7 @@ name: Libraries test module type: module description: Tests library detection and loading. -# core: 8.x +core: 8.x dependencies: - libraries hidden: TRUE @@ -9,8 +9,7 @@ library_dependencies: - test_asset_library - test_asset_multiple_library -# Information added by Drupal.org packaging script on 2018-01-27 -version: '8.x-3.0-alpha1' -core: '8.x' +# Information added by Drupal.org packaging script on 2020-12-16 +version: '8.x-3.0-alpha6' project: 'libraries' -datestamp: 1517046488 +datestamp: 1608099126 diff --git a/web/modules/libraries/tests/modules/libraries_test/libraries_test.module b/web/modules/libraries/tests/modules/libraries_test/libraries_test.module index 1a30ebc1c3c78ffab668b0b0a848bb370a2307cd..af1a4ac39a4c01a76cd561e1362c11d76af49e02 100644 --- a/web/modules/libraries/tests/modules/libraries_test/libraries_test.module +++ b/web/modules/libraries/tests/modules/libraries_test/libraries_test.module @@ -6,6 +6,7 @@ */ use Drupal\Component\Utility\SafeMarkup; +use Drupal\Core\Messenger\MessengerTrait; /** * Implements hook_libraries_info(). @@ -461,7 +462,7 @@ function _libraries_test_callback(&$library, $version, $variant, $group) { // Only set the message for the top-level library to prevent confusing, // duplicate messages. if (!isset($version) && !isset($variant) && \Drupal::state()->get('libraries_test.cache', FALSE)) { - drupal_set_message(SafeMarkup::set("The <em>$group</em> callback group was invoked.")); + \Drupal::messenger()->addMessage(SafeMarkup::set("The <em>$group</em> callback group was invoked.")); } } diff --git a/web/modules/libraries/tests/modules/libraries_test/src/Controller/ExampleController.php b/web/modules/libraries/tests/modules/libraries_test/src/Controller/ExampleController.php index 217ebf2f8ab4028aff545bd7560818355f602957..522967c50bd11e170e77043a471b3d8f56833516 100644 --- a/web/modules/libraries/tests/modules/libraries_test/src/Controller/ExampleController.php +++ b/web/modules/libraries/tests/modules/libraries_test/src/Controller/ExampleController.php @@ -23,7 +23,7 @@ public static function create(ContainerInterface $container) { * more information. */ private function buildPage($library, $variant = NULL) { - libraries_load($library, $variant); + \Drupal::service('libraries.manager')->load($library, $variant); // JavaScript and CSS files can be checked directly by SimpleTest, so we only // need to manually check for PHP files. $output = ''; diff --git a/web/modules/metatag/CHANGELOG.txt b/web/modules/metatag/CHANGELOG.txt index bba6db1911a7bf34583a30d3395d98425630c12a..e5549ef09e1afdad0c6d23fcd1ba16fd8b3b7926 100644 --- a/web/modules/metatag/CHANGELOG.txt +++ b/web/modules/metatag/CHANGELOG.txt @@ -1,3 +1,44 @@ +Metatag 8.x-1.15, 2020-12-05 +---------------------------- +#3165112 by Wim Leers, tim.plunkett: Metatag migrations should be tagged + "Configuration". +#2782797 by DamienMcKenna, Rolf van de Krol, michaelpetri, mvwensen, mxr576, + Alex G: Allow each tag to have its own permission (merge Metatag Access). +#3180470 by SivaprasadC: Typos in couple of files. +#3169952 by Wim Leers, tim.plunkett: Derive metatag migrations by entity type + (and bundle). +#3171660 by richgerdes, DamienMcKenna: Migration Plugin Alter breaks with + commerce_migrate_commerce. +#3176513 by mglaman: \Drupal\metatag\MetatagManager::defaultTagsFromEntity + should not return null. +#3166569 by munish.kumar, yogeshmpawar, Dom.: Add Facebook verification meta + header. +#2994433 by DamienMcKenna, ChandeepKhosa, vishnukumar, Berdir, joakland, + pdenooijer, benstjohn, muranod: Automatically parse URLs from image field + tokens. +#3171302 by ankithashetty, DamienMcKenna: \Drupal calls should be avoided in + classes, use dependency injection instead. +#2996883 by rokzabukovec, mrinalini9, thejimbirch, DamienMcKenna, Nchase, + Schwarz Developing: Sorting the output of meta tags. +#3135365 by novchuk.v, mero.S: Replace assertions involving calls to empty() + with assertEmpty()/assertNotEmpty()/assertArrayNotHasKey(). +#2782797 by DamienMcKenna: Added missing $defaultTheme test variable. +#3159192 by heddn: Hide from translation system from unsupported entities. +#3175269 by mbovan, DamienMcKenna: Pass optional bubbleable metadata parameter + to generateRawElements to avoid issues with early rendering and JSON:API. +#3154416 by novchuk.v: Replace assert* involving an instanceof operator with + assertInstanceOf()/assertNotInstanceOf(). +#3170298 by thejimbirch: Provide link to Facebook Open Graph debugger. +#3136470 by novchuk.v: Replace assertEqual()/assertSame() with count() with + assertCount(). +#3108052 by Berdir, SpadXIII: metatag_get_default_tags() does not revert config + override language. +#2862747 by JeroenT, DanielVeza, rwohleb, joshua.boltz, henrikakselsen, + Vitalyos, Phil Wolstenholme: Tokens to access individual meta tag values. +#3154513 by rokzabukovec: Exclusive robots metatags (index, noindex, follow, + nofollow). + + Metatag 8.x-1.14, 2020-08-11 ---------------------------- #3129664 by phenaproxima: Fix failure in MetatagPageManagerTest. diff --git a/web/modules/metatag/README.txt b/web/modules/metatag/README.txt index 8aa13064c060db0d6683324d7312c7191a11cb1f..635559b50468a451c614af88c237bc74a535ca4d 100644 --- a/web/modules/metatag/README.txt +++ b/web/modules/metatag/README.txt @@ -258,7 +258,7 @@ Two migration processes are supported: 2. A custom migration using Migrate Plus [3] and possibly Migrate Tools [4]. This will require manually creating the meta tag fields and assigning a custom process plugin as the source for its data. For example, if the name - of the field is "field_meta_tags" the lines fron the "process" section of + of the field is "field_meta_tags" the lines from the "process" section of the migration yml file would need to look line the following: For migrating from Nodewords on D6: diff --git a/web/modules/metatag/metatag.info.yml b/web/modules/metatag/metatag.info.yml index 24672a84a3c77664f6bd218d002dc16b47d08f87..bf207cb89a8ab498844687bf86c3cb6bcc94f511 100644 --- a/web/modules/metatag/metatag.info.yml +++ b/web/modules/metatag/metatag.info.yml @@ -8,7 +8,7 @@ dependencies: - drupal:field - token:token -# Information added by Drupal.org packaging script on 2020-08-11 -version: '8.x-1.14' +# Information added by Drupal.org packaging script on 2020-12-05 +version: '8.x-1.15' project: 'metatag' -datestamp: 1597183854 +datestamp: 1607188982 diff --git a/web/modules/metatag/metatag.module b/web/modules/metatag/metatag.module index abd3e643730da3b95d8d4efcec5ef291af4f92ce..a5fc44b39782eb66fc6234a0e5854ae298dc8ce6 100644 --- a/web/modules/metatag/metatag.module +++ b/web/modules/metatag/metatag.module @@ -509,15 +509,20 @@ function metatag_get_default_tags($entity = NULL) { $metatag_manager = \Drupal::service('metatag.manager'); // Load config based on language. + $current_language = NULL; if ($entity !== NULL) { /** @var \Drupal\Core\Language\LanguageManagerInterface $language_manager */ $language_manager = \Drupal::languageManager(); + $current_language = $language_manager->getConfigOverrideLanguage(); $language_manager->setConfigOverrideLanguage($entity->language()); } // First we load global defaults. $metatags = $metatag_manager->getGlobalMetatags(); if (!$metatags) { + if ($current_language) { + $language_manager->setConfigOverrideLanguage($current_language); + } return NULL; } @@ -551,7 +556,11 @@ function metatag_get_default_tags($entity = NULL) { } } - return $metatags->get('tags'); + $tags = $metatags->get('tags'); + if ($current_language) { + $language_manager->setConfigOverrideLanguage($current_language); + } + return $tags; } /** @@ -571,7 +580,7 @@ function metatag_entity_base_field_info(EntityTypeInterface $entity_type) { // there's no point in supporting it. if (!empty($base_table) && $canonical_template_exists && !in_array($original_class, $classes_to_skip)) { $fields['metatag'] = BaseFieldDefinition::create('map') - ->setLabel(t('Metatags')) + ->setLabel(t('Metatags (Hidden field for JSON support)')) ->setDescription(t('The meta tags for the entity.')) ->setClass('\Drupal\metatag\Plugin\Field\MetatagEntityFieldItemList') ->setComputed(TRUE) @@ -829,8 +838,35 @@ function metatag_migration_plugins_alter(array &$definitions) { 'plugin' => 'd7_metatag_entities', 'source' => 'pseudo_metatag_entities', ]; - $definition['migration_dependencies']['optional'][] = 'd7_metatag_field'; - $definition['migration_dependencies']['optional'][] = 'd7_metatag_field_instance'; + $destination_plugin_parts = explode(':', $definition['destination']['plugin']); + $entity_destination_plugins = ['entity', 'entity_complete']; + $entity_type_id = in_array($destination_plugin_parts[0], $entity_destination_plugins, TRUE) ? + $destination_plugin_parts[1] : NULL; + $bundle_id = $definition['destination']['default_bundle'] ?? NULL; + + // When there are no bundle derivatives, make e.g. the d7_node_complete + // migration depend on: + // - d7_metatag_field:node + // - d7_metatag_field_instance:node + // - d7_field_instance_widget:node + // but if there is a bundle derivative such as d7_node_complete:article, + // then instead make it depend on: + // - d7_metatag_field:node + // - d7_metatag_field_instance:node:article + // - d7_field_instance_widget:node:article + // Either way, this matches the dependencies used by for example + // d7_node_complete, which has dependencies on d7_field_instance and + // d7_comment_field_instance to ensure correct migration order. + if ($bundle_id && isset($definitions["d7_metatag_field_instance:$entity_type_id:$bundle_id"])) { + $definition['migration_dependencies']['required'][] = "d7_metatag_field:$entity_type_id"; + $definition['migration_dependencies']['required'][] = "d7_metatag_field_instance:$entity_type_id:$bundle_id"; + $definition['migration_dependencies']['required'][] = "d7_metatag_field_instance_widget_settings:$entity_type_id:$bundle_id"; + } + elseif (isset($definitions["d7_metatag_field_instance:$entity_type_id"])) { + $definition['migration_dependencies']['required'][] = "d7_metatag_field:$entity_type_id"; + $definition['migration_dependencies']['required'][] = "d7_metatag_field_instance:$entity_type_id"; + $definition['migration_dependencies']['required'][] = "d7_metatag_field_instance_widget_settings:$entity_type_id"; + } } } } @@ -900,11 +936,18 @@ function _metatag_is_migration_plugin_supported(array $definition) { } } - // Only support content entity destinations. - $plugin_definition = \Drupal::service('plugin.manager.migrate.destination') - ->getDefinition($definition['destination']['plugin']); - $destination_plugin = DefaultFactory::getPluginClass($definition['destination']['plugin'], $plugin_definition); - if (!is_subclass_of($destination_plugin, EntityContentBase::class) && $destination_plugin !== EntityContentBase::class) { + // Only support content entity destinations. Protect against situations where + // the plugins haven't loaded yet, e.g. when using Commerce Migrate. + try { + $plugin_definition = \Drupal::service('plugin.manager.migrate.destination') + ->getDefinition($definition['destination']['plugin']); + $destination_plugin = DefaultFactory::getPluginClass($definition['destination']['plugin'], $plugin_definition); + if (!is_subclass_of($destination_plugin, EntityContentBase::class) && $destination_plugin !== EntityContentBase::class) { + return FALSE; + } + } + catch (\Drupal\Component\Plugin\Exception\PluginNotFoundException $e) { + // If the entity type doesn't exist, neither with the migration plugin. return FALSE; } diff --git a/web/modules/metatag/metatag.tokens.inc b/web/modules/metatag/metatag.tokens.inc new file mode 100644 index 0000000000000000000000000000000000000000..836bfd7e90eafef500731ae1f3977396fa143064 --- /dev/null +++ b/web/modules/metatag/metatag.tokens.inc @@ -0,0 +1,228 @@ +<?php + +/** + * @file + * Metatag token integration. + */ + +use Drupal\Component\Utility\Html; +use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\Core\Render\BubbleableMetadata; + +/** + * Implements hook_token_info(). + */ +function metatag_token_info() { + $info = []; + + $group_manager = \Drupal::service('plugin.manager.metatag.group'); + $tag_manager = \Drupal::service('plugin.manager.metatag.tag'); + $tag_definitions = $tag_manager->getDefinitions(); + + $info['types']['metatag'] = [ + 'name' => t('Metatags'), + 'description' => t('Tokens related to Metatags.'), + 'needs-data' => 'metatag', + ]; + + foreach ($tag_definitions as $tag_id => $tag_definition) { + $label = $tag_definition['label']; + $description = $tag_definition['description']; + $multiple = $tag_definition['multiple']; + $metatag_token_name = 'metatag-' . $tag_id; + + $group = $group_manager->getDefinition($tag_definition['group']); + if ($group) { + $label = $group['label'] . ': ' . $label; + } + + $info['tokens']['current-page']['metatag'] = [ + 'name' => t('Metatags'), + 'description' => t('Metatag values for the current page.'), + 'type' => 'metatag', + ]; + $info['tokens']['metatag'][$tag_id] = [ + 'name' => Html::escape($label), + 'description' => $description, + 'type' => $multiple ? "list<$metatag_token_name>" : $metatag_token_name, + ]; + + $info['types'][$metatag_token_name] = [ + 'name' => Html::escape($label), + 'description' => t('@label tokens.', ['@label' => Html::escape($label)]), + 'needs-data' => $metatag_token_name, + 'nested' => TRUE, + ]; + + // Tag list token type. + if ($multiple) { + $info['types']["list<$metatag_token_name>"] = [ + 'name' => t('List of @type values', ['@type' => Html::escape($label)]), + 'description' => t('Tokens for lists of @type values.', ['@type' => Html::escape($label)]), + 'needs-data' => "list<$metatag_token_name>", + 'nested' => TRUE, + ]; + + // Show a different token for each tag delta. + // Since we don't know how many there will be, we will just show 3. + for ($delta = 0; $delta < 3; $delta++) { + $info['tokens']["list<$metatag_token_name>"][$delta] = [ + 'name' => t('@type type with delta @delta', [ + '@type' => Html::escape($label), + '@delta' => $delta, + ]), + 'module' => 'token', + 'type' => $metatag_token_name, + ]; + } + } + } + + return $info; +} + +/** + * Implements hook_token_info_alter(). + */ +function metatag_token_info_alter(&$info) { + foreach (\Drupal::entityTypeManager()->getDefinitions() as $entity_type_id => $entity_type) { + if (!$entity_type->entityClassImplements(ContentEntityInterface::class)) { + continue; + } + + // Make sure a token type exists for this entity. + $token_type = \Drupal::service('token.entity_mapper')->getTokenTypeForEntityType($entity_type_id); + if (empty($token_type) || !isset($info['types'][$token_type])) { + continue; + } + + $fields = \Drupal::entityTypeManager()->getStorage('field_storage_config')->loadByProperties([ + 'entity_type' => $entity_type_id, + 'type' => 'metatag', + ]); + foreach ($fields as $field) { + $field_token_name = $token_type . '-' . $field->getName(); + $info['types'][$field_token_name] = [ + 'name' => Html::escape($field->getName()), + 'description' => t('@label tokens.', ['@label' => Html::escape($field->getName())]), + 'needs-data' => $field_token_name, + 'nested' => TRUE, + 'type' => 'metatag', + 'module' => 'metatag', + ]; + } + } +} + +/** + * Implements hook_tokens(). + */ +function metatag_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) { + $replacements = []; + + switch ($type) { + case 'current-page': + /** @var \Drupal\token\TokenInterface $token_service */ + $token_service = \Drupal::token(); + $metatag_tokens = $token_service->findWithPrefix($tokens, 'metatag'); + if (!empty($metatag_tokens) && metatag_is_current_route_supported()) { + // Add cache contexts to ensure this token functions on a per-path + // basis. + $bubbleable_metadata->addCacheContexts(['url.site']); + $replacements += $token_service->generate('metatag', $metatag_tokens, [], $options, $bubbleable_metadata); + } + break; + + case 'entity': + if (!empty($data['entity_type']) && !empty($data['entity']) && !empty($data['token_type'])) { + /* @var \Drupal\Core\Entity\ContentEntityInterface $entity */ + $entity = $data['entity']; + if (!($entity instanceof ContentEntityInterface)) { + return $replacements; + } + + $metatag_fields = []; + foreach ($tokens as $name => $original) { + $field_name = explode(':', $name)[0]; + + if ($entity->hasField($field_name) && $entity->get($field_name)->getFieldDefinition()->getType() === 'metatag') { + $metatag_fields[] = $field_name; + } + } + + if (!empty($metatag_fields)) { + /** @var \Drupal\token\TokenInterface $token_service */ + $token_service = \Drupal::token(); + $metatag_tokens = []; + foreach ($metatag_fields as $metatag_field) { + $metatag_tokens += $token_service->findWithPrefix($tokens, $metatag_field); + } + $replacements += $token_service->generate('metatag', $metatag_tokens, ['entity' => $entity], $options, $bubbleable_metadata); + } + } + break; + + case 'metatag': + $metatag_manager = \Drupal::service('metatag.manager'); + + $entity = $options['entity'] ?? metatag_get_route_entity(); + $tags = metatag_get_default_tags($entity); + if ($entity instanceof ContentEntityInterface) { + // If content entity does not have an ID the page is likely an "Add" + // page, so skip processing for entity which has not been created yet. + if (!$entity->id()) { + return NULL; + } + + $tags += $metatag_manager->tagsFromEntity($entity); + } + + // Trigger hook_metatags_alter(). + // Allow modules to override tags or the entity used for token + // replacements. + $context = [ + 'entity' => &$entity, + ]; + \Drupal::service('module_handler')->alter('metatags', $tags, $context); + + // If the entity was changed above, use that for generating the meta tags. + if (isset($context['entity'])) { + $entity = $context['entity']; + } + + $processed_tags = $metatag_manager->generateTokenValues($tags, $entity); + + foreach ($tokens as $name => $original) { + // For the [metatag:tag_name] token. + if (strpos($name, ':') === FALSE) { + $tag_name = $name; + } + // For [metatag:tag_name:0], [metatag:tag_name:0:value] and + // [metatag:tag_name:value] tokens. + else { + [$tag_name, $delta] = explode(':', $name, 2); + if (!is_numeric($delta)) { + unset($delta); + } + } + + // Replace dashes (-) with underscores (_) for e.g. canonical-url. + $tag_name = str_replace('-', '_', $tag_name); + + if (empty($processed_tags[$tag_name])) { + continue; + } + + // Render only one delta. + if (isset($delta)) { + $replacements[$original] = $processed_tags[$tag_name][$delta]; + } + else { + $replacements[$original] = is_array($processed_tags[$tag_name]) ? implode(',', $processed_tags[$tag_name]) : $processed_tags[$tag_name]; + } + } + break; + } + + return $replacements; +} diff --git a/web/modules/metatag/metatag_app_links/metatag_app_links.info.yml b/web/modules/metatag/metatag_app_links/metatag_app_links.info.yml index 492e7aa835aab533ed79b8a599b5e689cb4dc7c4..6953e156bb4a03221e267918d2dd51023e91b27d 100644 --- a/web/modules/metatag/metatag_app_links/metatag_app_links.info.yml +++ b/web/modules/metatag/metatag_app_links/metatag_app_links.info.yml @@ -6,7 +6,7 @@ package: SEO dependencies: - metatag:metatag -# Information added by Drupal.org packaging script on 2020-08-11 -version: '8.x-1.14' +# Information added by Drupal.org packaging script on 2020-12-05 +version: '8.x-1.15' project: 'metatag' -datestamp: 1597183854 +datestamp: 1607188982 diff --git a/web/modules/metatag/metatag_dc/metatag_dc.info.yml b/web/modules/metatag/metatag_dc/metatag_dc.info.yml index 8aab6e84e8a5d93bda94f5d69ecf4b352d73ff8f..59c782f66a0c5eafc3d9ccfab20a58c1b4b1cec1 100644 --- a/web/modules/metatag/metatag_dc/metatag_dc.info.yml +++ b/web/modules/metatag/metatag_dc/metatag_dc.info.yml @@ -6,7 +6,7 @@ package: SEO dependencies: - metatag:metatag -# Information added by Drupal.org packaging script on 2020-08-11 -version: '8.x-1.14' +# Information added by Drupal.org packaging script on 2020-12-05 +version: '8.x-1.15' project: 'metatag' -datestamp: 1597183854 +datestamp: 1607188982 diff --git a/web/modules/metatag/metatag_dc_advanced/metatag_dc_advanced.info.yml b/web/modules/metatag/metatag_dc_advanced/metatag_dc_advanced.info.yml index 9c2285ee222fb061ada4caa7120ceef7744b8f06..598e9c5073193eab84c7e532b443884968d5d95e 100644 --- a/web/modules/metatag/metatag_dc_advanced/metatag_dc_advanced.info.yml +++ b/web/modules/metatag/metatag_dc_advanced/metatag_dc_advanced.info.yml @@ -7,7 +7,7 @@ dependencies: - metatag:metatag - metatag:metatag_dc -# Information added by Drupal.org packaging script on 2020-08-11 -version: '8.x-1.14' +# Information added by Drupal.org packaging script on 2020-12-05 +version: '8.x-1.15' project: 'metatag' -datestamp: 1597183854 +datestamp: 1607188982 diff --git a/web/modules/metatag/metatag_extended_perms/README.txt b/web/modules/metatag/metatag_extended_perms/README.txt new file mode 100644 index 0000000000000000000000000000000000000000..ba2d609c96ff4ca9d127066f03e683f2b99c5ebb --- /dev/null +++ b/web/modules/metatag/metatag_extended_perms/README.txt @@ -0,0 +1,35 @@ +Metatag Extended Permissions +---------------------------- +This add-on for Metatag creates a new permission for each individual meta tag, +allowing for very fine controlled access over meta tag customization. + + +Usage +-------------------------------------------------------------------------------- +* Enable the Metatag Extended Permissions module. +* Assign the appropriate permissions via the admin/people/permisisons page. + + +Known issues +-------------------------------------------------------------------------------- +This module introduces a possibility for dramatically increasing the number of +checkboxes on the permissions page. This can lead to the following problems: +* The permissions admin page or node forms taking a long time to load. +* PHP timeout errors on the permissions admin or node forms pages. +* Out-of-memory errors loading the above. +* The web server not being able to process the permissions form due to hitting + PHP's max_input_vars limit. + +Because of these, it is advised to fully test this module on a copy of a site +before enabling it on production, to help ensure these problems are not +encountered. + + +Credits / contact +-------------------------------------------------------------------------------- +Originally written by Michael Petri [1]. + + +References +-------------------------------------------------------------------------------- +1: https://www.drupal.org/u/michaelpetri diff --git a/web/modules/metatag/metatag_extended_perms/metatag_extended_perms.info.yml b/web/modules/metatag/metatag_extended_perms/metatag_extended_perms.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..f2d6d1f7869c1facc533940f42915a6cdb70f1f3 --- /dev/null +++ b/web/modules/metatag/metatag_extended_perms/metatag_extended_perms.info.yml @@ -0,0 +1,12 @@ +name: Metatag Extended Permissions +type: module +description: "Adds individual permissions for each meta tag, allowing for fine-grained access to the meta tags. Note: this may nead to performance issues on the permissions admin page, please see the included README.txt file for details." +core_version_requirement: '^8.7.7 || ^9' +package: SEO +dependencies: + - metatag + +# Information added by Drupal.org packaging script on 2020-12-05 +version: '8.x-1.15' +project: 'metatag' +datestamp: 1607188982 diff --git a/web/modules/metatag/metatag_extended_perms/metatag_extended_perms.module b/web/modules/metatag/metatag_extended_perms/metatag_extended_perms.module new file mode 100644 index 0000000000000000000000000000000000000000..fb0ff5706eea8ed80557c88c6474c5d13126d5c1 --- /dev/null +++ b/web/modules/metatag/metatag_extended_perms/metatag_extended_perms.module @@ -0,0 +1,43 @@ +<?php + +/** + * @file + * Primary hook implementations for metatag_extended_perms. + */ + +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element; +use Drupal\metatag\Plugin\Field\FieldWidget\MetatagFirehose; + +/** + * Implements hook_field_widget_form_alter(). + */ +function metatag_extended_perms_field_widget_form_alter(&$element, FormStateInterface $form_state, $context) { + if ($context['widget'] instanceof MetatagFirehose) { + $group_manager = \Drupal::getContainer()->get('plugin.manager.metatag.group'); + + foreach (Element::children($element) as $group_id) { + $group = $group_manager->getDefinition($group_id, FALSE); + if ($group === NULL) { + continue; + } + + // By default restrict access to group and regain access when user has + // access to at least one tag in group; this prevents displaying empty + // groups. + $element[$group_id]['#access'] = FALSE; + + // Check through each meta tag field on the field widget. + foreach (Element::children($element[$group_id]) as $tag_id) { + // Check tag permission. + $element[$group_id][$tag_id]['#access'] = \Drupal::currentUser() + ->hasPermission('access metatag ' . $group_id . '__' . $tag_id); + + // Make the group accessible if user has access to the tag. + if ($element[$group_id][$tag_id]['#access']) { + $element[$group_id]['#access'] = TRUE; + } + } + } + } +} diff --git a/web/modules/metatag/metatag_extended_perms/metatag_extended_perms.permissions.yml b/web/modules/metatag/metatag_extended_perms/metatag_extended_perms.permissions.yml new file mode 100644 index 0000000000000000000000000000000000000000..1ebde3d45fa1c04715a7219dd99fc0c94399ffce --- /dev/null +++ b/web/modules/metatag/metatag_extended_perms/metatag_extended_perms.permissions.yml @@ -0,0 +1,6 @@ +# @file +# Provide individual permissions for each meta tag available on the site. + +# All of the actual permissions are provided by a callback. +permission_callbacks: + - Drupal\metatag_extended_perms\MetatagPermissions::permissions diff --git a/web/modules/metatag/metatag_extended_perms/src/MetatagPermissions.php b/web/modules/metatag/metatag_extended_perms/src/MetatagPermissions.php new file mode 100644 index 0000000000000000000000000000000000000000..cc9370520c8dca26955cb55f30e450da22bad7ee --- /dev/null +++ b/web/modules/metatag/metatag_extended_perms/src/MetatagPermissions.php @@ -0,0 +1,83 @@ +<?php + +namespace Drupal\metatag_extended_perms; + +use Drupal\Core\DependencyInjection\ContainerInjectionInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\metatag\MetatagGroupPluginManager; +use Drupal\metatag\MetatagTagPluginManager; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Provides dynamic permissions for the Metatag module. + * + * @see metatag_extended_perms.permissions.yml + */ +class MetatagPermissions implements ContainerInjectionInterface { + + use StringTranslationTrait; + + /** + * The Metatag Tag Plugin Manager. + * + * @var \Drupal\metatag\MetatagTagPluginManager + */ + protected $tagManager; + + /** + * The Metatag Group Plugin Manager. + * + * @var \Drupal\metatag\MetatagGroupPluginManager + */ + protected $groupManager; + + /** + * Constructs a MetatagPermissions instance. + * + * @param \Drupal\metatag\MetatagTagPluginManager $tag_manager + * The tag plugin manager. + * @param \Drupal\metatag\MetatagGroupPluginManager $group_manager + * The group plugin manager. + */ + public function __construct(MetatagTagPluginManager $tag_manager, MetatagGroupPluginManager $group_manager) { + $this->tagManager = $tag_manager; + $this->groupManager = $group_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.metatag.tag'), + $container->get('plugin.manager.metatag.group') + ); + } + + /** + * Get each permission. + * + * @return array + * Permissions array. + */ + public function permissions() { + $permissions = []; + + // Build permissions for each tag in each group. + foreach ($this->tagManager->getDefinitions() as $key => $metatag) { + $group = $this->groupManager->getDefinition($metatag['group']); + + $permissions += [ + 'access metatag ' . $metatag['group'] . '__' . $key => [ + 'title' => $this->t('Access %tag in %group', [ + '%tag' => $metatag['label'], + '%group' => $group['label'], + ]), + ], + ]; + } + + return $permissions; + } + +} diff --git a/web/modules/metatag/metatag_extended_perms/tests/src/Functional/PermissionsTest.php b/web/modules/metatag/metatag_extended_perms/tests/src/Functional/PermissionsTest.php new file mode 100644 index 0000000000000000000000000000000000000000..830a4d3d98045b5a50f2f96de5924f516d0ab384 --- /dev/null +++ b/web/modules/metatag/metatag_extended_perms/tests/src/Functional/PermissionsTest.php @@ -0,0 +1,214 @@ +<?php + +namespace Drupal\Tests\metatag_extended_perms\Functional; + +use Drupal\Tests\BrowserTestBase; + +/** + * Verify the new permissions are added. + * + * @group metatag + */ +class PermissionsTest extends BrowserTestBase { + + // Contains helper methods. + use \Drupal\Tests\metatag\Functional\MetatagHelperTrait; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * {@inheritdoc} + */ + public static $modules = [ + // Modules for core functionality. + 'node', + + // Needed for the field UI testing. + 'field_ui', + + // This custom module. + 'metatag_extended_perms', + ]; + + /** + * Permissions to check for. + * + * @var string + */ + protected $permissions = [ + 'basic' => [ + 'abstract' => 'Abstract', + 'description' => 'Description', + 'keywords' => 'Keywords', + 'title' => 'Page title', + ], + + // Tags in the "Advanced" group. + 'advanced' => [ + 'cache_control' => 'Cache control', + 'canonical_url' => 'Canonical URL', + 'content_language' => 'Content Language', + 'expires' => 'Expires', + 'generator' => 'Generator', + 'geo_placename' => 'Geographical place name', + 'geo_position' => 'Geographical position', + 'geo_region' => 'Geographical region', + 'google' => 'Google', + 'icbm' => 'ICBM', + 'image_src' => 'Image', + 'news_keywords' => 'News Keywords', + 'next' => 'Next page URL', + 'original_source' => 'Original source', + 'pragma' => 'Pragma', + 'prev' => 'Previous page URL', + 'rating' => 'Rating', + 'referrer' => 'Referrer policy', + 'refresh' => 'Refresh', + 'revisit_after' => 'Revisit After', + 'rights' => 'Rights', + // This one is more complicated, so skip it. + // 'robots' => 'Robots', + 'set_cookie' => 'Set cookie', + 'shortlink' => 'Shortlink URL', + 'standout' => 'Standout', + ] + ]; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + // Log in as the super admin. + $this->loginUser1(); + + // Create a content type with a Metatag field. + $this->createContentType(); + $this->drupalGet('admin/people/permissions'); + } + + /** + * Confirm each permission is listed. + * + * User 1 will be logged in from the setUp() method. + */ + public function testPermissionsExist() { + // Load the front page. + $this->drupalGet('admin/people/permissions'); + + // Confirm that the site didn't throw a server error or something else. + $session = $this->assertSession(); + $session->statusCodeEquals(200); + + // Confirm that the page contains the standard text iddicating this is the + // permissions page. + $session->pageTextContains('Administer menus and menu items'); + + // Look for each of the meta tags. + foreach ($this->permissions as $group => $perms) { + foreach ($perms as $tag_name => $tag_label) { + // Look for the checkbox. + $session->fieldExists('anonymous[access metatag ' . $group . '__' . $tag_name . ']'); + } + } + } + + /** + * Confirm that the node form isn't affected for user 1. + * + * User 1 will be logged in from the setUp() method. + */ + public function testUser1() { + // Load the node form. + $this->drupalGet('node/add/page'); + + // Confirm that the site didn't throw a server error or something else. + $session = $this->assertSession(); + $session->statusCodeEquals(200); + + // Look for each of the meta tags. + foreach ($this->permissions as $group => $perms) { + foreach ($perms as $tag_name => $tag_label) { + // Look for the checkbox. + $session->fieldExists("field_metatag[0][{$group}][{$tag_name}]"); + } + } + } + + /** + * Confirm that the node form is affected for a limited-access user. + */ + public function testUserPerms() { + // Split up the permissions into ones that will be granted and ones that + // will not. + $group_yes = 'basic'; + $group_no = 'advanced'; + + // Work out a list of permissions to grant the user. These are base perms. + $perms_yes = [ + 'access administration pages', + 'access content', + 'administer meta tags', + 'administer nodes', + 'create page content', + ]; + // Grant each of the "yes" tag's permissions. + foreach ($this->permissions[$group_yes] as $tag_name => $tag_label) { + $perms_yes[] = "access metatag {$group_yes}__{$tag_name}"; + } + + $this->verbose($perms_yes); + + // Create a user account with the above permissions. + $user = $this->createUser($perms_yes); + $this->drupalLogin($user); + + // Load the node form. + $this->drupalGet('node/add/page'); + + // Confirm that the site didn't throw a server error or something else. + $session = $this->assertSession(); + $session->statusCodeEquals(200); + + // Look for each of the "yes" meta tags. + foreach ($this->permissions[$group_yes] as $tag_name => $tag_label) { + // Look for the checkbox. + $session->fieldExists("field_metatag[0][{$group_yes}][{$tag_name}]"); + } + + // Make sure each of the "no" meta tags is not present. + foreach ($this->permissions[$group_no] as $tag_name => $tag_label) { + // Look for the checkbox. + $session->fieldNotExists("field_metatag[0][{$group_no}][{$tag_name}]"); + } + } + + /** + * {@inheritdoc} + */ + protected function createContentType(array $values = []) { + parent::createContentType(['type' => 'page']); + + // Load a node form. + $this->drupalGet('node/add/page'); + + // Add a metatag field to the entity type test_entity. + $this->drupalGet('admin/structure/types/manage/page/fields/add-field'); + $this->assertSession()->statusCodeEquals(200); + $edit = [ + 'label' => 'Metatag', + 'field_name' => 'metatag', + 'new_storage_type' => 'metatag', + ]; + $this->drupalPostForm(NULL, $edit, 'Save and continue'); + $this->drupalPostForm(NULL, [], 'Save field settings'); + + // Clear all settings. + $this->container->get('entity_field.manager')->clearCachedFieldDefinitions(); + } + +} diff --git a/web/modules/metatag/metatag_facebook/metatag_facebook.info.yml b/web/modules/metatag/metatag_facebook/metatag_facebook.info.yml index 0801c69f27f780e9a421118e3228b436e88922a2..11944e97e8d4f9271d60b96a125a910a73d57dc7 100644 --- a/web/modules/metatag/metatag_facebook/metatag_facebook.info.yml +++ b/web/modules/metatag/metatag_facebook/metatag_facebook.info.yml @@ -6,7 +6,7 @@ package: SEO dependencies: - metatag:metatag -# Information added by Drupal.org packaging script on 2020-08-11 -version: '8.x-1.14' +# Information added by Drupal.org packaging script on 2020-12-05 +version: '8.x-1.15' project: 'metatag' -datestamp: 1597183854 +datestamp: 1607188982 diff --git a/web/modules/metatag/metatag_facebook/src/Plugin/metatag/Group/Facebook.php b/web/modules/metatag/metatag_facebook/src/Plugin/metatag/Group/Facebook.php index 64ccbfb78f336e18f6494851a9329da076e9fc9e..b8594adf1ef3a499ea872a3fbdcf3315d4f7e10e 100644 --- a/web/modules/metatag/metatag_facebook/src/Plugin/metatag/Group/Facebook.php +++ b/web/modules/metatag/metatag_facebook/src/Plugin/metatag/Group/Facebook.php @@ -10,7 +10,7 @@ * @MetatagGroup( * id = "facebook", * label = @Translation("facebook"), - * description = @Translation("A set of meta tags specially for controlling advanced functionality with <a href=':fb'>Facebook</a>.", arguments = { ":fb" = "https://www.facebook.com/" }), + * description = @Translation("A set of meta tags specially for controlling advanced functionality with <a href=':fb'>Facebook</a>.<br><br>The Facebook <a href='https://developers.facebook.com/tools/debug/'>Sharing Debugger</a> lets you preview how your content will look when it's shared to Facebook and debug any issues with your Open Graph tags.", arguments = { ":fb" = "https://www.facebook.com/" }), * weight = 4 * ) */ diff --git a/web/modules/metatag/metatag_favicons/metatag_favicons.info.yml b/web/modules/metatag/metatag_favicons/metatag_favicons.info.yml index 42e308d5a707baa49cd90e127428d26c86da3321..b5179b53f7c81c9aa6f70778ebafda30efca2bdd 100644 --- a/web/modules/metatag/metatag_favicons/metatag_favicons.info.yml +++ b/web/modules/metatag/metatag_favicons/metatag_favicons.info.yml @@ -6,7 +6,7 @@ package: SEO dependencies: - metatag:metatag -# Information added by Drupal.org packaging script on 2020-08-11 -version: '8.x-1.14' +# Information added by Drupal.org packaging script on 2020-12-05 +version: '8.x-1.15' project: 'metatag' -datestamp: 1597183854 +datestamp: 1607188982 diff --git a/web/modules/metatag/metatag_google_cse/metatag_google_cse.info.yml b/web/modules/metatag/metatag_google_cse/metatag_google_cse.info.yml index aafc3a5eae98d4f7b61af92a4981f4ea3d23a523..beb9dbe7dd650b2e58776fdb3db16f990487b4c3 100644 --- a/web/modules/metatag/metatag_google_cse/metatag_google_cse.info.yml +++ b/web/modules/metatag/metatag_google_cse/metatag_google_cse.info.yml @@ -6,7 +6,7 @@ package: SEO dependencies: - metatag:metatag -# Information added by Drupal.org packaging script on 2020-08-11 -version: '8.x-1.14' +# Information added by Drupal.org packaging script on 2020-12-05 +version: '8.x-1.15' project: 'metatag' -datestamp: 1597183854 +datestamp: 1607188982 diff --git a/web/modules/metatag/metatag_google_plus/metatag_google_plus.info.yml b/web/modules/metatag/metatag_google_plus/metatag_google_plus.info.yml index cab2f5568dfd5a189d9240d545611570576f1cf2..e9c4e69c5df6f9581c8fafcc02ab301c83e776ec 100644 --- a/web/modules/metatag/metatag_google_plus/metatag_google_plus.info.yml +++ b/web/modules/metatag/metatag_google_plus/metatag_google_plus.info.yml @@ -6,7 +6,7 @@ package: SEO dependencies: - metatag:metatag -# Information added by Drupal.org packaging script on 2020-08-11 -version: '8.x-1.14' +# Information added by Drupal.org packaging script on 2020-12-05 +version: '8.x-1.15' project: 'metatag' -datestamp: 1597183854 +datestamp: 1607188982 diff --git a/web/modules/metatag/metatag_hreflang/metatag_hreflang.info.yml b/web/modules/metatag/metatag_hreflang/metatag_hreflang.info.yml index 9cd5956a7b1cb5e192a0d88bd089cd5213f46981..2c14c44d6981e967c081364710ff2f85f39c697a 100644 --- a/web/modules/metatag/metatag_hreflang/metatag_hreflang.info.yml +++ b/web/modules/metatag/metatag_hreflang/metatag_hreflang.info.yml @@ -6,7 +6,7 @@ package: SEO dependencies: - metatag:metatag -# Information added by Drupal.org packaging script on 2020-08-11 -version: '8.x-1.14' +# Information added by Drupal.org packaging script on 2020-12-05 +version: '8.x-1.15' project: 'metatag' -datestamp: 1597183854 +datestamp: 1607188982 diff --git a/web/modules/metatag/metatag_mobile/metatag_mobile.info.yml b/web/modules/metatag/metatag_mobile/metatag_mobile.info.yml index 03ef7dd58340eda6ae881dad86fe2c14d1d651cd..856d0a3e7d19a68684e498294c94bbaef6aa05c9 100644 --- a/web/modules/metatag/metatag_mobile/metatag_mobile.info.yml +++ b/web/modules/metatag/metatag_mobile/metatag_mobile.info.yml @@ -6,7 +6,7 @@ package: SEO dependencies: - metatag:metatag -# Information added by Drupal.org packaging script on 2020-08-11 -version: '8.x-1.14' +# Information added by Drupal.org packaging script on 2020-12-05 +version: '8.x-1.15' project: 'metatag' -datestamp: 1597183854 +datestamp: 1607188982 diff --git a/web/modules/metatag/metatag_mobile/src/Plugin/metatag/Tag/ThemeColor.php b/web/modules/metatag/metatag_mobile/src/Plugin/metatag/Tag/ThemeColor.php index 07cd4a217852ca78f81f3372e3dc67d44482839a..75a3461a820dc2c50aa7623d41a2c16018d0853c 100644 --- a/web/modules/metatag/metatag_mobile/src/Plugin/metatag/Tag/ThemeColor.php +++ b/web/modules/metatag/metatag_mobile/src/Plugin/metatag/Tag/ThemeColor.php @@ -10,7 +10,7 @@ * @MetatagTag( * id = "theme_color", * label = @Translation("Theme Color"), - * description = @Translation("A color in hexidecimal format, e.g. '#0000ff' for blue; must include the '#' symbol. Used by some browsers to control the background color of the toolbar, the color used with an icon, etc."), + * description = @Translation("A color in hexadecimal format, e.g. '#0000ff' for blue; must include the '#' symbol. Used by some browsers to control the background color of the toolbar, the color used with an icon, etc."), * name = "theme-color", * group = "mobile", * weight = 81, diff --git a/web/modules/metatag/metatag_open_graph/metatag_open_graph.info.yml b/web/modules/metatag/metatag_open_graph/metatag_open_graph.info.yml index a083a86882c482bbc24dca81ad2f290d652f5c42..5e4d81943605008cbf2a5a9cc8a5d9ed1c5cc169 100644 --- a/web/modules/metatag/metatag_open_graph/metatag_open_graph.info.yml +++ b/web/modules/metatag/metatag_open_graph/metatag_open_graph.info.yml @@ -6,7 +6,7 @@ package: SEO dependencies: - metatag:metatag -# Information added by Drupal.org packaging script on 2020-08-11 -version: '8.x-1.14' +# Information added by Drupal.org packaging script on 2020-12-05 +version: '8.x-1.15' project: 'metatag' -datestamp: 1597183854 +datestamp: 1607188982 diff --git a/web/modules/metatag/metatag_open_graph/src/Plugin/metatag/Group/OpenGraph.php b/web/modules/metatag/metatag_open_graph/src/Plugin/metatag/Group/OpenGraph.php index cdb82a72e2400636fb6be5e58cdc80c3b75aca1a..f3c1dab8bbbdf589725002b529ff9491c0198aeb 100644 --- a/web/modules/metatag/metatag_open_graph/src/Plugin/metatag/Group/OpenGraph.php +++ b/web/modules/metatag/metatag_open_graph/src/Plugin/metatag/Group/OpenGraph.php @@ -10,7 +10,7 @@ * @MetatagGroup( * id = "open_graph", * label = @Translation("Open Graph"), - * description = @Translation("The <a href='https://ogp.me/'>Open Graph meta tags</a> are used to control how Facebook, Pinterest, LinkedIn and other social networking sites interpret the site's content."), + * description = @Translation("The <a href='https://ogp.me/'>Open Graph meta tags</a> are used to control how Facebook, Pinterest, LinkedIn and other social networking sites interpret the site's content.<br><br>The Facebook <a href='https://developers.facebook.com/tools/debug/'>Sharing Debugger</a> lets you preview how your content will look when it's shared to Facebook and debug any issues with your Open Graph tags."), * weight = 3 * ) */ diff --git a/web/modules/metatag/metatag_open_graph_products/metatag_open_graph_products.info.yml b/web/modules/metatag/metatag_open_graph_products/metatag_open_graph_products.info.yml index b3420183c6143602458b3c8f2ea6917b32d43bd6..49e50b7371ff2528ba235c3846bca02454266617 100644 --- a/web/modules/metatag/metatag_open_graph_products/metatag_open_graph_products.info.yml +++ b/web/modules/metatag/metatag_open_graph_products/metatag_open_graph_products.info.yml @@ -7,7 +7,7 @@ dependencies: - metatag:metatag - metatag:metatag_open_graph -# Information added by Drupal.org packaging script on 2020-08-11 -version: '8.x-1.14' +# Information added by Drupal.org packaging script on 2020-12-05 +version: '8.x-1.15' project: 'metatag' -datestamp: 1597183854 +datestamp: 1607188982 diff --git a/web/modules/metatag/metatag_open_graph_products/src/Plugin/metatag/Group/OpenGraphProducts.php b/web/modules/metatag/metatag_open_graph_products/src/Plugin/metatag/Group/OpenGraphProducts.php index 3a254cb23dba9287dfb65d60ecb541ea51f0ddb2..ccb7b804bc7be30fb30c5b2aebdf93ba7a897668 100644 --- a/web/modules/metatag/metatag_open_graph_products/src/Plugin/metatag/Group/OpenGraphProducts.php +++ b/web/modules/metatag/metatag_open_graph_products/src/Plugin/metatag/Group/OpenGraphProducts.php @@ -10,7 +10,7 @@ * @MetatagGroup( * id = "open_graph_products", * label = @Translation("Open Graph - Products"), - * description = @Translation("These Open Graph meta tags are for describing products."), + * description = @Translation("These <a href='https://ogp.me/'>Open Graph meta tags</a> are for describing products.<br><br>The Facebook <a href='https://developers.facebook.com/tools/debug/'>Sharing Debugger</a> lets you preview how your content will look when it's shared to Facebook and debug any issues with your Open Graph tags."), * weight = 0, * ) */ diff --git a/web/modules/metatag/metatag_page_manager/metatag_page_manager.info.yml b/web/modules/metatag/metatag_page_manager/metatag_page_manager.info.yml index e79b5d4017f2613b15e23d80304ad070b703069a..b266b506d4dcf1a9b906effd7438c662ad584cca 100644 --- a/web/modules/metatag/metatag_page_manager/metatag_page_manager.info.yml +++ b/web/modules/metatag/metatag_page_manager/metatag_page_manager.info.yml @@ -7,7 +7,7 @@ dependencies: - page_manager:page_manager - metatag:metatag -# Information added by Drupal.org packaging script on 2020-08-11 -version: '8.x-1.14' +# Information added by Drupal.org packaging script on 2020-12-05 +version: '8.x-1.15' project: 'metatag' -datestamp: 1597183854 +datestamp: 1607188982 diff --git a/web/modules/metatag/metatag_pinterest/metatag_pinterest.info.yml b/web/modules/metatag/metatag_pinterest/metatag_pinterest.info.yml index 51054f5e80166dfdf1133d25333240229e9b6bc9..fbd29135f4ac6224c1d056e75d00707402bac858 100644 --- a/web/modules/metatag/metatag_pinterest/metatag_pinterest.info.yml +++ b/web/modules/metatag/metatag_pinterest/metatag_pinterest.info.yml @@ -6,7 +6,7 @@ package: SEO dependencies: - metatag:metatag -# Information added by Drupal.org packaging script on 2020-08-11 -version: '8.x-1.14' +# Information added by Drupal.org packaging script on 2020-12-05 +version: '8.x-1.15' project: 'metatag' -datestamp: 1597183854 +datestamp: 1607188982 diff --git a/web/modules/metatag/metatag_twitter_cards/metatag_twitter_cards.info.yml b/web/modules/metatag/metatag_twitter_cards/metatag_twitter_cards.info.yml index d70703cce75d6204e4baddda43cb42ff3b428060..75987c56b424f52f4bcc77b603bf6b0c86563144 100644 --- a/web/modules/metatag/metatag_twitter_cards/metatag_twitter_cards.info.yml +++ b/web/modules/metatag/metatag_twitter_cards/metatag_twitter_cards.info.yml @@ -6,7 +6,7 @@ package: SEO dependencies: - metatag:metatag -# Information added by Drupal.org packaging script on 2020-08-11 -version: '8.x-1.14' +# Information added by Drupal.org packaging script on 2020-12-05 +version: '8.x-1.15' project: 'metatag' -datestamp: 1597183854 +datestamp: 1607188982 diff --git a/web/modules/metatag/metatag_twitter_cards/src/Plugin/metatag/Tag/TwitterCardsGalleryImage0.php b/web/modules/metatag/metatag_twitter_cards/src/Plugin/metatag/Tag/TwitterCardsGalleryImage0.php index d0545cdebedcde99ad2226f8ce894d978be674f9..6418d9a6665fda8480e06594589e6231c8a14ee2 100644 --- a/web/modules/metatag/metatag_twitter_cards/src/Plugin/metatag/Tag/TwitterCardsGalleryImage0.php +++ b/web/modules/metatag/metatag_twitter_cards/src/Plugin/metatag/Tag/TwitterCardsGalleryImage0.php @@ -10,7 +10,7 @@ * @MetatagTag( * id = "twitter_cards_gallery_image0", * label = @Translation("1st gallery image"), - * description = @Translation("A URL to the image representing the first photo in your gallery. This will be able to extract the URL from an image field."), + * description = @Translation("A URL to the image representing the first photo in your gallery."), * name = "twitter:gallery:image0", * group = "twitter_cards", * weight = 200, diff --git a/web/modules/metatag/metatag_twitter_cards/src/Plugin/metatag/Tag/TwitterCardsGalleryImage1.php b/web/modules/metatag/metatag_twitter_cards/src/Plugin/metatag/Tag/TwitterCardsGalleryImage1.php index 8de3792ed22cd8d6d6c310d81baedf376b02a73e..0cb9541013f37884f32acf05607b072df1dffbfd 100644 --- a/web/modules/metatag/metatag_twitter_cards/src/Plugin/metatag/Tag/TwitterCardsGalleryImage1.php +++ b/web/modules/metatag/metatag_twitter_cards/src/Plugin/metatag/Tag/TwitterCardsGalleryImage1.php @@ -10,7 +10,7 @@ * @MetatagTag( * id = "twitter_cards_gallery_image1", * label = @Translation("2nd gallery image"), - * description = @Translation("A URL to the image representing the second photo in your gallery. This will be able to extract the URL from an image field."), + * description = @Translation("A URL to the image representing the second photo in your gallery."), * name = "twitter:gallery:image1", * group = "twitter_cards", * weight = 201, diff --git a/web/modules/metatag/metatag_twitter_cards/src/Plugin/metatag/Tag/TwitterCardsGalleryImage2.php b/web/modules/metatag/metatag_twitter_cards/src/Plugin/metatag/Tag/TwitterCardsGalleryImage2.php index 75816ba2f41fd5044c45cca6b50d33b2b457e7bc..136043e665f3650bc82196adfcddd036cf933ff8 100644 --- a/web/modules/metatag/metatag_twitter_cards/src/Plugin/metatag/Tag/TwitterCardsGalleryImage2.php +++ b/web/modules/metatag/metatag_twitter_cards/src/Plugin/metatag/Tag/TwitterCardsGalleryImage2.php @@ -10,7 +10,7 @@ * @MetatagTag( * id = "twitter_cards_gallery_image2", * label = @Translation("3rd gallery image"), - * description = @Translation("A URL to the image representing the third photo in your gallery. This will be able to extract the URL from an image field."), + * description = @Translation("A URL to the image representing the third photo in your gallery."), * name = "twitter:gallery:image2", * group = "twitter_cards", * weight = 202, diff --git a/web/modules/metatag/metatag_twitter_cards/src/Plugin/metatag/Tag/TwitterCardsGalleryImage3.php b/web/modules/metatag/metatag_twitter_cards/src/Plugin/metatag/Tag/TwitterCardsGalleryImage3.php index 1c991b88bea85df914ea6e5ca742888632c1814b..5c0a18641c78969e61b0c72170ac4b3b60efd4ec 100644 --- a/web/modules/metatag/metatag_twitter_cards/src/Plugin/metatag/Tag/TwitterCardsGalleryImage3.php +++ b/web/modules/metatag/metatag_twitter_cards/src/Plugin/metatag/Tag/TwitterCardsGalleryImage3.php @@ -10,7 +10,7 @@ * @MetatagTag( * id = "twitter_cards_gallery_image3", * label = @Translation("4th gallery image"), - * description = @Translation("A URL to the image representing the fourth photo in your gallery. This will be able to extract the URL from an image field."), + * description = @Translation("A URL to the image representing the fourth photo in your gallery."), * name = "twitter:gallery:image3", * group = "twitter_cards", * weight = 203, diff --git a/web/modules/metatag/metatag_twitter_cards/src/Plugin/metatag/Tag/TwitterCardsImage.php b/web/modules/metatag/metatag_twitter_cards/src/Plugin/metatag/Tag/TwitterCardsImage.php index 4fa71b7034c5bca89ef8ee9ce136572008c17ab4..bcb307c12b71db2036c6ce5a31f980dd917a1b4a 100644 --- a/web/modules/metatag/metatag_twitter_cards/src/Plugin/metatag/Tag/TwitterCardsImage.php +++ b/web/modules/metatag/metatag_twitter_cards/src/Plugin/metatag/Tag/TwitterCardsImage.php @@ -10,7 +10,7 @@ * @MetatagTag( * id = "twitter_cards_image", * label = @Translation("Image URL"), - * description = @Translation("The URL to a unique image representing the content of the page. Do not use a generic image such as your website logo, author photo, or other image that spans multiple pages. Images larger than 120x120px will be resized and cropped square based on longest dimension. Images smaller than 60x60px will not be shown. If the 'type' is set to Photo then the image must be at least 280x150px. This will be able to extract the URL from an image field."), + * description = @Translation("The URL to a unique image representing the content of the page. Do not use a generic image such as your website logo, author photo, or other image that spans multiple pages. Images larger than 120x120px will be resized and cropped square based on longest dimension. Images smaller than 60x60px will not be shown. If the 'type' is set to Photo then the image must be at least 280x150px."), * name = "twitter:image", * group = "twitter_cards", * weight = 7, diff --git a/web/modules/metatag/metatag_twitter_cards/src/Plugin/metatag/Tag/TwitterCardsPlayerStream.php b/web/modules/metatag/metatag_twitter_cards/src/Plugin/metatag/Tag/TwitterCardsPlayerStream.php index 0be2723a81748dd13953e6c7219967a5be550cb1..1e6c7fc47045c34109f9bb6ab0e5530a8153f3a7 100644 --- a/web/modules/metatag/metatag_twitter_cards/src/Plugin/metatag/Tag/TwitterCardsPlayerStream.php +++ b/web/modules/metatag/metatag_twitter_cards/src/Plugin/metatag/Tag/TwitterCardsPlayerStream.php @@ -10,7 +10,7 @@ * @MetatagTag( * id = "twitter_cards_player_stream", * label = @Translation("MP4 media stream URL"), - * description = @Translation("The full URL for an MP4 video (h.264) or audio (AAC) stream, takes precidence over the other media player field."), + * description = @Translation("The full URL for an MP4 video (h.264) or audio (AAC) stream, takes precedence over the other media player field."), * name = "twitter:player:stream", * group = "twitter_cards", * weight = 403, diff --git a/web/modules/metatag/metatag_verification/config/schema/metatag_verification.metatag_tag.schema.yml b/web/modules/metatag/metatag_verification/config/schema/metatag_verification.metatag_tag.schema.yml index 7288fff9e6dfa6c49f6b956ec6c7c9a116467610..177592d8853bb195fc29605d4282172b10ee3c16 100644 --- a/web/modules/metatag/metatag_verification/config/schema/metatag_verification.metatag_tag.schema.yml +++ b/web/modules/metatag/metatag_verification/config/schema/metatag_verification.metatag_tag.schema.yml @@ -11,6 +11,9 @@ metatag.metatag_tag.baidu: metatag.metatag_tag.bing: type: label label: 'Site validation: Bing' +metatag.metatag_tag.facebook_domain_verification: + type: label + label: 'Site validation: Facebook' metatag.metatag_tag.google: type: label label: 'Site validation: Google' diff --git a/web/modules/metatag/metatag_verification/metatag_verification.info.yml b/web/modules/metatag/metatag_verification/metatag_verification.info.yml index 39cebae11526c56a0a57afe227043eff509b3672..7bc07262576578b51200112f05c2e471d2faba1f 100644 --- a/web/modules/metatag/metatag_verification/metatag_verification.info.yml +++ b/web/modules/metatag/metatag_verification/metatag_verification.info.yml @@ -6,7 +6,7 @@ package: SEO dependencies: - metatag:metatag -# Information added by Drupal.org packaging script on 2020-08-11 -version: '8.x-1.14' +# Information added by Drupal.org packaging script on 2020-12-05 +version: '8.x-1.15' project: 'metatag' -datestamp: 1597183854 +datestamp: 1607188982 diff --git a/web/modules/metatag/metatag_verification/src/Plugin/metatag/Tag/Baidu.php b/web/modules/metatag/metatag_verification/src/Plugin/metatag/Tag/Baidu.php index 673f67bae3831b62b6b6d0478cf437b6ed45c75d..4e6b78687d08698632bbcd19fde333e8e27f7be8 100644 --- a/web/modules/metatag/metatag_verification/src/Plugin/metatag/Tag/Baidu.php +++ b/web/modules/metatag/metatag_verification/src/Plugin/metatag/Tag/Baidu.php @@ -13,7 +13,7 @@ * description = @Translation("A string provided by <a href=':baidu'>Baidu</a>.", arguments = { ":baidu" = "https://www.baidu.com/" }), * name = "baidu-site-verification", * group = "site_verification", - * weight = 2, + * weight = 1, * type = "label", * secure = FALSE, * multiple = FALSE diff --git a/web/modules/metatag/metatag_verification/src/Plugin/metatag/Tag/Bing.php b/web/modules/metatag/metatag_verification/src/Plugin/metatag/Tag/Bing.php index 1a8a0a4a7ff4de3741b281a08bc5975ff5d89dba..c61aa0392cf2b3bff288e65681bf59b0ea5cf52a 100644 --- a/web/modules/metatag/metatag_verification/src/Plugin/metatag/Tag/Bing.php +++ b/web/modules/metatag/metatag_verification/src/Plugin/metatag/Tag/Bing.php @@ -13,7 +13,7 @@ * description = @Translation("A string provided by <a href=':bing'>Bing</a>, full details are available from the <a href=':verify_url'>Bing online help</a>.", arguments = { ":bing" = "https://www.bing.com/", ":verify_url" = "https://www.bing.com/webmaster/help/how-to-verify-ownership-of-your-site-afcfefc6" }), * name = "msvalidate.01", * group = "site_verification", - * weight = 3, + * weight = 2, * type = "label", * secure = FALSE, * multiple = FALSE diff --git a/web/modules/metatag/metatag_verification/src/Plugin/metatag/Tag/FacebookDomainVerification.php b/web/modules/metatag/metatag_verification/src/Plugin/metatag/Tag/FacebookDomainVerification.php new file mode 100644 index 0000000000000000000000000000000000000000..7018d0620730e2bbf91d6bfbca9bba1fba9b2818 --- /dev/null +++ b/web/modules/metatag/metatag_verification/src/Plugin/metatag/Tag/FacebookDomainVerification.php @@ -0,0 +1,23 @@ +<?php + +namespace Drupal\metatag_verification\Plugin\metatag\Tag; + +use Drupal\metatag\Plugin\metatag\Tag\MetaNameBase; + +/** + * Provides a plugin for the 'facebook-domain-verification' meta tag. + * + * @MetatagTag( + * id = "facebook_domain_verification", + * label = @Translation("Facebook"), + * description = @Translation("A string provided by <a href=':facebook'>Facebook</a>, full details are available from the <a href=':help'>Facebook online help</a>.", arguments = { ":facebook" = "https://facebook.com", ":help" = "https://developers.facebook.com/docs/sharing/domain-verification/verifying-your-domain/#meta-tags" }), + * name = "facebook-domain-verification", + * group = "site_verification", + * weight = 3, + * type = "label", + * secure = FALSE, + * multiple = FALSE + * ) + */ +class FacebookDomainVerification extends MetaNameBase { +} diff --git a/web/modules/metatag/metatag_verification/tests/src/Functional/MetatagVerificationTagsTest.php b/web/modules/metatag/metatag_verification/tests/src/Functional/MetatagVerificationTagsTest.php index cfc7ed26a1b6a805ae4fb6c07c83501b9f090f87..4ebc2882eef8de8715895f086365a88629fb0686 100644 --- a/web/modules/metatag/metatag_verification/tests/src/Functional/MetatagVerificationTagsTest.php +++ b/web/modules/metatag/metatag_verification/tests/src/Functional/MetatagVerificationTagsTest.php @@ -17,6 +17,7 @@ class MetatagVerificationTagsTest extends MetatagTagsTestBase { protected $tags = [ 'baidu', 'bing', + 'facebook_domain_verification', 'google', 'norton_safe_web', 'pinterest', @@ -43,6 +44,9 @@ protected function getTestTagName($tag_name) { elseif ($tag_name == 'bing') { $tag_name = 'msvalidate.01'; } + elseif ($tag_name == 'facebook_domain_verification') { + $tag_name = 'facebook-domain-verification'; + } elseif ($tag_name == 'google') { $tag_name = 'google-site-verification'; } diff --git a/web/modules/metatag/metatag_views/metatag_views.info.yml b/web/modules/metatag/metatag_views/metatag_views.info.yml index 4aaf8e53864df54b015ae201254a123c288d3fb1..eb0f40ff725b91ee721c7d45cf11178cfad28536 100644 --- a/web/modules/metatag/metatag_views/metatag_views.info.yml +++ b/web/modules/metatag/metatag_views/metatag_views.info.yml @@ -7,7 +7,7 @@ dependencies: - metatag:metatag - drupal:views -# Information added by Drupal.org packaging script on 2020-08-11 -version: '8.x-1.14' +# Information added by Drupal.org packaging script on 2020-12-05 +version: '8.x-1.15' project: 'metatag' -datestamp: 1597183854 +datestamp: 1607188982 diff --git a/web/modules/metatag/metatag_views/src/Form/MetatagViewsEditForm.php b/web/modules/metatag/metatag_views/src/Form/MetatagViewsEditForm.php index c9df9fede3ba2bc2473e8c55f68b475080a526aa..59ddb01dc633bcdd86f5fc3df647ba72c08d5402 100644 --- a/web/modules/metatag/metatag_views/src/Form/MetatagViewsEditForm.php +++ b/web/modules/metatag/metatag_views/src/Form/MetatagViewsEditForm.php @@ -76,8 +76,8 @@ public function getFormId() { */ public function buildForm(array $form, FormStateInterface $form_state) { // Get the parameters from request. - $view_id = \Drupal::request()->get('view_id'); - $display_id = \Drupal::request()->get('display_id'); + $view_id = $this->getRequest()->get('view_id'); + $display_id = $this->getRequest()->get('display_id'); // Get meta tags from the view entity. $metatags = []; @@ -110,13 +110,13 @@ public function buildForm(array $form, FormStateInterface $form_state) { /** * {@inheritdoc} */ - public function form(array $values, array $element, array $token_types = [], array $included_groups = NULL, array $included_tags = NULL) { + public function form(array $values, array $element, array $token_types = [], array $included_groups = NULL, array $included_tags = NULL, $verbose_help = FALSE) { // Add the outer fieldset. $element += [ '#type' => 'details', ]; - $element += $this->tokenService->tokenBrowser($token_types); + $element += $this->tokenService->tokenBrowser($token_types, $verbose_help); $groups_and_tags = $this->sortedGroupsWithTags(); diff --git a/web/modules/metatag/metatag_views/src/Form/MetatagViewsTranslationForm.php b/web/modules/metatag/metatag_views/src/Form/MetatagViewsTranslationForm.php index 123cc3504f935704efcb58e1a60300ab5ce23faa..89be8defb545fbe014da1feed4a03139e2b4830a 100644 --- a/web/modules/metatag/metatag_views/src/Form/MetatagViewsTranslationForm.php +++ b/web/modules/metatag/metatag_views/src/Form/MetatagViewsTranslationForm.php @@ -138,7 +138,7 @@ protected function prepareValues() { $config_name = $this->view->getConfigDependencyName(); $config_path = 'display.' . $this->displayId . '.display_options.display_extenders.metatag_display_extender.metatags'; - $configuration = \Drupal::service('config.factory')->get($config_name); + $configuration = $this->configFactory()->get($config_name); $this->baseData = $configuration->getOriginal($config_path, FALSE); // Set the translation target language on the configuration factory. @@ -146,7 +146,7 @@ protected function prepareValues() { $this->languageManager->setConfigOverrideLanguage($this->language); // Read in translated values. - $configuration = \Drupal::service('config.factory')->get($config_name); + $configuration = $this->configFactory()->get($config_name); $translated_values = $configuration->get($config_path); // Set the configuration language back. @@ -160,9 +160,9 @@ protected function prepareValues() { */ public function buildForm(array $form, FormStateInterface $form_state) { // Get the parameters from request. - $this->viewId = \Drupal::request()->get('view_id'); - $this->displayId = \Drupal::request()->get('display_id'); - $langcode = \Drupal::request()->get('langcode'); + $this->viewId = $this->getRequest()->get('view_id'); + $this->displayId = $this->getRequest()->get('display_id'); + $langcode = $this->getRequest()->get('langcode'); $this->view = $this->viewsManager->load($this->viewId); $this->language = $this->languageManager->getLanguage($langcode); diff --git a/web/modules/metatag/migrations/d7_metatag_field.yml b/web/modules/metatag/migrations/d7_metatag_field.yml index 8b9d6790b08fd0a18df63faf878e8ca60e992bbd..c526a61de494e9a3b41079c78b8873db5b312344 100644 --- a/web/modules/metatag/migrations/d7_metatag_field.yml +++ b/web/modules/metatag/migrations/d7_metatag_field.yml @@ -6,6 +6,8 @@ id: d7_metatag_field label: Metatag field migration_tags: - Drupal 7 + - Configuration +deriver: Drupal\metatag\Plugin\migrate\source\d7\MetatagFieldDeriver source: plugin: d7_metatag_field source_module: metatag diff --git a/web/modules/metatag/migrations/d7_metatag_field_instance.yml b/web/modules/metatag/migrations/d7_metatag_field_instance.yml index 86082b78628fe87b42d46d4030e8e6d425bb299f..5be196c0fc69a81f322575a4b12f70c233a015dd 100644 --- a/web/modules/metatag/migrations/d7_metatag_field_instance.yml +++ b/web/modules/metatag/migrations/d7_metatag_field_instance.yml @@ -6,6 +6,8 @@ id: d7_metatag_field_instance label: Metatag field instance migration_tags: - Drupal 7 + - Configuration +deriver: Drupal\metatag\Plugin\migrate\source\d7\MetatagFieldInstanceDeriver source: plugin: d7_metatag_field_instance source_module: metatag @@ -24,9 +26,4 @@ migration_dependencies: required: # The base field migration is required before this migration can run. - d7_metatag_field - - # @todo Is this accurate? Does it really need the vocabulary migration, or - # is it more precautionary, that it *might* be needed so it might as well be - # executed first? - - d7_node_type - - d7_taxonomy_vocabulary + # Relevant required dependencies are added in metatag_migration_plugins_alter(). diff --git a/web/modules/metatag/migrations/d7_metatag_field_instance_widget_settings.yml b/web/modules/metatag/migrations/d7_metatag_field_instance_widget_settings.yml index 58191cc0bcadd0b714692c5ce42e2a43e7fe5d97..789770f4205132c9b2a8c96f6f3d24b0ef5c5e0c 100644 --- a/web/modules/metatag/migrations/d7_metatag_field_instance_widget_settings.yml +++ b/web/modules/metatag/migrations/d7_metatag_field_instance_widget_settings.yml @@ -6,6 +6,8 @@ id: d7_metatag_field_instance_widget_settings label: Metatag field instance widget settings migration_tags: - Drupal 7 + - Configuration +deriver: Drupal\metatag\Plugin\migrate\source\d7\MetatagFieldInstanceDeriver source: plugin: d7_metatag_field_instance source_module: metatag diff --git a/web/modules/metatag/src/Form/MetatagDefaultsForm.php b/web/modules/metatag/src/Form/MetatagDefaultsForm.php index 705f04960203c7abda2e0085c6e438725a9c03ff..bbbbc88d4b2ac7d08b36b086edeaac94157a9da0 100644 --- a/web/modules/metatag/src/Form/MetatagDefaultsForm.php +++ b/web/modules/metatag/src/Form/MetatagDefaultsForm.php @@ -4,10 +4,16 @@ use Drupal\Core\Entity\ContentEntityType; use Drupal\Core\Entity\EntityForm; +use Drupal\Core\Entity\EntityTypeBundleInfoInterface; use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\metatag\MetatagManager; +use Drupal\metatag\MetatagManagerInterface; +use Drupal\metatag\MetatagTagPluginManager; +use Drupal\metatag\MetatagToken; use Drupal\page_manager\Entity\PageVariant; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Class MetatagDefaultsForm. @@ -16,13 +22,82 @@ */ class MetatagDefaultsForm extends EntityForm { + /** + * The entity type bundle info service. + * + * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface + */ + protected $entityTypeBundleInfo; + + /** + * The Metatag manager service. + * + * @var \Drupal\metatag\MetatagManagerInterface + */ + protected $metatagManager; + + /** + * The Metatag token service. + * + * @var \Drupal\metatag\MetatagToken + */ + protected $metatagToken; + + /** + * The module handler service. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected $moduleHandler; + + /** + * The Metatag tag plugin manager service. + * + * @var \Drupal\metatag\MetatagTagPluginManager + */ + protected $metatagPluginManager; + + /** + * Constructs a new MetatagDefaultsForm. + * + * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info + * The entity type bundle service. + * @param \Drupal\metatag\MetatagManagerInterface $metatag_manager + * The Metatag manager service. + * @param \Drupal\metatag\MetatagToken $metatag_token + * The Metatag token service. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler service. + * @param \Drupal\metatag\MetatagTagPluginManager $metatag_plugin_manager + * The Metatag tag plugin manager service. + */ + public function __construct(EntityTypeBundleInfoInterface $entity_type_bundle_info, MetatagManagerInterface $metatag_manager, MetatagToken $metatag_token, ModuleHandlerInterface $module_handler, MetatagTagPluginManager $metatag_plugin_manager) { + $this->entityTypeBundleInfo = $entity_type_bundle_info; + $this->metatagManager = $metatag_manager; + $this->metatagToken = $metatag_token; + $this->moduleHandler = $module_handler; + $this->metatagPluginManager = $metatag_plugin_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.bundle.info'), + $container->get('metatag.manager'), + $container->get('metatag.token'), + $container->get('module_handler'), + $container->get('plugin.manager.metatag.tag') + ); + } + /** * {@inheritdoc} */ public function form(array $form, FormStateInterface $form_state) { $form = parent::form($form, $form_state); $metatag_defaults = $this->entity; - $metatag_manager = \Drupal::service('metatag.manager'); $form['#ajax_wrapper_id'] = 'metatag-defaults-form-ajax-wrapper'; $ajax = [ @@ -42,9 +117,6 @@ public function form(array $form, FormStateInterface $form_state) { $token_types = empty($default_type) ? [] : [explode('__', $default_type)[0]]; - // Add the token browser at the top. - $form += \Drupal::service('metatag.token')->tokenBrowser($token_types); - // If this is a new Metatag defaults, then list available bundles. if ($metatag_defaults->isNew()) { $options = $this->getAvailableBundles(); @@ -90,11 +162,11 @@ public function form(array $form, FormStateInterface $form_state) { $groups = !empty($entity_type_groups[$entity_type]) && !empty($entity_type_groups[$entity_type][$entity_bundle]) ? $entity_type_groups[$entity_type][$entity_bundle] : []; // Limit the form to requested groups, if any. if (!empty($groups)) { - $form = $metatag_manager->form($values, $form, [$entity_type], $groups); + $form = $this->metatagManager->form($values, $form, [$entity_type], $groups, NULL, TRUE); } // Otherwise, display all groups. else { - $form = $metatag_manager->form($values, $form); + $form = $this->metatagManager->form($values, $form, [], NULL, NULL, TRUE); } $form['status'] = [ @@ -167,19 +239,16 @@ public function save(array $form, FormStateInterface $form_state) { $entity_bundle = isset($type_parts[1]) ? $type_parts[1] : NULL; // Get the entity label. - $entity_type_manager = \Drupal::service('entity_type.manager'); - $entity_info = $entity_type_manager->getDefinitions(); + $entity_info = $this->entityTypeManager->getDefinitions(); $entity_label = (string) $entity_info[$entity_type]->get('label'); if (!is_null($entity_bundle)) { // Get the bundle label. - $bundle_manager = \Drupal::service('entity_type.bundle.info'); - $bundle_info = $bundle_manager->getBundleInfo($entity_type); + $bundle_info = $this->entityTypeBundleInfo->getBundleInfo($entity_type); if ($entity_type === 'page_variant') { // Check if page manager is enabled and try to load the page variant // so the label of the variant can be used. - $moduleHandler = \Drupal::service('module_handler'); - if ($moduleHandler->moduleExists('metatag_page_manager')) { + if ($this->moduleHandler->moduleExists('metatag_page_manager')) { $page_variant = PageVariant::load($entity_bundle); $page = $page_variant->getPage(); if ($page_variant) { @@ -197,14 +266,13 @@ public function save(array $form, FormStateInterface $form_state) { } // Set tags within the Metatag entity. - $tag_manager = \Drupal::service('plugin.manager.metatag.tag'); - $tags = $tag_manager->getDefinitions(); + $tags = $this->metatagPluginManager->getDefinitions(); $tag_values = []; foreach ($tags as $tag_id => $tag_definition) { if ($form_state->hasValue($tag_id)) { // Some plugins need to process form input before storing it. Hence, we // set it and then get it. - $tag = $tag_manager->createInstance($tag_id); + $tag = $this->metatagPluginManager->createInstance($tag_id); $tag->setValue($form_state->getValue($tag_id)); if (!empty($tag->value())) { $tag_values[$tag_id] = $tag->value(); @@ -239,17 +307,13 @@ public function save(array $form, FormStateInterface $form_state) { protected function getAvailableBundles() { $options = []; $entity_types = static::getSupportedEntityTypes(); - /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */ - $entity_type_manager = \Drupal::service('entity_type.manager'); - /** @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info */ - $bundle_info = \Drupal::service('entity_type.bundle.info'); - $metatags_defaults_manager = $entity_type_manager->getStorage('metatag_defaults'); + $metatags_defaults_manager = $this->entityTypeManager->getStorage('metatag_defaults'); foreach ($entity_types as $entity_type => $entity_label) { if (empty($metatags_defaults_manager->load($entity_type))) { $options[$entity_label][$entity_type] = "$entity_label (Default)"; } - $bundles = $bundle_info->getBundleInfo($entity_type); + $bundles = $this->entityTypeBundleInfo->getBundleInfo($entity_type); foreach ($bundles as $bundle_id => $bundle_metadata) { $metatag_defaults_id = $entity_type . '__' . $bundle_id; diff --git a/web/modules/metatag/src/MetatagManager.php b/web/modules/metatag/src/MetatagManager.php index bafd6f9f3db64fb542dcf4a3d4ff53c83e052ed9..53399bc9a8079c9cb99afef31b97d19588fd68e5 100644 --- a/web/modules/metatag/src/MetatagManager.php +++ b/web/modules/metatag/src/MetatagManager.php @@ -7,6 +7,8 @@ use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Logger\LoggerChannelFactoryInterface; +use Drupal\Core\Render\BubbleableMetadata; +use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\views\ViewEntityInterface; /** @@ -16,6 +18,8 @@ */ class MetatagManager implements MetatagManagerInterface { + use StringTranslationTrait; + /** * The group plugin manager. * @@ -51,6 +55,13 @@ class MetatagManager implements MetatagManagerInterface { */ protected $logger; + /** + * Caches processed strings, keyed by tag name. + * + * @var array + */ + protected $processedTokenCache = []; + /** * Constructor for MetatagManager. * @@ -66,10 +77,11 @@ class MetatagManager implements MetatagManagerInterface { * The EntityTypeManagerInterface object. */ public function __construct(MetatagGroupPluginManager $groupPluginManager, - MetatagTagPluginManager $tagPluginManager, - MetatagToken $token, - LoggerChannelFactoryInterface $channelFactory, - EntityTypeManagerInterface $entityTypeManager) { + MetatagTagPluginManager $tagPluginManager, + MetatagToken $token, + LoggerChannelFactoryInterface $channelFactory, + EntityTypeManagerInterface $entityTypeManager + ) { $this->groupPluginManager = $groupPluginManager; $this->tagPluginManager = $tagPluginManager; $this->tokenService = $token; @@ -126,7 +138,7 @@ public function defaultTagsFromEntity(ContentEntityInterface $entity) { /** @var \Drupal\metatag\Entity\MetatagDefaults $metatags */ $metatags = $this->metatagDefaults->load('global'); if (!$metatags || !$metatags->status()) { - return NULL; + return []; } // Add/overwrite with tags set on the entity type. /** @var \Drupal\metatag\Entity\MetatagDefaults $entity_type_tags */ @@ -245,13 +257,19 @@ public function sortedGroupsWithTags() { /** * {@inheritdoc} */ - public function form(array $values, array $element, array $token_types = [], array $included_groups = NULL, array $included_tags = NULL) { + public function form(array $values, array $element, array $token_types = [], array $included_groups = NULL, array $included_tags = NULL, $verbose_help = FALSE) { // Add the outer fieldset. $element += [ '#type' => 'details', ]; - $element += $this->tokenService->tokenBrowser($token_types); + // Add a title to the form. + $element['preamble'] = [ + '#markup' => '<p><strong>' . $this->t('Configure the meta tags below.') . '</strong></p>', + '#weight' => -11, + ]; + + $element += $this->tokenService->tokenBrowser($token_types, $verbose_help); $groups_and_tags = $this->sortedGroupsWithTags(); @@ -493,11 +511,13 @@ public function generateElements(array $tags, $entity = NULL) { * The array of tags as plugin_id => value. * @param object $entity * Optional entity object to use for token replacements. + * @param \Drupal\Core\Render\BubbleableMetadata|null $cache + * (optional) Cacheability metadata. * * @return array * Render array with tag elements. */ - public function generateRawElements(array $tags, $entity = NULL) { + public function generateRawElements(array $tags, $entity = NULL, BubbleableMetadata $cache = NULL) { // Ignore the update.php path. $request = \Drupal::request(); if ($request->getBaseUrl() == '/update.php') { @@ -527,17 +547,21 @@ public function generateRawElements(array $tags, $entity = NULL) { $metatag_tags = $this->tagPluginManager->getDefinitions(); - // Order the elements by weight first, as some systems like Facebook care. - uksort($tags, function ($tag_name_a, $tag_name_b) use ($metatag_tags) { - $weight_a = isset($metatag_tags[$tag_name_a]['weight']) ? $metatag_tags[$tag_name_a]['weight'] : 0; - $weight_b = isset($metatag_tags[$tag_name_b]['weight']) ? $metatag_tags[$tag_name_b]['weight'] : 0; + // Order metatags based on the group and weight. + $group = array_column($metatag_tags, 'group'); + $weight = array_column($metatag_tags, 'weight'); + array_multisort($group, SORT_ASC, $weight, SORT_ASC, $metatag_tags); - return ($weight_a < $weight_b) ? -1 : 1; - }); + $ordered_tags = []; + foreach ($metatag_tags as $metatag) { + if (isset($tags[$metatag['id']])) { + $ordered_tags[$metatag['id']] = $tags[$metatag['id']]; + } + } // Each element of the $values array is a tag with the tag plugin name as // the key. - foreach ($tags as $tag_name => $value) { + foreach ($ordered_tags as $tag_name => $value) { // Check to ensure there is a matching plugin. if (isset($metatag_tags[$tag_name])) { // Get an instance of the plugin. @@ -550,7 +574,7 @@ public function generateRawElements(array $tags, $entity = NULL) { $tag->setValue($value); // Obtain the processed value. - $processed_value = PlainTextOutput::renderFromHtml(htmlspecialchars_decode($this->tokenService->replace($tag->value(), $token_replacements, ['langcode' => $langcode]))); + $processed_value = htmlspecialchars_decode($this->tokenService->replace($tag->value(), $token_replacements, ['langcode' => $langcode], $cache)); // Now store the value with processed tokens back into the plugin. $tag->setValue($processed_value); @@ -578,6 +602,69 @@ public function generateRawElements(array $tags, $entity = NULL) { return $rawTags; } + /** + * Generate the actual meta tag values for use as tokens. + * + * @param array $tags + * The array of tags as plugin_id => value. + * @param object $entity + * Optional entity object to use for token replacements. + * + * @return array + * Array of MetatagTag plugin instances. + */ + public function generateTokenValues(array $tags, $entity = NULL) { + // Ignore the update.php path. + $request = \Drupal::request(); + if ($request->getBaseUrl() == '/update.php') { + return []; + } + + $entity_identifier = '_none'; + if ($entity) { + $entity_identifier = $entity->getEntityTypeId() . ':' . ($entity->uuid() ?: $entity->id()); + } + + if (!isset($this->processedTokenCache[$entity_identifier])) { + $metatag_tags = $this->tagPluginManager->getDefinitions(); + + // Each element of the $values array is a tag with the tag plugin name as + // the key. + foreach ($tags as $tag_name => $value) { + // Check to ensure there is a matching plugin. + if (isset($metatag_tags[$tag_name])) { + // Get an instance of the plugin. + $tag = $this->tagPluginManager->createInstance($tag_name); + + // Render any tokens in the value. + $token_replacements = []; + if ($entity) { + // @todo This needs a better way of discovering the context. + if ($entity instanceof ViewEntityInterface) { + // Views tokens require the ViewExecutable, not the config entity. + // @todo Can we move this into metatag_views somehow? + $token_replacements = ['view' => $entity->getExecutable()]; + } + elseif ($entity instanceof ContentEntityInterface) { + $token_replacements = [$entity->getEntityTypeId() => $entity]; + } + } + + // Set the value as sometimes the data needs massaging, such as when + // field defaults are used for the Robots field, which come as an + // array that needs to be filtered and converted to a string. + // @see Robots::setValue() + $tag->setValue($value); + $langcode = \Drupal::languageManager()->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId(); + $value = PlainTextOutput::renderFromHtml(htmlspecialchars_decode($this->tokenService->replace($value, $token_replacements, ['langcode' => $langcode]))); + $this->processedTokenCache[$entity_identifier][$tag_name] = $tag->multiple() ? explode(',', $value) : $value; + } + } + } + + return $this->processedTokenCache[$entity_identifier]; + } + /** * Returns a list of fields handled by Metatag. * diff --git a/web/modules/metatag/src/MetatagManagerInterface.php b/web/modules/metatag/src/MetatagManagerInterface.php index 0cc060cbc35fdd5a1e84133473eda2d148a7b8fb..dd0f7ca1c340cf09f77b639c91121fafa2faf891 100644 --- a/web/modules/metatag/src/MetatagManagerInterface.php +++ b/web/modules/metatag/src/MetatagManagerInterface.php @@ -89,10 +89,13 @@ public function sortedGroupsWithTags(); * Available group plugins. * @param array $included_tags * Available tag plugins. + * @param bool $verbose_help + * Whether to include extra help text at the top of the form or keep it + * short. * * @return array * Render array for metatag form. */ - public function form(array $values, array $element, array $token_types = [], array $included_groups = NULL, array $included_tags = NULL); + public function form(array $values, array $element, array $token_types = [], array $included_groups = NULL, array $included_tags = NULL, $verbose_help = FALSE); } diff --git a/web/modules/metatag/src/MetatagToken.php b/web/modules/metatag/src/MetatagToken.php index de5e26868fa45026a6bc64b44a0bc448158f4779..f2b3fae7141fc007b0e9e749f4b511f8dd1398ce 100644 --- a/web/modules/metatag/src/MetatagToken.php +++ b/web/modules/metatag/src/MetatagToken.php @@ -77,17 +77,28 @@ public function replace($string, array $data = [], array $options = [], Bubbleab * * @param array $token_types * The token types to filter the tokens list by. Defaults to an empty array. + * @param bool $image_help + * Whether to include an extra message about how image field tokens should + * be processed. * * @return array * If token module is installed, a popup browser plus a help text. If not * only the help text. */ - public function tokenBrowser(array $token_types = []) { + public function tokenBrowser(array $token_types = [], $image_help = FALSE) { $form = []; $form['intro_text'] = [ - '#markup' => '<p>' . $this->t('<strong>Configure the meta tags below.</strong><br /> To view a summary of the individual meta tags and the pattern for a specific configuration, click on its name below. Use tokens to avoid redundant meta data and search engine penalization. For example, a \'keyword\' value of "example" will be shown on all content using this configuration, whereas using the [node:field_keywords] automatically inserts the "keywords" values from the current entity (node, term, etc).') . '</p>', + '#markup' => '<p>' . $this->t('Use tokens to avoid redundant meta data and search engine penalization. For example, a \'keyword\' value of "example" will be shown on all content using this configuration, whereas using the [node:field_keywords] automatically inserts the "keywords" values from the current entity (node, term, etc).') . '</p>', + // Define a specific weight. + '#weight' => -10, ]; + if ($image_help) { + $form['image_help'] = [ + '#markup' => '<p>' . $this->t('To use tokens to image fields, the image field on that entity bundle (content type, term, etc) must have the "Token" display settings enabled, the image field must not be hidden, and it must be set to output as an image, e.g. using the "Thumbnail" field formatter. It is also recommended to use an appropriate image style that resizes the image rather than output the original image; see individual meta tag descriptions for size recommendations.') . '</strong></p>', + '#weight' => -9, + ]; + } // Normalize token types. if (!empty($token_types)) { diff --git a/web/modules/metatag/src/Plugin/metatag/Tag/MetaNameBase.php b/web/modules/metatag/src/Plugin/metatag/Tag/MetaNameBase.php index f64476b516e57aa221c5cddf66ae11e4b56f9bfa..1c46c9a5fbaee2db49d27f2e8f8d8c40e7edf0f3 100644 --- a/web/modules/metatag/src/Plugin/metatag/Tag/MetaNameBase.php +++ b/web/modules/metatag/src/Plugin/metatag/Tag/MetaNameBase.php @@ -3,6 +3,7 @@ namespace Drupal\metatag\Plugin\metatag\Tag; use Drupal\Component\Plugin\PluginBase; +use Drupal\Component\Render\PlainTextOutput; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; @@ -277,7 +278,7 @@ public function form(array $element = []) { // Optional handling for images. if ((!empty($this->type())) && ($this->type() === 'image')) { - $form['#description'] .= ' ' . $this->t('This will be able to extract the URL from an image field.'); + $form['#description'] .= ' ' . $this->t('This will be able to extract the URL from an image field if the field is configured properly.'); } if (!empty($this->absolute_url)) { @@ -337,8 +338,14 @@ public function output() { return $this->multiple() ? [] : ''; } - // Parse out the image URL, if needed. - $value = $this->parseImageUrl(); + // If this contains embedded image tags, extract the image URLs. + if ($this->type() === 'image') { + $value = $this->parseImageUrl($this->value); + } + else { + $value = PlainTextOutput::renderFromHtml($this->value); + } + $values = $this->multiple() ? explode(',', $value) : [$value]; $elements = []; foreach ($values as $value) { @@ -391,41 +398,43 @@ public static function validateTag(array &$element, FormStateInterface $form_sta * A comma separated list of any image URLs found in the meta tag's value, * or the original string if no images were identified. */ - protected function parseImageUrl() { - $value = $this->value(); + protected function parseImageUrl($value) { + global $base_root; - // If this contains embedded image tags, extract the image URLs. - if ($this->type() === 'image') { - // If image tag src is relative (starts with /), convert to an absolute - // link; ignore protocol-relative URLs. - global $base_root; - if (strpos($value, '<img src="/') !== FALSE && strpos($value, '<img src="//') === FALSE) { - $value = str_replace('<img src="/', '<img src="' . $base_root . '/', $value); - } + // If image tag src is relative (starts with /), convert to an absolute + // link; ignore protocol-relative URLs. + if (strpos($value, '<img src="/') !== FALSE && strpos($value, '<img src="//') === FALSE) { + $value = str_replace('<img src="/', '<img src="' . $base_root . '/', $value); + } - if ($this->multiple()) { - // Split the string into an array, remove empty items. - $values = array_filter(explode(',', $value)); - } - else { - $values = [$value]; - } + if ($this->multiple()) { + // Split the string into an array, remove empty items. + $values = array_filter(explode(',', $value)); + } + else { + $values = [$value]; + } - // Check through the value(s) to see if there are any image tags. - foreach ($values as $key => $val) { - $matches = []; - preg_match('/src="([^"]*)"/', $val, $matches); - if (!empty($matches[1])) { - $values[$key] = $matches[1]; - } + // Check through the value(s) to see if there are any image tags. + foreach ($values as $key => $val) { + $matches = []; + preg_match('/src="([^"]*)"/', $val, $matches); + if (!empty($matches[1])) { + $values[$key] = $matches[1]; } - $value = implode(',', $values); - // Remove any HTML tags that might remain. - $value = strip_tags($value); + // If an image wasn't found then remove any other HTML tags in the string. + else { + $values[$key] = PlainTextOutput::renderFromHtml($val); + } } - return $value; + // Make sure there aren't any blank items in the array. + $values = array_filter($values); + + // Convert the array back into a comma-delimited string before sending it + // back. + return implode(',', $values); } } diff --git a/web/modules/metatag/src/Plugin/metatag/Tag/Rating.php b/web/modules/metatag/src/Plugin/metatag/Tag/Rating.php index 7e536a4b6dbb2e39286dc862d79bbbc7b2993817..f813e8bbb80c0feb98a8eef9edb2020cb0d4b324 100644 --- a/web/modules/metatag/src/Plugin/metatag/Tag/Rating.php +++ b/web/modules/metatag/src/Plugin/metatag/Tag/Rating.php @@ -8,7 +8,7 @@ * @MetatagTag( * id = "rating", * label = @Translation("Rating"), - * description = @Translation("Used to rate content for audience appropriateness. This tag has little known influence on search engine rankings, but can be used by browsers, browser extentions, and apps. The <a href='https://www.metatags.org/meta_name_rating'>most common options</a> are general, mature, restricted, 14 years, safe for kids. If you follow the <a href='https://www.rtalabel.org/index.php?content=howto'>RTA Documentation</a> you should enter RTA-5042-1996-1400-1577-RTA"), + * description = @Translation("Used to rate content for audience appropriateness. This tag has little known influence on search engine rankings, but can be used by browsers, browser extensions, and apps. The <a href='https://www.metatags.org/meta_name_rating'>most common options</a> are general, mature, restricted, 14 years, safe for kids. If you follow the <a href='https://www.rtalabel.org/index.php?content=howto'>RTA Documentation</a> you should enter RTA-5042-1996-1400-1577-RTA"), * name = "rating", * group = "advanced", * weight = 5, diff --git a/web/modules/metatag/src/Plugin/metatag/Tag/Robots.php b/web/modules/metatag/src/Plugin/metatag/Tag/Robots.php index 01e59064d86a650b9e90c9f54fa5c6ed0e7572fe..bd333e0ff63df28bd8772dce19357a5e53a0626f 100644 --- a/web/modules/metatag/src/Plugin/metatag/Tag/Robots.php +++ b/web/modules/metatag/src/Plugin/metatag/Tag/Robots.php @@ -66,6 +66,34 @@ public function form(array $element = []) { 'noimageindex' => $this->t('noimageindex - Prevent search engines from indexing images on this page.'), 'notranslate' => $this->t('notranslate - Prevent search engines from offering to translate this page in search results.'), ], + 'index' => [ + '#states' => [ + 'disabled' => [ + ':input[name="robots[noindex]"]' => ['checked' => TRUE], + ], + ], + ], + 'noindex' => [ + '#states' => [ + 'disabled' => [ + ':input[name="robots[index]"]' => ['checked' => TRUE], + ], + ], + ], + 'follow' => [ + '#states' => [ + 'disabled' => [ + ':input[name="robots[nofollow]"]' => ['checked' => TRUE], + ], + ], + ], + 'nofollow' => [ + '#states' => [ + 'disabled' => [ + ':input[name="robots[follow]"]' => ['checked' => TRUE], + ], + ], + ], '#default_value' => $default_value, '#required' => isset($element['#required']) ? $element['#required'] : FALSE, '#element_validate' => [[get_class($this), 'validateTag']], diff --git a/web/modules/metatag/src/Plugin/migrate/process/d6/NodewordsEntities.php b/web/modules/metatag/src/Plugin/migrate/process/d6/NodewordsEntities.php index 9159d3fbd0e21bfda3c2af876457b853da164883..4121c442ee4de6f2b96c60c21c56ae6bf4c73a5a 100644 --- a/web/modules/metatag/src/Plugin/migrate/process/d6/NodewordsEntities.php +++ b/web/modules/metatag/src/Plugin/migrate/process/d6/NodewordsEntities.php @@ -438,6 +438,7 @@ public function tagsMap() { // From metatag_verification.metatag.inc: 'baidu-site-verification' => 'baidu', + 'facebook-domain-verification' => 'facebook_domain_verification', 'google-site-verification' => 'bing', 'msvalidate.01' => 'google', 'norton-safeweb-site-verification' => 'norton_safe_web', diff --git a/web/modules/metatag/src/Plugin/migrate/process/d7/MetatagEntities.php b/web/modules/metatag/src/Plugin/migrate/process/d7/MetatagEntities.php index 2f55a55bcab49ff40f0b1adcc7fc20f28b98f957..c5df768f8a521991d94e0721c91076d2aa7aebfd 100644 --- a/web/modules/metatag/src/Plugin/migrate/process/d7/MetatagEntities.php +++ b/web/modules/metatag/src/Plugin/migrate/process/d7/MetatagEntities.php @@ -424,6 +424,7 @@ protected function tagsMap() { // From metatag_verification.metatag.inc: 'baidu-site-verification' => 'baidu', + 'facebook-domain-verification' => 'facebook_domain_verification', 'google-site-verification' => 'bing', 'msvalidate.01' => 'google', 'norton-safeweb-site-verification' => 'norton_safe_web', diff --git a/web/modules/metatag/src/Plugin/migrate/source/d7/MetatagFieldDeriver.php b/web/modules/metatag/src/Plugin/migrate/source/d7/MetatagFieldDeriver.php new file mode 100644 index 0000000000000000000000000000000000000000..294dc2b476d7b25a154d23b23d275ea3945a96b2 --- /dev/null +++ b/web/modules/metatag/src/Plugin/migrate/source/d7/MetatagFieldDeriver.php @@ -0,0 +1,91 @@ +<?php + +namespace Drupal\metatag\Plugin\migrate\source\d7; + +use Drupal\Component\Plugin\Derivative\DeriverBase; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\migrate\Exception\RequirementsException; +use Drupal\migrate\Plugin\MigrationDeriverTrait; +use Drupal\migrate_drupal\MigrationConfigurationTrait; +use Drupal\migrate_drupal\NodeMigrateType; +use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * @todo. + */ +class MetatagFieldDeriver extends DeriverBase implements ContainerDeriverInterface { + + use MigrationDeriverTrait; + use MigrationConfigurationTrait; + use StringTranslationTrait; + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * Constructs a MetatagFieldDeriver instance. + * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + */ + public function __construct(EntityTypeManagerInterface $entity_type_manager) { + $this->entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, $base_plugin_id) { + return new static( + $container->get('entity_type.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function getDerivativeDefinitions($base_plugin_definition) { + $source = $this->getSourcePlugin('d7_metatag_field'); + assert($source instanceof DrupalSqlBase); + + try { + $source->checkRequirements(); + } + catch (RequirementsException $e) { + // If the source plugin requirements failed, that means we do not have a + // Drupal source database configured - return nothing. + return $this->derivatives; + } + + $entity_type_ids = $source->getDatabase()->select('metatag', 'm') + ->fields('m', ['entity_type']) + ->distinct() + ->execute() + ->fetchAllKeyed(0, 0); + foreach ($entity_type_ids as $entity_type_id) { + // Skip if the entity type is missing. + if (!($entity_type_definition = $this->entityTypeManager->getDefinition($entity_type_id, FALSE))) { + continue; + } + + $this->derivatives[$entity_type_id] = $base_plugin_definition; + $this->derivatives[$entity_type_id]['source']['entity_type'] = $entity_type_id; + $this->derivatives[$entity_type_id]['source']['entity_type_id'] = $entity_type_id; + $this->derivatives[$entity_type_id]['label'] = $this->t('@label of @type', [ + '@label' => $base_plugin_definition['label'], + '@type' => $this->entityTypeManager->getDefinition($entity_type_id)->getPluralLabel(), + ]); + } + + return $this->derivatives; + } + +} diff --git a/web/modules/metatag/src/Plugin/migrate/source/d7/MetatagFieldInstance.php b/web/modules/metatag/src/Plugin/migrate/source/d7/MetatagFieldInstance.php index 217f49c95b4a710db5d34fbc6c6abd6f7f66c11f..015f28ce9a097c5736e5104d5d785a4b303ce66a 100644 --- a/web/modules/metatag/src/Plugin/migrate/source/d7/MetatagFieldInstance.php +++ b/web/modules/metatag/src/Plugin/migrate/source/d7/MetatagFieldInstance.php @@ -48,9 +48,45 @@ public function setEntityTypeBundleInfo(EntityTypeBundleInfoInterface $entity_ty * {@inheritdoc} */ public function query() { - return $this->select('metatag', 'm') + $base_query = $this->select('metatag', 'm') ->fields('m', ['entity_type']) ->groupBy('entity_type'); + + if (isset($this->configuration['entity_type_id'])) { + $entity_type_id = $this->configuration['entity_type_id']; + $base_query->condition('m.entity_type', $entity_type_id); + + if (isset($this->configuration['bundle'])) { + $bundle = $this->configuration['bundle']; + switch ($entity_type_id) { + case 'node': + // We want to get a per-node-type metatag migration. So we inner join + // the base query on node table based on the parsed node ID. + $base_query->join('node', 'n', "n.nid = m.entity_id"); + $base_query->condition('n.type', $bundle); + $base_query->addField('n', 'type', 'bundle'); + $base_query->groupBy('bundle'); + break; + + case 'taxonomy_term': + // Join the taxonomy term data table to the base query; based on + // the parsed taxonomy term ID. + $base_query->join('taxonomy_term_data', 'ttd', "ttd.tid = m.entity_id"); + $base_query->fields('ttd', ['vid']); + // Since the "taxonomy_term_data" table contains only the taxonomy + // vocabulary ID, but not the vocabulary name, we have to inner + // join the "taxonomy_vocabulary" table as well. + $base_query->join('taxonomy_vocabulary', 'tv', 'ttd.vid = tv.vid'); + $base_query->condition('tv.machine_name', $bundle); + $base_query->addField('tv', 'machine_name', 'bundle'); + $base_query->groupBy('ttd.vid'); + $base_query->groupBy('bundle'); + break; + } + } + } + + return $base_query; } /** @@ -69,6 +105,14 @@ public function fields() { public function initializeIterator() { $bundles = []; foreach (parent::initializeIterator() as $instance) { + // For entity types for which we support creating derivatives, do not + // retrieve the bundles using the D8|9 entity type bundle info service, + // because then we will end up creating meta tag fields even for bundles + // that do not use meta tags. + if (isset($instance['bundle'])) { + $bundles[] = $instance; + continue; + } $bundle_info = $this->entityTypeBundleInfo ->getBundleInfo($instance['entity_type']); foreach (array_keys($bundle_info) as $bundle) { diff --git a/web/modules/metatag/src/Plugin/migrate/source/d7/MetatagFieldInstanceDeriver.php b/web/modules/metatag/src/Plugin/migrate/source/d7/MetatagFieldInstanceDeriver.php new file mode 100644 index 0000000000000000000000000000000000000000..832559232a1a1770b3f5ebf1e8b4402066cba50a --- /dev/null +++ b/web/modules/metatag/src/Plugin/migrate/source/d7/MetatagFieldInstanceDeriver.php @@ -0,0 +1,210 @@ +<?php + +namespace Drupal\metatag\Plugin\migrate\source\d7; + +use Drupal\Component\Plugin\Derivative\DeriverBase; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\migrate\Exception\RequirementsException; +use Drupal\migrate\Plugin\MigrationDeriverTrait; +use Drupal\migrate_drupal\MigrationConfigurationTrait; +use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Deriver for d7_metatag_field_instance and d7_metatag_field_instance_widget_settings. + */ +class MetatagFieldInstanceDeriver extends DeriverBase implements ContainerDeriverInterface { + + use MigrationDeriverTrait; + use MigrationConfigurationTrait; + use StringTranslationTrait; + + /** + * Required entity type DB tables, keyed by the entity type ID. + * + * @var string[][] + */ + protected $supportedEntityTypesTables = [ + 'node' => ['node', 'node_type'], + 'taxonomy_term' => ['taxonomy_term_data', 'taxonomy_vocabulary'], + 'user' => [], + ]; + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * Constructs a PathRedirectDeriver instance. + * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + */ + public function __construct(EntityTypeManagerInterface $entity_type_manager) { + $this->entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, $base_plugin_id) { + return new static( + $container->get('entity_type.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function getDerivativeDefinitions($base_plugin_definition) { + $source = $this->getSourcePlugin($base_plugin_definition['source']['plugin']); + assert($source instanceof DrupalSqlBase); + + try { + $source->checkRequirements(); + } + catch (RequirementsException $e) { + // If the source plugin requirements failed, that means we do not have a + // Drupal source database configured - return nothing. + return $this->derivatives; + } + foreach ($this->supportedEntityTypesTables as $entity_type_id => $entity_type_tables) { + // Skip if the entity type is missing. + if (!$this->entityTypeManager->hasDefinition($entity_type_id)) { + continue; + } + + // Skip if the required database tables are missing. + foreach ($entity_type_tables as $entity_type_table) { + if (!$source->getDatabase()->schema()->tableExists($entity_type_table)) { + continue; + } + } + + $base_query = $source->getDatabase()->select('metatag', 'm'); + $base_query->condition('m.entity_type', $entity_type_id); + + // If there are no metatags for this entity type, no derivatives needed. + $metatag_count = (int) (clone $base_query)->countQuery()->execute()->fetchField(); + if ($metatag_count === 0) { + continue; + } + + $metatags_grouped_by_bundle = []; + switch ($entity_type_id) { + case 'node': + // We want to get a per-node-type metatag migration. So we inner join + // the base query on node table based on the parsed node ID. + $base_query->join('node', 'n', "n.nid = m.entity_id"); + $base_query->fields('n', ['type']); + // We'll need the "human" name of the node type. + $base_query->join('node_type', 'nt', 'nt.type = n.type'); + $base_query->fields('nt', ['name']); + $base_query->groupBy('n.type'); + $base_query->groupBy('nt.name'); + + // Get every node-related metatag, grouped by node type. + $rows = $base_query->execute()->fetchAllAssoc('type'); + $metatags_grouped_by_bundle = array_reduce($rows, function (array $carry, $row) { + $carry[$row->type] = $row->name; + + return $carry; + }, []); + break; + + case 'taxonomy_term': + // Join the taxonomy term data table to the base query; based on + // the parsed taxonomy term ID. + $base_query->join('taxonomy_term_data', 'ttd', "ttd.tid = m.entity_id"); + $base_query->fields('ttd', ['vid']); + // Since the "taxonomy_term_data" table contains only the taxonomy + // vocabulary ID, but not the vocabulary name, we have to inner + // join the "taxonomy_vocabulary" table as well. + $base_query->join('taxonomy_vocabulary', 'tv', 'ttd.vid = tv.vid'); + $base_query->fields('tv', ['machine_name', 'name']); + $base_query->groupBy('ttd.vid'); + $base_query->groupBy('tv.machine_name'); + $base_query->groupBy('tv.name'); + + // Get all of the metatags whose destination is a taxonomy + // term URL. + $rows = $base_query->execute()->fetchAllAssoc('machine_name'); + $metatags_grouped_by_bundle = array_reduce($rows, function (array $carry, $row) { + $carry[$row->machine_name] = $row->name; + + return $carry; + }, []); + break; + } + + // If we have per-bundle results for a content entity type, we are + // able to derive migrations per entity type and bundle. + // Dependency metadata is added in metatag_migration_plugins_alter(). + if (!empty($metatags_grouped_by_bundle)) { + foreach ($metatags_grouped_by_bundle as $bundle_id => $bundle_label) { + $derivative_id = "$entity_type_id:$bundle_id"; + $this->derivatives[$derivative_id] = $base_plugin_definition; + $this->derivatives[$derivative_id]['source']['entity_type_id'] = $entity_type_id; + $this->derivatives[$derivative_id]['source']['entity_type'] = $entity_type_id; + $this->derivatives[$derivative_id]['source']['bundle'] = $bundle_id; + $this->derivatives[$derivative_id]['label'] = $this->t('@label of @type @entity-type-label', [ + '@label' => $base_plugin_definition['label'], + '@type' => $bundle_label, + '@entity-type-label' => $this->entityTypeManager->getDefinition($entity_type_id)->getPluralLabel(), + ]); + // :<entity type ID>:<bundle> suffix for dependencies on + // d7_metatag_field_instance. + foreach (['d7_metatag_field_instance'] as $dep_id) { + $dependency_index = array_search($dep_id, $this->derivatives[$derivative_id]['migration_dependencies']['required']); + if ($dependency_index !== FALSE) { + $this->derivatives[$derivative_id]['migration_dependencies']['required'][$dependency_index] .= ":$entity_type_id:$bundle_id"; + } + } + // Add bundle dependency. + switch ($entity_type_id) { + case 'node': + $this->derivatives[$derivative_id]['migration_dependencies']['required'][] = "d7_node_type:$bundle_id"; + break; + case 'taxonomy_term': + $this->derivatives[$derivative_id]['migration_dependencies']['required'][] = "d7_taxonomy_vocabulary:$bundle_id"; + break; + } + + // :<entity type ID> suffix for dependencies on d7_metatag_field. + foreach (['d7_metatag_field'] as $dep_id) { + $dependency_index = array_search($dep_id, $this->derivatives[$derivative_id]['migration_dependencies']['required']); + if ($dependency_index !== FALSE) { + $this->derivatives[$derivative_id]['migration_dependencies']['required'][$dependency_index] .= ":$entity_type_id"; + } + } + } + } + // If we don't have per-bundle results, we will derive only a + // per-entity-type metatag migration. + else { + $this->derivatives[$entity_type_id] = $base_plugin_definition; + $this->derivatives[$entity_type_id]['source']['entity_type_id'] = $entity_type_id; + $this->derivatives[$entity_type_id]['label'] = $this->t('@label of @type', [ + '@label' => $base_plugin_definition['label'], + '@type' => $this->entityTypeManager->getDefinition($entity_type_id)->getPluralLabel(), + ]); + // :<entity type ID> suffix for dependencies on + // d7_metatag_field and d7_metatag_field_instance. + foreach (['d7_metatag_field', 'd7_metatag_field_instance'] as $dep_id) { + $dependency_index = array_search($dep_id, $this->derivatives[$entity_type_id]['migration_dependencies']['required']); + if ($dependency_index !== FALSE) { + $this->derivatives[$entity_type_id]['migration_dependencies']['required'][$dependency_index] .= ":$entity_type_id"; + } + } + } + } + + return $this->derivatives; + } + +} diff --git a/web/modules/metatag/tests/modules/metatag_test_custom_route/metatag_test_custom_route.info.yml b/web/modules/metatag/tests/modules/metatag_test_custom_route/metatag_test_custom_route.info.yml index 86e1739af56dec4c6a649bc397a1431f4be45c6b..dc263b855278e883973063cccd99998098a45683 100644 --- a/web/modules/metatag/tests/modules/metatag_test_custom_route/metatag_test_custom_route.info.yml +++ b/web/modules/metatag/tests/modules/metatag_test_custom_route/metatag_test_custom_route.info.yml @@ -6,7 +6,7 @@ package: Testing dependencies: - metatag:metatag -# Information added by Drupal.org packaging script on 2020-08-11 -version: '8.x-1.14' +# Information added by Drupal.org packaging script on 2020-12-05 +version: '8.x-1.15' project: 'metatag' -datestamp: 1597183854 +datestamp: 1607188982 diff --git a/web/modules/metatag/tests/modules/metatag_test_integration/metatag_test_integration.info.yml b/web/modules/metatag/tests/modules/metatag_test_integration/metatag_test_integration.info.yml index abed889c2c685df572ed7061a63e259657426d0c..ac185d1e1b797011c25fdfd544435acd9ed50a74 100644 --- a/web/modules/metatag/tests/modules/metatag_test_integration/metatag_test_integration.info.yml +++ b/web/modules/metatag/tests/modules/metatag_test_integration/metatag_test_integration.info.yml @@ -5,7 +5,7 @@ package: Testing dependencies: - metatag:metatag -# Information added by Drupal.org packaging script on 2020-08-11 -version: '8.x-1.14' +# Information added by Drupal.org packaging script on 2020-12-05 +version: '8.x-1.15' project: 'metatag' -datestamp: 1597183854 +datestamp: 1607188982 diff --git a/web/modules/metatag/tests/modules/metatag_test_tag/metatag_test_tag.info.yml b/web/modules/metatag/tests/modules/metatag_test_tag/metatag_test_tag.info.yml index 61c1e2a62cce812bfcd79a17961837f155df06a2..94ea3713404798d00b7629d2d3ea5cffe870e9c6 100644 --- a/web/modules/metatag/tests/modules/metatag_test_tag/metatag_test_tag.info.yml +++ b/web/modules/metatag/tests/modules/metatag_test_tag/metatag_test_tag.info.yml @@ -6,7 +6,7 @@ package: Testing dependencies: - metatag:metatag -# Information added by Drupal.org packaging script on 2020-08-11 -version: '8.x-1.14' +# Information added by Drupal.org packaging script on 2020-12-05 +version: '8.x-1.15' project: 'metatag' -datestamp: 1597183854 +datestamp: 1607188982 diff --git a/web/modules/metatag/tests/src/Functional/DefaultTags.php b/web/modules/metatag/tests/src/Functional/DefaultTags.php index f528983f278bd948a661f7a51a46f74fe961a7ca..53abe27dde56d4c3025f87be2eac1534801a1fdf 100644 --- a/web/modules/metatag/tests/src/Functional/DefaultTags.php +++ b/web/modules/metatag/tests/src/Functional/DefaultTags.php @@ -145,7 +145,7 @@ public function testUserLoginPages() { foreach ($routes as $route) { // Identify the path to load. $this_page_url = $this->buildUrl($route, ['absolute' => TRUE]); - $this->assertTrue(!empty($this_page_url)); + $this->assertNotEmpty($this_page_url); // Load the path. $this->drupalGet($this_page_url); diff --git a/web/modules/metatag/tests/src/Functional/MetatagAdminTest.php b/web/modules/metatag/tests/src/Functional/MetatagAdminTest.php index 2ce4f22943666a279df8fb7b18d3ba6a7b801151..79e0e292a4cfdd10c2b5d3d6e8462e7c0b811e10 100644 --- a/web/modules/metatag/tests/src/Functional/MetatagAdminTest.php +++ b/web/modules/metatag/tests/src/Functional/MetatagAdminTest.php @@ -188,13 +188,13 @@ public function testAvailableConfigEntities() { // Check through the values that are in the 'select' list, make sure that // unwanted items are not present. - $this->assertFalse(isset($types['block_content']), 'Custom block entities are not supported.'); - $this->assertFalse(isset($types['comment']), 'Comment entities are not supported.'); - $this->assertFalse(isset($types['menu_link_content']), 'Menu link entities are not supported.'); - $this->assertFalse(isset($types['shortcut']), 'Shortcut entities are not supported.'); - $this->assertTrue(isset($types['node__page']), 'Nodes are supported.'); - $this->assertTrue(isset($types['user__user']), 'Users are supported.'); - $this->assertTrue(isset($types['entity_test']), 'Test entities are supported.'); + $this->assertArrayNotHasKey('block_content', $types, 'Custom block entities are not supported.'); + $this->assertArrayNotHasKey('comment', $types, 'Comment entities are not supported.'); + $this->assertArrayNotHasKey('menu_link_content', $types, 'Menu link entities are not supported.'); + $this->assertArrayNotHasKey('shortcut', $types, 'Shortcut entities are not supported.'); + $this->assertArrayHasKey('node__page', $types, 'Nodes are supported.'); + $this->assertArrayHasKey('user__user', $types, 'Users are supported.'); + $this->assertArrayHasKey('entity_test', $types, 'Test entities are supported.'); } /** diff --git a/web/modules/metatag/tests/src/Functional/MetatagConfigTranslationTest.php b/web/modules/metatag/tests/src/Functional/MetatagConfigTranslationTest.php index db6128b2d13808af692e55de50ddafdc58d26d0b..a3179c09e79627dcee05b6f5d08d114385e540b8 100644 --- a/web/modules/metatag/tests/src/Functional/MetatagConfigTranslationTest.php +++ b/web/modules/metatag/tests/src/Functional/MetatagConfigTranslationTest.php @@ -2,6 +2,8 @@ namespace Drupal\Tests\metatag\Functional; +use Drupal\language\Entity\ConfigurableLanguage; +use Drupal\metatag\Entity\MetatagDefaults; use Drupal\Tests\BrowserTestBase; use Drupal\Core\StringTranslation\StringTranslationTrait; @@ -37,6 +39,7 @@ class MetatagConfigTranslationTest extends BrowserTestBase { 'metatag', 'language', 'config_translation', + 'node', ]; /** @@ -72,16 +75,7 @@ protected function setUp() { $this->drupalLogin($this->adminUser); // Enable the French language. - $this->drupalGet('admin/config/regional/language/add'); - $this->assertSession()->statusCodeEquals(200); - $edit = [ - 'predefined_langcode' => 'fr', - ]; - $this->drupalPostForm(NULL, $edit, $this->t('Add language')); - $this->assertRaw($this->t( - 'The language %language has been created and can now be used.', - ['%language' => $this->t('French')] - )); + ConfigurableLanguage::createFromLangcode('fr')->save(); } /** @@ -163,6 +157,31 @@ public function testConfigTranslations() { $this->drupalPostForm(NULL, $edit, $this->t('Save translation')); $this->assertSession()->statusCodeEquals(200); $this->assertText($this->t('Successfully saved French translation')); + + // Delete the node metatag defaults to simplify the test. + MetatagDefaults::load('node')->delete(); + + // Create a node in french, request default tags for it. Ensure that the + // config translation language is afterwards still/again set to EN and + // tags are returned in FR. + $this->drupalCreateContentType(['type' => 'page']); + $node = $this->drupalCreateNode([ + 'title' => 'Metatag Test FR', + 'langcode' => 'fr', + ]); + + $language_manager = \Drupal::languageManager(); + $this->assertEquals('en', $language_manager->getConfigOverrideLanguage()->getId()); + $fr_default_tags = metatag_get_default_tags($node); + $this->assertEquals('Le title', $fr_default_tags['title']); + $this->assertEquals('Le description', $fr_default_tags['description']); + $this->assertEquals('en', $language_manager->getConfigOverrideLanguage()->getId()); + + // Delete the default tags as well to test the early return. + MetatagDefaults::load('global')->delete(); + $fr_default_tags = metatag_get_default_tags($node); + $this->assertNull($fr_default_tags); + $this->assertEquals('en', $language_manager->getConfigOverrideLanguage()->getId()); } } diff --git a/web/modules/metatag/tests/src/Functional/MetatagCustomRouteTest.php b/web/modules/metatag/tests/src/Functional/MetatagCustomRouteTest.php index 3f39e019c019833385df5ba1cb892c5321377124..bd3d2eaaaaef8d26f4418d3e71d778a75e573b85 100644 --- a/web/modules/metatag/tests/src/Functional/MetatagCustomRouteTest.php +++ b/web/modules/metatag/tests/src/Functional/MetatagCustomRouteTest.php @@ -56,7 +56,7 @@ public function testCustomRoute() { $this->drupalGet('metatag_test_custom_route/' . $entity_test->id()); $this->assertSession()->statusCodeEquals(200); $xpath = $this->xpath("//meta[@name='keywords']"); - $this->assertEqual(count($xpath), 1); + $this->assertCount(1, $xpath); $this->assertEqual($xpath[0]->getAttribute('content'), 'test'); } diff --git a/web/modules/metatag/tests/src/Functional/MetatagFieldTestBase.php b/web/modules/metatag/tests/src/Functional/MetatagFieldTestBase.php index 96f55b7d30f8b79686939f815d488fbde1a36a40..83dd14944248bcaea338818e3a900c263475e7dd 100644 --- a/web/modules/metatag/tests/src/Functional/MetatagFieldTestBase.php +++ b/web/modules/metatag/tests/src/Functional/MetatagFieldTestBase.php @@ -354,7 +354,7 @@ public function testEntityFieldValuesOldEntity() { $entities = \Drupal::entityTypeManager() ->getStorage($this->entityType) ->loadByProperties([$this->entityTitleField => $title]); - $this->assertEqual(1, count($entities), 'Entity was saved'); + $this->assertCount(1, $entities, 'Entity was saved'); $entity = reset($entities); // @todo Confirm the values output correctly. @@ -436,7 +436,7 @@ public function testEntityFieldValuesNewEntity() { $entities = \Drupal::entityTypeManager() ->getStorage($this->entityType) ->loadByProperties([$this->entityTitleField => $title]); - $this->assertEqual(1, count($entities), 'Entity was saved'); + $this->assertCount(1, $entities, 'Entity was saved'); $entity = reset($entities); // @todo Confirm the values output correctly. @@ -477,7 +477,7 @@ public function tofixTestEntityField() { $entities = \Drupal::entityTypeManager() ->getStorage('entity_test') ->loadByProperties([$this->entityTitleField => 'Barfoo']); - $this->assertEqual(1, count($entities), 'Entity was saved'); + $this->assertCount(1, $entities, 'Entity was saved'); $entity = reset($entities); // Make sure tags that have a field value but no default value still show @@ -485,7 +485,7 @@ public function tofixTestEntityField() { $this->drupalGet($entity->toUrl()); $this->assertSession()->statusCodeEquals(200); $elements = $this->cssSelect('meta[name=metatag_test_tag]'); - $this->assertTrue(count($elements) === 1, 'Found keywords metatag_test_tag from defaults'); + $this->assertCount(1, $elements, 'Found keywords metatag_test_tag from defaults'); $this->assertEqual((string) $elements[0]['content'], 'Kilimanjaro', 'Field value for metatag_test_tag found when no default set.'); // @todo This should not be required, but meta tags does not invalidate @@ -503,7 +503,7 @@ public function tofixTestEntityField() { $this->drupalGet($entity->toUrl()); $this->assertSession()->statusCodeEquals(200); $elements = $this->cssSelect('meta[name=metatag_test_tag]'); - $this->assertTrue(count($elements) === 1, 'Found test metatag from defaults'); + $this->assertCount(1, $elements, 'Found test metatag from defaults'); $this->verbose('<pre>' . print_r($elements, TRUE) . '</pre>'); $this->assertEqual((string) $elements[0]['content'], $values['metatag_test_tag']); } diff --git a/web/modules/metatag/tests/src/Functional/MetatagFrontpageTest.php b/web/modules/metatag/tests/src/Functional/MetatagFrontpageTest.php index 55c8fbab8426e54e9c4f82c83cb7c8e23729020b..50deeec347dc3af2b24121c1903eef02d7a7990c 100644 --- a/web/modules/metatag/tests/src/Functional/MetatagFrontpageTest.php +++ b/web/modules/metatag/tests/src/Functional/MetatagFrontpageTest.php @@ -79,13 +79,13 @@ public function testFrontPageMetatagsEnabledConfig() { foreach ($edit as $metatag => $metatag_value) { $xpath = $this->xpath("//meta[@name='" . $metatag . "']"); if ($metatag == 'title') { - $this->assertEqual(count($xpath), 0, 'Title meta tag not found.'); + $this->assertCount(0, $xpath, 'Title meta tag not found.'); $xpath = $this->xpath("//title"); - $this->assertEqual(count($xpath), 1, 'Head title tag found.'); + $this->assertCount(1, $xpath, 'Head title tag found.'); $value = $xpath[0]->getText(); } else { - $this->assertEqual(count($xpath), 1, 'Exactly one ' . $metatag . ' meta tag found.'); + $this->assertCount(1, $xpath, 'Exactly one ' . $metatag . ' meta tag found.'); $value = $xpath[0]->getAttribute('content'); } $this->assertEqual($value, $metatag_value); @@ -97,13 +97,13 @@ public function testFrontPageMetatagsEnabledConfig() { foreach ($edit as $metatag => $metatag_value) { $xpath = $this->xpath("//meta[@name='" . $metatag . "']"); if ($metatag == 'title') { - $this->assertEqual(count($xpath), 0, 'Title meta tag not found.'); + $this->assertCount(0, $xpath, 'Title meta tag not found.'); $xpath = $this->xpath("//title"); - $this->assertEqual(count($xpath), 1, 'Head title tag found.'); + $this->assertCount(1, $xpath, 'Head title tag found.'); $value = $xpath[0]->getText(); } else { - $this->assertEqual(count($xpath), 1, 'Exactly one ' . $metatag . ' meta tag found.'); + $this->assertCount(1, $xpath, 'Exactly one ' . $metatag . ' meta tag found.'); $value = $xpath[0]->getAttribute('content'); } $this->assertEqual($value, $metatag_value); @@ -124,7 +124,7 @@ public function testFrontPageMetatagsEnabledConfig() { $this->assertSession()->statusCodeEquals(200); foreach ($edit as $metatag => $metatag_value) { $xpath = $this->xpath("//meta[@name='" . $metatag . "']"); - $this->assertEqual(count($xpath), 1, 'Exactly one ' . $metatag . ' meta tag found.'); + $this->assertCount(1, $xpath, 'Exactly one ' . $metatag . ' meta tag found.'); $value = $xpath[0]->getAttribute('content'); $this->assertEqual($value, $metatag_value); } @@ -154,13 +154,13 @@ public function testFrontPageMetatagDisabledConfig() { foreach ($edit as $metatag => $metatag_value) { $xpath = $this->xpath("//meta[@name='" . $metatag . "']"); if ($metatag == 'title') { - $this->assertEqual(count($xpath), 0, 'Title meta tag not found.'); + $this->assertCount(0, $xpath, 'Title meta tag not found.'); $xpath = $this->xpath("//title"); - $this->assertEqual(count($xpath), 1, 'Head title tag found.'); + $this->assertCount(1, $xpath, 'Head title tag found.'); $value = $xpath[0]->getText(); } else { - $this->assertEqual(count($xpath), 1, 'Exactly one ' . $metatag . ' meta tag found.'); + $this->assertCount(1, $xpath, 'Exactly one ' . $metatag . ' meta tag found.'); $value = $xpath[0]->getAttribute('content'); } $this->assertEqual($value, $metatag_value); @@ -192,13 +192,13 @@ public function testFrontPageMetatagDisabledConfig() { foreach ($edit as $metatag => $metatag_value) { $xpath = $this->xpath("//meta[@name='" . $metatag . "']"); if ($metatag == 'title') { - $this->assertEqual(count($xpath), 0, 'Title meta tag not found.'); + $this->assertCount(0, $xpath, 'Title meta tag not found.'); $xpath = $this->xpath("//title"); - $this->assertEqual(count($xpath), 1, 'Head title tag found.'); + $this->assertCount(1, $xpath, 'Head title tag found.'); $value = $xpath[0]->getText(); } else { - $this->assertEqual(count($xpath), 1, 'Exactly one ' . $metatag . ' meta tag found.'); + $this->assertCount(1, $xpath, 'Exactly one ' . $metatag . ' meta tag found.'); $value = $xpath[0]->getAttribute('content'); } $this->assertEqual($value, $metatag_value); diff --git a/web/modules/metatag/tests/src/Functional/MetatagNodeTranslationTest.php b/web/modules/metatag/tests/src/Functional/MetatagNodeTranslationTest.php index 3e3419d048aade81255ba04297ead3cadcecfa78..8470d12f1fbfd5b484164e162efbf6e637d1d535 100644 --- a/web/modules/metatag/tests/src/Functional/MetatagNodeTranslationTest.php +++ b/web/modules/metatag/tests/src/Functional/MetatagNodeTranslationTest.php @@ -150,7 +150,7 @@ public function testMetatagValueTranslation() { $this->assertSession()->statusCodeEquals(200); $xpath = $this->xpath("//meta[@name='description']"); - $this->assertEqual(count($xpath), 1, 'Exactly one description meta tag found.'); + $this->assertCount(1, $xpath, 'Exactly one description meta tag found.'); $value = $xpath[0]->getAttribute('content'); $this->assertEqual($value, 'French summary.'); @@ -170,7 +170,7 @@ public function testMetatagValueTranslation() { $this->drupalGet('es/node/1'); $this->assertSession()->statusCodeEquals(200); $xpath = $this->xpath("//meta[@name='description']"); - $this->assertEqual(count($xpath), 1, 'Exactly one description meta tag found.'); + $this->assertCount(1, $xpath, 'Exactly one description meta tag found.'); $value = $xpath[0]->getAttribute('content'); $this->assertEqual($value, 'Spanish summary.'); $this->assertNotEqual($value, 'French summary.'); @@ -192,7 +192,7 @@ public function testMetatagValueTranslation() { $this->assertSession()->statusCodeEquals(200); $xpath = $this->xpath("//meta[@name='description']"); - $this->assertEqual(count($xpath), 1, 'Exactly one description meta tag found.'); + $this->assertCount(1, $xpath, 'Exactly one description meta tag found.'); $value = $xpath[0]->getAttribute('content'); $this->assertEqual($value, 'Overridden French description.'); $this->assertNotEqual($value, 'Spanish summary.'); @@ -207,7 +207,7 @@ public function testMetatagValueTranslation() { $this->assertSession()->statusCodeEquals(200); $xpath = $this->xpath("//meta[@name='description']"); - $this->assertEqual(count($xpath), 1, 'Exactly one description meta tag found.'); + $this->assertCount(1, $xpath, 'Exactly one description meta tag found.'); $value = $xpath[0]->getAttribute('content'); $this->assertEqual($value, 'Overridden Spanish description.'); $this->assertNotEqual($value, 'Spanish summary.'); diff --git a/web/modules/metatag/tests/src/Functional/MetatagTagsTestBase.php b/web/modules/metatag/tests/src/Functional/MetatagTagsTestBase.php index 624fd9f1b1405ead8654710ed76b44802e1d037f..ac78d0bff8fdeca1a00526526fe209d317736d49 100644 --- a/web/modules/metatag/tests/src/Functional/MetatagTagsTestBase.php +++ b/web/modules/metatag/tests/src/Functional/MetatagTagsTestBase.php @@ -250,7 +250,7 @@ public function testTagsInputOutput($tag_name) { // Extract the meta tag from the HTML. $xpath = $this->xpath($xpath_string); - $this->assertEqual(count($xpath), 1, new FormattableMarkup('One @tag tag found using @xpath.', ['@tag' => $tag_name, '@xpath' => $xpath_string])); + $this->assertCount(1, $xpath, new FormattableMarkup('One @tag tag found using @xpath.', ['@tag' => $tag_name, '@xpath' => $xpath_string])); if (count($xpath) !== 1) { $this->verbose($xpath, $tag_name . ': ' . $xpath_string); } diff --git a/web/modules/metatag/tests/src/Functional/MetatagTokenTest.php b/web/modules/metatag/tests/src/Functional/MetatagTokenTest.php new file mode 100644 index 0000000000000000000000000000000000000000..bf81357b9aec8f599153b49f9a302384852ccef5 --- /dev/null +++ b/web/modules/metatag/tests/src/Functional/MetatagTokenTest.php @@ -0,0 +1,113 @@ +<?php + +namespace Drupal\Tests\metatag\Functional; + +use Drupal\Tests\BrowserTestBase; +use Drupal\Tests\field_ui\Traits\FieldUiTestTrait; +use Drupal\Tests\token\Functional\TokenTestTrait; + +/** + * Verify that metatag token generation is working. + * + * @group metatag + */ +class MetatagTokenTest extends BrowserTestBase { + + use TokenTestTrait; + use FieldUiTestTrait; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'block', + 'field_ui', + 'user', + 'token', + 'token_module_test', + 'metatag', + 'metatag_open_graph', + ]; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * {@inheritdoc} + */ + public function setUp(): void { + parent::setUp(); + + $this->drupalPlaceBlock('system_breadcrumb_block'); + $this->drupalPlaceBlock('local_tasks_block'); + $this->drupalPlaceBlock('page_title_block'); + + $this->drupalLogin($this->rootUser); + $this->fieldUIAddNewField('/admin/config/people/accounts', 'metatags', 'Metatags', 'metatag'); + } + + /** + * Test current-page metatag token generation. + */ + public function testMetatagCurrentPageTokens() { + $user = $this->createUser([]); + $this->drupalGet($user->toUrl('edit-form')); + $this->submitForm([ + 'field_metatags[0][basic][abstract]' => 'My abstract', + 'field_metatags[0][open_graph][og_title]' => 'My OG Title', + 'field_metatags[0][open_graph][og_image]' => 'Image 1,Image 2', + ], 'Save'); + + $tokens = [ + // Test globally configured metatags. + '[current-page:metatag:title]' => sprintf('%s | %s', $user->getAccountName(), $this->config('system.site') + ->get('name')), + '[current-page:metatag:description]' => $this->config('system.site') + ->get('name'), + '[current-page:metatag:canonical-url]' => $user->toUrl('canonical', ['absolute' => TRUE]) + ->toString(), + // Test entity overridden metatags. + '[current-page:metatag:abstract]' => 'My abstract', + // Test metatags provided by a submodule. + '[current-page:metatag:og-title]' => 'My OG Title', + // Test metatags that can contain multiple values. + '[current-page:metatag:og_image]' => 'Image 1,Image 2', + '[current-page:metatag:og_image:0]' => 'Image 1', + '[current-page:metatag:og_image:1]' => 'Image 2', + ]; + $this->assertPageTokens($user->toUrl(), $tokens); + } + + /** + * Test entity token generation. + */ + public function testMetatagEntityTokens() { + $user = $this->createUser(); + $this->drupalGet($user->toUrl('edit-form')); + $this->submitForm([ + 'field_metatags[0][basic][abstract]' => 'My abstract', + 'field_metatags[0][open_graph][og_title]' => 'My OG Title', + 'field_metatags[0][open_graph][og_image]' => 'Image 1,Image 2', + ], 'Save'); + + $tokens = [ + // Test globally configured metatags. + '[user:field_metatags:title]' => sprintf('%s | %s', $user->getAccountName(), $this->config('system.site')->get('name')), + '[user:field_metatags:description]' => $this->config('system.site')->get('name'), + '[user:field_metatags:canonical-url]' => $user->toUrl('canonical', ['absolute' => TRUE])->toString(), + // Test entity overridden metatags. + '[user:field_metatags:abstract]' => 'My abstract', + // Test metatags provided by a submodule. + '[user:field_metatags:og-title]' => 'My OG Title', + // Test metatags that can contain multiple values. + '[user:field_metatags:og_image]' => 'Image 1,Image 2', + '[user:field_metatags:og_image:0]' => 'Image 1', + '[user:field_metatags:og_image:1]' => 'Image 2', + ]; + + $this->assertPageTokens($user->toUrl(), $tokens, ['user' => $user]); + } + +} diff --git a/web/modules/metatag/tests/src/Functional/NodeJsonOutput.php b/web/modules/metatag/tests/src/Functional/NodeJsonOutput.php index 85f9a4a8837c8022294c1d40348e3b9d17ebefb0..5c079df0a8d872670d0531dba58b42770b1cabcc 100644 --- a/web/modules/metatag/tests/src/Functional/NodeJsonOutput.php +++ b/web/modules/metatag/tests/src/Functional/NodeJsonOutput.php @@ -64,10 +64,10 @@ public function testNode() { // Decode the JSON output. $response = $this->getRawContent(); - $this->assertTrue(!empty($response)); + $this->assertNotEmpty($response); $json = json_decode($response); $this->verbose($json, 'JSON output'); - $this->assertTrue(!empty($json)); + $this->assertNotEmpty($json); // Confirm the JSON object's values. $this->assertTrue(isset($json->nid)); diff --git a/web/modules/metatag/tests/src/Functional/RemoveCoreMetaTags.php b/web/modules/metatag/tests/src/Functional/RemoveCoreMetaTags.php index 57b237898a5b3abf416f1481239bfe42f85d47a7..93b492b07ca2f43e124f459feed1a839b9bcdae8 100644 --- a/web/modules/metatag/tests/src/Functional/RemoveCoreMetaTags.php +++ b/web/modules/metatag/tests/src/Functional/RemoveCoreMetaTags.php @@ -59,7 +59,7 @@ public function testTaxonomyPage() { // Ensure there is only 1 canonical metatag. $this->drupalGet('taxonomy/term/' . $term->id()); $xpath = $this->xpath("//link[@rel='canonical']"); - $this->assertEquals(1, count($xpath), 'Exactly one canonical rel meta tag found.'); + $this->assertCount(1, $xpath, 'Exactly one canonical rel meta tag found.'); } } diff --git a/web/modules/metatag/tests/src/Kernel/Migrate/d7/MetatagEntitiesTest.php b/web/modules/metatag/tests/src/Kernel/Migrate/d7/MetatagEntitiesTest.php index 9299032c6deb140bb1a67fa41aa7bfd20f7c3563..2584e14fd264247ea45c41e3a344572a36bdf4d5 100644 --- a/web/modules/metatag/tests/src/Kernel/Migrate/d7/MetatagEntitiesTest.php +++ b/web/modules/metatag/tests/src/Kernel/Migrate/d7/MetatagEntitiesTest.php @@ -120,7 +120,7 @@ protected function setUp() { public function testMetatag() { /** @var \Drupal\node\Entity\Node $node */ $node = Node::load(998); - $this->assertTrue($node instanceof NodeInterface); + $this->assertInstanceOf(NodeInterface::class, $node); $this->assertTrue($node->hasField('field_metatag')); // This should have the "current revision" keywords value, indicating it is // the current revision. @@ -132,7 +132,7 @@ public function testMetatag() { $this->assertSame(serialize($expected), $node->field_metatag->value); $node = node_revision_load(998); - $this->assertTrue($node instanceof NodeInterface); + $this->assertInstanceOf(NodeInterface::class, $node); $this->assertTrue($node->hasField('field_metatag')); // This should have the "old revision" keywords value, indicating it is // a non-current revision. @@ -145,7 +145,7 @@ public function testMetatag() { /** @var \Drupal\user\Entity\User $user */ $user = User::load(2); - $this->assertTrue($user instanceof UserInterface); + $this->assertInstanceOf(UserInterface::class, $user); $this->assertTrue($user->hasField('field_metatag')); $expected = [ 'keywords' => 'a user', @@ -155,7 +155,7 @@ public function testMetatag() { /** @var \Drupal\taxonomy\Entity\Term $term */ $term = Term::load(152); - $this->assertTrue($term instanceof TermInterface); + $this->assertInstanceOf(TermInterface::class, $term); $this->assertTrue($term->hasField('field_metatag')); $expected = [ 'keywords' => 'a taxonomy', diff --git a/web/modules/search_api/CHANGELOG.txt b/web/modules/search_api/CHANGELOG.txt index 6198b241f331d78d5b704e5b82faa47d85b9433e..d4b5c72b49f0df2f2c59a7b9f7f9f6efc45d9a1c 100644 --- a/web/modules/search_api/CHANGELOG.txt +++ b/web/modules/search_api/CHANGELOG.txt @@ -1,3 +1,48 @@ +Search API 1.18 (2020-10-22): +----------------------------- +- #3153153 by mkalkbrenner, cristiroma, drunken monkey: Fixed serialization of + backends using BackendPluginBase. +- #3169531 by akalam, drunken monkey: Fixed warning when non-array "f" + parameter is present in URL. +- #3173400 by drunken monkey: Removed drupalPostForm() calls from functional + tests. +- #3160757 by JordanDukart, drunken monkey: Fixed handling of NULL values in + "Ignore characters" processor. +- #2936043 by froboy, drunken monkey, ressa, agentrickard, navneet0693: Added + an option to the "URI" field to make the created URL absolute. +- #3151966 by robin.ingelbrecht, drunken monkey: Fixed parsing of keywords with + stand-alone dash (-). +- #3157958 by maximpodorov, ankithashetty, ressa, drunken monkey: Fixed display + of Media entities in Views. +- #3137627 by SimeonKesmev, drunken monkey: Added a test for field value + extraction in edge cases. +- #3150764 by bendev, drunken monkey: Fixed documentation for Drush "sapi-i" + command. +- #3145955 by b_sharpe, drunken monkey: Added Tokenizer option to configure + ignored characters. +- #3164255 by drunken monkey, borisson_: Added support for the excerpt field to + the Views "Rendered item" field. +- #3157397 by Murz, drunken monkey: Fixed indexing of complex properties with + computed main properties. +- #3155965 by Ramya Balasubramanian, solideogloria, drunken monkey: Fixed + missing single quote in search_api.index.schema.yml. +- #3136543 by deulenko, drunken monkey: Replaced assertTrue(isset()) with + assertArrayNotHasKey() where appropriate. +- #3168114 by drunken monkey: Fixed test fails against Drupal 9.1.x. +- #3165015 by penyaskito: search_api_test_example_content_references test + module broke D9 build +- #3131384 by lolandese, drunken monkey: Fixed Composer license value. +- #2007692 by bucefal91, drunken monkey, marassa: Added automatic re-indexing + when indexed related entities are changed. +- #3082868 by Raunak.singh, drunken monkey: Changed README.txt to README.md. +- #3158559 by drunken monkey: Updated coding standards and fixed adherence. +- #3126367 by drunken monkey: Fixed fatal error after module upgrade due to + changed service definition. +- #2976339 by drunken monkey, sker101, vflirt: Fixed bug when importing server + configs. +- #2873023 by drunken monkey, borisson_: Fixed handling of special characters + in searches on DB backend. + Search API 1.17 (2020-06-02): ----------------------------- - #3126751 by douggreen, drunken monkey, Berdir: Fixed calls to deprecated diff --git a/web/modules/search_api/README.md b/web/modules/search_api/README.md new file mode 100644 index 0000000000000000000000000000000000000000..17cc93f59815c514b0785286f1ac66af45610f46 --- /dev/null +++ b/web/modules/search_api/README.md @@ -0,0 +1,161 @@ +# Search API + +## Table of contents + +- Introduction +- Requirements +- Installation +- Configuration +- Developers +- Maintainers + +## Introduction + +This module provides a framework for easily creating searches on any entity +known to Drupal, using any kind of search engine. For site administrators, it is +a great alternative to other search solutions, since it already incorporates +faceting support (with [Facets] []) and the ability to use the Views +module for displaying search results, filters, etc. Also, with the +[Apache Solr integration] [], a high-performance search engine is +available for this module. + +[Facets]: https://www.drupal.org/project/facets +[Apache Solr integration]: https://www.drupal.org/project/search_api_solr + +Developers, on the other hand, will be impressed by the large flexibility and +numerous ways of extension the module provides. Hence, the growing number of +additional contrib modules, providing additional functionality or helping users +customize some aspects of the search process. + +- For a full description of the module, visit the [project page] []. +- To submit bug reports and feature suggestions, or to track changes, use the + [issue queue] []. + +[Project page]: https://www.drupal.org/project/search_api +[issue queue]: https://www.drupal.org/project/issues/search_api + +## Requirements + +No other modules are required. If you want to use a different backend than the +database (for instance, Apache Solr or Elasticsearch), you will need to install +the respective module providing the required backend plugin. + +## Installation + +- Install as you would normally install a contributed Drupal module. For further + information, see _[Installing Drupal Modules] []_. + +[Installing Drupal Modules]: https://www.drupal.org/docs/extending-drupal/installing-drupal-modules + +## Configuration + +After installation, for a quick start, just install the “Database Search +Defaults” module provided with this project. This will automatically set up a +search view for node content, using a database server for indexing. + +Otherwise, you need to enable at least a module providing integration with a +search backend (like database, Solr, Elasticsearch, …). Possible options are +listed at _[Server backends and features] []_. + +[Server backends and features]: https://www.drupal.org/docs/8/modules/search-api/getting-started/server-backends-and-features + +Then, go to `/admin/config/search/search-api` on your site and create a search +server and search index. Afterwards, you can create a view based on your index +to enable users to search the content you configured to be indexed. More details +are available in _[Getting started] []_. There, you can also find answers to +[frequently asked questions] [] and [common pitfalls] [] to avoid. + +[Getting started]: https://www.drupal.org/docs/8/modules/search-api/getting-started +[frequently asked questions]: https://www.drupal.org/docs/8/modules/search-api/getting-started/frequently-asked-questions +[common pitfalls]: https://www.drupal.org/docs/8/modules/search-api/getting-started/common-pitfalls + +## Developers + +The Search API provides a lot of ways for developers to extend or customize the +framework. + +### Hooks + +All available hooks are listed in `search_api.api.php`. They have been +deprecated at this point, though, and replaced by events. Hooks will be removed +from the module in version 2.0.0. + +### Events + +All events defined by this module are documented in +`\Drupal\search_api\Event\SearchApiEvents`. + +In addition, the Search API’s task system (for reliably executing necessary +system tasks) makes use of events. Every time a task is executed, an event will +be fired based on the task’s type and the sub-system that scheduled the task is +responsible for reacting to it. This system is extensible and can therefore also +easily be used by contrib modules based on the Search API. For details, see the +description of the `\Drupal\search_api\Task\TaskManager` class, and the other +classes in `src/Task` for examples. + +### Plugins + +The Search API defines several plugin types, all listed in its +`search_api.plugin_type.yml` file. Here is a list of them, along with the +directory in which you can find their definition files (interface, plugin base +and plugin manager): + +- Backends: `src/Backend` +- Datasources: `src/Datasource` +- Data types: `src/DataType` +- Displays: `src/Display` +- Parse modes: `src/ParseMode` +- Processors: `src/Processor` +- Trackers: `src/Tracker` + +The display plugins are a bit of a special case there, because they aren’t +really “extending” the framework, but are rather a way of telling the Search API +(and all modules integrating with it) about search pages your module defines. +They can then be used to provide, for example, faceting support for those pages. +Therefore, if your module provides any search pages, it’s a good idea to provide +display plugins for them. For an example (for Views pages), see +`\Drupal\search_api\Plugin\search_api\display\ViewsPage`. + +For more information, see the +[handbook documentation for developers] [developers handbook]. + +[Developers handbook]: https://www.drupal.org/docs/8/modules/search-api/developer-documentation + +To know which parts of the module can be relied upon as its public API, please +read the [Drupal 8 backwards compatibility and internal API policy] +[core BC policy] and the module’s issue regarding +[potential module-specific changes to that policy] [module BC policy]. + +[Core BC policy]: https://www.drupal.org/core/d8-bc-policy +[Module BC policy]: https://www.drupal.org/node/2871549 + +### Server backend features + +Server backend features are a way for other contrib modules to cleanly define +ways in which the Search API can be extended. For more information, see +_[Server backends and features] []_. + +The Search API module itself currently defines one feature: + +- More Like This (`search_api_mlt`) + This feature can be used to retrieve a list of search results that are similar + to a given indexed item. A backend that supports this feature has to recognize + the `search_api_mlt` query option. If present, it contains an associative + array with the following keys: + - `id`: The Search API item ID (consisting of the datasource ID and the + datasource-specific item ID – passing a plain entity ID will NOT work!) of + the item for which similar results should be found. + - `fields`: A simple array of fields which should be used for determining + similarity. Backends can choose to ignore this field. + - `field boosts`: (optional) An associative array mapping fields to a numeric + “boost” value that determines how important they should be considered when + determining similarity. Backends can choose to ignore this field. + + The feature can be used in the UI via the “More like this” Views contextual + filter. + +## Maintainers + +### Current maintainers + +- [Thomas Seidl (drunken monkey)](https://www.drupal.org/u/drunken-monkey) diff --git a/web/modules/search_api/README.txt b/web/modules/search_api/README.txt deleted file mode 100644 index 21198bc18fd7fb0f585fd67b70345c99f2c5fdbe..0000000000000000000000000000000000000000 --- a/web/modules/search_api/README.txt +++ /dev/null @@ -1,138 +0,0 @@ -CONTENTS OF THIS FILE ---------------------- - * Introduction - * Requirements - * Installation - * Configuration - * Developers - * Maintainers - -INTRODUCTION ------------- -This module provides a framework for easily creating searches on any entity -known to Drupal, using any kind of search engine. For site administrators, -it is a great alternative to other search solutions, since it already -incorporates faceting support (with [1]) and the ability to use the Views module -for displaying search results, filters, etc. Also, with the Apache Solr -integration [2], a high-performance search engine is available for this module. - -[1] https://www.drupal.org/project/facets -[2] https://www.drupal.org/project/search_api_solr - -Developers, on the other hand, will be impressed by the large flexibility and -numerous ways of extension the module provides. Hence, the growing number of -additional contrib modules, providing additional functionality or helping users -customize some aspects of the search process. - * For a full description of the module, visit the project page: - https://www.drupal.org/project/search_api - * To submit bug reports and feature suggestions, or to track changes: - https://www.drupal.org/project/issues/search_api - -REQUIREMENTS ------------- -No other modules are required. - -INSTALLATION ------------- -Install as you would normally install a contributed Drupal module. For further -information, see: - https://www.drupal.org/docs/8/extending-drupal-8/installing-modules - -CONFIGURATION -------------- -After installation, for a quick start, just install the "Database Search -Defaults" module provided with this project. This will automatically set up a -search view for node content, using a database server for indexing. - -Otherwise, you need to enable at least a module providing integration with a -search backend (like database, Solr, Elasticsearch, …). Possible options are -listed at [3]. - -Then, go to - /admin/config/search/search-api -on your site and create a search server and search index. Afterwards, you can -create a view based on your index to enable users to search the content you -configured to be indexed. More details are available online in the handbook [4]. -There, you can also find answers to frequently asked questions and common -pitfalls to avoid. - -[3] https://www.drupal.org/docs/8/modules/search-api/getting-started/server-backends-and-features -[4] https://www.drupal.org/docs/8/modules/search-api/getting-started - -DEVELOPERS ----------- - -The Search API provides a lot of ways for developers to extend or customize the -framework. - -- Hooks - All available hooks are listed in search_api.api.php. -- Events - Currently, only the Search API's task system (for reliably executing necessary - system tasks) makes use of events. Every time a task is executed, an event - will be fired based on the task's type and the sub-system that scheduled the - task is responsible for reacting to it. This system is extensible and can - therefore also easily be used by contrib modules based on the Search API. For - details, see the description of the \Drupal\search_api\Task\TaskManager class, - and the other classes in src/Task for examples. -- Plugins - The Search API defines several plugin types, all listed in its - search_api.plugin_type.yml file. Here is a list of them, along with the - directory in which you can find there definition files (interface, plugin base - and plugin manager): - - Backends: src/Backend - - Datasources: src/Datasource - - Data types: src/DataType - - Displays: src/Display - - Parse modes: src/ParseMode - - Processors: src/Processor - - Trackers: src/Tracker - The display plugins are a bit of a special case there, because they aren't - really "extending" the framework, but are rather a way of telling the Search - API (and all modules integrating with it) about search pages your module - defines. They can then be used to provide, for example, faceting support for - those pages. Therefore, if your module provides any search pages, it's a good - idea to provide display plugins for them. For an example (for Views pages), - see \Drupal\search_api\Plugin\search_api\display\ViewsPage. - -The handbook documentation for developers is available at [5]. - -[5] https://www.drupal.org/docs/8/modules/search-api/developer-documentation - -To know which parts of the module can be relied upon as its public API, please -read the "Drupal 8 backwards compatibility and internal API policy" [6] and the -module's issue regarding potential module-specific changes to that policy [7]. - -[6] https://www.drupal.org/core/d8-bc-policy -[7] https://www.drupal.org/node/2871549 - -SERVER BACKEND FEATURES ------------------------ - -Server backend features are a way for other contrib modules to cleanly define -ways in which the Search API can be extended. For more information, see [8]. - -[8] https://www.drupal.org/docs/8/modules/search-api/getting-started/server-backends-and-features - -The Search API module itself currently defines one feature: - -- More Like This (search_api_mlt) - This feature can be used to retrieve a list of search results that are similar - to a given indexed item. A backend that supports this plugin has to recognize - the "search_api_mlt" query option. If present, it contains an associative - array with the following keys: - - id: The Search API item ID (consisting of the datasource ID and the - datasource-specific item ID – passing a plain entity ID will NOT work!) of - the item for which similar results should be found. - - fields: A simple array of fields which should be used for determining - similarity. Backends can choose to ignore this field. - - "field boosts": (optional) An associative array mapping fields to a numeric - "boost" value that determines how important they should be considered when - determining similarity. Backends can choose to ignore this field. - The feature can be used in the UI via the "More like this" Views contextual - filter. - -MAINTAINERS ------------ -Current maintainers: - * Thomas Seidl (drunken monkey) - https://www.drupal.org/u/drunken-monkey diff --git a/web/modules/search_api/composer.json b/web/modules/search_api/composer.json index 1fc046d41f15b1f50205efa4c071e106385d9d08..85057c82d7c5d7038bf056dd051b1e564bf2cbaa 100644 --- a/web/modules/search_api/composer.json +++ b/web/modules/search_api/composer.json @@ -22,7 +22,7 @@ "irc": "irc://irc.freenode.org/drupal-search-api", "source": "https://git.drupalcode.org/project/search_api" }, - "license": "GPL-2.0+", + "license": "GPL-2.0-or-later", "require-dev": { "drupal/language_fallback_fix": "@dev", "drupal/search_api_autocomplete": "@dev" diff --git a/web/modules/search_api/config/schema/search_api.index.schema.yml b/web/modules/search_api/config/schema/search_api.index.schema.yml index 8e519eb0f5e5402b6018eb7a0415996056906849..02ce5d322e22decebec8020b055ba3acbced08b8 100644 --- a/web/modules/search_api/config/schema/search_api.index.schema.yml +++ b/web/modules/search_api/config/schema/search_api.index.schema.yml @@ -7,7 +7,7 @@ search_api.index.*: label: 'ID' name: type: label - label: Name' + label: 'Name' description: type: text label: 'Description' diff --git a/web/modules/search_api/config/schema/search_api.processor.schema.yml b/web/modules/search_api/config/schema/search_api.processor.schema.yml index a3b84083bceffc01eaf70970c86388a1e4c15a72..6a1f144122bd86367697a22a727b53871755da1d 100644 --- a/web/modules/search_api/config/schema/search_api.processor.schema.yml +++ b/web/modules/search_api/config/schema/search_api.processor.schema.yml @@ -158,9 +158,9 @@ plugin.plugin_configuration.search_api_processor.tokenizer: spaces: type: string label: 'Regular expression for spaces' - ignorable: + ignored: type: string - label: 'Regular expression for ignorable characters' + label: 'Regular expression for ignored characters' overlap_cjk: type: integer label: 'Defines if simple CJK handling should be enabled.' @@ -239,3 +239,11 @@ search_api.property_configuration.rendered_item: sequence: type: string label: 'The view mode used to render the entity for the specified bundle' + +search_api.property_configuration.search_api_url: + type: mapping + label: 'Add URL configuration' + mapping: + absolute: + type: boolean + label: 'Whether to generate an absolute URL' diff --git a/web/modules/search_api/modules/search_api_db/search_api_db.api.php b/web/modules/search_api/modules/search_api_db/search_api_db.api.php index 41a612047981a2ec2eff7968966b86005c8e7562..fcbc45974ca3f495333676be3f2989c5bbca6cf3 100644 --- a/web/modules/search_api/modules/search_api_db/search_api_db.api.php +++ b/web/modules/search_api/modules/search_api_db/search_api_db.api.php @@ -22,11 +22,11 @@ * @param \Drupal\search_api\Query\QueryInterface $query * The search query that is being executed. * - * @deprecated in search_api:8.x-1.16 and will be removed in search_api:9.x-1.0. + * @deprecated in search_api:8.x-1.16 and is removed from search_api:2.0.0. * Please use the "search_api_db.query_pre_execute" event instead. * - * @see \Drupal\search_api_db\Plugin\search_api\backend\Database::preQuery() * @see https://www.drupal.org/node/3103591 + * @see \Drupal\search_api_db\Plugin\search_api\backend\Database::preQuery() */ function hook_search_api_db_query_alter(\Drupal\Core\Database\Query\SelectInterface &$db_query, \Drupal\search_api\Query\QueryInterface $query) { // If the option was set on the query, add additional SQL conditions. diff --git a/web/modules/search_api/modules/search_api_db/search_api_db.info.yml b/web/modules/search_api/modules/search_api_db/search_api_db.info.yml index 77149422eb07c60eeb67ad6c9aea17f0f73091fe..4141fde16c0dac18b4491fa4af96f816813ccd1d 100644 --- a/web/modules/search_api/modules/search_api_db/search_api_db.info.yml +++ b/web/modules/search_api/modules/search_api_db/search_api_db.info.yml @@ -6,7 +6,7 @@ core_version_requirement: ^8.8 || ^9 dependencies: - search_api:search_api -# Information added by Drupal.org packaging script on 2020-06-02 -version: '8.x-1.17' +# Information added by Drupal.org packaging script on 2020-10-22 +version: '8.x-1.18' project: 'search_api' -datestamp: 1591128372 +datestamp: 1603359377 diff --git a/web/modules/search_api/modules/search_api_db/search_api_db_defaults/search_api_db_defaults.info.yml b/web/modules/search_api/modules/search_api_db/search_api_db_defaults/search_api_db_defaults.info.yml index 5f56be3025ff1fb01051661544f93d3a44c62419..86b46a70d3fe9f138c35fa3bdb430d60829a63f8 100644 --- a/web/modules/search_api/modules/search_api_db/search_api_db_defaults/search_api_db_defaults.info.yml +++ b/web/modules/search_api/modules/search_api_db/search_api_db_defaults/search_api_db_defaults.info.yml @@ -13,7 +13,7 @@ dependencies: - drupal:views - search_api:search_api_db -# Information added by Drupal.org packaging script on 2020-06-02 -version: '8.x-1.17' +# Information added by Drupal.org packaging script on 2020-10-22 +version: '8.x-1.18' project: 'search_api' -datestamp: 1591128372 +datestamp: 1603359377 diff --git a/web/modules/search_api/modules/search_api_db/search_api_db_defaults/tests/src/Functional/IntegrationTest.php b/web/modules/search_api/modules/search_api_db/search_api_db_defaults/tests/src/Functional/IntegrationTest.php index 28ff6fd54e3fa6c8b7de142fd6f3cc5ce13ea9a6..485b55032d4053874c7534f356dc6c56db1ca329 100644 --- a/web/modules/search_api/modules/search_api_db/search_api_db_defaults/tests/src/Functional/IntegrationTest.php +++ b/web/modules/search_api/modules/search_api_db/search_api_db_defaults/tests/src/Functional/IntegrationTest.php @@ -65,17 +65,19 @@ public function testInstallAndDefaultSetupWorking() { $edit_enable = [ 'modules[search_api_db_defaults][enable]' => TRUE, ]; - $this->drupalPostForm('admin/modules', $edit_enable, 'Install'); + $this->drupalGet('admin/modules'); + $this->submitForm($edit_enable, 'Install'); $this->assertSession()->pageTextContains('Some required modules must be enabled'); - $this->drupalPostForm(NULL, [], 'Continue'); + $this->submitForm([], 'Continue'); $this->assertSession()->pageTextContains('3 modules have been enabled: Database Search Defaults, Database Search, Search API'); $this->rebuildContainer(); - $this->drupalPostForm('admin/config/search/search-api/server/default_server/edit', [], 'Save'); + $this->drupalGet('admin/config/search/search-api/server/default_server/edit'); + $this->submitForm([], 'Save'); $this->assertSession()->pageTextContains('The server was successfully saved.'); $server = Server::load('default_server'); @@ -94,7 +96,8 @@ public function testInstallAndDefaultSetupWorking() { 'title[0][value]' => $title, 'body[0][value]' => 'This is test content for the Search API to index.', ]; - $this->drupalPostForm('node/add/article', $edit, 'Save'); + $this->drupalGet('node/add/article'); + $this->submitForm($edit, 'Save'); $this->drupalLogout(); $this->drupalGet('search/content'); @@ -113,7 +116,8 @@ public function testInstallAndDefaultSetupWorking() { $edit_disable = [ 'uninstall[search_api_db_defaults]' => TRUE, ]; - $this->drupalPostForm('admin/modules/uninstall', $edit_disable, 'Uninstall'); + $this->drupalGet('admin/modules/uninstall'); + $this->submitForm($edit_disable, 'Uninstall'); $this->submitForm([], 'Uninstall'); $this->rebuildContainer(); $this->assertFalse($this->container->get('module_handler')->moduleExists('search_api_db_defaults'), 'Search API DB Defaults module uninstalled.'); @@ -141,7 +145,8 @@ public function testInstallAndDefaultSetupWorking() { // Enable the module again. This should fail because the either the index // or the server or the view was found. - $this->drupalPostForm('admin/modules', $edit_enable, 'Install'); + $this->drupalGet('admin/modules'); + $this->submitForm($edit_enable, 'Install'); $this->assertSession()->pageTextContains('It looks like the default setup provided by this module already exists on your site. Cannot re-install module.'); // Delete all the entities that we would fail on if they exist. @@ -173,7 +178,8 @@ public function testInstallAndDefaultSetupWorking() { // Try to install search_api_db_defaults module and test if it failed // because there was no content type "article". - $this->drupalPostForm('admin/modules', $edit_enable, 'Install'); + $this->drupalGet('admin/modules'); + $this->submitForm($edit_enable, 'Install'); $success_text = new FormattableMarkup('Content type @content_type not found. Database Search Defaults module could not be installed.', ['@content_type' => 'article']); $this->assertSession()->pageTextContains($success_text); } diff --git a/web/modules/search_api/modules/search_api_db/src/Plugin/search_api/backend/Database.php b/web/modules/search_api/modules/search_api_db/src/Plugin/search_api/backend/Database.php index 3d349b9561b93bba86b4fb6becc68683d5967606..be0254cdbd1b9e58a24f3743d08554e9d1130b07 100644 --- a/web/modules/search_api/modules/search_api_db/src/Plugin/search_api/backend/Database.php +++ b/web/modules/search_api/modules/search_api_db/src/Plugin/search_api/backend/Database.php @@ -893,6 +893,7 @@ protected function sqlType($type) { switch ($type) { case 'text': return ['type' => 'varchar', 'length' => 30]; + case 'string': case 'uri': return ['type' => 'varchar', 'length' => 255]; @@ -1709,7 +1710,8 @@ public function search(QueryInterface $query) { protected function createDbQuery(QueryInterface $query, array $fields) { $keys = &$query->getKeys(); $keys_set = (boolean) $keys; - $keys = $this->prepareKeys($keys); + $tokenizer_active = $query->getIndex()->isValidProcessor('tokenizer'); + $keys = $this->prepareKeys($keys, $tokenizer_active); // Only filter by fulltext keys if there are any real keys present. if ($keys && (!is_array($keys) || count($keys) > 2 || (!isset($keys['#negation']) && count($keys) > 1))) { @@ -1776,7 +1778,7 @@ protected function createDbQuery(QueryInterface $query, array $fields) { $this->getEventDispatcher()->dispatch($event_base_name, $event); $db_query = $event->getDbQuery(); - $description = 'This hook is deprecated in search_api 8.x-1.16 and will be removed in 9.x-1.0. Please use the "search_api_db.query_pre_execute" event instead. See https://www.drupal.org/node/3103591'; + $description = 'This hook is deprecated in search_api:8.x-1.16 and is removed from search_api:2.0.0. Please use the "search_api_db.query_pre_execute" event instead. See https://www.drupal.org/node/3103591'; $this->getModuleHandler()->alterDeprecated($description, 'search_api_db_query', $db_query, $query); $this->preQuery($db_query, $query); @@ -1790,24 +1792,28 @@ protected function createDbQuery(QueryInterface $query, array $fields) { * * @param array|string|null $keys * The keys which should be preprocessed. + * @param bool $tokenizer_active + * (optional) TRUE if we can rely on the "Tokenizer" processor already + * having preprocessed the keywords. * * @return array|string|null * The preprocessed keys. */ - protected function prepareKeys($keys) { + protected function prepareKeys($keys, bool $tokenizer_active = FALSE) { if (is_scalar($keys)) { - $keys = $this->splitKeys($keys); + $keys = $this->splitKeys($keys, $tokenizer_active); return is_array($keys) ? $this->eliminateDuplicates($keys) : $keys; } elseif (!$keys) { return NULL; } - $keys = $this->eliminateDuplicates($this->splitKeys($keys)); + $keys = $this->splitKeys($keys, $tokenizer_active); + $keys = $this->eliminateDuplicates($keys); $conj = $keys['#conjunction']; $neg = !empty($keys['#negation']); foreach ($keys as $i => &$nested) { if (is_array($nested)) { - $nested = $this->prepareKeys($nested); + $nested = $this->prepareKeys($nested, $tokenizer_active); if (is_array($nested) && $neg == !empty($nested['#negation'])) { if ($nested['#conjunction'] == $conj) { unset($nested['#conjunction'], $nested['#negation']); @@ -1839,11 +1845,14 @@ protected function prepareKeys($keys) { * * @param array|string $keys * The keys to split. + * @param bool $tokenizer_active + * (optional) TRUE if we can rely on the "Tokenizer" processor already + * having preprocessed the keywords. * * @return array|string|null * The keys split into separate words. */ - protected function splitKeys($keys) { + protected function splitKeys($keys, bool $tokenizer_active = FALSE) { if (is_scalar($keys)) { $processed_keys = $this->dbmsCompatibility->preprocessIndexValue(trim($keys)); if (is_numeric($processed_keys)) { @@ -1853,9 +1862,14 @@ protected function splitKeys($keys) { $this->ignored[$keys] = 1; return NULL; } - $words = static::splitIntoWords($processed_keys); + if ($tokenizer_active) { + $words = array_filter(explode(' ', $processed_keys), 'strlen'); + } + else { + $words = static::splitIntoWords($processed_keys); + } if (count($words) > 1) { - $processed_keys = $this->splitKeys($words); + $processed_keys = $this->splitKeys($words, $tokenizer_active); if ($processed_keys) { $processed_keys['#conjunction'] = 'AND'; } @@ -1867,7 +1881,7 @@ protected function splitKeys($keys) { } foreach ($keys as $i => $key) { if (Element::child($i)) { - $keys[$i] = $this->splitKeys($key); + $keys[$i] = $this->splitKeys($key, $tokenizer_active); } } return array_filter($keys); @@ -2238,7 +2252,8 @@ protected function createDbCondition(ConditionGroupInterface $conditions, array } } elseif ($this->getDataTypeHelper()->isTextType($field_info['type'])) { - $keys = $this->prepareKeys($value); + $tokenizer_active = $index->isValidProcessor('tokenizer'); + $keys = $this->prepareKeys($value, $tokenizer_active); if (!isset($keys)) { continue; } @@ -2627,7 +2642,7 @@ public function getAutocompleteSuggestions(QueryInterface $query, SearchInterfac $settings = $this->configuration['autocomplete']; // If none of the options is checked, the user apparently chose a very - // roundabout way of telling us he doesn't want autocompletion. + // roundabout way of telling us they don't want autocompletion. if (!array_filter($settings)) { return []; } @@ -2669,7 +2684,12 @@ public function getAutocompleteSuggestions(QueryInterface $query, SearchInterfac // Also collect all keywords already contained in the query so we don't // suggest them. - $keys = static::splitIntoWords($user_input); + if ($query->getIndex()->isValidProcessor('tokenizer')) { + $keys = array_filter(explode(' ', $user_input), 'strlen'); + } + else { + $keys = static::splitIntoWords($user_input); + } $keys = array_combine($keys, $keys); foreach ($passes as $pass) { @@ -2816,7 +2836,6 @@ protected function getIndexDbInfo(IndexInterface $index) { public function __sleep() { $properties = array_flip(parent::__sleep()); unset($properties['database']); - unset($properties['logger']); return array_keys($properties); } diff --git a/web/modules/search_api/modules/search_api_db/src/Tests/Update/SearchApiDbUpdate8102Test.php b/web/modules/search_api/modules/search_api_db/src/Tests/Update/SearchApiDbUpdate8102Test.php index 5b6a2eb0bf4c2844b2b8dae1bc4a84f8a6305aaf..fc54329b079db0a8757bc330c32d5920cb829862 100644 --- a/web/modules/search_api/modules/search_api_db/src/Tests/Update/SearchApiDbUpdate8102Test.php +++ b/web/modules/search_api/modules/search_api_db/src/Tests/Update/SearchApiDbUpdate8102Test.php @@ -27,7 +27,7 @@ protected function setUp() { $entity_type_ids = [ 'search_api_index', 'search_api_server', - 'search_api_task' + 'search_api_task', ]; foreach ($entity_type_ids as $entity_type_id) { $entity_type = \Drupal::getContainer() diff --git a/web/modules/search_api/modules/search_api_db/tests/search_api_db_test_autocomplete/search_api_db_test_autocomplete.info.yml b/web/modules/search_api/modules/search_api_db/tests/search_api_db_test_autocomplete/search_api_db_test_autocomplete.info.yml index 675f533784ad3040223822e5c661d1a60d51a2a1..41ebc400d4cb772751204d2b3b94b8258a1dc61b 100644 --- a/web/modules/search_api/modules/search_api_db/tests/search_api_db_test_autocomplete/search_api_db_test_autocomplete.info.yml +++ b/web/modules/search_api/modules/search_api_db/tests/search_api_db_test_autocomplete/search_api_db_test_autocomplete.info.yml @@ -8,7 +8,7 @@ dependencies: core_version_requirement: ^8.8 || ^9 hidden: true -# Information added by Drupal.org packaging script on 2020-06-02 -version: '8.x-1.17' +# Information added by Drupal.org packaging script on 2020-10-22 +version: '8.x-1.18' project: 'search_api' -datestamp: 1591128372 +datestamp: 1603359377 diff --git a/web/modules/search_api/modules/search_api_db/tests/src/Kernel/BackendTest.php b/web/modules/search_api/modules/search_api_db/tests/src/Kernel/BackendTest.php index c964311840b4741954a56b242ad6e4079ac86cfa..0d47c6e2a9d9f7c59eb740dac26d5be716921ed5 100644 --- a/web/modules/search_api/modules/search_api_db/tests/src/Kernel/BackendTest.php +++ b/web/modules/search_api/modules/search_api_db/tests/src/Kernel/BackendTest.php @@ -122,6 +122,7 @@ protected function backendSpecificRegressionTests() { $this->regressionTest2925464(); $this->regressionTest2994022(); $this->regressionTest2916534(); + $this->regressionTest2873023(); } /** @@ -725,8 +726,6 @@ protected function regressionTest2994022() { /** * Tests edge cases for partial matching. * - * @throws \Drupal\Core\Entity\EntityStorageException - * * @see https://www.drupal.org/node/2916534 */ protected function regressionTest2916534() { @@ -747,6 +746,50 @@ protected function regressionTest2916534() { $this->setServerMatchMode($old); } + /** + * Tests whether keywords with special characters work correctly. + * + * @see https://www.drupal.org/node/2873023 + */ + protected function regressionTest2873023() { + $keyword = 'regression@test@2873023'; + + $entity_id = count($this->entities) + 1; + $entity = $this->addTestEntity($entity_id, [ + 'name' => $keyword, + 'type' => 'article', + ]); + + $index = $this->getIndex(); + $this->assertFalse($index->isValidProcessor('tokenizer')); + $this->indexItems($this->indexId); + $results = $this->buildSearch($keyword, [], ['name'])->execute(); + $this->assertResults([$entity_id], $results, 'Keywords with special characters (Tokenizer disabled)'); + + $processor = \Drupal::getContainer()->get('search_api.plugin_helper') + ->createProcessorPlugin($index, 'tokenizer'); + $index->addProcessor($processor); + $index->save(); + $this->assertTrue($index->isValidProcessor('tokenizer')); + $this->indexItems($this->indexId); + $results = $this->buildSearch($keyword, [], ['name'])->execute(); + $this->assertResults([$entity_id], $results, 'Keywords with special characters (Tokenizer enabled)'); + + $index->getProcessor('tokenizer')->setConfiguration([ + 'spaces' => '\s', + ]); + $index->save(); + $this->indexItems($this->indexId); + $results = $this->buildSearch($keyword, [], ['name'])->execute(); + $this->assertResults([$entity_id], $results, 'Keywords with special characters (Tokenizer with special config)'); + + $index->removeProcessor('tokenizer'); + $index->save(); + $this->assertFalse($index->isValidProcessor('tokenizer')); + + $entity->delete(); + } + /** * {@inheritdoc} */ diff --git a/web/modules/search_api/phpcs.xml b/web/modules/search_api/phpcs.xml index 8a8fcce6a18ddf6e8df1e6236f4e21f1d17583b6..50516dd81fd2e2298d6d55f9dfa4c805c563c388 100644 --- a/web/modules/search_api/phpcs.xml +++ b/web/modules/search_api/phpcs.xml @@ -6,37 +6,72 @@ <!-- Only include specific sniffs that pass. This ensures that, if new sniffs are added, HEAD does not fail.--> <!-- Drupal sniffs --> - <rule ref="Drupal.Arrays.DisallowLongArraySyntax"/> + <rule ref="Drupal.Arrays.Array"> + <!-- Sniff for these errors: CommaLastItem --> + <exclude name="Drupal.Arrays.Array.ArrayClosingIndentation"/> + <exclude name="Drupal.Arrays.Array.ArrayIndentation"/> + <exclude name="Drupal.Arrays.Array.LongLineDeclaration"/> + </rule> <rule ref="Drupal.Classes.ClassCreateInstance"/> <rule ref="Drupal.Classes.ClassDeclaration"/> + <rule ref="Drupal.Classes.ClassFileName"/> <rule ref="Drupal.Classes.FullyQualifiedNamespace"/> <rule ref="Drupal.Classes.InterfaceName"/> <rule ref="Drupal.Classes.UnusedUseStatement"/> <rule ref="Drupal.Classes.UseLeadingBackslash"/> <rule ref="Drupal.CSS.ClassDefinitionNameSpacing"/> <rule ref="Drupal.CSS.ColourDefinition"/> - <rule ref="Drupal.Commenting.ClassComment"/> - <rule ref="Drupal.Commenting.DataTypeNamespace" /> - <rule ref="Drupal.Commenting.DocComment"/> + <rule ref="Drupal.Commenting.ClassComment"> + <exclude name="Drupal.Commenting.ClassComment.Missing"/> + </rule> + <rule ref="Drupal.Commenting.DataTypeNamespace"/> + <rule ref="Drupal.Commenting.Deprecated"/> + <rule ref="Drupal.Commenting.DocComment"> + <!-- Sniff for these errors: SpacingAfterTagGroup, WrongEnd, SpacingBetween, + ContentAfterOpen, SpacingBeforeShort, TagValueIndent, ShortStartSpace, + SpacingAfter, LongNotCapital --> + <!-- ParamNotFirst still not decided for PHPUnit-based tests. + @see https://www.drupal.org/node/2253915 --> + <exclude name="Drupal.Commenting.DocComment.ParamNotFirst"/> + <exclude name="Drupal.Commenting.DocComment.SpacingBeforeTags"/> + <exclude name="Drupal.Commenting.DocComment.LongFullStop"/> + <exclude name="Drupal.Commenting.DocComment.ShortNotCapital"/> + <exclude name="Drupal.Commenting.DocComment.ShortFullStop"/> + <exclude name="Drupal.Commenting.DocComment.ParamGroup"/> + <exclude name="Drupal.Commenting.DocComment.ShortSingleLine"/> + <exclude name="Drupal.Commenting.DocComment.MissingShort"/> + </rule> <rule ref="Drupal.Commenting.DocCommentStar"/> <rule ref="Drupal.Commenting.FileComment"/> - <rule ref="Drupal.Commenting.FunctionComment"/> - <rule ref="Drupal.Commenting.InlineComment"> - <!-- This is impractical when commenting code out. --> - <exclude name="Drupal.Commenting.InlineComment.InvalidEndChar" /> - <!-- We (rarely) use comments as "headings" for multiple functions. --> - <exclude name="Drupal.Commenting.InlineComment.SpacingAfter" /> - <!-- - This disallows indentation in comments, even though it can sometimes be - helpful for structured explanations. - - @see \Drupal\search_api\Plugin\search_api\processor\ContentAccess::addNodeAccess() - --> - <exclude name="Drupal.Commenting.InlineComment.SpacingBefore" /> - </rule> + <rule ref="Drupal.Commenting.FunctionComment"> + <exclude name="Drupal.Commenting.FunctionComment.IncorrectTypeHint"/> + <exclude name="Drupal.Commenting.FunctionComment.InvalidNoReturn"/> + <exclude name="Drupal.Commenting.FunctionComment.InvalidTypeHint"/> + <exclude name="Drupal.Commenting.FunctionComment.Missing"/> + <exclude name="Drupal.Commenting.FunctionComment.MissingParamComment"/> + <exclude name="Drupal.Commenting.FunctionComment.MissingParamType"/> + <exclude name="Drupal.Commenting.FunctionComment.MissingReturnComment"/> + <exclude name="Drupal.Commenting.FunctionComment.MissingReturnType"/> + <exclude name="Drupal.Commenting.FunctionComment.ParamCommentFullStop"/> + <exclude name="Drupal.Commenting.FunctionComment.ParamMissingDefinition"/> + <exclude name="Drupal.Commenting.FunctionComment.TypeHintMissing"/> + </rule> + <rule ref="Drupal.Commenting.GenderNeutralComment"/> <rule ref="Drupal.Commenting.VariableComment"> - <!-- This finds false positives when @code is used. --> + <!-- Sniff for: DuplicateVar, EmptyVar, InlineVariableName --> + <exclude name="Drupal.Commenting.VariableComment.IncorrectVarType"/> + <exclude name="Drupal.Commenting.VariableComment.Missing"/> + <exclude name="Drupal.Commenting.VariableComment.MissingVar"/> <exclude name="Drupal.Commenting.VariableComment.VarOrder"/> + <exclude name="Drupal.Commenting.VariableComment.WrongStyle"/> + </rule> + <rule ref="Drupal.Commenting.InlineComment"> + <!-- Sniff for: NoSpaceBefore, WrongStyle --> + <exclude name="Drupal.Commenting.InlineComment.DocBlock"/> + <exclude name="Drupal.Commenting.InlineComment.InvalidEndChar"/> + <exclude name="Drupal.Commenting.InlineComment.NotCapital"/> + <exclude name="Drupal.Commenting.InlineComment.SpacingAfter"/> + <exclude name="Drupal.Commenting.InlineComment.SpacingBefore"/> </rule> <rule ref="Drupal.Commenting.PostStatementComment"/> <rule ref="Drupal.ControlStructures.ElseIf"/> @@ -46,6 +81,7 @@ <rule ref="Drupal.Files.FileEncoding"/> <rule ref="Drupal.Files.TxtFileLineLength"/> <rule ref="Drupal.Formatting.MultiLineAssignment"/> + <rule ref="Drupal.Formatting.MultipleStatementAlignment"/> <rule ref="Drupal.Formatting.SpaceInlineIf"/> <rule ref="Drupal.Formatting.SpaceUnaryOperator"/> <rule ref="Drupal.Functions.DiscouragedFunctions"/> @@ -54,21 +90,28 @@ <rule ref="Drupal.InfoFiles.ClassFiles"/> <rule ref="Drupal.InfoFiles.DuplicateEntry"/> <rule ref="Drupal.InfoFiles.Required"/> - <rule ref="Drupal.Methods.MethodDeclaration"/> + <rule ref="Drupal.Methods.MethodDeclaration"> + <!-- Silence method name underscore warning which is covered already in + Drupal.NamingConventions.ValidFunctionName.ScopeNotCamelCaps. --> + <exclude name="Drupal.Methods.MethodDeclaration.Underscore"/> + </rule> <rule ref="Drupal.NamingConventions.ValidVariableName"> - <!-- This interferes with the stored entity properties. --> + <!-- Sniff for: LowerStart --> <exclude name="Drupal.NamingConventions.ValidVariableName.LowerCamelName"/> </rule> <rule ref="Drupal.Scope.MethodScope"/> <rule ref="Drupal.Semantics.EmptyInstall"/> <rule ref="Drupal.Semantics.FunctionAlias"/> - <rule ref="Drupal.Semantics.FunctionT"/> + <rule ref="Drupal.Semantics.FunctionT"> + <exclude name="Drupal.Semantics.FunctionT.NotLiteralString"/> + </rule> <rule ref="Drupal.Semantics.FunctionWatchdog"/> <rule ref="Drupal.Semantics.InstallHooks"/> <rule ref="Drupal.Semantics.LStringTranslatable"/> <rule ref="Drupal.Semantics.PregSecurity"/> <rule ref="Drupal.Semantics.TInHookMenu"/> <rule ref="Drupal.Semantics.TInHookSchema"/> + <rule ref="Drupal.Strings.UnnecessaryStringConcat"/> <rule ref="Drupal.WhiteSpace.CloseBracketSpacing"/> <rule ref="Drupal.WhiteSpace.Comma"/> <rule ref="Drupal.WhiteSpace.EmptyLines"/> @@ -77,18 +120,17 @@ <rule ref="Drupal.WhiteSpace.ObjectOperatorSpacing"/> <rule ref="Drupal.WhiteSpace.OpenBracketSpacing"/> <rule ref="Drupal.WhiteSpace.OpenTagNewline"/> - <rule ref="Squiz.WhiteSpace.OperatorSpacing"> - <properties> - <property name="ignoreNewlines" value="true"/> - </properties> - </rule> <rule ref="Drupal.WhiteSpace.ScopeClosingBrace"/> <rule ref="Drupal.WhiteSpace.ScopeIndent"/> <!-- Drupal Practice sniffs --> <rule ref="DrupalPractice.Commenting.ExpectedException"/> + <rule ref="DrupalPractice.General.ExceptionT"/> + <rule ref="DrupalPractice.InfoFiles.NamespacedDependency"/> <!-- Generic sniffs --> + <rule ref="Generic.Arrays.DisallowLongArraySyntax"/> + <rule ref="Generic.CodeAnalysis.EmptyPHPStatement"/> <rule ref="Generic.Files.ByteOrderMark"/> <rule ref="Generic.Files.LineEndings"/> <rule ref="Generic.Formatting.SpaceAfterCast"/> @@ -98,6 +140,7 @@ <property name="checkClosures" value="true"/> </properties> </rule> + <rule ref="Drupal.NamingConventions.ValidClassName"/> <rule ref="Generic.NamingConventions.ConstructorName"/> <rule ref="Generic.NamingConventions.UpperCaseConstantName"/> <rule ref="Generic.PHP.DeprecatedFunctions"/> @@ -106,6 +149,12 @@ <rule ref="Generic.PHP.UpperCaseConstant"/> <rule ref="Generic.WhiteSpace.DisallowTabIndent"/> + <!-- Internal sniffs --> + <rule ref="Internal.NoCodeFound"> + <!-- No PHP code in *.yml --> + <exclude-pattern>*.yml</exclude-pattern> + </rule> + <!-- MySource sniffs --> <rule ref="MySource.Debug.DebugCode"/> @@ -156,7 +205,9 @@ </rule> <!-- PSR-2 sniffs --> - <rule ref="PSR2.Classes.PropertyDeclaration"/> + <rule ref="PSR2.Classes.PropertyDeclaration"> + <exclude name="PSR2.Classes.PropertyDeclaration.Underscore"/> + </rule> <rule ref="PSR2.Namespaces.NamespaceDeclaration"/> <rule ref="PSR2.Namespaces.UseDeclaration"/> @@ -219,6 +270,41 @@ <rule ref="Squiz.ControlStructures.ForLoopDeclaration.SpacingBeforeClose"> <severity>0</severity> </rule> + <rule ref="Squiz.ControlStructures.SwitchDeclaration"/> + <!-- Disable some error messages that we do not want. --> + <rule ref="Squiz.ControlStructures.SwitchDeclaration.BreakIndent"> + <severity>0</severity> + </rule> + <rule ref="Squiz.ControlStructures.SwitchDeclaration.CaseIndent"> + <severity>0</severity> + </rule> + <rule ref="Squiz.ControlStructures.SwitchDeclaration.CloseBraceAlign"> + <severity>0</severity> + </rule> + <rule ref="Squiz.ControlStructures.SwitchDeclaration.DefaultIndent"> + <severity>0</severity> + </rule> + <rule ref="Squiz.ControlStructures.SwitchDeclaration.DefaultNoBreak"> + <severity>0</severity> + </rule> + <rule ref="Squiz.ControlStructures.SwitchDeclaration.EmptyCase"> + <severity>0</severity> + </rule> + <rule ref="Squiz.ControlStructures.SwitchDeclaration.EmptyDefault"> + <severity>0</severity> + </rule> + <rule ref="Squiz.ControlStructures.SwitchDeclaration.MissingDefault"> + <severity>0</severity> + </rule> + <rule ref="Squiz.ControlStructures.SwitchDeclaration.SpacingAfterCase"> + <severity>0</severity> + </rule> + <rule ref="Squiz.ControlStructures.SwitchDeclaration.SpacingAfterDefaultBreak"> + <severity>0</severity> + </rule> + <rule ref="Squiz.ControlStructures.SwitchDeclaration.SpacingBeforeBreak"> + <severity>0</severity> + </rule> <rule ref="Squiz.Functions.MultiLineFunctionDeclaration"/> <rule ref="Squiz.Functions.MultiLineFunctionDeclaration.BraceOnSameLine"> <severity>0</severity> @@ -245,13 +331,24 @@ <severity>0</severity> </rule> <rule ref="Squiz.PHP.LowercasePHPFunctions"/> + <rule ref="Squiz.PHP.NonExecutableCode"/> <rule ref="Squiz.Strings.ConcatenationSpacing"> <properties> <property name="spacing" value="1"/> <property name="ignoreNewlines" value="true"/> </properties> </rule> + <rule ref="Squiz.WhiteSpace.FunctionSpacing"> + <properties> + <property name="spacing" value="1"/> + </properties> + </rule> <rule ref="Squiz.WhiteSpace.LanguageConstructSpacing" /> + <rule ref="Squiz.WhiteSpace.OperatorSpacing"> + <properties> + <property name="ignoreNewlines" value="true"/> + </properties> + </rule> <rule ref="Squiz.WhiteSpace.SemicolonSpacing"/> <rule ref="Squiz.WhiteSpace.SuperfluousWhitespace"/> diff --git a/web/modules/search_api/search_api.api.php b/web/modules/search_api/search_api.api.php index b0e80738d5963e4476597490255af30facc5efb4..9e36b45ca6200373c0f12d35b5925af70b50e63f 100644 --- a/web/modules/search_api/search_api.api.php +++ b/web/modules/search_api/search_api.api.php @@ -21,7 +21,7 @@ * @param array $backend_info * The Search API backend info array, keyed by backend ID. * - * @deprecated in search_api:8.x-1.14 and will be removed in search_api:9.x-1.0. + * @deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. * Please use the "search_api.gathering_backends" event instead. * * @see https://www.drupal.org/node/3059866 @@ -42,7 +42,7 @@ function hook_search_api_backend_info_alter(array &$backend_info) { * @param \Drupal\search_api\ServerInterface $server * The search server in question. * - * @deprecated in search_api:8.x-1.14 and will be removed in search_api:9.x-1.0. + * @deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. * Please use the "search_api.determining_server_features" event instead. * * @see https://www.drupal.org/node/3059866 @@ -65,7 +65,7 @@ function hook_search_api_server_features_alter(array &$features, \Drupal\search_ * @param array $infos * The datasource info array, keyed by datasource IDs. * - * @deprecated in search_api:8.x-1.14 and will be removed in search_api:9.x-1.0. + * @deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. * Please use the "search_api.gathering_data_sources" event instead. * * @see https://www.drupal.org/node/3059866 @@ -87,7 +87,7 @@ function hook_search_api_datasource_info_alter(array &$infos) { * @param array $processors * The processor information to be altered, keyed by processor IDs. * - * @deprecated in search_api:8.x-1.14 and will be removed in search_api:9.x-1.0. + * @deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. * Please use the "search_api.gathering_processors" event instead. * * @see https://www.drupal.org/node/3059866 @@ -105,7 +105,7 @@ function hook_search_api_processor_info_alter(array &$processors) { * @param array $data_type_definitions * The definitions of the parse mode plugins. * - * @deprecated in search_api:8.x-1.14 and will be removed in search_api:9.x-1.0. + * @deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. * Please use the "search_api.gathering_data_types" event instead. * * @see https://www.drupal.org/node/3059866 @@ -123,7 +123,7 @@ function hook_search_api_data_type_info_alter(array &$data_type_definitions) { * @param array $parse_mode_definitions * The definitions of the parse mode plugins. * - * @deprecated in search_api:8.x-1.14 and will be removed in search_api:9.x-1.0. + * @deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. * Please use the "search_api.gathering_parse_modes" event instead. * * @see https://www.drupal.org/node/3059866 @@ -141,7 +141,7 @@ function hook_search_api_parse_mode_info_alter(array &$parse_mode_definitions) { * @param array $tracker_info * The Search API tracker info array, keyed by tracker ID. * - * @deprecated in search_api:8.x-1.14 and will be removed in search_api:9.x-1.0. + * @deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. * Please use the "search_api.gathering_trackers" event instead. * * @see https://www.drupal.org/node/3059866 @@ -160,7 +160,7 @@ function hook_search_api_tracker_info_alter(array &$tracker_info) { * @param array $displays * The Search API display info array, keyed by display ID. * - * @deprecated in search_api:8.x-1.14 and will be removed in search_api:9.x-1.0. + * @deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. * Please use the "search_api.gathering_displays" event instead. * * @see https://www.drupal.org/node/3059866 @@ -180,7 +180,7 @@ function hook_search_api_displays_alter(array &$displays) { * corresponding Search API data types. A value of FALSE means that fields of * that type should be ignored by the Search API. * - * @deprecated in search_api:8.x-1.14 and will be removed in search_api:9.x-1.0. + * @deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. * Please use the "search_api.mapping_field_types" event instead. * * @see https://www.drupal.org/node/3059866 @@ -206,7 +206,7 @@ function hook_search_api_field_type_mapping_alter(array &$mapping) { * (with "ENTITY_TYPE" being the machine name of an entity type) for entities * of that type. * - * @deprecated in search_api:8.x-1.14 and will be removed in search_api:9.x-1.0. + * @deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. * Please use the "search_api.mapping_views_handlers" event instead. * * @see https://www.drupal.org/node/3059866 @@ -247,7 +247,7 @@ function hook_search_api_views_handler_mapping_alter(array &$mapping) { * ones. The "*" mapping therefore is the default if no other match could be * found. * - * @deprecated in search_api:8.x-1.14 and will be removed in search_api:9.x-1.0. + * @deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. * Please use the "search_api.mapping_views_field_handlers" event instead. * * @see https://www.drupal.org/node/3059866 @@ -276,7 +276,7 @@ function hook_search_api_views_field_handler_mapping_alter(array &$mapping) { * @param \Drupal\search_api\Item\ItemInterface[] $items * The items that will be indexed. * - * @deprecated in search_api:8.x-1.14 and will be removed in search_api:9.x-1.0. + * @deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. * Please use the "search_api.indexing_items" event instead. * * @see https://www.drupal.org/node/3059866 @@ -304,7 +304,7 @@ function hook_search_api_index_items_alter(\Drupal\search_api\IndexInterface $in * @param array $item_ids * An array containing the successfully indexed items' IDs. * - * @deprecated in search_api:8.x-1.14 and will be removed in search_api:9.x-1.0. + * @deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. * Please use the "search_api.items_indexed" event instead. * * @see https://www.drupal.org/node/3059866 @@ -327,7 +327,7 @@ function hook_search_api_items_indexed(\Drupal\search_api\IndexInterface $index, * @param \Drupal\search_api\Query\QueryInterface $query * The query that will be executed. * - * @deprecated in search_api:8.x-1.14 and will be removed in search_api:9.x-1.0. + * @deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. * Please use the "search_api.query_pre_execute" event instead. * * @see https://www.drupal.org/node/3059866 @@ -356,7 +356,7 @@ function hook_search_api_query_alter(\Drupal\search_api\Query\QueryInterface $qu * @param \Drupal\search_api\Query\QueryInterface $query * The query that will be executed. * - * @deprecated in search_api:8.x-1.14 and will be removed in search_api:9.x-1.0. + * @deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. * Please use the "search_api.query_pre_execute.TAG" event instead. * * @see https://www.drupal.org/node/3059866 @@ -382,7 +382,7 @@ function hook_search_api_query_TAG_alter(\Drupal\search_api\Query\QueryInterface * @param \Drupal\search_api\Query\ResultSetInterface $results * The search results to alter. * - * @deprecated in search_api:8.x-1.14 and will be removed in search_api:9.x-1.0. + * @deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. * Please use the "search_api.processing_results" event instead. * * @see https://www.drupal.org/node/3059866 @@ -400,7 +400,7 @@ function hook_search_api_results_alter(\Drupal\search_api\Query\ResultSetInterfa * @param \Drupal\search_api\Query\ResultSetInterface $results * The search results to alter. * - * @deprecated in search_api:8.x-1.14 and will be removed in search_api:9.x-1.0. + * @deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. * Please use the "search_api.processing_results.TAG" event instead. * * @see https://www.drupal.org/node/3059866 @@ -417,7 +417,7 @@ function hook_search_api_results_TAG_alter(\Drupal\search_api\Query\ResultSetInt * @param bool $clear * Boolean indicating whether the index was also cleared. * - * @deprecated in search_api:8.x-1.14 and will be removed in search_api:9.x-1.0. + * @deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. * Please use the "search_api.reindex_scheduled" event instead. * * @see https://www.drupal.org/node/3059866 diff --git a/web/modules/search_api/search_api.drush.inc b/web/modules/search_api/search_api.drush.inc index 808e373e73c03cb2f2263fba19218d743de7a082..e1b72a94ff32db0fc465f1f81adc86200a124056 100644 --- a/web/modules/search_api/search_api.drush.inc +++ b/web/modules/search_api/search_api.drush.inc @@ -83,8 +83,8 @@ function search_api_drush_command() { 'drush search-api-index' => dt('Index all items for all enabled indexes.'), 'drush sapi-i' => dt('Alias to index all items for all enabled indexes.'), 'drush sapi-i node_index' => dt('Index all items for the index with the ID @name.', ['@name' => 'node_index']), - 'drush sapi-i node_index 100' => dt('Index a maximum number of @limit items for the index with the ID @name.', ['@limit' => 100, '@name' => 'node_index']), - 'drush sapi-i node_index 100 10' => dt('Index a maximum number of @limit items (@batch_size items per batch run) for the index with the ID @name.', ['@limit' => 100, '@batch_size' => 10, '@name' => 'node_index']), + 'drush sapi-i --limit=100 node_index' => dt('Index a maximum number of @limit items for the index with the ID @name.', ['@limit' => 100, '@name' => 'node_index']), + 'drush sapi-i --limit=100 --batch-size=10 node_index' => dt('Index a maximum number of @limit items (@batch_size items per batch run) for the index with the ID @name.', ['@limit' => 100, '@batch_size' => 10, '@name' => 'node_index']), ], 'options' => [ 'limit' => dt('The number of items to index. Set to 0 to index all items. Defaults to 0 (index all).'), diff --git a/web/modules/search_api/search_api.info.yml b/web/modules/search_api/search_api.info.yml index 1fee870ebcd054481bd99cad62d408cf72057a35..0b7ad670ee6a94b7100772df533b871bd891388f 100644 --- a/web/modules/search_api/search_api.info.yml +++ b/web/modules/search_api/search_api.info.yml @@ -5,7 +5,7 @@ package: Search core_version_requirement: ^8.8 || ^9 configure: search_api.overview -# Information added by Drupal.org packaging script on 2020-06-02 -version: '8.x-1.17' +# Information added by Drupal.org packaging script on 2020-10-22 +version: '8.x-1.18' project: 'search_api' -datestamp: 1591128372 +datestamp: 1603359377 diff --git a/web/modules/search_api/search_api.install b/web/modules/search_api/search_api.install index b51b66bc16ab030a144332d303740519a12f20e1..5aa7a17850b96952dbc0440e969806c1e325965c 100644 --- a/web/modules/search_api/search_api.install +++ b/web/modules/search_api/search_api.install @@ -102,8 +102,8 @@ function search_api_requirements($phase) { 'The following search servers are not available: @servers', ['@servers' => implode(', ', $unavailable_servers)] ), - 'severity' => REQUIREMENT_ERROR - ] + 'severity' => REQUIREMENT_ERROR, + ], ]; } diff --git a/web/modules/search_api/search_api.module b/web/modules/search_api/search_api.module index 0ca428ee8d21bc52036a101ac459e2c0fa7ba4ee..e10bc0bf4695bd2d07dd1ab3265e7806c160f8e3 100644 --- a/web/modules/search_api/search_api.module +++ b/web/modules/search_api/search_api.module @@ -7,25 +7,22 @@ use Drupal\comment\Entity\Comment; use Drupal\Core\Config\ConfigImporter; -use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\ContentEntityType; +use Drupal\Core\Entity\Display\EntityViewDisplayInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Url; use Drupal\node\NodeInterface; use Drupal\search_api\Entity\Index; use Drupal\search_api\IndexInterface; -use Drupal\search_api\Plugin\search_api\datasource\ContentEntity; -use Drupal\search_api\Plugin\search_api\datasource\ContentEntityTaskManager; -use Drupal\search_api\Plugin\views\argument\SearchApiTerm as TermArgument; use Drupal\search_api\Plugin\views\argument\SearchApiAllTerms; +use Drupal\search_api\Plugin\views\argument\SearchApiTerm as TermArgument; use Drupal\search_api\Plugin\views\filter\SearchApiTerm as TermFilter; use Drupal\search_api\Plugin\views\query\SearchApiQuery; use Drupal\search_api\SearchApiException; use Drupal\search_api\Task\IndexTaskManager; use Drupal\views\ViewEntityInterface; use Drupal\views\ViewExecutable; -use Drupal\Core\Entity\Display\EntityViewDisplayInterface; /* * Load all theme functions. @@ -194,151 +191,68 @@ function search_api_config_import_steps_alter(&$sync_steps, ConfigImporter $conf /** * Implements hook_entity_insert(). * - * Adds entries for all languages of the new entity to the tracking table for - * each index that tracks entities of this type. - * - * By setting the $entity->search_api_skip_tracking property to a true-like - * value before this hook is invoked, you can prevent this behavior and make the - * Search API ignore this new entity. - * * Note that this function implements tracking only on behalf of the "Content * Entity" datasource defined in this module, not for entity-based datasources * in general. Datasources defined by other modules still have to implement * their own mechanism for tracking new/updated/deleted entities. * - * @see \Drupal\search_api\Plugin\search_api\datasource\ContentEntity + * @see \Drupal\search_api\Plugin\search_api\datasource\ContentEntityTrackingManager::entityInsert() */ function search_api_entity_insert(EntityInterface $entity) { - // Check if the entity is a content entity. - if (!($entity instanceof ContentEntityInterface) || $entity->search_api_skip_tracking) { - return; - } - $indexes = ContentEntity::getIndexesForEntity($entity); - if (!$indexes) { - return; - } - - // Compute the item IDs for all languages of the entity. - $item_ids = []; - $entity_id = $entity->id(); - foreach (array_keys($entity->getTranslationLanguages()) as $langcode) { - $item_ids[] = $entity_id . ':' . $langcode; - } - $datasource_id = 'entity:' . $entity->getEntityTypeId(); - foreach ($indexes as $index) { - $filtered_item_ids = ContentEntity::filterValidItemIds($index, $datasource_id, $item_ids); - $index->trackItemsInserted($datasource_id, $filtered_item_ids); - } + // Call this hook on behalf of the Content Entity datasource. + \Drupal::getContainer()->get('search_api.entity_datasource.tracking_manager') + ->entityInsert($entity); } /** * Implements hook_entity_update(). * - * Updates the corresponding tracking table entries for each index that tracks - * this entity. - * - * Also takes care of new or deleted translations. - * - * By setting the $entity->search_api_skip_tracking property to a true-like - * value before this hook is invoked, you can prevent this behavior and make the - * Search API ignore this update. - * * Note that this function implements tracking only on behalf of the "Content * Entity" datasource defined in this module, not for entity-based datasources * in general. Datasources defined by other modules still have to implement * their own mechanism for tracking new/updated/deleted entities. * - * @see \Drupal\search_api\Plugin\search_api\datasource\ContentEntity + * Independent of datasources, however, this will also call + * \Drupal\search_api\Utility\TrackingHelper::trackReferencedEntityUpdate() to + * attempt to mark all items for reindexing that indirectly indexed changed + * fields of this entity. + * + * @see \Drupal\search_api\Plugin\search_api\datasource\ContentEntityTrackingManager::entityUpdate() + * @see \Drupal\search_api\Utility\TrackingHelper::trackReferencedEntityUpdate() */ function search_api_entity_update(EntityInterface $entity) { - // Check if the entity is a content entity. - if (!($entity instanceof ContentEntityInterface) || $entity->search_api_skip_tracking) { - return; - } - $indexes = ContentEntity::getIndexesForEntity($entity); - if (!$indexes) { - return; - } + // Call this hook on behalf of the Content Entity datasource. + \Drupal::getContainer()->get('search_api.entity_datasource.tracking_manager') + ->entityUpdate($entity); - // Compare old and new languages for the entity to identify inserted, - // updated and deleted translations (and, therefore, search items). - $entity_id = $entity->id(); - $inserted_item_ids = []; - $updated_item_ids = $entity->getTranslationLanguages(); - $deleted_item_ids = []; - $old_translations = $entity->original->getTranslationLanguages(); - foreach ($old_translations as $langcode => $language) { - if (!isset($updated_item_ids[$langcode])) { - $deleted_item_ids[] = $langcode; - } - } - foreach ($updated_item_ids as $langcode => $language) { - if (!isset($old_translations[$langcode])) { - unset($updated_item_ids[$langcode]); - $inserted_item_ids[] = $langcode; - } - } - - $datasource_id = 'entity:' . $entity->getEntityTypeId(); - $combine_id = function ($langcode) use ($entity_id) { - return $entity_id . ':' . $langcode; - }; - $inserted_item_ids = array_map($combine_id, $inserted_item_ids); - $updated_item_ids = array_map($combine_id, array_keys($updated_item_ids)); - $deleted_item_ids = array_map($combine_id, $deleted_item_ids); - foreach ($indexes as $index) { - if ($inserted_item_ids) { - $filtered_item_ids = ContentEntity::filterValidItemIds($index, $datasource_id, $inserted_item_ids); - $index->trackItemsInserted($datasource_id, $filtered_item_ids); - } - if ($updated_item_ids) { - $index->trackItemsUpdated($datasource_id, $updated_item_ids); - } - if ($deleted_item_ids) { - $index->trackItemsDeleted($datasource_id, $deleted_item_ids); - } - } + // Attempt to track all items as changed that indexed updated data indirectly. + \Drupal::getContainer()->get('search_api.tracking_helper') + ->trackReferencedEntityUpdate($entity); } /** * Implements hook_entity_delete(). * - * Deletes all entries for this entity from the tracking table for each index - * that tracks this entity type. - * - * By setting the $entity->search_api_skip_tracking property to a true-like - * value before this hook is invoked, you can prevent this behavior and make the - * Search API ignore this deletion. (Note that this might lead to stale data in - * the tracking table or on the server, since the item will not removed from - * there (if it has been added before).) - * * Note that this function implements tracking only on behalf of the "Content * Entity" datasource defined in this module, not for entity-based datasources * in general. Datasources defined by other modules still have to implement * their own mechanism for tracking new/updated/deleted entities. * - * @see \Drupal\search_api\Plugin\search_api\datasource\ContentEntity + * Independent of datasources, however, this will also call + * \Drupal\search_api\Utility\TrackingHelper::trackReferencedEntityUpdate() to + * attempt to mark all items for reindexing that indirectly indexed any fields + * of this entity. + * + * @see \Drupal\search_api\Plugin\search_api\datasource\ContentEntityTrackingManager::entityDelete() */ function search_api_entity_delete(EntityInterface $entity) { - // Check if the entity is a content entity. - if (!($entity instanceof ContentEntityInterface) || $entity->search_api_skip_tracking) { - return; - } - $indexes = ContentEntity::getIndexesForEntity($entity); - if (!$indexes) { - return; - } + // Call this hook on behalf of the Content Entity datasource. + \Drupal::getContainer()->get('search_api.entity_datasource.tracking_manager') + ->entityDelete($entity); - // Remove the search items for all the entity's translations. - $item_ids = []; - $entity_id = $entity->id(); - foreach (array_keys($entity->getTranslationLanguages()) as $langcode) { - $item_ids[] = $entity_id . ':' . $langcode; - } - $datasource_id = 'entity:' . $entity->getEntityTypeId(); - foreach ($indexes as $index) { - $index->trackItemsDeleted($datasource_id, $item_ids); - } + // Attempt to track all items as changed that indexed the entity indirectly. + \Drupal::getContainer()->get('search_api.tracking_helper') + ->trackReferencedEntityUpdate($entity, TRUE); } /** @@ -453,91 +367,11 @@ function search_api_search_api_index_insert(Index $index) { * * Implemented on behalf of the "entity" datasource plugin. * - * @see \Drupal\search_api\Plugin\search_api\datasource\ContentEntity + * @see \Drupal\search_api\Plugin\search_api\datasource\ContentEntityTrackingManager::indexUpdate() */ function search_api_search_api_index_update(IndexInterface $index) { - if (!$index->status() || empty($index->original)) { - return; - } - /** @var \Drupal\search_api\IndexInterface $original */ - $original = $index->original; - if (!$original->status()) { - return; - } - - foreach ($index->getDatasources() as $datasource_id => $datasource) { - if ($datasource->getBaseId() != 'entity' - || !$original->isValidDatasource($datasource_id)) { - continue; - } - $old_datasource = $original->getDatasource($datasource_id); - $old_config = $old_datasource->getConfiguration(); - $new_config = $datasource->getConfiguration(); - - if ($old_config != $new_config) { - // Bundles and languages share the same structure, so changes can be - // processed in a unified way. - $tasks = []; - $insert_task = ContentEntityTaskManager::INSERT_ITEMS_TASK_TYPE; - $delete_task = ContentEntityTaskManager::DELETE_ITEMS_TASK_TYPE; - $settings = []; - $entity_type = \Drupal::entityTypeManager() - ->getDefinition($datasource->getEntityTypeId()); - if ($entity_type->hasKey('bundle')) { - $settings['bundles'] = $datasource->getBundles(); - } - if ($entity_type->isTranslatable()) { - $settings['languages'] = \Drupal::languageManager()->getLanguages(); - } - - // Determine which bundles/languages have been newly selected or - // deselected and then assign them to the appropriate actions depending - // on the current "default" setting. - foreach ($settings as $setting => $all) { - $old_selected = array_flip($old_config[$setting]['selected']); - $new_selected = array_flip($new_config[$setting]['selected']); - - // First, check if the "default" setting changed and invert the checked - // items for the old config, so the following comparison makes sense. - if ($old_config[$setting]['default'] != $new_config[$setting]['default']) { - $old_selected = array_diff_key($all, $old_selected); - } - - $newly_selected = array_keys(array_diff_key($new_selected, $old_selected)); - $newly_unselected = array_keys(array_diff_key($old_selected, $new_selected)); - if ($new_config[$setting]['default']) { - $tasks[$insert_task][$setting] = $newly_unselected; - $tasks[$delete_task][$setting] = $newly_selected; - } - else { - $tasks[$insert_task][$setting] = $newly_selected; - $tasks[$delete_task][$setting] = $newly_unselected; - } - } - - // This will keep only those tasks where at least one of "bundles" or - // "languages" is non-empty. - $tasks = array_filter($tasks, 'array_filter'); - $task_manager = \Drupal::getContainer() - ->get('search_api.task_manager'); - foreach ($tasks as $task => $data) { - $data += [ - 'datasource' => $datasource_id, - 'page' => 0, - ]; - $task_manager->addTask($task, NULL, $index, $data); - } - - // If we added any new tasks, set a batch for them. (If we aren't in a - // form submission, this will just be ignored.) - if ($tasks) { - $task_manager->setTasksBatch([ - 'index_id' => $index->id(), - 'type' => array_keys($tasks), - ]); - } - } - } + \Drupal::getContainer()->get('search_api.entity_datasource.tracking_manager') + ->indexUpdate($index); } /** @@ -756,6 +590,12 @@ function search_api_form_views_exposed_form_alter(&$form, FormStateInterface $fo // Get the active facet filters from the query parameters. $filter_key = $facet_source->getFilterKey() ?: 'f'; $filters = \Drupal::request()->query->get($filter_key, []); + + // Do not iterate over facet filters if the parameter is not an array. + if (!is_array($filters)) { + return; + } + // Iterate through the facet filters. foreach ($filters as $key => $value) { if (!is_string($value)) { @@ -805,7 +645,7 @@ function search_api_entity_view(array &$build, EntityInterface $entity, EntityVi '#type' => 'markup', '#markup' => $build['#search_api_excerpt'], '#cache' => [ - 'contexts' => ['url.query_args'] + 'contexts' => ['url.query_args'], ], ]; } diff --git a/web/modules/search_api/search_api.services.yml b/web/modules/search_api/search_api.services.yml index c67add92984cc1a29b3e3cf9a22f928a8b94f631..867a866d2d07071079e9ac5b512a04ff30555ff2 100644 --- a/web/modules/search_api/search_api.services.yml +++ b/web/modules/search_api/search_api.services.yml @@ -38,15 +38,19 @@ services: class: Drupal\search_api\Tracker\TrackerPluginManager arguments: ['@container.namespaces', '@cache.discovery', '@module_handler', '@event_dispatcher'] - search_api.datasource_task_manager: + search_api.data_type_helper: + class: Drupal\search_api\Utility\DataTypeHelper + arguments: ['@module_handler', '@event_dispatcher', '@plugin.manager.search_api.data_type'] + + search_api.entity_datasource.task_manager: class: Drupal\search_api\Plugin\search_api\datasource\ContentEntityTaskManager arguments: ['@search_api.task_manager', '@entity_type.manager'] tags: - { name: event_subscriber } - search_api.data_type_helper: - class: Drupal\search_api\Utility\DataTypeHelper - arguments: ['@module_handler', '@event_dispatcher', '@plugin.manager.search_api.data_type'] + search_api.entity_datasource.tracking_manager: + class: Drupal\search_api\Plugin\search_api\datasource\ContentEntityTrackingManager + arguments: ['@entity_type.manager', '@language_manager', '@search_api.task_manager'] search_api.fields_helper: class: Drupal\search_api\Utility\FieldsHelper @@ -82,6 +86,10 @@ services: class: Drupal\search_api\Task\TaskManager arguments: ['@entity_type.manager', '@event_dispatcher', '@string_translation', '@messenger'] + search_api.tracking_helper: + class: Drupal\search_api\Utility\TrackingHelper + arguments: ['@entity_type.manager', '@language_manager', '@event_dispatcher', '@search_api.fields_helper', '@cache.default'] + search_api.vbo_view_data_provider: class: Drupal\search_api\Contrib\ViewsBulkOperationsEventSubscriber tags: diff --git a/web/modules/search_api/search_api.theme.inc b/web/modules/search_api/search_api.theme.inc index 12c896ad43c972a439e884fb7745142ef922a857..bde23f87f620207538a69a3f59a0d1603b96ce2c 100644 --- a/web/modules/search_api/search_api.theme.inc +++ b/web/modules/search_api/search_api.theme.inc @@ -49,7 +49,7 @@ function template_preprocess_search_api_admin_fields_table(array &$variables) { $variables['note'] = $form['note'] ?? ''; unset($form['note'], $form['submit']); - $variables['table'] = [ + $variables['table'] = [ '#theme' => 'table', '#header' => $form['#header'], '#rows' => $rows, diff --git a/web/modules/search_api/search_api.views.inc b/web/modules/search_api/search_api.views.inc index a65656b2b22acb639468f8134d85d0140e97b93f..2a5d0298d537868d7b47f44d4860b1babda666dc 100644 --- a/web/modules/search_api/search_api.views.inc +++ b/web/modules/search_api/search_api.views.inc @@ -395,7 +395,7 @@ function _search_api_views_handler_mapping() { ]; $alter_id = 'search_api_views_handler_mapping'; - $description = 'This hook is deprecated in search_api 8.x-1.14 and will be removed in 9.x-1.0. Please use the "search_api.mapping_views_handlers" event instead. See https://www.drupal.org/node/3059866'; + $description = 'This hook is deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. Please use the "search_api.mapping_views_handlers" event instead. See https://www.drupal.org/node/3059866'; \Drupal::moduleHandler() ->alterDeprecated($description, $alter_id, $mapping); @@ -978,7 +978,7 @@ function _search_api_views_get_field_handler_mapping() { // Let other modules change or expand this mapping. $alter_id = 'search_api_views_field_handler_mapping'; - $description = 'This hook is deprecated in search_api 8.x-1.14 and will be removed in 9.x-1.0. Please use the "search_api.mapping_views_field_handlers" event instead. See https://www.drupal.org/node/3059866'; + $description = 'This hook is deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. Please use the "search_api.mapping_views_field_handlers" event instead. See https://www.drupal.org/node/3059866'; \Drupal::moduleHandler() ->alterDeprecated($description, $alter_id, $plain_mapping); diff --git a/web/modules/search_api/src/Backend/BackendPluginBase.php b/web/modules/search_api/src/Backend/BackendPluginBase.php index 63afc8eac187d0fa1667d22d2ccdafe882c25e61..bf4b57544fa9f6eea770f7d62e3285ff075b0eab 100644 --- a/web/modules/search_api/src/Backend/BackendPluginBase.php +++ b/web/modules/search_api/src/Backend/BackendPluginBase.php @@ -145,6 +145,17 @@ public function setMessenger(MessengerInterface $messenger) { return $this; } + /** + * {@inheritdoc} + */ + public function setConfiguration(array $configuration) { + parent::setConfiguration($configuration); + + if ($this->server && $this->server->getBackendConfig() !== $configuration) { + $this->server->setBackendConfig($configuration); + } + } + /** * {@inheritdoc} */ @@ -329,6 +340,7 @@ public function __sleep() { } $properties = array_flip(parent::__sleep()); unset($properties['server']); + unset($properties['logger']); return array_keys($properties); } diff --git a/web/modules/search_api/src/Commands/SearchApiCommands.php b/web/modules/search_api/src/Commands/SearchApiCommands.php index f15d5c035caa3537dee684be0ffa3b93f7352aae..cfe152959db28bbd2f376dca0517ca6c717551b3 100644 --- a/web/modules/search_api/src/Commands/SearchApiCommands.php +++ b/web/modules/search_api/src/Commands/SearchApiCommands.php @@ -230,9 +230,9 @@ public function status($indexId = NULL) { * Alias to index all items for all enabled indexes. * @usage drush sapi-i node_index * Index all items for the index with the ID node_index. - * @usage drush sapi-i node_index 100 + * @usage drush sapi-i --limit=100 node_index * Index a maximum number of 100 items for the index with the ID node_index. - * @usage drush sapi-i node_index 100 10 + * @usage drush sapi-i --limit=100 --batch-size=10 node_index * Index a maximum number of 100 items (10 items per batch run) for the * index with the ID node_index. * diff --git a/web/modules/search_api/src/Datasource/DatasourceInterface.php b/web/modules/search_api/src/Datasource/DatasourceInterface.php index 8534f462c96df4d145d9981d6a4d4ec9681a668f..1df6301cb7186b4e146d647072cf15c77605cc2f 100644 --- a/web/modules/search_api/src/Datasource/DatasourceInterface.php +++ b/web/modules/search_api/src/Datasource/DatasourceInterface.php @@ -2,6 +2,7 @@ namespace Drupal\search_api\Datasource; +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\TypedData\ComplexDataInterface; use Drupal\search_api\Plugin\IndexPluginInterface; @@ -138,7 +139,7 @@ public function getItemUrl(ComplexDataInterface $item); * @return bool * TRUE if access is granted, FALSE otherwise. * - * @deprecated in search_api:8.x-1.14 and is removed from search_api:9.x-1.0. + * @deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. * Use getItemAccessResult() instead. * * @see https://www.drupal.org/node/3051902 @@ -244,6 +245,62 @@ public function getEntityTypeId(); */ public function getItemIds($page = NULL); + /** + * Determines whether this datasource can contain entity references. + * + * If this method returns TRUE, the Search API will attempt to mark items for + * reindexing if indexed data in entities referenced by those items changes, + * using the datasource property information and the + * getAffectedItemsForEntityChange() method. + * + * @return bool + * TRUE if this datasource can contain entity references, FALSE otherwise. + * + * @see \Drupal\search_api\Datasource\DatasourceInterface::getAffectedItemsForEntityChange() + * @see \Drupal\search_api\Utility\TrackingHelper::trackReferencedEntityUpdate() + */ + public function canContainEntityReferences(): bool; + + /** + * Identifies items affected by a change to a referenced entity. + * + * A "change" in this context means an entity getting updated or deleted. (It + * won't get called for entities being inserted, as new entities cannot + * already have references pointing to them.) + * + * This method usually doesn't have to return the specified entity itself, + * even if it is part of this datasource. This method should instead only be + * used to detect items that are indirectly affected by this change. + * + * For instance, if an index contains nodes, and nodes can contain tags (which + * are taxonomy term references), and the search index contains the name of + * the tags as one of its fields, then a change of a term name should result + * in all nodes being reindexed that contain that term as a tag. So, the item + * IDs of those nodes should be returned by this method (in case this + * datasource contains them). + * + * This method will only be called if this datasource plugin returns TRUE in + * canContainEntityReferences(). + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity that just got changed. + * @param array[] $foreign_entity_relationship_map + * Map of known entity relationships that exist in the index. Its structure + * is identical to the return value of the + * \Drupal\search_api\Utility\TrackingHelper::getForeignEntityRelationsMap() + * method. + * @param \Drupal\Core\Entity\EntityInterface|null $original_entity + * (optional) The original entity before the change. If this argument is + * NULL, it means the entity got deleted. + * + * @return string[] + * Array of item IDs that are affected by the changes between $entity and + * $original_entity entities. + * + * @see \Drupal\search_api\Datasource\DatasourceInterface::canContainEntityReferences() + */ + public function getAffectedItemsForEntityChange(EntityInterface $entity, array $foreign_entity_relationship_map, EntityInterface $original_entity = NULL): array; + /** * Retrieves any dependencies of the given fields. * diff --git a/web/modules/search_api/src/Datasource/DatasourcePluginBase.php b/web/modules/search_api/src/Datasource/DatasourcePluginBase.php index 1de0c21192dac01151b5a3e820575737c2c7a67c..15f01fe4641ca16a0c31eb615bb5c3e415615800 100644 --- a/web/modules/search_api/src/Datasource/DatasourcePluginBase.php +++ b/web/modules/search_api/src/Datasource/DatasourcePluginBase.php @@ -3,6 +3,7 @@ namespace Drupal\search_api\Datasource; use Drupal\Core\Access\AccessResult; +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Language\Language; use Drupal\Core\Session\AccountInterface; use Drupal\Core\TypedData\ComplexDataInterface; @@ -98,7 +99,7 @@ public function getItemUrl(ComplexDataInterface $item) { * {@inheritdoc} */ public function checkItemAccess(ComplexDataInterface $item, AccountInterface $account = NULL) { - @trigger_error('\Drupal\search_api\Datasource\DatasourceInterface::checkItemAccess() is deprecated in search_api:8.x-1.14 and is removed from search_api:9.x-1.0. Use getItemAccessResult() instead. See https://www.drupal.org/node/3051902', E_USER_DEPRECATED); + @trigger_error('\Drupal\search_api\Datasource\DatasourceInterface::checkItemAccess() is deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. Use getItemAccessResult() instead. See https://www.drupal.org/node/3051902', E_USER_DEPRECATED); return $this->getItemAccessResult($item, $account)->isAllowed(); } @@ -157,6 +158,20 @@ public function getItemIds($page = NULL) { return NULL; } + /** + * {@inheritdoc} + */ + public function canContainEntityReferences(): bool { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function getAffectedItemsForEntityChange(EntityInterface $entity, array $foreign_entity_relationship_map, EntityInterface $original_entity = NULL): array { + return []; + } + /** * {@inheritdoc} */ diff --git a/web/modules/search_api/src/Display/DisplayInterface.php b/web/modules/search_api/src/Display/DisplayInterface.php index c5969a38f9450e3e848916cf2b131f4eeadc220e..c62c642e0cdf8234703c9de6fb0fe8e11c47f0c8 100644 --- a/web/modules/search_api/src/Display/DisplayInterface.php +++ b/web/modules/search_api/src/Display/DisplayInterface.php @@ -48,10 +48,10 @@ public function getIndex(); * @return \Drupal\Core\Url|null * The URL of the display, or NULL if there is no specific URL for it. * - * @deprecated in favor of getPath(). Creating an URL object from a path needs - * a lot of Core's API which might lead to errors when used in certain - * situations. This method will be removed in a future version of the Search - * API module. + * @deprecated in search_api:8.x-1.0-beta5 and is removed from + * search_api:2.0.0. Use getPath() instead. + * + * @see https://www.drupal.org/node/2856050 */ public function getUrl(); diff --git a/web/modules/search_api/src/Entity/Index.php b/web/modules/search_api/src/Entity/Index.php index 7c8f8bc3f67578e1c0db5bd0bffc9295bf6a0750..221b3fb904dc746ff49d93efb6f50c01dfb7aa8f 100644 --- a/web/modules/search_api/src/Entity/Index.php +++ b/web/modules/search_api/src/Entity/Index.php @@ -964,7 +964,7 @@ public function indexSpecificItems(array $search_objects) { // Preprocess the indexed items. $this->alterIndexedItems($items); - $description = 'This hook is deprecated in search_api 8.x-1.14 and will be removed in 9.x-1.0. Please use the "search_api.indexing_items" event instead. See https://www.drupal.org/node/3059866'; + $description = 'This hook is deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. Please use the "search_api.indexing_items" event instead. See https://www.drupal.org/node/3059866'; \Drupal::moduleHandler()->alterDeprecated($description, 'search_api_index_items', $this, $items); $event = new IndexingItemsEvent($this, $items); \Drupal::getContainer()->get('event_dispatcher') @@ -1005,7 +1005,7 @@ public function indexSpecificItems(array $search_objects) { // effect again. Therefore, we reset the flag. $this->setHasReindexed(FALSE); - $description = 'This hook is deprecated in search_api 8.x-1.14 and will be removed in 9.x-1.0. Please use the "search_api.items_indexed" event instead. See https://www.drupal.org/node/3059866'; + $description = 'This hook is deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. Please use the "search_api.items_indexed" event instead. See https://www.drupal.org/node/3059866'; \Drupal::moduleHandler()->invokeAllDeprecated($description, 'search_api_items_indexed', [$this, $processed_ids]); /** @var \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher */ @@ -1114,7 +1114,7 @@ public function reindex() { if ($this->status() && !$this->isReindexing()) { $this->setHasReindexed(); $this->getTrackerInstance()->trackAllItemsUpdated(); - $description = 'This hook is deprecated in search_api 8.x-1.14 and will be removed in 9.x-1.0. Please use the "search_api.reindex_scheduled" event instead. See https://www.drupal.org/node/3059866'; + $description = 'This hook is deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. Please use the "search_api.reindex_scheduled" event instead. See https://www.drupal.org/node/3059866'; \Drupal::moduleHandler()->invokeAllDeprecated($description, 'search_api_index_reindex', [$this, FALSE]); /** @var \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher */ $dispatcher = \Drupal::getContainer()->get('event_dispatcher'); @@ -1142,7 +1142,7 @@ public function clear() { $this->getServerInstance()->deleteAllIndexItems($this); } if ($invoke_hook) { - $description = 'This hook is deprecated in search_api 8.x-1.14 and will be removed in 9.x-1.0. Please use the "search_api.reindex_scheduled" event instead. See https://www.drupal.org/node/3059866'; + $description = 'This hook is deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. Please use the "search_api.reindex_scheduled" event instead. See https://www.drupal.org/node/3059866'; \Drupal::moduleHandler()->invokeAllDeprecated($description, 'search_api_index_reindex', [$this, !$this->isReadOnly()]); /** @var \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher */ @@ -1164,7 +1164,7 @@ public function rebuildTracker() { $index_task_manager->stopTracking($this); $index_task_manager->startTracking($this); $this->setHasReindexed(); - $description = 'This hook is deprecated in search_api 8.x-1.14 and will be removed in 9.x-1.0. Please use the "search_api.reindex_scheduled" event instead. See https://www.drupal.org/node/3059866'; + $description = 'This hook is deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. Please use the "search_api.reindex_scheduled" event instead. See https://www.drupal.org/node/3059866'; \Drupal::moduleHandler() ->invokeAllDeprecated($description, 'search_api_index_reindex', [$this, FALSE]); /** @var \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher */ diff --git a/web/modules/search_api/src/Entity/Server.php b/web/modules/search_api/src/Entity/Server.php index 943a76e5c5b472946e40fad0a5e1f79a71c42b8a..3f2b06d2a116c6b3ac610af3fba17aa283859f58 100644 --- a/web/modules/search_api/src/Entity/Server.php +++ b/web/modules/search_api/src/Entity/Server.php @@ -167,7 +167,12 @@ public function getBackendConfig() { */ public function setBackendConfig(array $backend_config) { $this->backend_config = $backend_config; - $this->getBackend()->setConfiguration($backend_config); + // In case the backend plugin is already loaded, make sure the configuration + // stays in sync. + if ($this->backendPlugin + && $this->getBackend()->getConfiguration() !== $backend_config) { + $this->getBackend()->setConfiguration($backend_config); + } return $this; } @@ -209,7 +214,7 @@ public function getSupportedFeatures() { if ($this->hasValidBackend()) { $this->features = $this->getBackend()->getSupportedFeatures(); } - $description = 'This hook is deprecated in search_api 8.x-1.14 and will be removed in 9.x-1.0. Please use the "search_api.determining_server_features" event instead. See https://www.drupal.org/node/3059866'; + $description = 'This hook is deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. Please use the "search_api.determining_server_features" event instead. See https://www.drupal.org/node/3059866'; \Drupal::moduleHandler() ->alterDeprecated($description, 'search_api_server_features', $this->features, $this); /** @var \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher */ @@ -561,24 +566,6 @@ public static function preDelete(EntityStorageInterface $storage, array $entitie } } - /** - * {@inheritdoc} - */ - public function toArray() { - // @todo It's a bug that we have to do this. Backend configuration should - // always be set via the server's setBackendConfiguration() method, - // otherwise the two can diverge causing this and other problems. The - // alternative would be to call $server->setBackendConfiguration() in the - // backend's setConfiguration() method and use a second $propagate - // parameter to avoid an infinite loop. Similar things go for the index's - // various plugins. Maybe using plugin bags is the solution here? - $properties = parent::toArray(); - if ($this->hasValidBackend()) { - $properties['backend_config'] = $this->getBackend()->getConfiguration(); - } - return $properties; - } - /** * {@inheritdoc} */ diff --git a/web/modules/search_api/src/Event/MappingForeignRelationshipsEvent.php b/web/modules/search_api/src/Event/MappingForeignRelationshipsEvent.php new file mode 100644 index 0000000000000000000000000000000000000000..d700ac6c016b182f2dce8f8208353319626b1203 --- /dev/null +++ b/web/modules/search_api/src/Event/MappingForeignRelationshipsEvent.php @@ -0,0 +1,93 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\search_api\Event; + +use Drupal\Core\Cache\RefinableCacheableDependencyInterface; +use Drupal\search_api\IndexInterface; +use Symfony\Component\EventDispatcher\Event; + +/** + * Wraps a foreign relationships mapping event. + */ +final class MappingForeignRelationshipsEvent extends Event { + + /** + * The index whose foreign relationships are mapped. + * + * @var \Drupal\search_api\IndexInterface + */ + protected $index; + + /** + * Reference to the foreign relationships mapping. + * + * @var array + */ + protected $foreignRelationshipsMapping; + + /** + * Cacheability associated with the foreign relationships mapping. + * + * @var \Drupal\Core\Cache\RefinableCacheableDependencyInterface + */ + protected $cacheability; + + /** + * Constructs a new class instance. + * + * @param \Drupal\search_api\IndexInterface $index + * The index whose foreign relationships are mapped. + * @param array $foreign_relationships_mapping + * The foreign relationships that were already found. + * @param \Drupal\Core\Cache\RefinableCacheableDependencyInterface $cacheability + * The cacheability associated with the foreign relationships mapping. + */ + public function __construct(IndexInterface $index, array &$foreign_relationships_mapping, RefinableCacheableDependencyInterface $cacheability) { + $this->index = $index; + $this->foreignRelationshipsMapping = &$foreign_relationships_mapping; + $this->cacheability = $cacheability; + } + + /** + * Retrieves the index whose foreign relationships are mapped. + * + * @return \Drupal\search_api\IndexInterface + * The index whose foreign relationships are mapped. + */ + public function getIndex(): IndexInterface { + return $this->index; + } + + /** + * Retrieves a reference to the foreign relationships mapping. + * + * @return array[] + * A (numerically keyed) array of foreign relationship mappings. Each + * sub-array here represents a single known relationship. Such sub-arrays + * will have the following structure: + * - entity_type: (string) Entity type that is referred to from the index. + * - bundles: (array) Optional array of particular entity bundles that are + * referred to from the index. Empty array here means index refers to + * all the bundles. + * - property_path_to_foreign_entity: (string) Property path where the index + * refers to this entity. + * - field_name: (string) Name of the field on the referenced entity that + * actively participates in the search index. + */ + public function &getForeignRelationshipsMapping(): array { + return $this->foreignRelationshipsMapping; + } + + /** + * Retrieves cacheability associated with the foreign relationships mapping. + * + * @return \Drupal\Core\Cache\RefinableCacheableDependencyInterface + * Cacheability associated with the foreign relationships mapping. + */ + public function getCacheability(): RefinableCacheableDependencyInterface { + return $this->cacheability; + } + +} diff --git a/web/modules/search_api/src/Event/SearchApiEvents.php b/web/modules/search_api/src/Event/SearchApiEvents.php index d955fbca240a099b430d192c3fad602af7aa2f43..9a13c18394967fdd8066ee8c2b9c0cae2c4c78cf 100644 --- a/web/modules/search_api/src/Event/SearchApiEvents.php +++ b/web/modules/search_api/src/Event/SearchApiEvents.php @@ -124,6 +124,21 @@ final class SearchApiEvents { */ const MAPPING_FIELD_TYPES = 'search_api.mapping_field_types'; + /** + * The name of the event fired when mapping foreign relationships of an index. + * + * Foreign relationships of an index help Search API to mark for reindexing + * search items affected by changes to entities that are indirectly indexed. + * + * This event can be leveraged to alter the map of foreign + * relationships discovered for any particular search index. + * + * @Event + * + * @see \Drupal\search_api\Event\MappingForeignRelationshipsEvent + */ + const MAPPING_FOREIGN_RELATIONSHIPS = 'search_api.mapping_foreign_relationships'; + /** * The name of the event fired when building a map of Views field handlers. * diff --git a/web/modules/search_api/src/IndexInterface.php b/web/modules/search_api/src/IndexInterface.php index 7eeb42476cce68afd45b823b48edc2d1181038e3..f2a23515067f8b20489b8da3714239fcf6bb5ae0 100644 --- a/web/modules/search_api/src/IndexInterface.php +++ b/web/modules/search_api/src/IndexInterface.php @@ -30,6 +30,19 @@ interface IndexInterface extends ConfigEntityInterface { */ const DATASOURCE_ID_SEPARATOR = '/'; + /** + * String used to separate individual properties within a property path. + * + * Property paths are used throughout the Search API to reference properties + * that are not (necessarily) present directly on some entity or item, but + * could be nested in some referenced entity/item, even over multiple levels. + * Properties in such a path are separated by this value. + * + * An example for a property path would be "field_tags:entity:name" for the + * name of the associated tag(s) of an entity. + */ + const PROPERTY_PATH_SEPARATOR = ':'; + /** * Retrieves the index description. * diff --git a/web/modules/search_api/src/IndexListBuilder.php b/web/modules/search_api/src/IndexListBuilder.php index 622efe3fd8f026898f3b2efdcabcd8aa9934490f..f285ccf43e16ad09a3ecb2c7c6aad708bed1a7d0 100644 --- a/web/modules/search_api/src/IndexListBuilder.php +++ b/web/modules/search_api/src/IndexListBuilder.php @@ -89,7 +89,7 @@ public static function checkDefaultsModuleCanBeInstalled() { if (!isset($fields[$required_field])) { $errors[$required_type_id . ':' . $required_field] = t('Field @field in content type @node_type not found. Database Search Defaults module could not be installed', [ '@node_type' => $required_type_id, - '@field' => $required_field + '@field' => $required_field, ]); } } diff --git a/web/modules/search_api/src/Item/Item.php b/web/modules/search_api/src/Item/Item.php index 4edb20f3ffade683fd465061dfb5cb8b6174974f..acba4cad2fe2e7ef9601697e2c2625ac53bf3ba5 100644 --- a/web/modules/search_api/src/Item/Item.php +++ b/web/modules/search_api/src/Item/Item.php @@ -416,7 +416,7 @@ public function setExtraData($key, $data = NULL) { * {@inheritdoc} */ public function checkAccess(AccountInterface $account = NULL) { - @trigger_error('\Drupal\search_api\Item\ItemInterface::checkAccess() is deprecated in search_api:8.x-1.14 and is removed from search_api:9.x-1.0. Use getAccessResult() instead. See https://www.drupal.org/node/3051902', E_USER_DEPRECATED); + @trigger_error('\Drupal\search_api\Item\ItemInterface::checkAccess() is deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. Use getAccessResult() instead. See https://www.drupal.org/node/3051902', E_USER_DEPRECATED); return $this->getAccessResult($account)->isAllowed(); } diff --git a/web/modules/search_api/src/Item/ItemInterface.php b/web/modules/search_api/src/Item/ItemInterface.php index ebf49b26585d7c59e381b86565c14ea951a8a6db..f7c708309890b9c0b9e1f06a3e4d7706fed58424 100644 --- a/web/modules/search_api/src/Item/ItemInterface.php +++ b/web/modules/search_api/src/Item/ItemInterface.php @@ -296,7 +296,7 @@ public function setExtraData($key, $data = NULL); * @return bool * TRUE if access is granted, FALSE otherwise. * - * @deprecated in search_api:8.x-1.14 and is removed from search_api:9.x-1.0. + * @deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. * Use getAccessResult() instead. * * @see https://www.drupal.org/node/3051902 diff --git a/web/modules/search_api/src/ParamConverter/SearchApiConverter.php b/web/modules/search_api/src/ParamConverter/SearchApiConverter.php index f0ce1589ae131b40e46df53874c16bc13e2837d6..f9fa697edb04b5dd0a77ab78e3ed4b2426e5b729 100644 --- a/web/modules/search_api/src/ParamConverter/SearchApiConverter.php +++ b/web/modules/search_api/src/ParamConverter/SearchApiConverter.php @@ -38,6 +38,8 @@ class SearchApiConverter extends EntityConverter implements ParamConverterInterf */ protected $currentUser; + // phpcs:disable Drupal.Commenting.FunctionComment.TypeHintMissing + /** * Constructs a new SearchApiConverter. * @@ -50,13 +52,35 @@ class SearchApiConverter extends EntityConverter implements ParamConverterInterf * @param \Drupal\Core\Session\AccountInterface $user * The current user. */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityRepositoryInterface $entity_repository, SharedTempStoreFactory $temp_store_factory, AccountInterface $user) { + public function __construct(EntityTypeManagerInterface $entity_type_manager, $entity_repository, $temp_store_factory, $user = NULL) { + // For backwards-compatibility, we still support passing just + // ($entity_manager, $temp_store_factory, $user). + if (!$user) { + @trigger_error('Constructing \Drupal\search_api\ParamConverter\SearchApiConverter with ($entity_manager, $temp_store_factory, $user) is deprecated in search_api 8.x-1.18 and will stop working in 2.0.0. Pass ($entity_type_manager, $entity_repository, $temp_store_factory, $user) instead. See https://www.drupal.org/node/3164248', E_USER_DEPRECATED); + $user = $temp_store_factory; + $temp_store_factory = $entity_repository; + $entity_repository = \Drupal::getContainer()->get('entity.repository'); + } + $type_checks = [ + 2 => [$entity_repository, EntityRepositoryInterface::class], + 3 => [$temp_store_factory, SharedTempStoreFactory::class], + 4 => [$user, AccountInterface::class], + ]; + foreach ($type_checks as $i => list($object, $expected)) { + if (!($object instanceof $expected)) { + $actual = get_class($object); + throw new \TypeError("Argument $i passed to Drupal\search_api\ParamConverter\SearchApiConverter::__construct() must implement interface $expected, instance of $actual given"); + } + } + parent::__construct($entity_type_manager, $entity_repository); $this->tempStoreFactory = $temp_store_factory; $this->currentUser = $user; } + // phpcs:enable Drupal.Commenting.FunctionComment.TypeHintMissing + /** * {@inheritdoc} */ diff --git a/web/modules/search_api/src/Plugin/ConfigurablePluginBase.php b/web/modules/search_api/src/Plugin/ConfigurablePluginBase.php index 2258a0578f89dd31488f75e6fd77a58cdb4915ad..da836e8ec0e370506b84bb42882e0acc7fa0a432 100644 --- a/web/modules/search_api/src/Plugin/ConfigurablePluginBase.php +++ b/web/modules/search_api/src/Plugin/ConfigurablePluginBase.php @@ -112,14 +112,14 @@ public function onDependencyRemoval(array $dependencies) { * @return array * An array of dependencies keyed by the type of dependency. * - * @deprecated in search_api:8.x-1.16 and is removed from search_api:9.x-1.0. + * @deprecated in search_api:8.x-1.16 and is removed from search_api:2.0.0. * Add \Drupal\Core\Plugin\PluginDependencyTrait manually for your class if * you need it. * * @see https://www.drupal.org/node/3099004 */ protected function getPluginDependencies(PluginInspectionInterface $instance) { - @trigger_error('The use of \Drupal\Core\Plugin\PluginDependencyTrait via \Drupal\search_api\Plugin\ConfigurablePluginBase is deprecated in search_api:8.x-1.16 and is removed from search_api:9.x-1.0. Add \Drupal\Core\Plugin\PluginDependencyTrait manually for your class if you need it. See https://www.drupal.org/node/3099004', E_USER_DEPRECATED); + @trigger_error('The use of \Drupal\Core\Plugin\PluginDependencyTrait via \Drupal\search_api\Plugin\ConfigurablePluginBase is deprecated in search_api:8.x-1.16 and is removed from search_api:2.0.0. Add \Drupal\Core\Plugin\PluginDependencyTrait manually for your class if you need it. See https://www.drupal.org/node/3099004', E_USER_DEPRECATED); return $this->traitGetPluginDependencies($instance); } @@ -134,14 +134,14 @@ protected function getPluginDependencies(PluginInspectionInterface $instance) { * @param \Drupal\Component\Plugin\PluginInspectionInterface $instance * The plugin instance. * - * @deprecated in search_api:8.x-1.16 and is removed from search_api:9.x-1.0. + * @deprecated in search_api:8.x-1.16 and is removed from search_api:2.0.0. * Add \Drupal\Core\Plugin\PluginDependencyTrait manually for your class if * you need it. * * @see https://www.drupal.org/node/3099004 */ protected function calculatePluginDependencies(PluginInspectionInterface $instance) { - @trigger_error('The use of \Drupal\Core\Plugin\PluginDependencyTrait via \Drupal\search_api\Plugin\ConfigurablePluginBase is deprecated in search_api:8.x-1.16 and is removed from search_api:9.x-1.0. Add \Drupal\Core\Plugin\PluginDependencyTrait manually for your class if you need it. See https://www.drupal.org/node/3099004', E_USER_DEPRECATED); + @trigger_error('The use of \Drupal\Core\Plugin\PluginDependencyTrait via \Drupal\search_api\Plugin\ConfigurablePluginBase is deprecated in search_api:8.x-1.16 and is removed from search_api:2.0.0. Add \Drupal\Core\Plugin\PluginDependencyTrait manually for your class if you need it. See https://www.drupal.org/node/3099004', E_USER_DEPRECATED); $this->traitCalculatePluginDependencies($instance); } @@ -151,14 +151,14 @@ protected function calculatePluginDependencies(PluginInspectionInterface $instan * @return \Drupal\Core\Extension\ModuleHandlerInterface * The module handler. * - * @deprecated in search_api:8.x-1.16 and is removed from search_api:9.x-1.0. + * @deprecated in search_api:8.x-1.16 and is removed from search_api:2.0.0. * Add \Drupal\Core\Plugin\PluginDependencyTrait manually for your class if * you need it. * * @see https://www.drupal.org/node/3099004 */ protected function moduleHandler() { - @trigger_error('The use of \Drupal\Core\Plugin\PluginDependencyTrait via \Drupal\search_api\Plugin\ConfigurablePluginBase is deprecated in search_api:8.x-1.16 and is removed from search_api:9.x-1.0. Add \Drupal\Core\Plugin\PluginDependencyTrait manually for your class if you need it. See https://www.drupal.org/node/3099004', E_USER_DEPRECATED); + @trigger_error('The use of \Drupal\Core\Plugin\PluginDependencyTrait via \Drupal\search_api\Plugin\ConfigurablePluginBase is deprecated in search_api:8.x-1.16 and is removed from search_api:2.0.0. Add \Drupal\Core\Plugin\PluginDependencyTrait manually for your class if you need it. See https://www.drupal.org/node/3099004', E_USER_DEPRECATED); return $this->traitModuleHandler(); } @@ -168,14 +168,14 @@ protected function moduleHandler() { * @return \Drupal\Core\Extension\ThemeHandlerInterface * The theme handler. * - * @deprecated in search_api:8.x-1.16 and is removed from search_api:9.x-1.0. + * @deprecated in search_api:8.x-1.16 and is removed from search_api:2.0.0. * Add \Drupal\Core\Plugin\PluginDependencyTrait manually for your class if * you need it. * * @see https://www.drupal.org/node/3099004 */ protected function themeHandler() { - @trigger_error('The use of \Drupal\Core\Plugin\PluginDependencyTrait via \Drupal\search_api\Plugin\ConfigurablePluginBase is deprecated in search_api:8.x-1.16 and is removed from search_api:9.x-1.0. Add \Drupal\Core\Plugin\PluginDependencyTrait manually for your class if you need it. See https://www.drupal.org/node/3099004', E_USER_DEPRECATED); + @trigger_error('The use of \Drupal\Core\Plugin\PluginDependencyTrait via \Drupal\search_api\Plugin\ConfigurablePluginBase is deprecated in search_api:8.x-1.16 and is removed from search_api:2.0.0. Add \Drupal\Core\Plugin\PluginDependencyTrait manually for your class if you need it. See https://www.drupal.org/node/3099004', E_USER_DEPRECATED); return $this->traitThemeHandler(); } diff --git a/web/modules/search_api/src/Plugin/search_api/datasource/ContentEntity.php b/web/modules/search_api/src/Plugin/search_api/datasource/ContentEntity.php index ec7588f19dee003b41c7e32720f29a737859b8f8..06cceafc427bdbaf07f6df9ec4ab6252698c3306 100644 --- a/web/modules/search_api/src/Plugin/search_api/datasource/ContentEntity.php +++ b/web/modules/search_api/src/Plugin/search_api/datasource/ContentEntity.php @@ -27,10 +27,9 @@ use Drupal\field\FieldConfigInterface; use Drupal\field\FieldStorageConfigInterface; use Drupal\search_api\Datasource\DatasourcePluginBase; -use Drupal\search_api\Entity\Index; -use Drupal\search_api\Plugin\PluginFormTrait; use Drupal\search_api\IndexInterface; use Drupal\search_api\Utility\Dependencies; +use Drupal\search_api\Plugin\PluginFormTrait; use Drupal\search_api\Utility\FieldsHelperInterface; use Drupal\search_api\Utility\Utility; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -1022,6 +1021,72 @@ public function getFieldDependencies(array $fields) { return $dependencies; } + /** + * {@inheritdoc} + */ + public function canContainEntityReferences(): bool { + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function getAffectedItemsForEntityChange(EntityInterface $entity, array $foreign_entity_relationship_map, EntityInterface $original_entity = NULL): array { + if (!($entity instanceof ContentEntityInterface)) { + return []; + } + + $ids_to_reindex = []; + $path_separator = IndexInterface::PROPERTY_PATH_SEPARATOR; + foreach ($foreign_entity_relationship_map as $relation_info) { + // Check whether entity type and (if specified) bundles match the entity. + if ($relation_info['entity_type'] !== $entity->getEntityTypeId()) { + continue; + } + if (!empty($relation_info['bundles']) + && !in_array($entity->bundle(), $relation_info['bundles'])) { + continue; + } + // Maybe this entity belongs to a bundle that does not have this field + // attached. Hence we have this check to ensure the field is present on + // this particular entity. + if (!$entity->hasField($relation_info['field_name'])) { + continue; + } + + $items = $entity->get($relation_info['field_name']); + + // We trigger re-indexing if either it is a removed entity or the + // entity has changed its field value (in case it's an update). + if (!$original_entity || !$items->equals($original_entity->get($relation_info['field_name']))) { + $query = $this->entityTypeManager->getStorage($this->getEntityTypeId()) + ->getQuery(); + $query->accessCheck(FALSE); + + // Luckily, to translate from property path to the entity query + // condition syntax, all we have to do is replace the property path + // separator with the entity query path separator (a dot) and that's it. + $property_path = $relation_info['property_path_to_foreign_entity']; + $property_path = str_replace($path_separator, '.', $property_path); + $query->condition($property_path, $entity->id()); + + try { + $entity_ids = array_values($query->execute()); + } + catch (\Exception $e) { + continue; + } + foreach ($entity_ids as $entity_id) { + foreach ($this->getLanguages() as $language) { + $ids_to_reindex["$entity_id:{$language->getId()}"] = 1; + } + } + } + } + + return array_keys($ids_to_reindex); + } + /** * Computes all dependencies of the given property path. * @@ -1081,79 +1146,9 @@ protected function getPropertyPathDependencies($property_path, array $properties * {@inheritdoc} */ public static function getIndexesForEntity(ContentEntityInterface $entity) { - $datasource_id = 'entity:' . $entity->getEntityTypeId(); - $entity_bundle = $entity->bundle(); - $has_bundles = $entity->getEntityType()->hasKey('bundle'); - - // Needed for PhpStorm. See https://youtrack.jetbrains.com/issue/WI-23395. - /** @var \Drupal\search_api\IndexInterface[] $indexes */ - $indexes = Index::loadMultiple(); - - foreach ($indexes as $index_id => $index) { - // Filter out indexes that don't contain the datasource in question. - if (!$index->isValidDatasource($datasource_id)) { - unset($indexes[$index_id]); - } - elseif ($has_bundles) { - // If the entity type supports bundles, we also have to filter out - // indexes that exclude the entity's bundle. - $config = $index->getDatasource($datasource_id)->getConfiguration(); - if (!Utility::matches($entity_bundle, $config['bundles'])) { - unset($indexes[$index_id]); - } - } - } - - return $indexes; - } - - /** - * Filters a set of datasource-specific item IDs. - * - * Returns only those item IDs that are valid for the given datasource and - * index. This method only checks the item language, though – whether an - * entity with that ID actually exists, or whether it has a bundle included - * for that datasource, is not verified. - * - * @param \Drupal\search_api\IndexInterface $index - * The index for which to validate. - * @param string $datasource_id - * The ID of the datasource on the index for which to validate. - * @param string[] $item_ids - * The item IDs to be validated. - * - * @return string[] - * All given item IDs that are valid for that index and datasource. - */ - public static function filterValidItemIds(IndexInterface $index, $datasource_id, array $item_ids) { - if (!$index->isValidDatasource($datasource_id)) { - return $item_ids; - } - $config = $index->getDatasource($datasource_id)->getConfiguration(); - // If the entity type doesn't allow translations, we just accept all IDs. - // (If the entity type were translatable, the config key would have been set - // with the default configuration.) - if (!isset($config['languages']['selected'])) { - return $item_ids; - } - $always_valid = [ - LanguageInterface::LANGCODE_NOT_SPECIFIED, - LanguageInterface::LANGCODE_NOT_APPLICABLE, - ]; - $valid_ids = []; - foreach ($item_ids as $item_id) { - $pos = strrpos($item_id, ':'); - // Item IDs without colons are always invalid. - if ($pos === FALSE) { - continue; - } - $langcode = substr($item_id, $pos + 1); - if (Utility::matches($langcode, $config['languages']) - || in_array($langcode, $always_valid)) { - $valid_ids[] = $item_id; - } - } - return $valid_ids; + return \Drupal::getContainer() + ->get('search_api.entity_datasource.tracking_manager') + ->getIndexesForEntity($entity); } /** diff --git a/web/modules/search_api/src/Plugin/search_api/datasource/ContentEntityTrackingManager.php b/web/modules/search_api/src/Plugin/search_api/datasource/ContentEntityTrackingManager.php new file mode 100644 index 0000000000000000000000000000000000000000..d918a8ab973c176ec403cd9b264692d9ff8bd830 --- /dev/null +++ b/web/modules/search_api/src/Plugin/search_api/datasource/ContentEntityTrackingManager.php @@ -0,0 +1,422 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\search_api\Plugin\search_api\datasource; + +use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException; +use Drupal\Component\Plugin\Exception\PluginNotFoundException; +use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityTypeManager; +use Drupal\Core\Language\LanguageInterface; +use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\search_api\IndexInterface; +use Drupal\search_api\SearchApiException; +use Drupal\search_api\Task\TaskManagerInterface; +use Drupal\search_api\Utility\Utility; + +/** + * Provides hook implementations on behalf of the Content Entity datasource. + * + * @see \Drupal\search_api\Plugin\search_api\datasource\ContentEntity + */ +class ContentEntityTrackingManager { + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManager + */ + protected $entityTypeManager; + + /** + * The language manager. + * + * @var \Drupal\Core\Language\LanguageManagerInterface + */ + protected $languageManager; + + /** + * The Search API task manager. + * + * @var \Drupal\search_api\Task\TaskManagerInterface + */ + protected $taskManager; + + /** + * Constructs a new class instance. + * + * @param \Drupal\Core\Entity\EntityTypeManager $entityTypeManager + * The entity type manager. + * @param \Drupal\Core\Language\LanguageManagerInterface $languageManager + * The language manager. + * @param \Drupal\search_api\Task\TaskManagerInterface $taskManager + * The task manager. + */ + public function __construct(EntityTypeManager $entityTypeManager, LanguageManagerInterface $languageManager, TaskManagerInterface $taskManager) { + $this->entityTypeManager = $entityTypeManager; + $this->languageManager = $languageManager; + $this->taskManager = $taskManager; + } + + /** + * Implements hook_entity_insert(). + * + * Adds entries for all languages of the new entity to the tracking table for + * each index that tracks entities of this type. + * + * By setting the $entity->search_api_skip_tracking property to a true-like + * value before this hook is invoked, you can prevent this behavior and make the + * Search API ignore this new entity. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The new entity. + * + * @see search_api_entity_insert() + */ + public function entityInsert(EntityInterface $entity) { + // Check if the entity is a content entity. + if (!($entity instanceof ContentEntityInterface) || $entity->search_api_skip_tracking) { + return; + } + $indexes = $this->getIndexesForEntity($entity); + if (!$indexes) { + return; + } + + // Compute the item IDs for all languages of the entity. + $item_ids = []; + $entity_id = $entity->id(); + foreach (array_keys($entity->getTranslationLanguages()) as $langcode) { + $item_ids[] = $entity_id . ':' . $langcode; + } + $datasource_id = 'entity:' . $entity->getEntityTypeId(); + foreach ($indexes as $index) { + $filtered_item_ids = $this->filterValidItemIds($index, $datasource_id, $item_ids); + $index->trackItemsInserted($datasource_id, $filtered_item_ids); + } + } + + /** + * Implements hook_entity_update(). + * + * Updates the corresponding tracking table entries for each index that tracks + * this entity. + * + * Also takes care of new or deleted translations. + * + * By setting the $entity->search_api_skip_tracking property to a true-like + * value before this hook is invoked, you can prevent this behavior and make the + * Search API ignore this update. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The updated entity. + * + * @see search_api_entity_update() + */ + public function entityUpdate(EntityInterface $entity) { + // Check if the entity is a content entity. + if (!($entity instanceof ContentEntityInterface) || $entity->search_api_skip_tracking) { + return; + } + + $indexes = $this->getIndexesForEntity($entity); + if (!$indexes) { + return; + } + + // Compare old and new languages for the entity to identify inserted, + // updated and deleted translations (and, therefore, search items). + $entity_id = $entity->id(); + $inserted_item_ids = []; + $updated_item_ids = $entity->getTranslationLanguages(); + $deleted_item_ids = []; + $old_translations = $entity->original->getTranslationLanguages(); + foreach ($old_translations as $langcode => $language) { + if (!isset($updated_item_ids[$langcode])) { + $deleted_item_ids[] = $langcode; + } + } + foreach ($updated_item_ids as $langcode => $language) { + if (!isset($old_translations[$langcode])) { + unset($updated_item_ids[$langcode]); + $inserted_item_ids[] = $langcode; + } + } + + $datasource_id = 'entity:' . $entity->getEntityTypeId(); + $combine_id = function ($langcode) use ($entity_id) { + return $entity_id . ':' . $langcode; + }; + $inserted_item_ids = array_map($combine_id, $inserted_item_ids); + $updated_item_ids = array_map($combine_id, array_keys($updated_item_ids)); + $deleted_item_ids = array_map($combine_id, $deleted_item_ids); + foreach ($indexes as $index) { + if ($inserted_item_ids) { + $filtered_item_ids = $this->filterValidItemIds($index, $datasource_id, $inserted_item_ids); + $index->trackItemsInserted($datasource_id, $filtered_item_ids); + } + if ($updated_item_ids) { + $index->trackItemsUpdated($datasource_id, $updated_item_ids); + } + if ($deleted_item_ids) { + $index->trackItemsDeleted($datasource_id, $deleted_item_ids); + } + } + } + + /** + * Implements hook_entity_delete(). + * + * Deletes all entries for this entity from the tracking table for each index + * that tracks this entity type. + * + * By setting the $entity->search_api_skip_tracking property to a true-like + * value before this hook is invoked, you can prevent this behavior and make the + * Search API ignore this deletion. (Note that this might lead to stale data in + * the tracking table or on the server, since the item will not removed from + * there (if it has been added before).) + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The deleted entity. + * + * @see search_api_entity_delete() + */ + public function entityDelete(EntityInterface $entity) { + // Check if the entity is a content entity. + if (!($entity instanceof ContentEntityInterface) || $entity->search_api_skip_tracking) { + return; + } + + $indexes = $this->getIndexesForEntity($entity); + if (!$indexes) { + return; + } + + // Remove the search items for all the entity's translations. + $item_ids = []; + $entity_id = $entity->id(); + foreach (array_keys($entity->getTranslationLanguages()) as $langcode) { + $item_ids[] = $entity_id . ':' . $langcode; + } + $datasource_id = 'entity:' . $entity->getEntityTypeId(); + foreach ($indexes as $index) { + $index->trackItemsDeleted($datasource_id, $item_ids); + } + } + + /** + * Retrieves all indexes that are configured to index the given entity. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The entity for which to check. + * + * @return \Drupal\search_api\IndexInterface[] + * All indexes that are configured to index the given entity (using the + * default Content Entity datasource plugin). + */ + public function getIndexesForEntity(ContentEntityInterface $entity): array { + // @todo This is called for every single entity insert, update or deletion + // on the whole site. Should maybe be cached? + $datasource_id = 'entity:' . $entity->getEntityTypeId(); + $entity_bundle = $entity->bundle(); + $has_bundles = $entity->getEntityType()->hasKey('bundle'); + + /** @var \Drupal\search_api\IndexInterface[] $indexes */ + $indexes = []; + try { + $indexes = $this->entityTypeManager->getStorage('search_api_index') + ->loadMultiple(); + } + // @todo Replace with multi-catch once we depend on PHP 7.1+. + catch (InvalidPluginDefinitionException $e) { + // Can't really happen, but play it safe to appease static code analysis. + } + catch (PluginNotFoundException $e) { + // Can't really happen, but play it safe to appease static code analysis. + } + + foreach ($indexes as $index_id => $index) { + // Filter out indexes that don't contain the datasource in question. + if (!$index->isValidDatasource($datasource_id)) { + unset($indexes[$index_id]); + } + elseif ($has_bundles) { + // If the entity type supports bundles, we also have to filter out + // indexes that exclude the entity's bundle. + try { + $config = $index->getDatasource($datasource_id)->getConfiguration(); + } + catch (SearchApiException $e) { + // Can't really happen, but play it safe to appease static code + // analysis. + unset($indexes[$index_id]); + continue; + } + if (!Utility::matches($entity_bundle, $config['bundles'])) { + unset($indexes[$index_id]); + } + } + } + + return $indexes; + } + + /** + * Implements hook_ENTITY_TYPE_update() for type "search_api_index". + * + * Detects changes in the selected bundles or languages and adds/removes items + * to/from tracking accordingly. + * + * @param \Drupal\search_api\IndexInterface $index + * The index that was updated. + * + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + * Thrown if a datasource referenced an unknown entity type. + * @throws \Drupal\search_api\SearchApiException + * Never thrown, but static analysis tools think it could be. + * + * @see search_api_search_api_index_update() + */ + public function indexUpdate(IndexInterface $index) { + if (!$index->status()) { + return; + } + /** @var \Drupal\search_api\IndexInterface $original */ + $original = $index->original ?? NULL; + if (!$original || !$original->status()) { + return; + } + + foreach ($index->getDatasources() as $datasource_id => $datasource) { + if ($datasource->getBaseId() != 'entity' + || !$original->isValidDatasource($datasource_id)) { + continue; + } + $old_datasource = $original->getDatasource($datasource_id); + $old_config = $old_datasource->getConfiguration(); + $new_config = $datasource->getConfiguration(); + + if ($old_config != $new_config) { + // Bundles and languages share the same structure, so changes can be + // processed in a unified way. + $tasks = []; + $insert_task = ContentEntityTaskManager::INSERT_ITEMS_TASK_TYPE; + $delete_task = ContentEntityTaskManager::DELETE_ITEMS_TASK_TYPE; + $settings = []; + $entity_type = $this->entityTypeManager + ->getDefinition($datasource->getEntityTypeId()); + if ($entity_type->hasKey('bundle')) { + $settings['bundles'] = $datasource->getBundles(); + } + if ($entity_type->isTranslatable()) { + $settings['languages'] = $this->languageManager->getLanguages(); + } + + // Determine which bundles/languages have been newly selected or + // deselected and then assign them to the appropriate actions depending + // on the current "default" setting. + foreach ($settings as $setting => $all) { + $old_selected = array_flip($old_config[$setting]['selected']); + $new_selected = array_flip($new_config[$setting]['selected']); + + // First, check if the "default" setting changed and invert the checked + // items for the old config, so the following comparison makes sense. + if ($old_config[$setting]['default'] != $new_config[$setting]['default']) { + $old_selected = array_diff_key($all, $old_selected); + } + + $newly_selected = array_keys(array_diff_key($new_selected, $old_selected)); + $newly_unselected = array_keys(array_diff_key($old_selected, $new_selected)); + if ($new_config[$setting]['default']) { + $tasks[$insert_task][$setting] = $newly_unselected; + $tasks[$delete_task][$setting] = $newly_selected; + } + else { + $tasks[$insert_task][$setting] = $newly_selected; + $tasks[$delete_task][$setting] = $newly_unselected; + } + } + + // This will keep only those tasks where at least one of "bundles" or + // "languages" is non-empty. + $tasks = array_filter($tasks, 'array_filter'); + foreach ($tasks as $task => $data) { + $data += [ + 'datasource' => $datasource_id, + 'page' => 0, + ]; + $this->taskManager->addTask($task, NULL, $index, $data); + } + + // If we added any new tasks, set a batch for them. (If we aren't in a + // form submission, this will just be ignored.) + if ($tasks) { + $this->taskManager->setTasksBatch([ + 'index_id' => $index->id(), + 'type' => array_keys($tasks), + ]); + } + } + } + } + + /** + * Filters a set of datasource-specific item IDs. + * + * Returns only those item IDs that are valid for the given datasource and + * index. This method only checks the item language, though – whether an + * entity with that ID actually exists, or whether it has a bundle included + * for that datasource, is not verified. + * + * @param \Drupal\search_api\IndexInterface $index + * The index for which to validate. + * @param string $datasource_id + * The ID of the datasource on the index for which to validate. + * @param string[] $item_ids + * The item IDs to be validated. + * + * @return string[] + * All given item IDs that are valid for that index and datasource. + */ + protected function filterValidItemIds(IndexInterface $index, string $datasource_id, array $item_ids): array { + if (!$index->isValidDatasource($datasource_id)) { + return $item_ids; + } + + try { + $config = $index->getDatasource($datasource_id)->getConfiguration(); + } + catch (SearchApiException $e) { + // Can't really happen, but play it safe to appease static code analysis. + return $item_ids; + } + + // If the entity type doesn't allow translations, we just accept all IDs. + // (If the entity type were translatable, the config key would have been set + // with the default configuration.) + if (!isset($config['languages']['selected'])) { + return $item_ids; + } + $always_valid = [ + LanguageInterface::LANGCODE_NOT_SPECIFIED, + LanguageInterface::LANGCODE_NOT_APPLICABLE, + ]; + $valid_ids = []; + foreach ($item_ids as $item_id) { + $pos = strrpos($item_id, ':'); + // Item IDs without colons are always invalid. + if ($pos === FALSE) { + continue; + } + $langcode = substr($item_id, $pos + 1); + if (Utility::matches($langcode, $config['languages']) + || in_array($langcode, $always_valid)) { + $valid_ids[] = $item_id; + } + } + return $valid_ids; + } + +} diff --git a/web/modules/search_api/src/Plugin/search_api/datasource/EntityDatasourceInterface.php b/web/modules/search_api/src/Plugin/search_api/datasource/EntityDatasourceInterface.php index c3ad7b38d837fc830daafbd1341583fbd4dd51d8..3805ddeb66391de45e8e301950117014d0c5d341 100644 --- a/web/modules/search_api/src/Plugin/search_api/datasource/EntityDatasourceInterface.php +++ b/web/modules/search_api/src/Plugin/search_api/datasource/EntityDatasourceInterface.php @@ -2,7 +2,7 @@ namespace Drupal\search_api\Plugin\search_api\datasource; -@trigger_error('\Drupal\search_api\Plugin\search_api\datasource\EntityDatasourceInterface is deprecated in search_api:8.x-1.16 and will be removed in search_api:9.x-1.0. There is no replacement. See https://www.drupal.org/node/3103584.', E_USER_DEPRECATED); +@trigger_error('\Drupal\search_api\Plugin\search_api\datasource\EntityDatasourceInterface is deprecated in search_api:8.x-1.16 and is removed from search_api:2.0.0. There is no replacement. See https://www.drupal.org/node/3103584.', E_USER_DEPRECATED); use Drupal\Core\Entity\ContentEntityInterface; use Drupal\search_api\Datasource\DatasourceInterface; @@ -10,7 +10,7 @@ /** * Describes an interface for entity datasources. * - * @deprecated in search_api:8.x-1.16 and will be removed in search_api:9.x-1.0. + * @deprecated in search_api:8.x-1.16 and is removed from search_api:2.0.0. * There is no replacement. * * @see https://www.drupal.org/node/3103584 diff --git a/web/modules/search_api/src/Plugin/search_api/parse_mode/Terms.php b/web/modules/search_api/src/Plugin/search_api/parse_mode/Terms.php index c9aef058a57027e91efb34fbb223eb493cf0c719..88a6d72094ee941afde2b74cb755018263b4b0cd 100644 --- a/web/modules/search_api/src/Plugin/search_api/parse_mode/Terms.php +++ b/web/modules/search_api/src/Plugin/search_api/parse_mode/Terms.php @@ -35,8 +35,12 @@ public function parseInput($keys) { // Check for negation. if ($token[0] === '-' && !$quoted) { - $negated = TRUE; $token = ltrim($token, '-'); + // If token is empty after trimming, ignore it. + if ($token === '') { + continue; + } + $negated = TRUE; } // Depending on whether we are currently in a quoted phrase, or maybe just diff --git a/web/modules/search_api/src/Plugin/search_api/processor/AddURL.php b/web/modules/search_api/src/Plugin/search_api/processor/AddURL.php index 5fd163bc699bd3203f89448d277b1ea0b007145b..0ad57ea8f62a62457c944a0d772dfcff01085baf 100644 --- a/web/modules/search_api/src/Plugin/search_api/processor/AddURL.php +++ b/web/modules/search_api/src/Plugin/search_api/processor/AddURL.php @@ -5,7 +5,7 @@ use Drupal\search_api\Datasource\DatasourceInterface; use Drupal\search_api\Item\ItemInterface; use Drupal\search_api\Processor\ProcessorPluginBase; -use Drupal\search_api\Processor\ProcessorProperty; +use Drupal\search_api\Plugin\search_api\processor\Property\AddURLProperty; /** * Adds the item's URL to the indexed data. @@ -36,7 +36,7 @@ public function getPropertyDefinitions(DatasourceInterface $datasource = NULL) { 'type' => 'string', 'processor_id' => $this->getPluginId(), ]; - $properties['search_api_url'] = new ProcessorProperty($definition); + $properties['search_api_url'] = new AddURLProperty($definition); } return $properties; @@ -48,12 +48,13 @@ public function getPropertyDefinitions(DatasourceInterface $datasource = NULL) { public function addFieldValues(ItemInterface $item) { $url = $item->getDatasource()->getItemUrl($item->getOriginalObject()); if ($url) { - $url = $url->toString(); $fields = $item->getFields(FALSE); $fields = $this->getFieldsHelper() ->filterForPropertyPath($fields, NULL, 'search_api_url'); foreach ($fields as $field) { - $field->addValue($url); + $config = $field->getConfiguration(); + $url->setAbsolute(!empty($config['absolute'])); + $field->addValue($url->toString()); } } } diff --git a/web/modules/search_api/src/Plugin/search_api/processor/AggregatedFields.php b/web/modules/search_api/src/Plugin/search_api/processor/AggregatedFields.php index 87952a26de630d7365791dc09bdab07611c99f1f..b869b89c335ddcfe91c8b4bcda314aff94e29c2f 100644 --- a/web/modules/search_api/src/Plugin/search_api/processor/AggregatedFields.php +++ b/web/modules/search_api/src/Plugin/search_api/processor/AggregatedFields.php @@ -110,6 +110,7 @@ public function addFieldValues(ItemInterface $item) { $values = [reset($values)]; } break; + case 'last': if ($values) { $values = [end($values)]; diff --git a/web/modules/search_api/src/Plugin/search_api/processor/IgnoreCharacters.php b/web/modules/search_api/src/Plugin/search_api/processor/IgnoreCharacters.php index 0ee887497dd748079beb5e3a983f9a6ada9ec0d6..5a21e5dfe805b9f7c0f18b5bb7849409cc521a4b 100644 --- a/web/modules/search_api/src/Plugin/search_api/processor/IgnoreCharacters.php +++ b/web/modules/search_api/src/Plugin/search_api/processor/IgnoreCharacters.php @@ -115,6 +115,9 @@ public function submitConfigurationForm(array &$form, FormStateInterface $form_s * {@inheritdoc} */ protected function process(&$value) { + if ($value === NULL) { + return; + } if ($this->configuration['ignorable']) { if (!isset($this->ignorable)) { $this->ignorable = str_replace('/', '\/', $this->configuration['ignorable']); diff --git a/web/modules/search_api/src/Plugin/search_api/processor/Property/AddURLProperty.php b/web/modules/search_api/src/Plugin/search_api/processor/Property/AddURLProperty.php new file mode 100644 index 0000000000000000000000000000000000000000..e18b104dbac7ebdefa3226474753ab0315db1034 --- /dev/null +++ b/web/modules/search_api/src/Plugin/search_api/processor/Property/AddURLProperty.php @@ -0,0 +1,45 @@ +<?php + +namespace Drupal\search_api\Plugin\search_api\processor\Property; + +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\search_api\Item\FieldInterface; +use Drupal\search_api\Processor\ConfigurablePropertyBase; + +/** + * Defines an "Item URL" property. + * + * @see \Drupal\search_api\Plugin\search_api\processor\AddURL + */ +class AddURLProperty extends ConfigurablePropertyBase { + + use StringTranslationTrait; + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return [ + 'absolute' => FALSE, + ]; + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(FieldInterface $field, array $form, FormStateInterface $form_state) { + $configuration = $field->getConfiguration(); + + $form['absolute'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Generate absolute URL'), + '#description' => $this->t('Check this box to pass absolute URLs to the index. This can be useful when indexing multiple sites with a single search index.'), + '#default_value' => $configuration['absolute'] ?? FALSE, + '#return_value' => TRUE, + ]; + + return $form; + } + +} diff --git a/web/modules/search_api/src/Plugin/search_api/processor/RenderedItem.php b/web/modules/search_api/src/Plugin/search_api/processor/RenderedItem.php index cc7b8cbeefef0c248ebd19723a35f5255d5089a4..1f7688e89099762cca1ea2c7348cb6da6913d434 100644 --- a/web/modules/search_api/src/Plugin/search_api/processor/RenderedItem.php +++ b/web/modules/search_api/src/Plugin/search_api/processor/RenderedItem.php @@ -269,13 +269,13 @@ public function addFieldValues(ItemInterface $item) { try { $build = $datasource->viewItem($item->getOriginalObject(), $view_mode); - // Add the excerpt to the render array to allow adding it to view modes. - if ($item->getExcerpt()) { + if ($build) { + // Add the excerpt to the render array to allow adding it to view modes. $build['#search_api_excerpt'] = $item->getExcerpt(); - } - $value = (string) $this->getRenderer()->renderPlain($build); - if ($value) { - $field->addValue($value); + $value = (string) $this->getRenderer()->renderPlain($build); + if ($value) { + $field->addValue($value); + } } } catch (\Exception $e) { diff --git a/web/modules/search_api/src/Plugin/search_api/processor/Tokenizer.php b/web/modules/search_api/src/Plugin/search_api/processor/Tokenizer.php index 2b6fd4575c196f2da528c9a8ff607f1fcc9773a1..d42ff7173d873d927cd26dcc7e6a3e159f32dd09 100644 --- a/web/modules/search_api/src/Plugin/search_api/processor/Tokenizer.php +++ b/web/modules/search_api/src/Plugin/search_api/processor/Tokenizer.php @@ -27,7 +27,14 @@ class Tokenizer extends FieldsProcessorPluginBase { /** - * PCRE character class contents identifying spaces in this processor. + * PCRE character class contents identifying ignored characters. + * + * @var string + */ + protected $ignored; + + /** + * PCRE character class contents identifying spaces. * * @var string */ @@ -40,6 +47,7 @@ public function defaultConfiguration() { $configuration = parent::defaultConfiguration(); $configuration += [ + 'ignored' => '._-', 'spaces' => '', 'overlap_cjk' => TRUE, 'minimum_word_size' => 3, @@ -66,6 +74,14 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta ':pcre-url' => Url::fromUri('https://php.net/manual/regexp.reference.character-classes.php')->toString(), ':doc-url' => Url::fromUri('https://api.drupal.org/api/drupal/core!lib!Drupal!Component!Utility!Unicode.php/constant/Unicode%3A%3APREG_CLASS_WORD_BOUNDARY/8')->toString(), ]; + + $form['ignored'] = [ + '#type' => 'textfield', + '#title' => $this->t('Ignored characters'), + '#description' => $this->t('Specify the characters that should be removed prior to processing. Dots, dashes, and underscores are ignored by default to allow meaningful search behavior with acronyms and URLs. Specify the characters as the inside of a <a href=":pcre-url">PCRE character class</a>.', $args), + '#default_value' => $this->configuration['ignored'], + ]; + $form['spaces'] = [ '#type' => 'textfield', '#title' => $this->t('Whitespace characters'), @@ -98,9 +114,11 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { parent::validateConfigurationForm($form, $form_state); - $spaces = str_replace('/', '\/', trim($form_state->getValues()['spaces'])); - if ($spaces !== '' && @preg_match('/[' . $spaces . ']+/u', '') === FALSE) { - $form_state->setError($form['spaces'], $form['spaces']['#title'] . ': ' . $this->t('The entered text is no valid PCRE character class.')); + foreach (['spaces', 'ignored'] as $field) { + $field_value = str_replace('/', '\/', trim($form_state->getValues()[$field])); + if ($field_value !== '' && @preg_match('/[' . $field_value . ']+/u', '') === FALSE) { + $form_state->setError($form[$field], $form[$field]['#title'] . ': ' . $this->t('The entered text is no valid PCRE character class.')); + } } } @@ -236,17 +254,14 @@ protected function simplifyText($text) { // Readable regular expression: "([number]+)[punctuation]+(?=[number])". $text = preg_replace('/([' . $this->getPregClassNumbers() . ']+)[' . $this->getPregClassPunctuation() . ']+(?=[' . $this->getPregClassNumbers() . '])/u', '\1', $text); - // Multiple dot and dash groups are word boundaries and replaced with space. - // No need to use the Unicode modifier here because 0-127 ASCII characters - // can't match higher UTF-8 characters as the leftmost bit of those are 1. - $text = preg_replace('/[.-]{2,}/', ' ', $text); + // A group of multiple ignored characters is still treated as whitespace. + $text = preg_replace('/[' . $this->ignored . ']{2,}/u', ' ', $text); - // The dot, underscore and dash are simply removed. This allows meaningful - // search behavior with acronyms and URLs. See Unicode note directly above. - $text = preg_replace('/[._-]+/', '', $text); + // Remove all other instances of ignored characters. + $text = preg_replace('/[' . $this->ignored . ']+/u', '', $text); - // With the exception of the rules above, we consider all punctuation, - // marks, spaces, etc, to be a word boundary. + // Finally, convert all characters we want to treat as word boundaries to + // plain spaces. $text = preg_replace('/[' . $this->spaces . ']+/u', ' ', $text); return trim($text); @@ -334,6 +349,14 @@ protected function prepare() { $this->spaces = Unicode::PREG_CLASS_WORD_BOUNDARY; } } + if (!isset($this->ignored)) { + if ($this->configuration['ignored'] !== '') { + $this->ignored = str_replace('/', '\/', $this->configuration['ignored']); + } + else { + $this->ignored = '._-'; + } + } } } diff --git a/web/modules/search_api/src/Plugin/views/field/SearchApiBulkForm.php b/web/modules/search_api/src/Plugin/views/field/SearchApiBulkForm.php index 89782fbe6e98826a7a72bdcc4a1f6db97aa8b5a7..35c6a63662d40dbc474672f95cef0cfbd8de659b 100644 --- a/web/modules/search_api/src/Plugin/views/field/SearchApiBulkForm.php +++ b/web/modules/search_api/src/Plugin/views/field/SearchApiBulkForm.php @@ -84,6 +84,7 @@ public function getEntity(ResultRow $values) { } // phpcs:disable Drupal.Commenting.FunctionComment.TypeHintMissing + /** * Form constructor for the bulk form. * @@ -158,6 +159,8 @@ public function viewsForm(&$form, FormStateInterface $form_state) { } } + // phpcs:enable Drupal.Commenting.FunctionComment.TypeHintMissing + /** * {@inheritdoc} */ diff --git a/web/modules/search_api/src/Plugin/views/field/SearchApiEntity.php b/web/modules/search_api/src/Plugin/views/field/SearchApiEntity.php index 30644eb431b70983755b375130a763bc387ac9c6..62e3bd0a6232dc2fe6c9a233b9427f00201e26e4 100644 --- a/web/modules/search_api/src/Plugin/views/field/SearchApiEntity.php +++ b/web/modules/search_api/src/Plugin/views/field/SearchApiEntity.php @@ -316,7 +316,7 @@ protected function getItem(EntityInterface $entity) { } $view_mode = $this->options['display_methods'][$bundle]['view_mode']; - $build = $this->getEntityFieldManager() + $build = $this->getEntityTypeManager() ->getViewBuilder($entity->getEntityTypeId()) ->view($entity, $view_mode); return [ diff --git a/web/modules/search_api/src/Plugin/views/field/SearchApiRenderedItem.php b/web/modules/search_api/src/Plugin/views/field/SearchApiRenderedItem.php index 8ea57fa9c2e40bd90367570e9628a6358097338d..9b039129c014fcb75846a1ae49b1ac34b48d91c5 100644 --- a/web/modules/search_api/src/Plugin/views/field/SearchApiRenderedItem.php +++ b/web/modules/search_api/src/Plugin/views/field/SearchApiRenderedItem.php @@ -112,7 +112,7 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { /** * {@inheritdoc} */ - public function query() { + public function query($use_groupby = FALSE) { $this->addRetrievedProperty('_object'); } @@ -144,7 +144,13 @@ public function render(ResultRow $row) { $view_mode = $this->options['view_modes'][$datasource_id][$bundle] ?? 'default'; try { - return $this->index->getDatasource($datasource_id)->viewItem($row->_object, $view_mode); + $build = $this->index->getDatasource($datasource_id) + ->viewItem($row->_object, $view_mode); + if ($build) { + // Add the excerpt to the render array to allow adding it to view modes. + $render['#search_api_excerpt'] = $row->_item->getExcerpt(); + } + return $build; } catch (SearchApiException $e) { $this->logException($e); diff --git a/web/modules/search_api/src/Plugin/views/filter/SearchApiFulltext.php b/web/modules/search_api/src/Plugin/views/filter/SearchApiFulltext.php index 85056564307af45e3bc5284756b67bd12b1c7265..df5f705e75be0773cc9641d406fe16d3fc693fc4 100644 --- a/web/modules/search_api/src/Plugin/views/filter/SearchApiFulltext.php +++ b/web/modules/search_api/src/Plugin/views/filter/SearchApiFulltext.php @@ -281,7 +281,7 @@ public function validateExposed(&$form, FormStateInterface $form_state) { if (!$words) { $vars['@count'] = $this->options['min_length']; $msg = $this->t('You must include at least one positive keyword with @count characters or more.', $vars); - $form_state->setError($form[$identifier], $msg); + $form_state->setErrorByName($identifier, $msg); } $input = implode(' ', $words); } diff --git a/web/modules/search_api/src/Plugin/views/filter/SearchApiString.php b/web/modules/search_api/src/Plugin/views/filter/SearchApiString.php index 59f46d4712e97d3f3c259e8120e0489c3c47576f..8f5592bf0ad7e07fefc09a3ba0c5e4a531bad1e7 100644 --- a/web/modules/search_api/src/Plugin/views/filter/SearchApiString.php +++ b/web/modules/search_api/src/Plugin/views/filter/SearchApiString.php @@ -26,7 +26,7 @@ protected function opBetween($field) { $operator = $this->operator == 'between' ? 'BETWEEN' : 'NOT BETWEEN'; $this->getQuery()->addWhere($this->options['group'], $field, [ $this->value['min'], - $this->value['max'] + $this->value['max'], ], $operator); } elseif ($this->value['min'] != '') { diff --git a/web/modules/search_api/src/Plugin/views/query/SearchApiQuery.php b/web/modules/search_api/src/Plugin/views/query/SearchApiQuery.php index 03727f6698a23754787b87e56caeb899d879974e..2a06e9e39089116315fafda0829812179e2fe952 100644 --- a/web/modules/search_api/src/Plugin/views/query/SearchApiQuery.php +++ b/web/modules/search_api/src/Plugin/views/query/SearchApiQuery.php @@ -282,10 +282,13 @@ public function init(ViewExecutable $view, DisplayPluginBase $display, array &$o * * @return $this * - * @deprecated in 8.x-1.11. Use ::addRetrievedFieldValue() instead. + * @deprecated in search_api:8.x-1.11 and is removed from search_api:2.0.0. Use + * addRetrievedFieldValue() instead. + * + * @see https://www.drupal.org/node/3011060 */ public function addRetrievedProperty($combined_property_path) { - @trigger_error('\Drupal\search_api\Plugin\views\query\SearchApiQuery::addRetrievedProperty() is deprecated in Search API 8.x-1.11. Use addRetrievedFieldValue() instead. See https://www.drupal.org/node/3009136', E_USER_DEPRECATED); + @trigger_error('\Drupal\search_api\Plugin\views\query\SearchApiQuery::addRetrievedProperty() is deprecated in search_api:8.x-1.11 and is removed from search_api:2.0.0. Use addRetrievedFieldValue() instead. See https://www.drupal.org/node/3011060', E_USER_DEPRECATED); $this->addField(NULL, $combined_property_path); return $this; } @@ -417,11 +420,13 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { * containing entities to their entity types. Otherwise, TRUE if there is at * least one such datasource. * - * @deprecated Will be removed in a future version of the module. Use + * @deprecated in search_api:8.x-1.5 and is removed from search_api:2.0.0. Use * \Drupal\search_api\IndexInterface::getEntityTypes() instead. + * + * @see https://www.drupal.org/node/2899682 */ public function getEntityTypes($return_bool = FALSE) { - @trigger_error('\Drupal\search_api\Plugin\views\query\SearchApiQuery::getEntityTypes() is deprecated in Search API 8.x-1.5. Use \Drupal\search_api\IndexInterface::getEntityTypes() instead. See https://www.drupal.org/node/2899678', E_USER_DEPRECATED); + @trigger_error('\Drupal\search_api\Plugin\views\query\SearchApiQuery::getEntityTypes() is deprecated in Search API 8.x-1.5. Use \Drupal\search_api\IndexInterface::getEntityTypes() instead. See https://www.drupal.org/node/2899682', E_USER_DEPRECATED); $types = $this->index->getEntityTypes(); return $return_bool ? (bool) $types : $types; } diff --git a/web/modules/search_api/src/Query/Query.php b/web/modules/search_api/src/Query/Query.php index c582f768794cd54844b3334a1a0e077827846764..f6b419ae81ab58947e48d87e3ab1f06da984d124 100644 --- a/web/modules/search_api/src/Query/Query.php +++ b/web/modules/search_api/src/Query/Query.php @@ -593,7 +593,7 @@ public function preExecute() { $this->getEventDispatcher()->dispatch($event_name, $event); } - $description = 'This hook is deprecated in search_api 8.x-1.14 and will be removed in 9.x-1.0. Please use the "search_api.query_pre_execute" event instead. See https://www.drupal.org/node/3059866'; + $description = 'This hook is deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. Please use the "search_api.query_pre_execute" event instead. See https://www.drupal.org/node/3059866'; $this->getModuleHandler()->alterDeprecated($description, $hooks, $this); } } @@ -623,7 +623,7 @@ public function postExecute() { $this->getEventDispatcher()->dispatch("$event_base_name.$tag", $event); $this->results = $event->getResults(); } - $description = 'This hook is deprecated in search_api 8.x-1.14 and will be removed in 9.x-1.0. Please use the "search_api.processing_results" event instead. See https://www.drupal.org/node/3059866'; + $description = 'This hook is deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. Please use the "search_api.processing_results" event instead. See https://www.drupal.org/node/3059866'; $this->getModuleHandler()->alterDeprecated($description, $hooks, $this->results); // Store the results in the static cache. @@ -799,7 +799,7 @@ public function getCacheMaxAge() { } /** - * {@inheritdoc} + * Implements the magic __clone() method to properly clone nested objects. */ public function __clone() { $this->results = $this->getResults()->getCloneForQuery($this); diff --git a/web/modules/search_api/src/SearchApiPluginManager.php b/web/modules/search_api/src/SearchApiPluginManager.php index cb33f134cc18fcbaa841b273b0f212d898ba2051..aa767a7bd4f893c41de8f0d6e189330a910727f5 100644 --- a/web/modules/search_api/src/SearchApiPluginManager.php +++ b/web/modules/search_api/src/SearchApiPluginManager.php @@ -71,7 +71,7 @@ protected function alterDefinitions(&$definitions) { $this->moduleHandler->alter($this->alterHook, $definitions); return; } - $description = "This hook is deprecated in search_api 8.x-1.14 and will be removed in 9.x-1.0. Please use the \"{$this->alterEventName}\" event instead. See https://www.drupal.org/node/3059866"; + $description = "This hook is deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. Please use the \"{$this->alterEventName}\" event instead. See https://www.drupal.org/node/3059866"; $this->moduleHandler->alterDeprecated($description, $this->alterHook, $definitions); } diff --git a/web/modules/search_api/src/Utility/CommandHelper.php b/web/modules/search_api/src/Utility/CommandHelper.php index dd7a7a86c27e6dbf8d4eedf992cae50da154a79f..2e443df4fb36f4b616f35b93eba17e6b74c3ef94 100644 --- a/web/modules/search_api/src/Utility/CommandHelper.php +++ b/web/modules/search_api/src/Utility/CommandHelper.php @@ -17,6 +17,8 @@ use Psr\Log\LoggerAwareTrait; use Symfony\Component\EventDispatcher\EventDispatcherInterface; +// phpcs:disable DrupalPractice.General.ExceptionT.ExceptionT + /** * Provides functionality to be used by CLI tools. */ @@ -373,7 +375,7 @@ public function resetTrackerCommand(array $indexIds = NULL, array $entityTypes = $reindexed_datasources[] = $datasource->label(); } } - $description = 'This hook is deprecated in search_api 8.x-1.14 and will be removed in 9.x-1.0. Please use the "search_api.reindex_scheduled" event instead. See https://www.drupal.org/node/3059866'; + $description = 'This hook is deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. Please use the "search_api.reindex_scheduled" event instead. See https://www.drupal.org/node/3059866'; $this->moduleHandler->invokeAllDeprecated($description, 'search_api_index_reindex', [$index, FALSE]); $event_name = SearchApiEvents::REINDEX_SCHEDULED; $event = new ReindexScheduledEvent($index, FALSE); diff --git a/web/modules/search_api/src/Utility/DataTypeHelper.php b/web/modules/search_api/src/Utility/DataTypeHelper.php index 970892e7ecbd1262c56b543430525c249136c809..b6e7a0446501e95c28790390027060d4eedd241e 100644 --- a/web/modules/search_api/src/Utility/DataTypeHelper.php +++ b/web/modules/search_api/src/Utility/DataTypeHelper.php @@ -138,7 +138,7 @@ public function getFieldTypeMapping() { // Allow other modules to intercept and define what default type they want // to use for their data type. - $description = 'This hook is deprecated in search_api 8.x-1.14 and will be removed in 9.x-1.0. Please use the "search_api.mapping_field_types" event instead. See https://www.drupal.org/node/3059866'; + $description = 'This hook is deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. Please use the "search_api.mapping_field_types" event instead. See https://www.drupal.org/node/3059866'; $hook = 'search_api_field_type_mapping'; $this->moduleHandler->alterDeprecated($description, $hook, $mapping); $eventName = SearchApiEvents::MAPPING_FIELD_TYPES; diff --git a/web/modules/search_api/src/Utility/FieldsHelper.php b/web/modules/search_api/src/Utility/FieldsHelper.php index bbf6bf3ff6a0461add3d6e34c604269c644424d6..ac02381117fe41b55bca12d74a7f7dd20f112fe2 100644 --- a/web/modules/search_api/src/Utility/FieldsHelper.php +++ b/web/modules/search_api/src/Utility/FieldsHelper.php @@ -199,7 +199,7 @@ public function extractFieldValues(TypedDataInterface $data) { // Process complex data types. if ($definition instanceof ComplexDataDefinitionInterface) { $main_property_name = $definition->getMainPropertyName(); - $data_properties = $data->getProperties(); + $data_properties = $data->getProperties(TRUE); if (isset($data_properties[$main_property_name])) { return $this->extractFieldValues($data_properties[$main_property_name]); } diff --git a/web/modules/search_api/src/Utility/TrackingHelper.php b/web/modules/search_api/src/Utility/TrackingHelper.php new file mode 100644 index 0000000000000000000000000000000000000000..9d9ac87c4a69cd60676c3e958b33c8f5ae4402cb --- /dev/null +++ b/web/modules/search_api/src/Utility/TrackingHelper.php @@ -0,0 +1,281 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\search_api\Utility; + +use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException; +use Drupal\Component\Plugin\Exception\PluginNotFoundException; +use Drupal\Core\Cache\CacheableMetadata; +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Cache\RefinableCacheableDependencyInterface; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Entity\TypedData\EntityDataDefinitionInterface; +use Drupal\Core\Field\TypedData\FieldItemDataDefinitionInterface; +use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\Core\TypedData\ComplexDataDefinitionInterface; +use Drupal\Core\TypedData\DataDefinitionInterface; +use Drupal\search_api\Event\MappingForeignRelationshipsEvent; +use Drupal\search_api\Event\SearchApiEvents; +use Drupal\search_api\IndexInterface; +use Drupal\search_api\SearchApiException; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; + +/** + * Provides datasource-independent item change tracking functionality. + */ +class TrackingHelper implements TrackingHelperInterface { + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The language manager. + * + * @var \Drupal\Core\Language\LanguageManagerInterface + */ + protected $languageManager; + + /** + * The event dispatcher. + * + * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface + */ + protected $eventDispatcher; + + /** + * The fields helper. + * + * @var \Drupal\search_api\Utility\FieldsHelperInterface + */ + protected $fieldsHelper; + + /** + * The cache backend. + * + * @var \Drupal\Core\Cache\CacheBackendInterface + */ + protected $cache; + + /** + * Constructs a new class instance. + * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager + * The entity type manager. + * @param \Drupal\Core\Language\LanguageManagerInterface $languageManager + * The language manager. + * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher + * The event dispatcher. + * @param \Drupal\search_api\Utility\FieldsHelperInterface $fieldsHelper + * The fields helper. + * @param \Drupal\Core\Cache\CacheBackendInterface $cache + * The cache backend. + */ + public function __construct(EntityTypeManagerInterface $entityTypeManager, LanguageManagerInterface $languageManager, EventDispatcherInterface $eventDispatcher, FieldsHelperInterface $fieldsHelper, CacheBackendInterface $cache) { + $this->languageManager = $languageManager; + $this->entityTypeManager = $entityTypeManager; + $this->eventDispatcher = $eventDispatcher; + $this->fieldsHelper = $fieldsHelper; + $this->cache = $cache; + } + + /** + * {@inheritdoc} + */ + public function trackReferencedEntityUpdate(EntityInterface $entity, bool $deleted = FALSE) { + /** @var \Drupal\search_api\IndexInterface[] $indexes */ + $indexes = []; + try { + $indexes = $this->entityTypeManager->getStorage('search_api_index') + ->loadMultiple(); + } + // @todo Replace with multi-catch once we depend on PHP 7.1+. + catch (InvalidPluginDefinitionException $e) { + // Can't really happen, but play it safe to appease static code analysis. + } + catch (PluginNotFoundException $e) { + // Can't really happen, but play it safe to appease static code analysis. + } + + // Map of foreign entity relations. Will get lazily populated as soon as we + // actually need it. + $original = $deleted ? NULL : $entity->original ?? NULL; + foreach ($indexes as $index) { + $map = NULL; + foreach ($index->getDatasources() as $datasource_id => $datasource) { + if (!$datasource->canContainEntityReferences()) { + continue; + } + + if ($map === NULL) { + $map = $this->getForeignEntityRelationsMap($index); + // If there are no foreign entities in the index, no need to continue. + if (!$map) { + break 1; + } + } + + $item_ids = $datasource->getAffectedItemsForEntityChange($entity, $map, $original); + if (!empty($item_ids)) { + $index->trackItemsUpdated($datasource_id, $item_ids); + } + } + } + } + + /** + * Analyzes the index fields and constructs a map of entity references. + * + * This map tries to record all ways that entities' values are indirectly + * indexed by the given index. (That is, what items' indexed contents might be + * affected by a given entity being updated or deleted.) + * + * @param \Drupal\search_api\IndexInterface $index + * The index for which to create the map. + * + * @return array[] + * A (numerically keyed) array of foreign relationship mappings. Each + * sub-array represents a single known relationship. Such sub-arrays will + * have the following structure: + * - entity_type: (string) The entity type that is referenced from the + * index. + * - bundles: (string[]) An optional array of particular entity bundles that + * are referred to from the index. An empty array here means that the + * index refers to all the bundles. + * - property_path_to_foreign_entity: (string) Property path where the index + * refers to this entity. + * - field_name: (string) Name of the field on the referenced entity that is + * indexed in the search index. + */ + protected function getForeignEntityRelationsMap(IndexInterface $index): array { + $cid = "search_api:{$index->id()}:foreign_entities_relations_map"; + + $cache = $this->cache->get($cid); + if ($cache) { + return $cache->data; + } + + $cacheability = new CacheableMetadata(); + $cacheability->addCacheableDependency($index); + $data = []; + foreach ($index->getFields() as $field) { + try { + $datasource = $field->getDatasource(); + } + catch (SearchApiException $e) { + continue; + } + if (!$datasource) { + continue; + } + + $relation_info = [ + 'entity_type' => NULL, + 'bundles' => NULL, + 'property_path_to_foreign_entity' => NULL, + ]; + $seen_path_chunks = []; + $property_definitions = $datasource->getPropertyDefinitions(); + $field_property = Utility::splitPropertyPath($field->getPropertyPath(), FALSE); + for (; $field_property[0]; $field_property = Utility::splitPropertyPath($field_property[1], FALSE)) { + $property_definition = $this->fieldsHelper->retrieveNestedProperty($property_definitions, $field_property[0]); + if (!$property_definition) { + // Seems like we could not map it from the property path to some Typed + // Data definition. In the absence of a better alternative, let's + // simply disregard this field. + break; + } + + $seen_path_chunks[] = $field_property[0]; + + if ($property_definition instanceof FieldItemDataDefinitionInterface + && $property_definition->getFieldDefinition()->isComputed()) { + // We cannot really deal with computed fields since we have no + // knowledge about their internal logic. Thus we cannot process + // this field any further. + break; + } + + if ($relation_info['entity_type'] && $property_definition instanceof FieldItemDataDefinitionInterface) { + // Parent is an entity. Hence this level is fields of the entity. + $cacheability->addCacheableDependency($property_definition->getFieldDefinition()); + + $data[] = $relation_info + [ + 'field_name' => $property_definition->getFieldDefinition() + ->getName(), + ]; + } + + $entity_reference = $this->isEntityReferenceDataDefinition($property_definition, $cacheability); + if ($entity_reference + && $relation_info['entity_type'] !== $entity_reference['entity_type']) { + $relation_info = $entity_reference; + $relation_info['property_path_to_foreign_entity'] = implode(IndexInterface::PROPERTY_PATH_SEPARATOR, $seen_path_chunks); + } + + if ($property_definition instanceof ComplexDataDefinitionInterface) { + $property_definitions = $this->fieldsHelper->getNestedProperties($property_definition); + } + else { + // This item no longer has "nested" properties in its Typed Data + // definition. Thus we cannot examine it any further than the current + // point. + break; + } + } + } + + // Let other modules alter this information, potentially adding more + // relationships. + $event = new MappingForeignRelationshipsEvent($index, $data, $cacheability); + $this->eventDispatcher->dispatch(SearchApiEvents::MAPPING_FOREIGN_RELATIONSHIPS, $event); + + $this->cache->set($cid, $data, $cacheability->getCacheMaxAge(), $cacheability->getCacheTags()); + + return $data; + } + + /** + * Determines whether the given property is a reference to an entity. + * + * @param \Drupal\Core\TypedData\DataDefinitionInterface $property_definition + * The property to test. + * @param \Drupal\Core\Cache\RefinableCacheableDependencyInterface $cacheability + * A cache metadata object to track any caching information necessary in + * this method call. + * + * @return array + * This method will return an empty array if $property is not an entity + * reference. Otherwise it will return an associative array with the + * following structure: + * - entity_type: (string) The entity type to which $property refers. + * - bundles: (array) A list of bundles to which $property refers. In case + * specific bundles cannot be determined or the $property points to all + * the bundles, this key will contain an empty array. + */ + protected function isEntityReferenceDataDefinition(DataDefinitionInterface $property_definition, RefinableCacheableDependencyInterface $cacheability): array { + $return = []; + + if ($property_definition instanceof FieldItemDataDefinitionInterface + && $property_definition->getFieldDefinition()->getType() === 'entity_reference') { + $field = $property_definition->getFieldDefinition(); + $cacheability->addCacheableDependency($field); + + $return['entity_type'] = $field->getSetting('target_type'); + $field_settings = $field->getSetting('handler_settings'); + $return['bundles'] = $field_settings['target_bundles'] ?? []; + } + elseif ($property_definition instanceof EntityDataDefinitionInterface) { + $return['entity_type'] = $property_definition->getEntityTypeId(); + $return['bundles'] = $property_definition->getBundles() ?: []; + } + + return $return; + } + +} diff --git a/web/modules/search_api/src/Utility/TrackingHelperInterface.php b/web/modules/search_api/src/Utility/TrackingHelperInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..a7df0cd4560d2b3d3bba2d1eec29ee727ecb7486 --- /dev/null +++ b/web/modules/search_api/src/Utility/TrackingHelperInterface.php @@ -0,0 +1,29 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\search_api\Utility; + +use Drupal\Core\Entity\EntityInterface; + +/** + * Provides an interface for the tracking helper service. + */ +interface TrackingHelperInterface { + + /** + * Reacts to an entity being updated or deleted. + * + * Determines whether this entity is indirectly referenced in any search index + * and, if so, marks all items referencing it as updated. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity that just got changed (updated or deleted). + * @param bool $deleted + * (optional) TRUE if the entity was deleted, FALSE if it was updated. + * + * @see \Drupal\search_api\Datasource\DatasourceInterface::getAffectedItemsForEntityChange() + */ + public function trackReferencedEntityUpdate(EntityInterface $entity, bool $deleted = FALSE); + +} diff --git a/web/modules/search_api/src/Utility/Utility.php b/web/modules/search_api/src/Utility/Utility.php index 5049a422a9f942d3d203f3a6bbcadd9893088121..c6d6707340c66c74e107d0c694c0a4e367394ef6 100644 --- a/web/modules/search_api/src/Utility/Utility.php +++ b/web/modules/search_api/src/Utility/Utility.php @@ -137,7 +137,7 @@ public static function splitCombinedId($combined_id) { * always contain a single property name (without any colons) and index 1 * might be NULL. If $separate_last is TRUE it's the exact other way round. */ - public static function splitPropertyPath($property_path, $separate_last = TRUE, $separator = ':') { + public static function splitPropertyPath($property_path, $separate_last = TRUE, $separator = IndexInterface::PROPERTY_PATH_SEPARATOR) { $function = $separate_last ? 'strrpos' : 'strpos'; $pos = $function($property_path, $separator); if ($pos !== FALSE) { diff --git a/web/modules/search_api/tests/search_api_test/search_api_test.info.yml b/web/modules/search_api/tests/search_api_test/search_api_test.info.yml index d51b921f2586b14dc40785e27e0b9c93ef00b20e..08921e779d5e89065f05a77affd86cff43ffcf43 100644 --- a/web/modules/search_api/tests/search_api_test/search_api_test.info.yml +++ b/web/modules/search_api/tests/search_api_test/search_api_test.info.yml @@ -7,7 +7,7 @@ dependencies: core_version_requirement: ^8.8 || ^9 hidden: true -# Information added by Drupal.org packaging script on 2020-06-02 -version: '8.x-1.17' +# Information added by Drupal.org packaging script on 2020-10-22 +version: '8.x-1.18' project: 'search_api' -datestamp: 1591128372 +datestamp: 1603359377 diff --git a/web/modules/search_api/tests/search_api_test/search_api_test.module b/web/modules/search_api/tests/search_api_test/search_api_test.module index eb14868a0737f166382a64fb00d00a9c7a62b6f2..36a8e5a0265c2b79c4bb47427d4344aa086604ee 100644 --- a/web/modules/search_api/tests/search_api_test/search_api_test.module +++ b/web/modules/search_api/tests/search_api_test/search_api_test.module @@ -40,3 +40,15 @@ function search_api_test_node_access_records(NodeInterface $node) { return $grants; } + +/** + * Implements hook_ENTITY_TYPE_load() for "search_api_server". + */ +function search_api_test_search_api_server_load($servers) { + /** @var \Drupal\search_api\ServerInterface $server */ + foreach ($servers as $server) { + if ($server->hasValidBackend()) { + $server->getBackend(); + } + } +} diff --git a/web/modules/search_api/tests/search_api_test_bulk_form/search_api_test_bulk_form.info.yml b/web/modules/search_api/tests/search_api_test_bulk_form/search_api_test_bulk_form.info.yml index eef5873df23c2003b4b666cb98adc01161a5de17..cdc5011309df62dba5e8644af80308d380189db6 100644 --- a/web/modules/search_api/tests/search_api_test_bulk_form/search_api_test_bulk_form.info.yml +++ b/web/modules/search_api/tests/search_api_test_bulk_form/search_api_test_bulk_form.info.yml @@ -10,7 +10,7 @@ dependencies: core_version_requirement: ^8 || ^9 hidden: true -# Information added by Drupal.org packaging script on 2020-06-02 -version: '8.x-1.17' +# Information added by Drupal.org packaging script on 2020-10-22 +version: '8.x-1.18' project: 'search_api' -datestamp: 1591128372 +datestamp: 1603359377 diff --git a/web/modules/search_api/tests/search_api_test_db/search_api_test_db.info.yml b/web/modules/search_api/tests/search_api_test_db/search_api_test_db.info.yml index 46339cc913e5c124af5f0f4076defa912e93d990..ad96200c3bc0335e80d9eb313c765e241fdc2f29 100644 --- a/web/modules/search_api/tests/search_api_test_db/search_api_test_db.info.yml +++ b/web/modules/search_api/tests/search_api_test_db/search_api_test_db.info.yml @@ -8,7 +8,7 @@ dependencies: core_version_requirement: ^8.8 || ^9 hidden: true -# Information added by Drupal.org packaging script on 2020-06-02 -version: '8.x-1.17' +# Information added by Drupal.org packaging script on 2020-10-22 +version: '8.x-1.18' project: 'search_api' -datestamp: 1591128372 +datestamp: 1603359377 diff --git a/web/modules/search_api/tests/search_api_test_events/search_api_test_events.info.yml b/web/modules/search_api/tests/search_api_test_events/search_api_test_events.info.yml index 1574451073e06d0131ac0aaee7a0ab9cd8059a7d..2528bdc72a47f341c1ea521bd6ee11c9e2ddfad0 100644 --- a/web/modules/search_api/tests/search_api_test_events/search_api_test_events.info.yml +++ b/web/modules/search_api/tests/search_api_test_events/search_api_test_events.info.yml @@ -7,7 +7,7 @@ dependencies: core_version_requirement: ^8.8 || ^9 hidden: true -# Information added by Drupal.org packaging script on 2020-06-02 -version: '8.x-1.17' +# Information added by Drupal.org packaging script on 2020-10-22 +version: '8.x-1.18' project: 'search_api' -datestamp: 1591128372 +datestamp: 1603359377 diff --git a/web/modules/search_api/tests/search_api_test_example_content/search_api_test_example_content.info.yml b/web/modules/search_api/tests/search_api_test_example_content/search_api_test_example_content.info.yml index de6648e234ce00b5c0e0742ed23d576175e43ec0..4b5356a2283901d9839fefe6ee0c4ba7d0ab8c1c 100644 --- a/web/modules/search_api/tests/search_api_test_example_content/search_api_test_example_content.info.yml +++ b/web/modules/search_api/tests/search_api_test_example_content/search_api_test_example_content.info.yml @@ -7,7 +7,7 @@ dependencies: core_version_requirement: ^8.8 || ^9 hidden: true -# Information added by Drupal.org packaging script on 2020-06-02 -version: '8.x-1.17' +# Information added by Drupal.org packaging script on 2020-10-22 +version: '8.x-1.18' project: 'search_api' -datestamp: 1591128372 +datestamp: 1603359377 diff --git a/web/modules/search_api/tests/search_api_test_example_content_references/config/install/field.field.node.child.indexed.yml b/web/modules/search_api/tests/search_api_test_example_content_references/config/install/field.field.node.child.indexed.yml new file mode 100644 index 0000000000000000000000000000000000000000..a1b7be19b2213d08eb600e7b2b74d64cd6d82e9e --- /dev/null +++ b/web/modules/search_api/tests/search_api_test_example_content_references/config/install/field.field.node.child.indexed.yml @@ -0,0 +1,20 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.node.indexed + - node.type.child + module: + - node +id: node.child.indexed +field_name: indexed +entity_type: node +bundle: child +label: 'Indexed' +description: 'This field is supposed to be indexed by Search API.' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: { } +field_type: string diff --git a/web/modules/search_api/tests/search_api_test_example_content_references/config/install/field.field.node.child.not_indexed.yml b/web/modules/search_api/tests/search_api_test_example_content_references/config/install/field.field.node.child.not_indexed.yml new file mode 100644 index 0000000000000000000000000000000000000000..010c0d0e6c0be436494c63c41bf0170c8ff605ea --- /dev/null +++ b/web/modules/search_api/tests/search_api_test_example_content_references/config/install/field.field.node.child.not_indexed.yml @@ -0,0 +1,20 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.node.not_indexed + - node.type.child + module: + - node +id: node.child.not_indexed +field_name: not_indexed +entity_type: node +bundle: child +label: 'Not Indexed' +description: 'This field is not supposed to be indexed by Search API.' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: { } +field_type: string diff --git a/web/modules/search_api/tests/search_api_test_example_content_references/config/install/field.field.node.parent.entity_reference.yml b/web/modules/search_api/tests/search_api_test_example_content_references/config/install/field.field.node.parent.entity_reference.yml new file mode 100644 index 0000000000000000000000000000000000000000..2937cc0b9059cad2d0eb4d25e698e2f379c80bb5 --- /dev/null +++ b/web/modules/search_api/tests/search_api_test_example_content_references/config/install/field.field.node.parent.entity_reference.yml @@ -0,0 +1,25 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.node.entity_reference + - node.type.parent + - node.type.child +id: node.parent.entity_reference +field_name: entity_reference +entity_type: node +bundle: parent +label: Reference +description: 'Reference to the child node type.' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: + handler: 'default:node' + handler_settings: + target_bundles: + child: child + auto_create: false + auto_create_bundle: '' +field_type: entity_reference diff --git a/web/modules/search_api/tests/search_api_test_example_content_references/config/install/field.storage.node.entity_reference.yml b/web/modules/search_api/tests/search_api_test_example_content_references/config/install/field.storage.node.entity_reference.yml new file mode 100644 index 0000000000000000000000000000000000000000..588fb38b08689e285e3ddf768977f898c4646bb0 --- /dev/null +++ b/web/modules/search_api/tests/search_api_test_example_content_references/config/install/field.storage.node.entity_reference.yml @@ -0,0 +1,18 @@ +langcode: en +status: true +dependencies: + module: + - node +id: node.entity_reference +field_name: entity_reference +entity_type: node +type: entity_reference +settings: + target_type: node +module: core +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/web/modules/search_api/tests/search_api_test_example_content_references/config/install/field.storage.node.indexed.yml b/web/modules/search_api/tests/search_api_test_example_content_references/config/install/field.storage.node.indexed.yml new file mode 100644 index 0000000000000000000000000000000000000000..c0de2fef9f88fb7afe67ebaba2f008b00781f662 --- /dev/null +++ b/web/modules/search_api/tests/search_api_test_example_content_references/config/install/field.storage.node.indexed.yml @@ -0,0 +1,20 @@ +langcode: en +status: true +dependencies: + module: + - node +id: node.indexed +field_name: indexed +entity_type: node +type: string +settings: + max_length: 255 + is_ascii: false + case_sensitive: false +module: core +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/web/modules/search_api/tests/search_api_test_example_content_references/config/install/field.storage.node.not_indexed.yml b/web/modules/search_api/tests/search_api_test_example_content_references/config/install/field.storage.node.not_indexed.yml new file mode 100644 index 0000000000000000000000000000000000000000..af874d9f22bea771b38b267aa83ffa83ea390fa7 --- /dev/null +++ b/web/modules/search_api/tests/search_api_test_example_content_references/config/install/field.storage.node.not_indexed.yml @@ -0,0 +1,20 @@ +langcode: en +status: true +dependencies: + module: + - node +id: node.not_indexed +field_name: not_indexed +entity_type: node +type: string +settings: + max_length: 255 + is_ascii: false + case_sensitive: false +module: core +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/web/modules/search_api/tests/search_api_test_example_content_references/config/install/node.type.child.yml b/web/modules/search_api/tests/search_api_test_example_content_references/config/install/node.type.child.yml new file mode 100644 index 0000000000000000000000000000000000000000..5b921f7364f0627036dfe79b65a4a8e29470b402 --- /dev/null +++ b/web/modules/search_api/tests/search_api_test_example_content_references/config/install/node.type.child.yml @@ -0,0 +1,10 @@ +langcode: en +status: true +dependencies: { } +name: Child +type: child +description: 'Child node type that gets referenced by other types.' +help: '' +new_revision: true +preview_mode: 1 +display_submitted: true diff --git a/web/modules/search_api/tests/search_api_test_example_content_references/config/install/node.type.parent.yml b/web/modules/search_api/tests/search_api_test_example_content_references/config/install/node.type.parent.yml new file mode 100644 index 0000000000000000000000000000000000000000..4e6430f2e6d0a408ab45200764582befc63f17f3 --- /dev/null +++ b/web/modules/search_api/tests/search_api_test_example_content_references/config/install/node.type.parent.yml @@ -0,0 +1,10 @@ +langcode: en +status: true +dependencies: { } +name: Parent +type: parent +description: 'Parent node type that references other types.' +help: '' +new_revision: true +preview_mode: 1 +display_submitted: true diff --git a/web/modules/search_api/tests/search_api_test_example_content_references/search_api_test_example_content_references.info.yml b/web/modules/search_api/tests/search_api_test_example_content_references/search_api_test_example_content_references.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..e87cf643630158a53b653e8541578c7713f22cf4 --- /dev/null +++ b/web/modules/search_api/tests/search_api_test_example_content_references/search_api_test_example_content_references.info.yml @@ -0,0 +1,13 @@ +type: module +name: 'Example Content with references' +description: 'Provides field definitions for example content that include entity references.' +package: Testing +dependencies: + - drupal:node +core_version_requirement: ^8 || ^9 +hidden: true + +# Information added by Drupal.org packaging script on 2020-10-22 +version: '8.x-1.18' +project: 'search_api' +datestamp: 1603359377 diff --git a/web/modules/search_api/tests/search_api_test_excerpt_field/search_api_test_excerpt_field.info.yml b/web/modules/search_api/tests/search_api_test_excerpt_field/search_api_test_excerpt_field.info.yml index f764be9700c65e919ea20b8a3aa24c99944bd744..5a079c3ddee373ef38108b57014153d7212c721d 100644 --- a/web/modules/search_api/tests/search_api_test_excerpt_field/search_api_test_excerpt_field.info.yml +++ b/web/modules/search_api/tests/search_api_test_excerpt_field/search_api_test_excerpt_field.info.yml @@ -9,7 +9,7 @@ dependencies: core_version_requirement: ^8.8 || ^9 hidden: true -# Information added by Drupal.org packaging script on 2020-06-02 -version: '8.x-1.17' +# Information added by Drupal.org packaging script on 2020-10-22 +version: '8.x-1.18' project: 'search_api' -datestamp: 1591128372 +datestamp: 1603359377 diff --git a/web/modules/search_api/tests/search_api_test_extraction/search_api_test_extraction.info.yml b/web/modules/search_api/tests/search_api_test_extraction/search_api_test_extraction.info.yml index 192e71c3892ad7225046fea5ae6fbb6262ac0972..e6386b6354fb851e3984f0c6a4a745911c3c7c34 100644 --- a/web/modules/search_api/tests/search_api_test_extraction/search_api_test_extraction.info.yml +++ b/web/modules/search_api/tests/search_api_test_extraction/search_api_test_extraction.info.yml @@ -7,7 +7,7 @@ dependencies: core_version_requirement: ^8.8 || ^9 hidden: true -# Information added by Drupal.org packaging script on 2020-06-02 -version: '8.x-1.17' +# Information added by Drupal.org packaging script on 2020-10-22 +version: '8.x-1.18' project: 'search_api' -datestamp: 1591128372 +datestamp: 1603359377 diff --git a/web/modules/search_api/tests/search_api_test_hooks/search_api_test_hooks.info.yml b/web/modules/search_api/tests/search_api_test_hooks/search_api_test_hooks.info.yml index 616b124ea856c19172d49181011649bf02cf7dbd..cb9eb68b4e43bc42d67f46316414a5ad221f62fd 100644 --- a/web/modules/search_api/tests/search_api_test_hooks/search_api_test_hooks.info.yml +++ b/web/modules/search_api/tests/search_api_test_hooks/search_api_test_hooks.info.yml @@ -7,7 +7,7 @@ dependencies: core_version_requirement: ^8.8 || ^9 hidden: true -# Information added by Drupal.org packaging script on 2020-06-02 -version: '8.x-1.17' +# Information added by Drupal.org packaging script on 2020-10-22 +version: '8.x-1.18' project: 'search_api' -datestamp: 1591128372 +datestamp: 1603359377 diff --git a/web/modules/search_api/tests/search_api_test_inconsistent_config/search_api_test_inconsistent_config.info.yml b/web/modules/search_api/tests/search_api_test_inconsistent_config/search_api_test_inconsistent_config.info.yml index 6c9dfec2b238aca5cf5c4547d5ac4d17e3cebf02..3fe52b63a7ae81ab26b182c2543ce4d5524b5f58 100644 --- a/web/modules/search_api/tests/search_api_test_inconsistent_config/search_api_test_inconsistent_config.info.yml +++ b/web/modules/search_api/tests/search_api_test_inconsistent_config/search_api_test_inconsistent_config.info.yml @@ -8,7 +8,7 @@ dependencies: core_version_requirement: ^8.8 || ^9 hidden: true -# Information added by Drupal.org packaging script on 2020-06-02 -version: '8.x-1.17' +# Information added by Drupal.org packaging script on 2020-10-22 +version: '8.x-1.18' project: 'search_api' -datestamp: 1591128372 +datestamp: 1603359377 diff --git a/web/modules/search_api/tests/search_api_test_language_fallback/search_api_test_language_fallback.info.yml b/web/modules/search_api/tests/search_api_test_language_fallback/search_api_test_language_fallback.info.yml index 42c3737872ca9671858cebebcf4d0582203b3ef5..0daaefa1383b44f1a3d1993358931137067c6108 100644 --- a/web/modules/search_api/tests/search_api_test_language_fallback/search_api_test_language_fallback.info.yml +++ b/web/modules/search_api/tests/search_api_test_language_fallback/search_api_test_language_fallback.info.yml @@ -5,7 +5,7 @@ package: 'Search API' core_version_requirement: ^8.8 || ^9 hidden: true -# Information added by Drupal.org packaging script on 2020-06-02 -version: '8.x-1.17' +# Information added by Drupal.org packaging script on 2020-10-22 +version: '8.x-1.18' project: 'search_api' -datestamp: 1591128372 +datestamp: 1603359377 diff --git a/web/modules/search_api/tests/search_api_test_no_ui/search_api_test_no_ui.info.yml b/web/modules/search_api/tests/search_api_test_no_ui/search_api_test_no_ui.info.yml index 1daedd4ea7bbe5cf3b7c589114c0744995d037b6..26efc29333b9ef9c759192a63604018a3a201302 100644 --- a/web/modules/search_api/tests/search_api_test_no_ui/search_api_test_no_ui.info.yml +++ b/web/modules/search_api/tests/search_api_test_no_ui/search_api_test_no_ui.info.yml @@ -7,7 +7,7 @@ dependencies: core_version_requirement: ^8.8 || ^9 hidden: true -# Information added by Drupal.org packaging script on 2020-06-02 -version: '8.x-1.17' +# Information added by Drupal.org packaging script on 2020-10-22 +version: '8.x-1.18' project: 'search_api' -datestamp: 1591128372 +datestamp: 1603359377 diff --git a/web/modules/search_api/tests/search_api_test_node_indexing/search_api_test_node_indexing.info.yml b/web/modules/search_api/tests/search_api_test_node_indexing/search_api_test_node_indexing.info.yml index 87bbbed1db46b0fd0acd9e5720014acb02b7ad4a..c26492cbec40f66efca433663f8d7f01f93b6c5b 100644 --- a/web/modules/search_api/tests/search_api_test_node_indexing/search_api_test_node_indexing.info.yml +++ b/web/modules/search_api/tests/search_api_test_node_indexing/search_api_test_node_indexing.info.yml @@ -7,7 +7,7 @@ dependencies: core_version_requirement: ^8.8 || ^9 hidden: true -# Information added by Drupal.org packaging script on 2020-06-02 -version: '8.x-1.17' +# Information added by Drupal.org packaging script on 2020-10-22 +version: '8.x-1.18' project: 'search_api' -datestamp: 1591128372 +datestamp: 1603359377 diff --git a/web/modules/search_api/tests/search_api_test_tasks/search_api_test_tasks.info.yml b/web/modules/search_api/tests/search_api_test_tasks/search_api_test_tasks.info.yml index a906f2a68bec0472e6f58b2ef311dc6c628febd6..49d04312cefa41c16ab91449b0b524229757d46d 100644 --- a/web/modules/search_api/tests/search_api_test_tasks/search_api_test_tasks.info.yml +++ b/web/modules/search_api/tests/search_api_test_tasks/search_api_test_tasks.info.yml @@ -7,7 +7,7 @@ dependencies: core_version_requirement: ^8.8 || ^9 hidden: true -# Information added by Drupal.org packaging script on 2020-06-02 -version: '8.x-1.17' +# Information added by Drupal.org packaging script on 2020-10-22 +version: '8.x-1.18' project: 'search_api' -datestamp: 1591128372 +datestamp: 1603359377 diff --git a/web/modules/search_api/tests/search_api_test_views/search_api_test_views.info.yml b/web/modules/search_api/tests/search_api_test_views/search_api_test_views.info.yml index 5eb085391cc127fb94f45cd02ddf56a1655ebb7b..8f20a4018975f1dd21f2020aca7ce3e29a961e2e 100644 --- a/web/modules/search_api/tests/search_api_test_views/search_api_test_views.info.yml +++ b/web/modules/search_api/tests/search_api_test_views/search_api_test_views.info.yml @@ -11,7 +11,7 @@ dependencies: core_version_requirement: ^8.8 || ^9 hidden: true -# Information added by Drupal.org packaging script on 2020-06-02 -version: '8.x-1.17' +# Information added by Drupal.org packaging script on 2020-10-22 +version: '8.x-1.18' project: 'search_api' -datestamp: 1591128372 +datestamp: 1603359377 diff --git a/web/modules/search_api/tests/src/Functional/IntegrationTest.php b/web/modules/search_api/tests/src/Functional/IntegrationTest.php index a6020f80733c11d0a7c76f938c72e0ec56952c5d..eb5a52fde9b3eba07981a26e2d806500869a167b 100644 --- a/web/modules/search_api/tests/src/Functional/IntegrationTest.php +++ b/web/modules/search_api/tests/src/Functional/IntegrationTest.php @@ -139,7 +139,7 @@ public function testIntegerIndex() { Server::create([ 'id' => 456, 'name' => 789, - 'description' => 'WebTest server' . ' description', + 'description' => 'WebTest server description', 'backend' => $this->serverBackend, 'backend_config' => [], ])->save(); @@ -1054,7 +1054,7 @@ protected function removeFieldsFromIndex() { $index = $this->getIndex(TRUE); $fields = $index->getFields(); - $this->assertTrue(!isset($fields['body']), 'The body field has been removed from the index.'); + $this->assertArrayNotHasKey('body', $fields); } /** @@ -1102,7 +1102,7 @@ protected function checkUnsavedChanges() { // Make sure the field has not been added to the index. $index = $this->getIndex(TRUE); $fields = $index->getFields(); - $this->assertTrue(!isset($fields['changed']), 'The changed field has not been added to the index.'); + $this->assertArrayNotHasKey('changed', $fields); // Find the "Remove" link for the "title" field. $links = $this->xpath('//a[@data-drupal-selector=:id]', [':id' => 'edit-fields-title-remove']); @@ -1449,7 +1449,8 @@ protected function checkIndexing() { // Ensure all items need to be indexed. $this->getIndex()->reindex(); - $this->drupalPostForm($this->getIndexPath(), [], 'Index now'); + $this->drupalGet($this->getIndexPath()); + $this->submitForm([], 'Index now'); $this->assertSession()->statusCodeEquals(200); $this->checkForMetaRefresh(); $count = \Drupal::entityQuery('node')->count()->execute() - 1; @@ -1458,7 +1459,8 @@ protected function checkIndexing() { $this->assertSession()->pageTextNotContains("Couldn't index items."); $this->assertSession()->pageTextNotContains('An error occurred'); - $this->drupalPostForm($this->getIndexPath(), [], 'Index now'); + $this->drupalGet($this->getIndexPath()); + $this->submitForm([], 'Index now'); $this->assertSession()->statusCodeEquals(200); $this->checkForMetaRefresh(); $this->assertSession()->pageTextContains("Couldn't index items."); @@ -1466,14 +1468,16 @@ protected function checkIndexing() { \Drupal::state()->set($key, []); $this->setError('backend', 'indexItems'); - $this->drupalPostForm($this->getIndexPath(), [], 'Index now'); + $this->drupalGet($this->getIndexPath()); + $this->submitForm([], 'Index now'); $this->assertSession()->statusCodeEquals(200); $this->checkForMetaRefresh(); $this->assertSession()->pageTextContains("Couldn't index items."); $this->assertSession()->pageTextNotContains('An error occurred'); $this->setError('backend', 'indexItems', FALSE); - $this->drupalPostForm($this->getIndexPath(), [], 'Index now'); + $this->drupalGet($this->getIndexPath()); + $this->submitForm([], 'Index now'); $this->assertSession()->statusCodeEquals(200); $this->checkForMetaRefresh(); $this->assertSession()->pageTextContains("Successfully indexed 1 item."); @@ -1505,21 +1509,24 @@ protected function checkIndexActions() { $this->assertEquals($manipulated_items_count, $tracker->getTotalItemsCount()); $this->assertEquals($manipulated_items_count + 1, $this->countItemsOnServer()); - $this->drupalPostForm($this->getIndexPath('reindex'), [], 'Confirm'); + $this->drupalGet($this->getIndexPath('reindex')); + $this->submitForm([], 'Confirm'); $assert_session->pageTextContains("The search index $label was successfully queued for reindexing."); $this->assertEquals(0, $tracker->getIndexedItemsCount()); $this->assertEquals($manipulated_items_count, $tracker->getTotalItemsCount()); $this->assertEquals($manipulated_items_count + 1, $this->countItemsOnServer()); $this->indexItems(); - $this->drupalPostForm($this->getIndexPath('clear'), [], 'Confirm'); + $this->drupalGet($this->getIndexPath('clear')); + $this->submitForm([], 'Confirm'); $assert_session->pageTextContains("All items were successfully deleted from search index $label."); $this->assertEquals(0, $tracker->getIndexedItemsCount()); $this->assertEquals($manipulated_items_count, $tracker->getTotalItemsCount()); $this->assertEquals(0, $this->countItemsOnServer()); $this->indexItems(); - $this->drupalPostForm($this->getIndexPath('rebuild-tracker'), [], 'Confirm'); + $this->drupalGet($this->getIndexPath('rebuild-tracker')); + $this->submitForm([], 'Confirm'); $assert_session->pageTextContains("The tracking information for search index $label will be rebuilt."); $this->assertEquals(0, $tracker->getIndexedItemsCount()); $this->assertEquals($manipulated_items_count + 1, $tracker->getTotalItemsCount()); diff --git a/web/modules/search_api/tests/src/Functional/ProcessorIntegrationTest.php b/web/modules/search_api/tests/src/Functional/ProcessorIntegrationTest.php index 265234a609af818494ec3bb80c72f50d680e9822..d68965753df1fe41f5a2e0da7177a9a6414cf66d 100644 --- a/web/modules/search_api/tests/src/Functional/ProcessorIntegrationTest.php +++ b/web/modules/search_api/tests/src/Functional/ProcessorIntegrationTest.php @@ -15,8 +15,6 @@ /** * Tests the admin UI for processors. * - * @todo Move this whole class into a single IntegrationTest check*() method? - * * @group search_api */ class ProcessorIntegrationTest extends SearchApiBrowserTestBase { @@ -102,7 +100,7 @@ public function testProcessorIntegration() { 'add_url', 'aggregated_field', 'language_with_fallback', - 'rendered_item' + 'rendered_item', ]; $actual_processors = array_keys($this->loadIndex()->getProcessors()); sort($actual_processors); @@ -269,7 +267,7 @@ public function testProcessorIntegration() { // After disabling some datasource, all related processors should be // disabled also. $this->drupalGet('admin/config/search/search-api/index/' . $this->indexId . '/edit'); - $this->drupalPostForm(NULL, ['datasources[entity:user]' => FALSE], 'Save'); + $this->submitForm(['datasources[entity:user]' => FALSE], 'Save'); $processors = $this->loadIndex()->getProcessors(); $this->assertArrayNotHasKey('role_filter', $processors); $this->drupalGet('admin/config/search/search-api/index/' . $this->indexId . '/processors'); diff --git a/web/modules/search_api/tests/src/Functional/SearchApiBrowserTestBase.php b/web/modules/search_api/tests/src/Functional/SearchApiBrowserTestBase.php index d30b93365e6d521a5bbd25687402dac9b757040a..48e300d11b097b3e3a473302caa2313b77d2a63b 100644 --- a/web/modules/search_api/tests/src/Functional/SearchApiBrowserTestBase.php +++ b/web/modules/search_api/tests/src/Functional/SearchApiBrowserTestBase.php @@ -148,7 +148,7 @@ public function getTestServer() { $server = Server::create([ 'id' => 'webtest_server', 'name' => 'WebTest server', - 'description' => 'WebTest server' . ' description', + 'description' => 'WebTest server description', 'backend' => 'search_api_test', 'backend_config' => [], ]); @@ -171,7 +171,7 @@ public function getTestIndex() { $index = Index::create([ 'id' => $this->indexId, 'name' => 'WebTest index', - 'description' => 'WebTest index' . ' description', + 'description' => 'WebTest index description', 'server' => 'webtest_server', 'datasource_settings' => [ 'entity:node' => [], diff --git a/web/modules/search_api/tests/src/Functional/ViewsTest.php b/web/modules/search_api/tests/src/Functional/ViewsTest.php index fac790af291414eb73da278e490f2629fa78ba50..f7f1bdb05a894ccceb8c62f88d1778e18b58aaca 100644 --- a/web/modules/search_api/tests/src/Functional/ViewsTest.php +++ b/web/modules/search_api/tests/src/Functional/ViewsTest.php @@ -433,7 +433,7 @@ protected function regressionTest2869121() { \Drupal::getContainer()->get('cache.page')->deleteAll(); \Drupal::getContainer()->get('cache.dynamic_page_cache')->deleteAll(); $this->submitForm([], 'Search'); - $this->assertSession()->addressEquals('search-api-test'); + $this->assertSession()->addressMatches('#^/search-api-test#'); $this->assertSession()->responseNotContains('Error message'); $this->assertSession()->pageTextNotContains('search results'); // Make sure the Views cache was used, none of the two page caches. diff --git a/web/modules/search_api/tests/src/Kernel/Datasource/ReferencedEntitiesReindexingTest.php b/web/modules/search_api/tests/src/Kernel/Datasource/ReferencedEntitiesReindexingTest.php new file mode 100644 index 0000000000000000000000000000000000000000..ead2c474612b4ac794ba9e1042e675fa54bcd686 --- /dev/null +++ b/web/modules/search_api/tests/src/Kernel/Datasource/ReferencedEntitiesReindexingTest.php @@ -0,0 +1,295 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\search_api\Kernel\Datasource; + +use Drupal\KernelTests\KernelTestBase; +use Drupal\node\Entity\Node; +use Drupal\search_api\Entity\Index; +use Drupal\search_api\Entity\Server; +use Drupal\search_api\Utility\Utility; + +/** + * Tests that changes in related entities are correctly tracked. + * + * @group search_api + */ +class ReferencedEntitiesReindexingTest extends KernelTestBase { + + /** + * {@inheritdoc} + */ + public static $modules = [ + 'user', + 'node', + 'field', + 'system', + 'search_api', + 'search_api_test', + 'search_api_test_example_content_references', + ]; + + /** + * The search index used for this test. + * + * @var \Drupal\search_api\IndexInterface + */ + protected $index; + + /** + * {@inheritdoc} + */ + public function setUp() { + parent::setUp(); + + $this->installSchema('search_api', ['search_api_item']); + $this->installSchema('node', ['node_access']); + $this->installEntitySchema('user'); + $this->installEntitySchema('node'); + $this->installEntitySchema('search_api_task'); + $this->installConfig([ + 'search_api', + 'search_api_test_example_content_references', + ]); + + // Do not use a batch for tracking the initial items after creating an + // index when running the tests via the GUI. Otherwise, it seems Drupal's + // Batch API gets confused and the test fails. + if (!Utility::isRunningInCli()) { + \Drupal::state()->set('search_api_use_tracking_batch', FALSE); + } + + Server::create([ + 'id' => 'server', + 'backend' => 'search_api_test', + ])->save(); + $this->index = Index::create([ + 'id' => 'index', + 'tracker_settings' => [ + 'default' => [], + ], + 'datasource_settings' => [ + 'entity:node' => [ + 'bundles' => [ + 'default' => FALSE, + 'selected' => ['parent'], + ], + ], + ], + 'server' => 'server', + 'field_settings' => [ + 'child_indexed' => [ + 'label' => 'Child > Indexed', + 'datasource_id' => 'entity:node', + 'property_path' => 'entity_reference:entity:indexed', + 'type' => 'text', + ], + ], + ]); + + $this->index->save(); + } + + /** + * Tests correct tracking of changes in referenced entities. + * + * @param array $parent_map + * Map of parent nodes to create. It should be compatible with the + * ::createEntitiesFromMap() method. + * @param array $child_map + * Map of the child nodes to create. It should be compatible with the + * ::createEntitiesFromMap(). + * @param array $updates + * A list of updates to child entities to execute. It should be keyed by the + * machine-name of the child entities. Value can be either FALSE (to remove + * an entity) or a list of the new raw values to apply to the entity. + * @param array $expected + * A list of search items that should be marked for reindexing. + * + * @dataProvider referencedEntityChangedDataProvider + */ + public function testReferencedEntityChanged(array $parent_map, array $child_map, array $updates, array $expected) { + $children = $this->createEntitiesFromMap($child_map, [], 'child'); + $this->createEntitiesFromMap($parent_map, $children, 'parent'); + + $this->index->indexItems(); + $tracker = $this->index->getTrackerInstance(); + $this->assertEquals([], $tracker->getRemainingItems()); + + // Now let's execute updates. + foreach ($updates as $i => $field_updates) { + if ($field_updates === FALSE) { + $children[$i]->delete(); + } + else { + foreach ($field_updates as $field => $items) { + $children[$i]->get($field)->setValue($items); + } + + $children[$i]->save(); + } + } + + $this->assertEquals($expected, $tracker->getRemainingItems()); + } + + /** + * Provides test data for testReferencedEntityChanged(). + * + * @return array[] + * An array of argument arrays for testReferencedEntityChanged(). + * + * @see \Drupal\Tests\search_api\Kernel\ReferencedEntitiesReindexingTest::testReferencedEntityChanged() + */ + public function referencedEntityChangedDataProvider(): array { + $tests = []; + + $parent_map = [ + 'parent' => [ + 'title' => 'Parent', + 'entity_reference' => 'child', + ], + ]; + + $parent_expected = ['entity:node/3:en']; + + $child_variants = ['child', 'unrelated']; + $field_variants = ['indexed', 'not_indexed']; + + foreach ($child_variants as $child) { + foreach ($field_variants as $field) { + if ($child == 'child' && $field == 'indexed') { + // This is how Search API represents our "parent" node in its tracking + // data. + $expected = $parent_expected; + } + else { + $expected = []; + } + + $tests["changing value of $field field within the $child entity"] = [ + $parent_map, + [ + 'child' => [ + 'title' => 'Child', + 'indexed' => 'Original indexed value', + 'not_indexed' => 'Original not indexed value.', + ], + 'unrelated' => [ + 'title' => 'Unrelated child', + 'indexed' => 'Original indexed value', + 'not_indexed' => 'Original not indexed value.', + ], + ], + [ + $child => [ + $field => "New $field value.", + ], + ], + $expected, + ]; + + $tests["appending value of $field field within the $child entity"] = [ + $parent_map, + [ + 'child' => [ + 'title' => 'Child', + 'indexed' => 'Original indexed value', + ], + 'unrelated' => [ + 'title' => 'Unrelated child', + 'indexed' => 'Original indexed value', + ], + ], + [ + $child => [ + $field => "New $field value.", + ], + ], + $expected, + ]; + + $tests["removing value of $field field within the $child entity"] = [ + $parent_map, + [ + 'child' => [ + 'title' => 'Child', + 'indexed' => 'Original indexed value', + 'not_indexed' => 'Original not indexed value.', + ], + 'unrelated' => [ + 'title' => 'Unrelated child', + 'indexed' => 'Original indexed value', + 'not_indexed' => 'Original not indexed value.', + ], + ], + [ + $child => [ + $field => [], + ], + ], + $expected, + ]; + } + + $tests["removing the $child entity"] = [ + $parent_map, + [ + 'child' => [ + 'title' => 'Child', + 'indexed' => 'Original indexed value', + 'not_indexed' => 'Original not indexed value.', + ], + 'unrelated' => [ + 'title' => 'Unrelated child', + 'indexed' => 'Original indexed value', + 'not_indexed' => 'Original not indexed value.', + ], + ], + [ + $child => FALSE, + ], + $child == 'child' ? $parent_expected : [], + ]; + } + + return $tests; + } + + /** + * Creates a list of entities with the given fields. + * + * @param array[] $entity_fields + * Map of entities to create. It should be keyed by a machine-friendly name. + * Values of this map should be sub-arrays that represent raw values to + * supply into the entity's fields when creating it. + * @param \Drupal\Core\Entity\ContentEntityInterface[] $references_map + * There is a magical field "entity_reference" in the $map input argument. + * Values of this field should reference some other entity. This "other" + * entity will be looked up by the key in this references map. This way you + * can create entity reference data without knowing the entity IDs ahead of + * time. + * @param string $bundle + * Bundle to utilize when creating entities from the $map array. + * + * @return \Drupal\Core\Entity\ContentEntityInterface[] + * Entities created according to the supplied $map array. This array will be + * keyed by the same machine-names as the input $map argument. + */ + protected function createEntitiesFromMap(array $entity_fields, array $references_map, string $bundle): array { + $entities = []; + + foreach ($entity_fields as $i => $fields) { + if (isset($fields['entity_reference'])) { + $fields['entity_reference'] = $references_map[$fields['entity_reference']]->id(); + } + $fields['type'] = $bundle; + $entities[$i] = Node::create($fields); + $entities[$i]->save(); + } + + return $entities; + } + +} diff --git a/web/modules/search_api/tests/src/Kernel/Processor/RenderedItemTest.php b/web/modules/search_api/tests/src/Kernel/Processor/RenderedItemTest.php index 70c099ff2e4d014741878029af415a2486b113dc..e3748f12ded1eeb5562f6b3e672b46ed2d96e02e 100644 --- a/web/modules/search_api/tests/src/Kernel/Processor/RenderedItemTest.php +++ b/web/modules/search_api/tests/src/Kernel/Processor/RenderedItemTest.php @@ -49,7 +49,7 @@ class RenderedItemTest extends ProcessorTestBase { 'comment', 'system', 'filter', - 'path_alias' + 'path_alias', ]; /** diff --git a/web/modules/search_api/tests/src/Kernel/Server/ServerImportTest.php b/web/modules/search_api/tests/src/Kernel/Server/ServerImportTest.php new file mode 100644 index 0000000000000000000000000000000000000000..1a27c86c9e48cab158e7ae13be2496cac07f4e0f --- /dev/null +++ b/web/modules/search_api/tests/src/Kernel/Server/ServerImportTest.php @@ -0,0 +1,91 @@ +<?php + +namespace Drupal\Tests\search_api\Kernel\Server; + +use Drupal\Component\Uuid\Php; +use Drupal\KernelTests\KernelTestBase; +use Drupal\search_api\Entity\Server; + +/** + * Tests that importing a server works correctly. + * + * @group search_api + */ +class ServerImportTest extends KernelTestBase { + + /** + * The test server. + * + * @var \Drupal\search_api\ServerInterface + */ + protected $server; + + /** + * {@inheritdoc} + */ + public static $modules = [ + 'search_api', + 'search_api_test', + 'user', + 'system', + ]; + + /** + * {@inheritdoc} + */ + public function setUp() { + parent::setUp(); + + $this->installEntitySchema('user'); + $this->installSchema('search_api', ['search_api_item']); + $this->installSchema('system', ['key_value_expire']); + $this->installEntitySchema('search_api_task'); + $this->installConfig('search_api'); + + // Create a test server. + $this->server = Server::create([ + 'name' => 'Test Server', + 'id' => 'test_server', + 'status' => 1, + 'backend' => 'search_api_test', + 'backend_config' => [ + 'test' => 'foo', + ], + ]); + $this->server->save(); + + $config_storage = $this->container->get('config.storage'); + $config_sync = $this->container->get('config.storage.sync'); + // Ensure the "system.site" config exists. + $config_storage->write('system.site', ['uuid' => (new Php())->generate()]); + $this->copyConfig($config_storage, $config_sync); + } + + /** + * Tests that importing new server config works correctly. + */ + public function testServerImport() { + // Stolen from + // \Drupal\Tests\system\Kernel\Entity\ConfigEntityImportTest::assertConfigUpdateImport(). + $name = $this->server->getConfigDependencyName(); + $original_data = $this->server->toArray(); + $custom_data = $original_data; + $custom_data['name'] = 'Old test server'; + $custom_data['backend_config']['test'] = 'bar'; + + $this->container->get('config.storage.sync')->write($name, $custom_data); + + // Verify the active configuration still returns the default values. + $config = $this->config($name); + $this->assertSame($config->get(), $original_data); + + // Import. + $this->configImporter()->import(); + + // Verify the values were updated. + $this->container->get('config.factory')->reset($name); + $config = $this->config($name); + $this->assertSame($config->get(), $custom_data); + } + +} diff --git a/web/modules/search_api/tests/src/Kernel/Views/ViewsCacheabilityMetadataExportTest.php b/web/modules/search_api/tests/src/Kernel/Views/ViewsCacheabilityMetadataExportTest.php index 7d6c91bff693f0947cc461decc4148c4befd2487..ff684a5344673c63359c7796bb575039e91dd75f 100644 --- a/web/modules/search_api/tests/src/Kernel/Views/ViewsCacheabilityMetadataExportTest.php +++ b/web/modules/search_api/tests/src/Kernel/Views/ViewsCacheabilityMetadataExportTest.php @@ -3,6 +3,7 @@ namespace Drupal\Tests\search_api\Kernel\Views; use Drupal\Core\Cache\Context\CacheContextsManager; +use Drupal\Core\Cache\Context\ContextCacheKeys; use Drupal\Core\Config\Config; use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\KernelTests\KernelTestBase; @@ -75,10 +76,10 @@ public function register(ContainerBuilder $container) { // error. $cache_contexts_manager = $this->createMock(CacheContextsManager::class); $cache_contexts_manager->method('assertValidTokens')->willReturn(TRUE); + $cache_contexts_manager->method('convertTokensToKeys')->willReturn(new ContextCacheKeys([])); $container->set('cache_contexts_manager', $cache_contexts_manager); } - /** * {@inheritdoc} */ diff --git a/web/modules/search_api/tests/src/Kernel/Views/ViewsPropertyExtractionTest.php b/web/modules/search_api/tests/src/Kernel/Views/ViewsPropertyExtractionTest.php index b38c61530b061b1adc053488786f771c70a06932..e22623122764d99cb6dc5f8bc97aece55cf5699c 100644 --- a/web/modules/search_api/tests/src/Kernel/Views/ViewsPropertyExtractionTest.php +++ b/web/modules/search_api/tests/src/Kernel/Views/ViewsPropertyExtractionTest.php @@ -195,7 +195,7 @@ public function testPropertyExtraction($property_path, $expected, $pre_set = FAL $field->preRender($values); - $this->assertTrue(isset($row->$property_path), "\"$property_path\" property is set on \$row"); + $this->assertObjectHasAttribute($property_path, $row); $this->assertEquals((array) $expected, $row->$property_path); // Check that $field->propertyReplacements was set correctly (if diff --git a/web/modules/search_api/tests/src/Unit/FieldsHelperTest.php b/web/modules/search_api/tests/src/Unit/FieldsHelperTest.php new file mode 100644 index 0000000000000000000000000000000000000000..74d946accd7ccf85330cd86b6ba21732dfc5feb4 --- /dev/null +++ b/web/modules/search_api/tests/src/Unit/FieldsHelperTest.php @@ -0,0 +1,90 @@ +<?php + +namespace Drupal\Tests\search_api\Unit; + +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Entity\EntityFieldManagerInterface; +use Drupal\Core\Entity\EntityTypeBundleInfoInterface; +use Drupal\Core\TypedData\ComplexDataDefinitionInterface; +use Drupal\Core\TypedData\TypedDataInterface; +use Drupal\Core\TypedData\DataDefinitionInterface; +use Drupal\search_api\Utility\DataTypeHelperInterface; +use Drupal\search_api\Utility\FieldsHelper; +use Drupal\Tests\UnitTestCase; + +/** + * Tests the fields helper utility class. + * + * @coversDefaultClass \Drupal\search_api\Utility\FieldsHelper + * + * @group search_api + */ +class FieldsHelperTest extends UnitTestCase { + + /** + * The field object being tested. + * + * @var \Drupal\search_api\Utility\FieldsHelper + */ + protected $fieldsHelper; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $entity_type_manager = $this->createMock(EntityTypeManagerInterface::class); + $entity_field_manager = $this->createMock(EntityFieldManagerInterface::class); + $entity_type_info = $this->createMock(EntityTypeBundleInfoInterface::class); + $data_type_helper = $this->createMock(DataTypeHelperInterface::class); + $this->fieldsHelper = new FieldsHelper($entity_type_manager, $entity_field_manager, $entity_type_info, $data_type_helper); + } + + /** + * Tests extracting field values. + * + * @covers ::extractFieldValues + */ + public function testExtractFieldValues() { + $field_data = $this->createMock(TestComplexDataInterface::class); + + $field_data_definition = $this->createMock(ComplexDataDefinitionInterface::class); + $field_data_definition->expects($this->any()) + ->method('isList') + ->will($this->returnValue(FALSE)); + + $field_data_definition->expects($this->any()) + ->method('getMainPropertyName') + ->will($this->returnValue('value')); + + $field_data->expects($this->any()) + ->method('getDataDefinition') + ->will($this->returnValue($field_data_definition)); + + $value_definition = $this->createMock(DataDefinitionInterface::class); + $value_definition->expects($this->any()) + ->method('isList') + ->will($this->returnValue(FALSE)); + + $value = $this->createMock(TypedDataInterface::class); + $value->expects($this->any()) + ->method('getValue') + ->will($this->returnValue('asd')); + + $value->expects($this->any()) + ->method('getDataDefinition') + ->will($this->returnValue($value_definition)); + + // Mock variants for with and without computed data. + $field_data->expects($this->any()) + ->method('getProperties') + ->willReturnMap([ + [FALSE, []], + [TRUE, ['value' => $value]], + ]); + + $this->assertArrayEquals(['asd'], $this->fieldsHelper->extractFieldValues($field_data)); + } + +} diff --git a/web/modules/search_api/tests/src/Unit/Processor/AddURLTest.php b/web/modules/search_api/tests/src/Unit/Processor/AddURLTest.php index 1e0f95ef894b35442fa0983ea9deb7cc38904254..9103922d3ba4325b6dfb428fa8b4ade49d3277bd 100644 --- a/web/modules/search_api/tests/src/Unit/Processor/AddURLTest.php +++ b/web/modules/search_api/tests/src/Unit/Processor/AddURLTest.php @@ -3,10 +3,11 @@ namespace Drupal\Tests\search_api\Unit\Processor; use Drupal\Core\Entity\Plugin\DataType\EntityAdapter; -use Drupal\Core\TypedData\DataDefinitionInterface; use Drupal\search_api\Datasource\DatasourceInterface; use Drupal\search_api\IndexInterface; use Drupal\search_api\Plugin\search_api\processor\AddURL; +use Drupal\search_api\Plugin\search_api\processor\Property\AddURLProperty; +use Drupal\Tests\search_api\Unit\TestUrl; use Drupal\Tests\UnitTestCase; /** @@ -42,20 +43,12 @@ protected function setUp() { $this->setUpMockContainer(); - // Create a mock for the URL to be returned. - $url = $this->getMockBuilder('Drupal\Core\Url') - ->disableOriginalConstructor() - ->getMock(); - $url->expects($this->any()) - ->method('toString') - ->will($this->returnValue('http://www.example.com/node/example')); - // Mock the datasource of the indexer to return the mocked url object. $datasource = $this->createMock(DatasourceInterface::class); $datasource->expects($this->any()) ->method('getItemUrl') ->withAnyParameters() - ->will($this->returnValue($url)); + ->will($this->returnValue(new TestUrl('/node/example'))); // Create a mock for the index to return the datasource mock. /** @var \Drupal\search_api\IndexInterface $index */ @@ -94,22 +87,29 @@ public function testAddFieldValues() { ]; $items = $this->createItems($this->index, 2, $fields, EntityAdapter::createFromEntity($node)); - // Add the processor's field values to the items. foreach ($items as $item) { + // Add a second URL field with "Generate absolute URL" enabled. + $field = (clone $item->getField('url')) + ->setFieldIdentifier('url_1') + ->setConfiguration(['absolute' => TRUE]); + $item->setField('url_1', $field); + + // Add the processor's field values to the items. $this->processor->addFieldValues($item); } - // Check the valid item. - $field = $items[$this->itemIds[0]]->getField('url'); - $this->assertEquals(['http://www.example.com/node/example'], $field->getValues(), 'Valid URL added as value to the field.'); + // Check the generated URLs. + $item_1 = $items[$this->itemIds[0]]; + $this->assertEquals(['/node/example'], $item_1->getField('url')->getValues()); + $this->assertEquals(['http://www.example.com/node/example'], $item_1->getField('url_1')->getValues()); // Check that no other fields were changed. - $field = $items[$this->itemIds[0]]->getField('body'); - $this->assertEquals($body_value, $field->getValues(), 'Body field was not changed.'); + $this->assertEquals($body_value, $item_1->getField('body')->getValues()); // Check the second item to be sure that all are processed. - $field = $items[$this->itemIds[1]]->getField('url'); - $this->assertEquals(['http://www.example.com/node/example'], $field->getValues(), 'Valid URL added as value to the field in the second item.'); + $item_2 = $items[$this->itemIds[1]]; + $this->assertEquals(['/node/example'], $item_2->getField('url')->getValues()); + $this->assertEquals(['http://www.example.com/node/example'], $item_2->getField('url_1')->getValues()); } /** @@ -120,16 +120,11 @@ public function testAddFieldValues() { public function testAlterPropertyDefinitions() { // Check for added properties when no datasource is given. $properties = $this->processor->getPropertyDefinitions(NULL); - $property_added = array_key_exists('search_api_url', $properties); - $this->assertTrue($property_added, 'The "search_api_url" property was added to the properties.'); - if ($property_added) { - $this->assertInstanceOf('Drupal\Core\TypedData\DataDefinitionInterface', $properties['search_api_url'], 'The "search_api_url" property contains a valid data definition.'); - if ($properties['search_api_url'] instanceof DataDefinitionInterface) { - $this->assertEquals('string', $properties['search_api_url']->getDataType(), 'Correct data type set in the data definition.'); - $this->assertEquals('URI', $properties['search_api_url']->getLabel(), 'Correct label set in the data definition.'); - $this->assertEquals('A URI where the item can be accessed', $properties['search_api_url']->getDescription(), 'Correct description set in the data definition.'); - } - } + $this->assertArrayHasKey('search_api_url', $properties); + $this->assertInstanceOf(AddURLProperty::class, $properties['search_api_url'], 'The "search_api_url" property contains a valid data definition.'); + $this->assertEquals('string', $properties['search_api_url']->getDataType(), 'Correct data type set in the data definition.'); + $this->assertEquals('URI', $properties['search_api_url']->getLabel(), 'Correct label set in the data definition.'); + $this->assertEquals('A URI where the item can be accessed', $properties['search_api_url']->getDescription(), 'Correct description set in the data definition.'); // Verify that there are no properties if a datasource is given. $datasource = $this->createMock(DatasourceInterface::class); diff --git a/web/modules/search_api/tests/src/Unit/Processor/IgnoreCharacterTest.php b/web/modules/search_api/tests/src/Unit/Processor/IgnoreCharacterTest.php index 5e15540dee81dee816a54441fcaf8da78a13b261..5f89214d6df7b3aa74e09bc1a949e4b86fea7512 100644 --- a/web/modules/search_api/tests/src/Unit/Processor/IgnoreCharacterTest.php +++ b/web/modules/search_api/tests/src/Unit/Processor/IgnoreCharacterTest.php @@ -118,7 +118,7 @@ public function ignoreCharacterSetsDataProvider() { public function testIgnorableCharacters($passed_value, $expected_value, $ignorable) { $this->processor->setConfiguration(['ignorable' => $ignorable, 'ignorable_classes' => []]); $this->invokeMethod('process', [&$passed_value, 'text']); - $this->assertEquals($expected_value, $passed_value); + $this->assertSame($expected_value, $passed_value); } /** @@ -133,6 +133,7 @@ public function ignorableCharactersDataProvider() { [['abcde', 'abcdef'], ['ace', 'ace'], '[bdf]'], ["ab.c'de", "a.'de", '[b-c]'], ['foo 13$%& (bar)[93]', 'foo $%& (bar)[]', '\d'], + [NULL, NULL, "['¿¡!?,.:;]"], ]; } diff --git a/web/modules/search_api/tests/src/Unit/Processor/StopwordsTest.php b/web/modules/search_api/tests/src/Unit/Processor/StopwordsTest.php index effcd6039a630d77c07841a84f2725dd4106bbf3..28141d77a3a0f9eea1e7714dee318b50e56d0dcd 100644 --- a/web/modules/search_api/tests/src/Unit/Processor/StopwordsTest.php +++ b/web/modules/search_api/tests/src/Unit/Processor/StopwordsTest.php @@ -23,7 +23,7 @@ class StopwordsTest extends UnitTestCase { protected function setUp() { parent::setUp(); $this->setUpMockContainer(); - $this->processor = new Stopwords([], 'stopwords', []);; + $this->processor = new Stopwords([], 'stopwords', []); } /** diff --git a/web/modules/search_api/tests/src/Unit/Processor/TokenizerTest.php b/web/modules/search_api/tests/src/Unit/Processor/TokenizerTest.php index b96d0b3c41b386b0e1717a7e7d0549ee7bc27fec..6b8a31f29bc4a763a65293b7409222adc406d61b 100644 --- a/web/modules/search_api/tests/src/Unit/Processor/TokenizerTest.php +++ b/web/modules/search_api/tests/src/Unit/Processor/TokenizerTest.php @@ -126,6 +126,23 @@ public function textDataProvider() { 'foo-bar', [Utility::createTextToken('foobar')], ], + // Test changing ignored characters. + [ + 'word-word', + [$word_token, $word_token], + ['ignored' => '._'], + ], + [ + 'foobar', + [Utility::createTextToken('foobr')], + ['ignored' => 'a'], + ], + // Test multiple ignored characters are still treated as word boundary. + [ + 'foobar', + [Utility::createTextToken('bar')], + ['ignored' => 'o'], + ], ]; } diff --git a/web/modules/search_api/tests/src/Unit/TermsParseModeTest.php b/web/modules/search_api/tests/src/Unit/TermsParseModeTest.php index d0e3ed80cfa994855ac1a70f00c0234efbb00658..25ec2c1c6501c84d3b58c5eaed51955d94c87cae 100644 --- a/web/modules/search_api/tests/src/Unit/TermsParseModeTest.php +++ b/web/modules/search_api/tests/src/Unit/TermsParseModeTest.php @@ -99,8 +99,16 @@ public function parseInputTestDataProvider() { ], ], ], + 'keywords with stand-alone dash' => [ + 'keys' => 'foo - bar', + 'expected' => [ + '#conjunction' => 'AND', + 'foo', + 'bar', + ], + ], 'really complicated search' => [ - 'keys' => 'pos -neg "quoted pos with -minus" -"quoted neg"', + 'keys' => 'pos -neg "quoted pos with -minus" - -"quoted neg"', 'expected' => [ '#conjunction' => 'AND', 'pos', diff --git a/web/modules/search_api/tests/src/Unit/TestUrl.php b/web/modules/search_api/tests/src/Unit/TestUrl.php new file mode 100644 index 0000000000000000000000000000000000000000..ed1dc1daba270e0fb354ca10a3deb7f7076a26da --- /dev/null +++ b/web/modules/search_api/tests/src/Unit/TestUrl.php @@ -0,0 +1,34 @@ +<?php + +namespace Drupal\Tests\search_api\Unit; + +use Drupal\Core\Url; +use Drupal\Tests\UnitTestCase; + +/** + * Provides a mock URL object. + */ +class TestUrl extends Url { + + /** + * Constructs a new class instance. + * + * @param string $path + * The internal path for this URL. + */ + public function __construct(string $path) { + $this->internalPath = $path; + } + + /** + * {@inheritdoc} + */ + public function toString($collect_bubbleable_metadata = FALSE) { + UnitTestCase::assertFalse($collect_bubbleable_metadata); + if (!empty($this->options['absolute'])) { + return 'http://www.example.com' . $this->internalPath; + } + return $this->internalPath; + } + +} diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_engines/simple_sitemap_engines.info.yml b/web/modules/simple_sitemap/modules/simple_sitemap_engines/simple_sitemap_engines.info.yml index 0ef2cb06a95c64eefd16105a25306f79d4adf3be..39730416e38e4edc00ee61200043ad74a9760b9b 100644 --- a/web/modules/simple_sitemap/modules/simple_sitemap_engines/simple_sitemap_engines.info.yml +++ b/web/modules/simple_sitemap/modules/simple_sitemap_engines/simple_sitemap_engines.info.yml @@ -8,7 +8,7 @@ core_version_requirement: ^8 || ^9 dependencies: - simple_sitemap:simple_sitemap -# Information added by Drupal.org packaging script on 2020-06-16 -version: '8.x-3.7' +# Information added by Drupal.org packaging script on 2020-11-12 +version: '8.x-3.8' project: 'simple_sitemap' -datestamp: 1592298876 +datestamp: 1605141361 diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_engines/src/Controller/SearchEngineListBuilder.php b/web/modules/simple_sitemap/modules/simple_sitemap_engines/src/Controller/SearchEngineListBuilder.php index 8ce2b2fc10584b09b0e690f5ad2b9866930c1d1f..3d7da378687fa4e25a352d8691bf18b3d3304aac 100644 --- a/web/modules/simple_sitemap/modules/simple_sitemap_engines/src/Controller/SearchEngineListBuilder.php +++ b/web/modules/simple_sitemap/modules/simple_sitemap_engines/src/Controller/SearchEngineListBuilder.php @@ -78,9 +78,10 @@ public function buildRow(EntityInterface $entity) { public function render() { return ['simple_sitemap_engines' => [ + '#type' => 'details', + '#open' => TRUE, '#prefix' => FormHelper::getDonationText(), '#title' => $this->t('Submission status'), - '#type' => 'fieldset', 'table' => parent::render(), '#description' => $this->t('Submission settings can be configured <a href="@url">here</a>.', ['@url' => $GLOBALS['base_url'] . '/admin/config/search/simplesitemap/engines/settings']), ]]; diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_views/config/schema/simple_sitemap_views.views.schema.yml b/web/modules/simple_sitemap/modules/simple_sitemap_views/config/schema/simple_sitemap_views.views.schema.yml index 99a96c90fef280c3af5653e9f31ae11ffa6de42d..10c9e5965af5b3478e78ad05eb77253d4c3ba7ad 100644 --- a/web/modules/simple_sitemap/modules/simple_sitemap_views/config/schema/simple_sitemap_views.views.schema.yml +++ b/web/modules/simple_sitemap/modules/simple_sitemap_views/config/schema/simple_sitemap_views.views.schema.yml @@ -1,12 +1,18 @@ views.display_extender.simple_sitemap_display_extender: type: views_display_extender + mapping: + variants: + type: sequence + label: 'Variants' + sequence: + type: simple_sitemap_views_variant_settings + +simple_sitemap_views_variant_settings: + type: mapping mapping: index: label: 'Index' type: boolean - variant: - label: 'Sitemap variant' - type: string priority: label: 'Priority' type: string diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_views/js/simple_sitemap.viewsUi.js b/web/modules/simple_sitemap/modules/simple_sitemap_views/js/simple_sitemap.viewsUi.js index 503eee1fdb2749f8946747b8dd8b2d9386ac4ca0..a6ca3d83f2712b09dfc17fd4191d3c5b2b833968 100755 --- a/web/modules/simple_sitemap/modules/simple_sitemap_views/js/simple_sitemap.viewsUi.js +++ b/web/modules/simple_sitemap/modules/simple_sitemap_views/js/simple_sitemap.viewsUi.js @@ -6,35 +6,20 @@ (function ($, Drupal) { Drupal.simpleSitemapViewsUi = {}; - Drupal.behaviors.simpleSitemapViewsUiCheckboxify = { - attach: function attach() { - var $button = $('[data-drupal-selector="edit-index-button"]').once('simple-sitemap-views-ui-checkboxify'); - if ($button.length) { - new Drupal.simpleSitemapViewsUi.Checkboxifier($button); - } - } - }; - Drupal.behaviors.simpleSitemapViewsUiArguments = { attach: function attach() { var $arguments = $('.indexed-arguments').once('simple-sitemap-views-ui-arguments'); - var $checkboxes = $arguments.find('input[type="checkbox"]'); - if ($checkboxes.length) { - new Drupal.simpleSitemapViewsUi.Arguments($checkboxes); - } - } - }; - Drupal.simpleSitemapViewsUi.Checkboxifier = function ($button) { - this.$button = $button; - this.$parent = this.$button.parent('div.simple-sitemap-views-index'); - this.$input = this.$parent.find('input:checkbox'); - this.$button.hide(); - this.$input.on('click', $.proxy(this, 'clickHandler')); - }; + if ($arguments.length) { + $arguments.each(function () { + var $checkboxes = $(this).find('input[type="checkbox"]'); - Drupal.simpleSitemapViewsUi.Checkboxifier.prototype.clickHandler = function () { - this.$button.trigger('click').trigger('submit'); + if ($checkboxes.length) { + new Drupal.simpleSitemapViewsUi.Arguments($checkboxes); + } + }); + } + } }; Drupal.simpleSitemapViewsUi.Arguments = function ($checkboxes) { @@ -55,4 +40,4 @@ this.$checkboxes.slice(index).prop('checked', false); }; -})(jQuery, Drupal); \ No newline at end of file +})(jQuery, Drupal); diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_views/simple_sitemap_views.info.yml b/web/modules/simple_sitemap/modules/simple_sitemap_views/simple_sitemap_views.info.yml index 11e2a8680e4c34d8b851b0f97ce1279e8202e8aa..25dc95007d6929b9f420527a1c7d1d79baacad4c 100644 --- a/web/modules/simple_sitemap/modules/simple_sitemap_views/simple_sitemap_views.info.yml +++ b/web/modules/simple_sitemap/modules/simple_sitemap_views/simple_sitemap_views.info.yml @@ -9,7 +9,7 @@ dependencies: - simple_sitemap:simple_sitemap - drupal:views -# Information added by Drupal.org packaging script on 2020-06-16 -version: '8.x-3.7' +# Information added by Drupal.org packaging script on 2020-11-12 +version: '8.x-3.8' project: 'simple_sitemap' -datestamp: 1592298876 +datestamp: 1605141361 diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_views/simple_sitemap_views.install b/web/modules/simple_sitemap/modules/simple_sitemap_views/simple_sitemap_views.install index 44b34e1bfd8a3abb06650cc0237ea61540a13a1e..ab4320f40e892625430cf95529fea12e19bd19cc 100755 --- a/web/modules/simple_sitemap/modules/simple_sitemap_views/simple_sitemap_views.install +++ b/web/modules/simple_sitemap/modules/simple_sitemap_views/simple_sitemap_views.install @@ -71,3 +71,40 @@ function simple_sitemap_views_schema() { ]; return $schema; } + +/** + * Update views display extender config. + */ +function simple_sitemap_views_update_8301() { + $config_factory = \Drupal::configFactory(); + $display_extender_name = 'simple_sitemap_display_extender'; + + // Find all views configs. + foreach ($config_factory->listAll('views.view.') as $view_config_name) { + $view = $config_factory->getEditable($view_config_name); + $changed = FALSE; + + // Go through each display on each view. + $displays = $view->get('display'); + foreach ($displays as $display_name => $display) { + if (isset($display['display_options']['display_extenders'][$display_extender_name])) { + $options = $display['display_options']['display_extenders'][$display_extender_name]; + + if (!isset($options['variants']) && isset($options['variant'])) { + $variant = $options['variant']; + unset($options['variant']); + + // Update display extender config. + $key = "display.$display_name.display_options.display_extenders.$display_extender_name"; + $options = ['variants' => [$variant => $options]]; + $view->set($key, $options); + $changed = TRUE; + } + } + } + + if ($changed) { + $view->save(TRUE); + } + } +} diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_views/simple_sitemap_views.services.yml b/web/modules/simple_sitemap/modules/simple_sitemap_views/simple_sitemap_views.services.yml index f01cceb37ec69de699ebc5a42fb8e35bf73b89e9..e9402ce2ce0dc43dffbdcaa9070c799e93283085 100755 --- a/web/modules/simple_sitemap/modules/simple_sitemap_views/simple_sitemap_views.services.yml +++ b/web/modules/simple_sitemap/modules/simple_sitemap_views/simple_sitemap_views.services.yml @@ -1,7 +1,7 @@ services: simple_sitemap.views: class: Drupal\simple_sitemap_views\SimpleSitemapViews - arguments: ['@entity_type.manager', '@config.factory', '@queue', '@database'] + arguments: ['@simple_sitemap.manager', '@entity_type.manager', '@config.factory', '@queue', '@database'] simple_sitemap.views.argument_collector: class: Drupal\simple_sitemap_views\EventSubscriber\ArgumentCollector arguments: ['@entity_type.manager', '@simple_sitemap.views', '@current_route_match'] diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_views/src/Controller/SimpleSitemapViewsController.php b/web/modules/simple_sitemap/modules/simple_sitemap_views/src/Controller/SimpleSitemapViewsController.php index cb6e8728a92a0de54036d5b44573d308999c7c8b..508e865c4e5be5e3854d57c4dcd9bce0b359a5af 100644 --- a/web/modules/simple_sitemap/modules/simple_sitemap_views/src/Controller/SimpleSitemapViewsController.php +++ b/web/modules/simple_sitemap/modules/simple_sitemap_views/src/Controller/SimpleSitemapViewsController.php @@ -51,7 +51,7 @@ public function content() { '#header' => [ $this->t('View'), $this->t('Display'), - $this->t('Arguments'), + $this->t('Variants'), $this->t('Operations'), ], '#empty' => $this->t('No view displays are set to be indexed yet. <a href="@url">Edit a view.</a>', ['@url' => $GLOBALS['base_url'] . '/admin/structure/views']), @@ -60,15 +60,17 @@ public function content() { foreach ($this->sitemapViews->getIndexableViews() as $index => $view) { $table[$index]['view'] = ['#markup' => $view->storage->label()]; $table[$index]['display'] = ['#markup' => $view->display_handler->display['display_title']]; - // Determine whether view display arguments are indexed. - $arguments_status = $this->sitemapViews->getIndexableArguments($view) ? $this->t('Yes') : $this->t('No'); - $table[$index]['arguments'] = ['#markup' => $arguments_status]; + + $variants = $this->sitemapViews->getIndexableVariants($view); + $variants = implode(', ', array_keys($variants)); + $table[$index]['variants'] = ['#markup' => $variants]; // Link to view display edit form. $display_edit_url = Url::fromRoute('entity.view.edit_display_form', [ 'view' => $view->id(), 'display_id' => $view->current_display, ]); + $table[$index]['operations'] = [ '#type' => 'operations', '#links' => [ diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_views/src/EventSubscriber/ArgumentCollector.php b/web/modules/simple_sitemap/modules/simple_sitemap_views/src/EventSubscriber/ArgumentCollector.php index f33ef222ea5c1194145a6215aef4fa5def480182..fc63255faea12541171d991a310a996531574549 100755 --- a/web/modules/simple_sitemap/modules/simple_sitemap_views/src/EventSubscriber/ArgumentCollector.php +++ b/web/modules/simple_sitemap/modules/simple_sitemap_views/src/EventSubscriber/ArgumentCollector.php @@ -56,7 +56,6 @@ public function __construct(EntityTypeManagerInterface $entity_type_manager, Sim */ public static function getSubscribedEvents() { $events[KernelEvents::TERMINATE] = 'onTerminate'; - return $events; } @@ -73,16 +72,16 @@ public function onTerminate(PostResponseEvent $event) { return; } - // Get view ID from route. $view_id = $this->routeMatch->getParameter('view_id'); /** @var \Drupal\views\ViewEntityInterface $view_entity */ if ($view_id && $view_entity = $this->viewStorage->load($view_id)) { - // Get display ID from route. $display_id = $this->routeMatch->getParameter('display_id'); + // Get a set of view arguments and try to add them to the index. $view = $view_entity->getExecutable(); $args = $this->getViewArgumentsFromRoute(); $this->sitemapViews->addArgumentsToIndex($view, $args, $display_id); + // Destroy a view instance. $view->destroy(); } @@ -103,11 +102,12 @@ protected function getViewArgumentsFromRoute() { $args = []; foreach ($map as $attribute => $parameter_name) { $parameter_name = isset($parameter_name) ? $parameter_name : $attribute; - if (($arg = $this->routeMatch->getRawParameter($parameter_name)) !== NULL) { + $arg = $this->routeMatch->getRawParameter($parameter_name); + + if ($arg !== NULL) { $args[] = $arg; } } - return $args; } diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_views/src/Plugin/QueueWorker/GarbageCollector.php b/web/modules/simple_sitemap/modules/simple_sitemap_views/src/Plugin/QueueWorker/GarbageCollector.php index 91a6b284f95f3839ac0a215ef3c860ff0fc4c685..5643b72ff0dc32c4dd7094df7aee8094c368bee3 100755 --- a/web/modules/simple_sitemap/modules/simple_sitemap_views/src/Plugin/QueueWorker/GarbageCollector.php +++ b/web/modules/simple_sitemap/modules/simple_sitemap_views/src/Plugin/QueueWorker/GarbageCollector.php @@ -79,19 +79,33 @@ public function processItem($data) { // Check that the view exists and it is enabled. if ($view_entity && $view_entity->status()) { $view = $view_entity->getExecutable(); + foreach ($this->sitemapViews->getRouterDisplayIds($view_entity) as $display_id) { // Ensure the display was correctly set. // Check that the display is enabled. if (!$view->setDisplay($display_id) || !$view->display_handler->isEnabled()) { continue; } + + $variants = $this->sitemapViews->getIndexableVariants($view); + $variants = array_keys($variants); + + $args_ids = []; + foreach ($variants as $variant) { + $variant_args_ids = $this->sitemapViews->getIndexableArguments($view, $variant); + + if (count($variant_args_ids) > count($args_ids)) { + $args_ids = $variant_args_ids; + } + } + // Check that the display has indexable arguments. - $args_ids = $this->sitemapViews->getIndexableArguments($view); if (empty($args_ids)) { continue; } $display_ids[] = $display_id; + // Delete records about sets of arguments that are no longer indexed. $args_ids = $this->sitemapViews->getArgumentsStringVariations($args_ids); $condition = new Condition('AND'); @@ -100,13 +114,26 @@ public function processItem($data) { $condition->condition('arguments_ids', $args_ids, 'NOT IN'); $this->sitemapViews->removeArgumentsFromIndex($condition); + $max_links = 0; + foreach ($variants as $variant) { + $settings = $this->sitemapViews->getSitemapSettings($view, $variant); + $variant_max_links = is_numeric($settings['max_links']) ? $settings['max_links'] : 0; + + if ($variant_max_links == 0) { + $max_links = 0; + break; + } + elseif ($variant_max_links > $max_links) { + $max_links = $variant_max_links; + } + } + // Check if the records limit for display is exceeded. - $settings = $this->sitemapViews->getSitemapSettings($view); - $max_links = is_numeric($settings['max_links']) ? $settings['max_links'] : 0; if ($max_links > 0) { $condition = new Condition('AND'); $condition->condition('view_id', $view_id); $condition->condition('display_id', $display_id); + // Delete records that exceed the limit. if ($index_id = $this->sitemapViews->getIndexIdByPosition($max_links, $condition)) { $condition->condition('id', $index_id, '>'); @@ -114,6 +141,7 @@ public function processItem($data) { } } } + // Delete records about view displays that do not exist or are disabled. if (!empty($display_ids)) { $condition = new Condition('AND'); @@ -121,6 +149,7 @@ public function processItem($data) { $condition->condition('display_id', $display_ids, 'NOT IN'); $this->sitemapViews->removeArgumentsFromIndex($condition); } + // Destroy a view instance. $view->destroy(); } diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_views/src/Plugin/simple_sitemap/UrlGenerator/ViewsUrlGenerator.php b/web/modules/simple_sitemap/modules/simple_sitemap_views/src/Plugin/simple_sitemap/UrlGenerator/ViewsUrlGenerator.php index 76ee0461e291e2fd226e8ece0eb6e87a25b66854..16f50c6036e9bc42984afaa396307f20f67e4eca 100755 --- a/web/modules/simple_sitemap/modules/simple_sitemap_views/src/Plugin/simple_sitemap/UrlGenerator/ViewsUrlGenerator.php +++ b/web/modules/simple_sitemap/modules/simple_sitemap_views/src/Plugin/simple_sitemap/UrlGenerator/ViewsUrlGenerator.php @@ -116,9 +116,9 @@ public function getDataSets() { // Get data sets. foreach ($this->sitemapViews->getIndexableViews() as $view) { - $settings = $this->sitemapViews->getSitemapSettings($view); - if ($settings['variant'] != $this->sitemapVariant) { - // Destroy a view instance. + $settings = $this->sitemapViews->getSitemapSettings($view, $this->sitemapVariant); + + if (empty($settings)) { $view->destroy(); continue; } @@ -131,17 +131,20 @@ public function getDataSets() { $data_sets[] = $base_data_set + ['arguments' => NULL]; // Process indexed arguments. - if ($args_ids = $this->sitemapViews->getIndexableArguments($view)) { + if ($args_ids = $this->sitemapViews->getIndexableArguments($view, $this->sitemapVariant)) { + $args_ids = $this->sitemapViews->getArgumentsStringVariations($args_ids); + // Form the condition according to the variants of the // indexable arguments. - $args_ids = $this->sitemapViews->getArgumentsStringVariations($args_ids); $condition = new Condition('AND'); $condition->condition('view_id', $view->id()); $condition->condition('display_id', $view->current_display); $condition->condition('arguments_ids', $args_ids, 'IN'); + // Get the arguments values from the index. $max_links = is_numeric($settings['max_links']) ? $settings['max_links'] : NULL; $indexed_arguments = $this->sitemapViews->getArgumentsFromIndex($condition, $max_links, TRUE); + // Add the arguments values for processing. foreach ($indexed_arguments as $index_id => $arguments_info) { $data_sets[] = $base_data_set + [ @@ -150,6 +153,7 @@ public function getDataSets() { ]; } } + // Destroy a view instance. $view->destroy(); } @@ -179,7 +183,7 @@ protected function processDataSet($data_set) { } // Trying to get the sitemap settings. - $settings = $this->sitemapViews->getSitemapSettings($view); + $settings = $this->sitemapViews->getSitemapSettings($view, $this->sitemapVariant); if (empty($settings)) { throw new \UnexpectedValueException('Failed to get the sitemap settings.'); } @@ -191,13 +195,16 @@ protected function processDataSet($data_set) { if (is_array($args)) { $params = array_merge([$view_id, $display_id], $args); $view_result = call_user_func_array('views_get_view_result', $params); + // Do not include paths on which the view returns an empty result. if (empty($view_result)) { throw new \UnexpectedValueException('The view returned an empty result.'); } + // Remove empty arguments from URL. $this->cleanRouteParameters($url, $args); } + $path = $url->getInternalPath(); // Destroy a view instance. $view->destroy(); @@ -243,11 +250,13 @@ protected function processDataSet($data_set) { */ protected function cleanRouteParameters(Url $url, array $args) { $parameters = $url->getRouteParameters(); + // Check that the number of params does not match the number of arguments. if (count($parameters) != count($args)) { $route_name = $url->getRouteName(); $route = $this->routeProvider->getRouteByName($route_name); $variables = $route->compile()->getVariables(); + // Remove params that are not present in the arguments. foreach ($variables as $variable_name) { if (empty($args)) { @@ -257,6 +266,7 @@ protected function cleanRouteParameters(Url $url, array $args) { array_shift($args); } } + // Set new route params. $url->setRouteParameters($parameters); } diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_views/src/Plugin/views/display_extender/SimpleSitemapDisplayExtender.php b/web/modules/simple_sitemap/modules/simple_sitemap_views/src/Plugin/views/display_extender/SimpleSitemapDisplayExtender.php index 0ffa6db3f427a1d4c093a1f6a83bc55952a62062..accb215f2f4478b35f8ae05902ebc6c2bd6d00ad 100755 --- a/web/modules/simple_sitemap/modules/simple_sitemap_views/src/Plugin/views/display_extender/SimpleSitemapDisplayExtender.php +++ b/web/modules/simple_sitemap/modules/simple_sitemap_views/src/Plugin/views/display_extender/SimpleSitemapDisplayExtender.php @@ -39,6 +39,13 @@ class SimpleSitemapDisplayExtender extends DisplayExtenderPluginBase { */ protected $sitemapManager; + /** + * The sitemap variants. + * + * @var array + */ + protected $variants = []; + /** * Constructs the plugin. * @@ -57,6 +64,7 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition parent::__construct($configuration, $plugin_id, $plugin_definition); $this->formHelper = $form_helper; $this->sitemapManager = $sitemap_manager; + $this->variants = $sitemap_manager->getSitemapVariants(NULL, FALSE); } /** @@ -87,12 +95,7 @@ public function init(ViewExecutable $view, DisplayPluginBase $display, array &$o */ protected function defineOptions() { $options = parent::defineOptions(); - $options['index'] = ['default' => 0]; - $options['variant'] = ['default' => NULL]; - $options['priority'] = ['default' => 0.5]; - $options['changefreq'] = ['default' => '']; - $options['arguments'] = ['default' => []]; - $options['max_links'] = ['default' => 100]; + $options['variants'] = ['default' => []]; return $options; } @@ -101,92 +104,81 @@ protected function defineOptions() { */ public function buildOptionsForm(&$form, FormStateInterface $form_state) { if ($this->hasSitemapSettings() && $form_state->get('section') == 'simple_sitemap') { - $form['#title'] .= $this->t('Simple XML Sitemap settings for this display'); - $settings = $this->getSitemapSettings(); + $arguments_options = $this->getArgumentsOptions(); - // The index section. - $form['index'] = [ - '#prefix' => '<div class="simple-sitemap-views-index">', - '#suffix' => '</div>', - ]; - // Add a checkbox for JS users, which will have behavior attached to it - // so it can replace the button. - $form['index']['index'] = [ - '#type' => 'checkbox', - '#title' => $this->t('Index this display'), - '#default_value' => $settings['index'], - ]; - // Then add the button itself. - $form['index']['index_button'] = [ - '#limit_validation_errors' => [], - '#type' => 'submit', - '#value' => $this->t('Index this display'), - '#submit' => [[$this, 'displaySitemapSettingsForm']], + $form['variants'] = [ + '#type' => 'container', + '#tree' => TRUE, ]; - // Show the whole form only if indexing is checked. - if ($this->options['index']) { - // Main settings fieldset. - $form['main'] = [ - '#type' => 'fieldset', - '#title' => $this->t('Main settings'), + foreach ($this->variants as $variant => $definition) { + $settings = $this->getSitemapSettings($variant); + $variant_form = &$form['variants'][$variant]; + + $variant_form = [ + '#type' => 'details', + '#title' => '<em>' . $definition['label'] . '</em>', + '#open' => (bool) $settings['index'], ]; - // The sitemap variant. - $form['main']['variant'] = [ - '#type' => 'select', - '#title' => $this->t('Sitemap variant'), - '#description' => $this->t('The sitemap variant this display is to be indexed in.'), - '#options' => $this->formHelper->getVariantSelectValues(), - '#default_value' => $this->formHelper->getVariantSelectValuesDefault($settings['variant']), - '#required' => TRUE, + + $variant_form['index'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Index this display in variant <em>@variant_label</em>', [ + '@variant_label' => $definition['label'], + ]), + '#default_value' => $settings['index'], + ]; + + $states = [ + 'visible' => [ + ':input[name="variants[' . $variant . '][index]"]' => ['checked' => TRUE], + ], ]; + // The sitemap priority. - $form['main']['priority'] = [ + $variant_form['priority'] = [ '#type' => 'select', '#title' => $this->t('Priority'), '#description' => $this->t('The priority this display will have in the eyes of search engine bots.'), '#default_value' => $settings['priority'], '#options' => $this->formHelper->getPrioritySelectValues(), + '#states' => $states, ]; + // The sitemap change frequency. - $form['main']['changefreq'] = [ + $variant_form['changefreq'] = [ '#type' => 'select', '#title' => $this->t('Change frequency'), '#description' => $this->t('The frequency with which this display changes. Search engine bots may take this as an indication of how often to index it.'), '#default_value' => $settings['changefreq'], '#options' => $this->formHelper->getChangefreqSelectValues(), + '#states' => $states, ]; - // Argument settings fieldset. - $form['arguments'] = [ - '#type' => 'fieldset', - '#title' => $this->t('Argument settings'), + // Arguments to index. + $variant_form['arguments'] = [ + '#type' => 'checkboxes', + '#title' => $this->t('Indexed arguments'), + '#options' => $arguments_options, + '#default_value' => $settings['arguments'], + '#attributes' => ['class' => ['indexed-arguments']], + '#access' => !empty($arguments_options), + '#states' => $states, + ]; + + // Max links with arguments. + $variant_form['max_links'] = [ + '#type' => 'number', + '#title' => $this->t('Maximum display variations'), + '#description' => $this->t('The maximum number of link variations to be indexed for this display. If left blank, each argument will create link variations for this display. Use with caution, as a large number of argument valuescan significantly increase the number of sitemap links.'), + '#default_value' => $settings['max_links'], + '#min' => 1, + '#access' => !empty($arguments_options), + '#states' => $states, ]; - // Get view arguments options. - if ($arguments_options = $this->getArgumentsOptions()) { - // Indexed arguments element. - $form['arguments']['arguments'] = [ - '#type' => 'checkboxes', - '#title' => $this->t('Indexed arguments'), - '#options' => $arguments_options, - '#default_value' => $settings['arguments'], - '#attributes' => ['class' => ['indexed-arguments']], - ]; - // Max links with arguments. - $form['arguments']['max_links'] = [ - '#type' => 'number', - '#title' => $this->t('Maximum display variations'), - '#description' => $this->t('The maximum number of link variations to be indexed for this display. If left blank, each argument will create link variations for this display. Use with caution, as a large number of argument valuescan significantly increase the number of sitemap links.'), - '#default_value' => $settings['max_links'], - '#min' => 1, - ]; - } - else { - $form['arguments']['#description'] = $this->t('This display has no arguments.'); - } } - // Attaching script to form. + $form['#title'] .= $this->t('Simple XML Sitemap settings for this display'); $form['#attached']['library'][] = 'simple_sitemap_views/viewsUi'; } } @@ -196,11 +188,13 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { */ public function validateOptionsForm(&$form, FormStateInterface $form_state) { if ($this->hasSitemapSettings() && $form_state->get('section') == 'simple_sitemap') { - // Validate indexed arguments. - $arguments = $form_state->getValue('arguments', []); - $errors = $this->validateIndexedArguments($arguments); - foreach ($errors as $message) { - $form_state->setError($form['arguments']['arguments'], $message); + foreach (array_keys($this->variants) as $variant) { + $arguments = $form_state->getValue(['variants', $variant, 'arguments'], []); + $errors = $this->validateIndexedArguments($arguments); + + foreach ($errors as $message) { + $form_state->setError($form['variants'][$variant]['arguments'], $message); + } } } } @@ -210,12 +204,16 @@ public function validateOptionsForm(&$form, FormStateInterface $form_state) { */ public function submitOptionsForm(&$form, FormStateInterface $form_state) { if ($this->hasSitemapSettings() && $form_state->get('section') == 'simple_sitemap') { - $values = $form_state->cleanValues()->getValues(); - $values['arguments'] = isset($values['arguments']) ? array_filter($values['arguments']) : []; - // Save sitemap settings. - foreach ($values as $key => $value) { - if (array_key_exists($key, $this->options)) { - $this->options[$key] = $value; + $variants = $form_state->getValue('variants'); + $this->options['variants'] = []; + + // Save settings for each variant. + foreach (array_keys($this->variants) as $variant) { + $settings = $variants[$variant] + $this->getSitemapSettings($variant); + + if ($settings['index']) { + $settings['arguments'] = array_filter($settings['arguments']); + $this->options['variants'][$variant] = $settings; } } } @@ -225,16 +223,18 @@ public function submitOptionsForm(&$form, FormStateInterface $form_state) { * {@inheritdoc} */ public function validate() { - $errors = parent::validate(); + $errors = [parent::validate()]; // Validate the argument options relative to the // current state of the view argument handlers. if ($this->hasSitemapSettings()) { - $settings = $this->getSitemapSettings(); - $result = $this->validateIndexedArguments($settings['arguments']); - $errors = array_merge($errors, $result); + foreach (array_keys($this->variants) as $variant) { + $settings = $this->getSitemapSettings($variant); + $errors[] = $this->validateIndexedArguments($settings['arguments']); + } } - return $errors; + + return array_merge([], ...$errors); } /** @@ -246,48 +246,48 @@ public function optionsSummary(&$categories, &$options) { 'title' => $this->t('Simple XML Sitemap'), 'column' => 'second', ]; + + $included_variants = []; + foreach (array_keys($this->variants) as $variant) { + $settings = $this->getSitemapSettings($variant); + + if ($settings['index']) { + $included_variants[] = $variant; + } + } + $options['simple_sitemap'] = [ + 'title' => NULL, 'category' => 'simple_sitemap', - 'title' => $this->t('Status'), - 'value' => $this->isIndexingEnabled() ? $this->t('Included in sitemap') : $this->t('Excluded from sitemap'), + 'value' => $included_variants ? $this->t('Included in sitemap variants: @variants', [ + '@variants' => implode(', ', $included_variants), + ]) : $this->t('Excluded from all sitemap variants'), ]; } } /** - * Displays the sitemap settings form. + * Gets the sitemap settings. * - * @param array $form - * The form structure. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * Current form state. - */ - public function displaySitemapSettingsForm(array $form, FormStateInterface $form_state) { - // Update index option. - $this->options['index'] = empty($this->options['index']); - - // Rebuild settings form. - /** @var \Drupal\views_ui\ViewUI $view */ - $view = $form_state->get('view'); - $display_handler = $view->getExecutable()->display_handler; - $extender_options = $display_handler->getOption('display_extenders'); - if (isset($extender_options[$this->pluginId])) { - $extender_options[$this->pluginId] = $this->options; - $display_handler->setOption('display_extenders', $extender_options); - } - $view->cacheSet(); - $form_state->set('rerender', TRUE); - $form_state->setRebuild(); - } - - /** - * Get sitemap settings configuration for this display. + * @param string $variant + * The name of the sitemap variant. * * @return array * The sitemap settings. */ - public function getSitemapSettings() { - return $this->options; + public function getSitemapSettings($variant) { + $settings = [ + 'index' => 0, + 'priority' => 0.5, + 'changefreq' => '', + 'arguments' => [], + 'max_links' => 100, + ]; + + if (isset($this->options['variants'][$variant])) { + $settings = $this->options['variants'][$variant] + $settings; + } + return $settings; } /** @@ -300,17 +300,6 @@ public function hasSitemapSettings() { return $this->displayHandler instanceof DisplayRouterInterface; } - /** - * Identify whether or not the current display indexing is enabled. - * - * @return bool - * Indexing is enabled (TRUE) or not (FALSE). - */ - public function isIndexingEnabled() { - $settings = $this->getSitemapSettings(); - return !empty($settings['index']); - } - /** * Returns available view arguments options. * @@ -318,9 +307,9 @@ public function isIndexingEnabled() { * View arguments labels keyed by argument ID. */ protected function getArgumentsOptions() { - $arguments_options = []; - // Get view argument handlers. $arguments = $this->displayHandler->getHandlers('argument'); + $arguments_options = []; + /** @var \Drupal\views\Plugin\views\argument\ArgumentPluginBase $argument */ foreach ($arguments as $id => $argument) { $arguments_options[$id] = $argument->adminLabel(); diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_views/src/SimpleSitemapViews.php b/web/modules/simple_sitemap/modules/simple_sitemap_views/src/SimpleSitemapViews.php index d7cf0db54df3e3b7e760a5f485a035742126f163..7fae9b93cddb167d3f3ee2b9cd7add0809071d60 100755 --- a/web/modules/simple_sitemap/modules/simple_sitemap_views/src/SimpleSitemapViews.php +++ b/web/modules/simple_sitemap/modules/simple_sitemap_views/src/SimpleSitemapViews.php @@ -3,6 +3,7 @@ namespace Drupal\simple_sitemap_views; use Drupal\simple_sitemap_views\Plugin\views\display_extender\SimpleSitemapDisplayExtender; +use Drupal\simple_sitemap\SimplesitemapManager; use Drupal\Core\Database\Query\ConditionInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Config\ConfigFactoryInterface; @@ -29,6 +30,13 @@ class SimpleSitemapViews { */ const PLUGIN_ID = 'simple_sitemap_display_extender'; + /** + * Simple XML Sitemap manager. + * + * @var \Drupal\simple_sitemap\SimplesitemapManager + */ + protected $sitemapManager; + /** * View entities storage. * @@ -60,6 +68,8 @@ class SimpleSitemapViews { /** * SimpleSitemapViews constructor. * + * @param \Drupal\simple_sitemap\SimplesitemapManager $sitemap_manager + * Simple XML Sitemap manager. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * The entity type manager. * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory @@ -70,11 +80,13 @@ class SimpleSitemapViews { * The current active database's master connection. */ public function __construct( + SimplesitemapManager $sitemap_manager, EntityTypeManagerInterface $entity_type_manager, ConfigFactoryInterface $config_factory, QueueFactory $queue_factory, Connection $database ) { + $this->sitemapManager = $sitemap_manager; $this->viewStorage = $entity_type_manager->getStorage('view'); $this->configFactory = $config_factory; $this->queueFactory = $queue_factory; @@ -90,7 +102,6 @@ public function __construct( public function isEnabled() { // Support enabled when views display extender is enabled. $enabled = Views::getEnabledDisplayExtenders(); - return isset($enabled[self::PLUGIN_ID]); } @@ -127,49 +138,59 @@ public function disable() { } /** - * Get sitemap settings for view display. + * Gets the sitemap settings for view display. * * @param \Drupal\views\ViewExecutable $view * A view executable instance. + * @param string $variant + * The name of the sitemap variant. * @param string|null $display_id - * The display id. If empty uses the preselected display. + * The display id. If empty uses the current display. * * @return array|null * The sitemap settings if the display is indexed, NULL otherwise. */ - public function getSitemapSettings(ViewExecutable $view, $display_id = NULL) { + public function getSitemapSettings(ViewExecutable $view, $variant, $display_id = NULL) { // Ensure the display was correctly set. if (!$view->setDisplay($display_id)) { return NULL; } - // Get the list of extenders. + $extenders = $view->display_handler->getExtenders(); $extender = isset($extenders[self::PLUGIN_ID]) ? $extenders[self::PLUGIN_ID] : NULL; + // Retrieve the sitemap settings from the extender. - if ($extender instanceof SimpleSitemapDisplayExtender && $extender->hasSitemapSettings() && $extender->isIndexingEnabled()) { - return $extender->getSitemapSettings(); - } + if ($extender instanceof SimpleSitemapDisplayExtender && $extender->hasSitemapSettings()) { + $settings = $extender->getSitemapSettings($variant); + if ($settings['index']) { + return $settings; + } + } return NULL; } /** - * Get indexable arguments for view display. + * Gets the indexable arguments for view display. * * @param \Drupal\views\ViewExecutable $view * A view executable instance. + * @param string $variant + * The name of the sitemap variant. * @param string|null $display_id - * The display id. If empty uses the preselected display. + * The display id. If empty uses the current display. * * @return array * Indexable arguments identifiers. */ - public function getIndexableArguments(ViewExecutable $view, $display_id = NULL) { + public function getIndexableArguments(ViewExecutable $view, $variant, $display_id = NULL) { + $settings = $this->getSitemapSettings($view, $variant, $display_id); $indexable_arguments = []; - $settings = $this->getSitemapSettings($view, $display_id); - if ($settings && !empty($settings['arguments']) && is_array($settings['arguments'])) { - // Find indexable arguments. + + // Find indexable arguments. + if ($settings && !empty($settings['arguments'])) { $arguments = array_keys($view->display_handler->getHandlers('argument')); + foreach ($arguments as $argument_id) { if (empty($settings['arguments'][$argument_id])) { break; @@ -177,7 +198,6 @@ public function getIndexableArguments(ViewExecutable $view, $display_id = NULL) $indexable_arguments[] = $argument_id; } } - return $indexable_arguments; } @@ -189,37 +209,69 @@ public function getIndexableArguments(ViewExecutable $view, $display_id = NULL) * @param array $args * Array of arguments to add to the index. * @param string|null $display_id - * The display id. If empty uses the preselected display. + * The display id. If empty uses the current display. * * @return bool * TRUE if the arguments are added to the index, FALSE otherwise. */ public function addArgumentsToIndex(ViewExecutable $view, array $args, $display_id = NULL) { + $variants = $this->sitemapManager->getSitemapVariants(NULL, FALSE); + + foreach (array_keys($variants) as $variant) { + $result = $this->addArgumentsToIndexByVariant($view, $variant, $args, $display_id); + + if ($result) { + return $result; + } + } + return FALSE; + } + + /** + * Adds view arguments to the index by the sitemap variant. + * + * @param \Drupal\views\ViewExecutable $view + * A view executable instance. + * @param string $variant + * The name of the sitemap variant. + * @param array $args + * Array of arguments to add to the index. + * @param string|null $display_id + * The display id. If empty uses the current display. + * + * @return bool + * TRUE if the arguments are added to the index, FALSE otherwise. + */ + public function addArgumentsToIndexByVariant(ViewExecutable $view, $variant, array $args, $display_id = NULL) { // An array of arguments to be added to the index can not be empty. // Also ensure the display was correctly set. if (empty($args) || !$view->setDisplay($display_id)) { return FALSE; } + // Check that indexing of at least one argument is enabled. - $indexable_arguments = $this->getIndexableArguments($view); + $indexable_arguments = $this->getIndexableArguments($view, $variant); if (empty($indexable_arguments)) { return FALSE; } + // Check that the number of identifiers is equal to the number of values. $args_ids = array_slice($indexable_arguments, 0, count($args)); if (count($args_ids) != count($args)) { return FALSE; } + // Check that the current number of rows in the index does not // exceed the specified number. $condition = new Condition('AND'); $condition->condition('view_id', $view->id()); $condition->condition('display_id', $view->current_display); - $settings = $this->getSitemapSettings($view); + $settings = $this->getSitemapSettings($view, $variant); $max_links = is_numeric($settings['max_links']) ? $settings['max_links'] : 0; if ($max_links > 0 && $this->getArgumentsFromIndexCount($condition) >= $max_links) { return FALSE; } + // Convert the set of identifiers and a set of values to string. $args_ids = $this->convertArgumentsArrayToString($args_ids); $args_values = $this->convertArgumentsArrayToString($args); @@ -229,12 +281,14 @@ public function addArgumentsToIndex(ViewExecutable $view, array $args, $display_ if ($this->getArgumentsFromIndexCount($condition)) { return FALSE; } + // Check that the view result is not empty for this set of arguments. $params = array_merge([$view->id(), $view->current_display], $args); $view_result = call_user_func_array('views_get_view_result', $params); if (empty($view_result)) { return FALSE; } + // Add a set of arguments to the index. $options = ['return' => Database::RETURN_AFFECTED]; $query = $this->database->insert('simple_sitemap_views', $options); @@ -244,7 +298,6 @@ public function addArgumentsToIndex(ViewExecutable $view, array $args, $display_ 'arguments_ids' => $args_ids, 'arguments_values' => $args_values, ]); - return (bool) $query->execute(); } @@ -264,23 +317,22 @@ public function addArgumentsToIndex(ViewExecutable $view, array $args, $display_ * An array with information about the indexed arguments. */ public function getArgumentsFromIndex(ConditionInterface $condition = NULL, $limit = NULL, $convert = FALSE) { - // Select the rows from the index table. $query = $this->database->select('simple_sitemap_views', 'ssv'); $query->addField('ssv', 'id'); $query->addField('ssv', 'view_id'); $query->addField('ssv', 'display_id'); $query->addField('ssv', 'arguments_values', 'arguments'); - // Add conditions if necessary. + if (!empty($condition)) { $query->condition($condition); } - // Limit results if necessary. if (!empty($limit)) { $query->range(0, $limit); } + $rows = $query->execute()->fetchAll(); - // Form the result. $arguments = []; + foreach ($rows as $row) { $arguments[$row->id] = [ 'view_id' => $row->view_id, @@ -288,7 +340,6 @@ public function getArgumentsFromIndex(ConditionInterface $condition = NULL, $lim 'arguments' => $convert ? $this->convertArgumentsStringToArray($row->arguments) : $row->arguments, ]; } - return $arguments; } @@ -303,12 +354,10 @@ public function getArgumentsFromIndex(ConditionInterface $condition = NULL, $lim */ public function getArgumentsFromIndexCount(ConditionInterface $condition = NULL) { $query = $this->database->select('simple_sitemap_views', 'ssv'); - // Add conditions if necessary. + if (!empty($condition)) { $query->condition($condition); } - - // Get the number of rows from the index table. return $query->countQuery()->execute()->fetchField(); } @@ -326,13 +375,13 @@ public function getArgumentsFromIndexCount(ConditionInterface $condition = NULL) public function getIndexIdByPosition($position, ConditionInterface $condition = NULL) { $query = $this->database->select('simple_sitemap_views', 'ssv'); $query->addField('ssv', 'id'); - // Add conditions if necessary. + if (!empty($condition)) { $query->condition($condition); } + $query->orderBy('id', 'ASC'); $query->range($position - 1, 1); - return $query->execute()->fetchField(); } @@ -366,11 +415,12 @@ public function removeArgumentsFromIndex(ConditionInterface $condition = NULL) { */ public function getRouterDisplayIds(ViewEntityInterface $view_entity) { $display_plugins = $this->getRouterDisplayPluginIds(); + $filter_callback = function (array $display) use ($display_plugins) { return !empty($display['display_plugin']) && in_array($display['display_plugin'], $display_plugins); }; - $displays = array_filter($view_entity->get('display'), $filter_callback); + $displays = array_filter($view_entity->get('display'), $filter_callback); return array_keys($displays); } @@ -385,11 +435,13 @@ public function getIndexableViews() { if (!$this->isEnabled()) { return []; } + // Load views with display plugins that use the route. $query = $this->viewStorage->getQuery(); $query->condition('status', TRUE); $query->condition("display.*.display_plugin", $this->getRouterDisplayPluginIds(), 'IN'); $view_ids = $query->execute(); + // If there are no such views, then return an empty array. if (empty($view_ids)) { return []; @@ -400,6 +452,7 @@ public function getIndexableViews() { foreach ($this->viewStorage->loadMultiple($view_ids) as $view_entity) { foreach ($this->getRouterDisplayIds($view_entity) as $display_id) { $view = Views::executableFactory()->get($view_entity); + // Ensure the display was correctly set. if (!$view->setDisplay($display_id)) { $view->destroy(); @@ -407,30 +460,58 @@ public function getIndexableViews() { } // Check that the display is enabled and indexed. - if ($view->display_handler->isEnabled() && $this->getSitemapSettings($view)) { + if ($view->display_handler->isEnabled() && $this->getIndexableVariants($view)) { $indexable_views[] = $view; } } } - return $indexable_views; } + /** + * Returns an array of indexable sitemap variants for view display. + * + * @param \Drupal\views\ViewExecutable $view + * A view executable instance. + * @param string|null $display_id + * The display id. If empty uses the current display. + * + * @return array + * An array of sitemap variants. + */ + public function getIndexableVariants(ViewExecutable $view, $display_id = NULL) { + // Ensure the display was correctly set. + if (!$view->setDisplay($display_id)) { + return []; + } + + $variants = $this->sitemapManager->getSitemapVariants(NULL, FALSE); + foreach (array_keys($variants) as $variant) { + if (!$this->getSitemapSettings($view, $variant)) { + unset($variants[$variant]); + } + } + return $variants; + } + /** * Creates tasks in the garbage collection queue. */ public function executeGarbageCollection() { // The task queue of garbage collection in the arguments index. $queue = $this->queueFactory->get('simple_sitemap.views.garbage_collector'); + // Check that the queue is empty. if ($queue->numberOfItems()) { return; } + // Get identifiers of indexed views. $query = $this->database->select('simple_sitemap_views', 'ssv'); $query->addField('ssv', 'view_id'); $query->distinct(); $result = $query->execute()->fetchCol(); + // Create a garbage collection tasks. foreach ($result as $view_id) { $queue->createItem(['view_id' => $view_id]); @@ -448,11 +529,11 @@ public function executeGarbageCollection() { */ public function getArgumentsStringVariations(array $args) { $variations = []; + for ($length = 1; $length <= count($args); $length++) { $args_slice = array_slice($args, 0, $length); $variations[] = $this->convertArgumentsArrayToString($args_slice); } - return $variations; } @@ -490,16 +571,17 @@ protected function convertArgumentsStringToArray($args) { */ protected function getRouterDisplayPluginIds() { static $plugin_ids = []; + if (empty($plugin_ids)) { - // Get all display plugins that use the route. $display_plugins = Views::pluginManager('display')->getDefinitions(); + + // Get all display plugins that use the route. foreach ($display_plugins as $plugin_id => $definition) { if (!empty($definition['uses_route'])) { $plugin_ids[$plugin_id] = $plugin_id; } } } - return $plugin_ids; } diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_views/tests/modules/simple_sitemap_views_test/config/install/views.view.simple_sitemap_views_test_view.yml b/web/modules/simple_sitemap/modules/simple_sitemap_views/tests/modules/simple_sitemap_views_test/config/install/views.view.simple_sitemap_views_test_view.yml index d4dfae2f2fc659ff00135bd2bd2a589bc5c8b831..acd3a247c8cf608f6fc8162fbcb6b73e77dc6ca8 100644 --- a/web/modules/simple_sitemap/modules/simple_sitemap_views/tests/modules/simple_sitemap_views_test/config/install/views.view.simple_sitemap_views_test_view.yml +++ b/web/modules/simple_sitemap/modules/simple_sitemap_views/tests/modules/simple_sitemap_views_test/config/install/views.view.simple_sitemap_views_test_view.yml @@ -262,14 +262,15 @@ display: display_options: display_extenders: simple_sitemap_display_extender: - index: true - variant: default - priority: '0.5' - changefreq: '' - arguments: - type: type - title: title - max_links: 2 + variants: + default: + index: true + priority: '0.5' + changefreq: '' + arguments: + type: type + title: title + max_links: 2 path: simple-sitemap-views-test-view rendering_language: en cache_metadata: diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_views/tests/modules/simple_sitemap_views_test/simple_sitemap_views_test.info.yml b/web/modules/simple_sitemap/modules/simple_sitemap_views/tests/modules/simple_sitemap_views_test/simple_sitemap_views_test.info.yml index 46a2a9cb6d926eb5ca8ca85702244824fbe90fe3..d6410836c0b3631fac8a3453b25d57cde04d9784 100644 --- a/web/modules/simple_sitemap/modules/simple_sitemap_views/tests/modules/simple_sitemap_views_test/simple_sitemap_views_test.info.yml +++ b/web/modules/simple_sitemap/modules/simple_sitemap_views/tests/modules/simple_sitemap_views_test/simple_sitemap_views_test.info.yml @@ -7,7 +7,7 @@ core_version_requirement: ^8 || ^9 dependencies: - simple_sitemap:simple_sitemap_views -# Information added by Drupal.org packaging script on 2020-06-16 -version: '8.x-3.7' +# Information added by Drupal.org packaging script on 2020-11-12 +version: '8.x-3.8' project: 'simple_sitemap' -datestamp: 1592298876 +datestamp: 1605141361 diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_views/tests/src/Functional/SimpleSitemapViewsTest.php b/web/modules/simple_sitemap/modules/simple_sitemap_views/tests/src/Functional/SimpleSitemapViewsTest.php index e3ffc43181ea9d1d68a21e6ac15a13e90af281f3..36f8035f1213920ff9a85f59b486fa18a73e4a74 100644 --- a/web/modules/simple_sitemap/modules/simple_sitemap_views/tests/src/Functional/SimpleSitemapViewsTest.php +++ b/web/modules/simple_sitemap/modules/simple_sitemap_views/tests/src/Functional/SimpleSitemapViewsTest.php @@ -42,7 +42,7 @@ public function testIndexableViews() { $this->assertTrue($test_view_exists); // Check the indexing status of the arguments. - $indexable_arguments = $this->sitemapViews->getIndexableArguments($this->testView); + $indexable_arguments = $this->sitemapViews->getIndexableArguments($this->testView, $this->sitemapVariant); $this->assertContains('type', $indexable_arguments); $this->assertContains('title', $indexable_arguments); $this->assertNotContains('nid', $indexable_arguments); diff --git a/web/modules/simple_sitemap/modules/simple_sitemap_views/tests/src/Functional/SimpleSitemapViewsTestBase.php b/web/modules/simple_sitemap/modules/simple_sitemap_views/tests/src/Functional/SimpleSitemapViewsTestBase.php index 3dae919d914b80c4414924d894d8f212e4943cc1..4d3888d78d1eba502d92c6115f592f6b8bbf8d00 100644 --- a/web/modules/simple_sitemap/modules/simple_sitemap_views/tests/src/Functional/SimpleSitemapViewsTestBase.php +++ b/web/modules/simple_sitemap/modules/simple_sitemap_views/tests/src/Functional/SimpleSitemapViewsTestBase.php @@ -40,6 +40,13 @@ abstract class SimpleSitemapViewsTestBase extends SimplesitemapTestBase { */ protected $testView; + /** + * The sitemap variant. + * + * @var string + */ + protected $sitemapVariant; + /** * {@inheritdoc} */ @@ -48,6 +55,7 @@ protected function setUp() { $this->sitemapViews = $this->container->get('simple_sitemap.views'); $this->cron = $this->container->get('cron'); + $this->sitemapVariant = 'default'; $this->testView = Views::getView('simple_sitemap_views_test_view'); $this->testView->setDisplay('page_1'); diff --git a/web/modules/simple_sitemap/simple_sitemap.api.php b/web/modules/simple_sitemap/simple_sitemap.api.php index 15593722d108a2fc51da0cd427781df8c380045c..4a8f55e4f18c4b0dc6baf77891329fce56774aa6 100644 --- a/web/modules/simple_sitemap/simple_sitemap.api.php +++ b/web/modules/simple_sitemap/simple_sitemap.api.php @@ -33,9 +33,7 @@ function hook_simple_sitemap_links_alter(array &$links, $sitemap_variant) { // If this 'loc' URL points to a non-german site, make sure to remove // its german alternate URL. else { - if ($link['alternate_urls']['de']) { - unset($links[$key]['alternate_urls']['de']); - } + unset($links[$key]['alternate_urls']['de']); } } } diff --git a/web/modules/simple_sitemap/simple_sitemap.info.yml b/web/modules/simple_sitemap/simple_sitemap.info.yml index ad557ba9d413da56ad6d05f84e64844e18708fa5..87aa33a8d28998535d4c75c61711b301f76d4042 100644 --- a/web/modules/simple_sitemap/simple_sitemap.info.yml +++ b/web/modules/simple_sitemap/simple_sitemap.info.yml @@ -6,7 +6,7 @@ package: SEO core: 8.x core_version_requirement: ^8 || ^9 -# Information added by Drupal.org packaging script on 2020-06-16 -version: '8.x-3.7' +# Information added by Drupal.org packaging script on 2020-11-12 +version: '8.x-3.8' project: 'simple_sitemap' -datestamp: 1592298876 +datestamp: 1605141361 diff --git a/web/modules/simple_sitemap/simple_sitemap.module b/web/modules/simple_sitemap/simple_sitemap.module index edf3b81a871638609de4b3a5c52f657c737bd752..dacec83fb36cb515f4fa8b4440fa08d9e445be35 100644 --- a/web/modules/simple_sitemap/simple_sitemap.module +++ b/web/modules/simple_sitemap/simple_sitemap.module @@ -33,6 +33,8 @@ function simple_sitemap_help($route_name, RouteMatchInterface $route_match) { * @param $form * @param \Drupal\Core\Form\FormStateInterface $form_state * @param $form_id + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException */ function simple_sitemap_form_alter(&$form, FormStateInterface $form_state, $form_id) { @@ -80,6 +82,9 @@ function simple_sitemap_form_alter(&$form, FormStateInterface $form_state, $form * * @param $form * @param \Drupal\Core\Form\FormStateInterface $form_state + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException */ function simple_sitemap_entity_form_submit($form, FormStateInterface &$form_state) { @@ -94,7 +99,6 @@ function simple_sitemap_entity_form_submit($form, FormStateInterface &$form_stat // Fix for values appearing in a sub array on a commerce product entity. $values = isset($values['simple_sitemap']) ? $values['simple_sitemap'] : $values; - // Only make changes in DB if sitemap settings actually changed. if ($f->valuesChanged($form, $values)) { /** @var \Drupal\simple_sitemap\Simplesitemap $generator */ @@ -113,16 +117,12 @@ function simple_sitemap_entity_form_submit($form, FormStateInterface &$form_stat $generator->setVariants($variant); switch ($f->getEntityCategory()) { - case 'bundle': $generator->setBundleSettings($f->getEntityTypeId(), $f->getBundleName(), $settings); - if (empty($settings['index'])) { - $generator->removeEntityInstanceSettings($f->getEntityTypeId(), $f->getInstanceId()); - } break; case 'instance': - if (!$f->entityIsNew()) { + if (!$f->entityIsNew()) { // Make sure the entity is saved first for multi-step forms, see https://www.drupal.org/project/simple_sitemap/issues/3080510. $generator->setEntityInstanceSettings($f->getEntityTypeId(), $f->getInstanceId(), $settings); } break; @@ -211,6 +211,8 @@ function simple_sitemap_entity_delete(EntityInterface $entity) { * * @param string $entity_type_id * @param string $bundle + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException */ function simple_sitemap_entity_bundle_delete($entity_type_id, $bundle) { @@ -225,6 +227,8 @@ function simple_sitemap_entity_bundle_delete($entity_type_id, $bundle) { * Removes settings for the removed menu. * * @param \Drupal\system\MenuInterface $menu + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException */ function simple_sitemap_menu_delete(MenuInterface $menu) { diff --git a/web/modules/simple_sitemap/simple_sitemap.services.yml b/web/modules/simple_sitemap/simple_sitemap.services.yml index 1e28807b4a1ab76624eb14c6ce80b24a5b125ab5..1b6ce081ef8f3b4540a339d04e2d619d4816895b 100644 --- a/web/modules/simple_sitemap/simple_sitemap.services.yml +++ b/web/modules/simple_sitemap/simple_sitemap.services.yml @@ -40,6 +40,7 @@ services: - '@state' - '@simple_sitemap.queue' - '@simple_sitemap.logger' + - '@module_handler' simple_sitemap.queue: class: Drupal\simple_sitemap\Queue\SimplesitemapQueue diff --git a/web/modules/simple_sitemap/src/Form/FormHelper.php b/web/modules/simple_sitemap/src/Form/FormHelper.php index c0d3d56ec657425266a74bfdff3f01eadbdd355e..ca0e69e1a0bc4a2115770872f665a96421831af3 100644 --- a/web/modules/simple_sitemap/src/Form/FormHelper.php +++ b/web/modules/simple_sitemap/src/Form/FormHelper.php @@ -126,8 +126,10 @@ public function __construct( public function processForm(FormStateInterface $form_state) { $this->formState = $form_state; $this->cleanUpFormInfo(); - $this->getEntityDataFromFormEntity(); - $this->negotiateSettings(); + + if ($this->getEntityDataFromFormEntity()) { + $this->negotiateSettings(); + } return $this->supports(); } @@ -204,18 +206,18 @@ public function getInstanceId() { */ protected function supports() { - // Do not alter the form if user lacks certain permissions. - if (!$this->currentUser->hasPermission('administer sitemap settings')) { + // Do not alter the form if it is irrelevant to sitemap generation. + if (empty($this->getEntityCategory())) { return FALSE; } - // Do not alter the form if it is irrelevant to sitemap generation. - elseif (empty($this->getEntityCategory())) { + // Do not alter the form if user lacks certain permissions. + if (!$this->currentUser->hasPermission('administer sitemap settings')) { return FALSE; } // Do not alter the form if entity is not enabled in sitemap settings. - elseif (!$this->generator->entityTypeIsEnabled($this->getEntityTypeId())) { + if (!$this->generator->entityTypeIsEnabled($this->getEntityTypeId())) { return FALSE; } @@ -401,7 +403,11 @@ protected function getEntityDataFromFormEntity() { } // Menu fix. - $this->setEntityCategory(NULL === $this->getEntityCategory() && $entity_type_id === 'menu' ? 'bundle' : $this->getEntityCategory()); + $this->setEntityCategory( + NULL === $this->getEntityCategory() && $entity_type_id === 'menu' + ? 'bundle' + : $this->getEntityCategory() + ); switch ($this->getEntityCategory()) { case 'bundle': @@ -469,6 +475,8 @@ public function cleanUpFormInfo() { * * @return bool * TRUE if simple_sitemap form values have been altered by the user. + * + * @todo Make it work with variants. */ public function valuesChanged($form, array $values) { // foreach (self::$valuesToCheck as $field_name) { @@ -480,7 +488,6 @@ public function valuesChanged($form, array $values) { // // return FALSE; - //todo return TRUE; } diff --git a/web/modules/simple_sitemap/src/Plugin/simple_sitemap/SitemapGenerator/DefaultSitemapGenerator.php b/web/modules/simple_sitemap/src/Plugin/simple_sitemap/SitemapGenerator/DefaultSitemapGenerator.php index 1c95134a430d2dcb7304d8cc5d3fcb9319593752..abf6e39e6d02d20881bafb2a7a8c6e8adc6dc467 100755 --- a/web/modules/simple_sitemap/src/Plugin/simple_sitemap/SitemapGenerator/DefaultSitemapGenerator.php +++ b/web/modules/simple_sitemap/src/Plugin/simple_sitemap/SitemapGenerator/DefaultSitemapGenerator.php @@ -2,8 +2,6 @@ namespace Drupal\simple_sitemap\Plugin\simple_sitemap\SitemapGenerator; -use Symfony\Component\DependencyInjection\ContainerInterface; - /** * Class DefaultSitemapGenerator * @package Drupal\simple_sitemap\Plugin\simple_sitemap\SitemapGenerator @@ -83,8 +81,6 @@ protected function addSitemapAttributes() { * @param array $links */ protected function addLinks(array $links) { - $sitemap_variant = $this->sitemapVariant; - $this->moduleHandler->alter('simple_sitemap_links', $links, $sitemap_variant); foreach ($links as $url_data) { $this->writer->startElement('url'); $this->addUrl($url_data); diff --git a/web/modules/simple_sitemap/src/Plugin/simple_sitemap/SitemapGenerator/SitemapGeneratorBase.php b/web/modules/simple_sitemap/src/Plugin/simple_sitemap/SitemapGenerator/SitemapGeneratorBase.php index 7ca401fb6e59732803197f12cd43903dc64edd54..da9704f695212bd080d9629f8c17acc550808023 100644 --- a/web/modules/simple_sitemap/src/Plugin/simple_sitemap/SitemapGenerator/SitemapGeneratorBase.php +++ b/web/modules/simple_sitemap/src/Plugin/simple_sitemap/SitemapGenerator/SitemapGeneratorBase.php @@ -3,7 +3,6 @@ namespace Drupal\simple_sitemap\Plugin\simple_sitemap\SitemapGenerator; use Drupal\Core\Url; -use Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationUrl; use Drupal\simple_sitemap\Plugin\simple_sitemap\SimplesitemapPluginBase; use Symfony\Component\DependencyInjection\ContainerInterface; use Drupal\Core\Database\Connection; @@ -346,16 +345,41 @@ public function getSitemapUrl($delta = NULL) { } /** + * Determines if the sitemap is to be a multilingual sitemap based on several + * factors. + * + * A hreflang/multilingual sitemap is only wanted if there are indexable + * languages available and if there is a language negotiation method enabled + * that is based on URL discovery. Any other language negotiation methods + * should be irrelevant, as a sitemap can only use URLs to guide to the + * correct language. + * + * @see https://www.drupal.org/project/simple_sitemap/issues/3154570#comment-13730522 + * * @return bool */ public static function isMultilingualSitemap() { + if (!\Drupal::moduleHandler()->moduleExists('language')) { + return FALSE; + } + + /** @var \Drupal\language\LanguageNegotiatorInterface $language_negotiator */ + $language_negotiator = \Drupal::service('language_negotiator'); + + $url_negotiation_method_enabled = FALSE; + foreach ($language_negotiator->getNegotiationMethods(LanguageInterface::TYPE_URL) as $method) { + if ($language_negotiator->isNegotiationMethodEnabled($method['id'])) { + $url_negotiation_method_enabled = TRUE; + break; + } + } + $has_multiple_indexable_languages = count( array_diff_key(\Drupal::languageManager()->getLanguages(), \Drupal::service('simple_sitemap.generator')->getSetting('excluded_languages', [])) ) > 1; - return $has_multiple_indexable_languages - && \Drupal::service('language_negotiator')->isNegotiationMethodEnabled(LanguageNegotiationUrl::METHOD_ID); + return $url_negotiation_method_enabled && $has_multiple_indexable_languages; } } diff --git a/web/modules/simple_sitemap/src/Plugin/simple_sitemap/SitemapGenerator/SitemapWriter.php b/web/modules/simple_sitemap/src/Plugin/simple_sitemap/SitemapGenerator/SitemapWriter.php index 36b52dbeea0ea9882d823ce38da088e3b876ea37..bfec3e9c30b1f0b0c1974caf50051408e222342b 100644 --- a/web/modules/simple_sitemap/src/Plugin/simple_sitemap/SitemapGenerator/SitemapWriter.php +++ b/web/modules/simple_sitemap/src/Plugin/simple_sitemap/SitemapGenerator/SitemapWriter.php @@ -2,7 +2,7 @@ namespace Drupal\simple_sitemap\Plugin\simple_sitemap\SitemapGenerator; -use Drupal\Core\Routing\RouteProvider; +use Drupal\Core\Routing\RouteProviderInterface; /** * Class SitemapWriter @@ -19,7 +19,7 @@ class SitemapWriter extends \XMLWriter { */ protected $routeProvider; - public function __construct(RouteProvider $route_provider) { + public function __construct(RouteProviderInterface $route_provider) { $this->routeProvider = $route_provider; } @@ -27,13 +27,21 @@ public function __construct(RouteProvider $route_provider) { * Adds the XML stylesheet to the XML page. */ public function writeXsl() { - // Use this instead of URL::fromRoute() to avoid creating a URL with the + // Using this instead of URL::fromRoute() to avoid creating a path with the // subdomain from which creation was triggered which might lead to a CORS // problem. See https://www.drupal.org/project/simple_sitemap/issues/3131672. $xsl_url = $this->routeProvider ->getRouteByName('simple_sitemap.sitemap_xsl') ->getPath(); + // The above workaround however generates an incorrect path when the site is + // located in a subdirectory, which is why the following logic adds the base + // path of the installation. + // See https://www.drupal.org/project/simple_sitemap/issues/3154494. + // All of this seems to be an over engineered way of writing 'sitemap.xsl', + // but may be useful in cases where another module alters the routes. + $xsl_url = base_path() . ltrim($xsl_url, '/'); + $this->writePI('xml-stylesheet', 'type="text/xsl" href="' . $xsl_url . '"'); } diff --git a/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/CustomUrlGenerator.php b/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/CustomUrlGenerator.php old mode 100755 new mode 100644 index 2bb641f80ba63768b1da5c683579e248bc6829da..0c31d784c3f0c913a3aefac6bfcb4e367bf8589f --- a/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/CustomUrlGenerator.php +++ b/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/CustomUrlGenerator.php @@ -113,7 +113,7 @@ protected function processDataSet($data_set) { return FALSE; } - $url_object = Url::fromUserInput($data_set['path'], ['absolute' => TRUE]); + $url_object = Url::fromUserInput($data_set['path'])->setAbsolute(); $path = $url_object->getInternalPath(); $entity = $this->entityHelper->getEntityFromUrlObject($url_object); @@ -133,7 +133,7 @@ protected function processDataSet($data_set) { ]; // Additional info useful in hooks. - if (NULL !== $entity) { + if (!empty($entity)) { $path_data['meta']['entity_info'] = [ 'entity_type' => $entity->getEntityTypeId(), 'id' => $entity->id(), diff --git a/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/EntityMenuLinkContentUrlGenerator.php b/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/EntityMenuLinkContentUrlGenerator.php old mode 100755 new mode 100644 index 921bfba00471a39768dd5cc415ef047e46fe252e..ca136153672718aab164c2e7081288509ee66779 --- a/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/EntityMenuLinkContentUrlGenerator.php +++ b/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/EntityMenuLinkContentUrlGenerator.php @@ -129,7 +129,7 @@ protected function processDataSet($data_set) { return FALSE; } - $url_object = $data_set->getUrlObject(); + $url_object = $data_set->getUrlObject()->setAbsolute(); // Do not include external paths. if ($url_object->isExternal()) { @@ -174,8 +174,6 @@ protected function processDataSet($data_set) { } } - $url_object->setOption('absolute', TRUE); - $entity = $this->entityHelper->getEntityFromUrlObject($url_object); $path_data = [ diff --git a/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/EntityUrlGenerator.php b/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/EntityUrlGenerator.php old mode 100755 new mode 100644 index efa2abfe6c720fd2572bab1c71eec59c6eae5608..7889994f8704b0c9fa2271045c6b63428083e677 --- a/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/EntityUrlGenerator.php +++ b/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/EntityUrlGenerator.php @@ -142,28 +142,21 @@ protected function processDataSet($data_set) { return FALSE; } - $entity_id = $entity->id(); - $entity_type_name = $entity->getEntityTypeId(); - $entity_settings = $this->generator ->setVariants($this->sitemapVariant) - ->getEntityInstanceSettings($entity_type_name, $entity_id); + ->getEntityInstanceSettings($entity->getEntityTypeId(), $entity->id()); if (empty($entity_settings['index'])) { return FALSE; } - $url_object = $entity->toUrl(); + $url_object = $entity->toUrl()->setAbsolute(); // Do not include external paths. if (!$url_object->isRouted()) { return FALSE; } - $path = $url_object->getInternalPath(); - - $url_object->setOption('absolute', TRUE); - return [ 'url' => $url_object, 'lastmod' => method_exists($entity, 'getChangedTime') ? date('c', $entity->getChangedTime()) : NULL, @@ -175,12 +168,32 @@ protected function processDataSet($data_set) { // Additional info useful in hooks. 'meta' => [ - 'path' => $path, + 'path' => $url_object->getInternalPath(), 'entity_info' => [ - 'entity_type' => $entity_type_name, - 'id' => $entity_id, + 'entity_type' => $entity->getEntityTypeId(), + 'id' => $entity->id(), ], ] ]; } + + /** + * @inheritdoc + * + * Make sure to clear entity cache so it does not build up resulting in a + * constant increase of memory. + * + * See https://www.drupal.org/project/simple_sitemap/issues/3170261. + */ + public function generate($data_set) { + $result = parent::generate($data_set); + + $storage = $this->entityTypeManager->getStorage($data_set['entity_type']); + if (method_exists($storage, 'resetCache')) { + $storage->resetCache([$data_set['id']]); + } + + return $result; + } + } diff --git a/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/EntityUrlGeneratorBase.php b/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/EntityUrlGeneratorBase.php old mode 100755 new mode 100644 index 7ef4e9e2e33c1f2e092b8fef23372640a54e50c2..8dd8e4281237bbc386cdc5077b13d81b832bc8e0 --- a/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/EntityUrlGeneratorBase.php +++ b/web/modules/simple_sitemap/src/Plugin/simple_sitemap/UrlGenerator/EntityUrlGeneratorBase.php @@ -4,7 +4,7 @@ use Drupal\simple_sitemap\Plugin\simple_sitemap\SitemapGenerator\SitemapGeneratorBase; use Symfony\Component\DependencyInjection\ContainerInterface; -use Drupal\Core\Entity\ContentEntityBase; +use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Url; use Drupal\file\Entity\File; use Drupal\simple_sitemap\EntityHelper; @@ -110,9 +110,9 @@ protected function getUrlVariants(array $path_data, Url $url_object) { $alternate_urls = $this->getAlternateUrlsForDefaultLanguage($url_object); } elseif ($this->settings['skip_untranslated'] - && ($entity = $this->entityHelper->getEntityFromUrlObject($url_object)) instanceof ContentEntityBase) { + && ($entity = $this->entityHelper->getEntityFromUrlObject($url_object)) instanceof ContentEntityInterface) { - /** @var ContentEntityBase $entity */ + /** @var ContentEntityInterface $entity */ $translation_languages = $entity->getTranslationLanguages(); if (isset($translation_languages[Language::LANGCODE_NOT_SPECIFIED]) || isset($translation_languages[Language::LANGCODE_NOT_APPLICABLE])) { @@ -149,7 +149,7 @@ protected function getAlternateUrlsForDefaultLanguage(Url $url_object) { $alternate_urls = []; if ($url_object->access($this->anonUser)) { $alternate_urls[$this->defaultLanguageId] = $this->replaceBaseUrlWithCustom($url_object - ->setOption('language', $this->languages[$this->defaultLanguageId])->toString() + ->setAbsolute()->setOption('language', $this->languages[$this->defaultLanguageId])->toString() ); } @@ -157,11 +157,11 @@ protected function getAlternateUrlsForDefaultLanguage(Url $url_object) { } /** - * @param \Drupal\Core\Entity\ContentEntityBase $entity + * @param \Drupal\Core\Entity\ContentEntityInterface $entity * @param \Drupal\Core\Url $url_object * @return array */ - protected function getAlternateUrlsForTranslatedLanguages(ContentEntityBase $entity, Url $url_object) { + protected function getAlternateUrlsForTranslatedLanguages(ContentEntityInterface $entity, Url $url_object) { $alternate_urls = []; /** @var Language $language */ @@ -169,7 +169,7 @@ protected function getAlternateUrlsForTranslatedLanguages(ContentEntityBase $ent if (!isset($this->settings['excluded_languages'][$language->getId()]) || $language->isDefault()) { if ($entity->getTranslation($language->getId())->access('view', $this->anonUser)) { $alternate_urls[$language->getId()] = $this->replaceBaseUrlWithCustom($url_object - ->setOption('language', $language)->toString() + ->setAbsolute()->setOption('language', $language)->toString() ); } } @@ -188,7 +188,7 @@ protected function getAlternateUrlsForAllLanguages(Url $url_object) { foreach ($this->languages as $language) { if (!isset($this->settings['excluded_languages'][$language->getId()]) || $language->isDefault()) { $alternate_urls[$language->getId()] = $this->replaceBaseUrlWithCustom($url_object - ->setOption('language', $language)->toString() + ->setAbsolute()->setOption('language', $language)->toString() ); } } @@ -215,11 +215,11 @@ public function generate($data_set) { } /** - * @param \Drupal\Core\Entity\ContentEntityBase $entity + * @param \Drupal\Core\Entity\ContentEntityInterface $entity * * @return array */ - protected function getEntityImageData(ContentEntityBase $entity) { + protected function getEntityImageData(ContentEntityInterface $entity) { $image_data = []; foreach ($entity->getFieldDefinitions() as $field) { if ($field->getType() === 'image') { diff --git a/web/modules/simple_sitemap/src/Queue/QueueWorker.php b/web/modules/simple_sitemap/src/Queue/QueueWorker.php index 0f01394a80cc11cffdf3b4349889b56a71be58ed..562ef91d40241076bf173b110409ad56f88b7f68 100644 --- a/web/modules/simple_sitemap/src/Queue/QueueWorker.php +++ b/web/modules/simple_sitemap/src/Queue/QueueWorker.php @@ -3,13 +3,13 @@ namespace Drupal\simple_sitemap\Queue; use Drupal\Component\Utility\Timer; +use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\simple_sitemap\Plugin\simple_sitemap\SitemapGenerator\SitemapGeneratorBase; use Drupal\simple_sitemap\SimplesitemapSettings; use Drupal\simple_sitemap\SimplesitemapManager; use Drupal\Core\State\StateInterface; use Drupal\simple_sitemap\Logger; - class QueueWorker { use BatchTrait; @@ -46,6 +46,11 @@ class QueueWorker { */ protected $logger; + /** + * @var \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected $moduleHandler; + /** * @var string|null */ @@ -61,6 +66,11 @@ class QueueWorker { */ protected $results = []; + /** + * @var array + */ + protected $processedResults = []; + /** * @var array */ @@ -93,35 +103,20 @@ class QueueWorker { * @param \Drupal\Core\State\StateInterface $state * @param \Drupal\simple_sitemap\Queue\SimplesitemapQueue $element_queue * @param \Drupal\simple_sitemap\Logger $logger + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler */ public function __construct(SimplesitemapSettings $settings, SimplesitemapManager $manager, StateInterface $state, SimplesitemapQueue $element_queue, - Logger $logger) { + Logger $logger, + ModuleHandlerInterface $module_handler) { $this->settings = $settings; $this->manager = $manager; $this->state = $state; $this->queue = $element_queue; $this->logger = $logger; - } - - /** - * @return $this - */ - public function deleteQueue() { - $this->queue->deleteQueue(); - SitemapGeneratorBase::purgeSitemapVariants(NULL, 'unpublished'); - $this->variantProcessedNow = NULL; - $this->generatorProcessedNow = NULL; - $this->results = []; - $this->processedPaths = []; - $this->state->set('simple_sitemap.queue_items_initial_amount', 0); - $this->state->delete('simple_sitemap.queue_stashed_results'); - $this->elementsTotal = NULL; - $this->elementsRemaining = NULL; - - return $this; + $this->moduleHandler = $module_handler; } /** @@ -305,23 +300,32 @@ protected function removeDuplicates(&$results) { */ protected function generateVariantChunksFromResults($complete = FALSE) { if (!empty($this->results)) { - $generator = $this->manager->getSitemapGenerator($this->generatorProcessedNow) - ->setSitemapVariant($this->variantProcessedNow) - ->setSettings($this->generatorSettings); + $processed_results = $this->results; + $this->moduleHandler->alter('simple_sitemap_links', $processed_results, $this->variantProcessedNow); + $this->processedResults = array_merge($this->processedResults, $processed_results); + $this->results = []; + } - if (empty($this->maxLinks) || $complete) { - $generator->generate($this->results); - $this->results = []; - } - else { - foreach (array_chunk($this->results, $this->maxLinks, TRUE) as $chunk_links) { - if (count($chunk_links) === $this->maxLinks || $complete) { - $generator->generate($chunk_links); - $this->results = array_diff_key($this->results, $chunk_links); - } + if (empty($this->processedResults)) { + return; + } + + $generator = $this->manager->getSitemapGenerator($this->generatorProcessedNow) + ->setSitemapVariant($this->variantProcessedNow) + ->setSettings($this->generatorSettings); + + if (!empty($this->maxLinks)) { + foreach (array_chunk($this->processedResults, $this->maxLinks, TRUE) as $chunk_links) { + if ($complete || count($chunk_links) === $this->maxLinks) { + $generator->generate($chunk_links); + $this->processedResults = array_diff_key($this->processedResults, $chunk_links); } } } + else { + $generator->generate($this->processedResults); + $this->processedResults = []; + } } protected function publishCurrentVariant() { @@ -334,23 +338,45 @@ protected function publishCurrentVariant() { } } + protected function resetWorker() { + $this->results = []; + $this->processedPaths = []; + $this->processedResults = []; + $this->variantProcessedNow = NULL; + $this->generatorProcessedNow = NULL; + $this->elementsTotal = NULL; + $this->elementsRemaining = NULL; + } + + /** + * @return $this + */ + public function deleteQueue() { + $this->queue->deleteQueue(); + SitemapGeneratorBase::purgeSitemapVariants(NULL, 'unpublished'); + $this->state->set('simple_sitemap.queue_items_initial_amount', 0); + $this->state->delete('simple_sitemap.queue_stashed_results'); + $this->resetWorker(); + + return $this; + } + protected function stashResults() { $this->state->set('simple_sitemap.queue_stashed_results', [ 'variant' => $this->variantProcessedNow, 'generator' => $this->generatorProcessedNow, 'results' => $this->results, + 'processed_results' => $this->processedResults, 'processed_paths' => $this->processedPaths, ]); - $this->results = []; - $this->processedPaths = []; - $this->generatorProcessedNow = NULL; - $this->variantProcessedNow = NULL; + $this->resetWorker(); } protected function unstashResults() { if (NULL !== $results = $this->state->get('simple_sitemap.queue_stashed_results')) { $this->state->delete('simple_sitemap.queue_stashed_results'); $this->results = !empty($results['results']) ? $results['results'] : []; + $this->processedResults = !empty($results['processed_results']) ? $results['processed_results'] : []; $this->processedPaths = !empty($results['processed_paths']) ? $results['processed_paths'] : []; $this->variantProcessedNow = $results['variant']; $this->generatorProcessedNow = $results['generator']; @@ -381,7 +407,9 @@ public function getQueuedElementCount($force_recount = FALSE) { * @return int */ public function getStashedResultCount() { - return count($this->state->get('simple_sitemap.queue_stashed_results', ['results' => []])['results']); + $results = $this->state->get('simple_sitemap.queue_stashed_results', []); + return (!empty($results['results']) ? count($results['results']) : 0) + + (!empty($results['processed_results']) ? count($results['processed_results']) : 0); } /** diff --git a/web/modules/simple_sitemap/src/Simplesitemap.php b/web/modules/simple_sitemap/src/Simplesitemap.php index eed94bffd9466bc0bf24369132e8c4cb09a4a2ef..a3782eda4633f4d217906bacf714a4934a9abc7b 100644 --- a/web/modules/simple_sitemap/src/Simplesitemap.php +++ b/web/modules/simple_sitemap/src/Simplesitemap.php @@ -466,8 +466,19 @@ public function setBundleSettings($entity_type_id, $bundle_name = NULL, $setting } $bundle_settings->save(); + if (empty($entity_ids = $this->entityHelper->getEntityInstanceIds($entity_type_id, $bundle_name))) { + return $this; + } + + // Delete all entity overrides in case bundle indexation is disabled. + if (empty($settings['index'])) { + $this->removeEntityInstanceSettings($entity_type_id, $entity_ids); + + return $this; + } + // Delete entity overrides which are identical to new bundle settings. - $entity_ids = $this->entityHelper->getEntityInstanceIds($entity_type_id, $bundle_name); + // todo Enclose into some sensible method. $query = $this->db->select('simple_sitemap_entity_overrides', 'o') ->fields('o', ['id', 'inclusion_settings']) ->condition('o.entity_type', $entity_type_id) @@ -491,6 +502,8 @@ public function setBundleSettings($entity_type_id, $bundle_name = NULL, $setting } } if (!empty($delete_instances)) { + + // todo Use removeEntityInstanceSettings() instead. $this->db->delete('simple_sitemap_entity_overrides') ->condition('id', $delete_instances, 'IN') ->execute(); @@ -522,7 +535,6 @@ public function setBundleSettings($entity_type_id, $bundle_name = NULL, $setting * entity type does not exist. */ public function getBundleSettings($entity_type_id = NULL, $bundle_name = NULL, $supplement_defaults = TRUE, $multiple_variants = FALSE) { - $bundle_name = NULL !== $bundle_name ? $bundle_name : $entity_type_id; $all_bundle_settings = []; @@ -595,8 +607,9 @@ public function removeBundleSettings($entity_type_id = NULL, $bundle_name = NULL ->getEditable("simple_sitemap.bundle_settings.$variant.$entity_type_id.$bundle_name")->delete(); } - $entity_ids = $this->entityHelper->getEntityInstanceIds($entity_type_id, $bundle_name); - $this->removeEntityInstanceSettings($entity_type_id, (empty($entity_ids) ? NULL : $entity_ids)); + if (!empty($entity_ids = $this->entityHelper->getEntityInstanceIds($entity_type_id, $bundle_name))) { + $this->removeEntityInstanceSettings($entity_type_id, $entity_ids); + } } else { foreach ($variants as $variant) { diff --git a/web/modules/simple_sitemap/src/SimplesitemapManager.php b/web/modules/simple_sitemap/src/SimplesitemapManager.php index 6a48d5cd71b9d6884041c7df5fea3b99b52f9694..a2fe3f50e5184afd1e38f9b7f9043d933d9181d8 100644 --- a/web/modules/simple_sitemap/src/SimplesitemapManager.php +++ b/web/modules/simple_sitemap/src/SimplesitemapManager.php @@ -135,20 +135,22 @@ public function getSitemapTypes() { */ public function getSitemapVariants($sitemap_type = NULL, $attach_type_info = TRUE) { if (NULL === $sitemap_type) { - $variants = []; + $variants_by_type = []; foreach ($this->configFactory->listAll('simple_sitemap.variants.') as $config_name) { - $config_name_parts = explode('.', $config_name); - $saved_variants = $this->configFactory->get($config_name)->get('variants'); - $saved_variants = $attach_type_info ? $this->attachSitemapTypeToVariants($saved_variants, $config_name_parts[2]) : $saved_variants; - $variants = array_merge($variants, (is_array($saved_variants) ? $saved_variants : [])); + $variants = !empty($variants = $this->configFactory->get($config_name)->get('variants')) ? $variants : []; + $variants = $attach_type_info ? $this->attachSitemapTypeToVariants($variants, explode('.', $config_name)[2]) : $variants; + $variants_by_type[] = $variants; } + $variants = array_merge([], ...$variants_by_type); } else { - $variants = $this->configFactory->get("simple_sitemap.variants.$sitemap_type")->get('variants'); - $variants = is_array($variants) ? $variants : []; + $variants = !empty($variants = $this->configFactory->get("simple_sitemap.variants.$sitemap_type")->get('variants')) ? $variants : []; $variants = $attach_type_info ? $this->attachSitemapTypeToVariants($variants, $sitemap_type) : $variants; } - array_multisort(array_column($variants, "weight"), SORT_ASC, $variants); + + // Sort variants by weight. + $variant_weights = array_column($variants, 'weight'); + array_multisort($variant_weights, SORT_ASC, $variants); return $variants; } diff --git a/web/modules/simple_sitemap/tests/src/Functional/SimplesitemapTest.php b/web/modules/simple_sitemap/tests/src/Functional/SimplesitemapTest.php index d5e18d76b497301eedc8da1072312e789f594275..1b2ad00a51ad65c000ab49e8443fdabf6c4d06ec 100644 --- a/web/modules/simple_sitemap/tests/src/Functional/SimplesitemapTest.php +++ b/web/modules/simple_sitemap/tests/src/Functional/SimplesitemapTest.php @@ -344,13 +344,7 @@ public function testSetEntityInstanceSettings() { $this->assertSession()->responseContains('<option value="never" selected="selected">never</option>'); // Test database changes. - $result = $this->database->select('simple_sitemap_entity_overrides', 'o') - ->fields('o', ['inclusion_settings']) - ->condition('o.entity_type', 'node') - ->condition('o.entity_id', $this->node->id()) - ->execute() - ->fetchField(); - $this->assertNotEmpty($result); + $this->assertEquals(1, $this->getOverridesCount('node', $this->node->id())); $this->generator->setBundleSettings('node', 'page', ['priority' => 0.1, 'changefreq' => 'never']) ->generateSitemap(QueueWorker::GENERATE_TYPE_BACKEND); @@ -370,13 +364,47 @@ public function testSetEntityInstanceSettings() { // Test if entity override has been removed from database after its equal to // its bundle settings. - $result = $this->database->select('simple_sitemap_entity_overrides', 'o') + $this->assertEquals(0, $this->getOverridesCount('node', $this->node->id())); + + // Assert that creating a new content type doesn't remove the overrides. + $this->drupalGet('node/' . $this->node->id() . '/edit'); + $this->submitForm(['index_default_node_settings' => 0], 'Save'); + $this->assertEquals(1, $this->getOverridesCount('node', $this->node->id())); + // Create a new content type. + $this->drupalGet('admin/structure/types/add'); + $this->submitForm([ + 'name' => 'simple_sitemap_type', + 'type' => 'simple_sitemap_type', + 'index_default_node_settings' => 0, + ], 'Save content type'); + // The entity override from the other content type should not be affected. + $this->assertEquals(1, $this->getOverridesCount('node', $this->node->id())); + + // Assert that removing the other content type doesn't remove the overrides. + $this->drupalGet('admin/structure/types/manage/simple_sitemap_type/delete'); + $this->submitForm([], 'Delete'); + $this->assertEquals(1, $this->getOverridesCount('node', $this->node->id())); + } + + /** + * Returns the number of entity overrides for the given entity type/ID. + * + * @param string $entity_type_id + * The entity type ID. + * @param string $entity_id + * The entity ID. + * + * @return int + * The number of overrides for the given entity type ID and entity ID. + */ + protected function getOverridesCount($entity_type_id, $entity_id) { + return $this->database->select('simple_sitemap_entity_overrides', 'o') ->fields('o', ['inclusion_settings']) - ->condition('o.entity_type', 'node') - ->condition('o.entity_id', $this->node->id()) + ->condition('o.entity_type', $entity_type_id) + ->condition('o.entity_id', $entity_id) + ->countQuery() ->execute() ->fetchField(); - $this->assertEmpty($result); } /** @@ -384,7 +412,7 @@ public function testSetEntityInstanceSettings() { */ public function testNewEntityWithIdSet() { $new_node = Node::create([ - 'nid' => rand(5, 10), + 'nid' => mt_rand(5, 10), 'type' => 'page', ]); // Assert that the form does not break if an entity has an id but is not