From a8556f6f3c24a71180e55c8f55e15e61741be81d Mon Sep 17 00:00:00 2001
From: Lee Rowlands <lee.rowlands@previousnext.com.au>
Date: Tue, 24 Dec 2024 06:51:01 +1000
Subject: [PATCH] Issue #3482691 by james.williams, arunkumark,
 kristiaanvandeneynde, smustgrave: BreadcrumbManager ignores cacheability when
 no builders apply

(cherry picked from commit 78bbe4db986fb8ce21a892eec8c971fb92dd73db)
---
 .../Core/Breadcrumb/BreadcrumbManager.php     |  6 ++-
 .../modules/menu_test/menu_test.routing.yml   |  8 ++++
 .../menu_test/src/MenuTestServiceProvider.php | 28 +++++++++++++
 .../SkippablePathBasedBreadcrumbBuilder.php   | 40 +++++++++++++++++++
 .../src/Functional/Menu/BreadcrumbTest.php    |  9 +++++
 5 files changed, 89 insertions(+), 2 deletions(-)
 create mode 100644 core/modules/system/tests/modules/menu_test/src/MenuTestServiceProvider.php
 create mode 100644 core/modules/system/tests/modules/menu_test/src/SkippablePathBasedBreadcrumbBuilder.php

diff --git a/core/lib/Drupal/Core/Breadcrumb/BreadcrumbManager.php b/core/lib/Drupal/Core/Breadcrumb/BreadcrumbManager.php
index 1c745ed6a262..0a75387dd1d8 100644
--- a/core/lib/Drupal/Core/Breadcrumb/BreadcrumbManager.php
+++ b/core/lib/Drupal/Core/Breadcrumb/BreadcrumbManager.php
@@ -83,16 +83,18 @@ public function build(RouteMatchInterface $route_match) {
       }
 
       $breadcrumb = $builder->build($route_match);
-
       if ($breadcrumb instanceof Breadcrumb) {
         $context['builder'] = $builder;
-        $breadcrumb->addCacheableDependency($cacheable_metadata);
         break;
       }
       else {
         throw new \UnexpectedValueException('Invalid breadcrumb returned by ' . get_class($builder) . '::build().');
       }
     }
+
+    // Ensure all collected cacheability is applied.
+    $breadcrumb->addCacheableDependency($cacheable_metadata);
+
     // Allow modules to alter the breadcrumb.
     $this->moduleHandler->alter('system_breadcrumb', $breadcrumb, $route_match, $context);
 
diff --git a/core/modules/system/tests/modules/menu_test/menu_test.routing.yml b/core/modules/system/tests/modules/menu_test/menu_test.routing.yml
index 4f3c9e28b1c5..f427fe5fe87e 100644
--- a/core/modules/system/tests/modules/menu_test/menu_test.routing.yml
+++ b/core/modules/system/tests/modules/menu_test/menu_test.routing.yml
@@ -577,3 +577,11 @@ menu_test.breadcrumb3:
     _title: 'Normal title'
   requirements:
     _access: 'TRUE'
+
+menu_test.skippable-breadcrumb:
+  path: '/menu-test/skippable-breadcrumb'
+  defaults:
+    _controller: '\Drupal\menu_test\Controller\MenuTestController::menuTestCallback'
+    _title: 'Normal title'
+  requirements:
+    _access: 'TRUE'
diff --git a/core/modules/system/tests/modules/menu_test/src/MenuTestServiceProvider.php b/core/modules/system/tests/modules/menu_test/src/MenuTestServiceProvider.php
new file mode 100644
index 000000000000..f6aad34ecf18
--- /dev/null
+++ b/core/modules/system/tests/modules/menu_test/src/MenuTestServiceProvider.php
@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\menu_test;
+
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Core\DependencyInjection\ServiceModifierInterface;
+use Symfony\Component\DependencyInjection\Reference;
+
+/**
+ * Decorate core's default path-based breadcrumb builder when it is available.
+ */
+class MenuTestServiceProvider implements ServiceModifierInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function alter(ContainerBuilder $container): void {
+    if ($container->has('system.breadcrumb.default')) {
+      $container->register('menu_test.breadcrumb.default', SkippablePathBasedBreadcrumbBuilder::class)
+        ->setDecoratedService('system.breadcrumb.default')
+        ->addArgument(new Reference('menu_test.breadcrumb.default.inner'))
+        ->addArgument(new Reference('request_stack'));
+    }
+  }
+
+}
diff --git a/core/modules/system/tests/modules/menu_test/src/SkippablePathBasedBreadcrumbBuilder.php b/core/modules/system/tests/modules/menu_test/src/SkippablePathBasedBreadcrumbBuilder.php
new file mode 100644
index 000000000000..fc075042ad6e
--- /dev/null
+++ b/core/modules/system/tests/modules/menu_test/src/SkippablePathBasedBreadcrumbBuilder.php
@@ -0,0 +1,40 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\menu_test;
+
+use Drupal\Core\Breadcrumb\Breadcrumb;
+use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface;
+use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Symfony\Component\HttpFoundation\RequestStack;
+
+/**
+ * A path-based breadcrumb builder can be skipped from applying.
+ */
+class SkippablePathBasedBreadcrumbBuilder implements BreadcrumbBuilderInterface {
+
+  public function __construct(
+    protected BreadcrumbBuilderInterface $pathBasedBreadcrumbBuilder,
+    protected RequestStack $requestStack,
+  ) {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function applies(RouteMatchInterface $route_match, ?CacheableMetadata $cacheable_metadata = NULL): bool {
+    $query_arg = 'menu_test_skip_breadcrumbs';
+    $cacheable_metadata?->addCacheContexts(['url.query_args:' . $query_arg]);
+    // Apply unless the query argument is present.
+    return !$this->requestStack->getCurrentRequest()->query->has($query_arg);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function build(RouteMatchInterface $route_match): Breadcrumb {
+    return $this->pathBasedBreadcrumbBuilder->build($route_match);
+  }
+
+}
diff --git a/core/modules/system/tests/src/Functional/Menu/BreadcrumbTest.php b/core/modules/system/tests/src/Functional/Menu/BreadcrumbTest.php
index ce5ebd448eef..e450a0de6e63 100644
--- a/core/modules/system/tests/src/Functional/Menu/BreadcrumbTest.php
+++ b/core/modules/system/tests/src/Functional/Menu/BreadcrumbTest.php
@@ -388,6 +388,15 @@ public function testBreadCrumbs(): void {
     $this->drupalGet('menu-test/breadcrumb1/breadcrumb2/breadcrumb3');
     $this->assertSession()->responseContains('<script>alert(12);</script>');
     $this->assertSession()->assertEscaped('<script>alert(123);</script>');
+
+    // Assert that the breadcrumb cacheability is respected after not applying.
+    $this->assertBreadcrumb(Url::fromRoute('menu_test.skippable-breadcrumb', [], [
+      'query' => [
+        'menu_test_skip_breadcrumbs' => 'yes',
+      ],
+    ]), []);
+    $trail = $home + ['menu-test' => 'Menu test root'];
+    $this->assertBreadcrumb(Url::fromRoute('menu_test.skippable-breadcrumb'), $trail);
   }
 
   /**
-- 
GitLab