From bee410682f117595f96c509241e2d977acd1f5c8 Mon Sep 17 00:00:00 2001 From: laghee <20972610+laghee@users.noreply.github.com> Date: Tue, 14 Oct 2025 21:13:15 +0200 Subject: [PATCH 1/9] Initial PoC windows brand hints override --- .../config/brand-override.json | 23 +++ .../test-pages/windows-brand-hints/index.html | 15 ++ .../pages/brand-override.html | 139 ++++++++++++++++++ .../windows-brand-hints/pages/quick-test.html | 111 ++++++++++++++ .../windows-brand-hints.spec.js | 19 +++ injected/playwright.config.js | 1 + injected/src/features.js | 2 + injected/src/features/windows-brand-hints.js | 62 ++++++++ 8 files changed, 372 insertions(+) create mode 100644 injected/integration-test/test-pages/windows-brand-hints/config/brand-override.json create mode 100644 injected/integration-test/test-pages/windows-brand-hints/index.html create mode 100644 injected/integration-test/test-pages/windows-brand-hints/pages/brand-override.html create mode 100644 injected/integration-test/test-pages/windows-brand-hints/pages/quick-test.html create mode 100644 injected/integration-test/windows-brand-hints.spec.js create mode 100644 injected/src/features/windows-brand-hints.js diff --git a/injected/integration-test/test-pages/windows-brand-hints/config/brand-override.json b/injected/integration-test/test-pages/windows-brand-hints/config/brand-override.json new file mode 100644 index 0000000000..c344a16bc7 --- /dev/null +++ b/injected/integration-test/test-pages/windows-brand-hints/config/brand-override.json @@ -0,0 +1,23 @@ +{ + "readme": "This config tests that windowsBrandHints can override navigator.userAgentData.brands", + "version": 1, + "unprotectedTemporary": [], + "features": { + "windowsBrandHints": { + "state": "enabled", + "exceptions": [], + "settings": { + "brands": [ + { + "brand": "Chromium", + "version": "140" + }, + { + "brand": "DuckDuckGo", + "version": "140" + } + ] + } + } + } +} diff --git a/injected/integration-test/test-pages/windows-brand-hints/index.html b/injected/integration-test/test-pages/windows-brand-hints/index.html new file mode 100644 index 0000000000..332f55e546 --- /dev/null +++ b/injected/integration-test/test-pages/windows-brand-hints/index.html @@ -0,0 +1,15 @@ + + + + + + Windows Brand Hints + + + +

[Home]

+ + + diff --git a/injected/integration-test/test-pages/windows-brand-hints/pages/brand-override.html b/injected/integration-test/test-pages/windows-brand-hints/pages/brand-override.html new file mode 100644 index 0000000000..683964cb26 --- /dev/null +++ b/injected/integration-test/test-pages/windows-brand-hints/pages/brand-override.html @@ -0,0 +1,139 @@ + + + + + + Windows Brand Hints - Brand Override + + + + +

[Windows Brand Hints]

+ +

This page verifies that navigator.userAgentData.brands can be overridden by the windowsBrandHints feature

+ + + + diff --git a/injected/integration-test/test-pages/windows-brand-hints/pages/quick-test.html b/injected/integration-test/test-pages/windows-brand-hints/pages/quick-test.html new file mode 100644 index 0000000000..6e82664a86 --- /dev/null +++ b/injected/integration-test/test-pages/windows-brand-hints/pages/quick-test.html @@ -0,0 +1,111 @@ + + + + + + Quick Test - Windows Brand Hints + + + +

Windows Brand Hints - Quick Test

