diff --git a/composer.json b/composer.json index da32471557c6ac54574f3eb1386509089eb42f61..1d2f895d81c5c5f5fc6044021c54ebddb4d7d092 100644 --- a/composer.json +++ b/composer.json @@ -107,7 +107,7 @@ "drupal/console": "1.9.7", "drupal/content_access": "1.0-alpha3", "drupal/core-composer-scaffold": "^9.0", - "drupal/core-recommended": "9.5.3", + "drupal/core-recommended": "9.5.5", "drupal/crop": "2.3", "drupal/ctools": "3.13", "drupal/dropzonejs": "2.7", diff --git a/composer.lock b/composer.lock index 73bbde26a634f4f7b5c7c6175a57f01cb337e18d..f9ca9a4d828cc2d41f44c368e5b8bcf4c8056a24 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": "87628b41be55de273fcabd1a1179e831", + "content-hash": "34d6f8d40b4e8320b4d8293429038bea", "packages": [ { "name": "alchemy/zippy", @@ -3014,16 +3014,16 @@ }, { "name": "drupal/core", - "version": "9.5.3", + "version": "9.5.5", "source": { "type": "git", "url": "https://github.com/drupal/core.git", - "reference": "67e34a5e8f48cafdd5c26e778a9570860e2d44a5" + "reference": "eae5e76a8b403cbd42b3465f567313b52d78b0dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/drupal/core/zipball/67e34a5e8f48cafdd5c26e778a9570860e2d44a5", - "reference": "67e34a5e8f48cafdd5c26e778a9570860e2d44a5", + "url": "https://api.github.com/repos/drupal/core/zipball/eae5e76a8b403cbd42b3465f567313b52d78b0dc", + "reference": "eae5e76a8b403cbd42b3465f567313b52d78b0dc", "shasum": "" }, "require": { @@ -3175,9 +3175,9 @@ ], "description": "Drupal is an open source content management platform powering millions of websites and applications.", "support": { - "source": "https://github.com/drupal/core/tree/9.5.3" + "source": "https://github.com/drupal/core/tree/9.5.5" }, - "time": "2023-02-01T19:47:31+00:00" + "time": "2023-03-15T14:30:25+00:00" }, { "name": "drupal/core-composer-scaffold", @@ -3231,16 +3231,16 @@ }, { "name": "drupal/core-recommended", - "version": "9.5.3", + "version": "9.5.5", "source": { "type": "git", "url": "https://github.com/drupal/core-recommended.git", - "reference": "3dc8d9757c6c4a0457d32dd58a755532504ad959" + "reference": "3c1d205349407e706cc89f56aa34448742fe81b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/drupal/core-recommended/zipball/3dc8d9757c6c4a0457d32dd58a755532504ad959", - "reference": "3dc8d9757c6c4a0457d32dd58a755532504ad959", + "url": "https://api.github.com/repos/drupal/core-recommended/zipball/3c1d205349407e706cc89f56aa34448742fe81b4", + "reference": "3c1d205349407e706cc89f56aa34448742fe81b4", "shasum": "" }, "require": { @@ -3249,7 +3249,7 @@ "doctrine/annotations": "~1.13.3", "doctrine/lexer": "~1.2.3", "doctrine/reflection": "~1.2.3", - "drupal/core": "9.5.3", + "drupal/core": "9.5.5", "egulias/email-validator": "~3.2.1", "guzzlehttp/guzzle": "~6.5.8", "guzzlehttp/promises": "~1.5.2", @@ -3311,9 +3311,9 @@ ], "description": "Core and its dependencies with known-compatible minor versions. Require this project INSTEAD OF drupal/core.", "support": { - "source": "https://github.com/drupal/core-recommended/tree/9.5.3" + "source": "https://github.com/drupal/core-recommended/tree/9.5.5" }, - "time": "2023-02-01T19:47:31+00:00" + "time": "2023-03-15T14:30:25+00:00" }, { "name": "drupal/crop", diff --git a/scripts/tmux-parallel-push.sh b/scripts/tmux-parallel-push.sh index df7193fb7e4a180cc165fb0803656f5778a0e985..a89b1781c90c5a458b80942de2d0da0a6cb27b0b 100755 --- a/scripts/tmux-parallel-push.sh +++ b/scripts/tmux-parallel-push.sh @@ -40,7 +40,7 @@ terminus org:site:list ohio-state-arts-and-sciences --upstream=5161e51a-0e4a-414c-974e-8565094b76b1 --tag=D8 --fields=name --format=string | sort | tee $LOG_DIR/site_list.txt; -parallel --delay 0.2 --tmuxpane --fg -j 36 -a $LOG_DIR/site_list.txt "scripts/deploy-site-env.sh {}.$ENV $DEPLOY_MSG 2>&1 | tee $LOG_DIR/{}.log"; +parallel --delay 0.2 -j 36 -a $LOG_DIR/site_list.txt "scripts/deploy-site-env.sh {}.$ENV $DEPLOY_MSG 2>&1 | tee $LOG_DIR/{}.log"; echo; echo; echo "Sleeping for 11 seconds..."; echo; echo; sleep 11; diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index d3ae0b61eca855490d00e4ff5a55e1b29a744a7f..7a4aaa4c7fd4941bdf2dc35cb0836136b568d036 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -3117,17 +3117,17 @@ }, { "name": "drupal/core", - "version": "9.5.3", - "version_normalized": "9.5.3.0", + "version": "9.5.5", + "version_normalized": "9.5.5.0", "source": { "type": "git", "url": "https://github.com/drupal/core.git", - "reference": "67e34a5e8f48cafdd5c26e778a9570860e2d44a5" + "reference": "eae5e76a8b403cbd42b3465f567313b52d78b0dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/drupal/core/zipball/67e34a5e8f48cafdd5c26e778a9570860e2d44a5", - "reference": "67e34a5e8f48cafdd5c26e778a9570860e2d44a5", + "url": "https://api.github.com/repos/drupal/core/zipball/eae5e76a8b403cbd42b3465f567313b52d78b0dc", + "reference": "eae5e76a8b403cbd42b3465f567313b52d78b0dc", "shasum": "" }, "require": { @@ -3207,7 +3207,7 @@ "drupal/core-uuid": "self.version", "drupal/core-version": "self.version" }, - "time": "2023-02-01T19:47:31+00:00", + "time": "2023-03-15T14:30:25+00:00", "type": "drupal-core", "extra": { "drupal-scaffold": { @@ -3286,7 +3286,7 @@ ], "description": "Drupal is an open source content management platform powering millions of websites and applications.", "support": { - "source": "https://github.com/drupal/core/tree/9.5.3" + "source": "https://github.com/drupal/core/tree/9.5.5" }, "install-path": "../../web/core" }, @@ -3342,17 +3342,17 @@ }, { "name": "drupal/core-recommended", - "version": "9.5.3", - "version_normalized": "9.5.3.0", + "version": "9.5.5", + "version_normalized": "9.5.5.0", "source": { "type": "git", "url": "https://github.com/drupal/core-recommended.git", - "reference": "3dc8d9757c6c4a0457d32dd58a755532504ad959" + "reference": "3c1d205349407e706cc89f56aa34448742fe81b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/drupal/core-recommended/zipball/3dc8d9757c6c4a0457d32dd58a755532504ad959", - "reference": "3dc8d9757c6c4a0457d32dd58a755532504ad959", + "url": "https://api.github.com/repos/drupal/core-recommended/zipball/3c1d205349407e706cc89f56aa34448742fe81b4", + "reference": "3c1d205349407e706cc89f56aa34448742fe81b4", "shasum": "" }, "require": { @@ -3361,7 +3361,7 @@ "doctrine/annotations": "~1.13.3", "doctrine/lexer": "~1.2.3", "doctrine/reflection": "~1.2.3", - "drupal/core": "9.5.3", + "drupal/core": "9.5.5", "egulias/email-validator": "~3.2.1", "guzzlehttp/guzzle": "~6.5.8", "guzzlehttp/promises": "~1.5.2", @@ -3416,7 +3416,7 @@ "conflict": { "webflo/drupal-core-strict": "*" }, - "time": "2023-02-01T19:47:31+00:00", + "time": "2023-03-15T14:30:25+00:00", "type": "metapackage", "notification-url": "https://packagist.org/downloads/", "license": [ @@ -3424,7 +3424,7 @@ ], "description": "Core and its dependencies with known-compatible minor versions. Require this project INSTEAD OF drupal/core.", "support": { - "source": "https://github.com/drupal/core-recommended/tree/9.5.3" + "source": "https://github.com/drupal/core-recommended/tree/9.5.5" }, "install-path": null }, diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php index 5296a79fcf4b12637825a487c54563844eb1f123..ac30584a02de4615b16e1b75749eb77392aadb57 100644 --- a/vendor/composer/installed.php +++ b/vendor/composer/installed.php @@ -3,7 +3,7 @@ 'name' => 'osu-asc-webservices/d8-upstream', 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => '0a423db0646b509ef70824e7c1183e79aa41052b', + 'reference' => '1c4ba03b2165c82f1bbded8bf2f748a63f1121f7', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -476,9 +476,9 @@ 'dev_requirement' => false, ), 'drupal/core' => array( - 'pretty_version' => '9.5.3', - 'version' => '9.5.3.0', - 'reference' => '67e34a5e8f48cafdd5c26e778a9570860e2d44a5', + 'pretty_version' => '9.5.5', + 'version' => '9.5.5.0', + 'reference' => 'eae5e76a8b403cbd42b3465f567313b52d78b0dc', 'type' => 'drupal-core', 'install_path' => __DIR__ . '/../../web/core', 'aliases' => array(), @@ -487,25 +487,25 @@ 'drupal/core-annotation' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '9.5.3', + 0 => '9.5.5', ), ), 'drupal/core-assertion' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '9.5.3', + 0 => '9.5.5', ), ), 'drupal/core-bridge' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '9.5.3', + 0 => '9.5.5', ), ), 'drupal/core-class-finder' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '9.5.3', + 0 => '9.5.5', ), ), 'drupal/core-composer-scaffold' => array( @@ -520,97 +520,97 @@ 'drupal/core-datetime' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '9.5.3', + 0 => '9.5.5', ), ), 'drupal/core-dependency-injection' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '9.5.3', + 0 => '9.5.5', ), ), 'drupal/core-diff' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '9.5.3', + 0 => '9.5.5', ), ), 'drupal/core-discovery' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '9.5.3', + 0 => '9.5.5', ), ), 'drupal/core-event-dispatcher' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '9.5.3', + 0 => '9.5.5', ), ), 'drupal/core-file-cache' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '9.5.3', + 0 => '9.5.5', ), ), 'drupal/core-file-security' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '9.5.3', + 0 => '9.5.5', ), ), 'drupal/core-filesystem' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '9.5.3', + 0 => '9.5.5', ), ), 'drupal/core-front-matter' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '9.5.3', + 0 => '9.5.5', ), ), 'drupal/core-gettext' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '9.5.3', + 0 => '9.5.5', ), ), 'drupal/core-graph' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '9.5.3', + 0 => '9.5.5', ), ), 'drupal/core-http-foundation' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '9.5.3', + 0 => '9.5.5', ), ), 'drupal/core-php-storage' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '9.5.3', + 0 => '9.5.5', ), ), 'drupal/core-plugin' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '9.5.3', + 0 => '9.5.5', ), ), 'drupal/core-proxy-builder' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '9.5.3', + 0 => '9.5.5', ), ), 'drupal/core-recommended' => array( - 'pretty_version' => '9.5.3', - 'version' => '9.5.3.0', - 'reference' => '3dc8d9757c6c4a0457d32dd58a755532504ad959', + 'pretty_version' => '9.5.5', + 'version' => '9.5.5.0', + 'reference' => '3c1d205349407e706cc89f56aa34448742fe81b4', 'type' => 'metapackage', 'install_path' => NULL, 'aliases' => array(), @@ -619,37 +619,37 @@ 'drupal/core-render' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '9.5.3', + 0 => '9.5.5', ), ), 'drupal/core-serialization' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '9.5.3', + 0 => '9.5.5', ), ), 'drupal/core-transliteration' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '9.5.3', + 0 => '9.5.5', ), ), 'drupal/core-utility' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '9.5.3', + 0 => '9.5.5', ), ), 'drupal/core-uuid' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '9.5.3', + 0 => '9.5.5', ), ), 'drupal/core-version' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '9.5.3', + 0 => '9.5.5', ), ), 'drupal/crop' => array( @@ -1588,7 +1588,7 @@ 'osu-asc-webservices/d8-upstream' => array( 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => '0a423db0646b509ef70824e7c1183e79aa41052b', + 'reference' => '1c4ba03b2165c82f1bbded8bf2f748a63f1121f7', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), diff --git a/web/core/MAINTAINERS.txt b/web/core/MAINTAINERS.txt index 717ed7e50229ae96d0a48960c040b32b26950b01..3aaa40b15a758bb481f383b21c4b84d746a14d11 100644 --- a/web/core/MAINTAINERS.txt +++ b/web/core/MAINTAINERS.txt @@ -143,7 +143,6 @@ Comment Configuration API - Alex Pott 'alexpott' https://www.drupal.org/u/alexpott -- Matthew Tift 'mtift' https://www.drupal.org/u/mtift Configuration Entity API - Alex Pott 'alexpott' https://www.drupal.org/u/alexpott diff --git a/web/core/core.api.php b/web/core/core.api.php index 312af5e12c6704ac84ccaccbfe2f44ef1f5b484b..d42b5c776f0251cf3213c6aadb617b2ab9e65bab 100644 --- a/web/core/core.api.php +++ b/web/core/core.api.php @@ -2448,6 +2448,28 @@ function hook_validation_constraint_alter(array &$definitions) { * autocomplete, as a \Symfony\Component\HttpFoundation\JsonResponse object. * See the @link menu Routing topic @endlink for more information about * routing. + * + * @section sec_query Query parameters in Ajax requests + * If a form uses an Ajax field, all the query parameters in the current request + * will be also added to the Ajax POST requests along with an additional + * 'ajax_form=1' parameter (See \Drupal\Core\Render\Element\RenderElement). + * @code + * $settings['options']['query'] += \Drupal::request()->query->all(); + * $settings['options']['query'][FormBuilderInterface::AJAX_FORM_REQUEST] = TRUE; + * @endcode + * + * Form elements of type 'managed_file' will have an additional + * 'element_parents' query parameter in Ajax POST requests. This parameter will + * include the name of the element and its parents as per the render array. + * This helps to identify the position of the element in the form (See + * \Drupal\file\Element\ManagedFile). + * @code + * 'options' => [ + * 'query' => [ + * 'element_parents' => implode('/', $element['#array_parents']), + * ], + * ], + * @endcode */ /** diff --git a/web/core/includes/install.core.inc b/web/core/includes/install.core.inc index b4956350ab8fe7510558b6120bd3ec67b7386eed..fcbf23c6d6ed935ade44ab6019476b8ea1e899b9 100644 --- a/web/core/includes/install.core.inc +++ b/web/core/includes/install.core.inc @@ -388,7 +388,7 @@ function install_begin_request($class_loader, &$install_state) { $install_state['database_verified'] = install_verify_database_settings($site_path); // A valid settings.php has database settings and a hash_salt value. Other // settings will be checked by system_requirements(). - $install_state['settings_verified'] = $install_state['database_verified'] && (bool) Settings::get('hash_salt', FALSE); + $install_state['settings_verified'] = $install_state['config_verified'] && $install_state['database_verified'] && (bool) Settings::get('hash_salt', FALSE); if ($install_state['settings_verified']) { try { diff --git a/web/core/lib/Drupal.php b/web/core/lib/Drupal.php index f327e72f118cc9e4cf1dfc1b2b5de22ce95e2071..91d7cfd1e6eb5f1af44520b69989374c9dbdf3b6 100644 --- a/web/core/lib/Drupal.php +++ b/web/core/lib/Drupal.php @@ -75,7 +75,7 @@ class Drupal { /** * The current system version. */ - const VERSION = '9.5.3'; + const VERSION = '9.5.5'; /** * Core API compatibility. diff --git a/web/core/lib/Drupal/Component/Utility/DeprecatedArray.php b/web/core/lib/Drupal/Component/Utility/DeprecatedArray.php index 63b5b7f1335042c5e2b27fb3862f2aba61da7491..f3ef310ce43c7e08f3c474e67b87f65bb9716e1a 100644 --- a/web/core/lib/Drupal/Component/Utility/DeprecatedArray.php +++ b/web/core/lib/Drupal/Component/Utility/DeprecatedArray.php @@ -75,6 +75,7 @@ public function getIterator() { /** * {@inheritdoc} */ + #[\ReturnTypeWillChange] public function unserialize($serialized) { @trigger_error($this->message, E_USER_DEPRECATED); parent::unserialize($serialized); @@ -83,6 +84,7 @@ public function unserialize($serialized) { /** * {@inheritdoc} */ + #[\ReturnTypeWillChange] public function serialize() { @trigger_error($this->message, E_USER_DEPRECATED); return parent::serialize(); diff --git a/web/core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php b/web/core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php index 09bdc01d3d8ec3d2dfed0f717e59496434624a2f..9ab9a1dde11519a3d1f69ea9a2b7f1063b92c3cc 100644 --- a/web/core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php +++ b/web/core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php @@ -107,6 +107,18 @@ public function __construct($root, ModuleHandlerInterface $module_handler, Theme * Thrown when a library has no js/css/setting. * @throws \UnexpectedValueException * Thrown when a js file defines a positive weight. + * @throws \UnknownExtensionTypeException + * Thrown when the extension type is unknown. + * @throws \UnknownExtensionException + * Thrown when the extension is unknown. + * @throws \InvalidLibraryFileException + * Thrown when the library file is invalid. + * @throws \InvalidLibrariesOverrideSpecificationException + * Thrown when a definition refers to a non-existent library. + * @throws \Drupal\Core\Asset\Exception\LibraryDefinitionMissingLicenseException + * Thrown when a library definition has no license information. + * @throws \LogicException + * Thrown when a header key in a library definition is invalid. */ public function buildByExtension($extension) { if ($extension === 'core') { diff --git a/web/core/lib/Drupal/Core/Block/BlockPluginTrait.php b/web/core/lib/Drupal/Core/Block/BlockPluginTrait.php index 401ab2bb154a8b724c0452490a7a325b563e6370..acb850ed0135274ef26c9dfef46fb04ef0d1a732 100644 --- a/web/core/lib/Drupal/Core/Block/BlockPluginTrait.php +++ b/web/core/lib/Drupal/Core/Block/BlockPluginTrait.php @@ -138,7 +138,7 @@ public function access(AccountInterface $account, $return_as_object = FALSE) { * @param \Drupal\Core\Session\AccountInterface $account * The user session for which to check access. * - * @return \Drupal\Core\Access\AccessResult + * @return \Drupal\Core\Access\AccessResultInterface * The access result. * * @see self::access() diff --git a/web/core/lib/Drupal/Core/DrupalKernel.php b/web/core/lib/Drupal/Core/DrupalKernel.php index c70ccdc61b9c9889771a52803c21221be2d4e484..98a1969e39a64099a84dac4049d2df5011918d96 100644 --- a/web/core/lib/Drupal/Core/DrupalKernel.php +++ b/web/core/lib/Drupal/Core/DrupalKernel.php @@ -230,7 +230,12 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface { protected static $isEnvironmentInitialized = FALSE; /** - * The site directory. + * The site path directory. + * + * Site path is relative to the app root directory. + * Usually defined as "sites/default". + * + * By default, Drupal uses sites/default. * * @var string */ @@ -1584,7 +1589,7 @@ public static function validateHostname(Request $request) { * @see \Drupal\Core\Http\TrustedHostsRequestFactory */ protected static function setupTrustedHosts(Request $request, $host_patterns) { - $request->setTrustedHosts($host_patterns); + Request::setTrustedHosts($host_patterns); // Get the host, which will validate the current request. try { diff --git a/web/core/lib/Drupal/Core/DrupalKernelInterface.php b/web/core/lib/Drupal/Core/DrupalKernelInterface.php index dfd00f20170ec8eef732ca2438859e592fa014f9..f42e8eec6fb3fc88b11d9e7269737d0e56dd15b6 100644 --- a/web/core/lib/Drupal/Core/DrupalKernelInterface.php +++ b/web/core/lib/Drupal/Core/DrupalKernelInterface.php @@ -74,7 +74,9 @@ public function getContainer(); public function getCachedContainerDefinition(); /** - * Set the current site path. + * Set the current site path directory. + * + * Format: "folder-name/child-folder" usually uses "sites/default". * * @param string $path * The current site path. @@ -85,10 +87,10 @@ public function getCachedContainerDefinition(); public function setSitePath($path); /** - * Get the site path. + * Gets the site path directory. * * @return string - * The current site path. + * The current site path directory. */ public function getSitePath(); diff --git a/web/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManagerInterface.php b/web/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManagerInterface.php index 0a6dc5d6b2141ddb0e25398d36b1b71248e8482a..edae57c9e44ec41d701afe12d3d1f3f5f6dea1ea 100644 --- a/web/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManagerInterface.php +++ b/web/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManagerInterface.php @@ -184,8 +184,9 @@ public function updateFieldableEntityType(EntityTypeInterface $entity_type, arra * @param string $entity_type_id * The entity type identifier. * - * @return \Drupal\Core\Field\FieldStorageDefinitionInterface - * The field storage definition. + * @return \Drupal\Core\Field\FieldStorageDefinitionInterface|null + * The field storage definition or NULL if there is none for the given field + * name and entity type. * * @todo Make this return a mutable storage definition interface when we have * one. See https://www.drupal.org/node/2346329. diff --git a/web/core/lib/Drupal/Core/Entity/EntityType.php b/web/core/lib/Drupal/Core/Entity/EntityType.php index 6fbbbe950d78fadeb60398aa899b536cb9b129fd..13fc14775edb36655c21a766b9eecdc5920455af 100644 --- a/web/core/lib/Drupal/Core/Entity/EntityType.php +++ b/web/core/lib/Drupal/Core/Entity/EntityType.php @@ -200,9 +200,15 @@ class EntityType extends PluginDefinition implements EntityTypeInterface { /** * A definite singular/plural name of the type. * - * Needed keys: "singular" and "plural". + * Needed keys: "singular" and "plural". Can also have key: "context". + * @code + * [ + * 'singular' => '@count entity', + * 'plural' => '@count entities', + * 'context' => 'Entity context', + * ] * - * @var string|\Drupal\Core\StringTranslation\TranslatableMarkup + * @var string[] * * @see \Drupal\Core\Entity\EntityTypeInterface::getCountLabel() */ diff --git a/web/core/lib/Drupal/Core/Extension/module.api.php b/web/core/lib/Drupal/Core/Extension/module.api.php index f19b5ca667d0a3760d0cac158377e2e36c21309d..9cd4a033663656f30c9ebcf9f9b6a9da6c4dd43a 100644 --- a/web/core/lib/Drupal/Core/Extension/module.api.php +++ b/web/core/lib/Drupal/Core/Extension/module.api.php @@ -183,7 +183,9 @@ function hook_module_preinstall($module) { * TRUE if the module is being installed as part of a configuration import. In * these cases, your hook implementation needs to carefully consider what * changes, if any, it should make. For example, it should not make any - * changes to configuration objects or entities. + * changes to configuration objects or configuration entities. Those changes + * should be made earlier and exported so during import there's no need to + * do them again. * * @see \Drupal\Core\Extension\ModuleInstaller::install() * @see hook_install() @@ -230,8 +232,9 @@ function hook_modules_installed($modules, $is_syncing) { * @param bool $is_syncing * TRUE if the module is being installed as part of a configuration import. In * these cases, your hook implementation needs to carefully consider what - * changes, if any, it should make. For example, it should not make any - * changes to configuration objects or entities. + * changes to configuration objects or configuration entities. Those changes + * should be made earlier and exported so during import there's no need to + * do them again. * * @see \Drupal\Core\Config\ConfigInstallerInterface::isSyncing * @see hook_schema() @@ -269,8 +272,9 @@ function hook_module_preuninstall($module) { * @param bool $is_syncing * TRUE if the module is being uninstalled as part of a configuration import. * In these cases, your hook implementation needs to carefully consider what - * changes, if any, it should make. For example, it should not make any - * changes to configuration objects or entities. + * changes to configuration objects or configuration entities. Those changes + * should be made earlier and exported so during import there's no need to + * do them again. * * @see hook_uninstall() */ @@ -307,8 +311,9 @@ function hook_modules_uninstalled($modules, $is_syncing) { * @param bool $is_syncing * TRUE if the module is being uninstalled as part of a configuration import. * In these cases, your hook implementation needs to carefully consider what - * changes, if any, it should make. For example, it should not make any - * changes to configuration objects or entities. + * changes to configuration objects or configuration entities. Those changes + * should be made earlier and exported so during import there's no need to + * do them again. * * @see hook_install() * @see hook_schema() diff --git a/web/core/lib/Drupal/Core/Mail/Plugin/Mail/PhpMail.php b/web/core/lib/Drupal/Core/Mail/Plugin/Mail/PhpMail.php index 138257b8da486cff940135178250fc5723c0d5fa..cae954c49e005b1b05e3cdbe6f2ba17b49e48dfe 100644 --- a/web/core/lib/Drupal/Core/Mail/Plugin/Mail/PhpMail.php +++ b/web/core/lib/Drupal/Core/Mail/Plugin/Mail/PhpMail.php @@ -87,7 +87,9 @@ public function mail(array $message) { $headers = new Headers(); foreach ($message['headers'] as $name => $value) { if (in_array(strtolower($name), self::MAILBOX_LIST_HEADERS, TRUE)) { - $value = explode(',', $value); + // Split values by comma, but ignore commas encapsulated in double + // quotes. + $value = str_getcsv($value, ','); } $headers->addHeader($name, $value); } @@ -104,12 +106,7 @@ public function mail(array $message) { $mail_headers = str_replace("\r\n", "\n", $headers->toString()); $mail_subject = str_replace("\r\n", "\n", $mail_subject); - $request = \Drupal::request(); - - // We suppress warnings and notices from mail() because of issues on some - // hosts. The return value of this method will still indicate whether mail - // was sent successfully. - if (!$request->server->has('WINDIR') && strpos($request->server->get('SERVER_SOFTWARE'), 'Win32') === FALSE) { + if (substr(PHP_OS, 0, 3) != 'WIN') { // On most non-Windows systems, the "-f" option to the sendmail command // is used to set the Return-Path. There is no space between -f and // the value of the return path. @@ -117,7 +114,7 @@ public function mail(array $message) { // we assume to be safe. $site_mail = $this->configFactory->get('system.site')->get('mail'); $additional_headers = isset($message['Return-Path']) && ($site_mail === $message['Return-Path'] || static::_isShellSafe($message['Return-Path'])) ? '-f' . $message['Return-Path'] : ''; - $mail_result = @mail( + $mail_result = $this->doMail( $message['to'], $mail_subject, $mail_body, @@ -130,7 +127,7 @@ public function mail(array $message) { // Return-Path header. $old_from = ini_get('sendmail_from'); ini_set('sendmail_from', $message['Return-Path']); - $mail_result = @mail( + $mail_result = $this->doMail( $message['to'], $mail_subject, $mail_body, @@ -142,6 +139,36 @@ public function mail(array $message) { return $mail_result; } + /** + * Wrapper around PHP's mail() function. + * + * We suppress warnings and notices from mail() because of issues on some + * hosts. The return value of this method will still indicate whether mail was + * sent successfully. + * + * @param string $to + * Receiver, or receivers of the mail. + * @param string $subject + * Subject of the email to be sent. + * @param string $message + * Message to be sent. + * @param array $additional_headers + * (optional) Array to be inserted at the end of the email header. + * @param string $additional_params + * (optional) Can be used to pass additional flags as command line options. + * + * @see mail() + */ + protected function doMail(string $to, string $subject, string $message, $additional_headers = [], string $additional_params = ''): bool { + return @mail( + $to, + $subject, + $message, + $additional_headers, + $additional_params + ); + } + /** * Disallows potentially unsafe shell characters. * diff --git a/web/core/lib/Drupal/Core/ParamConverter/EntityConverter.php b/web/core/lib/Drupal/Core/ParamConverter/EntityConverter.php index 823dcdaa1e12cbba1c0be167d3cb8152a817e39b..c3820baaca01c8a2a92b308d39f2b8dd4c0d9af4 100644 --- a/web/core/lib/Drupal/Core/ParamConverter/EntityConverter.php +++ b/web/core/lib/Drupal/Core/ParamConverter/EntityConverter.php @@ -120,7 +120,16 @@ public function convert($value, $definition, $name, array $defaults) { // If the entity type is revisionable and the parameter has the // "load_latest_revision" flag, load the active variant. if (!empty($definition['load_latest_revision'])) { - return $this->entityRepository->getActive($entity_type_id, $value); + $entity = $this->entityRepository->getActive($entity_type_id, $value); + + if ( + !empty($definition['bundle']) && + $entity instanceof EntityInterface && + !in_array($entity->bundle(), $definition['bundle'], TRUE) + ) { + return NULL; + } + return $entity; } // Do not inject the context repository as it is not an actual dependency: diff --git a/web/core/lib/Drupal/Core/Routing/UrlGeneratorInterface.php b/web/core/lib/Drupal/Core/Routing/UrlGeneratorInterface.php index 891b035413a3c5c45e77cf58cc22c86c4e4a2a76..a69ffb39a16682c401fd1ddc1c9d675d57b5bb4b 100644 --- a/web/core/lib/Drupal/Core/Routing/UrlGeneratorInterface.php +++ b/web/core/lib/Drupal/Core/Routing/UrlGeneratorInterface.php @@ -53,6 +53,8 @@ public function getPathFromRoute($name, $parameters = []); * - 'https': Whether this URL should point to a secure location. If not * defined, the current scheme is used, so the user stays on HTTP or HTTPS * respectively. TRUE enforces HTTPS and FALSE enforces HTTP. + * - 'path_processing': Defaults to TRUE. Whether to pass the path to a + * processor manager to allow alterations. * - 'base_url': Only used internally by a path processor, for example, to * modify the base URL when a language dependent URL requires so. * - 'prefix': Only used internally, to modify the path when a language diff --git a/web/core/misc/states.es6.js b/web/core/misc/states.es6.js index b45e1e0504cdabc803d95f24e301f1b0f83e9bbc..0a9071ef5c657c1d1c5d79b51eb8cb59ad84decf 100644 --- a/web/core/misc/states.es6.js +++ b/web/core/misc/states.es6.js @@ -526,6 +526,10 @@ // the state. return this.val() === ''; }, + // Listen to 'change' for number native "spinner" widgets. + change() { + return this.val() === ''; + }, }, checked: { @@ -722,7 +726,7 @@ $document.on('state:checked', (e) => { if (e.trigger) { - $(e.target).prop('checked', e.value); + $(e.target).prop('checked', e.value).trigger('change'); } }); diff --git a/web/core/misc/states.js b/web/core/misc/states.js index e827a7baa83a88080bb37ad9f6459503c8d4e079..d42bb2cd1d8bb9f3112cfb6bcfbf8acafdb28d7d 100644 --- a/web/core/misc/states.js +++ b/web/core/misc/states.js @@ -217,6 +217,9 @@ empty: { keyup: function keyup() { return this.val() === ''; + }, + change: function change() { + return this.val() === ''; } }, checked: { @@ -320,7 +323,7 @@ }); $document.on('state:checked', function (e) { if (e.trigger) { - $(e.target).prop('checked', e.value); + $(e.target).prop('checked', e.value).trigger('change'); } }); $document.on('state:collapsed', function (e) { diff --git a/web/core/modules/aggregator/src/Controller/AggregatorController.php b/web/core/modules/aggregator/src/Controller/AggregatorController.php index 0d2a329a9e3a59db2555f8846ce25f96a1cca1ce..118a107c29cb77fa7a3560ed787ff52c9093ef9f 100644 --- a/web/core/modules/aggregator/src/Controller/AggregatorController.php +++ b/web/core/modules/aggregator/src/Controller/AggregatorController.php @@ -73,8 +73,8 @@ protected function buildPageList(array $items, $feed_source = '') { if ($items) { $build['items'] = $this->entityTypeManager()->getViewBuilder('aggregator_item') ->viewMultiple($items, 'default'); - $build['pager'] = ['#type' => 'pager']; } + $build['pager'] = ['#type' => 'pager']; return $build; } diff --git a/web/core/modules/aggregator/tests/src/Functional/AggregatorRenderingTest.php b/web/core/modules/aggregator/tests/src/Functional/AggregatorRenderingTest.php index ca27af1d5687c7ce6b1e166fea6a4f3806691232..7ea19b985821ff4d88b9fbe04d7827e469611c82 100644 --- a/web/core/modules/aggregator/tests/src/Functional/AggregatorRenderingTest.php +++ b/web/core/modules/aggregator/tests/src/Functional/AggregatorRenderingTest.php @@ -103,6 +103,8 @@ public function testFeedPage() { $feed = $this->createFeed(); $this->updateFeedItems($feed, 30); + // Request page with no feed items to ensure cache context is set correctly. + $this->drupalGet('aggregator', ['query' => ['page' => 2]]); // Check for presence of an aggregator pager. $this->drupalGet('aggregator'); $this->assertSession()->elementExists('xpath', '//ul[contains(@class, "pager__items")]'); diff --git a/web/core/modules/book/book.views.inc b/web/core/modules/book/book.views.inc index 63dfb38aec386f789651009013373ae71580f92e..9e10ccb2e834874aca7b98cb08ba184f4f59d089 100644 --- a/web/core/modules/book/book.views.inc +++ b/web/core/modules/book/book.views.inc @@ -84,7 +84,7 @@ function book_views_data() { $data['book']['depth'] = [ 'title' => t('Depth'), - 'help' => t('The depth of the book page in the hierarchy; top level books have a depth of 1.'), + 'help' => t('The depth of the book page in the hierarchy; top level book pages have a depth of 1.'), 'field' => [ 'id' => 'numeric', ], diff --git a/web/core/modules/book/src/BookManagerInterface.php b/web/core/modules/book/src/BookManagerInterface.php index 959c45cb9a94806ff7b1d8dfa82d927f799387ba..f787971655b0af68e952c55aa34cb7a532e5ef0c 100644 --- a/web/core/modules/book/src/BookManagerInterface.php +++ b/web/core/modules/book/src/BookManagerInterface.php @@ -183,15 +183,23 @@ public function getAllBooks(); public function updateOutline(NodeInterface $node); /** - * Saves a single book entry. + * Saves a link for a single book entry to the book. * * @param array $link - * The link data to save. + * The link data to save. $link['nid'] must be set. Other keys in this array + * get default values from + * \Drupal\book\BookManagerInterface::getLinkDefaults(). The array keys + * available to be set are documented in + * \Drupal\book\BookOutlineStorageInterface::loadMultiple(). * @param bool $new - * Is this a new book. + * Whether this is a link to a new book entry. * * @return array - * The book data of that node. + * The book entry link information. This is $link with values added or + * updated. + * + * @see \Drupal\book\BookManagerInterface::getLinkDefaults() + * @see \Drupal\book\BookOutlineStorageInterface::loadMultiple() */ public function saveBookLink(array $link, $new); diff --git a/web/core/modules/ckeditor5/src/SmartDefaultSettings.php b/web/core/modules/ckeditor5/src/SmartDefaultSettings.php index a10a64b2133e2188c45b26facc900d71cc0a8a53..9da23d1aba1eca47e92c6d6a784a35a3633544a8 100644 --- a/web/core/modules/ckeditor5/src/SmartDefaultSettings.php +++ b/web/core/modules/ckeditor5/src/SmartDefaultSettings.php @@ -178,39 +178,40 @@ public function computeSmartDefaultSettings(?EditorInterface $text_editor, Filte $unsupported = $missing->diff($missing_attributes); if ($enabling_message_content) { - $this->logger->info(new FormattableMarkup('The CKEditor 5 migration enabled the following plugins to support tags that are allowed by the %text_format text format: %enabling_message_content. The text format must be saved to make these changes active.', + $this->logger->info('The CKEditor 5 migration enabled the following plugins to support tags that are allowed by the %text_format text format: %enabling_message_content. The text format must be saved to make these changes active.', [ '%text_format' => $editor->getFilterFormat()->get('name'), '%enabling_message_content' => $enabling_message_content, - ], - )); + ] + ); } + // Warn user about unsupported tags. if (!$unsupported->allowsNothing()) { $this->addTagsToSourceEditing($editor, $unsupported); $source_editing_additions = $source_editing_additions->merge($unsupported); - $this->logger->info(new FormattableMarkup("The following tags were permitted by the %text_format text format's filter configuration, but no plugin was available that supports them. To ensure the tags remain supported by this text format, the following were added to the Source Editing plugin's <em>Manually editable HTML tags</em>: @unsupported_string. The text format must be saved to make these changes active.", [ + $this->logger->info("The following tags were permitted by the %text_format text format's filter configuration, but no plugin was available that supports them. To ensure the tags remain supported by this text format, the following were added to the Source Editing plugin's <em>Manually editable HTML tags</em>: @unsupported_string. The text format must be saved to make these changes active.", [ '%text_format' => $editor->getFilterFormat()->get('name'), '@unsupported_string' => $unsupported->toFilterHtmlAllowedTagsString(), - ])); + ]); } if ($enabled_for_attributes_message_content) { - $this->logger->info(new FormattableMarkup('The CKEditor 5 migration process enabled the following plugins to support specific attributes that are allowed by the %text_format text format: %enabled_for_attributes_message_content.', + $this->logger->info('The CKEditor 5 migration process enabled the following plugins to support specific attributes that are allowed by the %text_format text format: %enabled_for_attributes_message_content.', [ '%text_format' => $editor->getFilterFormat()->get('name'), '%enabled_for_attributes_message_content' => $enabled_for_attributes_message_content, ], - )); + ); } // Warn user about supported tags but missing attributes. if (!$missing_attributes->allowsNothing()) { $this->addTagsToSourceEditing($editor, $missing_attributes); $source_editing_additions = $source_editing_additions->merge($missing_attributes); - $this->logger->info(new FormattableMarkup("As part of migrating to CKEditor 5, it was found that the %text_format text format's HTML filters includes plugins that support the following tags, but not some of their attributes. To ensure these attributes remain supported, the following were added to the Source Editing plugin's <em>Manually editable HTML tags</em>: @missing_attributes. The text format must be saved to make these changes active.", [ + $this->logger->info("As part of migrating to CKEditor 5, it was found that the %text_format text format's HTML filters includes plugins that support the following tags, but not some of their attributes. To ensure these attributes remain supported, the following were added to the Source Editing plugin's <em>Manually editable HTML tags</em>: @missing_attributes. The text format must be saved to make these changes active.", [ '%text_format' => $editor->getFilterFormat()->get('name'), '@missing_attributes' => $missing_attributes->toFilterHtmlAllowedTagsString(), - ])); + ]); } } @@ -225,10 +226,10 @@ public function computeSmartDefaultSettings(?EditorInterface $text_editor, Filte $missing_fundamental_tags = $fundamental->diff($filter_html_restrictions); if (!$missing_fundamental_tags->allowsNothing()) { $editor->getFilterFormat()->setFilterConfig('filter_html', $filter_html_restrictions->merge($fundamental)->getAllowedElements()); - $this->logger->warning(new FormattableMarkup("As part of migrating the %text_format text format to CKEditor 5, the following tag(s) were added to <em>Limit allowed HTML tags and correct faulty HTML</em>, because they are needed to provide fundamental CKEditor 5 functionality : @missing_tags. The text format must be saved to make these changes active.", [ + $this->logger->warning("As part of migrating the %text_format text format to CKEditor 5, the following tag(s) were added to <em>Limit allowed HTML tags and correct faulty HTML</em>, because they are needed to provide fundamental CKEditor 5 functionality : @missing_tags. The text format must be saved to make these changes active.", [ '%text_format' => $editor->getFilterFormat()->get('name'), '@missing_tags' => $missing_fundamental_tags->toFilterHtmlAllowedTagsString(), - ])); + ]); } } @@ -448,9 +449,9 @@ private function createSettingsFromCKEditor4(array $ckeditor4_settings, HTMLRest $equivalent = $this->upgradePluginManager->mapCKEditor4ToolbarButtonToCKEditor5ToolbarItem($cke4_button, $text_format_html_restrictions); } catch (\OutOfBoundsException $e) { - $this->logger->warning(new FormattableMarkup('The CKEditor 4 button %button does not have a known upgrade path. If it allowed editing markup, then you can do so now through the Source Editing functionality.', [ + $this->logger->warning('The CKEditor 4 button %button does not have a known upgrade path. If it allowed editing markup, then you can do so now through the Source Editing functionality.', [ '%button' => $cke4_button, - ])); + ]); $messages[MessengerInterface::TYPE_WARNING][] = $this->t('The CKEditor 4 button %button does not have a known upgrade path. If it allowed editing markup, then you can do so now through the Source Editing functionality.', [ '%button' => $cke4_button, ]); @@ -489,9 +490,9 @@ private function createSettingsFromCKEditor4(array $ckeditor4_settings, HTMLRest $settings['plugins'] += $cke5_plugin_settings; } catch (\OutOfBoundsException $e) { - $this->logger->warning(new FormattableMarkup('The %cke4_plugin_id plugin settings do not have a known upgrade path.', [ + $this->logger->warning('The %cke4_plugin_id plugin settings do not have a known upgrade path.', [ '%cke4_plugin_id' => $cke4_plugin_id, - ])); + ]); $messages[MessengerInterface::TYPE_WARNING][] = $this->t('The %cke4_plugin_id plugin settings do not have a known upgrade path.', [ '%cke4_plugin_id' => $cke4_plugin_id, ]); diff --git a/web/core/modules/ckeditor5/tests/src/Kernel/SmartDefaultSettingsTest.php b/web/core/modules/ckeditor5/tests/src/Kernel/SmartDefaultSettingsTest.php index 1903d3b008aa53bd68864721fb4b02e6a92ff046..65c64f348593c05964a608ee9c454dd1857e2003 100644 --- a/web/core/modules/ckeditor5/tests/src/Kernel/SmartDefaultSettingsTest.php +++ b/web/core/modules/ckeditor5/tests/src/Kernel/SmartDefaultSettingsTest.php @@ -5,6 +5,7 @@ namespace Drupal\Tests\ckeditor5\Kernel; use Drupal\ckeditor5\HTMLRestrictions; +use Drupal\Component\Render\FormattableMarkup; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Entity\Entity\EntityViewMode; use Drupal\editor\Entity\Editor; @@ -572,7 +573,9 @@ public function test(string $format_id, array $filters_to_drop, array $expected_ ]; $db_logs = []; foreach ($db_logged as $log) { - $db_logs[$type_to_status[$log->severity]][] = $log->message; + $variables = unserialize($log->variables); + $message = new FormattableMarkup($log->message, $variables); + $db_logs[$type_to_status[$log->severity]][] = (string) $message; } // Transforms TranslatableMarkup objects to string. diff --git a/web/core/modules/content_moderation/src/Entity/ContentModerationState.php b/web/core/modules/content_moderation/src/Entity/ContentModerationState.php index 6d0cb0abd57f252056ff859dd48b5747884fa71c..fabce0090f52edd91c4d07173777a7de30029890 100644 --- a/web/core/modules/content_moderation/src/Entity/ContentModerationState.php +++ b/web/core/modules/content_moderation/src/Entity/ContentModerationState.php @@ -134,12 +134,17 @@ public static function loadFromModeratedEntity(EntityInterface $entity) { /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ $storage = \Drupal::entityTypeManager()->getStorage('content_moderation_state'); + // New entities may not have a loaded revision ID at this point, but the + // creation of a content moderation state entity may have already been + // triggered elsewhere. In this case we have to match on the revision ID + // (instead of the loaded revision ID). + $revision_id = $entity->getLoadedRevisionId() ?: $entity->getRevisionId(); $ids = $storage->getQuery() ->accessCheck(FALSE) ->condition('content_entity_type_id', $entity->getEntityTypeId()) ->condition('content_entity_id', $entity->id()) ->condition('workflow', $moderation_info->getWorkflowForEntity($entity)->id()) - ->condition('content_entity_revision_id', $entity->getLoadedRevisionId()) + ->condition('content_entity_revision_id', $revision_id) ->allRevisions() ->execute(); diff --git a/web/core/modules/content_moderation/tests/modules/content_moderation_test_resave/content_moderation_test_resave.info.yml b/web/core/modules/content_moderation/tests/modules/content_moderation_test_resave/content_moderation_test_resave.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..25d8618bbf4c7fd32f0b9ec3be8472cff867441c --- /dev/null +++ b/web/core/modules/content_moderation/tests/modules/content_moderation_test_resave/content_moderation_test_resave.info.yml @@ -0,0 +1,7 @@ +name: 'Content moderation test re-save' +type: module +description: 'Re-saves moderated entities for testing purposes.' +package: Testing +version: VERSION +dependencies: + - drupal:content_moderation diff --git a/web/core/modules/content_moderation/tests/modules/content_moderation_test_resave/content_moderation_test_resave.install b/web/core/modules/content_moderation/tests/modules/content_moderation_test_resave/content_moderation_test_resave.install new file mode 100644 index 0000000000000000000000000000000000000000..383e59ee8c22ffb7b23b35123cbcd82230f78d9a --- /dev/null +++ b/web/core/modules/content_moderation/tests/modules/content_moderation_test_resave/content_moderation_test_resave.install @@ -0,0 +1,15 @@ +<?php + +/** + * @file + * Contains install functions for the Content moderation test re-save module. + */ + +/** + * Implements hook_install(). + */ +function content_moderation_test_resave_install() { + // Make sure that this module's hooks are run before Content Moderation's + // hooks. + module_set_weight('content_moderation_test_resave', -10); +} diff --git a/web/core/modules/content_moderation/tests/modules/content_moderation_test_resave/content_moderation_test_resave.module b/web/core/modules/content_moderation/tests/modules/content_moderation_test_resave/content_moderation_test_resave.module new file mode 100644 index 0000000000000000000000000000000000000000..74292920d762494072b079cc6da72c7e16bfa5c1 --- /dev/null +++ b/web/core/modules/content_moderation/tests/modules/content_moderation_test_resave/content_moderation_test_resave.module @@ -0,0 +1,30 @@ +<?php + +/** + * @file + * Contains hook implementations for the Content moderation test re-save module. + */ + +use Drupal\Core\Entity\EntityInterface; + +/** + * Implements hook_entity_insert(). + */ +function content_moderation_test_resave_entity_insert(EntityInterface $entity) { + /** @var \Drupal\content_moderation\ModerationInformationInterface $content_moderation */ + $content_moderation = \Drupal::service('content_moderation.moderation_information'); + if ($content_moderation->isModeratedEntity($entity)) { + /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ + // Saving the passed entity object would populate its loaded revision ID, + // which we want to avoid. Thus, save a clone of the original object. + $cloned_entity = clone $entity; + // Set the entity's syncing status, as we do not want Content Moderation to + // create new revisions for the re-saving. Without this call Content + // Moderation would end up creating two separate content moderation state + // entities: one for the re-save revision and one for the initial revision. + $cloned_entity->setSyncing(TRUE)->save(); + + // Record the fact that a re-save happened. + \Drupal::state()->set('content_moderation_test_resave', TRUE); + } +} diff --git a/web/core/modules/content_moderation/tests/src/Kernel/ContentModerationResaveTest.php b/web/core/modules/content_moderation/tests/src/Kernel/ContentModerationResaveTest.php new file mode 100644 index 0000000000000000000000000000000000000000..15f9aaf03a59b4b0103721383d174c6d79e29392 --- /dev/null +++ b/web/core/modules/content_moderation/tests/src/Kernel/ContentModerationResaveTest.php @@ -0,0 +1,107 @@ +<?php + +namespace Drupal\Tests\content_moderation\Kernel; + +use Drupal\content_moderation\Entity\ContentModerationState; +use Drupal\KernelTests\KernelTestBase; +use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait; + +/** + * Tests Content Moderation with entities that get re-saved automatically. + * + * @group content_moderation + */ +class ContentModerationResaveTest extends KernelTestBase { + + use ContentModerationTestTrait; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + // Make sure the test module is listed first as module weights do not apply + // for kernel tests. + /* @see \content_moderation_test_resave_install() */ + 'content_moderation_test_resave', + 'content_moderation', + 'entity_test', + 'user', + 'workflows', + ]; + + /** + * The content moderation state entity storage. + * + * @var \Drupal\Core\Entity\EntityStorageInterface + */ + protected $contentModerationStateStorage; + + /** + * The entity storage for the entity type used in the test. + * + * @var \Drupal\Core\Entity\EntityStorageInterface + */ + protected $entityStorage; + + /** + * The state service. + * + * @var \Drupal\Core\State\StateInterface + */ + protected $state; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + $entity_type_id = 'entity_test_rev'; + + $this->installEntitySchema('content_moderation_state'); + $this->installEntitySchema($entity_type_id); + + $workflow = $this->createEditorialWorkflow(); + $this->addEntityTypeAndBundleToWorkflow($workflow, $entity_type_id, $entity_type_id); + + /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */ + $entity_type_manager = $this->container->get('entity_type.manager'); + $this->contentModerationStateStorage = $entity_type_manager->getStorage('content_moderation_state'); + $this->entityStorage = $entity_type_manager->getStorage($entity_type_id); + $this->state = $this->container->get('state'); + } + + /** + * Tests that Content Moderation works with entities being resaved. + */ + public function testContentModerationResave() { + $entity = $this->entityStorage->create(); + $this->assertSame('draft', $entity->get('moderation_state')->value); + $this->assertNull(\Drupal::state()->get('content_moderation_test_resave')); + $this->assertNull(ContentModerationState::loadFromModeratedEntity($entity)); + $content_moderation_state_query = $this->contentModerationStateStorage + ->getQuery() + ->accessCheck(FALSE) + ->count(); + $this->assertSame(0, (int) $content_moderation_state_query->execute()); + $content_moderation_state_revision_query = $this->contentModerationStateStorage + ->getQuery() + ->accessCheck(FALSE) + ->allRevisions() + ->count(); + $this->assertSame(0, (int) $content_moderation_state_revision_query->execute()); + + // The test module will re-save the entity in its hook_insert() + // implementation creating the content moderation state entity before + // Content Moderation's hook_insert() has run for the initial save + // operation. + $entity->save(); + $this->assertSame('draft', $entity->get('moderation_state')->value); + $this->assertTrue(\Drupal::state()->get('content_moderation_test_resave')); + $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity); + $this->assertInstanceOf(ContentModerationState::class, $content_moderation_state); + $this->assertSame(1, (int) $content_moderation_state_query->execute()); + $this->assertSame(1, (int) $content_moderation_state_revision_query->execute()); + } + +} diff --git a/web/core/modules/contextual/js/contextual.es6.js b/web/core/modules/contextual/js/contextual.es6.js index b5fe7d094fb0e1685308b8ed4b577bda418c981b..90b0d9056a900799a81cd2ce88f38333acdbe649 100644 --- a/web/core/modules/contextual/js/contextual.es6.js +++ b/web/core/modules/contextual/js/contextual.es6.js @@ -99,7 +99,7 @@ // Set the destination parameter on each of the contextual links. const destination = `destination=${Drupal.encodePath( - Drupal.url(drupalSettings.path.currentPath), + Drupal.url(drupalSettings.path.currentPath + window.location.search), )}`; $contextual.find('.contextual-links a').each(function () { const url = this.getAttribute('href'); diff --git a/web/core/modules/contextual/js/contextual.js b/web/core/modules/contextual/js/contextual.js index 65e5dfb3d764c306aa077ad297e6fb4516c3dc64..c473ca8119310bf7b7d047b016b14ee4f09cf916 100644 --- a/web/core/modules/contextual/js/contextual.js +++ b/web/core/modules/contextual/js/contextual.js @@ -46,7 +46,7 @@ var $region = $contextual.closest('.contextual-region'); var contextual = Drupal.contextual; $contextual.html(html).addClass('contextual').prepend(Drupal.theme('contextualTrigger')); - var destination = "destination=".concat(Drupal.encodePath(Drupal.url(drupalSettings.path.currentPath))); + var destination = "destination=".concat(Drupal.encodePath(Drupal.url(drupalSettings.path.currentPath + window.location.search))); $contextual.find('.contextual-links a').each(function () { var url = this.getAttribute('href'); var glue = url.indexOf('?') === -1 ? '?' : '&'; diff --git a/web/core/modules/contextual/tests/src/FunctionalJavascript/ContextualLinksTest.php b/web/core/modules/contextual/tests/src/FunctionalJavascript/ContextualLinksTest.php index 1d16440cbf50a08753345b9ea1a2aed4b7ae55f9..0a16d616b8c3de70843eb5da895c4fb4ce58e1b6 100644 --- a/web/core/modules/contextual/tests/src/FunctionalJavascript/ContextualLinksTest.php +++ b/web/core/modules/contextual/tests/src/FunctionalJavascript/ContextualLinksTest.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\contextual\FunctionalJavascript; +use Drupal\Core\Url; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\user\Entity\Role; @@ -114,4 +115,20 @@ public function testContextualLinksDestination() { $this->assertEquals("destination=$expected_destination_value", $contextual_link_url_parsed['query']); } + /** + * Tests the contextual links destination with query. + */ + public function testContextualLinksDestinationWithQuery() { + $this->grantPermissions(Role::load(Role::AUTHENTICATED_ID), [ + 'access contextual links', + 'administer blocks', + ]); + + $this->drupalGet('admin/structure/block', ['query' => ['foo' => 'bar']]); + $this->assertSession()->waitForElement('css', '.contextual button'); + $expected_destination_value = Url::fromRoute('block.admin_display')->toString(); + $contextual_link_url_parsed = parse_url($this->getSession()->getPage()->findLink('Configure block')->getAttribute('href')); + $this->assertEquals("destination=$expected_destination_value%3Ffoo%3Dbar", $contextual_link_url_parsed['query']); + } + } diff --git a/web/core/modules/contextual/tests/src/Kernel/ContextualUnitTest.php b/web/core/modules/contextual/tests/src/Kernel/ContextualUnitTest.php index 9e088aefb70a4ca362c4091b2d11a8e6e58b3c03..dc46bef32a4432b161a268e7d5437cd920746aa7 100644 --- a/web/core/modules/contextual/tests/src/Kernel/ContextualUnitTest.php +++ b/web/core/modules/contextual/tests/src/Kernel/ContextualUnitTest.php @@ -5,8 +5,7 @@ use Drupal\KernelTests\KernelTestBase; /** - * Tests all edge cases of converting from #contextual_links to ids and vice - * versa. + * Tests edge cases for converting between contextual links and IDs. * * @group contextual */ @@ -23,14 +22,13 @@ class ContextualUnitTest extends KernelTestBase { * Provides testcases for both test functions. * * Used in testContextualLinksToId() and testContextualIdToLinks(). + * + * @return array[] + * Test cases. */ - public function _contextual_links_id_testcases() { - // Test branch conditions: - // - one group. - // - one dynamic path argument. - // - no metadata. - $tests[] = [ - 'links' => [ + public function contextualLinksDataProvider(): array { + $tests['one group, one dynamic path argument, no metadata'] = [ + [ 'node' => [ 'route_parameters' => [ 'node' => '14031991', @@ -38,33 +36,25 @@ public function _contextual_links_id_testcases() { 'metadata' => ['langcode' => 'en'], ], ], - 'id' => 'node:node=14031991:langcode=en', + 'node:node=14031991:langcode=en', ]; - // Test branch conditions: - // - one group. - // - multiple dynamic path arguments. - // - no metadata. - $tests[] = [ - 'links' => [ + $tests['one group, multiple dynamic path arguments, no metadata'] = [ + [ 'foo' => [ 'route_parameters' => [ - 'bar', + 0 => 'bar', 'key' => 'baz', - 'qux', + 1 => 'qux', ], 'metadata' => ['langcode' => 'en'], ], ], - 'id' => 'foo:0=bar&key=baz&1=qux:langcode=en', + 'foo:0=bar&key=baz&1=qux:langcode=en', ]; - // Test branch conditions: - // - one group. - // - one dynamic path argument. - // - metadata. - $tests[] = [ - 'links' => [ + $tests['one group, one dynamic path argument, metadata'] = [ + [ 'views_ui_edit' => [ 'route_parameters' => [ 'view' => 'frontpage', @@ -76,14 +66,11 @@ public function _contextual_links_id_testcases() { ], ], ], - 'id' => 'views_ui_edit:view=frontpage:location=page&display=page_1&langcode=en', + 'views_ui_edit:view=frontpage:location=page&display=page_1&langcode=en', ]; - // Test branch conditions: - // - multiple groups. - // - multiple dynamic path arguments. - $tests[] = [ - 'links' => [ + $tests['multiple groups, multiple dynamic path arguments'] = [ + [ 'node' => [ 'route_parameters' => [ 'node' => '14031991', @@ -92,9 +79,9 @@ public function _contextual_links_id_testcases() { ], 'foo' => [ 'route_parameters' => [ - 'bar', + 0 => 'bar', 'key' => 'baz', - 'qux', + 1 => 'qux', ], 'metadata' => ['langcode' => 'en'], ], @@ -103,30 +90,42 @@ public function _contextual_links_id_testcases() { 'metadata' => ['langcode' => 'en'], ], ], - 'id' => 'node:node=14031991:langcode=en|foo:0=bar&key=baz&1=qux:langcode=en|edge:0=20011988:langcode=en', + 'node:node=14031991:langcode=en|foo:0=bar&key=baz&1=qux:langcode=en|edge:0=20011988:langcode=en', ]; return $tests; } /** - * Tests _contextual_links_to_id(). + * Tests the conversion from contextual links to IDs. + * + * @param array $links + * The #contextual_links property value array. + * @param string $id + * The serialized representation of the passed links. + * + * @covers ::_contextual_links_to_id + * + * @dataProvider contextualLinksDataProvider */ - public function testContextualLinksToId() { - $tests = $this->_contextual_links_id_testcases(); - foreach ($tests as $test) { - $this->assertSame($test['id'], _contextual_links_to_id($test['links'])); - } + public function testContextualLinksToId(array $links, string $id) { + $this->assertSame($id, _contextual_links_to_id($links)); } /** - * Tests _contextual_id_to_links(). + * Tests the conversion from contextual ID to links. + * + * @param array $links + * The #contextual_links property value array. + * @param string $id + * The serialized representation of the passed links. + * + * @covers ::_contextual_id_to_links + * + * @dataProvider contextualLinksDataProvider */ - public function testContextualIdToLinks() { - $tests = $this->_contextual_links_id_testcases(); - foreach ($tests as $test) { - $this->assertSame($test['links'], _contextual_id_to_links($test['id'])); - } + public function testContextualIdToLinks(array $links, string $id) { + $this->assertSame($links, _contextual_id_to_links($id)); } } diff --git a/web/core/modules/datetime/tests/src/Functional/Views/FilterDateTest.php b/web/core/modules/datetime/tests/src/Functional/Views/FilterDateTest.php index 7d0bb6c455cb8bdd4012d4327ee1c4736eb62c62..ce3f43014533d4d30adc3f4f223acd9db158ee90 100644 --- a/web/core/modules/datetime/tests/src/Functional/Views/FilterDateTest.php +++ b/web/core/modules/datetime/tests/src/Functional/Views/FilterDateTest.php @@ -198,4 +198,55 @@ protected function assertIds(array $expected_ids = []): void { $this->assertEquals($expected_ids, $actual_ids); } + /** + * Tests exposed date filters with a pager. + */ + public function testExposedFilterWithPager() { + // Expose the empty and not empty operators in a grouped filter. + $this->drupalGet('admin/structure/views/nojs/handler/test_filter_datetime/default/filter/' . $this->fieldName . '_value'); + $this->submitForm([], t('Expose filter')); + + $edit = []; + $edit['options[operator]'] = '>'; + + $this->submitForm($edit, 'Apply'); + + // Expose the view and set the pager to 2 items. + $path = 'test_filter_datetime-path'; + $this->drupalGet('admin/structure/views/view/test_filter_datetime/edit'); + $this->submitForm([], 'Add Page'); + $this->drupalGet('admin/structure/views/nojs/display/test_filter_datetime/page_1/path'); + $this->submitForm(['path' => $path], 'Apply'); + $this->drupalGet('admin/structure/views/nojs/display/test_filter_datetime/default/pager_options'); + $this->submitForm(['pager_options[items_per_page]' => 2], 'Apply'); + $this->submitForm([], t('Save')); + + // Assert the page without filters. + $this->drupalGet($path); + $results = $this->cssSelect('.views-row'); + $this->assertCount(2, $results); + $this->assertSession()->pageTextContains('Next'); + + // Assert the page with filter in the future, one results without pager. + $page = $this->getSession()->getPage(); + $now = \Drupal::time()->getRequestTime(); + $page->fillField($this->fieldName . '_value', DrupalDateTime::createFromTimestamp($now + 1)->format('Y-m-d H:i:s')); + $page->pressButton('Apply'); + + $results = $this->cssSelect('.views-row'); + $this->assertCount(1, $results); + $this->assertSession()->pageTextNotContains('Next'); + + // Assert the page with filter in the past, 3 results with pager. + $page->fillField($this->fieldName . '_value', DrupalDateTime::createFromTimestamp($now - 1000000)->format('Y-m-d H:i:s')); + $this->getSession()->getPage()->pressButton('Apply'); + $results = $this->cssSelect('.views-row'); + $this->assertCount(2, $results); + $this->assertSession()->pageTextContains('Next'); + $page->clickLink('2'); + $results = $this->cssSelect('.views-row'); + $this->assertCount(1, $results); + + } + } diff --git a/web/core/modules/file/src/FileRepository.php b/web/core/modules/file/src/FileRepository.php index 3ec359c61db770d5978014b872703cbf3ac75cfc..1a2d08d75af16cc0fb6577f74e8134fc4ea983f6 100644 --- a/web/core/modules/file/src/FileRepository.php +++ b/web/core/modules/file/src/FileRepository.php @@ -88,7 +88,7 @@ public function __construct(FileSystemInterface $fileSystem, StreamWrapperManage */ public function writeData(string $data, string $destination, int $replace = FileSystemInterface::EXISTS_RENAME): FileInterface { if (!$this->streamWrapperManager->isValidUri($destination)) { - throw new InvalidStreamWrapperException(sprintf('Invalid stream wrapper: %destination', ['%destination' => $destination])); + throw new InvalidStreamWrapperException("Invalid stream wrapper: {$destination}"); } $uri = $this->fileSystem->saveData($data, $destination, $replace); return $this->createOrUpdate($uri, $destination, $replace === FileSystemInterface::EXISTS_RENAME); @@ -132,7 +132,7 @@ protected function createOrUpdate(string $uri, string $destination, bool $rename */ public function copy(FileInterface $source, string $destination, int $replace = FileSystemInterface::EXISTS_RENAME): FileInterface { if (!$this->streamWrapperManager->isValidUri($destination)) { - throw new InvalidStreamWrapperException(sprintf('Invalid stream wrapper: %destination', ['%destination' => $destination])); + throw new InvalidStreamWrapperException("Invalid stream wrapper: {$destination}"); } $uri = $this->fileSystem->copy($source->getFileUri(), $destination, $replace); @@ -166,7 +166,7 @@ public function copy(FileInterface $source, string $destination, int $replace = */ public function move(FileInterface $source, string $destination, int $replace = FileSystemInterface::EXISTS_RENAME): FileInterface { if (!$this->streamWrapperManager->isValidUri($destination)) { - throw new InvalidStreamWrapperException(sprintf('Invalid stream wrapper: %destination', ['%destination' => $destination])); + throw new InvalidStreamWrapperException("Invalid stream wrapper: {$destination}"); } $uri = $this->fileSystem->move($source->getFileUri(), $destination, $replace); $delete_source = FALSE; diff --git a/web/core/modules/file/tests/src/Kernel/CopyTest.php b/web/core/modules/file/tests/src/Kernel/CopyTest.php index 668f99ae408938cd0257b4aa1cfa165292ad3033..9c8e79766b79dff48650732634f4c97c0b6f3aad 100644 --- a/web/core/modules/file/tests/src/Kernel/CopyTest.php +++ b/web/core/modules/file/tests/src/Kernel/CopyTest.php @@ -185,6 +185,7 @@ public function testExistingError() { */ public function testInvalidStreamWrapper() { $this->expectException(InvalidStreamWrapperException::class); + $this->expectExceptionMessage('Invalid stream wrapper: foo://'); $source = $this->createFile(); $this->fileRepository->copy($source, 'foo://'); } diff --git a/web/core/modules/file/tests/src/Kernel/FileRepositoryTest.php b/web/core/modules/file/tests/src/Kernel/FileRepositoryTest.php index 7db4dd08b9fcc82d90613995dcf7a95eb4516c2d..b7790a659c4938829493fa3fbc99a3169b1d55c4 100644 --- a/web/core/modules/file/tests/src/Kernel/FileRepositoryTest.php +++ b/web/core/modules/file/tests/src/Kernel/FileRepositoryTest.php @@ -170,6 +170,7 @@ public function testExistingError() { */ public function testInvalidStreamWrapper() { $this->expectException(InvalidStreamWrapperException::class); + $this->expectExceptionMessage('Invalid stream wrapper: foo://'); $this->fileRepository->writeData('asdf', 'foo://'); } diff --git a/web/core/modules/file/tests/src/Kernel/MoveTest.php b/web/core/modules/file/tests/src/Kernel/MoveTest.php index 389b86210fe679a9b22e6b2cbf735c4d6899832f..c48ed721cd095e1e3183f582afd8c1e873589af4 100644 --- a/web/core/modules/file/tests/src/Kernel/MoveTest.php +++ b/web/core/modules/file/tests/src/Kernel/MoveTest.php @@ -209,6 +209,7 @@ public function testExistingError() { */ public function testInvalidStreamWrapper() { $this->expectException(InvalidStreamWrapperException::class); + $this->expectExceptionMessage('Invalid stream wrapper: foo://'); $source = $this->createFile(); $this->fileRepository->move($source, 'foo://'); } diff --git a/web/core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php b/web/core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php index 2f358158f5ebeb0c5c384748335ae16a2ef77073..0099573062940f1583d1f3ddba179cd2b40cfc35 100644 --- a/web/core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php +++ b/web/core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php @@ -607,21 +607,14 @@ protected static function getExpectedCollectionCacheability(AccountInterface $ac /** * Sets up the necessary authorization. * - * In case of a test verifying publicly accessible REST resources: grant - * permissions to the anonymous user role. - * - * In case of a test verifying behavior when using a particular authentication - * provider: create a user with a particular set of permissions. - * * Because of the $method parameter, it's possible to first set up - * authentication for only GET, then add POST, et cetera. This then also + * authorization for only GET, then add POST, et cetera. This then also * allows for verifying a 403 in case of missing authorization. * * @param string $method - * The HTTP method for which to set up authentication. + * The HTTP method for which to set up authorization. * - * @see ::grantPermissionsToAnonymousRole() - * @see ::grantPermissionsToAuthenticatedRole() + * @see ::grantPermissionsToTestedRole() */ abstract protected function setUpAuthorization($method); diff --git a/web/core/modules/language/src/ConfigurableLanguageManager.php b/web/core/modules/language/src/ConfigurableLanguageManager.php index 9dd65ff4953a215e018834b8033fef1416e92306..b72f9b37098383103d95079f223c750e6ae243b1 100644 --- a/web/core/modules/language/src/ConfigurableLanguageManager.php +++ b/web/core/modules/language/src/ConfigurableLanguageManager.php @@ -408,7 +408,23 @@ public function getLanguageSwitchLinks($type, Url $url) { $reflector = new \ReflectionClass($method['class']); if ($reflector->implementsInterface('\Drupal\language\LanguageSwitcherInterface')) { + $original_languages = $this->negotiatedLanguages; $result = $this->negotiator->getNegotiationMethodInstance($method_id)->getLanguageSwitchLinks($this->requestStack->getCurrentRequest(), $type, $url); + $result = array_filter($result, function (array $link): bool { + $url = $link['url'] ?? NULL; + $language = $link['language'] ?? NULL; + if ($language instanceof LanguageInterface) { + $this->negotiatedLanguages[LanguageInterface::TYPE_CONTENT] = $language; + $this->negotiatedLanguages[LanguageInterface::TYPE_INTERFACE] = $language; + } + try { + return $url instanceof Url && $url->access(); + } + catch (\Exception $e) { + return FALSE; + } + }); + $this->negotiatedLanguages = $original_languages; if (!empty($result)) { // Allow modules to provide translations for specific links. diff --git a/web/core/modules/language/src/Plugin/Block/LanguageBlock.php b/web/core/modules/language/src/Plugin/Block/LanguageBlock.php index 2765971b8e6d478fdf071d2c50560538e4f1d6fa..58307583481f2fccf8c43af43c27a1e72f0f2d5b 100644 --- a/web/core/modules/language/src/Plugin/Block/LanguageBlock.php +++ b/web/core/modules/language/src/Plugin/Block/LanguageBlock.php @@ -83,9 +83,8 @@ protected function blockAccess(AccountInterface $account) { */ public function build() { $build = []; - $route_name = $this->pathMatcher->isFrontPage() ? '<front>' : '<current>'; $type = $this->getDerivativeId(); - $links = $this->languageManager->getLanguageSwitchLinks($type, Url::fromRoute($route_name)); + $links = $this->languageManager->getLanguageSwitchLinks($type, Url::fromRouteMatch(\Drupal::routeMatch())); if (isset($links->links)) { $build = [ diff --git a/web/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationContentEntity.php b/web/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationContentEntity.php index 11c59adb4ac382074c22108140b4bb9150d73c7a..2c5157d20d611ec83eb6c75c0af4efdcc64e75d9 100644 --- a/web/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationContentEntity.php +++ b/web/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationContentEntity.php @@ -139,7 +139,7 @@ public function processOutbound($path, &$options = [], Request $request = NULL, public function getLanguageSwitchLinks(Request $request, $type, Url $url) { $links = []; $query = []; - parse_str($request->getQueryString(), $query); + parse_str($request->getQueryString() ?? '', $query); foreach ($this->languageManager->getNativeLanguages() as $language) { $langcode = $language->getId(); diff --git a/web/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationSession.php b/web/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationSession.php index f36d43e9465231e3a10f4e5a3c81504dc3e3cb7d..77df05538d957412954c8f7dffaff4f21d0aaaa2 100644 --- a/web/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationSession.php +++ b/web/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationSession.php @@ -125,10 +125,11 @@ public function processOutbound($path, &$options = [], Request $request = NULL, */ public function getLanguageSwitchLinks(Request $request, $type, Url $url) { $links = []; + $query = []; + parse_str($request->getQueryString() ?? '', $query); $config = $this->config->get('language.negotiation')->get('session'); $param = $config['parameter']; $language_query = $_SESSION[$param] ?? $this->languageManager->getCurrentLanguage($type)->getId(); - $query = $request->query->all(); foreach ($this->languageManager->getNativeLanguages() as $language) { $langcode = $language->getId(); diff --git a/web/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationUrl.php b/web/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationUrl.php index 48ee5e028b5c68919d44781d54806b61799918b1..f894f52e80e5c7473f4e6aa3fbe41f740b98cd78 100644 --- a/web/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationUrl.php +++ b/web/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationUrl.php @@ -195,7 +195,8 @@ public function processOutbound($path, &$options = [], Request $request = NULL, */ public function getLanguageSwitchLinks(Request $request, $type, Url $url) { $links = []; - $query = $request->query->all(); + $query = []; + parse_str($request->getQueryString() ?? '', $query); foreach ($this->languageManager->getNativeLanguages() as $language) { $links[$language->getId()] = [ diff --git a/web/core/modules/layout_builder/layout_builder.module b/web/core/modules/layout_builder/layout_builder.module index 1646139aa7bf8aed43d3d91c698a6311746a7215..a6e9981fe049a36e0880cfe3dc8f27ff28254448 100644 --- a/web/core/modules/layout_builder/layout_builder.module +++ b/web/core/modules/layout_builder/layout_builder.module @@ -158,7 +158,7 @@ function layout_builder_entity_view_alter(array &$build, EntityInterface $entity // If the entity is displayed within a Layout Builder block and the current // route is in the Layout Builder UI, then remove all contextual link // placeholders. - if ($display instanceof LayoutBuilderEntityViewDisplay && strpos($route_name, 'layout_builder.') === 0) { + if ($route_name && $display instanceof LayoutBuilderEntityViewDisplay && strpos($route_name, 'layout_builder.') === 0) { unset($build['#contextual_links']); } } diff --git a/web/core/modules/layout_builder/src/Form/MoveBlockForm.php b/web/core/modules/layout_builder/src/Form/MoveBlockForm.php index d051de47e8392c11a99010f97c2a319bb5bf4c06..beb3aa9757250d74e7b1bc256320dbe221b7a0f4 100644 --- a/web/core/modules/layout_builder/src/Form/MoveBlockForm.php +++ b/web/core/modules/layout_builder/src/Form/MoveBlockForm.php @@ -190,6 +190,7 @@ public function buildForm(array $form, FormStateInterface $form_state, SectionSt if (!isset($components[$uuid])) { $components[$uuid] = $sections[$delta]->getComponent($uuid); } + $state_weight_delta = round(count($components) / 2); foreach ($components as $component_uuid => $component) { /** @var \Drupal\Core\Block\BlockPluginInterface $plugin */ $plugin = $component->getPlugin(); @@ -222,6 +223,7 @@ public function buildForm(array $form, FormStateInterface $form_state, SectionSt '#attributes' => [ 'class' => ['table-sort-weight'], ], + '#delta' => $state_weight_delta, ], ]; } diff --git a/web/core/modules/layout_builder/tests/src/FunctionalJavascript/MoveBlockFormTest.php b/web/core/modules/layout_builder/tests/src/FunctionalJavascript/MoveBlockFormTest.php index d6b0093271e56a3be23a525d5d65b687a07ae763..4f739703066c1662fe86a6d8858029e48f58d1c6 100644 --- a/web/core/modules/layout_builder/tests/src/FunctionalJavascript/MoveBlockFormTest.php +++ b/web/core/modules/layout_builder/tests/src/FunctionalJavascript/MoveBlockFormTest.php @@ -96,6 +96,7 @@ protected function setUp(): void { */ public function testMoveBlock() { $page = $this->getSession()->getPage(); + $assert_session = $this->assertSession(); // Reorder body field in current region. $this->openBodyMoveForm(1, 'content', ['Links', 'Body (current)']); @@ -135,6 +136,47 @@ public function testMoveBlock() { $this->assertBlockTable(['Body (current)']); $page->pressButton('Move'); $this->assertRegionBlocksOrder(0, 'second', ['.block-field-blocknodebundle-with-section-fieldbody']); + + // The weight element uses -10 to 10 by default, which can cause bugs. + // Add 25 'Powered by Drupal' blocks to a new section. + $page->clickLink('Add section'); + $assert_session->waitForElementVisible('css', '#drupal-off-canvas'); + $assert_session->assertWaitOnAjaxRequest(); + $page->clickLink('One column'); + $assert_session->assertWaitOnAjaxRequest(); + $this->assertNotEmpty($assert_session->waitForElementVisible('css', 'input[value="Add section"]')); + $page->pressButton('Add section'); + $assert_session->assertNoElementAfterWait('css', '#drupal-off-canvas'); + $large_block_number = 25; + for ($i = 0; $i < $large_block_number; $i++) { + $assert_session->elementExists('css', '[data-layout-delta="0"].layout--onecol [data-region="content"] .layout-builder__add-block')->click(); + $this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas a:contains("Powered by Drupal")')); + $assert_session->assertWaitOnAjaxRequest(); + $page->clickLink('Powered by Drupal'); + $this->assertNotEmpty($assert_session->waitForElementVisible('css', 'input[value="Add block"]')); + $assert_session->assertWaitOnAjaxRequest(); + $page->pressButton('Add block'); + $assert_session->assertNoElementAfterWait('css', '#drupal-off-canvas'); + } + $first_region_block_locator = '[data-layout-delta="0"].layout--onecol [data-region="content"] [data-layout-block-uuid]'; + $assert_session->elementsCount('css', $first_region_block_locator, $large_block_number); + + // Move the Body block to the end of the new section. + $this->openBodyMoveForm(1, 'second', ['Body (current)']); + $page->selectFieldOption('Region', '0:content'); + $expected_block_table = array_fill(0, $large_block_number, 'Powered by Drupal'); + $expected_block_table[] = 'Body (current)'; + $this->assertBlockTable($expected_block_table); + $expected_block_table = array_fill(0, $large_block_number - 1, 'Powered by Drupal'); + $expected_block_table[] = 'Body (current)*'; + $expected_block_table[] = 'Powered by Drupal'; + $this->moveBlockWithKeyboard('up', 'Body', $expected_block_table); + $page->pressButton('Move'); + $assert_session->assertNoElementAfterWait('css', '#drupal-off-canvas'); + // Get all blocks currently in the region. + $blocks = $page->findAll('css', $first_region_block_locator); + // The second to last $block should be the body. + $this->assertTrue($blocks[count($blocks) - 2]->hasClass('block-field-blocknodebundle-with-section-fieldbody')); } /** diff --git a/web/core/modules/link/tests/src/Functional/LinkFieldTest.php b/web/core/modules/link/tests/src/Functional/LinkFieldTest.php index 47ddf484d38424140c8b4c0bacc2a119ef119a72..9f1255bce7bdd2d8335d7d16e01138fc93b6ffea 100644 --- a/web/core/modules/link/tests/src/Functional/LinkFieldTest.php +++ b/web/core/modules/link/tests/src/Functional/LinkFieldTest.php @@ -68,10 +68,24 @@ protected function setUp(): void { ])); } + /** + * Tests the functionality and rendering of the link field. + * + * This is being as one to avoid multiple Drupal install. + */ + public function testLinkField() { + $this->doTestURLValidation(); + $this->doTestLinkTitle(); + $this->doTestLinkFormatter(); + $this->doTestLinkSeparateFormatter(); + $this->doTestEditNonNodeEntityLink(); + $this->doTestLinkTypeOnLinkWidget(); + } + /** * Tests link field URL validation. */ - public function testURLValidation() { + protected function doTestURLValidation() { $field_name = mb_strtolower($this->randomMachineName()); // Create a field with settings to validate. $this->fieldStorage = FieldStorageConfig::create([ @@ -255,7 +269,7 @@ protected function assertInvalidEntries(string $field_name, array $invalid_entri /** * Tests the link title settings of a link field. */ - public function testLinkTitle() { + protected function doTestLinkTitle() { $field_name = mb_strtolower($this->randomMachineName()); // Create a field with settings to validate. $this->fieldStorage = FieldStorageConfig::create([ @@ -380,7 +394,7 @@ public function testLinkTitle() { /** * Tests the default 'link' formatter. */ - public function testLinkFormatter() { + protected function doTestLinkFormatter() { $field_name = mb_strtolower($this->randomMachineName()); // Create a field with settings to validate. $this->fieldStorage = FieldStorageConfig::create([ @@ -537,7 +551,7 @@ public function testLinkFormatter() { * This test is mostly the same as testLinkFormatter(), but they cannot be * merged, since they involve different configuration and output. */ - public function testLinkSeparateFormatter() { + protected function doTestLinkSeparateFormatter() { $field_name = mb_strtolower($this->randomMachineName()); // Create a field with settings to validate. $this->fieldStorage = FieldStorageConfig::create([ @@ -664,7 +678,7 @@ public function testLinkSeparateFormatter() { * a link and also which LinkItemInterface::LINK_* is (EXTERNAL, GENERIC, * INTERNAL). */ - public function testLinkTypeOnLinkWidget() { + protected function doTestLinkTypeOnLinkWidget() { $link_type = LinkItemInterface::LINK_EXTERNAL; $field_name = mb_strtolower($this->randomMachineName()); @@ -702,7 +716,7 @@ public function testLinkTypeOnLinkWidget() { /** * Tests editing a link to a non-node entity. */ - public function testEditNonNodeEntityLink() { + protected function doTestEditNonNodeEntityLink() { $entity_type_manager = \Drupal::entityTypeManager(); $entity_test_storage = $entity_type_manager->getStorage('entity_test'); diff --git a/web/core/modules/media/src/Plugin/Field/FieldFormatter/MediaThumbnailFormatter.php b/web/core/modules/media/src/Plugin/Field/FieldFormatter/MediaThumbnailFormatter.php index dc77b17888d0f299e5f6dca9bf289115ab01670b..83a506c65ee89bb088a0c50257637bc447406ec8 100644 --- a/web/core/modules/media/src/Plugin/Field/FieldFormatter/MediaThumbnailFormatter.php +++ b/web/core/modules/media/src/Plugin/Field/FieldFormatter/MediaThumbnailFormatter.php @@ -183,6 +183,14 @@ public static function isApplicable(FieldDefinitionInterface $field_definition) return ($field_definition->getFieldStorageDefinition()->getSetting('target_type') == 'media'); } + /** + * {@inheritdoc} + */ + protected function checkAccess(EntityInterface $entity) { + return $entity->access('view', NULL, TRUE) + ->andIf(parent::checkAccess($entity)); + } + /** * Get the URL for the media thumbnail. * diff --git a/web/core/modules/menu_link_content/src/Plugin/migrate/source/MenuLink.php b/web/core/modules/menu_link_content/src/Plugin/migrate/source/MenuLink.php index b998b6eb1e870c39dc729a891e350ac433061d9b..0f4676b62bcf824e594fc6111f1184307874a4d2 100644 --- a/web/core/modules/menu_link_content/src/Plugin/migrate/source/MenuLink.php +++ b/web/core/modules/menu_link_content/src/Plugin/migrate/source/MenuLink.php @@ -50,7 +50,11 @@ class MenuLink extends DrupalSqlBase { */ public function query() { $query = $this->select('menu_links', 'ml') - ->fields('ml'); + ->fields('ml') + // Shortcut set links are migrated by the d7_shortcut migration. + // Shortcuts are not used in Drupal 6. + // @see Drupal\shortcut\Plugin\migrate\source\d7\Shortcut::query() + ->condition('ml.menu_name', 'shortcut-set-%', 'NOT LIKE'); $and = $query->andConditionGroup() ->condition('ml.module', 'menu') ->condition('ml.router_path', ['admin/build/menu-customize/%', 'admin/structure/menu/manage/%'], 'NOT IN'); diff --git a/web/core/modules/menu_link_content/tests/src/Kernel/Migrate/d7/MigrateMenuLinkTest.php b/web/core/modules/menu_link_content/tests/src/Kernel/Migrate/d7/MigrateMenuLinkTest.php index cd271f89d516f813b9b0862f18c8fc921c3f9b7c..dbe7d0f1374a538c19a6273cdd4a7a4a57cabc51 100644 --- a/web/core/modules/menu_link_content/tests/src/Kernel/Migrate/d7/MigrateMenuLinkTest.php +++ b/web/core/modules/menu_link_content/tests/src/Kernel/Migrate/d7/MigrateMenuLinkTest.php @@ -112,6 +112,11 @@ public function testMenuLinks() { $this->assertEntity(485, 'en', 'is - The thing about Deep Space 9', 'tools', NULL, TRUE, FALSE, ['attributes' => ['title' => '']], 'entity:node/2', 10); $this->assertEntity(486, 'und', 'is - The thing about Firefly', 'tools', NULL, TRUE, FALSE, ['attributes' => ['title' => '']], 'entity:node/4', 11); $this->assertEntity(487, 'en', 'en - The thing about Firefly', 'tools', NULL, TRUE, FALSE, ['attributes' => ['title' => '']], 'entity:node/4', 12); + + // Test there have been no attempts to stub a shortcut in a MigrationLookup + // process. + $messages = $this->getMigration('d7_menu')->getIdMap()->getMessages()->fetchAll(); + $this->assertCount(0, $messages); } } diff --git a/web/core/modules/menu_link_content/tests/src/Kernel/Plugin/migrate/source/MenuLinkTest.php b/web/core/modules/menu_link_content/tests/src/Kernel/Plugin/migrate/source/MenuLinkTest.php index b8d80c1531806afd711de61a52f9dd49f410cea2..2f6d19a3e1baf591a9ec1ad447b068c34b268118 100644 --- a/web/core/modules/menu_link_content/tests/src/Kernel/Plugin/migrate/source/MenuLinkTest.php +++ b/web/core/modules/menu_link_content/tests/src/Kernel/Plugin/migrate/source/MenuLinkTest.php @@ -241,6 +241,37 @@ public function providerSource() { 'i18n_tsid' => '1', 'skip_translation' => FALSE, ], + [ + // D7 shortcut set link. + 'menu_name' => 'shortcut-set-1', + 'mlid' => 301, + 'plid' => 0, + 'link_path' => 'node/add', + 'router_path' => 'node/add', + 'link_title' => 'Add Content', + 'options' => [], + 'module' => 'menu', + 'hidden' => 0, + 'external' => 0, + 'has_children' => 0, + 'expanded' => 0, + 'weight' => 0, + 'depth' => 1, + 'customized' => 0, + 'p1' => '301', + 'p2' => '0', + 'p3' => '0', + 'p4' => '0', + 'p5' => '0', + 'p6' => '0', + 'p7' => '0', + 'p8' => '0', + 'p9' => '0', + 'updated' => '0', + 'language' => 'und', + 'i18n_tsid' => '0', + 'skip_translation' => TRUE, + ], ]; // Add long link title attributes to source data. diff --git a/web/core/modules/node/node.api.php b/web/core/modules/node/node.api.php index face0558d241da725b3adf6e091e88b4dd906f87..8b01c6632397d6319a4e37a1054cc249c1d91989 100644 --- a/web/core/modules/node/node.api.php +++ b/web/core/modules/node/node.api.php @@ -22,11 +22,8 @@ * "realms". In hook_node_access_records(), the realms and grant IDs are * associated with permission to view, edit, and delete individual nodes. * - * The realms and grant IDs can be arbitrarily defined by your node access - * module; it is common to use role IDs as grant IDs, but that is not required. - * Your module could instead maintain its own list of users, where each list has - * an ID. In that case, the return value of this hook would be an array of the - * list IDs that this user is a member of. + * Grant IDs can be arbitrarily defined by a node access module using a list of + * integer IDs associated with users. * * A node access module may implement as many realms as necessary to properly * define the access privileges for the nodes. Note that the system makes no diff --git a/web/core/modules/responsive_image/responsive_image.module b/web/core/modules/responsive_image/responsive_image.module index 6bd0e51f28b3a5825e3e2679033fe27ffc0b057d..9a5620458993f07296492614ad604c3167d908ac 100644 --- a/web/core/modules/responsive_image/responsive_image.module +++ b/web/core/modules/responsive_image/responsive_image.module @@ -348,8 +348,8 @@ function template_preprocess_responsive_image(&$variables) { * @param array $multipliers * An array with multipliers as keys and image style mappings as values. * - * @return \Drupal\Core\Template\Attribute[] - * An array of attributes for the source tag. + * @return \Drupal\Core\Template\Attribute + * An object of attributes for the source tag. */ function _responsive_image_build_source_attributes(array $variables, BreakpointInterface $breakpoint, array $multipliers) { if ((empty($variables['width']) || empty($variables['height']))) { diff --git a/web/core/modules/system/src/Controller/SystemInfoController.php b/web/core/modules/system/src/Controller/SystemInfoController.php index 76c2c7a377560e174f16111059b7963e8985329e..3cbfa68652f61541f740a377f9d68ab1d0a378f7 100644 --- a/web/core/modules/system/src/Controller/SystemInfoController.php +++ b/web/core/modules/system/src/Controller/SystemInfoController.php @@ -59,7 +59,7 @@ public function status() { public function php() { if (function_exists('phpinfo')) { ob_start(); - phpinfo(); + phpinfo(~ (INFO_VARIABLES | INFO_ENVIRONMENT)); $output = ob_get_clean(); } else { diff --git a/web/core/modules/system/src/Form/ModulesUninstallForm.php b/web/core/modules/system/src/Form/ModulesUninstallForm.php index 82c014c230e79a4b130f327830e66b4d883d954b..483959410cf9c525afba640d0d0b29a3753e90b0 100644 --- a/web/core/modules/system/src/Form/ModulesUninstallForm.php +++ b/web/core/modules/system/src/Form/ModulesUninstallForm.php @@ -146,8 +146,22 @@ public function buildForm(array $form, FormStateInterface $form_state) { return $form; } - // Sort all modules by their name. - uasort($uninstallable, [ModuleExtensionList::class, 'sortByName']); + // Deprecated and obsolete modules should appear at the top of the + // uninstallation list. + $unstable_lifecycle = array_flip([ + ExtensionLifecycle::DEPRECATED, + ExtensionLifecycle::OBSOLETE, + ]); + + // Sort all modules by their lifecycle identifier and name. + uasort($uninstallable, function ($a, $b) use ($unstable_lifecycle) { + $lifecycle_a = isset($unstable_lifecycle[$a->info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER]]) ? -1 : 1; + $lifecycle_b = isset($unstable_lifecycle[$b->info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER]]) ? -1 : 1; + if ($lifecycle_a === $lifecycle_b) { + return ModuleExtensionList::sortByName($a, $b); + } + return $lifecycle_a <=> $lifecycle_b; + }); $validation_reasons = $this->moduleInstaller->validateUninstall(array_keys($uninstallable)); $form['uninstall'] = ['#tree' => TRUE]; diff --git a/web/core/modules/system/tests/modules/entity_test/src/EntityTestAccessControlHandler.php b/web/core/modules/system/tests/modules/entity_test/src/EntityTestAccessControlHandler.php index 4e7b4dc2a0475f2c61e1466371fd918c056f4d82..7aab7849b521619623e87416c058859198d8871c 100644 --- a/web/core/modules/system/tests/modules/entity_test/src/EntityTestAccessControlHandler.php +++ b/web/core/modules/system/tests/modules/entity_test/src/EntityTestAccessControlHandler.php @@ -55,6 +55,9 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter return AccessResult::allowedIfHasPermission($account, 'view test entity translations'); } } + if ($entity instanceof EntityPublishedInterface && !$entity->isPublished()) { + return AccessResult::neutral('Unpublished entity'); + } return AccessResult::allowedIfHasPermission($account, 'view test entity'); } elseif (in_array($operation, ['update', 'delete'])) { diff --git a/web/core/modules/system/tests/modules/form_test/src/Form/JavascriptStatesForm.php b/web/core/modules/system/tests/modules/form_test/src/Form/JavascriptStatesForm.php index 4d589d349d35ca99f9bb08a77db6642e5eb79614..851c55e2fc533deabcc2713ee659c2d202dfce86 100644 --- a/web/core/modules/system/tests/modules/form_test/src/Form/JavascriptStatesForm.php +++ b/web/core/modules/system/tests/modules/form_test/src/Form/JavascriptStatesForm.php @@ -31,6 +31,52 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#type' => 'textfield', '#title' => 'Textfield trigger', ]; + $form['radios_opposite1'] = [ + '#type' => 'radios', + '#title' => 'Radios opposite 1', + '#options' => [ + 0 => 'zero', + 1 => 'one', + ], + '#default_value' => 0, + 0 => [ + '#states' => [ + 'checked' => [ + ':input[name="radios_opposite2"]' => ['value' => 1], + ], + ], + ], + 1 => [ + '#states' => [ + 'checked' => [ + ':input[name="radios_opposite2"]' => ['value' => 0], + ], + ], + ], + ]; + $form['radios_opposite2'] = [ + '#type' => 'radios', + '#title' => 'Radios opposite 2', + '#options' => [ + 0 => 'zero', + 1 => 'one', + ], + '#default_value' => 1, + 0 => [ + '#states' => [ + 'checked' => [ + ':input[name="radios_opposite1"]' => ['value' => 1], + ], + ], + ], + 1 => [ + '#states' => [ + 'checked' => [ + ':input[name="radios_opposite1"]' => ['value' => 0], + ], + ], + ], + ]; $form['radios_trigger'] = [ '#type' => 'radios', '#title' => 'Radios trigger', @@ -60,6 +106,10 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#empty_value' => '_none', '#empty_option' => '- None -', ]; + $form['number_trigger'] = [ + '#type' => 'number', + '#title' => 'Number trigger', + ]; // Tested fields. // Checkbox trigger. @@ -328,6 +378,17 @@ public function buildForm(array $form, FormStateInterface $form_state) { ], ]; + // Number triggers. + $form['item_visible_when_number_trigger_filled_by_spinner'] = [ + '#type' => 'item', + '#title' => 'Item visible when number trigger filled by spinner widget', + '#states' => [ + 'visible' => [ + ':input[name="number_trigger"]' => ['filled' => TRUE], + ], + ], + ]; + $form['select'] = [ '#type' => 'select', '#title' => 'select 1', diff --git a/web/core/modules/system/tests/src/Functional/Module/UninstallTest.php b/web/core/modules/system/tests/src/Functional/Module/UninstallTest.php index a5ecb9c2420470bb40d5c3875315b52591520d01..1fb8c5234de00f387280a1b0c58f7593c285b27a 100644 --- a/web/core/modules/system/tests/src/Functional/Module/UninstallTest.php +++ b/web/core/modules/system/tests/src/Functional/Module/UninstallTest.php @@ -82,6 +82,20 @@ public function testUninstallPage() { $this->assertSession()->elementExists('xpath', "//a[contains(@aria-label, 'View information on the Obsolete status of the module System obsolete status test')]"); $this->assertSession()->elementExists('xpath', "//a[contains(@href, 'https://example.com/obsolete')]"); + $form = $this->assertSession()->elementExists('xpath', "//form[@id='system-modules-uninstall']"); + $form_html = $form->getOuterHtml(); + + // Select the first stable module on the uninstall list. + $module_stable = $this->assertSession()->elementExists('xpath', "//label[contains(@class, 'module-name') and not(./a[contains(@class, 'module-link--non-stable')])]")->getOuterHtml(); + + // Select the unstable modules (deprecated, and obsolete). + $module_unstable_1 = $this->assertSession()->elementExists('xpath', "//label[./a[contains(@aria-label, 'View information on the Deprecated status of the module Deprecated module')]]")->getOuterHtml(); + $module_unstable_2 = $this->assertSession()->elementExists('xpath', "//label[./a[contains(@aria-label, 'View information on the Obsolete status of the module System obsolete status test')]]")->getOuterHtml(); + + // Check that all unstable modules appear before the first stable module. + $this->assertGreaterThan(strpos($form_html, $module_unstable_1), strpos($form_html, $module_stable)); + $this->assertGreaterThan(strpos($form_html, $module_unstable_2), strpos($form_html, $module_stable)); + foreach (\Drupal::service('extension.list.module')->getAllInstalledInfo() as $module => $info) { $field_name = "uninstall[$module]"; if (!empty($info['required'])) { diff --git a/web/core/modules/system/tests/src/Kernel/Mail/MailTest.php b/web/core/modules/system/tests/src/Kernel/Mail/MailTest.php index 1eed7c500777e24ed0f4ab362533684dd1aaed40..76167feaa3947b8a332ebde61d3234213ed3ac6b 100644 --- a/web/core/modules/system/tests/src/Kernel/Mail/MailTest.php +++ b/web/core/modules/system/tests/src/Kernel/Mail/MailTest.php @@ -40,6 +40,12 @@ protected function setUp(): void { parent::setUp(); $this->installEntitySchema('user'); $this->installEntitySchema('file'); + + // Set required site configuration. + $this->config('system.site') + ->set('mail', 'mailtest@example.com') + ->set('name', 'Drupal') + ->save(); } /** @@ -115,12 +121,6 @@ public function testCancelMessage() { public function testFromAndReplyToHeader() { $language = \Drupal::languageManager()->getCurrentLanguage(); - // Set required site configuration. - $this->config('system.site') - ->set('mail', 'mailtest@example.com') - ->set('name', 'Drupal') - ->save(); - // Reset the state variable that holds sent messages. \Drupal::state()->set('system.test_mail_collector', []); // Send an email with a reply-to address specified. @@ -153,6 +153,15 @@ public function testFromAndReplyToHeader() { // Errors-to header must not be set, it is deprecated. $this->assertFalse(isset($sent_message['headers']['Errors-To'])); + // Test that From names containing commas work as expected. + $this->config('system.site')->set('name', 'Foo, Bar, and Baz')->save(); + // Send an email and check that the From-header contains the site name. + \Drupal::service('plugin.manager.mail')->mail('mail_cancel_test', 'from_test', 'from_test@example.com', $language); + $captured_emails = \Drupal::state()->get('system.test_mail_collector'); + $sent_message = end($captured_emails); + // From header contains the quoted site name with commas. + $this->assertEquals('"Foo, Bar, and Baz" <mailtest@example.com>', $sent_message['headers']['From']); + // Test RFC-2822 rules are respected for 'display-name' component of // 'From:' header. Specials characters are not allowed, so randomly add one // of them to the site name and check the string is wrapped in quotes. Also diff --git a/web/core/modules/system/tests/src/Kernel/System/FloodTest.php b/web/core/modules/system/tests/src/Kernel/System/FloodTest.php index 97b78aa974e644f9aa5a111e880890fdeeb94c98..cc7b04bb4d4db44949df9f9e1390b1b29f89ec7c 100644 --- a/web/core/modules/system/tests/src/Kernel/System/FloodTest.php +++ b/web/core/modules/system/tests/src/Kernel/System/FloodTest.php @@ -3,7 +3,6 @@ namespace Drupal\Tests\system\Kernel\System; use Drupal\Core\Flood\DatabaseBackend; -use Drupal\Core\Flood\MemoryBackend; use Drupal\KernelTests\KernelTestBase; /** @@ -46,46 +45,6 @@ public function testCleanUp() { $this->assertFalse($flood->isAllowed($name, $threshold)); } - /** - * Tests flood control memory backend. - */ - public function testMemoryBackend() { - $threshold = 1; - $window_expired = -1; - $name = 'flood_test_cleanup'; - - $request_stack = \Drupal::service('request_stack'); - $flood = new MemoryBackend($request_stack); - $this->assertTrue($flood->isAllowed($name, $threshold)); - // Register expired event. - $flood->register($name, $window_expired); - // Verify event is not allowed. - $this->assertFalse($flood->isAllowed($name, $threshold)); - // Run cron and verify event is now allowed. - $flood->garbageCollection(); - $this->assertTrue($flood->isAllowed($name, $threshold)); - - // Register unexpired event. - $flood->register($name); - // Verify event is not allowed. - $this->assertFalse($flood->isAllowed($name, $threshold)); - // Run cron and verify event is still not allowed. - $flood->garbageCollection(); - $this->assertFalse($flood->isAllowed($name, $threshold)); - } - - /** - * Tests memory backend records events to the nearest microsecond. - */ - public function testMemoryBackendThreshold() { - $request_stack = \Drupal::service('request_stack'); - $flood = new MemoryBackend($request_stack); - $flood->register('new event'); - $this->assertTrue($flood->isAllowed('new event', '2')); - $flood->register('new event'); - $this->assertFalse($flood->isAllowed('new event', '2')); - } - /** * Tests flood control database backend. */ diff --git a/web/core/modules/tour/src/TourViewBuilder.php b/web/core/modules/tour/src/TourViewBuilder.php index 61e17a732e30741d198d3c60a166d09172188ac0..105563ad5fccd83ae85aee9a30b8a58b2f521551 100644 --- a/web/core/modules/tour/src/TourViewBuilder.php +++ b/web/core/modules/tour/src/TourViewBuilder.php @@ -60,7 +60,7 @@ public function viewMultiple(array $entities = [], $view_mode = 'full', $langcod $body = (string) \Drupal::service('renderer')->renderPlain($body_render_array); $output = [ 'body' => $body, - 'title' => Html::escape($tip->getLabel()), + 'title' => $tip->getLabel(), ]; $selector = $tip->getSelector(); diff --git a/web/core/modules/tour/tests/src/Functional/TourTest.php b/web/core/modules/tour/tests/src/Functional/TourTest.php index 32e958dbbd6273611cbd7963d5e6739d30e1cc45..aaf2a781a28395061cfe4e33539f98204da7101b 100644 --- a/web/core/modules/tour/tests/src/Functional/TourTest.php +++ b/web/core/modules/tour/tests/src/Functional/TourTest.php @@ -158,7 +158,7 @@ public function testTourFunctionality() { 'tour-test-1' => [ 'id' => 'tour-code-test-1', 'plugin' => 'text', - 'label' => 'The rain in spain', + 'label' => 'The rain in spain is <strong>strong</strong>', 'body' => 'Falls mostly on the plain.', 'weight' => '100', 'selector' => '#tour-code-test-1', @@ -194,7 +194,7 @@ public function testTourFunctionality() { $elements = $this->findTip([ 'id' => 'tour-code-test-1', - 'title' => 'The rain in spain', + 'title' => 'The rain in spain is <strong>strong</strong>', ]); $this->assertCount(1, $elements, 'Found the required tip markup for tip 4'); diff --git a/web/core/modules/user/config/schema/user.schema.yml b/web/core/modules/user/config/schema/user.schema.yml index 99285373c3d16604d6ce5422cf3ef24398a55151..b778163bf5c0a85a6dd3351f1db4987d506679a3 100644 --- a/web/core/modules/user/config/schema/user.schema.yml +++ b/web/core/modules/user/config/schema/user.schema.yml @@ -122,6 +122,7 @@ user.role.*: permissions: type: sequence label: 'Permissions' + orderby: value sequence: type: string label: 'Permission' diff --git a/web/core/modules/user/src/Entity/Role.php b/web/core/modules/user/src/Entity/Role.php index 97b01557194325674f007a6f7e58d39c43f0a2a2..50258f5aca13730c41aff45a41b08f93e81fc606 100644 --- a/web/core/modules/user/src/Entity/Role.php +++ b/web/core/modules/user/src/Entity/Role.php @@ -185,12 +185,6 @@ public function preSave(EntityStorageInterface $storage) { }); $this->weight = $max + 1; } - - if (!$this->isSyncing()) { - // Permissions are always ordered alphabetically to avoid conflicts in the - // exported configuration. - sort($this->permissions); - } } /** diff --git a/web/core/modules/user/src/Plugin/views/field/Roles.php b/web/core/modules/user/src/Plugin/views/field/Roles.php index ad504200baab63dfa30eaff228032b3f8e97d187..cf36ba7f4e4680445890610ba6dff86ea990c8a4 100644 --- a/web/core/modules/user/src/Plugin/views/field/Roles.php +++ b/web/core/modules/user/src/Plugin/views/field/Roles.php @@ -85,7 +85,7 @@ public function preRender(&$values) { $sorted_keys = array_intersect_key($ordered_roles, $user_roles); // Merge with the unsorted array of role information which has the // effect of sorting it. - $user_roles = array_merge($sorted_keys, $user_roles); + $user_roles = array_replace($sorted_keys, $user_roles); } } } diff --git a/web/core/modules/user/tests/src/Functional/Update/UserUpdateRoleDependenciesTest.php b/web/core/modules/user/tests/src/Functional/Update/UserUpdateRoleDependenciesTest.php index c865a05b291cda0238323f98cd068758493e59cd..9140a06b9a05faa69491465e10cd17f6ed3821da 100644 --- a/web/core/modules/user/tests/src/Functional/Update/UserUpdateRoleDependenciesTest.php +++ b/web/core/modules/user/tests/src/Functional/Update/UserUpdateRoleDependenciesTest.php @@ -47,7 +47,7 @@ public function testRolePermissions() { $this->drupalLogin($this->createUser(['access site reports'])); $this->drupalGet('admin/reports/dblog', ['query' => ['type[]' => 'update']]); $this->clickLink('The role Authenticated user has had the following non-…'); - $this->assertSession()->pageTextContains('The role Authenticated user has had the following non-existent permission(s) removed: use text format plain_text, does_not_exist.'); + $this->assertSession()->pageTextContains('The role Authenticated user has had the following non-existent permission(s) removed: does_not_exist, use text format plain_text.'); $this->getSession()->back(); $this->clickLink('The role Anonymous user has had the following non-…'); $this->assertSession()->pageTextContains('The role Anonymous user has had the following non-existent permission(s) removed: use text format plain_text.'); diff --git a/web/core/modules/user/tests/src/Kernel/UserMailNotifyTest.php b/web/core/modules/user/tests/src/Kernel/UserMailNotifyTest.php index 2038d18c0ec09ad56077627a5e322c6a317e4db0..a8ac6824847b3b695e9469601b26df7e0e0f9e7b 100644 --- a/web/core/modules/user/tests/src/Kernel/UserMailNotifyTest.php +++ b/web/core/modules/user/tests/src/Kernel/UserMailNotifyTest.php @@ -82,6 +82,7 @@ public function userMailsProvider() { */ public function testUserMailsSent($op, array $mail_keys) { $this->installConfig('user'); + $this->config('system.site')->set('mail', 'test@example.com')->save(); $this->config('user.settings')->set('notify.' . $op, TRUE)->save(); $return = _user_mail_notify($op, $this->createUser()); $this->assertTrue($return); @@ -173,6 +174,7 @@ public function testUserRecoveryMailLanguage() { // Recovery email should respect user preferred langcode by default if // langcode not set. + $this->config('system.site')->set('mail', 'test@example.com')->save(); $params['account'] = $user; $default_email = \Drupal::service('plugin.manager.mail')->mail('user', 'password_reset', $user->getEmail(), $preferredLangcode, $params); $this->assertTrue($default_email['result']); diff --git a/web/core/modules/user/tests/src/Kernel/UserRoleEntityTest.php b/web/core/modules/user/tests/src/Kernel/UserRoleEntityTest.php index 5d85147ca13b966ebf27bc0c9479128dec8d99d8..aa6f9c4acfc32d3ed214e5a05e7e2d167d6a3d02 100644 --- a/web/core/modules/user/tests/src/Kernel/UserRoleEntityTest.php +++ b/web/core/modules/user/tests/src/Kernel/UserRoleEntityTest.php @@ -46,4 +46,16 @@ public function testGrantingNonExistentPermission() { ->save(); } + public function testPermissionRevokeAndConfigSync() { + $role = Role::create(['id' => 'test_role', 'label' => 'Test role']); + $role->setSyncing(TRUE); + $role->grantPermission('a') + ->grantPermission('b') + ->grantPermission('c') + ->save(); + $this->assertSame(['a', 'b', 'c'], $role->getPermissions()); + $role->revokePermission('b')->save(); + $this->assertSame(['a', 'c'], $role->getPermissions()); + } + } diff --git a/web/core/modules/user/tests/src/Kernel/Views/UserRoleTest.php b/web/core/modules/user/tests/src/Kernel/Views/UserRoleTest.php new file mode 100644 index 0000000000000000000000000000000000000000..28b9e7fae0f5392bbf0a7c7ec4ad7d61c38b5d82 --- /dev/null +++ b/web/core/modules/user/tests/src/Kernel/Views/UserRoleTest.php @@ -0,0 +1,41 @@ +<?php + +namespace Drupal\Tests\user\Kernel\Views; + +use Drupal\Tests\views\Kernel\ViewsKernelTestBase; +use Drupal\user\Entity\Role; +use Drupal\user\Entity\User; +use Drupal\views\Views; + +/** + * Tests rendering when the role is numeric. + * + * @group user + */ +class UserRoleTest extends ViewsKernelTestBase { + + /** + * Tests numeric role. + */ + public function testNumericRole() { + $this->installEntitySchema('user'); + $this->installSchema('user', ['users_data']); + + Role::create(['id' => 123, 'label' => 'Numeric']) + ->save(); + + $user = User::create([ + 'uid' => 2, + 'name' => 'foo', + 'roles' => 123, + ]); + $user->save(); + + $view = Views::getView('user_admin_people'); + $this->executeView($view); + $view->render('user_admin_people'); + $output = $view->field['roles_target_id']->render($view->result[0]); + $this->assertEquals(2, $output); + } + +} diff --git a/web/core/modules/user/user.module b/web/core/modules/user/user.module index 5eb8c5218814ef04cb9568357d9782b25846d91d..8312a1efae042f5de3bde391918c82b7695ce527 100644 --- a/web/core/modules/user/user.module +++ b/web/core/modules/user/user.module @@ -167,7 +167,7 @@ function user_user_presave(UserInterface $account) { * @param string $mail * String with the account's email address. * - * @return object|bool + * @return \Drupal\user\UserInterface|false * A fully-loaded $user object upon successful user load or FALSE if user * cannot be loaded. * @@ -185,7 +185,7 @@ function user_load_by_mail($mail) { * @param string $name * String with the account's user name. * - * @return object|bool + * @return \Drupal\user\UserInterface|false * A fully-loaded $user object upon successful user load or FALSE if user * cannot be loaded. * diff --git a/web/core/modules/user/user.post_update.php b/web/core/modules/user/user.post_update.php index 14662df8ef017cdc5bc7650d6e9e4dafa53de260..7bb42b2d566148d742ca65efcee8128018d7c2f2 100644 --- a/web/core/modules/user/user.post_update.php +++ b/web/core/modules/user/user.post_update.php @@ -47,3 +47,14 @@ function user_post_update_update_roles(&$sandbox = NULL) { ); } } + +/** + * Ensure permissions stored in role configuration are sorted using the schema. + */ +function user_post_update_sort_permissions(&$sandbox = NULL) { + \Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'user_role', function (Role $role) { + $permissions = $role->getPermissions(); + sort($permissions); + return $permissions !== $role->getPermissions(); + }); +} diff --git a/web/core/modules/views/src/Plugin/views/PluginBase.php b/web/core/modules/views/src/Plugin/views/PluginBase.php index 0f588c531cd9bde0df624aaa8a6bc1a0423887b0..9e6d3c05674ed1a83b70df44d6ec4316adf3cab8 100644 --- a/web/core/modules/views/src/Plugin/views/PluginBase.php +++ b/web/core/modules/views/src/Plugin/views/PluginBase.php @@ -89,7 +89,7 @@ abstract class PluginBase extends ComponentPluginBase implements ContainerFactor public $displayHandler; /** - * Plugins's definition. + * Plugins' definition. * * @var array */ diff --git a/web/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php b/web/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php index 303cba292a343651f910f1104b74fef1b36f3d1b..c404d1c86328837f0e5315481c9e61e65346e63b 100644 --- a/web/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php +++ b/web/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php @@ -2231,7 +2231,7 @@ public function elementPreRender(array $element) { $element['#feed_icons'] = !empty($view->feedIcons) ? $view->feedIcons : []; if ($view->display_handler->renderPager()) { - $exposed_input = $view->exposed_raw_input ?? NULL; + $exposed_input = $view->getExposedInput(); $element['#pager'] = $view->renderPager($exposed_input); } diff --git a/web/core/modules/views/src/Plugin/views/field/TimeInterval.php b/web/core/modules/views/src/Plugin/views/field/TimeInterval.php index 3c288fe61e4d80167dceec4d112066a8ec81bfc2..c3eba54387e015da7987bdd7ca1bd707139c23d5 100644 --- a/web/core/modules/views/src/Plugin/views/field/TimeInterval.php +++ b/web/core/modules/views/src/Plugin/views/field/TimeInterval.php @@ -82,7 +82,10 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { */ public function render(ResultRow $values) { $value = $values->{$this->field_alias}; - return $this->dateFormatter->formatInterval((int) $value, $this->options['granularity'] ?? 2); + if ($value != NULL) { + return $this->dateFormatter->formatInterval((int) $value, $this->options['granularity'] ?? 2); + } + return ''; } } diff --git a/web/core/modules/views/tests/src/Functional/Handler/FieldTimeIntervalTest.php b/web/core/modules/views/tests/src/Functional/Handler/FieldTimeIntervalTest.php new file mode 100644 index 0000000000000000000000000000000000000000..2344984fb6e6d8746dc77a56ed65870a5cfa5a5e --- /dev/null +++ b/web/core/modules/views/tests/src/Functional/Handler/FieldTimeIntervalTest.php @@ -0,0 +1,97 @@ +<?php + +namespace Drupal\Tests\views\Functional\Handler; + +use Drupal\Tests\views\Functional\ViewTestBase; +use Drupal\views\Views; +use Drupal\Core\StringTranslation\StringTranslationTrait; + +/** + * Tests the time interval handler. + * + * @group views + * @see \Drupal\views\Plugin\views\field\TimeInterval + */ +class FieldTimeIntervalTest extends ViewTestBase { + + use StringTranslationTrait; + + /** + * Views used by this test. + * + * @var array + */ + public static $testViews = ['test_view']; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * Ages dataset. + * + * @var array + */ + protected $ages = [ + [0, '0 sec', 2], + [1000, '16 min', 1], + [1000000, '1 week 4 days 13 hours 46 min', 4], + // DateFormatter::formatInterval will output 2 because there are no weeks. + [100000000, '3 years 2 months', 5], + [NULL, '', 3], + ]; + + /** + * {@inheritdoc} + */ + protected function setUp($import_test_views = TRUE, $modules = ['views_test_config']): void { + parent::setUp($import_test_views, $modules); + + $this->enableViewsTestModule(); + } + + /** + * Test TimeInterval handler. + */ + public function testFieldTimeInterval() { + $view = Views::getView('test_view'); + $view->setDisplay(); + $this->executeView($view); + foreach ($view->result as $delta => $row) { + [$value, $formatted_value, $granularity] = $this->ages[$delta]; + $view->field['age']->options['granularity'] = $granularity; + $this->assertEquals($formatted_value, $view->field['age']->advancedRender($row)); + } + } + + /** + * Overrides \Drupal\views\Tests\ViewUnitTestBase::schemaDefinition(). + */ + protected function schemaDefinition() { + $schema_definition = parent::schemaDefinition(); + $schema_definition['views_test_data']['fields']['age']['not null'] = FALSE; + return $schema_definition; + } + + /** + * Overrides \Drupal\views\Tests\ViewUnitTestBase::viewsData(). + */ + protected function viewsData() { + $data = parent::viewsData(); + $data['views_test_data']['age']['field']['id'] = 'time_interval'; + return $data; + } + + /** + * Overrides \Drupal\views\Tests\ViewUnitTestBase::dataSet(). + */ + protected function dataSet() { + $data_set = parent::dataSet(); + foreach ($data_set as $delta => $person) { + $data_set[$delta]['age'] = $this->ages[$delta][0]; + } + return $data_set; + } + +} diff --git a/web/core/modules/views/tests/src/FunctionalJavascript/PaginationAJAXTest.php b/web/core/modules/views/tests/src/FunctionalJavascript/PaginationAJAXTest.php index f96b468e3ca1efcd79020910153a5c140927d89d..00454912af121b32cc88906c7e3690046f3e7b10 100644 --- a/web/core/modules/views/tests/src/FunctionalJavascript/PaginationAJAXTest.php +++ b/web/core/modules/views/tests/src/FunctionalJavascript/PaginationAJAXTest.php @@ -99,7 +99,7 @@ public function testBasicPagination() { $this->assertStringContainsString('Node 6 content', $rows[0]->getHtml()); $link = $page->findLink('Go to page 3'); // Test that no unwanted parameters are added to the URL. - $this->assertEquals('?status=All&type=All&langcode=All&items_per_page=5&order=changed&sort=asc&title=&page=2', $link->getAttribute('href')); + $this->assertEquals('?status=All&type=All&langcode=All&items_per_page=5&order=changed&sort=asc&page=2', $link->getAttribute('href')); $this->assertNoDuplicateAssetsOnPage(); $this->clickLink('Go to page 3'); diff --git a/web/core/modules/workspaces/src/WorkspaceRepository.php b/web/core/modules/workspaces/src/WorkspaceRepository.php index 535c83366b3d128085acf0d0329ba5cc8164ce8b..d939cafdf5906c1b8adbc54f03c7a929f3ab41f0 100644 --- a/web/core/modules/workspaces/src/WorkspaceRepository.php +++ b/web/core/modules/workspaces/src/WorkspaceRepository.php @@ -116,6 +116,7 @@ public function loadTree() { } $graph = (new Graph($graph))->searchAndSort(); + $this->tree = []; foreach (array_keys($tree) as $workspace_id) { $this->tree[$workspace_id] = [ 'depth' => count($graph[$workspace_id]['paths']), diff --git a/web/core/modules/workspaces/tests/src/Kernel/WorkspaceCRUDTest.php b/web/core/modules/workspaces/tests/src/Kernel/WorkspaceCRUDTest.php index 19dc0e166167242a29d5d3862a69c2f518dad111..71803e50d9bffe2480950583e8b3a5d410b99c50 100644 --- a/web/core/modules/workspaces/tests/src/Kernel/WorkspaceCRUDTest.php +++ b/web/core/modules/workspaces/tests/src/Kernel/WorkspaceCRUDTest.php @@ -323,4 +323,12 @@ public function testDeletingWorkspaceWithChildren() { $this->assertNull(Workspace::load('stage')); } + /** + * Tests loading the workspace tree when there are no workspaces available. + */ + public function testEmptyWorkspaceTree() { + $tree = \Drupal::service('workspaces.repository')->loadTree(); + $this->assertSame([], $tree); + } + } diff --git a/web/core/profiles/demo_umami/config/install/user.role.author.yml b/web/core/profiles/demo_umami/config/install/user.role.author.yml index c9d34695e68248ea3d400a46753f5dea6c15b1e8..53860ff036c82e8d60179d5ffe5366624187ae41 100644 --- a/web/core/profiles/demo_umami/config/install/user.role.author.yml +++ b/web/core/profiles/demo_umami/config/install/user.role.author.yml @@ -2,6 +2,9 @@ langcode: en status: true dependencies: config: + - core.entity_view_display.node.article.full + - core.entity_view_display.node.page.full + - core.entity_view_display.node.recipe.full - node.type.article - node.type.page - node.type.recipe @@ -12,6 +15,7 @@ dependencies: - content_moderation - contextual - file + - layout_builder - node - path - system @@ -28,6 +32,10 @@ permissions: - 'access toolbar' - 'cancel account' - 'change own username' + - 'configure editable article node layout overrides' + - 'configure editable page node layout overrides' + - 'configure editable recipe node layout overrides' + - 'create and edit custom blocks' - 'create article content' - 'create page content' - 'create recipe content' diff --git a/web/core/profiles/demo_umami/config/install/user.role.editor.yml b/web/core/profiles/demo_umami/config/install/user.role.editor.yml index 40d5f651ee9d60f65dc5a2bca1f61892632df377..0a0558c7688d0e24909e1ccdb2068a06a183ccdb 100644 --- a/web/core/profiles/demo_umami/config/install/user.role.editor.yml +++ b/web/core/profiles/demo_umami/config/install/user.role.editor.yml @@ -2,6 +2,9 @@ langcode: en status: true dependencies: config: + - core.entity_view_display.node.article.full + - core.entity_view_display.node.page.full + - core.entity_view_display.node.recipe.full - node.type.article - node.type.page - node.type.recipe @@ -13,6 +16,7 @@ dependencies: - content_translation - contextual - file + - layout_builder - node - path - shortcut @@ -33,6 +37,10 @@ permissions: - 'administer shortcuts' - 'cancel account' - 'change own username' + - 'configure editable article node layout overrides' + - 'configure editable page node layout overrides' + - 'configure editable recipe node layout overrides' + - 'create and edit custom blocks' - 'create content translations' - 'create terms in recipe_category' - 'create terms in tags' diff --git a/web/core/profiles/demo_umami/themes/umami/css/components/tour/tour.theme.css b/web/core/profiles/demo_umami/themes/umami/css/components/tour/tour.theme.css index 64ee4f64f30aba836231e02dbba1963a92f14c5a..484cf8ed5613d81fdd22f4b1ce7bb212cb11b1d0 100644 --- a/web/core/profiles/demo_umami/themes/umami/css/components/tour/tour.theme.css +++ b/web/core/profiles/demo_umami/themes/umami/css/components/tour/tour.theme.css @@ -7,6 +7,7 @@ } .shepherd-cancel-icon { + border: solid 2px transparent; line-height: 1; } diff --git a/web/core/scripts/dev/commit-code-check.sh b/web/core/scripts/dev/commit-code-check.sh index cbabd8efa824627961d83fb8da859a77aa54afcc..46b90c11f0fac14bbb5af39915b68a6f15a4793e 100755 --- a/web/core/scripts/dev/commit-code-check.sh +++ b/web/core/scripts/dev/commit-code-check.sh @@ -72,29 +72,31 @@ green="" reset="" DRUPAL_VERSION=$(php -r "include 'vendor/autoload.php'; print preg_replace('#\.[0-9]+-dev#', '.x', \Drupal::VERSION);") + GIT="sudo -u www-data git" else red=$(tput setaf 1 && tput bold) green=$(tput setaf 2) reset=$(tput sgr0) + GIT="git" fi # Gets list of files to check. if [[ "$BRANCH" != "" ]]; then - FILES=$(git diff --name-only $BRANCH HEAD); + FILES=$($GIT diff --name-only $BRANCH HEAD); elif [[ "$CACHED" == "0" ]]; then # For DrupalCI patch testing or when running without --cached or --branch, # list of all changes in the working directory. - FILES=$(git ls-files --other --modified --exclude-standard --exclude=vendor) + FILES=$($GIT ls-files --other --modified --exclude-standard --exclude=vendor) else # Check staged files only. - if git rev-parse --verify HEAD >/dev/null 2>&1 + if $GIT rev-parse --verify HEAD >/dev/null 2>&1 then AGAINST=HEAD else # Initial commit: diff against an empty tree object AGAINST=4b825dc642cb6eb9a060e54bf8d69288fbee4904 fi - FILES=$(git diff --cached --name-only $AGAINST); + FILES=$($GIT diff --cached --name-only $AGAINST); fi if [[ "$FILES" == "" ]] && [[ "$DRUPALCI" == "1" ]]; then @@ -102,10 +104,10 @@ # need to diff against the Drupal branch or tag related to the Drupal version. printf "Creating list of files to check by comparing branch to %s\n" "$DRUPAL_VERSION" # On DrupalCI there's a merge commit so we can compare to HEAD~1. - FILES=$(git diff --name-only HEAD~1 HEAD); + FILES=$($GIT diff --name-only HEAD~1 HEAD); fi -TOP_LEVEL=$(git rev-parse --show-toplevel) +TOP_LEVEL=$($GIT rev-parse --show-toplevel) # This variable will be set to one when the file core/phpcs.xml.dist is changed. PHPCS_XML_DIST_FILE_CHANGED=0 diff --git a/web/core/scripts/run-tests.sh b/web/core/scripts/run-tests.sh index 1ce883352d1db9dc5730c76bc5615767960e3d71..59149f587c43b0ac8d07f46655d1e8a1004e9e0b 100755 --- a/web/core/scripts/run-tests.sh +++ b/web/core/scripts/run-tests.sh @@ -2,7 +2,12 @@ /** * @file - * This script runs Drupal tests from command line. + * Script for running tests on DrupalCI. + * + * This script is intended for use only by drupal.org's testing. In general, + * tests should be run directly with phpunit. + * + * @internal */ use Drupal\Component\FileSystem\FileSystem; diff --git a/web/core/tests/Drupal/FunctionalJavascriptTests/Ajax/MultiFormTest.php b/web/core/tests/Drupal/FunctionalJavascriptTests/Ajax/MultiFormTest.php index fbd979c4e36b073a738da5b4e343d93d57eef1fd..83c26e96e1a6dfa5c5e1448c0cc756e170e15152 100644 --- a/web/core/tests/Drupal/FunctionalJavascriptTests/Ajax/MultiFormTest.php +++ b/web/core/tests/Drupal/FunctionalJavascriptTests/Ajax/MultiFormTest.php @@ -92,20 +92,21 @@ public function testMultiForm() { // page update, ensure the same as above. for ($i = 0; $i < 2; $i++) { - $forms = $page->find('xpath', $form_xpath); + $forms = $page->findAll('xpath', $form_xpath); foreach ($forms as $offset => $form) { $button = $form->findButton('Add another item'); $this->assertNotNull($button, 'Add Another Item button exists'); $button->press(); - // Wait for page update. - $this->assertSession()->assertWaitOnAjaxRequest(); + // Wait for field to be added with ajax. + $this->assertNotEmpty($page->waitFor(10, function () use ($form, $i) { + return $form->findField('field_ajax_test[' . ($i + 1) . '][value]'); + })); - // After AJAX request and response page will update. - $page_updated = $session->getPage(); - $field = $page_updated->findAll('xpath', '.' . $field_xpath); - $this->assertCount($i + 2, $field[0]->find('xpath', '.' . $field_items_xpath_suffix), 'Found the correct number of field items after an AJAX submission.'); - $this->assertNotNull($field[0]->find('xpath', '.' . $button_xpath_suffix), 'Found the "add more" button after an AJAX submission.'); + // After AJAX request and response verify the correct number of text + // fields (including title), as well as the "Add another item" button. + $this->assertCount($i + 3, $form->findAll('css', 'input[type="text"]'), 'Found the correct number of field items after an AJAX submission.'); + $this->assertNotEmpty($form->findButton('Add another item'), 'Found the "add more" button after an AJAX submission.'); $this->assertSession()->pageContainsNoDuplicateId(); } } diff --git a/web/core/tests/Drupal/FunctionalJavascriptTests/Core/Form/JavascriptStatesTest.php b/web/core/tests/Drupal/FunctionalJavascriptTests/Core/Form/JavascriptStatesTest.php index 259a7e146f0615c217bbf743f1d5485353548070..80790d48f33bdfee8c8ab0d5b05acf09f2c696cf 100644 --- a/web/core/tests/Drupal/FunctionalJavascriptTests/Core/Form/JavascriptStatesTest.php +++ b/web/core/tests/Drupal/FunctionalJavascriptTests/Core/Form/JavascriptStatesTest.php @@ -63,6 +63,7 @@ public function testJavascriptStates() { $this->doRadiosTriggerTests(); $this->doSelectTriggerTests(); $this->doMultipleTriggerTests(); + $this->doNestedTriggerTests(); } /** @@ -322,4 +323,30 @@ protected function doMultipleTriggerTests() { $this->assertTrue($item_visible_value2_and_textfield->isVisible()); } + /** + * Tests states of radios element triggered by other radios element. + */ + protected function doNestedTriggerTests() { + $this->drupalGet('form-test/javascript-states-form'); + $page = $this->getSession()->getPage(); + + // Find trigger and target elements. + $radios_opposite1 = $page->findField('radios_opposite1'); + $this->assertNotEmpty($radios_opposite1); + $radios_opposite2 = $page->findField('radios_opposite2'); + $this->assertNotEmpty($radios_opposite2); + + // Verify initial state. + $this->assertEquals('0', $radios_opposite1->getValue()); + $this->assertEquals('1', $radios_opposite2->getValue()); + + // Set $radios_opposite2 value to 0, $radios_opposite1 value should be 1. + $radios_opposite2->setValue('0'); + $this->assertEquals('1', $radios_opposite1->getValue()); + + // Set $radios_opposite1 value to 1, $radios_opposite2 value should be 0. + $radios_opposite1->setValue('0'); + $this->assertEquals('1', $radios_opposite2->getValue()); + } + } diff --git a/web/core/tests/Drupal/FunctionalTests/AssertLegacyTrait.php b/web/core/tests/Drupal/FunctionalTests/AssertLegacyTrait.php index 25bf18878b9acef0f021d233046757cbc0623956..6ac0ffaf37b5b73c60c5692efad59fb87fcccc58 100644 --- a/web/core/tests/Drupal/FunctionalTests/AssertLegacyTrait.php +++ b/web/core/tests/Drupal/FunctionalTests/AssertLegacyTrait.php @@ -4,7 +4,6 @@ use Behat\Mink\Element\NodeElement; use Behat\Mink\Exception\ExpectationException; -use Behat\Mink\Selector\Xpath\Escaper; use Drupal\Component\Render\FormattableMarkup; use Drupal\Component\Utility\Xss; use Drupal\KernelTests\AssertLegacyTrait as BaseAssertLegacyTrait; @@ -759,7 +758,7 @@ protected function assertFieldsByValue($fields, $value = NULL, $message = '') { // Input element with correct value. $found = TRUE; } - elseif ($field->find('xpath', '//option[@value = ' . (new Escaper())->escapeLiteral($value) . ' and @selected = "selected"]')) { + elseif ($field->find('xpath', '//option[@value = ' . $value . ' and @selected = "selected"]')) { // Select element with an option. $found = TRUE; } diff --git a/web/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigExistingSettingsTest.php b/web/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigExistingSettingsTest.php new file mode 100644 index 0000000000000000000000000000000000000000..fe762f113afdb1ea7d41e4e107c061102d08000e --- /dev/null +++ b/web/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigExistingSettingsTest.php @@ -0,0 +1,40 @@ +<?php + +namespace Drupal\FunctionalTests\Installer; + +use Drupal\Core\Database\Database; + +/** + * Verifies that installing from existing configuration works. + * + * @group Installer + */ +class InstallerExistingConfigExistingSettingsTest extends InstallerExistingConfigTest { + + /** + * {@inheritdoc} + * + * Partially configures a preexisting settings.php file before invoking the + * interactive installer. + */ + protected function prepareEnvironment() { + parent::prepareEnvironment(); + // Pre-configure hash salt. + // Any string is valid, so simply use the class name of this test. + $this->settings['settings']['hash_salt'] = (object) [ + 'value' => __CLASS__, + 'required' => TRUE, + ]; + + // Pre-configure database credentials. + $connection_info = Database::getConnectionInfo(); + unset($connection_info['default']['pdo']); + unset($connection_info['default']['init_commands']); + + $this->settings['databases']['default'] = (object) [ + 'value' => $connection_info, + 'required' => TRUE, + ]; + } + +} diff --git a/web/core/tests/Drupal/KernelTests/Core/Action/EmailActionTest.php b/web/core/tests/Drupal/KernelTests/Core/Action/EmailActionTest.php index 81d8aad8956ea51cf6e604a947c1fd3cc8143d75..f71fcd34a7fcffd2626b05b45533fadb6921638c 100644 --- a/web/core/tests/Drupal/KernelTests/Core/Action/EmailActionTest.php +++ b/web/core/tests/Drupal/KernelTests/Core/Action/EmailActionTest.php @@ -31,6 +31,8 @@ protected function setUp(): void { * Tests the email action plugin. */ public function testEmailAction() { + $this->config('system.site')->set('mail', 'test@example.com')->save(); + /** @var \Drupal\Core\Action\ActionManager $plugin_manager */ $plugin_manager = $this->container->get('plugin.manager.action'); $configuration = [ diff --git a/web/core/tests/Drupal/KernelTests/Core/ParamConverter/EntityConverterLatestRevisionTest.php b/web/core/tests/Drupal/KernelTests/Core/ParamConverter/EntityConverterLatestRevisionTest.php index 9e06a63b20ff972277c253738f36893a2db18bca..739c680124db5fb3167f175b976211bf4485c632 100644 --- a/web/core/tests/Drupal/KernelTests/Core/ParamConverter/EntityConverterLatestRevisionTest.php +++ b/web/core/tests/Drupal/KernelTests/Core/ParamConverter/EntityConverterLatestRevisionTest.php @@ -189,4 +189,66 @@ public function testConvertNonRevisionableEntityType() { $this->assertEquals($entity->id(), $converted->id()); } + /** + * Tests an entity route parameter having 'bundle' definition property. + * + * @covers ::convert + */ + public function testRouteParamWithBundleDefinition(): void { + $entity1 = EntityTestMulRev::create([ + 'name' => $this->randomString(), + 'type' => 'foo', + ]); + $entity1->save(); + $entity2 = EntityTestMulRev::create([ + 'name' => $this->randomString(), + 'type' => 'bar', + ]); + $entity2->save(); + $entity3 = EntityTestMulRev::create([ + 'name' => $this->randomString(), + 'type' => 'baz', + ]); + $entity3->save(); + + $definition = [ + 'type' => 'entity:entity_test_mulrev', + 'bundle' => [ + 'foo', + 'bar', + ], + 'load_latest_revision' => TRUE, + ]; + + // An entity whose bundle is in the definition list is converted. + $converted = $this->converter->convert($entity1->id(), $definition, 'qux', []); + $this->assertSame($entity1->id(), $converted->id()); + + // An entity whose bundle is in the definition list is converted. + $converted = $this->converter->convert($entity2->id(), $definition, 'qux', []); + $this->assertSame($entity2->id(), $converted->id()); + + // An entity whose bundle is missed from definition is not converted. + $converted = $this->converter->convert($entity3->id(), $definition, 'qux', []); + $this->assertNull($converted); + + // A non-existing entity returns NULL. + $converted = $this->converter->convert('some-non-existing-entity-id', $definition, 'qux', []); + $this->assertNull($converted); + + $definition = [ + 'type' => 'entity:entity_test_mulrev', + ]; + + // Check that all entities are returned when 'bundle' is not defined. + $converted = $this->converter->convert($entity1->id(), $definition, 'qux', []); + $this->assertSame($entity1->id(), $converted->id()); + $converted = $this->converter->convert($entity2->id(), $definition, 'qux', []); + $this->assertSame($entity2->id(), $converted->id()); + $converted = $this->converter->convert($entity3->id(), $definition, 'qux', []); + $this->assertSame($entity3->id(), $converted->id()); + $converted = $this->converter->convert('some-non-existing-entity-id', $definition, 'qux', []); + $this->assertNull($converted); + } + } diff --git a/web/core/tests/Drupal/Nightwatch/Tests/statesTest.js b/web/core/tests/Drupal/Nightwatch/Tests/statesTest.js index 2ae6d41de457cbef040f76f01503e5c3ef95aac8..622005ebe374f76741375d0e96590a60a41aebcc 100644 --- a/web/core/tests/Drupal/Nightwatch/Tests/statesTest.js +++ b/web/core/tests/Drupal/Nightwatch/Tests/statesTest.js @@ -21,4 +21,28 @@ module.exports = { .waitForElementNotVisible('input[name="textfield"]', 1000) .assert.noDeprecationErrors(); }, + 'Test number trigger with spinner widget': (browser) => { + browser + .drupalRelativeURL('/form-test/javascript-states-form') + .waitForElementVisible('body', 1000) + .waitForElementNotVisible( + '#edit-item-visible-when-number-trigger-filled-by-spinner', + 1000, + ) + .execute( + // eslint-disable-next-line func-names, prefer-arrow-callback, no-shadow + function () { + // Emulate usage of the spinner browser widget on number inputs + // on modern browsers. + const numberTrigger = document.getElementById('edit-number-trigger'); + numberTrigger.value = 1; + numberTrigger.dispatchEvent(new Event('change')); + }, + ); + + browser.waitForElementVisible( + '#edit-item-visible-when-number-trigger-filled-by-spinner', + 1000, + ); + }, }; diff --git a/web/core/tests/Drupal/Tests/Core/Command/GenerateThemeTest.php b/web/core/tests/Drupal/Tests/Core/Command/GenerateThemeTest.php index 14dfdda5db9cf51df11b790eaf28901ad77fb7ba..d5dba42a329fe223cbf66e3eaea2cc52b761ded8 100644 --- a/web/core/tests/Drupal/Tests/Core/Command/GenerateThemeTest.php +++ b/web/core/tests/Drupal/Tests/Core/Command/GenerateThemeTest.php @@ -45,7 +45,7 @@ public function setUp(): void { * @return \Symfony\Component\Process\Process * The PHP process */ - private function generateThemeFromStarterkit() : Process { + private function generateThemeFromStarterkit($env = NULL) : Process { $install_command = [ $this->php, 'core/scripts/drupal', @@ -54,7 +54,7 @@ private function generateThemeFromStarterkit() : Process { '--name="Test custom starterkit theme"', '--description="Custom theme generated from a starterkit theme"', ]; - $process = new Process($install_command, NULL); + $process = new Process($install_command, NULL, $env); $process->setTimeout(60); return $process; } @@ -262,20 +262,23 @@ public function testContribStarterkitDevSnapshotWithGitNotInstalled(): void { SH; file_put_contents($unavailableGitPath . '/git', $bash); chmod($unavailableGitPath . '/git', 0755); - $oldPath = getenv('PATH'); - putenv('PATH=' . $unavailableGitPath . ':' . getenv('PATH')); // Confirm that 'git' is no longer available. - $output = []; - exec('git --help', $output, $status); - $this->assertEquals(127, $status); - - $process = $this->generateThemeFromStarterkit(); + $env = [ + 'PATH' => $unavailableGitPath . ':' . getenv('PATH'), + 'COLUMNS' => 80, + ]; + $process = new Process([ + 'git', + '--help', + ], NULL, $env); + $process->run(); + $this->assertEquals(127, $process->getExitCode(), 'Fake git used by process.'); + + $process = $this->generateThemeFromStarterkit($env); $result = $process->run(); $this->assertEquals("[ERROR] The source theme starterkit_theme has a development version number \n (7.x-dev). Determining a specific commit is not possible because git is\n not installed. Either install git or use a tagged release to generate a\n theme.", trim($process->getOutput()), $process->getErrorOutput()); $this->assertSame(1, $result); $this->assertFileDoesNotExist($this->getWorkspaceDirectory() . "/themes/test_custom_theme"); - - putenv('PATH=' . $oldPath . ':' . getenv('PATH')); } /** diff --git a/web/core/tests/Drupal/Tests/Core/Flood/MemoryBackendTest.php b/web/core/tests/Drupal/Tests/Core/Flood/MemoryBackendTest.php new file mode 100644 index 0000000000000000000000000000000000000000..6be982db837b4fd004eeea7bcc3e097d895d436b --- /dev/null +++ b/web/core/tests/Drupal/Tests/Core/Flood/MemoryBackendTest.php @@ -0,0 +1,106 @@ +<?php + +namespace Drupal\Tests\Core\Flood; + +use Drupal\Core\Flood\MemoryBackend; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\Request; +use Drupal\Tests\UnitTestCase; + +/** + * Tests the memory flood implementation. + * + * @group flood + * @coversDefaultClass \Drupal\Core\Flood\MemoryBackend + */ +class MemoryBackendTest extends UnitTestCase { + + /** + * The tested memory flood backend. + * + * @var \Drupal\Core\Flood\MemoryBackend + */ + protected $flood; + + protected function setUp(): void { + $request = new RequestStack(); + $request_mock = $this->getMockBuilder(Request::class) + ->onlyMethods(['getClientIp']) + ->getMock(); + $request->push($request_mock); + $this->flood = new MemoryBackend($request); + } + + /** + * Tests an allowed flood event. + */ + public function testAllowedProceeding() { + $threshold = 2; + $window_expired = -1; + + $this->flood->register('test_event', $window_expired); + $this->assertTrue($this->flood->isAllowed('test_event', $threshold)); + } + + /** + * Tests a flood event with more than the allowed calls. + */ + public function testNotAllowedProceeding() { + $threshold = 1; + $window_expired = -1; + + // Register the event twice, so it is not allowed to proceed. + $this->flood->register('test_event', $window_expired); + $this->flood->register('test_event', $window_expired, 1); + + $this->assertFalse($this->flood->isAllowed('test_event', $threshold)); + } + + /** + * Tests a flood event with expiring, so cron will allow to proceed. + * + * @medium + */ + public function testExpiring() { + $threshold = 1; + $window_expired = -1; + + $this->flood->register('test_event', $window_expired); + usleep(2); + $this->flood->register('test_event', $window_expired); + + $this->assertFalse($this->flood->isAllowed('test_event', $threshold)); + + // "Run cron", which clears the flood data and verify event is now allowed. + $this->flood->garbageCollection(); + $this->assertTrue($this->flood->isAllowed('test_event', $threshold)); + } + + /** + * Tests a flood event with no expiring, so cron will not allow to proceed. + */ + public function testNotExpiring() { + $threshold = 2; + + $this->flood->register('test_event', 1); + usleep(3); + $this->flood->register('test_event', 1); + + $this->assertFalse($this->flood->isAllowed('test_event', $threshold)); + + // "Run cron", which clears the flood data and verify event is not allowed. + $this->flood->garbageCollection(); + $this->assertFalse($this->flood->isAllowed('test_event', $threshold)); + } + + /** + * Tests memory backend records events to the nearest microsecond. + */ + public function testMemoryBackendThreshold() { + $this->flood->register('new event'); + $this->assertTrue($this->flood->isAllowed('new event', '2')); + $this->flood->register('new event'); + $this->assertFalse($this->flood->isAllowed('new event', '2')); + } + +} diff --git a/web/core/tests/Drupal/Tests/Core/Mail/Plugin/Mail/PhpMailTest.php b/web/core/tests/Drupal/Tests/Core/Mail/Plugin/Mail/PhpMailTest.php new file mode 100644 index 0000000000000000000000000000000000000000..bdf97280cc0e6940d96e9ec8bfd1c96722f928e7 --- /dev/null +++ b/web/core/tests/Drupal/Tests/Core/Mail/Plugin/Mail/PhpMailTest.php @@ -0,0 +1,96 @@ +<?php + +namespace Drupal\Tests\Core\Mail\Plugin\Mail; + +use Drupal\Core\DependencyInjection\ContainerBuilder; +use Drupal\Core\Mail\Plugin\Mail\PhpMail; +use Drupal\Tests\UnitTestCase; + +/** + * @coversDefaultClass \Drupal\Core\Mail\Plugin\Mail\PhpMail + * @group Mail + */ +class PhpMailTest extends UnitTestCase { + + /** + * The configuration factory. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface|\PHPUnit\Framework\MockObject\MockObject + */ + protected $configFactory; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + // Use the provided config for system.mail.interface settings. + $this->configFactory = $this->getConfigFactoryStub([ + 'system.mail' => [ + 'interface' => [], + ], + 'system.site' => [ + 'mail' => 'test@example.com', + ], + ]); + + $container = new ContainerBuilder(); + $container->set('config.factory', $this->configFactory); + \Drupal::setContainer($container); + } + + /** + * Creates a mocked PhpMail object. + * + * The method "doMail()" gets overridden to avoid a mail() call in tests. + * + * @return \Drupal\Core\Mail\Plugin\Mail\PhpMail|\PHPUnit\Framework\MockObject\MockObject + * A PhpMail instance. + */ + protected function createPhpMailInstance(): PhpMail { + $mailer = $this->getMockBuilder(PhpMail::class) + ->onlyMethods(['doMail']) + ->getMock(); + + $mailer->expects($this->once())->method('doMail') + ->willReturn(TRUE); + + return $mailer; + } + + /** + * Tests sending a mail using a From address with a comma in it. + * + * @covers ::testMail + */ + public function testMail() { + // Setup a mail message. + $message = [ + 'id' => 'example_key', + 'module' => 'example', + 'key' => 'key', + 'to' => 'to@example.org', + 'from' => 'from@example.org', + 'reply-to' => 'from@example.org', + 'langcode' => 'en', + 'params' => [], + 'send' => TRUE, + 'subject' => '', + 'body' => '', + 'headers' => [ + 'MIME-Version' => '1.0', + 'Content-Type' => 'text/plain; charset=UTF-8; format=flowed; delsp=yes', + 'Content-Transfer-Encoding' => '8Bit', + 'X-Mailer' => 'Drupal', + 'Return-Path' => 'from@example.org', + 'From' => '"Foo, Bar, and Baz" <from@example.org>', + 'Reply-to' => 'from@example.org', + ], + ]; + + $mailer = $this->createPhpMailInstance(); + $this->assertTrue($mailer->mail($message)); + } + +} diff --git a/web/core/themes/claro/claro.theme b/web/core/themes/claro/claro.theme index 3447ff861961e6ae0448ec2780ff63c08df11b74..14540dd2080eaaec4b7b6677e545d017d6c38c34 100644 --- a/web/core/themes/claro/claro.theme +++ b/web/core/themes/claro/claro.theme @@ -1553,7 +1553,9 @@ function claro_form_views_ui_config_item_form_alter(array &$form, FormStateInter // needed or require refactoring in https://drupal.org/node/3164890 unset($form['options']['clear_markup_start']); unset($form['options']['clear_markup_end']); - $form['options']['expose_button']['#prefix'] = str_replace('clearfix', '', $form['options']['expose_button']['#prefix']); + if (isset($form['options']['expose_button']['#prefix'])) { + $form['options']['expose_button']['#prefix'] = str_replace('clearfix', '', $form['options']['expose_button']['#prefix']); + } if (isset($form['options']['group_button']['#prefix'])) { $form['options']['group_button']['#prefix'] = str_replace('clearfix', '', $form['options']['group_button']['#prefix']); }