From 2340df05fe1a1550ff5ed14c20a19b674c852d6d Mon Sep 17 00:00:00 2001 From: Peter Lieverdink Date: Wed, 20 Nov 2024 13:55:02 +1100 Subject: [PATCH] fix: Remove and re-add the lowcased patches dir and not the uppercase one, it's all very confusing. --- PATCHES/.gitkeep | 0 PATCHES/2844205.mr.patch | 426 ----- ...iew-unpublished-search-api-access-18.patch | 1442 ----------------- ...-add-relationships-to-layout-builder.patch | 156 -- PATCHES/3008924-rerolled-24.patch | 40 - PATCHES/3153137-custom-fix-3281606.patch | 13 - PATCHES/3467860.mr-9219.patch | 374 ----- PATCHES/gin-toolbar-id.patch | 171 -- ...n_lb_add_suggestions_alter-1.0.0-rc8.patch | 120 -- ...ontext-value-in-layout-builder-admin.patch | 18 - 10 files changed, 2760 deletions(-) delete mode 100755 PATCHES/.gitkeep delete mode 100644 PATCHES/2844205.mr.patch delete mode 100644 PATCHES/2958568-view-unpublished-search-api-access-18.patch delete mode 100644 PATCHES/3001188-make-it-possible-to-add-relationships-to-layout-builder.patch delete mode 100644 PATCHES/3008924-rerolled-24.patch delete mode 100644 PATCHES/3153137-custom-fix-3281606.patch delete mode 100644 PATCHES/3467860.mr-9219.patch delete mode 100644 PATCHES/gin-toolbar-id.patch delete mode 100644 PATCHES/hook_gin_lb_add_suggestions_alter-1.0.0-rc8.patch delete mode 100644 PATCHES/missing-context-value-in-layout-builder-admin.patch diff --git a/PATCHES/.gitkeep b/PATCHES/.gitkeep deleted file mode 100755 index e69de29bb..000000000 diff --git a/PATCHES/2844205.mr.patch b/PATCHES/2844205.mr.patch deleted file mode 100644 index 2a68f0b2e..000000000 --- a/PATCHES/2844205.mr.patch +++ /dev/null @@ -1,426 +0,0 @@ -diff --git a/seckit.api.php b/seckit.api.php -new file mode 100644 -index 0000000000000000000000000000000000000000..bcad867858e48681f51ad0af14752ba86c195b03 ---- /dev/null -+++ b/seckit.api.php -@@ -0,0 +1,32 @@ -+logger = $logger; - $this->config = $config_factory->get('seckit.settings'); - $this->moduleExtensionList = $extension_list_module; -+ $this->moduleHandler = $module_handler; - } - - /** -@@ -81,6 +94,7 @@ class SecKitEventSubscriber implements EventSubscriberInterface { - */ - public function onKernelRequest(RequestEvent $event) { - $this->request = $event->getRequest(); -+ $this->config = $this->getSeckitConfig(); - - // Execute necessary functions. - if ($this->config->get('seckit_csrf.origin')) { -@@ -96,6 +110,7 @@ class SecKitEventSubscriber implements EventSubscriberInterface { - */ - public function onKernelResponse(ResponseEvent $event) { - $this->response = $event->getResponse(); -+ $this->config = $this->getSeckitConfig(); - - // Execute necessary functions. - if ($this->config->get('seckit_xss.csp.checkbox')) { -@@ -206,103 +221,87 @@ class SecKitEventSubscriber implements EventSubscriberInterface { - * Based on specification available at http://www.w3.org/TR/CSP/ - */ - public function seckitCsp() { -- // Get default/set options. -- $csp_vendor_prefix_x = $this->config->get('seckit_xss.csp.vendor-prefix.x'); -- $csp_vendor_prefix_webkit = $this->config->get('seckit_xss.csp.vendor-prefix.webkit'); -- $csp_report_only = $this->config->get('seckit_xss.csp.report-only'); -- $csp_default_src = $this->config->get('seckit_xss.csp.default-src'); -- $csp_script_src = $this->config->get('seckit_xss.csp.script-src'); -- $csp_object_src = $this->config->get('seckit_xss.csp.object-src'); -- $csp_img_src = $this->config->get('seckit_xss.csp.img-src'); -- $csp_media_src = $this->config->get('seckit_xss.csp.media-src'); -- $csp_style_src = $this->config->get('seckit_xss.csp.style-src'); -- $csp_frame_src = $this->config->get('seckit_xss.csp.frame-src'); -- $csp_frame_ancestors = $this->config->get('seckit_xss.csp.frame-ancestors'); -- $csp_child_src = $this->config->get('seckit_xss.csp.child-src'); -- $csp_font_src = $this->config->get('seckit_xss.csp.font-src'); -- $csp_connect_src = $this->config->get('seckit_xss.csp.connect-src'); -- $csp_report_uri = $this->config->get('seckit_xss.csp.report-uri'); -- $csp_upgrade_req = $this->config->get('seckit_xss.csp.upgrade-req'); -- // $csp_policy_uri = $this->config->get('seckit_xss.csp.policy-uri'); -- // Prepare directives. -+ $options = $this->config->get('seckit_xss.csp'); - $directives = []; - -- // If policy-uri is declared, no other directives are permitted. -- /* if ($csp_report_only) { -- $directives = "policy-uri " . base_path() . $csp_report_only; -- } */ -- // Otherwise prepare directives. -- // else {. -- if ($csp_default_src) { -- $directives[] = "default-src $csp_default_src"; -- } -- if ($csp_script_src) { -- $directives[] = "script-src $csp_script_src"; -- } -- if ($csp_object_src) { -- $directives[] = "object-src $csp_object_src"; -- } -- if ($csp_style_src) { -- $directives[] = "style-src $csp_style_src"; -- } -- if ($csp_img_src) { -- $directives[] = "img-src $csp_img_src"; -- } -- if ($csp_media_src) { -- $directives[] = "media-src $csp_media_src"; -- } -- if ($csp_frame_src) { -- $directives[] = "frame-src $csp_frame_src"; -- } -- if ($csp_frame_ancestors) { -- $directives[] = "frame-ancestors $csp_frame_ancestors"; -- } -- if ($csp_child_src) { -- $directives[] = "child-src $csp_child_src"; -- } -- if ($csp_font_src) { -- $directives[] = "font-src $csp_font_src"; -+ // Iterate through the options to process special cases and CSP directives. -+ // All the special cases will be present in the $options array. -+ foreach ($options as $option_key => $option_value) { -+ switch ($option_key) { -+ case 'checkbox': -+ // This option only determined whether this function was called. -+ break; -+ -+ case 'report-only': -+ $csp_report_only = $option_value; -+ break; -+ -+ case 'vendor-prefix': -+ $csp_vendor_prefix_x = $option_value['x']; -+ $csp_vendor_prefix_webkit = $option_value['webkit']; -+ break; -+ -+ case 'policy-uri': -+ // Policy URIs aren't supported currently. -+ break; -+ -+ case 'report-uri': -+ if ($option_value) { -+ $base_path = ''; -+ $csp_report_uri = $option_value; -+ if (!UrlHelper::isExternal($csp_report_uri)) { -+ // Strip leading slashes from internal paths to prevent them -+ // becoming external URLs without protocol. /report-csp-violation -+ // should not be turned into //report-csp-violation. -+ $csp_report_uri = ltrim($option_value, '/'); -+ $base_path = base_path(); -+ } -+ $directives[$option_key] = "report-uri " . $base_path . $csp_report_uri; -+ } -+ break; -+ -+ case 'upgrade-req': -+ if ($option_value) { -+ $directives['upgrade-insecure-requests'] = 'upgrade-insecure-requests'; -+ } -+ break; -+ -+ default: -+ // All other entries represent verbatim CSP directives. -+ // Custom directives can be added via hook_seckit_options_alter(). -+ if ($option_value) { -+ $directives[$option_key] = $option_key . ' ' . $option_value; -+ } -+ break; -+ } - } -- if ($csp_connect_src) { -- $directives[] = "connect-src $csp_connect_src"; -+ -+ // Remove empty directives and merge. -+ $directives = implode('; ', array_filter($directives)); -+ -+ // Early return if no directives were prepared. -+ if (!$directives) { -+ return; - } -- if ($csp_report_uri) { -- $base_path = ''; -- if (!UrlHelper::isExternal($csp_report_uri)) { -- // Strip leading slashes from internal paths to prevent them becoming -- // external URLs without protocol. /report-csp-violation should not be -- // turned into //report-csp-violation. -- $csp_report_uri = ltrim($csp_report_uri, '/'); -- $base_path = base_path(); -+ -+ // Send HTTP response header if directives were prepared. -+ if ($csp_report_only) { -+ // Use report-only mode. -+ $this->response->headers->set('Content-Security-Policy-Report-Only', $directives); -+ if ($csp_vendor_prefix_x) { -+ $this->response->headers->set('X-Content-Security-Policy-Report-Only', $directives); -+ } -+ if ($csp_vendor_prefix_webkit) { -+ $this->response->headers->set('X-WebKit-CSP-Report-Only', $directives); - } -- $directives[] = "report-uri " . $base_path . $csp_report_uri; -- } -- if ($csp_upgrade_req) { -- $directives[] = 'upgrade-insecure-requests'; - } -- // Merge directives. -- $directives = implode('; ', $directives); -- // } -- // send HTTP response header if directives were prepared. -- if ($directives) { -- if ($csp_report_only) { -- // Use report-only mode. -- $this->response->headers->set('Content-Security-Policy-Report-Only', $directives); -- if ($csp_vendor_prefix_x) { -- $this->response->headers->set('X-Content-Security-Policy-Report-Only', $directives); -- } -- if ($csp_vendor_prefix_webkit) { -- $this->response->headers->set('X-WebKit-CSP-Report-Only', $directives); -- } -+ else { -+ $this->response->headers->set('Content-Security-Policy', $directives); -+ if ($csp_vendor_prefix_x) { -+ $this->response->headers->set('X-Content-Security-Policy', $directives); - } -- else { -- $this->response->headers->set('Content-Security-Policy', $directives); -- if ($csp_vendor_prefix_x) { -- $this->response->headers->set('X-Content-Security-Policy', $directives); -- } -- if ($csp_vendor_prefix_webkit) { -- $this->response->headers->set('X-WebKit-CSP', $directives); -- } -+ if ($csp_vendor_prefix_webkit) { -+ $this->response->headers->set('X-WebKit-CSP', $directives); - } - } - } -@@ -515,4 +514,42 @@ EOT; - $this->response->headers->set('Feature-Policy', $header); - } - -+ /** -+ * Returns the Seckit configuration. -+ * -+ * This method retrieves the seckit configuration, allowing modules -+ * to alter the configuration using the hook_seckit_options_alter hook. -+ * If no implementations of the hook are found, the original configuration -+ * is returned. -+ * -+ * @return \Drupal\Core\Config\ImmutableConfig -+ * The Seckit configuration. -+ */ -+ public function getSeckitConfig(): ImmutableConfig { -+ // Exit early and return the original configuration if no hooks -+ // need to be run. -+ if (!$this->moduleHandler->hasImplementations('seckit_options_alter')) { -+ return $this->config; -+ } -+ -+ // Cache the results as this function will run more than once during the -+ // request cycle. -+ $config = &drupal_static(__FUNCTION__); -+ -+ if (!isset($config)) { -+ /** @var \Drupal\Core\Config\ImmutableConfig $config */ -+ $config = $this->config; -+ $raw_data = $config->getRawData(); -+ -+ // Allow other modules to alter the configuration. -+ $this->moduleHandler->alter('seckit_options', $raw_data); -+ -+ // Merge the altered configuration with the original one. -+ $options = NestedArray::mergeDeep($raw_data, $config); -+ $config->setModuleOverride($options); -+ } -+ -+ return $config; -+ } -+ - } -diff --git a/tests/seckit_test/seckit_test.info.yml b/tests/seckit_test/seckit_test.info.yml -new file mode 100644 -index 0000000000000000000000000000000000000000..f5bef51c310311760d5b2f21792331596e7104c9 ---- /dev/null -+++ b/tests/seckit_test/seckit_test.info.yml -@@ -0,0 +1,6 @@ -+name: 'Seckit test module' -+type: module -+description: 'Test module for seckit.' -+package: Testing -+hidden: true -+version: VERSION -diff --git a/tests/seckit_test/seckit_test.module b/tests/seckit_test/seckit_test.module -new file mode 100644 -index 0000000000000000000000000000000000000000..8660474ea2a7dbb0de9223c0051c6e4b2f0651f5 ---- /dev/null -+++ b/tests/seckit_test/seckit_test.module -@@ -0,0 +1,14 @@ -+assertSession()->responseHeaderEquals('Feature-Policy', $expected); - } - -+ /** -+ * Tests the seckit_options_alter() hook. -+ */ -+ public function testSeckitOptionsAlter() { -+ \Drupal::service('module_installer')->install(['seckit_test']); -+ $form = [ -+ 'seckit_xss[csp][checkbox]' => TRUE, -+ 'seckit_xss[csp][script-src]' => "'self'", -+ 'seckit_various[from_origin]' => FALSE, -+ ]; -+ $this->drupalGet('admin/config/system/seckit'); -+ $this->submitForm($form, t('Save configuration')); -+ $expected = "script-src 'self' example.com; report-uri " . base_path() . $this->reportPath; -+ $this->assertSession()->responseHeaderEquals('Content-Security-Policy', $expected); -+ $this->assertSession()->responseHeaderEquals('From-Origin', 'same'); -+ } -+ - /** - * Adds an origin to requests if $this->originHeader is set. - * diff --git a/PATCHES/2958568-view-unpublished-search-api-access-18.patch b/PATCHES/2958568-view-unpublished-search-api-access-18.patch deleted file mode 100644 index 83b7b7066..000000000 --- a/PATCHES/2958568-view-unpublished-search-api-access-18.patch +++ /dev/null @@ -1,1442 +0,0 @@ -diff --git a/composer.json b/composer.json -index 71d1c19..2ba3cec 100644 ---- a/composer.json -+++ b/composer.json -@@ -2,6 +2,7 @@ - "name": "drupal/view_unpublished", - "type": "drupal-module", - "description": "Select which roles should be able to see unpublished nodes.", -+ "homepage": "https://www.drupal.org/project/view_unpublished", - "license": "GPL-2.0-or-later", - "authors": [ - { -@@ -9,7 +10,11 @@ - "email": "amaria@chisholmtech.com" - } - ], -+ "require": { -+ "drupal/core": "^9.4 || ^10" -+ }, - "require-dev": { -+ "drupal/search_api": "^1.26", - "phpcompatibility/php-compatibility": "10.x-dev@dev", - "drupal/coder": "^8.3.18" - } -diff --git a/src/Plugin/search_api/processor/ViewUnpublished.php b/src/Plugin/search_api/processor/ViewUnpublished.php -new file mode 100644 -index 0000000..b6d12e3 ---- /dev/null -+++ b/src/Plugin/search_api/processor/ViewUnpublished.php -@@ -0,0 +1,256 @@ -+This replaces the standard "Content access" processor from Search API."), -+ * stages = { -+ * "add_properties" = 0, -+ * "pre_index_save" = -10, -+ * "preprocess_query" = -30, -+ * }, -+ * locked = false, -+ * hidden = true, -+ * ) -+ * @noinspection PhpUnused -+ */ -+class ViewUnpublished extends ContentAccess { -+ -+ /** -+ * {@inheritdoc} -+ * -+ * @throws \Drupal\search_api\SearchApiException -+ * If the required "type" field is missing and cannot be added. -+ */ -+ public function preIndexSave() { -+ parent::preIndexSave(); -+ -+ foreach ($this->index->getDatasources() as $datasource_id => $datasource) { -+ $entity_type = $datasource->getEntityTypeId(); -+ -+ if ($entity_type == 'node') { -+ // Ensure node content type/bundle is indexed, for use in access checks. -+ $this->ensureField($datasource_id, 'type', 'string'); -+ } -+ } -+ } -+ -+ /** -+ * Check if an account has any of the permission defined by this module. -+ * -+ * @param \Drupal\Core\Session\AccountInterface $account -+ * The current user. -+ * -+ * @return bool -+ * TRUE if the logged-in user has one of the permissions exposed by this -+ * module; or, FALSE, if they do not. -+ */ -+ protected function hasAnyUnpublishedPermission(AccountInterface $account): bool { -+ if ($account->hasPermission("view any unpublished content")) { -+ return TRUE; -+ } -+ -+ $permission_manager = new ViewUnpublishedPermissions(); -+ foreach ($permission_manager->permissions() as $key => $value) { -+ if ($account->hasPermission($key)) { -+ return TRUE; -+ } -+ } -+ -+ return FALSE; -+ } -+ -+ /** -+ * {@inheritDoc} -+ * -+ * This revises access checks to allow authorized users to view unpublished -+ * content. -+ * -+ * @see \Drupal\search_api\Plugin\search_api\processor\ContentAccess::addNodeAccess() -+ * -+ * @noinspection DuplicatedCode -+ * @noinspection SpellCheckingInspection -+ * @phpcs:disable Drupal.Commenting.InlineComment.SpacingBefore -+ */ -+ protected function addNodeAccess(QueryInterface $query, AccountInterface $account) { -+ if (!$this->hasAnyUnpublishedPermission($account)) { -+ // For performance, skip our checks and defer to the parent implementation -+ // if the account has none of the permissions granted by this module. -+ parent::addNodeAccess($query, $account); -+ return; -+ } -+ -+ // Don't do anything if the user can access all content. -+ if ($account->hasPermission('bypass node access')) { -+ return; -+ } -+ -+ // Gather the affected datasources, grouped by entity type, as well as the -+ // unaffected ones. -+ $affected_datasources = []; -+ $unaffected_datasources = []; -+ foreach ($this->index->getDatasources() as $datasource_id => $datasource) { -+ $entity_type = $datasource->getEntityTypeId(); -+ if (in_array($entity_type, ['node', 'comment'])) { -+ $affected_datasources[$entity_type][] = $datasource_id; -+ } -+ else { -+ $unaffected_datasources[] = $datasource_id; -+ } -+ } -+ -+ // The filter structure we want looks like this: -+ // [belongs to other datasource] -+ // OR -+ // ( -+ // [is enabled (or was created by the user, if applicable)] -+ // AND -+ // [grants view access to one of the user's gid/realm combinations] -+ // ) -+ // If there are no "other" datasources, we don't need the nested OR, -+ // however, and can add the inner conditions directly to the query. -+ if (empty($unaffected_datasources)) { -+ $access_conditions = $query; -+ } -+ else { -+ $outer_conditions = $query->createConditionGroup('OR', ['content_access']); -+ $query->addConditionGroup($outer_conditions); -+ -+ foreach ($unaffected_datasources as $datasource_id) { -+ $outer_conditions->addCondition('search_api_datasource', $datasource_id); -+ } -+ -+ // phpcs:ignore Drupal.Commenting.InlineComment.DocBlock -+ /** @noinspection PhpRedundantOptionalArgumentInspection */ -+ $access_conditions = $query->createConditionGroup('AND'); -+ $outer_conditions->addConditionGroup($access_conditions); -+ } -+ -+ if (!$account->hasPermission('access content')) { -+ unset($affected_datasources['node']); -+ } -+ if (!$account->hasPermission('access comments')) { -+ unset($affected_datasources['comment']); -+ } -+ -+ // If the user does not have the permission to see any content at all, deny -+ // access to all items from affected datasources. -+ if (empty($affected_datasources)) { -+ // If there were "other" datasources, the existing filter will already -+ // remove all results of node or comment datasources. Otherwise, we should -+ // not return any results at all. -+ if (empty($unaffected_datasources)) { -+ $query->abort($this->t('You have no access to any results in this search.')); -+ } -+ return; -+ } -+ -+ if (!$account->hasPermission('view any unpublished content')) { -+ // This user doesn't have permission to view all unpublished things. We -+ // can only show them things that meet the following criteria: -+ // 1. The content is published; OR -+ // 2. The content is unpublished, but: -+ // a. The user has permission to see own unpublished content and this -+ // content was authored by this user. -+ // b. The user has permission to see unpublished content of this type -+ // of content. -+ $publish_conditions = $query->createConditionGroup('OR', ['content_access_enabled']); -+ $has_unpublished_own = $account->hasPermission('view own unpublished content'); -+ -+ foreach ($affected_datasources as $entity_type => $datasources) { -+ foreach ($datasources as $datasource_id) { -+ // 1. Allow users to see content that's been published. -+ // -+ // If this is a comment datasource, or users cannot view their own -+ // unpublished nodes, a simple filter on "status" is enough. -+ // Otherwise, it's a bit more complicated. -+ $status_field = $this->findField($datasource_id, 'status', 'boolean'); -+ if ($status_field !== NULL) { -+ $publish_conditions->addCondition( -+ $status_field->getFieldIdentifier(), -+ TRUE -+ ); -+ } -+ -+ if ($entity_type == 'node') { -+ // 2(a). If the user has permission to see their own unpublished -+ // content, allow them to see any unpublished content where they're -+ // the author. -+ if ($has_unpublished_own) { -+ $author_field = $this->findField($datasource_id, 'uid', 'integer'); -+ -+ if ($author_field !== NULL) { -+ $publish_conditions->addCondition( -+ $author_field->getFieldIdentifier(), -+ $account->id() -+ ); -+ } -+ } -+ -+ // 2(b). Allow the user to see unpublished content of the types that -+ // they've been granted to see. -+ $content_type_field = $this->findField($datasource_id, 'type', 'string'); -+ if ($content_type_field !== NULL) { -+ foreach (NodeType::loadMultiple() as $type) { -+ $type_id = $type->id(); -+ -+ if ($account->hasPermission("view any unpublished $type_id content")) { -+ $publish_conditions->addCondition( -+ $content_type_field->getFieldIdentifier(), -+ $type_id -+ ); -+ } -+ } -+ } -+ } -+ } -+ } -+ -+ $access_conditions->addConditionGroup($publish_conditions); -+ } -+ -+ // Filter by the user's node access grants. -+ $node_grants_field = $this->findField(NULL, 'search_api_node_grants', 'string'); -+ if ($node_grants_field === NULL) { -+ return; -+ } -+ -+ $node_grants_field_id = $node_grants_field->getFieldIdentifier(); -+ $grants_conditions = $query->createConditionGroup('OR', ['content_access_grants']); -+ $grants = node_access_grants('view', $account); -+ foreach ($grants as $realm => $gids) { -+ foreach ($gids as $gid) { -+ $grants_conditions->addCondition($node_grants_field_id, "node_access_$realm:$gid"); -+ } -+ } -+ -+ // Also add items that are accessible for everyone by checking the "access -+ // all" pseudo grant. -+ $grants_conditions->addCondition($node_grants_field_id, 'node_access__all'); -+ $access_conditions->addConditionGroup($grants_conditions); -+ } -+ -+} -diff --git a/tests/src/Kernel/Plugin/search_api/Processor/ViewUnpublishedContentAccessTest.php b/tests/src/Kernel/Plugin/search_api/Processor/ViewUnpublishedContentAccessTest.php -new file mode 100644 -index 0000000..05ead26 ---- /dev/null -+++ b/tests/src/Kernel/Plugin/search_api/Processor/ViewUnpublishedContentAccessTest.php -@@ -0,0 +1,1061 @@ -+nodes = []; -+ $this->comments = []; -+ $this->users = []; -+ -+ // Activate a custom grant that prevents all users except the original -+ // author of a node from viewing it. This only grants access to PUBLISHED -+ // nodes. Users without "view own unpublished content" permission will still -+ // not be able to see nodes they authored if those nodes are unpublished. -+ \Drupal::state()->set('search_api_test_add_node_access_grant', TRUE); -+ -+ // Create "page" and "blog" node types for testing. -+ foreach (['page' => 'Page', 'blog' => 'Blog'] as $type_name => $label) { -+ $type = NodeType::create([ -+ 'type' => $type_name, -+ 'name' => $label, -+ ]); -+ $type->save(); -+ } -+ -+ // Create anonymous user role. -+ $role = Role::create([ -+ 'id' => 'anonymous', -+ 'label' => 'anonymous', -+ ]); -+ $role->save(); -+ -+ // User Index 0: The anonymous user. We have to insert them because the user -+ // table is inner-joined by \Drupal\comment\CommentStorage. -+ $this->createUser([], '', FALSE, ['uid' => 0, 'name' => '']); -+ -+ // User Index 1: A moderator who can access unpublished "blog" content. -+ $this->blogModeratorUser = $this->createUser([ -+ 'view any unpublished blog content', -+ ]); -+ -+ // User Index 2: A moderator who can access all unpublished content. -+ $this->fullModeratorUser = $this->createUser([ -+ 'view any unpublished content', -+ ]); -+ -+ // Setup support for comments on page nodes. -+ $this->installConfig(['comment']); -+ $comment_type = CommentType::create([ -+ 'id' => 'comment', -+ 'target_entity_type_id' => 'node', -+ ]); -+ $comment_type->save(); -+ $this->addDefaultCommentField('node', 'page'); -+ -+ // Node Index 0: A published node with an attached, published comment. -+ $this->createNode([ -+ 'status' => NodeInterface::PUBLISHED, -+ 'type' => 'page', -+ 'title' => 'test title', -+ ]); -+ // Comment Index 0: A published comment on Node #0. -+ $this->createComment([ -+ 'status' => CommentInterface::PUBLISHED, -+ 'entity_type' => 'node', -+ 'entity_id' => $this->nodes[0]->id(), -+ 'field_name' => 'comment', -+ 'body' => 'test body', -+ 'comment_type' => $comment_type->id(), -+ ]); -+ -+ // Node Index 1: A published page node. -+ $this->createNode([ -+ 'status' => NodeInterface::PUBLISHED, -+ 'type' => 'page', -+ 'title' => 'some title', -+ ]); -+ -+ // Node Index 2: An unpublished page node. -+ $this->createNode([ -+ 'status' => NodeInterface::NOT_PUBLISHED, -+ 'type' => 'page', -+ 'title' => 'other title', -+ ]); -+ -+ // Also index users, to verify that they are unaffected by the processor. -+ assert($this->index instanceof IndexInterface); -+ $dataSources = \Drupal::getContainer() -+ ->get('search_api.plugin_helper') -+ ->createDatasourcePlugins($this->index, [ -+ 'entity:comment', -+ 'entity:node', -+ 'entity:user', -+ ]); -+ $this->index->setDatasources($dataSources); -+ $this->index->save(); -+ -+ \Drupal::getContainer() -+ ->get('search_api.index_task_manager') -+ ->addItemsAll($this->index); -+ $index_storage = \Drupal::entityTypeManager()->getStorage('search_api_index'); -+ $index_storage->resetCache([$this->index->id()]); -+ $this->index = $index_storage->load($this->index->id()); -+ } -+ -+ /** -+ * Tests that the "content_access" processor gets replaced by ours. -+ * -+ * @throws \Drupal\search_api\SearchApiException -+ * -+ * @noinspection DuplicatedCode -+ */ -+ public function testContentAccessProcessorReplacement() { -+ $processor = $this->index->getProcessor('content_access'); -+ -+ $this->assertInstanceOf( -+ ViewUnpublished::class, -+ $processor, -+ 'The "content_access" processor from Search API gets replaced with the custom version from "View Unpublished".' -+ ); -+ } -+ -+ /** -+ * Tests searching published content accessible to all w/out. VU perms. -+ * -+ * @throws \Drupal\search_api\SearchApiException -+ * -+ * @noinspection DuplicatedCode -+ */ -+ public function testQueryAccessAllWithoutViewUnpublishedPerms() { -+ $permissions = [ -+ 'access content', -+ 'access comments', -+ ]; -+ user_role_grant_permissions('anonymous', $permissions); -+ -+ // Total of 7 items: 3 nodes + 1 comment + 3 users. -+ $this->indexContentAndAssertCount(7); -+ -+ $query = $this->buildQuery(); -+ $result = $query->execute(); -+ -+ // These are indices of entities in $this->users, $this->comments, and -+ // $this->nodes. Search API provides a default node access grant for the -+ // anonymous user account not to have any content restrictions (independent -+ // of the published access requirement). -+ $expected = [ -+ 'user' => [0, 1, 2], -+ 'comment' => [0], -+ 'node' => [0, 1], -+ ]; -+ $this->assertResults($result, $expected); -+ } -+ -+ /** -+ * Tests searching published content accessible to all w/. VU perms. -+ * -+ * @throws \Drupal\search_api\SearchApiException -+ * -+ * @noinspection DuplicatedCode -+ * @noinspection PhpConditionAlreadyCheckedInspection -+ */ -+ public function testQueryAccessAllWithViewUnpublishedPerms() { -+ // Total of 7 items: 3 nodes + 1 comment + 3 users. -+ $this->indexContentAndAssertCount(7); -+ -+ // Grant users permission to see everything but unpublished pages and -+ // confirm they can see all published content. -+ user_role_grant_permissions('anonymous', [ -+ 'access content', -+ 'access comments', -+ 'view any unpublished blog content', -+ ]); -+ $query = $this->buildQuery(); -+ $result = $query->execute(); -+ // These are indices of entities in $this->users, $this->comments, and -+ // $this->nodes. Search API provides a default node access grant for the -+ // anonymous user account not to have any content restrictions (independent -+ // of the published access requirement). -+ $expected = [ -+ 'user' => [0, 1, 2], -+ 'comment' => [0], -+ 'node' => [0, 1], -+ ]; -+ $this->assertResults($result, $expected); -+ -+ // Now, grant the user permission to see unpublished 'page' content and -+ // confirm they can access all the nodes. -+ user_role_grant_permissions('anonymous', [ -+ 'view any unpublished page content', -+ ]); -+ $query = $this->buildQuery(); -+ $result = $query->execute(); -+ // These are indices of entities in $this->users, $this->comments, and -+ // $this->nodes. -+ $expected = [ -+ 'user' => [0, 1, 2], -+ 'comment' => [0], -+ 'node' => [0, 1, 2], -+ ]; -+ $this->assertResults($result, $expected); -+ } -+ -+ /** -+ * Tests searching when only comments are accessible. -+ * -+ * @throws \Drupal\search_api\SearchApiException -+ * -+ * @noinspection DuplicatedCode -+ */ -+ public function testQueryAccessCommentsWithoutViewUnpublishedPerms() { -+ user_role_grant_permissions('anonymous', ['access comments']); -+ -+ // Total of 7 items: 3 nodes + 1 comment + 3 users. -+ $this->indexContentAndAssertCount(7); -+ -+ $query = $this->buildQuery(); -+ $result = $query->execute(); -+ -+ // These are indices of entities in $this->users and $this->comments. -+ $this->assertResults($result, [ -+ 'user' => [0, 1, 2], -+ 'comment' => [0], -+ ]); -+ } -+ -+ /** -+ * Tests searching when only comments are accessible and user has VU perms. -+ * -+ * This creates a user having one of the permissions from "View Unpublished" -+ * and confirms that the extended processor provided by this module enables -+ * a user to view published comments even if the only way that content is -+ * accessible to the user is because it is published. -+ * -+ * @throws \Drupal\search_api\SearchApiException -+ * -+ * @noinspection DuplicatedCode -+ */ -+ public function testQueryAccessCommentsWithViewUnpublishedPerms() { -+ // Total of 7 items: 3 nodes + 1 comment + 3 users. -+ $this->indexContentAndAssertCount(7); -+ -+ // Grant users permission to see everything but unpublished pages and -+ // confirm they can see all published content. -+ user_role_grant_permissions('anonymous', [ -+ 'access comments', -+ 'view any unpublished blog content', -+ ]); -+ $query = $this->buildQuery(); -+ $result = $query->execute(); -+ // These are indices of entities in $this->users, $this->comments, and -+ // $this->nodes. -+ $this->assertResults($result, [ -+ 'user' => [0, 1, 2], -+ 'comment' => [0], -+ ]); -+ -+ // Now, grant the user permission to see unpublished 'page' content and -+ // confirm it has no impact on node access when the user lacks permission -+ // to view content. -+ user_role_grant_permissions('anonymous', [ -+ 'view any unpublished page content', -+ ]); -+ $query = $this->buildQuery(); -+ $result = $query->execute(); -+ // These are indices of entities in $this->users, $this->comments, and -+ // $this->nodes. -+ $this->assertResults($result, [ -+ 'user' => [0, 1, 2], -+ 'comment' => [0], -+ ]); -+ } -+ -+ /** -+ * Tests searching for own unpublished content w/out. VU perms. -+ * -+ * This creates a user with none of the permissions from "View Unpublished" -+ * and confirms that base functionality works when using the replacement -+ * processor provided by this module. -+ * -+ * @throws \Drupal\Core\Entity\EntityStorageException -+ * @throws \Drupal\search_api\SearchApiException -+ * -+ * @noinspection DuplicatedCode -+ */ -+ public function testQueryAccessWithViewOwnUnpublishedAndWithoutViewUnpublishedPerms() { -+ // User Index 3: A user who can access their own unpublished content. -+ $author_user = $this->createUser([ -+ 'access content', -+ 'access comments', -+ 'view own unpublished content', -+ ]); -+ -+ // Node Index 3: An unpublished node authored by user #3. -+ $this->createNode([ -+ 'status' => NodeInterface::NOT_PUBLISHED, -+ 'type' => 'page', -+ 'title' => 'foo', -+ 'uid' => $author_user->id(), -+ ]); -+ -+ // Total of 9 items: 4 nodes + 1 comment + 4 users. -+ $this->indexContentAndAssertCount(9); -+ -+ $query = $this->buildQuery(['search_api_access_account' => $author_user]); -+ $result = $query->execute(); -+ -+ // These are indices of entities in $this->users and $this->nodes. -+ // Because node 3 is unpublished, the custom Search API node access grant -+ // is not enough to allow access to the node. So, if the node appears in -+ // results, that means the author's permissions are allowing them to see -+ // the content. -+ $expected = [ -+ 'user' => [0, 1, 2, 3], -+ 'node' => [3], -+ ]; -+ $this->assertResults($result, $expected); -+ -+ // Revoke the permission to view unpublished content from the author and -+ // confirm they can no longer search their unpublished content. -+ $this->revokePermission($author_user, 'view own unpublished content'); -+ $query = $this->buildQuery(['search_api_access_account' => $author_user]); -+ $result = $query->execute(); -+ -+ // These are indices of entities in $this->users and $this->nodes. -+ // Because node 3 is unpublished, the custom Search API node access grant -+ // is not enough to allow access to the node. Now that the user has lost -+ // permission to see their own unpublished content, the node they authored -+ // should not appear. -+ $expected = [ -+ 'user' => [0, 1, 2, 3], -+ 'node' => [], -+ ]; -+ $this->assertResults($result, $expected); -+ } -+ -+ /** -+ * Tests searching for own unpublished content with VU perms. -+ * -+ * This creates two users with one of the permissions from "View Unpublished" -+ * and confirms that the extended processor provided by this module enables -+ * a user to view their own unpublished content even if the only way that -+ * content is accessible to the user is because they are the author. -+ * -+ * @throws \Drupal\Core\Entity\EntityStorageException -+ * @throws \Drupal\search_api\SearchApiException -+ * -+ * @noinspection DuplicatedCode -+ * @noinspection PhpConditionAlreadyCheckedInspection -+ */ -+ public function testQueryAccessWithViewOwnUnpublishedAndViewUnpublishedPerms() { -+ $shared_permissions = [ -+ 'access content', -+ 'access comments', -+ 'view own unpublished content', -+ 'view any unpublished blog content', -+ ]; -+ -+ // User Indices 3 and 4: Two users who can access their own unpublished -+ // content and unpublished *blog* content, but not unpublished *page* -+ // content. -+ $author_user = $this->createUser($shared_permissions); -+ $other_user = $this->createUser($shared_permissions); -+ -+ // We make the content authored only by the first user. -+ $author_uid = $author_user->id(); -+ -+ // Node Index 3: An unpublished node authored by user #3. -+ $this->createNode([ -+ 'status' => NodeInterface::NOT_PUBLISHED, -+ 'type' => 'page', -+ 'title' => 'foo', -+ 'uid' => $author_uid, -+ ]); -+ -+ // Total of 10 items: 4 nodes + 1 comment + 5 users. -+ $this->indexContentAndAssertCount(10); -+ -+ // Perform a query as the first user, who was the author of the page. -+ $query = $this->buildQuery(['search_api_access_account' => $author_user]); -+ $result = $query->execute(); -+ -+ // These are indices of entities in $this->users and $this->nodes. -+ // Because node 3 is unpublished, the custom Search API node access grant -+ // is not enough to allow access to the node. So, if the node appears in -+ // results, that means the author's permissions are allowing them to see -+ // the content. -+ $expected = [ -+ 'user' => [0, 1, 2, 3, 4], -+ 'node' => [3], -+ ]; -+ $this->assertResults($result, $expected); -+ -+ // Perform a query as the second user, who is NOT the author of the page. -+ $query = $this->buildQuery(['search_api_access_account' => $other_user]); -+ $result = $query->execute(); -+ -+ // These are indices of entities in $this->users and $this->nodes. -+ // Because of the custom Search API node access grant, this user has no -+ // default access to any of the published nodes. -+ $expected = [ -+ 'user' => [0, 1, 2, 3, 4], -+ 'node' => [], -+ ]; -+ $this->assertResults($result, $expected); -+ -+ // Revoke the permission to view unpublished content from the author and -+ // confirm they can no longer search their unpublished content. -+ $this->revokePermission($author_user, 'view own unpublished content'); -+ $query = $this->buildQuery(['search_api_access_account' => $author_user]); -+ $result = $query->execute(); -+ -+ // These are indices of entities in $this->users and $this->nodes. -+ // Because node 3 is unpublished, the custom Search API node access grant -+ // is not enough to allow access to the node. Now that the user has lost -+ // permission to see their own unpublished content, the node they authored -+ // should not appear. -+ $expected = [ -+ 'user' => [0, 1, 2, 3, 4], -+ 'node' => [], -+ ]; -+ $this->assertResults($result, $expected); -+ } -+ -+ /** -+ * Tests searching for content w/. node grants, w/out. VU perms. -+ * -+ * This creates a user with none of the permissions from "View Unpublished" -+ * but with content that is controlled by node grants, and confirms that base -+ * functionality works when using the replacement processor provided by this -+ * module. -+ * -+ * @throws \Drupal\Core\Entity\EntityStorageException -+ * @throws \Drupal\search_api\SearchApiException -+ * @throws \Exception -+ * -+ * @noinspection DuplicatedCode -+ */ -+ public function testQueryAccessWithNodeGrantsWithoutViewUnpublishedPerms() { -+ // User Index #3: A user who can access published content. -+ $user = $this->createUser([ -+ 'access content', -+ ]); -+ -+ Database::getConnection() -+ ->insert('node_access') -+ ->fields([ -+ 'nid' => $this->nodes[0]->id(), -+ 'langcode' => $this->nodes[0]->language()->getId(), -+ 'gid' => $user->id(), -+ 'realm' => 'search_api_test', -+ 'grant_view' => 1, -+ ]) -+ ->execute(); -+ -+ // Total of 8 items: 3 nodes + 1 comment + 4 users. -+ $this->indexContentAndAssertCount(8); -+ -+ $query = $this->buildQuery(['search_api_access_account' => $user]); -+ $result = $query->execute(); -+ -+ // These are indices of entities in $this->users and $this->nodes. -+ $expected = [ -+ 'user' => [0, 1, 2, 3], -+ 'node' => [0], -+ ]; -+ $this->assertResults($result, $expected); -+ } -+ -+ /** -+ * Tests searching for content w/. node grants and VU perms. -+ * -+ * This creates a user with one of the permissions from "View Unpublished" and -+ * content that is controlled by node grants, and confirms that the extended -+ * processor provided by this module enables a user to view the content -+ * controlled by grants even if the only way that content is accessible to the -+ * user is because of the grants. -+ * -+ * @throws \Drupal\Core\Entity\EntityStorageException -+ * @throws \Drupal\search_api\SearchApiException -+ * @throws \Exception -+ * -+ * @noinspection DuplicatedCode -+ * @noinspection PhpConditionAlreadyCheckedInspection -+ */ -+ public function testQueryAccessWithNodeGrantsWithViewUnpublishedPerms() { -+ // User Index #3: A user who can access published content and unpublished -+ // blog content. -+ $user = $this->createUser([ -+ 'access content', -+ 'view any unpublished blog content', -+ ]); -+ -+ Database::getConnection() -+ ->insert('node_access') -+ ->fields([ -+ 'nid' => $this->nodes[0]->id(), -+ 'langcode' => $this->nodes[0]->language()->getId(), -+ 'gid' => $user->id(), -+ 'realm' => 'search_api_test', -+ 'grant_view' => 1, -+ ]) -+ ->execute(); -+ -+ // Total of 8 items: 3 nodes + 1 comment + 4 users. -+ $this->indexContentAndAssertCount(8); -+ -+ // First, test that the grant now allows the user to view node #0. -+ $query = $this->buildQuery(['search_api_access_account' => $user]); -+ $result = $query->execute(); -+ // These are indices of entities in $this->users and $this->nodes. -+ $expected = [ -+ 'user' => [0, 1, 2, 3], -+ 'node' => [0], -+ ]; -+ $this->assertResults($result, $expected); -+ -+ // Now, grant the user permission to see unpublished 'page' content and -+ // confirm it has no impact on what nodes can be seen (since the other nodes -+ // have no grants that allow access to them). -+ $this->grantPermission($user, 'view any unpublished page content'); -+ $query = $this->buildQuery(['search_api_access_account' => $user]); -+ $result = $query->execute(); -+ // These are indices of entities in $this->users, $this->comments, and -+ // $this->nodes. -+ $expected = [ -+ 'user' => [0, 1, 2, 3], -+ 'node' => [0], -+ ]; -+ $this->assertResults($result, $expected); -+ } -+ -+ /** -+ * Tests the indexed and assigned grants w/out. VU perms. -+ * -+ * This checks to ensure that indexing is working properly when using the -+ * replacement processor provided by this module and fetching node grants for -+ * a user without any permissions from "View Unpublished". -+ * -+ * @noinspection DuplicatedCode -+ */ -+ public function testNodeGrantsWithoutViewUnpublishedPerms() { -+ user_role_grant_permissions('anonymous', [ -+ 'access content', -+ 'access comments', -+ ]); -+ -+ // Deactivate our custom grant and re-save the grant records. -+ $this->disableSearchApiNodeAccessGrant(); -+ -+ $items = []; -+ foreach ($this->comments as $comment) { -+ $items[] = [ -+ 'datasource' => 'entity:comment', -+ 'item' => $comment->getTypedData(), -+ 'item_id' => $comment->id(), -+ 'text' => 'Comment: ' . $comment->id(), -+ ]; -+ } -+ $items = $this->generateItems($items); -+ -+ // Add the processor's field values to the items. -+ foreach ($items as $item) { -+ $this->processor->addFieldValues($item); -+ } -+ -+ // Verify all items were indexed with the "all" realm grant. -+ $all = ['node_access_all:0']; -+ foreach ($items as $item) { -+ $this->assertEquals($all, $item->getField('node_grants')->getValues()); -+ } -+ -+ // Verify that the anonymous user has the "all" realm grant plus the general -+ // grant from View Unpublished that allows access to published content. -+ $grants = node_access_grants('view', new AnonymousUserSession()); -+ $this->assertEquals( -+ [ -+ 'all' => [0], -+ 'view_unpublished_published_content' => [1], -+ ], -+ $grants -+ ); -+ } -+ -+ /** -+ * Tests the indexed and assigned grants if users have VU perms. -+ * -+ * This checks to ensure that node grant indexing is working properly when -+ * using the replacement processor provided by this module and fetching node -+ * grants for a user having one of the permissions from "View Unpublished". -+ * -+ * @noinspection DuplicatedCode -+ */ -+ public function testNodeGrantsWithViewUnpublishedPerms() { -+ user_role_grant_permissions('anonymous', [ -+ 'access content', -+ 'access comments', -+ 'view any unpublished blog content', -+ ]); -+ -+ // Deactivate our custom grant and re-save the grant records. -+ $this->disableSearchApiNodeAccessGrant(); -+ -+ $items = []; -+ foreach ($this->comments as $comment) { -+ $items[] = [ -+ 'datasource' => 'entity:comment', -+ 'item' => $comment->getTypedData(), -+ 'item_id' => $comment->id(), -+ 'text' => 'Comment: ' . $comment->id(), -+ ]; -+ } -+ $items = $this->generateItems($items); -+ -+ // Add the processor's field values to the items. -+ foreach ($items as $item) { -+ $this->processor->addFieldValues($item); -+ } -+ -+ // Verify all items were indexed with the "all" realm grant. -+ $all = ['node_access_all:0']; -+ foreach ($items as $item) { -+ $this->assertEquals($all, $item->getField('node_grants')->getValues()); -+ } -+ -+ // Verify that the anonymous user has the "all" realm grant plus the grants -+ // added by View Unpublished. -+ $grants = node_access_grants('view', new AnonymousUserSession()); -+ $this->assertEquals( -+ [ -+ 'all' => [0], -+ 'view_unpublished_published_content' => [1], -+ 'view_unpublished_blog_content' => [1], -+ ], -+ $grants -+ ); -+ } -+ -+ /** -+ * Tests the grants indexed when using a standard hook_node_grants() impl. -+ * -+ * This checks to ensure that node grant indexing is working properly when -+ * using the replacement processor provided by this module. -+ * -+ * @noinspection DuplicatedCode -+ */ -+ public function testNodeGrantsStandard() { -+ $items = []; -+ foreach ($this->comments as $comment) { -+ $items[] = [ -+ 'datasource' => 'entity:comment', -+ 'item' => $comment->getTypedData(), -+ 'item_id' => $comment->id(), -+ 'field_text' => 'Text: &' . $comment->id(), -+ ]; -+ } -+ $items = $this->generateItems($items); -+ -+ // Add the processor's field values to the items. -+ foreach ($items as $item) { -+ $this->processor->addFieldValues($item); -+ } -+ -+ $grant = ['node_access_search_api_test:0']; -+ foreach ($items as $item) { -+ $this->assertEquals($grant, $item->getField('node_grants')->getValues()); -+ } -+ } -+ -+ /** -+ * Tests that acquiring grants for a node leads to re-indexing that node. -+ * -+ * @throws \Drupal\Core\Entity\EntityStorageException -+ * @throws \Drupal\search_api\SearchApiException -+ * -+ * @noinspection DuplicatedCode -+ */ -+ public function testNodeGrantsChange() { -+ $this->index->setOption('index_directly', FALSE)->save(); -+ $this->indexItems(); -+ $remaining = $this->index->getTrackerInstance()->getRemainingItems(); -+ $this->assertEquals([], $remaining, 'All items were indexed.'); -+ -+ /** @var \Drupal\node\NodeAccessControlHandlerInterface $access_control_handler */ -+ $access_control_handler = \Drupal::entityTypeManager() -+ ->getAccessControlHandler('node'); -+ $access_control_handler->acquireGrants($this->nodes[0]); -+ -+ $expected = [ -+ 'entity:comment/' . $this->comments[0]->id() . ':en', -+ 'entity:node/' . $this->nodes[0]->id() . ':en', -+ ]; -+ $remaining = $this->index->getTrackerInstance()->getRemainingItems(); -+ sort($remaining); -+ $this->assertEquals($expected, $remaining, 'The expected items were marked as "changed" when changing node access grants.'); -+ } -+ -+ /** -+ * Tests whether the "search_api_bypass_access" query option is respected. -+ * -+ * @throws \Drupal\search_api\SearchApiException -+ * -+ * @noinspection DuplicatedCode -+ * @noinspection PhpConditionAlreadyCheckedInspection -+ */ -+ public function testQueryAccessBypass() { -+ // Total of 7 items: 3 nodes + 1 comment + 3 users. -+ $this->indexContentAndAssertCount(7); -+ -+ // Query as anonymous user. -+ $query = $this->buildQuery(['search_api_bypass_access' => TRUE]); -+ $result = $query->execute(); -+ -+ // These are indices of entities in $this->users, $this->comments, and -+ // $this->nodes. -+ $expected = [ -+ 'user' => [0, 1, 2], -+ 'comment' => [0], -+ 'node' => [0, 1, 2], -+ ]; -+ $this->assertResults($result, $expected); -+ -+ // Query as a user with View Unpublished permissions. -+ $query = $this->buildQuery([ -+ 'search_api_bypass_access' => TRUE, -+ 'search_api_access_account' => $this->blogModeratorUser, -+ ]); -+ $result = $query->execute(); -+ -+ // These are indices of entities in $this->users, $this->comments, and -+ // $this->nodes. -+ $expected = [ -+ 'user' => [0, 1, 2], -+ 'comment' => [0], -+ 'node' => [0, 1, 2], -+ ]; -+ $this->assertResults($result, $expected); -+ } -+ -+ /** -+ * Rebuilds the index being used for this test and then checks its contents. -+ * -+ * @param int $expected_count -+ * The expected count of items in the index. -+ * -+ * @throws \Drupal\search_api\SearchApiException -+ */ -+ protected function indexContentAndAssertCount(int $expected_count): void { -+ $this->index->reindex(); -+ $this->indexItems(); -+ -+ $this->assertEquals( -+ $expected_count, -+ $this->index->getTrackerInstance()->getIndexedItemsCount(), -+ sprintf('%d items indexed, as expected.', $expected_count) -+ ); -+ } -+ -+ /** -+ * Builds a new query for the search index. -+ * -+ * @param array $options -+ * Additional options to set for the query. -+ * -+ * @return \Drupal\search_api\Query\QueryInterface -+ * The new query. -+ */ -+ protected function buildQuery(array $options = []): QueryInterface { -+ return \Drupal::getContainer() -+ ->get('search_api.query_helper') -+ ->createQuery($this->index, $options); -+ } -+ -+ /** -+ * Creates a node with the given field values. -+ * -+ * The node is added to the list of nodes created in the current test, for -+ * use with assertResults(). -+ * -+ * @param array $values -+ * The initial node field values. -+ * -+ * @return \Drupal\node\NodeInterface -+ * The new node. -+ * -+ * @throws \Drupal\Core\Entity\EntityStorageException -+ * If the node cannot be saved. -+ */ -+ protected function createNode(array $values): NodeInterface { -+ $node = Node::create($values); -+ $node->save(); -+ -+ $this->nodes[] = $node; -+ -+ return $node; -+ } -+ -+ /** -+ * Creates a comment with the given field values. -+ * -+ * The comment is added to the list of comments created in the current test, -+ * for use with assertResults(). -+ * -+ * @param array $values -+ * The initial comment field values. -+ * -+ * @return \Drupal\comment\CommentInterface -+ * The new comment. -+ * -+ * @throws \Drupal\Core\Entity\EntityStorageException -+ * If the comment cannot be saved. -+ */ -+ protected function createComment(array $values): CommentInterface { -+ $comment = Comment::create($values); -+ $comment->save(); -+ -+ $this->comments[] = $comment; -+ -+ return $comment; -+ } -+ -+ /** -+ * Create a user with a given set of permissions. -+ * -+ * The user is added to the list of users created in the current test, for -+ * use with assertResults(). -+ * -+ * @param array $permissions -+ * Array of permission names to assign to user. Note that the user always -+ * has the default permissions derived from the "authenticated users" role. -+ * @param string $name -+ * The user name. -+ * @param bool $admin -+ * (optional) Whether the user should be an administrator -+ * with all the available permissions. -+ * @param array $values -+ * (optional) An array of initial user field values. -+ * -+ * @return \Drupal\user\Entity\User|false -+ * A fully loaded user object with pass_raw property, or FALSE if account -+ * creation fails. -+ * -+ * @throws \Drupal\Core\Entity\EntityStorageException -+ * If the user creation fails. -+ */ -+ protected function createUser(array $permissions = [], $name = NULL, $admin = FALSE, array $values = []) { -+ $user = $this->baseCreateUser($permissions, $name, $admin, $values); -+ $this->users[] = $user; -+ -+ return $user; -+ } -+ -+ /** -+ * Asserts that the search results contain the expected IDs. -+ * -+ * Unlike \Drupal\Tests\search_api\Kernel\ResultsTrait::assertResults(), this -+ * version handles users just like any other type of entity and expects to -+ * find them defined in $this->users. This version also sanity-checks the -+ * passed-in indices to avoid undefined property errors. -+ * -+ * @param \Drupal\search_api\Query\ResultSetInterface $result -+ * The search results. -+ * @param int[][] $expected -+ * The expected entity IDs, grouped by entity type and with their indexes in -+ * this object's respective array properties as the values. -+ */ -+ protected function assertResults(ResultSetInterface $result, array $expected) { -+ $results = array_keys($result->getResultItems()); -+ sort($results); -+ -+ $ids = []; -+ foreach ($expected as $entity_type => $items) { -+ $datasource_id = "entity:$entity_type"; -+ foreach ($items as $index) { -+ $property_name = "{$entity_type}s"; -+ $entity = $this->{$property_name}[$index] ?? NULL; -+ -+ $this->assertNotNull( -+ $entity, -+ sprintf( -+ 'There is no "%s" at index "%d" of "$this->%s"', -+ $entity_type, -+ $index, -+ $property_name -+ ) -+ ); -+ assert($entity instanceof EntityInterface); -+ -+ $id = $entity->id() . ':en'; -+ $ids[] = Utility::createCombinedId($datasource_id, $id); -+ } -+ } -+ sort($ids); -+ -+ $this->assertEquals($ids, $results); -+ } -+ -+ /** -+ * Deactivates the custom grant added during setUp() and then re-saves grants. -+ */ -+ protected function disableSearchApiNodeAccessGrant(): void { -+ \Drupal::state()->set('search_api_test_add_node_access_grant', FALSE); -+ -+ /** @var \Drupal\node\NodeAccessControlHandlerInterface $access_control_handler */ -+ $access_control_handler = \Drupal::entityTypeManager() -+ ->getAccessControlHandler('node'); -+ $grants_storage = \Drupal::getContainer()->get('node.grant_storage'); -+ foreach ($this->nodes as $node) { -+ $grants = $access_control_handler->acquireGrants($node); -+ $grants_storage->write($node, $grants); -+ } -+ } -+ -+ /** -+ * Grants the specified permission to the first non-locked role of a user. -+ * -+ * @param \Drupal\user\UserInterface $user -+ * The user to which the permission should be granted. -+ * @param string $permission -+ * The machine name of the permission to grant. -+ * -+ * @throws \Drupal\Core\Entity\EntityStorageException -+ * If the first non-locked user role fails to save. -+ */ -+ protected function grantPermission(UserInterface $user, string $permission): void { -+ $regular_roles = $user->getRoles(TRUE); -+ -+ $this->assertNotEmpty( -+ $regular_roles, -+ 'The user does not have any non-locked roles.' -+ ); -+ -+ $rid = reset($regular_roles); -+ Role::load($rid)->grantPermission($permission)->save(); -+ } -+ -+ /** -+ * Revokes the specified permissions from all non-locked roles of a user. -+ * -+ * @param \Drupal\user\UserInterface $user -+ * The user from which the permission should be revoked. -+ * @param string $permission -+ * The machine name of the permission to revoke. -+ * -+ * @throws \Drupal\Core\Entity\EntityStorageException -+ * If any user role fails to save. -+ */ -+ protected function revokePermission(UserInterface $user, string $permission): void { -+ $regular_roles = $user->getRoles(TRUE); -+ -+ $this->assertNotEmpty( -+ $regular_roles, -+ 'The user does not have any non-locked roles.' -+ ); -+ -+ foreach ($regular_roles as $rid) { -+ Role::load($rid)->revokePermission($permission)->save(); -+ } -+ } -+ -+} -diff --git a/view_unpublished.install b/view_unpublished.install -index 941e106..d4bca73 100644 ---- a/view_unpublished.install -+++ b/view_unpublished.install -@@ -60,3 +60,42 @@ function view_unpublished_update_8002(): void { - $install_helper = Drupal::service('view_unpublished.install_helper'); - $install_helper->removeDependency(); - } -+ -+/** -+ * Switches away from the old "Content Access: View Unpublished" processor. -+ */ -+function view_unpublished_update_8003() { -+ // This update function fixes-up any indices that were using an earlier patch -+ // for https://www.drupal.org/project/view_unpublished/issues/2958568. In the -+ // original version of the patch, site builders had to switch to a different -+ // "Content Access" processor in order to use the "View Unpublished" module. -+ // This is no longer necessary -- this module overrides the default one with -+ // a hidden custom version so that site builders can just enable this module -+ // and run. -+ // -+ // Approach adapted from search_api_update_8103(). -+ $config_factory = \Drupal::configFactory(); -+ -+ foreach ($config_factory->listAll('search_api.index.') as $index_id) { -+ $index = $config_factory->getEditable($index_id); -+ $processors = $index->get('processor_settings'); -+ -+ if (isset($processors['view_unpublished'])) { -+ $processors['content_access'] = $processors['view_unpublished']; -+ unset($processors['view_unpublished']); -+ $index->set('processor_settings', $processors); -+ // Mark the resulting configuration as trusted data. This avoids issues -+ // with future schema changes. -+ $index->save(TRUE); -+ } -+ } -+ -+ // Clear the processor plugin cache so that if anything else indirectly tries -+ // to update Search API-related configuration, the plugin helper gets the most -+ // up-to-date plugin definitions. -+ \Drupal::getContainer() -+ ->get('plugin.manager.search_api.processor') -+ ->clearCachedDefinitions(); -+ -+ return t('Switched from old "Content Access: View Unpublished" to new "Content access" processor with automatic support for "View Unpublished".'); -+} -\ No newline at end of file -diff --git a/view_unpublished.module b/view_unpublished.module -index 073f511..61f796f 100644 ---- a/view_unpublished.module -+++ b/view_unpublished.module -@@ -12,6 +12,36 @@ use Drupal\Core\Session\AccountInterface; - use Drupal\node\Entity\NodeType; - use Drupal\node\NodeInterface; - -+/** -+ * Implements hook_search_api_processor_info_alter(). -+ */ -+function view_unpublished_search_api_processor_info_alter(array &$processors) { -+ $content_access_processor = $processors['content_access'] ?? NULL; -+ $view_unpublished_processor = $processors['view_unpublished'] ?? NULL; -+ -+ // Automatically replace the default 'content_access' handler with ours so -+ // that this module has immediate effect on searches without site builders -+ // having to reconfigure their search indices. The processor exposed by this -+ // module is hidden so that admins are not confused by two similar options. -+ if (($content_access_processor != NULL) && -+ ($view_unpublished_processor != NULL)) { -+ $keys_to_swap = [ -+ 'class', -+ 'provider', -+ ]; -+ -+ $processor_overrides = array_intersect_key( -+ $view_unpublished_processor, -+ array_flip($keys_to_swap) -+ ); -+ -+ $processors['content_access'] = array_merge( -+ $content_access_processor, -+ $processor_overrides -+ ); -+ } -+} -+ - /** - * Implements hook_node_access_records(). - */ diff --git a/PATCHES/3001188-make-it-possible-to-add-relationships-to-layout-builder.patch b/PATCHES/3001188-make-it-possible-to-add-relationships-to-layout-builder.patch deleted file mode 100644 index 0f4c52143..000000000 --- a/PATCHES/3001188-make-it-possible-to-add-relationships-to-layout-builder.patch +++ /dev/null @@ -1,156 +0,0 @@ -diff --git a/core/modules/layout_builder/layout_builder.api.php b/core/modules/layout_builder/layout_builder.api.php -index 66afc1fb175a49d6d475bc6a9f57e46937fcc915..fe31348445a34d99e00ee3e8564dbf80e6211c09 100644 ---- a/core/modules/layout_builder/layout_builder.api.php -+++ b/core/modules/layout_builder/layout_builder.api.php -@@ -5,6 +5,10 @@ - * Hooks provided by the Layout Builder module. - */ - -+use Drupal\Core\Plugin\Context\EntityContext; -+use Drupal\Core\Session\AccountInterface; -+use Drupal\layout_builder\SectionStorageInterface; -+ - /** - * @defgroup layout_builder_access Layout Builder access - * @{ -@@ -26,6 +30,56 @@ - * @see https://www.drupal.org/project/drupal/issues/2942975 - */ - -+/** -+ * Add and alter contexts available to sections before building content. -+ * -+ * In this example we add a customer profile context whenever a user entity is -+ * being viewed. -+ * -+ * @param \Drupal\Core\Plugin\Context\ContextInterface[] &$contexts -+ * Array of contexts for the layout keyed. -+ * @param \Drupal\layout_builder\SectionStorageInterface $section_storage -+ * The section storage used to construct the layout. This is provided so that -+ * DefaultsSectionStorage third party settings can be used to alter contexts. -+ * Note that OverridesSectionStorage does not have third party settings. -+ * @param bool $sample -+ * Whether or not to permit sample entities. If true, you should ensure that -+ * every possible context has a value to allow for preview and placeholder -+ * content in blocks. -+ */ -+function hook_layout_builder_view_context_alter(array &$contexts, SectionStorageInterface $section_storage, $sample = FALSE) { -+ if (!isset($contexts['layout_builder.entity'])) { -+ return; -+ } -+ -+ /** @var \Drupal\Core\Plugin\Context\EntityContext $layout_entity_context */ -+ $layout_entity_context = $contexts['layout_builder.entity']; -+ -+ /** @var \Drupal\Core\Entity\EntityInterface $layout_entity */ -+ $layout_entity = $layout_entity_context->getContextData()->getValue(); -+ -+ if ($layout_entity instanceof AccountInterface) { -+ $profile_types = [ -+ 'customer', -+ ]; -+ -+ foreach ($profile_types as $type) { -+ if ($layout_entity->get("profile_{$type}")->target_id) { -+ $entity = $layout_entity->get("profile_{$type}")->entity; -+ } -+ elseif ($sample) { -+ /** @var \Drupal\layout_builder\Entity\SampleEntityGeneratorInterface $sample_generator */ -+ $sample_generator = \Drupal::service('layout_builder.sample_entity_generator'); -+ $entity = $sample_generator->get('profile', $type); -+ } -+ -+ if (isset($entity)) { -+ $contexts['layout_builder.additional.' . $type] = EntityContext::fromEntity($entity); -+ } -+ } -+ } -+} -+ - /** - * @} End of "defgroup layout_builder_access". - */ -diff --git a/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php -index 70e86f69991359deb6c086cc1a12df7a3711037e..f4fae08d088ffc15680b8aeea4971048403d20f8 100644 ---- a/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php -+++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php -@@ -335,11 +335,22 @@ protected function buildSections(FieldableEntityInterface $entity) { - */ - protected function getContextsForEntity(FieldableEntityInterface $entity) { - $available_context_ids = array_keys($this->contextRepository()->getAvailableContexts()); -- return [ -+ $contexts = [ - 'view_mode' => new Context(ContextDefinition::create('string'), $this->getMode()), - 'entity' => EntityContext::fromEntity($entity), - 'display' => EntityContext::fromEntity($this), - ] + $this->contextRepository()->getRuntimeContexts($available_context_ids); -+ -+ // Get section storage to pass to contexts hook. -+ $cacheability = new CacheableMetadata(); -+ $storage = $this->sectionStorageManager()->findByContext($contexts, $cacheability); -+ -+ // Allow modules to alter the contexts available. Pass the section storage -+ // as context so that DefaultsSectionStorage's thirdPartySettings can be -+ // used to influence contexts. -+ \Drupal::moduleHandler()->alter('layout_builder_view_context', $contexts, $storage); -+ -+ return $contexts; - } - - /** -diff --git a/core/modules/layout_builder/src/Plugin/SectionStorage/DefaultsSectionStorage.php b/core/modules/layout_builder/src/Plugin/SectionStorage/DefaultsSectionStorage.php -index dc0f35707b412399c9a0bfb915e7c1f4e3c099a3..d8546c19577e394a2d9071c0cc3ec1b64fdb4710 100644 ---- a/core/modules/layout_builder/src/Plugin/SectionStorage/DefaultsSectionStorage.php -+++ b/core/modules/layout_builder/src/Plugin/SectionStorage/DefaultsSectionStorage.php -@@ -224,6 +224,9 @@ public function getContextsDuringPreview() { - $entity = $this->sampleEntityGenerator->get($display->getTargetEntityTypeId(), $display->getTargetBundle()); - - $contexts['layout_builder.entity'] = EntityContext::fromEntity($entity); -+ -+ $allow_sample = TRUE; -+ \Drupal::moduleHandler()->alter('layout_builder_view_context', $contexts, $this, $allow_sample); - return $contexts; - } - -diff --git a/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php -index 219a3c6a0a8446e16537c0563b942215c2cb032d..565c42d3ae1734f6e98efa291f81438f3a9845e5 100644 ---- a/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php -+++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php -@@ -316,6 +316,9 @@ public function getContextsDuringPreview() { - $contexts['layout_builder.entity'] = $contexts['entity']; - unset($contexts['entity']); - } -+ -+ $allow_sample = TRUE; -+ \Drupal::moduleHandler()->alter('layout_builder_view_context', $contexts, $this, $allow_sample); - return $contexts; - } - -diff --git a/core/modules/layout_builder/src/SectionComponent.php b/core/modules/layout_builder/src/SectionComponent.php -index b0f8ff13e4f3a8a6257411af2cffd78cb394d7c9..85274ca8a38976a2fadf683347959b4d6c22180b 100644 ---- a/core/modules/layout_builder/src/SectionComponent.php -+++ b/core/modules/layout_builder/src/SectionComponent.php -@@ -2,6 +2,7 @@ - - namespace Drupal\layout_builder; - -+use Drupal\Component\Plugin\Exception\ContextException; - use Drupal\Component\Plugin\Exception\PluginException; - use Drupal\Core\Plugin\ContextAwarePluginInterface; - use Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent; -@@ -86,7 +87,14 @@ public function __construct($uuid, $region, array $configuration = [], array $ad - * A renderable array representing the content of the component. - */ - public function toRenderArray(array $contexts = [], $in_preview = FALSE) { -- $event = new SectionComponentBuildRenderArrayEvent($this, $contexts, $in_preview); -+ // If plugin instantiation throws an exception due to missing context, -+ // return an empty array. -+ try { -+ $event = new SectionComponentBuildRenderArrayEvent($this, $contexts, $in_preview); -+ } -+ catch (ContextException $e) { -+ return []; -+ } - $this->eventDispatcher()->dispatch($event, LayoutBuilderEvents::SECTION_COMPONENT_BUILD_RENDER_ARRAY); - $output = $event->getBuild(); - $event->getCacheableMetadata()->applyTo($output); diff --git a/PATCHES/3008924-rerolled-24.patch b/PATCHES/3008924-rerolled-24.patch deleted file mode 100644 index daa4bc6e3..000000000 --- a/PATCHES/3008924-rerolled-24.patch +++ /dev/null @@ -1,40 +0,0 @@ -diff --git a/core/modules/layout_builder/src/LayoutEntityHelperTrait.php b/core/modules/layout_builder/src/LayoutEntityHelperTrait.php -index 1abf322178..39193dc132 100644 ---- a/core/modules/layout_builder/src/LayoutEntityHelperTrait.php -+++ b/core/modules/layout_builder/src/LayoutEntityHelperTrait.php -@@ -62,12 +62,14 @@ protected function getInlineBlockRevisionIdsInSections(array $sections) { - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity. -+ * @param string $view_mode -+ * A view mode identifier. - * - * @return \Drupal\layout_builder\Section[] - * The entity layout sections if available. - */ -- protected function getEntitySections(EntityInterface $entity) { -- $section_storage = $this->getSectionStorageForEntity($entity); -+ protected function getEntitySections(EntityInterface $entity, $view_mode = 'full') { -+ $section_storage = $this->getSectionStorageForEntity($entity, $view_mode); - return $section_storage ? $section_storage->getSections() : []; - } - -@@ -98,14 +100,13 @@ protected function getInlineBlockComponents(array $sections) { - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity. -+ * @param string $view_mode -+ * A view mode identifier. - * - * @return \Drupal\layout_builder\SectionStorageInterface|null -- * The section storage if found otherwise NULL. -+ * The section storage or NULL if its context requirements are not met. - */ -- protected function getSectionStorageForEntity(EntityInterface $entity) { -- // @todo Take into account other view modes in -- // https://www.drupal.org/node/3008924. -- $view_mode = 'full'; -+ protected function getSectionStorageForEntity(EntityInterface $entity, $view_mode = 'full') { - if ($entity instanceof LayoutEntityDisplayInterface) { - $contexts['display'] = EntityContext::fromEntity($entity); - $contexts['view_mode'] = new Context(new ContextDefinition('string'), $entity->getMode()); diff --git a/PATCHES/3153137-custom-fix-3281606.patch b/PATCHES/3153137-custom-fix-3281606.patch deleted file mode 100644 index 35cdff620..000000000 --- a/PATCHES/3153137-custom-fix-3281606.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/webp.module b/webp.module -index bdd4e35..7b6c056 100644 ---- a/webp.module -+++ b/webp.module -@@ -109,7 +109,7 @@ function webp_flush_webp_derivatives(EntityInterface $entity) { - $file_system = ($file_system) ?: \Drupal::service('file_system'); // Only load once. - foreach ($styles as $style) { - $derivative_uri = $style->buildUri($file_uri); -- $derivative_webp_uri = preg_replace('/\.(png|jpg|jpeg)$/i', '.webp', $derivative_uri); -+ $derivative_webp_uri = $derivative_uri . '.webp'; - - if (file_exists($derivative_webp_uri)) { - try { diff --git a/PATCHES/3467860.mr-9219.patch b/PATCHES/3467860.mr-9219.patch deleted file mode 100644 index 3a5deabf8..000000000 --- a/PATCHES/3467860.mr-9219.patch +++ /dev/null @@ -1,374 +0,0 @@ -diff --git a/core/core.libraries.yml b/core/core.libraries.yml -index 9e34b6c95bd1fe2e2581c987be2dbfa53dace052..aefb2db971777c71bfa1e03a728bea0dc504622a 100644 ---- a/core/core.libraries.yml -+++ b/core/core.libraries.yml -@@ -725,8 +725,7 @@ drupal.touchevents-test: - drupal.vertical-tabs: - version: VERSION - js: -- # Load before core/drupal.collapse. -- misc/vertical-tabs.js: { weight: -1 } -+ misc/vertical-tabs.js: {} - css: - component: - misc/vertical-tabs.css: {} -diff --git a/core/lib/Drupal/Core/Asset/AssetResolver.php b/core/lib/Drupal/Core/Asset/AssetResolver.php -index 4d42f4235984e499e92a765f4cf8301415c69cad..e853f63b78c864210dd78c004b8a2b651f01b081 100644 ---- a/core/lib/Drupal/Core/Asset/AssetResolver.php -+++ b/core/lib/Drupal/Core/Asset/AssetResolver.php -@@ -107,31 +107,77 @@ public function __construct(LibraryDiscoveryInterface $library_discovery, Librar - * $assets = new AttachedAssets(); - * $assets->setLibraries(['core/a', 'core/b', 'core/c']); - * $assets->setAlreadyLoadedLibraries(['core/c']); -- * $resolver->getLibrariesToLoad($assets) === ['core/a', 'core/b', 'core/d'] -+ * $resolver->getLibrariesToLoad($assets, 'js') === ['core/a', 'core/b', 'core/d'] - * @endcode - * -+ * The attached assets tend to be in the order that libraries were attached -+ * during a request. To minimize the number of unique aggregated asset URLs -+ * and files, we normalize the list by filtering out libraries that don't -+ * include the asset type being built as well as ensuring a reliable order of -+ * the libraries based on their dependencies. -+ * - * @param \Drupal\Core\Asset\AttachedAssetsInterface $assets - * The assets attached to the current response. -+ * @param string|null $asset_type -+ * The asset type to load. - * - * @return string[] - * A list of libraries and their dependencies, in the order they should be - * loaded, excluding any libraries that have already been loaded. - */ -- protected function getLibrariesToLoad(AttachedAssetsInterface $assets) { -- // The order of libraries passed in via assets can differ, so to reduce -- // variation, first normalize the requested libraries to the minimal -- // representative set before then expanding the list to include all -- // dependencies. -+ protected function getLibrariesToLoad(AttachedAssetsInterface $assets, ?string $asset_type = NULL) { - // @see Drupal\FunctionalTests\Core\Asset\AssetOptimizationTestUmami - // @todo https://www.drupal.org/project/drupal/issues/1945262 -- $libraries = $assets->getLibraries(); -- if ($libraries) { -- $libraries = $this->libraryDependencyResolver->getMinimalRepresentativeSubset($libraries); -+ $libraries_to_load = array_diff( -+ $this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getLibraries()), -+ $this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getAlreadyLoadedLibraries()) -+ ); -+ if ($asset_type) { -+ $libraries_to_load = $this->filterLibrariesByType($libraries_to_load, $asset_type); - } -- return array_diff( -- $this->libraryDependencyResolver->getLibrariesWithDependencies($libraries), -+ -+ // We now have a complete list of libraries requested. However, this list -+ // could be in any order depending on when libraries were attached during -+ // the page request, which can result in different file contents and URLs -+ // even for an otherwise identical set of libraries. To ensure that any -+ // particular set of libraries results in the same aggregate URL, sort the -+ // libraries, then generate the minimum representative set again. -+ sort($libraries_to_load); -+ $minimum_libraries = $this->libraryDependencyResolver->getMinimalRepresentativeSubset($libraries_to_load); -+ $libraries_to_load = array_diff( -+ $this->libraryDependencyResolver->getLibrariesWithDependencies($minimum_libraries), - $this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getAlreadyLoadedLibraries()) - ); -+ -+ // Now remove any libraries without the relevant asset type again, since -+ // they have been brought back in via dependencies. -+ if ($asset_type) { -+ $libraries_to_load = $this->filterLibrariesByType($libraries_to_load, $asset_type); -+ } -+ -+ return $libraries_to_load; -+ } -+ -+ /** -+ * Filter libraries that don't contain an asset type. -+ * -+ * @param array $libraries -+ * An array of library definitions. -+ * @param string $asset_type -+ * The type of asset, either 'js' or 'css'. -+ * -+ * @return array -+ * The filtered libraries array. -+ */ -+ protected function filterLibrariesByType(array $libraries, string $asset_type): array { -+ foreach ($libraries as $key => $library) { -+ [$extension, $name] = explode('/', $library, 2); -+ $definition = $this->libraryDiscovery->getLibraryByName($extension, $name); -+ if (empty($definition[$asset_type])) { -+ unset($libraries[$key]); -+ } -+ } -+ return $libraries; - } - - /** -@@ -141,15 +187,9 @@ public function getCssAssets(AttachedAssetsInterface $assets, $optimize, ?Langua - if (!$assets->getLibraries()) { - return []; - } -- $libraries_to_load = $this->getLibrariesToLoad($assets); -- foreach ($libraries_to_load as $key => $library) { -- [$extension, $name] = explode('/', $library, 2); -- $definition = $this->libraryDiscovery->getLibraryByName($extension, $name); -- if (empty($definition['css'])) { -- unset($libraries_to_load[$key]); -- } -- } -- $libraries_to_load = array_values($libraries_to_load); -+ // Get the complete list of libraries to load including dependencies. -+ $libraries_to_load = $this->getLibrariesToLoad($assets, 'css'); -+ - if (!$libraries_to_load) { - return []; - } -@@ -177,7 +217,7 @@ public function getCssAssets(AttachedAssetsInterface $assets, $optimize, ?Langua - 'preprocess' => TRUE, - ]; - -- foreach ($libraries_to_load as $key => $library) { -+ foreach ($libraries_to_load as $library) { - [$extension, $name] = explode('/', $library, 2); - $definition = $this->libraryDiscovery->getLibraryByName($extension, $name); - foreach ($definition['css'] as $options) { -@@ -231,7 +271,7 @@ public function getCssAssets(AttachedAssetsInterface $assets, $optimize, ?Langua - protected function getJsSettingsAssets(AttachedAssetsInterface $assets) { - $settings = []; - -- foreach ($this->getLibrariesToLoad($assets) as $library) { -+ foreach ($this->getLibrariesToLoad($assets, 'js') as $library) { - [$extension, $name] = explode('/', $library, 2); - $definition = $this->libraryDiscovery->getLibraryByName($extension, $name); - if (isset($definition['drupalSettings'])) { -@@ -253,24 +293,19 @@ public function getJsAssets(AttachedAssetsInterface $assets, $optimize, ?Languag - $language = $this->languageManager->getCurrentLanguage(); - } - $theme_info = $this->themeManager->getActiveTheme(); -- $libraries_to_load = $this->getLibrariesToLoad($assets); -+ -+ // Get the complete list of libraries to load including dependencies. -+ $libraries_to_load = $this->getLibrariesToLoad($assets, 'js'); - - // Collect all libraries that contain JS assets and are in the header. -- // Also remove any libraries with no JavaScript from the libraries to -- // load. - $header_js_libraries = []; - foreach ($libraries_to_load as $key => $library) { - [$extension, $name] = explode('/', $library, 2); - $definition = $this->libraryDiscovery->getLibraryByName($extension, $name); -- if (empty($definition['js'])) { -- unset($libraries_to_load[$key]); -- continue; -- } - if (!empty($definition['header'])) { - $header_js_libraries[] = $library; - } - } -- $libraries_to_load = array_values($libraries_to_load); - - // If all the libraries to load contained only CSS, there is nothing further - // to do here, so return early. -diff --git a/core/modules/ckeditor5/ckeditor5.libraries.yml b/core/modules/ckeditor5/ckeditor5.libraries.yml -index 70ebcf51704a2e7562eead3e69925b5148cc5246..73d3e7ea00af1483e4b41e73793096410b3c08bc 100644 ---- a/core/modules/ckeditor5/ckeditor5.libraries.yml -+++ b/core/modules/ckeditor5/ckeditor5.libraries.yml -@@ -99,6 +99,7 @@ internal.drupal.ckeditor5.filter.admin: - - core/once - - core/drupal.ajax - - core/drupalSettings -+ - core/drupal.vertical-tabs - - internal.drupal.ckeditor5.table: - css: -diff --git a/core/tests/Drupal/Tests/Core/Asset/AssetResolverTest.php b/core/tests/Drupal/Tests/Core/Asset/AssetResolverTest.php -index faf3dcd3c8b31b556e1653ed8f471a1629adf0de..3ea3dc87c0c3e2a178e88256a414610e4f0e4291 100644 ---- a/core/tests/Drupal/Tests/Core/Asset/AssetResolverTest.php -+++ b/core/tests/Drupal/Tests/Core/Asset/AssetResolverTest.php -@@ -8,6 +8,8 @@ - use Drupal\Core\Asset\AssetResolver; - use Drupal\Core\Asset\AttachedAssets; - use Drupal\Core\Asset\AttachedAssetsInterface; -+use Drupal\Core\Asset\JsCollectionGrouper; -+use Drupal\Core\Asset\LibraryDependencyResolver; - use Drupal\Core\Cache\MemoryBackend; - use Drupal\Core\Language\LanguageInterface; - use Drupal\Tests\UnitTestCase; -@@ -97,51 +99,74 @@ protected function setUp(): void { - $this->libraryDiscovery = $this->getMockBuilder('Drupal\Core\Asset\LibraryDiscovery') - ->disableOriginalConstructor() - ->getMock(); -+ $this->libraryDiscovery->expects($this->any()) -+ ->method('getLibraryByName') -+ ->willReturnCallback(function ($extension, $name) { -+ return $this->libraries[$extension . '/' . $name]; -+ }); - $this->libraries = [ -- 'drupal' => [ -+ 'core/drupal' => [ - 'version' => '1.0.0', - 'css' => [], - 'js' => -- [ -- 'core/misc/drupal.js' => ['data' => 'core/misc/drupal.js', 'preprocess' => TRUE], -- ], -+ [ -+ 'core/misc/drupal.js' => ['data' => 'core/misc/drupal.js', 'preprocess' => TRUE], -+ ], - 'license' => '', - ], -- 'jquery' => [ -+ 'core/jquery' => [ - 'version' => '1.0.0', - 'css' => [], - 'js' => -- [ -- 'core/misc/jquery.js' => ['data' => 'core/misc/jquery.js', 'minified' => TRUE], -- ], -+ [ -+ 'core/misc/jquery.js' => ['data' => 'core/misc/jquery.js', 'minified' => TRUE], -+ ], - 'license' => '', - ], -- 'llama' => [ -+ 'llama/css' => [ - 'version' => '1.0.0', - 'css' => -- [ -- 'core/misc/llama.css' => ['data' => 'core/misc/llama.css'], -- ], -+ [ -+ 'core/misc/llama.css' => ['data' => 'core/misc/llama.css'], -+ ], - 'js' => [], - 'license' => '', - ], -- 'piggy' => [ -+ 'piggy/css' => [ - 'version' => '1.0.0', - 'css' => -- [ -- 'core/misc/piggy.css' => ['data' => 'core/misc/piggy.css'], -+ [ -+ 'core/misc/piggy.css' => ['data' => 'core/misc/piggy.css'], -+ ], -+ 'js' => [], -+ 'license' => '', -+ ], -+ 'core/ckeditor5' => [ -+ 'remote' => 'https://github.com/ckeditor/ckeditor5', -+ 'version' => '1.0.0', -+ 'license' => '', -+ 'js' => [ -+ 'assets/vendor/ckeditor5/ckeditor5-dll/ckeditor5-dll.js' => [ -+ 'data' => 'assets/vendor/ckeditor5/ckeditor5-dll/ckeditor5-dll.js', -+ 'preprocess' => FALSE, -+ 'minified' => TRUE, -+ ], - ], -+ ], -+ 'piggy/ckeditor' => [ -+ 'version' => '1.0.0', -+ 'css' => -+ [ -+ 'core/misc/ckeditor.css' => ['data' => 'core/misc/ckeditor.css'], -+ ], - 'js' => [], - 'license' => '', -+ 'dependencies' => [ -+ 'core/ckeditor5', -+ ], - ], - ]; -- $this->libraryDependencyResolver = $this->createMock('\Drupal\Core\Asset\LibraryDependencyResolverInterface'); -- $this->libraryDependencyResolver->expects($this->any()) -- ->method('getLibrariesWithDependencies') -- ->willReturnArgument(0); -- $this->libraryDependencyResolver->expects($this->any()) -- ->method('getMinimalRepresentativeSubset') -- ->willReturnArgument(0); -+ $this->libraryDependencyResolver = new LibraryDependencyResolver($this->libraryDiscovery); - $this->moduleHandler = $this->createMock('\Drupal\Core\Extension\ModuleHandlerInterface'); - $this->themeManager = $this->createMock('\Drupal\Core\Theme\ThemeManagerInterface'); - $active_theme = $this->getMockBuilder('\Drupal\Core\Theme\ActiveTheme') -@@ -178,22 +203,17 @@ protected function setUp(): void { - * @dataProvider providerAttachedCssAssets - */ - public function testGetCssAssets(AttachedAssetsInterface $assets_a, AttachedAssetsInterface $assets_b, $expected_css_cache_item_count): void { -- $map = [ -- ['core', 'drupal', $this->libraries['drupal']], -- ['core', 'jquery', $this->libraries['jquery']], -- ['llama', 'css', $this->libraries['llama']], -- ['piggy', 'css', $this->libraries['piggy']], -- ]; -- $this->libraryDiscovery->method('getLibraryByName') -- ->willReturnMap($map); -- -+ $this->libraryDiscovery->expects($this->any()) -+ ->method('getLibraryByName') -+ ->willReturnCallback(function ($extension, $name) { -+ return $this->libraries[$extension . '/' . $name]; -+ }); - $this->assetResolver->getCssAssets($assets_a, FALSE, $this->english); - $this->assetResolver->getCssAssets($assets_b, FALSE, $this->english); - $this->assertCount($expected_css_cache_item_count, $this->cache->getAllCids()); - } - - public static function providerAttachedCssAssets() { -- $time = time(); - return [ - 'one js only library and one css only library' => [ - (new AttachedAssets())->setAlreadyLoadedLibraries([])->setLibraries(['core/drupal']), -@@ -213,13 +233,11 @@ public static function providerAttachedCssAssets() { - * @dataProvider providerAttachedJsAssets - */ - public function testGetJsAssets(AttachedAssetsInterface $assets_a, AttachedAssetsInterface $assets_b, $expected_js_cache_item_count, $expected_multilingual_js_cache_item_count): void { -- $map = [ -- ['core', 'drupal', $this->libraries['drupal']], -- ['core', 'jquery', $this->libraries['jquery']], -- ]; -- $this->libraryDiscovery->method('getLibraryByName') -- ->willReturnMap($map); -- -+ $this->libraryDiscovery->expects($this->any()) -+ ->method('getLibraryByName') -+ ->willReturnCallback(function ($extension, $name) { -+ return $this->libraries[$extension . '/' . $name]; -+ }); - $this->assetResolver->getJsAssets($assets_a, FALSE, $this->english); - $this->assetResolver->getJsAssets($assets_b, FALSE, $this->english); - $this->assertCount($expected_js_cache_item_count, $this->cache->getAllCids()); -@@ -247,6 +265,32 @@ public static function providerAttachedJsAssets() { - ]; - } - -+ /** -+ * Test that order of scripts are correct. -+ */ -+ public function testJsAssetsOrder(): void { -+ $time = time(); -+ $assets_a = (new AttachedAssets()) -+ ->setAlreadyLoadedLibraries([]) -+ ->setLibraries(['core/drupal', 'core/ckeditor5', 'core/jquery', 'piggy/ckeditor']) -+ ->setSettings(['currentTime' => $time]); -+ $assets_b = (new AttachedAssets()) -+ ->setAlreadyLoadedLibraries([]) -+ ->setLibraries(['piggy/ckeditor', 'core/drupal', 'core/ckeditor5', 'core/jquery']) -+ ->setSettings(['currentTime' => $time]); -+ $js_assets_a = $this->assetResolver->getJsAssets($assets_a, FALSE, $this->english); -+ $js_assets_b = $this->assetResolver->getJsAssets($assets_b, FALSE, $this->english); -+ -+ $grouper = new JsCollectionGrouper(); -+ -+ $group_a = $grouper->group($js_assets_a[1]); -+ $group_b = $grouper->group($js_assets_b[1]); -+ -+ foreach ($group_a as $key => $value) { -+ $this->assertSame($value['items'], $group_b[$key]['items']); -+ } -+ } -+ - } - - if (!defined('CSS_AGGREGATE_DEFAULT')) { \ No newline at end of file diff --git a/PATCHES/gin-toolbar-id.patch b/PATCHES/gin-toolbar-id.patch deleted file mode 100644 index dc1bd2217..000000000 --- a/PATCHES/gin-toolbar-id.patch +++ /dev/null @@ -1,171 +0,0 @@ -diff --git a/dist/css/components/settings_tray.css b/dist/css/components/settings_tray.css -index 6c23225..331d484 100644 ---- a/dist/css/components/settings_tray.css -+++ b/dist/css/components/settings_tray.css -@@ -1,20 +1,20 @@ --#gin-toolbar-bar.js-settings-tray-edit-mode button.toolbar-icon.toolbar-icon-edit.toolbar-item.is-active { -+#toolbar-bar.gin-toolbar-bar.js-settings-tray-edit-mode button.toolbar-icon.toolbar-icon-edit.toolbar-item.is-active { - color: var(--gin-bg-app); - } - --#gin-toolbar-bar.js-settings-tray-edit-mode { -+#toolbar-bar.gin-toolbar-bar.js-settings-tray-edit-mode { - background: var(--gin-color-primary); - } - --#gin-toolbar-bar.js-settings-tray-edit-mode button.toolbar-icon.toolbar-icon-edit.toolbar-item.is-active:hover { -+#toolbar-bar.gin-toolbar-bar.js-settings-tray-edit-mode button.toolbar-icon.toolbar-icon-edit.toolbar-item.is-active:hover { - background: var(--gin-color-primary-hover); - } - --#gin-toolbar-bar { -+#toolbar-bar.gin-toolbar-bar { - position: fixed; - } - --#gin-toolbar-bar .contextual-toolbar-tab { -+#toolbar-bar.gin-toolbar-bar .contextual-toolbar-tab { - order: 100; - } - -@@ -37,4 +37,3 @@ - #toolbar-bar.toolbar-bar.js-settings-tray-edit-mode button.toolbar-icon.toolbar-icon-edit.toolbar-item.is-active:hover { - background-image: none; - } -- -diff --git a/dist/css/layout/toolbar.css b/dist/css/layout/toolbar.css -index 4d6522f..8eb3824 100644 ---- a/dist/css/layout/toolbar.css -+++ b/dist/css/layout/toolbar.css -@@ -176,7 +176,7 @@ - color: var(--gin-color-text); - } - --#gin-toolbar-bar.js-settings-tray-edit-mode { -+#toolbar-bar.gin-toolbar-bar.js-settings-tray-edit-mode { - justify-content: flex-end; - } - -@@ -760,4 +760,3 @@ a.toolbar-menu__trigger, - .toolbar-loading #toolbar-item-administration-tray { - box-shadow: none; - } -- -diff --git a/dist/js/toolbar.js b/dist/js/toolbar.js -index 87371a2..3912e61 100644 ---- a/dist/js/toolbar.js -+++ b/dist/js/toolbar.js -@@ -13,11 +13,11 @@ - } - }, Drupal.ginToolbar = { - init: function(context) { -- once("ginToolbarInit", "#gin-toolbar-bar", context).forEach((() => { -+ once("ginToolbarInit", "#toolbar-bar.gin-toolbar-bar", context).forEach((() => { - const toolbarTrigger = document.querySelector(".toolbar-menu__trigger"); -- "classic" != toolbarVariant && localStorage.getItem("Drupal.toolbar.trayVerticalLocked") && localStorage.removeItem("Drupal.toolbar.trayVerticalLocked"), -- "true" === localStorage.getItem("Drupal.gin.toolbarExpanded") ? (document.body.setAttribute("data-toolbar-menu", "open"), -- toolbarTrigger.classList.add("is-active")) : (document.body.setAttribute("data-toolbar-menu", ""), -+ "classic" != toolbarVariant && localStorage.getItem("Drupal.toolbar.trayVerticalLocked") && localStorage.removeItem("Drupal.toolbar.trayVerticalLocked"), -+ "true" === localStorage.getItem("Drupal.gin.toolbarExpanded") ? (document.body.setAttribute("data-toolbar-menu", "open"), -+ toolbarTrigger.classList.add("is-active")) : (document.body.setAttribute("data-toolbar-menu", ""), - toolbarTrigger.classList.remove("is-active")), document.addEventListener("keydown", (e => { - !0 === e.altKey && "KeyT" === e.code && this.toggleToolbar(); - })), this.initDisplace(); -@@ -26,7 +26,7 @@ - })))); - }, - initDisplace: () => { -- const toolbar = document.querySelector("#gin-toolbar-bar .toolbar-menu-administration"); -+ const toolbar = document.querySelector("#toolbar-bar.gin-toolbar-bar .toolbar-menu-administration"); - toolbar && ("vertical" === toolbarVariant ? toolbar.setAttribute("data-offset-left", "") : toolbar.setAttribute("data-offset-top", "")); - }, - toggleToolbar: function() { -@@ -34,13 +34,13 @@ - toolbarTrigger.classList.toggle("is-active"), toolbarTrigger.classList.contains("is-active") ? this.showToolbar() : this.collapseToolbar(); - }, - showToolbar: function() { -- document.body.setAttribute("data-toolbar-menu", "open"), localStorage.setItem("Drupal.gin.toolbarExpanded", "true"), -+ document.body.setAttribute("data-toolbar-menu", "open"), localStorage.setItem("Drupal.gin.toolbarExpanded", "true"), - this.dispatchToolbarEvent("true"), this.displaceToolbar(), window.innerWidth < 1280 && "vertical" === toolbarVariant && Drupal.ginSidebar.collapseSidebar(); - }, - collapseToolbar: function() { - const toolbarTrigger = document.querySelector(".toolbar-menu__trigger"), elementToRemove = document.querySelector(".gin-toolbar-inline-styles"); -- toolbarTrigger.classList.remove("is-active"), document.body.setAttribute("data-toolbar-menu", ""), -- elementToRemove && elementToRemove.parentNode.removeChild(elementToRemove), localStorage.setItem("Drupal.gin.toolbarExpanded", "false"), -+ toolbarTrigger.classList.remove("is-active"), document.body.setAttribute("data-toolbar-menu", ""), -+ elementToRemove && elementToRemove.parentNode.removeChild(elementToRemove), localStorage.setItem("Drupal.gin.toolbarExpanded", "false"), - this.dispatchToolbarEvent("false"), this.displaceToolbar(); - }, - dispatchToolbarEvent: active => { -diff --git a/js/toolbar.js b/js/toolbar.js -index aa50459..aa76be0 100644 ---- a/js/toolbar.js -+++ b/js/toolbar.js -@@ -31,7 +31,7 @@ - - Drupal.ginToolbar = { - init: function (context) { -- once('ginToolbarInit', '#gin-toolbar-bar', context).forEach(() => { -+ once('ginToolbarInit', '#toolbar-bar.gin-toolbar-bar', context).forEach(() => { - const toolbarTrigger = document.querySelector('.toolbar-menu__trigger'); - - // Check for Drupal trayVerticalLocked and remove it. -diff --git a/styles/components/settings_tray.scss b/styles/components/settings_tray.scss -index 9df1f21..40d307a 100644 ---- a/styles/components/settings_tray.scss -+++ b/styles/components/settings_tray.scss -@@ -1,16 +1,16 @@ --#gin-toolbar-bar.js-settings-tray-edit-mode button.toolbar-icon.toolbar-icon-edit.toolbar-item.is-active { -+#toolbar-bar.gin-toolbar-bar.js-settings-tray-edit-mode button.toolbar-icon.toolbar-icon-edit.toolbar-item.is-active { - color: var(--gin-bg-app); - } - --#gin-toolbar-bar.js-settings-tray-edit-mode { -+#toolbar-bar.gin-toolbar-bar.js-settings-tray-edit-mode { - background: var(--gin-color-primary); - } - --#gin-toolbar-bar.js-settings-tray-edit-mode button.toolbar-icon.toolbar-icon-edit.toolbar-item.is-active:hover { -+#toolbar-bar.gin-toolbar-bar.js-settings-tray-edit-mode button.toolbar-icon.toolbar-icon-edit.toolbar-item.is-active:hover { - background: var(--gin-color-primary-hover); - } - --#gin-toolbar-bar { -+#toolbar-bar.gin-toolbar-bar { - position: fixed; - - .contextual-toolbar-tab { -@@ -36,4 +36,3 @@ - background-image: none; - } - } -- -diff --git a/styles/layout/toolbar.scss b/styles/layout/toolbar.scss -index 6e51cf5..d537490 100644 ---- a/styles/layout/toolbar.scss -+++ b/styles/layout/toolbar.scss -@@ -156,7 +156,7 @@ - color: var(--gin-color-text); - } - --#gin-toolbar-bar.js-settings-tray-edit-mode { -+#toolbar-bar.gin-toolbar-bar.js-settings-tray-edit-mode { - justify-content: flex-end; - } - -diff --git a/templates/navigation/toolbar--gin.html.twig b/templates/navigation/toolbar--gin.html.twig -index 908d3eb..2c7829a 100644 ---- a/templates/navigation/toolbar--gin.html.twig -+++ b/templates/navigation/toolbar--gin.html.twig -@@ -20,9 +20,9 @@ - * @see template_preprocess_toolbar() - */ - #} --{% set gin_toolbar_id = toolbar_variant != 'classic' ? 'gin-toolbar-bar' : 'toolbar-bar' %} -+{% set gin_toolbar_class = toolbar_variant != 'classic' ? 'gin-toolbar-bar' : 'toolbar-bar' %} - -- -+ -

{{ toolbar_heading }}

- {% for key, tab in tabs %} - {% set tray = trays[key] %} diff --git a/PATCHES/hook_gin_lb_add_suggestions_alter-1.0.0-rc8.patch b/PATCHES/hook_gin_lb_add_suggestions_alter-1.0.0-rc8.patch deleted file mode 100644 index 86d61b4f7..000000000 --- a/PATCHES/hook_gin_lb_add_suggestions_alter-1.0.0-rc8.patch +++ /dev/null @@ -1,120 +0,0 @@ -diff --git a/gin_lb.api.php b/gin_lb.api.php -index 2db241a..075d1ac 100644 ---- a/gin_lb.api.php -+++ b/gin_lb.api.php -@@ -42,6 +42,25 @@ function hook_gin_lb_is_layout_builder_route_alter(&$gin_lb_is_layout_builder_ro - } - } - -+/** -+ * Alter the template suggestion logic. -+ * -+ * Allows modules to overrule whether template suggestions should be added. -+ * -+ * @param boolean $add_suggestions -+ * Boolean flag. -+ * @param array $variables -+ * The variables array of a render item. -+ * @param string $hook -+ * The theme hook. -+ */ -+function hook_gin_lb_add_suggestions_alter(&$add_suggestions, $variables, $hook) { -+ $route_name = \Drupal::routeMatch()->getRouteName(); -+ if ($route_name == 'layout_builder.add_block' && $add_suggestions) { -+ $add_suggestions = FALSE; -+ } -+} -+ - /** - * @} End of "addtogroup hooks". - */ -diff --git a/gin_lb.module b/gin_lb.module -index 0322466..6da3c0d 100644 ---- a/gin_lb.module -+++ b/gin_lb.module -@@ -231,6 +231,17 @@ function gin_lb_theme(): array { - return $instance->themes(); - } - -+/** -+ * Implements hook_theme_registry_alter(). -+ */ -+function gin_lb_theme_registry_alter(&$theme_registry) { -+ foreach ($theme_registry as &$value) { -+ if (array_key_exists('variables', $value)) { -+ $value['variables']['gin_lb_theme_suggestions'] = NULL; -+ } -+ } -+} -+ - /** - * Implements hook_theme_suggestions_alter(). - */ -diff --git a/src/HookHandler/ThemeSuggestionsAlter.php b/src/HookHandler/ThemeSuggestionsAlter.php -index e43e4ce..185d8cb 100644 ---- a/src/HookHandler/ThemeSuggestionsAlter.php -+++ b/src/HookHandler/ThemeSuggestionsAlter.php -@@ -5,6 +5,7 @@ declare(strict_types=1); - namespace Drupal\gin_lb\HookHandler; - - use Drupal\Core\DependencyInjection\ContainerInjectionInterface; -+use Drupal\Core\Extension\ModuleHandlerInterface; - use Drupal\Core\Routing\RouteMatchInterface; - use Drupal\gin_lb\Service\ContextValidatorInterface; - use Drupal\views\ViewExecutable; -@@ -16,6 +17,13 @@ use Symfony\Component\HttpFoundation\RequestStack; - */ - class ThemeSuggestionsAlter implements ContainerInjectionInterface { - -+ /** -+ * The module handler. -+ * -+ * @var \Drupal\Core\Extension\ModuleHandlerInterface -+ */ -+ protected ModuleHandlerInterface $moduleHandler; -+ - /** - * The current route match. - * -@@ -77,6 +85,8 @@ class ThemeSuggestionsAlter implements ContainerInjectionInterface { - /** - * Constructor. - * -+ * @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler -+ * The module handler. - * @param \Drupal\Core\Routing\RouteMatchInterface $currentRouteMatch - * The current route match. - * @param \Symfony\Component\HttpFoundation\RequestStack $requestStack -@@ -85,10 +95,12 @@ class ThemeSuggestionsAlter implements ContainerInjectionInterface { - * The context validator. - */ - public function __construct( -+ ModuleHandlerInterface $moduleHandler, - RouteMatchInterface $currentRouteMatch, - RequestStack $requestStack, - ContextValidatorInterface $contextValidator, - ) { -+ $this->moduleHandler = $moduleHandler; - $this->currentRouteMatch = $currentRouteMatch; - $this->requestStack = $requestStack; - $this->contextValidator = $contextValidator; -@@ -100,6 +112,7 @@ class ThemeSuggestionsAlter implements ContainerInjectionInterface { - public static function create(ContainerInterface $container): static { - // @phpstan-ignore-next-line - return new static( -+ $container->get('module_handler'), - $container->get('current_route_match'), - $container->get('request_stack'), - $container->get('gin_lb.context_validator') -@@ -125,7 +138,10 @@ class ThemeSuggestionsAlter implements ContainerInjectionInterface { - $suggestions[] = $hook . '__gin_lb'; - } - -- if (isset($variables['element']['#gin_lb_form']) || $this->hasSuggestions($variables, $hook)) { -+ $has_suggestions = $this->hasSuggestions($variables, $hook); -+ $this->moduleHandler->alter('gin_lb_add_suggestions', $has_suggestions, $variables, $hook); -+ -+ if (isset($variables['element']['#gin_lb_form']) || $has_suggestions) { - // Fix form element suggestions when they are not implemented in the - // theme. - if (empty($suggestions) && !empty($variables['theme_hook_original'])) { diff --git a/PATCHES/missing-context-value-in-layout-builder-admin.patch b/PATCHES/missing-context-value-in-layout-builder-admin.patch deleted file mode 100644 index c7cb83b0d..000000000 --- a/PATCHES/missing-context-value-in-layout-builder-admin.patch +++ /dev/null @@ -1,18 +0,0 @@ -diff --git a/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php b/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php -index 248cf7adbf..3914f7521c 100644 ---- a/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php -+++ b/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php -@@ -104,8 +104,13 @@ public function applyContextMapping(ContextAwarePluginInterface $plugin, $contex - - // Pass the value to the plugin if there is one. - if ($contexts[$context_id]->hasContextValue()) { -+ // Passed in context. - $plugin->setContext($plugin_context_id, $contexts[$context_id]); - } -+ elseif ($plugin_context->hasContextValue()) { -+ // Default context value. -+ $plugin->setContext($plugin_context_id, $plugin_context); -+ } - elseif ($plugin_context_definition->isRequired()) { - // Collect required contexts that exist but are missing a value. - $missing_value[] = $plugin_context_id;