diff --git a/packages/react-aria/src/utils/getMetaValue.ts b/packages/react-aria/src/utils/getMetaValue.ts new file mode 100644 index 00000000000..54115f79ab6 --- /dev/null +++ b/packages/react-aria/src/utils/getMetaValue.ts @@ -0,0 +1,49 @@ +/* + * Copyright 2026 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {getOwnerDocument, getOwnerWindow} from './domHelpers'; + +declare global { + interface Window { + __webpack_nonce__?: string; + } + var __webpack_nonce__: string | undefined; +} + +export function getMetaValue(key: string, doc?: Document): string | undefined { + let ownerWindow = getOwnerWindow(doc); + let ownerDocument = getOwnerDocument(doc); + + if (ownerDocument == null || ownerWindow == null) { + return; + } + + let content: string | undefined = undefined; + let selector = `meta[name="${CSS.escape(key)}"], meta[property="${CSS.escape(key)}"]`; + let meta = ownerDocument.querySelector(selector); + + if (meta && meta instanceof ownerWindow.HTMLMetaElement) { + if (key === 'csp-nonce' && meta.nonce) { + content ??= meta.nonce || undefined; + } + + if (meta.content) { + content ??= meta.content || undefined; + } + } + + if (key === 'csp-nonce') { + content ??= ownerWindow.__webpack_nonce__ || globalThis.__webpack_nonce__ || undefined; + } + + return content; +} diff --git a/packages/react-aria/src/utils/getNonce.ts b/packages/react-aria/src/utils/getNonce.ts index 54ff2a0d08e..b03216cfe67 100644 --- a/packages/react-aria/src/utils/getNonce.ts +++ b/packages/react-aria/src/utils/getNonce.ts @@ -10,17 +10,8 @@ * governing permissions and limitations under the License. */ -import {getOwnerWindow} from './domHelpers'; - -type NonceWindow = Window & - typeof globalThis & { - __webpack_nonce__?: string; - }; - -function getWebpackNonce(doc?: Document): string | undefined { - let ownerWindow = doc?.defaultView as NonceWindow | null | undefined; - return ownerWindow?.__webpack_nonce__ || globalThis['__webpack_nonce__'] || undefined; -} +import {getMetaValue} from './getMetaValue'; +import {getOwnerDocument} from './domHelpers'; let nonceCache = new WeakMap(); @@ -35,25 +26,13 @@ export function resetNonceCache(): void { * Security Policy. */ export function getNonce(doc?: Document): string | undefined { - let d = doc ?? (typeof document !== 'undefined' ? document : undefined); - if (!d) { - return getWebpackNonce(d); - } - - if (nonceCache.has(d)) { - return nonceCache.get(d); - } + let ownerDocument = getOwnerDocument(doc); - let meta = d.querySelector('meta[property="csp-nonce"]'); - let nonce = - (meta && - meta instanceof getOwnerWindow(meta).HTMLMetaElement && - (meta.nonce || meta.content)) || - getWebpackNonce(d) || - undefined; + let nonce = nonceCache.get(ownerDocument); + nonce ??= getMetaValue('csp-nonce', ownerDocument); if (nonce !== undefined) { - nonceCache.set(d, nonce); + nonceCache.set(ownerDocument, nonce); } return nonce; } diff --git a/packages/react-aria/test/utils/getMetaValue.test.js b/packages/react-aria/test/utils/getMetaValue.test.js new file mode 100644 index 00000000000..5bd292215d2 --- /dev/null +++ b/packages/react-aria/test/utils/getMetaValue.test.js @@ -0,0 +1,57 @@ +/* + * Copyright 2026 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {getMetaValue} from '../../src/utils/getMetaValue'; + +describe('getMetaValue', () => { + afterEach(() => { + document.querySelectorAll('meta').forEach(el => el.remove()); + delete globalThis['__webpack_nonce__']; + }); + + it('returns undefined when no matching meta tag exists', () => { + expect(getMetaValue('theme-color')).toBeUndefined(); + }); + + it('reads a value from the name attribute for an arbitrary key', () => { + let meta = document.createElement('meta'); + meta.setAttribute('name', 'theme-color'); + meta.setAttribute('content', '#ff0000'); + document.head.appendChild(meta); + + expect(getMetaValue('theme-color')).toBe('#ff0000'); + }); + + it('reads a value from the property attribute for an arbitrary key', () => { + let meta = document.createElement('meta'); + meta.setAttribute('property', 'og:title'); + meta.setAttribute('content', 'Hello'); + document.head.appendChild(meta); + + expect(getMetaValue('og:title')).toBe('Hello'); + }); + + it('does not fall back to __webpack_nonce__ for non-nonce keys', () => { + globalThis['__webpack_nonce__'] = 'webpack-nonce'; + + expect(getMetaValue('theme-color')).toBeUndefined(); + }); + + it('escapes special characters in the key when building the selector', () => { + let meta = document.createElement('meta'); + meta.setAttribute('name', 'my:weird.key'); + meta.setAttribute('content', 'escaped'); + document.head.appendChild(meta); + + expect(getMetaValue('my:weird.key')).toBe('escaped'); + }); +});