From 4e0d8003e434f0ebe8f45ba4996bd8f15b23d263 Mon Sep 17 00:00:00 2001 From: Arthur Baghdasaryan Date: Wed, 15 Apr 2026 12:11:36 +0400 Subject: [PATCH 1/3] DP-46328: Restore asset_cache_bust module --- composer.lock | 16 +- .../AssetCacheBustBehaviorTest.php | 152 ++++++++++++++++++ 2 files changed, 160 insertions(+), 8 deletions(-) create mode 100644 docroot/modules/custom/mass_caching/tests/src/ExistingSite/AssetCacheBustBehaviorTest.php diff --git a/composer.lock b/composer.lock index ed96f99206..1eef510684 100644 --- a/composer.lock +++ b/composer.lock @@ -3140,26 +3140,26 @@ }, { "name": "drupal/asset_cache_bust", - "version": "1.0.5", + "version": "1.0.6", "source": { "type": "git", "url": "https://git.drupalcode.org/project/asset_cache_bust.git", - "reference": "1.0.5" + "reference": "1.0.6" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/asset_cache_bust-1.0.5.zip", - "reference": "1.0.5", - "shasum": "72968d03e22a74515143a20054617af6f88ea013" + "url": "https://ftp.drupal.org/files/projects/asset_cache_bust-1.0.6.zip", + "reference": "1.0.6", + "shasum": "e5c901b19e4ed837c810209f077d3768ab42246d" }, "require": { - "drupal/core": "^8.7.7 || ^9 || ^10 || ^11" + "drupal/core": "^10.5 || ^11" }, "type": "drupal-module", "extra": { "drupal": { - "version": "1.0.5", - "datestamp": "1731699630", + "version": "1.0.6", + "datestamp": "1776240482", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" diff --git a/docroot/modules/custom/mass_caching/tests/src/ExistingSite/AssetCacheBustBehaviorTest.php b/docroot/modules/custom/mass_caching/tests/src/ExistingSite/AssetCacheBustBehaviorTest.php new file mode 100644 index 0000000000..ced69eb112 --- /dev/null +++ b/docroot/modules/custom/mass_caching/tests/src/ExistingSite/AssetCacheBustBehaviorTest.php @@ -0,0 +1,152 @@ +assertTrue( + \Drupal::moduleHandler()->moduleExists('asset_cache_bust'), + 'asset_cache_bust module must be enabled for this test.' + ); + + // Ensure CSS/JS aggregation is enabled so aggregate URLs are rendered. + $this->setConfigValues([ + 'system.performance' => [ + 'css' => ['preprocess' => TRUE], + 'js' => ['preprocess' => TRUE], + ], + ]); + $this->container->get('config.factory')->clearStaticCache(); + drupal_flush_all_caches(); + } + + /** + * {@inheritdoc} + */ + protected function tearDown(): void { + $this->restoreConfigValues(); + parent::tearDown(); + } + + /** + * Ensures CSS/JS aggregate URLs get the active cache-bust token. + */ + public function testAggregateAssetTokenAppended(): void { + $expected_token = $this->getActiveAssetToken(); + $this->assertNotSame('0', $expected_token, 'Asset query string token is initialized.'); + + $result = $this->collectTokensFromFrontPage(); + $this->assertNotEmpty($result['css_urls'], 'Aggregated CSS URLs are present.'); + $this->assertNotEmpty($result['js_urls'], 'Aggregated JS URLs are present.'); + $this->assertNotEmpty($result['css_token'], 'Aggregated CSS has a bare token query value.'); + $this->assertNotEmpty($result['js_token'], 'Aggregated JS has a bare token query value.'); + $this->assertSame($result['css_token'], $result['js_token'], 'CSS and JS use the same cache-bust token.'); + $this->assertSame($expected_token, $result['css_token'], 'CSS token matches active asset.query_string token.'); + $this->assertSame($expected_token, $result['js_token'], 'JS token matches active asset.query_string token.'); + + $previous_token = $expected_token; + $this->rebuildCachesAndRotateAssetToken($previous_token); + $new_token = $this->getActiveAssetToken(); + + $this->assertNotSame($previous_token, $new_token, 'Asset token changes after cache clear.'); + + $post_clear = $this->collectTokensFromFrontPage(); + $this->assertSame($new_token, $post_clear['css_token'], 'CSS token matches refreshed token after cache clear.'); + $this->assertSame($new_token, $post_clear['js_token'], 'JS token matches refreshed token after cache clear.'); + } + + /** + * Collects aggregate CSS/JS URLs and discovered bare token values. + * + * @return array + * Collected aggregate URLs and token values. + */ + private function collectTokensFromFrontPage(): array { + $this->drupalGet(''); + $html = $this->getSession()->getPage()->getContent(); + + preg_match_all('/]+href="([^"]*\/sites\/default\/files\/css\/[^"]+)"/', $html, $css_matches); + preg_match_all('/]+src="([^"]*\/sites\/default\/files\/js\/[^"]+)"/', $html, $js_matches); + + $css_urls = $css_matches[1] ?? []; + $js_urls = $js_matches[1] ?? []; + + return [ + 'css_urls' => $css_urls, + 'js_urls' => $js_urls, + 'css_token' => $this->extractBareTokenFromUrls($css_urls), + 'js_token' => $this->extractBareTokenFromUrls($js_urls), + ]; + } + + /** + * Finds a keyless query token appended to aggregate URL. + * + * @param string[] $urls + * Aggregate asset URLs. + * + * @return string|null + * Keyless token value if found. + */ + private function extractBareTokenFromUrls(array $urls): ?string { + foreach ($urls as $url) { + $decoded_url = html_entity_decode($url); + $query = parse_url($decoded_url, PHP_URL_QUERY); + if (!$query) { + continue; + } + + foreach (explode('&', $query) as $query_part) { + if ($query_part !== '' && !str_contains($query_part, '=')) { + return $query_part; + } + } + } + + return NULL; + } + + /** + * Returns the active asset token across Drupal core versions. + */ + private function getActiveAssetToken(): string { + if (\Drupal::hasService('asset.query_string')) { + return \Drupal::service('asset.query_string')->get(); + } + + return (string) \Drupal::state()->get('system.css_js_query_string', '0'); + } + + /** + * Rebuilds caches and ensures asset token rotates in-process. + */ + private function rebuildCachesAndRotateAssetToken(string $previous_token): void { + drupal_flush_all_caches(); + \Drupal::service('asset.query_string')->reset(); + + // This test runs in one PHP process, so the time can stay the same. + // If the token did not change after cache clear, set a new token value + // so we can still confirm the "after cache clear" behavior clearly. + $current = $this->getActiveAssetToken(); + if ($current === $previous_token) { + $next = base_convert((string) (\Drupal::time()->getCurrentTime() + 1), 10, 36); + \Drupal::state()->set(AssetQueryString::STATE_KEY, $next); + } + } + +} From cf37075ce62ec8cc50219a716cbb73d344f67b38 Mon Sep 17 00:00:00 2001 From: Arthur Baghdasaryan Date: Wed, 15 Apr 2026 12:12:46 +0400 Subject: [PATCH 2/3] DP-46328: Restore asset_cache_bust module --- changelogs/DP-46328.yml | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 changelogs/DP-46328.yml diff --git a/changelogs/DP-46328.yml b/changelogs/DP-46328.yml new file mode 100644 index 0000000000..ad2a12269e --- /dev/null +++ b/changelogs/DP-46328.yml @@ -0,0 +1,41 @@ +# +# Write your changelog entry here. Every pull request must have a changelog yml file. +# +# Change types: +# ############################################################################# +# You can use one of the following types: +# - Added: For new features. +# - Changed: For changes to existing functionality. +# - Deprecated: For soon-to-be removed features. +# - Removed: For removed features. +# - Fixed: For any bug fixes. +# - Security: In case of vulnerabilities. +# +# Format +# ############################################################################# +# The format is crucial. Please follow the examples below. For reference, the requirements are: +# - All 3 parts are required and you must include "Type", "description" and "issue". +# - "Type" must be left aligned and followed by a colon. +# - "description" must be indented with 2 spaces followed by a colon +# - "issue" must be indented with 4 spaces followed by a colon. +# - "issue" is for the Jira ticket number only e.g. DP-1234 +# - No extra spaces, indents, or blank lines are allowed. +# +# Example: +# ############################################################################# +# Fixed: +# - description: Fixes scrolling on edit pages in Safari. +# issue: DP-13314 +# +# You may add more than 1 description & issue for each type using the following format: +# Changed: +# - description: Automating the release branch. +# issue: DP-10166 +# - description: Second change item that needs a description. +# issue: DP-19875 +# - description: Third change item that needs a description along with an issue. +# issue: DP-19843 +# +Fixed: + - description: Restore asset_cache_bust module. + issue: DP-46328 From a5a81e8bec6b0ea1a7183d2755df7a355c9d93f1 Mon Sep 17 00:00:00 2001 From: Arthur Baghdasaryan Date: Wed, 15 Apr 2026 12:44:31 +0400 Subject: [PATCH 3/3] Enable asset_cache_bust --- conf/drupal/config/core.extension.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/conf/drupal/config/core.extension.yml b/conf/drupal/config/core.extension.yml index ea3738276b..cb919c9711 100644 --- a/conf/drupal/config/core.extension.yml +++ b/conf/drupal/config/core.extension.yml @@ -15,6 +15,7 @@ module: ai_provider_aws_bedrock: 0 akamai: 0 allowed_formats: 0 + asset_cache_bust: 0 aws: 0 better_exposed_filters: 0 block: 0