+
+ + + + diff --git a/injected/integration-test/windows-brand-hints.spec.js b/injected/integration-test/windows-brand-hints.spec.js new file mode 100644 index 0000000000..8a0ef5b685 --- /dev/null +++ b/injected/integration-test/windows-brand-hints.spec.js @@ -0,0 +1,19 @@ +import { test, expect } from '@playwright/test'; +import { ResultsCollector } from './page-objects/results-collector.js'; + +test('Windows Brand Hints override', async ({ page }, testInfo) => { + const collector = ResultsCollector.create(page, testInfo.project.use); + await collector.load( + '/windows-brand-hints/pages/brand-override.html', + './integration-test/test-pages/windows-brand-hints/config/brand-override.json', + ); + const results = await collector.results(); + + for (const key in results) { + for (const result of results[key]) { + await test.step(`${key}: ${result.name}`, () => { + expect(result.result).toEqual(result.expected); + }); + } + } +}); diff --git a/injected/playwright.config.js b/injected/playwright.config.js index 5b059b7576..272349d03c 100644 --- a/injected/playwright.config.js +++ b/injected/playwright.config.js @@ -10,6 +10,7 @@ export default defineConfig({ 'integration-test/duckplayer-remote-config.spec.js', 'integration-test/harmful-apis.spec.js', 'integration-test/windows-permissions.spec.js', + 'integration-test/windows-brand-hints.spec.js', 'integration-test/broker-protection-tests/**/*.spec.js', 'integration-test/breakage-reporting.spec.js', 'integration-test/duck-ai-data-clearing.spec.js', diff --git a/injected/src/features.js b/injected/src/features.js index 929612d768..e8e1c0b133 100644 --- a/injected/src/features.js +++ b/injected/src/features.js @@ -26,6 +26,7 @@ const otherFeatures = /** @type {const} */ ([ 'harmfulApis', 'webCompat', 'windowsPermissionUsage', + 'windowsBrandHints', 'brokerProtection', 'performanceMetrics', 'breakageReporting', @@ -68,6 +69,7 @@ export const platformSupport = { ...baseFeatures, 'webTelemetry', 'windowsPermissionUsage', + 'windowsBrandHints', 'duckPlayer', 'brokerProtection', 'breakageReporting', diff --git a/injected/src/features/windows-brand-hints.js b/injected/src/features/windows-brand-hints.js new file mode 100644 index 0000000000..9e31f3c63d --- /dev/null +++ b/injected/src/features/windows-brand-hints.js @@ -0,0 +1,62 @@ +import ContentFeature from '../content-feature'; + +export default class WindowsBrandHints extends ContentFeature { + init() { + const brandsConfig = this.getFeatureSetting('brands'); + + if (!brandsConfig || brandsConfig.length === 0) { + return; + } + + // Override navigator.userAgentData.brands to match what the browser sends in Sec-CH-UA header + this.shimUserAgentDataBrands(brandsConfig); + } + + /** + * Override navigator.userAgentData.brands to match the Sec-CH-UA header + * The header is already being modified at the browser level, this ensures JS sees consistent values + * @param {Array<{brand: string, version: string}>} brandsConfig - Array of brand objects from config + */ + shimUserAgentDataBrands(brandsConfig) { + try { + // Check if userAgentData exists and has brands property + if (!navigator.userAgentData || !navigator.userAgentData.brands) { + return; + } + + const originalBrands = navigator.userAgentData.brands; + + // Keep GREASE value from original brands (e.g., "Not?A_Brand") + const greaseValue = originalBrands.find(b => b.brand.includes('Not') && b.brand.includes('Brand')); + + // Build new brands array: our configured brands + GREASE + const newBrands = [...brandsConfig]; + if (greaseValue) { + newBrands.push(greaseValue); + } + + // The brands property is on NavigatorUAData.prototype, not on the instance + const proto = Object.getPrototypeOf(navigator.userAgentData); + + this.wrapProperty(proto, 'brands', { + get: () => newBrands + }); + + // Also override getHighEntropyValues on the prototype + if (proto.getHighEntropyValues) { + this.wrapMethod(proto, 'getHighEntropyValues', (originalFn, ...args) => { + return originalFn.apply(navigator.userAgentData, args).then(result => { + // Override brands in high entropy values to match + if (args[0] && args[0].includes('brands')) { + result.brands = newBrands; + } + return result; + }); + }); + } + + } catch (error) { + // Silently fail if shimming doesn't work - the browser will use its default behavior + } + } +} \ No newline at end of file From b1a04583887f2d22a8d2b5470fca2b754fa9dc1f Mon Sep 17 00:00:00 2001 From: laghee <20972610+laghee@users.noreply.github.com> Date: Tue, 14 Oct 2025 21:21:29 +0200 Subject: [PATCH 2/9] Clean up extraneous quick test and comments --- .../pages/brand-override.html | 9 -- .../windows-brand-hints/pages/quick-test.html | 111 ------------------ injected/src/features/windows-brand-hints.js | 14 +-- 3 files changed, 4 insertions(+), 130 deletions(-) delete mode 100644 injected/integration-test/test-pages/windows-brand-hints/pages/quick-test.html diff --git a/injected/integration-test/test-pages/windows-brand-hints/pages/brand-override.html b/injected/integration-test/test-pages/windows-brand-hints/pages/brand-override.html index 683964cb26..e37a9ac3fa 100644 --- a/injected/integration-test/test-pages/windows-brand-hints/pages/brand-override.html +++ b/injected/integration-test/test-pages/windows-brand-hints/pages/brand-override.html @@ -16,7 +16,6 @@ test('Brand override', async () => { const results = []; - // Check if userAgentData exists results.push({ name: 'navigator.userAgentData exists', result: typeof navigator.userAgentData !== 'undefined', @@ -24,7 +23,6 @@ }); if (navigator.userAgentData) { - // Check if brands property exists results.push({ name: 'navigator.userAgentData.brands exists', result: typeof navigator.userAgentData.brands !== 'undefined', @@ -35,7 +33,6 @@ const brands = navigator.userAgentData.brands; const brandNames = new Set(brands.map(b => b.brand)); - // Must contain our two configured brands results.push({ name: 'contains Chromium brand', result: brandNames.has('Chromium'), @@ -48,11 +45,9 @@ expected: true }); - // Check if GREASE is present (it should be preserved if it existed originally) const hasGrease = brands.some(b => b.brand.includes('Not') && b.brand.includes('Brand')); // We expect 2 configured brands + GREASE if it was in the original - // (In some test environments GREASE may not be present) const expectedLength = hasGrease ? 3 : 2; results.push({ name: 'brands array has correct length', @@ -60,7 +55,6 @@ expected: expectedLength }); - // Verify no unexpected brands (only Chromium, DuckDuckGo, and optionally GREASE) const unexpectedBrands = brands.filter(b => !['Chromium', 'DuckDuckGo'].includes(b.brand) && !(b.brand.includes('Not') && b.brand.includes('Brand')) @@ -72,7 +66,6 @@ }); } - // Test getHighEntropyValues if available if (navigator.userAgentData.getHighEntropyValues) { try { const highEntropyValues = await navigator.userAgentData.getHighEntropyValues(['brands']); @@ -99,7 +92,6 @@ expected: true }); - // Check if GREASE is present const heHasGrease = heBrands.some(b => b.brand.includes('Not') && b.brand.includes('Brand')); const heExpectedLength = heHasGrease ? 3 : 2; @@ -109,7 +101,6 @@ expected: heExpectedLength }); - // Verify no unexpected brands const heUnexpectedBrands = heBrands.filter(b => !['Chromium', 'DuckDuckGo'].includes(b.brand) && !(b.brand.includes('Not') && b.brand.includes('Brand')) diff --git a/injected/integration-test/test-pages/windows-brand-hints/pages/quick-test.html b/injected/integration-test/test-pages/windows-brand-hints/pages/quick-test.html deleted file mode 100644 index 6e82664a86..0000000000 --- a/injected/integration-test/test-pages/windows-brand-hints/pages/quick-test.html +++ /dev/null @@ -1,111 +0,0 @@ - - - - - - Quick Test - Windows Brand Hints - - - -

Windows Brand Hints - Quick Test

-
- - - - diff --git a/injected/src/features/windows-brand-hints.js b/injected/src/features/windows-brand-hints.js index 9e31f3c63d..faa4ee849b 100644 --- a/injected/src/features/windows-brand-hints.js +++ b/injected/src/features/windows-brand-hints.js @@ -8,45 +8,39 @@ export default class WindowsBrandHints extends ContentFeature { return; } - // Override navigator.userAgentData.brands to match what the browser sends in Sec-CH-UA header this.shimUserAgentDataBrands(brandsConfig); } /** - * Override navigator.userAgentData.brands to match the Sec-CH-UA header - * The header is already being modified at the browser level, this ensures JS sees consistent values + * Override navigator.userAgentData.brands to match the Sec-CH-UA header we're sending. * @param {Array<{brand: string, version: string}>} brandsConfig - Array of brand objects from config */ shimUserAgentDataBrands(brandsConfig) { try { - // Check if userAgentData exists and has brands property if (!navigator.userAgentData || !navigator.userAgentData.brands) { return; } const originalBrands = navigator.userAgentData.brands; - // Keep GREASE value from original brands (e.g., "Not?A_Brand") + // Keep GREASE value from original brands const greaseValue = originalBrands.find(b => b.brand.includes('Not') && b.brand.includes('Brand')); - // Build new brands array: our configured brands + GREASE const newBrands = [...brandsConfig]; if (greaseValue) { newBrands.push(greaseValue); } - // The brands property is on NavigatorUAData.prototype, not on the instance + // The brands property is on NavigatorUAData.prototype, not the instance const proto = Object.getPrototypeOf(navigator.userAgentData); this.wrapProperty(proto, 'brands', { get: () => newBrands }); - // Also override getHighEntropyValues on the prototype if (proto.getHighEntropyValues) { this.wrapMethod(proto, 'getHighEntropyValues', (originalFn, ...args) => { return originalFn.apply(navigator.userAgentData, args).then(result => { - // Override brands in high entropy values to match if (args[0] && args[0].includes('brands')) { result.brands = newBrands; } @@ -56,7 +50,7 @@ export default class WindowsBrandHints extends ContentFeature { } } catch (error) { - // Silently fail if shimming doesn't work - the browser will use its default behavior + // TODO: log error somewhere? } } } \ No newline at end of file From 9592f42d555d58d52e361559d1b1e4259d7a6492 Mon Sep 17 00:00:00 2001 From: laghee <20972610+laghee@users.noreply.github.com> Date: Tue, 21 Oct 2025 15:57:13 +0200 Subject: [PATCH 3/9] Mirror native approach for headers in js api override --- injected/integration-test/pages.spec.js | 1 + .../pages/brand-override.html | 130 ------------------ .../config/brand-override.json | 2 +- .../index.html | 4 +- .../pages/brand-override.html | 103 ++++++++++++++ ...ints.spec.js => windows-ch-brands.spec.js} | 6 +- injected/playwright.config.js | 2 +- injected/src/features.js | 4 +- injected/src/features/windows-brand-hints.js | 56 -------- injected/src/features/windows-ch-brands.js | 102 ++++++++++++++ 10 files changed, 216 insertions(+), 194 deletions(-) delete mode 100644 injected/integration-test/test-pages/windows-brand-hints/pages/brand-override.html rename injected/integration-test/test-pages/{windows-brand-hints => windows-ch-brands}/config/brand-override.json (95%) rename injected/integration-test/test-pages/{windows-brand-hints => windows-ch-brands}/index.html (85%) create mode 100644 injected/integration-test/test-pages/windows-ch-brands/pages/brand-override.html rename injected/integration-test/{windows-brand-hints.spec.js => windows-ch-brands.spec.js} (70%) delete mode 100644 injected/src/features/windows-brand-hints.js create mode 100644 injected/src/features/windows-ch-brands.js diff --git a/injected/integration-test/pages.spec.js b/injected/integration-test/pages.spec.js index d6b02bff2c..9d41fe26f2 100644 --- a/injected/integration-test/pages.spec.js +++ b/injected/integration-test/pages.spec.js @@ -177,4 +177,5 @@ test.describe('Test integration pages', () => { { version: 99 }, ); }); + }); diff --git a/injected/integration-test/test-pages/windows-brand-hints/pages/brand-override.html b/injected/integration-test/test-pages/windows-brand-hints/pages/brand-override.html deleted file mode 100644 index e37a9ac3fa..0000000000 --- a/injected/integration-test/test-pages/windows-brand-hints/pages/brand-override.html +++ /dev/null @@ -1,130 +0,0 @@ - - - - - - Windows Brand Hints - Brand Override - - - - -

[Windows Brand Hints]

- -

This page verifies that navigator.userAgentData.brands can be overridden by the windowsBrandHints feature

- - - - diff --git a/injected/integration-test/test-pages/windows-brand-hints/config/brand-override.json b/injected/integration-test/test-pages/windows-ch-brands/config/brand-override.json similarity index 95% rename from injected/integration-test/test-pages/windows-brand-hints/config/brand-override.json rename to injected/integration-test/test-pages/windows-ch-brands/config/brand-override.json index c344a16bc7..ceb92a2785 100644 --- a/injected/integration-test/test-pages/windows-brand-hints/config/brand-override.json +++ b/injected/integration-test/test-pages/windows-ch-brands/config/brand-override.json @@ -3,7 +3,7 @@ "version": 1, "unprotectedTemporary": [], "features": { - "windowsBrandHints": { + "windowsChBrands": { "state": "enabled", "exceptions": [], "settings": { diff --git a/injected/integration-test/test-pages/windows-brand-hints/index.html b/injected/integration-test/test-pages/windows-ch-brands/index.html similarity index 85% rename from injected/integration-test/test-pages/windows-brand-hints/index.html rename to injected/integration-test/test-pages/windows-ch-brands/index.html index 332f55e546..baa3d2831d 100644 --- a/injected/integration-test/test-pages/windows-brand-hints/index.html +++ b/injected/integration-test/test-pages/windows-ch-brands/index.html @@ -3,11 +3,13 @@ - Windows Brand Hints + Windows CH Brands

[Home]

+ +

Windows CH Brands

diff --git a/injected/integration-test/test-pages/windows-ch-brands/pages/brand-override.html b/injected/integration-test/test-pages/windows-ch-brands/pages/brand-override.html new file mode 100644 index 0000000000..5be97c7cec --- /dev/null +++ b/injected/integration-test/test-pages/windows-ch-brands/pages/brand-override.html @@ -0,0 +1,103 @@ + + + + + + Windows CH Brands - Brand Override + + + + +

[Windows CH Brands]

+ +

This page verifies that navigator.userAgentData.brands uses the configured brands from the windowsChBrands feature settings

+ + + + diff --git a/injected/integration-test/windows-brand-hints.spec.js b/injected/integration-test/windows-ch-brands.spec.js similarity index 70% rename from injected/integration-test/windows-brand-hints.spec.js rename to injected/integration-test/windows-ch-brands.spec.js index 8a0ef5b685..825afc2bb6 100644 --- a/injected/integration-test/windows-brand-hints.spec.js +++ b/injected/integration-test/windows-ch-brands.spec.js @@ -1,11 +1,11 @@ import { test, expect } from '@playwright/test'; import { ResultsCollector } from './page-objects/results-collector.js'; -test('Windows Brand Hints override', async ({ page }, testInfo) => { +test('Windows CH Brands override', async ({ page }, testInfo) => { const collector = ResultsCollector.create(page, testInfo.project.use); await collector.load( - '/windows-brand-hints/pages/brand-override.html', - './integration-test/test-pages/windows-brand-hints/config/brand-override.json', + '/windows-ch-brands/pages/brand-override.html', + './integration-test/test-pages/windows-ch-brands/config/brand-override.json', ); const results = await collector.results(); diff --git a/injected/playwright.config.js b/injected/playwright.config.js index 272349d03c..f48ccede34 100644 --- a/injected/playwright.config.js +++ b/injected/playwright.config.js @@ -10,7 +10,7 @@ export default defineConfig({ 'integration-test/duckplayer-remote-config.spec.js', 'integration-test/harmful-apis.spec.js', 'integration-test/windows-permissions.spec.js', - 'integration-test/windows-brand-hints.spec.js', + 'integration-test/windows-ch-brands.spec.js', 'integration-test/broker-protection-tests/**/*.spec.js', 'integration-test/breakage-reporting.spec.js', 'integration-test/duck-ai-data-clearing.spec.js', diff --git a/injected/src/features.js b/injected/src/features.js index e8e1c0b133..e4bb48b3de 100644 --- a/injected/src/features.js +++ b/injected/src/features.js @@ -26,7 +26,7 @@ const otherFeatures = /** @type {const} */ ([ 'harmfulApis', 'webCompat', 'windowsPermissionUsage', - 'windowsBrandHints', + 'windowsChBrands', 'brokerProtection', 'performanceMetrics', 'breakageReporting', @@ -69,7 +69,7 @@ export const platformSupport = { ...baseFeatures, 'webTelemetry', 'windowsPermissionUsage', - 'windowsBrandHints', + 'windowsChBrands', 'duckPlayer', 'brokerProtection', 'breakageReporting', diff --git a/injected/src/features/windows-brand-hints.js b/injected/src/features/windows-brand-hints.js deleted file mode 100644 index faa4ee849b..0000000000 --- a/injected/src/features/windows-brand-hints.js +++ /dev/null @@ -1,56 +0,0 @@ -import ContentFeature from '../content-feature'; - -export default class WindowsBrandHints extends ContentFeature { - init() { - const brandsConfig = this.getFeatureSetting('brands'); - - if (!brandsConfig || brandsConfig.length === 0) { - return; - } - - this.shimUserAgentDataBrands(brandsConfig); - } - - /** - * Override navigator.userAgentData.brands to match the Sec-CH-UA header we're sending. - * @param {Array<{brand: string, version: string}>} brandsConfig - Array of brand objects from config - */ - shimUserAgentDataBrands(brandsConfig) { - try { - if (!navigator.userAgentData || !navigator.userAgentData.brands) { - return; - } - - const originalBrands = navigator.userAgentData.brands; - - // Keep GREASE value from original brands - const greaseValue = originalBrands.find(b => b.brand.includes('Not') && b.brand.includes('Brand')); - - const newBrands = [...brandsConfig]; - if (greaseValue) { - newBrands.push(greaseValue); - } - - // The brands property is on NavigatorUAData.prototype, not the instance - const proto = Object.getPrototypeOf(navigator.userAgentData); - - this.wrapProperty(proto, 'brands', { - get: () => newBrands - }); - - if (proto.getHighEntropyValues) { - this.wrapMethod(proto, 'getHighEntropyValues', (originalFn, ...args) => { - return originalFn.apply(navigator.userAgentData, args).then(result => { - if (args[0] && args[0].includes('brands')) { - result.brands = newBrands; - } - return result; - }); - }); - } - - } catch (error) { - // TODO: log error somewhere? - } - } -} \ No newline at end of file diff --git a/injected/src/features/windows-ch-brands.js b/injected/src/features/windows-ch-brands.js new file mode 100644 index 0000000000..c5f597c774 --- /dev/null +++ b/injected/src/features/windows-ch-brands.js @@ -0,0 +1,102 @@ +import ContentFeature from '../content-feature'; + +export default class WindowsChBrands extends ContentFeature { + constructor(featureName, importConfig, args) { + super(featureName, importConfig, args); + + this.cachedBrands = null; + this.originalBrands = null; + } + + init() { + this.updateConfig(); + this.shimUserAgentDataBrands(); + } + + updateConfig() { + // Clear cache when privacy config updated + this.cachedBrands = null; + this.log.info('Privacy remote config updated'); + } + + /** + * Override navigator.userAgentData.brands to match the Sec-CH-UA header + */ + shimUserAgentDataBrands() { + try { + // @ts-expect-error - userAgentData not yet standard + if (!navigator.userAgentData || !navigator.userAgentData.brands) { + return; + } + + if (!this.originalBrands) { + // @ts-expect-error - userAgentData not yet standard + this.originalBrands = [...navigator.userAgentData.brands]; + } + + if (this.cachedBrands) { + this.applyBrandsOverride(this.cachedBrands); + return; + } + + const mutatedBrands = this.applyBrandMutations(); + + if (mutatedBrands) { + this.cachedBrands = mutatedBrands; + this.applyBrandsOverride(mutatedBrands); + } + } catch (error) { + this.log.error('Error in shimUserAgentDataBrands:', error); + } + } + + /** + * Apply brand mutations using the configured brands from feature settings + * @returns {Array<{brand: string, version: string}>|null} - Configured brands or null if no changes + */ + applyBrandMutations() { + const configuredBrands = this.getFeatureSetting('brands'); + + if (!configuredBrands || configuredBrands.length === 0) { + this.log.info('No CH brands configured, skipping mutations'); + return null; + } + + this.log.info('Applying configured brands:', configuredBrands); + return configuredBrands; + } + + /** + * Apply the brand override to navigator.userAgentData.brands + * @param {Array<{brand: string, version: string}>} newBrands - Brands to apply + */ + applyBrandsOverride(newBrands) { + // @ts-expect-error - userAgentData not yet standard + const proto = Object.getPrototypeOf(navigator.userAgentData); + + this.wrapProperty(proto, 'brands', { + get: () => newBrands, + }); + + // Also override getHighEntropyValues on the prototype + if (proto.getHighEntropyValues) { + this.wrapMethod(proto, 'getHighEntropyValues', (originalFn, ...args) => { + // @ts-expect-error - userAgentData not yet standard + return originalFn.apply(navigator.userAgentData, args).then((result) => { + if (args[0] && args[0].includes('brands')) { + result.brands = newBrands; + } + return result; + }); + }); + } + } + + /** + * Handle configuration updates (called when remote config changes) + */ + update() { + this.updateConfig(); + this.shimUserAgentDataBrands(); + } +} \ No newline at end of file From dbdebd95afd9546a60be62810047105f8a7844a9 Mon Sep 17 00:00:00 2001 From: laghee <20972610+laghee@users.noreply.github.com> Date: Tue, 21 Oct 2025 16:02:22 +0200 Subject: [PATCH 4/9] Fix errant space --- injected/integration-test/pages.spec.js | 1 - 1 file changed, 1 deletion(-) diff --git a/injected/integration-test/pages.spec.js b/injected/integration-test/pages.spec.js index 9d41fe26f2..d6b02bff2c 100644 --- a/injected/integration-test/pages.spec.js +++ b/injected/integration-test/pages.spec.js @@ -177,5 +177,4 @@ test.describe('Test integration pages', () => { { version: 99 }, ); }); - }); From d1427357fd738ae7a5e663ec2e15b7b27848f215 Mon Sep 17 00:00:00 2001 From: laghee <20972610+laghee@users.noreply.github.com> Date: Sun, 2 Nov 2025 18:58:48 +0100 Subject: [PATCH 5/9] Return early if brands config is missing or malformed --- .../config/brands-missing.json | 13 ++++ .../test-pages/windows-ch-brands/index.html | 1 + .../pages/brands-missing.html | 71 +++++++++++++++++++ .../windows-ch-brands.spec.js | 17 +++++ injected/src/features/windows-ch-brands.js | 7 ++ 5 files changed, 109 insertions(+) create mode 100644 injected/integration-test/test-pages/windows-ch-brands/config/brands-missing.json create mode 100644 injected/integration-test/test-pages/windows-ch-brands/pages/brands-missing.html diff --git a/injected/integration-test/test-pages/windows-ch-brands/config/brands-missing.json b/injected/integration-test/test-pages/windows-ch-brands/config/brands-missing.json new file mode 100644 index 0000000000..87271bb3af --- /dev/null +++ b/injected/integration-test/test-pages/windows-ch-brands/config/brands-missing.json @@ -0,0 +1,13 @@ +{ + "readme": "This config tests that windowsChBrands feature returns early when brands setting is missing (feature is enabled but settings.brands is undefined)", + "version": 1, + "unprotectedTemporary": [], + "features": { + "windowsChBrands": { + "state": "enabled", + "exceptions": [], + "settings": { + } + } + } +} diff --git a/injected/integration-test/test-pages/windows-ch-brands/index.html b/injected/integration-test/test-pages/windows-ch-brands/index.html index baa3d2831d..5445632208 100644 --- a/injected/integration-test/test-pages/windows-ch-brands/index.html +++ b/injected/integration-test/test-pages/windows-ch-brands/index.html @@ -12,6 +12,7 @@

Windows CH Brands

diff --git a/injected/integration-test/test-pages/windows-ch-brands/pages/brands-missing.html b/injected/integration-test/test-pages/windows-ch-brands/pages/brands-missing.html new file mode 100644 index 0000000000..712a56684c --- /dev/null +++ b/injected/integration-test/test-pages/windows-ch-brands/pages/brands-missing.html @@ -0,0 +1,71 @@ + + + + + + Windows CH Brands - Brands Missing + + + + +

[Windows CH Brands]

+ +

This page verifies that when brands setting is missing/null/empty, the feature returns early and original brands are preserved

+ + + + diff --git a/injected/integration-test/windows-ch-brands.spec.js b/injected/integration-test/windows-ch-brands.spec.js index 825afc2bb6..e2371d05c6 100644 --- a/injected/integration-test/windows-ch-brands.spec.js +++ b/injected/integration-test/windows-ch-brands.spec.js @@ -17,3 +17,20 @@ test('Windows CH Brands override', async ({ page }, testInfo) => { } } }); + +test('Windows CH Brands when brands missing', async ({ page }, testInfo) => { + const collector = ResultsCollector.create(page, testInfo.project.use); + await collector.load( + '/windows-ch-brands/pages/brands-missing.html', + './integration-test/test-pages/windows-ch-brands/config/brands-missing.json', + ); + const results = await collector.results(); + + for (const key in results) { + for (const result of results[key]) { + await test.step(`${key}: ${result.name}`, () => { + expect(result.result).toEqual(result.expected); + }); + } + } +}); diff --git a/injected/src/features/windows-ch-brands.js b/injected/src/features/windows-ch-brands.js index c5f597c774..12e53c5c06 100644 --- a/injected/src/features/windows-ch-brands.js +++ b/injected/src/features/windows-ch-brands.js @@ -9,6 +9,13 @@ export default class WindowsChBrands extends ContentFeature { } init() { + const configuredBrands = this.getFeatureSetting('brands'); + + if (!configuredBrands || configuredBrands.length === 0) { + this.log.info('No client hint brands correctly configured, feature disabled'); + return; + } + this.updateConfig(); this.shimUserAgentDataBrands(); } From cc7a3167cae58f594bfdf90ed31c8caab2541bae Mon Sep 17 00:00:00 2001 From: laghee <20972610+laghee@users.noreply.github.com> Date: Sun, 2 Nov 2025 19:04:44 +0100 Subject: [PATCH 6/9] Rename feature more generally --- .../config/brand-override.json | 4 ++-- .../ua-ch-brands/config/brands-missing.json | 13 +++++++++++++ .../{windows-ch-brands => ua-ch-brands}/index.html | 4 ++-- .../pages/brand-override.html | 6 +++--- .../pages/brands-missing.html | 4 ++-- .../windows-ch-brands/config/brands-missing.json | 13 ------------- ...ndows-ch-brands.spec.js => ua-ch-brands.spec.js} | 12 ++++++------ injected/playwright.config.js | 2 +- injected/src/features.js | 4 ++-- .../{windows-ch-brands.js => ua-ch-brands.js} | 2 +- 10 files changed, 32 insertions(+), 32 deletions(-) rename injected/integration-test/test-pages/{windows-ch-brands => ua-ch-brands}/config/brand-override.json (79%) create mode 100644 injected/integration-test/test-pages/ua-ch-brands/config/brands-missing.json rename injected/integration-test/test-pages/{windows-ch-brands => ua-ch-brands}/index.html (88%) rename injected/integration-test/test-pages/{windows-ch-brands => ua-ch-brands}/pages/brand-override.html (95%) rename injected/integration-test/test-pages/{windows-ch-brands => ua-ch-brands}/pages/brands-missing.html (96%) delete mode 100644 injected/integration-test/test-pages/windows-ch-brands/config/brands-missing.json rename injected/integration-test/{windows-ch-brands.spec.js => ua-ch-brands.spec.js} (68%) rename injected/src/features/{windows-ch-brands.js => ua-ch-brands.js} (98%) diff --git a/injected/integration-test/test-pages/windows-ch-brands/config/brand-override.json b/injected/integration-test/test-pages/ua-ch-brands/config/brand-override.json similarity index 79% rename from injected/integration-test/test-pages/windows-ch-brands/config/brand-override.json rename to injected/integration-test/test-pages/ua-ch-brands/config/brand-override.json index ceb92a2785..f7322f6d2f 100644 --- a/injected/integration-test/test-pages/windows-ch-brands/config/brand-override.json +++ b/injected/integration-test/test-pages/ua-ch-brands/config/brand-override.json @@ -1,9 +1,9 @@ { - "readme": "This config tests that windowsBrandHints can override navigator.userAgentData.brands", + "readme": "This config tests that uaChBrands can override navigator.userAgentData.brands", "version": 1, "unprotectedTemporary": [], "features": { - "windowsChBrands": { + "uaChBrands": { "state": "enabled", "exceptions": [], "settings": { diff --git a/injected/integration-test/test-pages/ua-ch-brands/config/brands-missing.json b/injected/integration-test/test-pages/ua-ch-brands/config/brands-missing.json new file mode 100644 index 0000000000..f62047bc68 --- /dev/null +++ b/injected/integration-test/test-pages/ua-ch-brands/config/brands-missing.json @@ -0,0 +1,13 @@ +{ + "readme": "This config tests that uaChBrands feature returns early when brands setting is missing (feature is enabled but settings.brands is undefined)", + "version": 1, + "unprotectedTemporary": [], + "features": { + "uaChBrands": { + "state": "enabled", + "exceptions": [], + "settings": { + } + } + } +} diff --git a/injected/integration-test/test-pages/windows-ch-brands/index.html b/injected/integration-test/test-pages/ua-ch-brands/index.html similarity index 88% rename from injected/integration-test/test-pages/windows-ch-brands/index.html rename to injected/integration-test/test-pages/ua-ch-brands/index.html index 5445632208..3b1c82c1b2 100644 --- a/injected/integration-test/test-pages/windows-ch-brands/index.html +++ b/injected/integration-test/test-pages/ua-ch-brands/index.html @@ -3,13 +3,13 @@ - Windows CH Brands + UA CH Brands

[Home]

-

Windows CH Brands

+

UA CH Brands