Commit 97d6d0941c for woocommerce
commit 97d6d0941c5ef502bde2277d8c978d97595952d1
Author: Christopher Allford <6451942+ObliviousHarmony@users.noreply.github.com>
Date: Mon Nov 3 17:53:28 2025 -0800
Add `@woocommerce/sanitize` Trusted Type Policy Support (#61728)
Rather than exporting a no-op policy, this change makes it so
that we're providing a policy that uses the sanitization
function that the package exports.
diff --git a/packages/js/sanitize/README.md b/packages/js/sanitize/README.md
index 578c07f896..5662513f3f 100644
--- a/packages/js/sanitize/README.md
+++ b/packages/js/sanitize/README.md
@@ -9,10 +9,6 @@ WooCommerce HTML sanitization utilities using DOMPurify with trusted types suppo
- **Configurable**: Supports custom allowed tags and attributes
- **TypeScript Support**: Full TypeScript definitions included
-## Installation
-
-This package is part of the WooCommerce monorepo and is automatically available to other packages.
-
## Usage
### Basic HTML Sanitization
@@ -54,50 +50,32 @@ const customSanitized = sanitizeHTML(
);
```
-## API Reference
-
-### Functions
-
-#### `sanitizeHTML(html: string, config?: SanitizeConfig): string`
-
-Sanitizes HTML content using default allowed tags and attributes.
-
-#### `initializeTrustedTypesPolicy(): void`
-
-Manually initialize the trusted types policy (usually called automatically).
-
-### Constants
-
-#### `DEFAULT_ALLOWED_TAGS`
+### Selecting the return type
-Default array of allowed HTML tags for basic sanitization.
+By default, `sanitizeHTML` returns a string. You can opt into other DOMPurify return modes with the `returnType` option:
-#### `DEFAULT_ALLOWED_ATTR`
-
-Default array of allowed HTML attributes for basic sanitization.
-
-### Types
+```ts
+import { sanitizeHTML } from '@woocommerce/sanitize';
-#### `SanitizeConfig`
+// Return an HTMLBodyElement
+const bodyEl = sanitizeHTML('<p>Hi</p>', { returnType: 'HTMLBodyElement' });
-```typescript
-interface SanitizeConfig {
- tags?: readonly string[];
- attr?: readonly string[];
-}
+// Return a DocumentFragment
+const fragment = sanitizeHTML('<p>Hi</p>', { returnType: 'DocumentFragment' });
```
## Trusted Types
This package automatically configures a trusted types policy named `woocommerce-sanitize` to avoid conflicts with DOMPurify's default policy. The policy is initialized when the module is loaded.
-## Security
-
-- All HTML content is sanitized using DOMPurify
-- XSS attacks are prevented by removing dangerous content
-- Trusted types are used when available for additional security
-- Configurable allowlists for tags and attributes
+```ts
+import { getTrustedTypesPolicy } from '@woocommerce/sanitize';
-## Contributing
+const policy = getTrustedTypesPolicy();
+if (policy) {
+ const trustedHTML = policy.createHTML('<p>Content</p>');
+ element.innerHTML = trustedHTML; // Safe in Trusted Types environments
+}
+```
-This package follows the same contribution guidelines as the main WooCommerce repository.
+The policy automatically sanitizes HTML using the same rules as `sanitizeHTML()`.
diff --git a/packages/js/sanitize/package.json b/packages/js/sanitize/package.json
index 35bf34d5b2..a07f36410b 100644
--- a/packages/js/sanitize/package.json
+++ b/packages/js/sanitize/package.json
@@ -30,6 +30,7 @@
"@babel/core": "7.25.7",
"@types/dompurify": "^2.0.4",
"@types/jest": "29.5.x",
+ "@types/trusted-types": "^2.0.7",
"@woocommerce/eslint-plugin": "workspace:*",
"@woocommerce/internal-js-tests": "workspace:*",
"@wordpress/scripts": "^27.0.0",
diff --git a/packages/js/sanitize/src/index.ts b/packages/js/sanitize/src/index.ts
index 04c229a65c..b62b4690c8 100644
--- a/packages/js/sanitize/src/index.ts
+++ b/packages/js/sanitize/src/index.ts
@@ -1,74 +1,12 @@
-/**
- * External dependencies
- */
-import DOMPurify from 'dompurify';
-
-/**
- * Internal dependencies
- */
-import { getTrustedTypesPolicy } from './trusted-types-policy';
-
-/**
- * Default allowed HTML tags for basic sanitization.
- */
-export const DEFAULT_ALLOWED_TAGS = [
- 'a',
- 'b',
- 'em',
- 'i',
- 'strong',
- 'p',
- 'br',
- 'abbr',
-] as const;
-
-/**
- * Default allowed HTML attributes for basic sanitization.
- */
-export const DEFAULT_ALLOWED_ATTR = [
- 'target',
- 'href',
- 'rel',
- 'name',
- 'download',
- 'title',
-] as const;
-
-/**
- * Configuration options for HTML sanitization.
- */
-export interface SanitizeConfig {
- /** Allowed HTML tags */
- tags?: readonly string[];
- /** Allowed HTML attributes */
- attr?: readonly string[];
-}
-
-/**
- * Sanitizes HTML content using DOMPurify with default allowed tags and attributes.
- *
- * @param html - The HTML content to sanitize.
- * @param config - Optional configuration for allowed tags and attributes.
- * @return Sanitized HTML content.
- */
-export function sanitizeHTML( html: string, config?: SanitizeConfig ): string {
- const allowedTags = config?.tags || DEFAULT_ALLOWED_TAGS;
- const allowedAttr = config?.attr || DEFAULT_ALLOWED_ATTR;
-
- const policy = getTrustedTypesPolicy();
-
- const purifyConfig: DOMPurify.Config = {
- ALLOWED_TAGS: [ ...allowedTags ],
- ALLOWED_ATTR: [ ...allowedAttr ],
- ...( policy && { TRUSTED_TYPES_POLICY: policy } ),
- };
-
- const result = DOMPurify.sanitize( html, purifyConfig );
-
- return typeof result === 'string' ? result : String( result );
-}
-
-/**
- * The name of the trusted types policy.
- */
-export { TRUSTED_POLICY_NAME } from './trusted-types-policy';
+export {
+ WooCommerceSanitizePolicyType,
+ getTrustedTypesPolicy,
+} from './trusted-types-policy';
+export {
+ DEFAULT_ALLOWED_TAGS,
+ DEFAULT_ALLOWED_ATTR,
+ SanitizeReturnKind,
+ SanitizeReturnType,
+ SanitizeConfig,
+ sanitizeHTML,
+} from './sanitize';
diff --git a/packages/js/sanitize/src/noop-trusted-types-policy.ts b/packages/js/sanitize/src/noop-trusted-types-policy.ts
new file mode 100644
index 0000000000..75dac35ffe
--- /dev/null
+++ b/packages/js/sanitize/src/noop-trusted-types-policy.ts
@@ -0,0 +1,46 @@
+/**
+ * External dependencies
+ */
+import type { TrustedTypePolicy } from 'trusted-types';
+
+/**
+ * The type for our no-op trusted types policy.
+ */
+export type WooCommerceSanitizeNoopPolicyType = Pick<
+ TrustedTypePolicy,
+ 'name' | 'createHTML'
+>;
+
+/**
+ * Cached no-op policy instance to avoid duplicate creation.
+ */
+let noopPolicyInstance: WooCommerceSanitizeNoopPolicyType | null | undefined;
+
+export function getNoopTrustedTypesPolicy(): WooCommerceSanitizeNoopPolicyType | null {
+ if ( noopPolicyInstance !== undefined ) {
+ return noopPolicyInstance;
+ }
+
+ if ( typeof window === 'undefined' || ! window.trustedTypes ) {
+ noopPolicyInstance = null;
+ return null;
+ }
+
+ try {
+ noopPolicyInstance = window.trustedTypes.createPolicy(
+ 'woocommerce-sanitize-noop',
+ {
+ createHTML: ( input: string ): string => input,
+ }
+ );
+ } catch ( error ) {
+ noopPolicyInstance = null;
+ // eslint-disable-next-line no-console
+ console.warn(
+ 'Failed to create "woocommerce-sanitize-noop" trusted type policy:',
+ error
+ );
+ }
+
+ return noopPolicyInstance;
+}
diff --git a/packages/js/sanitize/src/sanitize.ts b/packages/js/sanitize/src/sanitize.ts
new file mode 100644
index 0000000000..995f3005dc
--- /dev/null
+++ b/packages/js/sanitize/src/sanitize.ts
@@ -0,0 +1,128 @@
+/**
+ * External dependencies
+ */
+import DOMPurify from 'dompurify';
+
+/**
+ * Internal dependencies
+ */
+import { getNoopTrustedTypesPolicy } from './noop-trusted-types-policy';
+
+/**
+ * Default allowed HTML tags for basic sanitization.
+ */
+export const DEFAULT_ALLOWED_TAGS = [
+ 'a',
+ 'b',
+ 'em',
+ 'i',
+ 'strong',
+ 'p',
+ 'br',
+ 'abbr',
+] as const;
+
+/**
+ * Default allowed HTML attributes for basic sanitization.
+ */
+export const DEFAULT_ALLOWED_ATTR = [
+ 'target',
+ 'href',
+ 'rel',
+ 'name',
+ 'download',
+ 'title',
+] as const;
+
+/**
+ * The set of supported return type kinds for sanitized content.
+ * These are the configuration values you can pass via `returnType`.
+ */
+export type SanitizeReturnKind =
+ | 'string'
+ | 'HTMLBodyElement'
+ | 'DocumentFragment';
+
+/**
+ * Mapping between `SanitizeReturnKind` and the actual returned value types.
+ */
+type ReturnTypeMap = {
+ string: string;
+ HTMLBodyElement: HTMLBodyElement;
+ DocumentFragment: DocumentFragment;
+};
+
+/**
+ * Union of the concrete value types this sanitizer can return.
+ * Useful when you want to accept any possible sanitizer output.
+ */
+export type SanitizeReturnType = ReturnTypeMap[ keyof ReturnTypeMap ];
+
+/**
+ * Configuration options for HTML sanitization.
+ */
+export interface SanitizeConfig< T extends SanitizeReturnKind = 'string' > {
+ /** Allowed HTML tags */
+ tags?: readonly string[];
+ /** Allowed HTML attributes */
+ attr?: readonly string[];
+ /** Desired return type for the sanitized content. Defaults to 'string'. */
+ returnType?: T;
+}
+
+export function sanitizeHTML(
+ html: string | null | undefined,
+ config?: SanitizeConfig< 'string' >
+): string;
+export function sanitizeHTML(
+ html: string | null | undefined,
+ config: SanitizeConfig< 'HTMLBodyElement' >
+): HTMLBodyElement;
+export function sanitizeHTML(
+ html: string | null | undefined,
+ config: SanitizeConfig< 'DocumentFragment' >
+): DocumentFragment;
+export function sanitizeHTML< T extends SanitizeReturnKind = 'string' >(
+ html: string | null | undefined,
+ config?: SanitizeConfig< T >
+): ReturnTypeMap[ T ];
+
+/**
+ * Sanitizes HTML content using DOMPurify with default allowed tags and attributes.
+ *
+ * @param html - The HTML content to sanitize.
+ * @param config - Optional configuration for allowed tags and attributes.
+ *
+ * @return Sanitized HTML content.
+ */
+export function sanitizeHTML< T extends SanitizeReturnKind = 'string' >(
+ html: string | null | undefined,
+ config?: SanitizeConfig< T >
+): ReturnTypeMap[ T ] {
+ const allowedTags = config?.tags || DEFAULT_ALLOWED_TAGS;
+ const allowedAttr = config?.attr || DEFAULT_ALLOWED_ATTR;
+
+ const purifyConfig: DOMPurify.Config = {
+ ALLOWED_TAGS: [ ...allowedTags ],
+ ALLOWED_ATTR: [ ...allowedAttr ],
+ };
+
+ // Provide a no-op TT policy (when supported) to prevent DOMPurify from
+ // creating its internal policy and emitting warnings with multiple instances.
+ const ttNoopPolicy = getNoopTrustedTypesPolicy();
+ if ( ttNoopPolicy ) {
+ purifyConfig.TRUSTED_TYPES_POLICY = ttNoopPolicy as TrustedTypePolicy;
+ }
+
+ // Only pass a single RETURN_* flag if a non-string return type is requested
+ if ( config?.returnType === 'HTMLBodyElement' ) {
+ purifyConfig.RETURN_DOM = true;
+ } else if ( config?.returnType === 'DocumentFragment' ) {
+ purifyConfig.RETURN_DOM_FRAGMENT = true;
+ }
+
+ return DOMPurify.sanitize(
+ html ?? '',
+ purifyConfig
+ ) as unknown as ReturnTypeMap[ T ];
+}
diff --git a/packages/js/sanitize/src/test/integration.test.ts b/packages/js/sanitize/src/test/integration.test.ts
index 658e3e6ff5..c44868028c 100644
--- a/packages/js/sanitize/src/test/integration.test.ts
+++ b/packages/js/sanitize/src/test/integration.test.ts
@@ -1,7 +1,7 @@
/**
* Internal dependencies
*/
-import { sanitizeHTML } from '../index';
+import { sanitizeHTML, getTrustedTypesPolicy } from '../index';
/**
* Integration tests using the real DOMPurify implementation.
@@ -41,4 +41,44 @@ describe( 'sanitizeHTML integration tests', () => {
expect( result ).toContain( '<p>Content</p>' );
expect( result ).toContain( 'Link' );
} );
+
+ test( 'should support Trusted Type Policy', () => {
+ const html =
+ '<p>Safe content</p><a href="#test">Link</a><script>alert("xss")</script><img src="x" onerror="alert(1)">';
+
+ // Provide a minimal Trusted Types factory so getTrustedTypesPolicy can create
+ // a policy in this test environment (JSDOM doesn't ship TT by default).
+ const mockCreatePolicy = jest.fn(
+ (
+ name: string,
+ config: {
+ createHTML: ( s: string ) => string;
+ }
+ ) => ( {
+ name,
+ createHTML: config.createHTML,
+ } )
+ );
+
+ Object.defineProperty( window, 'trustedTypes', {
+ value: { createPolicy: mockCreatePolicy },
+ writable: true,
+ configurable: true,
+ } );
+
+ const ttp = getTrustedTypesPolicy();
+ expect( ttp ).not.toBeNull();
+
+ const result = ttp?.createHTML( html ) ?? '';
+
+ expect( result ).not.toContain( '<script>' );
+ expect( result ).not.toContain( 'alert("xss")' );
+ expect( result ).not.toContain( 'onerror' );
+
+ expect( result ).toContain( '<p>Safe content</p>' );
+ expect( result ).toContain( '<a href="#test">Link</a>' );
+
+ // Cleanup to avoid leaking TT into other tests
+ delete ( window as unknown as { trustedTypes?: unknown } ).trustedTypes;
+ } );
} );
diff --git a/packages/js/sanitize/src/test/index.test.ts b/packages/js/sanitize/src/test/sanitize.test.ts
similarity index 70%
rename from packages/js/sanitize/src/test/index.test.ts
rename to packages/js/sanitize/src/test/sanitize.test.ts
index 43a49d298e..7036e6ccd2 100644
--- a/packages/js/sanitize/src/test/index.test.ts
+++ b/packages/js/sanitize/src/test/sanitize.test.ts
@@ -10,7 +10,7 @@ import {
sanitizeHTML,
DEFAULT_ALLOWED_TAGS,
DEFAULT_ALLOWED_ATTR,
-} from '../index';
+} from '../sanitize';
// Mock DOMPurify for testing
jest.mock( 'dompurify' );
@@ -23,6 +23,43 @@ describe( 'sanitizeHTML', () => {
( DOMPurify.sanitize as jest.Mock ) = mockSanitize;
} );
+ afterEach( () => {
+ delete ( window as unknown as { trustedTypes?: unknown } ).trustedTypes;
+ } );
+
+ test( 'should handle no-op trusted type policy creation errors', () => {
+ const mockCreatePolicy = jest.fn();
+
+ mockCreatePolicy.mockImplementation( () => {
+ throw new Error( 'Creation failed' );
+ } );
+
+ Object.defineProperty( window, 'trustedTypes', {
+ value: {
+ createPolicy: mockCreatePolicy,
+ },
+ writable: true,
+ configurable: true,
+ } );
+
+ const html =
+ '<a href="#" target="_blank" onclick="alert(1)">Link</a><script>alert("xss")</script>';
+ const expectedResult = '<a href="#" target="_blank">Link</a>';
+
+ mockSanitize.mockReturnValue( expectedResult );
+
+ let result = '';
+ expect( () => {
+ result = sanitizeHTML( html );
+ } ).not.toThrow();
+
+ expect( result ).toBe( expectedResult );
+ expect( mockSanitize ).toHaveBeenCalledWith( html, {
+ ALLOWED_TAGS: [ ...DEFAULT_ALLOWED_TAGS ],
+ ALLOWED_ATTR: [ ...DEFAULT_ALLOWED_ATTR ],
+ } );
+ } );
+
describe( 'basic sanitization', () => {
test( 'should sanitize HTML with default allowed tags and attributes', () => {
const html =
@@ -159,89 +196,3 @@ describe( 'sanitizeHTML', () => {
} );
} );
} );
-
-describe( 'trusted types policy', () => {
- test( 'should create trusted types policy when window.trustedTypes is available', async () => {
- const mockCreatePolicy = jest.fn();
- const mockPolicy = {
- name: 'woocommerce-sanitize',
- createHTML: jest.fn( ( str: string ) => str ),
- createScript: jest.fn( ( str: string ) => str ),
- createScriptURL: jest.fn( ( str: string ) => str ),
- };
-
- mockCreatePolicy.mockReturnValue( mockPolicy );
-
- Object.defineProperty( window, 'trustedTypes', {
- value: {
- createPolicy: mockCreatePolicy,
- },
- writable: true,
- configurable: true,
- } );
-
- jest.resetModules();
-
- const { getTrustedTypesPolicy } = await import(
- '../trusted-types-policy'
- );
- const policy = getTrustedTypesPolicy();
-
- expect( policy ).toBe( mockPolicy );
- expect( mockCreatePolicy ).toHaveBeenCalledWith(
- 'woocommerce-sanitize',
- {
- createHTML: expect.any( Function ),
- createScriptURL: expect.any( Function ),
- }
- );
-
- delete ( window as unknown as { trustedTypes?: unknown } ).trustedTypes;
- } );
-
- test( 'should cache the policy instance and not create it multiple times', async () => {
- const mockCreatePolicy = jest.fn();
- const mockPolicy = {
- name: 'woocommerce-sanitize',
- createHTML: jest.fn( ( str: string ) => str ),
- createScript: jest.fn( ( str: string ) => str ),
- createScriptURL: jest.fn( ( str: string ) => str ),
- };
-
- mockCreatePolicy.mockReturnValue( mockPolicy );
-
- Object.defineProperty( window, 'trustedTypes', {
- value: {
- createPolicy: mockCreatePolicy,
- },
- writable: true,
- configurable: true,
- } );
-
- jest.resetModules();
-
- const { getTrustedTypesPolicy } = await import(
- '../trusted-types-policy'
- );
- const policy1 = getTrustedTypesPolicy();
- const policy2 = getTrustedTypesPolicy();
-
- expect( policy1 ).toBe( policy2 );
- expect( mockCreatePolicy ).toHaveBeenCalledTimes( 1 );
-
- delete ( window as unknown as { trustedTypes?: unknown } ).trustedTypes;
- } );
-
- test( 'should handle case when window.trustedTypes is not available', async () => {
- delete ( window as unknown as { trustedTypes?: unknown } ).trustedTypes;
-
- jest.resetModules();
-
- const { getTrustedTypesPolicy } = await import(
- '../trusted-types-policy'
- );
- const policy = getTrustedTypesPolicy();
-
- expect( policy ).toBeNull();
- } );
-} );
diff --git a/packages/js/sanitize/src/test/trusted-types-policy.test.ts b/packages/js/sanitize/src/test/trusted-types-policy.test.ts
new file mode 100644
index 0000000000..f2964bf62a
--- /dev/null
+++ b/packages/js/sanitize/src/test/trusted-types-policy.test.ts
@@ -0,0 +1,120 @@
+describe( 'getTrustedTypesPolicy', () => {
+ let mockCreatePolicy: jest.Mock;
+
+ beforeEach( () => {
+ mockCreatePolicy = jest.fn();
+ Object.defineProperty( window, 'trustedTypes', {
+ value: {
+ createPolicy: mockCreatePolicy,
+ },
+ writable: true,
+ configurable: true,
+ } );
+ } );
+
+ afterEach( () => {
+ jest.resetModules();
+ delete ( window as unknown as { trustedTypes?: unknown } ).trustedTypes;
+ } );
+
+ test( 'should create trusted types policy when window.trustedTypes is available', async () => {
+ const mockPolicy = {
+ name: 'woocommerce-sanitize',
+ createHTML: jest.fn( ( str: string ) => str ),
+ };
+ mockCreatePolicy.mockReturnValue( mockPolicy );
+
+ const { getTrustedTypesPolicy } = await import(
+ '../trusted-types-policy'
+ );
+ const policy = getTrustedTypesPolicy();
+
+ expect( policy ).toBe( mockPolicy );
+ expect( mockCreatePolicy ).toHaveBeenCalledWith(
+ 'woocommerce-sanitize',
+ {
+ createHTML: expect.any( Function ),
+ }
+ );
+ } );
+
+ test( 'should cache the policy instance and not create it multiple times', async () => {
+ const mockPolicy = {
+ name: 'woocommerce-sanitize',
+ createHTML: jest.fn( ( str: string ) => str ),
+ };
+ mockCreatePolicy.mockReturnValue( mockPolicy );
+
+ const { getTrustedTypesPolicy } = await import(
+ '../trusted-types-policy'
+ );
+ const policy1 = getTrustedTypesPolicy();
+ const policy2 = getTrustedTypesPolicy();
+
+ expect( policy1 ).toBe( policy2 );
+ expect( mockCreatePolicy ).toHaveBeenCalledTimes( 1 );
+ } );
+
+ test( 'should handle case when window.trustedTypes is not available', async () => {
+ delete ( window as unknown as { trustedTypes?: unknown } ).trustedTypes;
+
+ const { getTrustedTypesPolicy } = await import(
+ '../trusted-types-policy'
+ );
+ const policy = getTrustedTypesPolicy();
+
+ expect( policy ).toBeNull();
+ } );
+
+ test( 'should handle policy creation errors', async () => {
+ mockCreatePolicy.mockImplementation( () => {
+ throw new Error( 'Creation failed' );
+ } );
+
+ const { getTrustedTypesPolicy } = await import(
+ '../trusted-types-policy'
+ );
+ const policy = getTrustedTypesPolicy();
+
+ expect( policy ).toBeNull();
+ } );
+
+ test( 'should call sanitizeHTML when createHTML is invoked', async () => {
+ // Mock sanitizeHTML
+ const mockSanitizeHTML = jest.fn(
+ ( input: string ) => `sanitized: ${ input }`
+ );
+
+ // Setup trusted types mock
+ const mockPolicy = {
+ name: 'woocommerce-sanitize',
+ createHTML: jest.fn(),
+ };
+
+ mockCreatePolicy.mockImplementation( ( name, config ) => {
+ // Capture the createHTML function that was passed
+ mockPolicy.createHTML = config.createHTML;
+ return mockPolicy;
+ } );
+
+ // Mock the sanitize module
+ jest.doMock( '../sanitize', () => ( {
+ sanitizeHTML: mockSanitizeHTML,
+ } ) );
+
+ const { getTrustedTypesPolicy } = await import(
+ '../trusted-types-policy'
+ );
+ const policy = getTrustedTypesPolicy();
+
+ // Now call createHTML on the policy
+ const testInput = '<script>alert("xss")</script><p>Hello</p>';
+ const result = policy?.createHTML( testInput );
+
+ // Verify sanitizeHTML was called with the input
+ expect( mockSanitizeHTML ).toHaveBeenCalledWith( testInput );
+ expect( result ).toBe( 'sanitized: ' + testInput );
+
+ jest.dontMock( '../sanitize' );
+ } );
+} );
diff --git a/packages/js/sanitize/src/trusted-types-policy.ts b/packages/js/sanitize/src/trusted-types-policy.ts
index 07c554060f..6b63e4a60f 100644
--- a/packages/js/sanitize/src/trusted-types-policy.ts
+++ b/packages/js/sanitize/src/trusted-types-policy.ts
@@ -1,62 +1,56 @@
/**
* External dependencies
*/
-import type DOMPurify from 'dompurify';
+import type { TrustedTypePolicy } from 'trusted-types';
/**
- * Extract the TrustedTypesPolicy type from DOMPurify's Config.
- * This ensures our policy type matches exactly what DOMPurify expects.
+ * Internal dependencies
*/
-export type TrustedTypesPolicy = NonNullable<
- DOMPurify.Config[ 'TRUSTED_TYPES_POLICY' ]
->;
-
-// Extend Window interface to include trustedTypes
-declare global {
- interface Window {
- trustedTypes?: {
- createPolicy: (
- name: string,
- rules: {
- createHTML?: ( input: string ) => string;
- createScript?: ( input: string ) => string;
- createScriptURL?: ( input: string ) => string;
- }
- ) => TrustedTypesPolicy;
- defaultPolicy?: TrustedTypesPolicy;
- };
- }
-}
+import { sanitizeHTML } from './sanitize';
/**
- * The name of the trusted types policy.
+ * The type for our trusted types policy.
*/
-export const TRUSTED_POLICY_NAME = 'woocommerce-sanitize';
+export type WooCommerceSanitizePolicyType = Pick<
+ TrustedTypePolicy,
+ 'name' | 'createHTML'
+>;
/**
* Cached policy instance to ensure it's only created once.
*/
-let policyInstance: TrustedTypesPolicy | null | undefined;
+let policyInstance: WooCommerceSanitizePolicyType | null | undefined;
/**
* Get or create a trusted types policy for DOMPurify.
*
- * @return TrustedTypesPolicy object or null if not supported.
+ * @return TrustedTypePolicy object or null if not supported.
*/
-export function getTrustedTypesPolicy(): TrustedTypesPolicy | null {
+export function getTrustedTypesPolicy(): WooCommerceSanitizePolicyType | null {
if ( policyInstance !== undefined ) {
return policyInstance;
}
- if ( ! window || ! window.trustedTypes ) {
+ if ( typeof window === 'undefined' || ! window.trustedTypes ) {
policyInstance = null;
return null;
}
- policyInstance = window.trustedTypes.createPolicy( TRUSTED_POLICY_NAME, {
- createHTML: ( string: string ) => string,
- createScriptURL: ( url ) => url,
- } );
+ try {
+ policyInstance = window.trustedTypes.createPolicy(
+ 'woocommerce-sanitize',
+ {
+ createHTML: ( input: string ): string => sanitizeHTML( input ),
+ }
+ );
+ } catch ( error ) {
+ policyInstance = null;
+ // eslint-disable-next-line no-console
+ console.warn(
+ 'Failed to create "woocommerce-sanitize" trusted type policy:',
+ error
+ );
+ }
return policyInstance;
}
diff --git a/packages/js/sanitize/tsconfig-cjs.json b/packages/js/sanitize/tsconfig-cjs.json
index 0b0da4d295..eaf1923721 100644
--- a/packages/js/sanitize/tsconfig-cjs.json
+++ b/packages/js/sanitize/tsconfig-cjs.json
@@ -14,4 +14,4 @@
"exclude": [
"**/test/**"
]
-}
\ No newline at end of file
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 476f720fe0..c415c22c61 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -28,7 +28,7 @@ importers:
version: 7.25.7
'@wordpress/babel-preset-default':
specifier: next
- version: 8.31.1-next.233ccab9b.0
+ version: 8.33.1-next.36001005c.0
lodash:
specifier: ^4.17.21
version: 4.17.21
@@ -150,7 +150,7 @@ importers:
version: link:../internal-style-build
'@wordpress/browserslist-config':
specifier: next
- version: 6.31.1-next.233ccab9b.0
+ version: 6.33.1-next.36001005c.0
css-loader:
specifier: 6.11.x
version: 6.11.0(webpack@5.97.1(@swc/core@1.3.100))
@@ -256,7 +256,7 @@ importers:
version: link:../internal-style-build
'@wordpress/browserslist-config':
specifier: next
- version: 6.31.1-next.233ccab9b.0
+ version: 6.33.1-next.36001005c.0
copy-webpack-plugin:
specifier: 13.0.x
version: 13.0.0(webpack@5.97.1(@swc/core@1.3.100))
@@ -563,10 +563,10 @@ importers:
version: link:../internal-style-build
'@wordpress/babel-preset-default':
specifier: next
- version: 8.31.1-next.233ccab9b.0
+ version: 8.33.1-next.36001005c.0
'@wordpress/browserslist-config':
specifier: next
- version: 6.31.1-next.233ccab9b.0
+ version: 6.33.1-next.36001005c.0
css-loader:
specifier: 6.11.x
version: 6.11.0(webpack@5.97.1(@swc/core@1.3.100))
@@ -818,7 +818,7 @@ importers:
version: link:../tracks
'@wordpress/browserslist-config':
specifier: next
- version: 6.31.1-next.233ccab9b.0
+ version: 6.33.1-next.36001005c.0
css-loader:
specifier: 6.11.x
version: 6.11.0(webpack@5.97.1(@swc/core@1.3.100))
@@ -1066,7 +1066,7 @@ importers:
dependencies:
'@wordpress/dependency-extraction-webpack-plugin':
specifier: next
- version: 6.31.1-next.233ccab9b.0(webpack@5.97.1)
+ version: 6.33.1-next.36001005c.0(webpack@5.97.1)
devDependencies:
'@babel/core':
specifier: 7.25.7
@@ -1119,7 +1119,7 @@ importers:
version: 7.25.7
'@wordpress/babel-preset-default':
specifier: next
- version: 8.31.1-next.233ccab9b.0
+ version: 8.33.1-next.36001005c.0
jest:
specifier: 29.5.x
version: 29.5.0(@types/node@20.17.8)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@20.17.8)(typescript@5.7.2))
@@ -1300,7 +1300,7 @@ importers:
version: link:../internal-style-build
'@wordpress/browserslist-config':
specifier: next
- version: 6.31.1-next.233ccab9b.0
+ version: 6.33.1-next.36001005c.0
'@wordpress/prettier-config':
specifier: 2.17.0
version: 2.17.0(wp-prettier@2.8.5)
@@ -1701,7 +1701,7 @@ importers:
version: link:../internal-js-tests
'@wordpress/browserslist-config':
specifier: next
- version: 6.31.1-next.233ccab9b.0
+ version: 6.33.1-next.36001005c.0
copy-webpack-plugin:
specifier: 13.0.x
version: 13.0.0(webpack@5.97.1(@swc/core@1.3.100))
@@ -2124,7 +2124,7 @@ importers:
version: link:../internal-style-build
'@wordpress/browserslist-config':
specifier: next
- version: 6.31.1-next.233ccab9b.0
+ version: 6.33.1-next.36001005c.0
css-loader:
specifier: 6.11.x
version: 6.11.0(webpack@5.97.1(@swc/core@1.3.100))
@@ -2377,7 +2377,7 @@ importers:
version: link:../internal-style-build
'@wordpress/browserslist-config':
specifier: next
- version: 6.31.1-next.233ccab9b.0
+ version: 6.33.1-next.36001005c.0
copy-webpack-plugin:
specifier: 13.0.x
version: 13.0.0(webpack@5.97.1(@swc/core@1.3.100))
@@ -2527,6 +2527,9 @@ importers:
'@types/jest':
specifier: 29.5.x
version: 29.5.14
+ '@types/trusted-types':
+ specifier: ^2.0.7
+ version: 2.0.7
'@woocommerce/eslint-plugin':
specifier: workspace:*
version: link:../eslint-plugin
@@ -2734,7 +2737,7 @@ importers:
version: 13.0.7(@emotion/is-prop-valid@1.2.1)(@types/react-dom@18.3.0)(@types/react@18.3.16)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@wordpress/browserslist-config':
specifier: next
- version: 6.31.1-next.233ccab9b.0
+ version: 6.33.1-next.36001005c.0
copy-webpack-plugin:
specifier: 13.0.x
version: 13.0.0(webpack@5.97.1(@swc/core@1.3.100))
@@ -2913,7 +2916,7 @@ importers:
version: link:../../packages/js/eslint-plugin
'@wordpress/browserslist-config':
specifier: next
- version: 6.31.1-next.233ccab9b.0
+ version: 6.33.1-next.36001005c.0
'@wordpress/e2e-test-utils-playwright':
specifier: wp-6.8
version: 1.19.1(@playwright/test@1.50.1)
@@ -3458,13 +3461,13 @@ importers:
version: link:../../../../packages/js/tracks
'@wordpress/babel-preset-default':
specifier: next
- version: 8.31.1-next.233ccab9b.0
+ version: 8.33.1-next.36001005c.0
'@wordpress/block-editor':
specifier: wp-6.6
version: 13.0.7(@emotion/is-prop-valid@1.2.1)(@types/react-dom@18.3.0)(@types/react@18.3.16)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@wordpress/browserslist-config':
specifier: next
- version: 6.31.1-next.233ccab9b.0
+ version: 6.33.1-next.36001005c.0
'@wordpress/jest-preset-default':
specifier: ^8.5.2
version: 8.5.2(@babel/core@7.25.7)(jest@29.5.0(@types/node@20.17.8)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@20.17.8)(typescript@5.7.2)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -3943,7 +3946,7 @@ importers:
version: 6.21.0
'@wordpress/babel-preset-default':
specifier: next
- version: 8.31.1-next.233ccab9b.0
+ version: 8.33.1-next.36001005c.0
'@wordpress/base-styles':
specifier: 4.35.0
version: 4.35.0
@@ -3958,7 +3961,7 @@ importers:
version: 13.0.3(react@18.3.1)
'@wordpress/browserslist-config':
specifier: next
- version: 6.31.1-next.233ccab9b.0
+ version: 6.33.1-next.36001005c.0
'@wordpress/components':
specifier: wp-6.6
version: 28.0.3(@emotion/is-prop-valid@1.2.1)(@types/react@18.3.16)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -3973,7 +3976,7 @@ importers:
version: 4.44.0
'@wordpress/dependency-extraction-webpack-plugin':
specifier: next
- version: 6.31.1-next.233ccab9b.0(webpack@5.97.1)
+ version: 6.33.1-next.36001005c.0(webpack@5.97.1)
'@wordpress/dom':
specifier: 3.27.0
version: 3.27.0
@@ -10187,8 +10190,8 @@ packages:
resolution: {integrity: sha512-DUEAseIg3Xqa4MroaFQEob4TYTGJv0zKRLsDrLHAgQCTtC4PcvUqU0gM7JZjG3zo20G9R5YCBNzx1353qd1t7Q==}
engines: {node: '>=18.12.0', npm: '>=8.19.2'}
- '@wordpress/babel-preset-default@8.31.1-next.233ccab9b.0':
- resolution: {integrity: sha512-etyZziIHWzBZ17JxY6pRD0xvAJ8cwp+BOZtxd9qpn5BUDyUm/x/MNurtQnonEzICunjdDLr4EWFBvyiw5vYXQQ==}
+ '@wordpress/babel-preset-default@8.33.1-next.36001005c.0':
+ resolution: {integrity: sha512-wfLkRkINO090EkK88g4w+/XpbaJOjoM0/jfXoE5qrXNgR5DBr78E9z9VfCFHzAcfJjoU+cwU+2hCh7EIXa29Ag==}
engines: {node: '>=18.12.0', npm: '>=8.19.2'}
'@wordpress/base-styles@3.6.0':
@@ -10335,8 +10338,8 @@ packages:
resolution: {integrity: sha512-CjirkPIkMf72VQcKmhmQZUJGHHFEt80ITZVgnxEtyswWA6QPRXIwFhQOAElmfhWg2wS6pCncyg6k7DfgYX3bOg==}
engines: {node: '>=18.12.0', npm: '>=8.19.2'}
- '@wordpress/browserslist-config@6.31.1-next.233ccab9b.0':
- resolution: {integrity: sha512-iznSWhxvGWUE4KEkNxk9zk0JKBOWwbHLwQFuYaeP2UPTGVI4N4Av558U12XrzhpAayCRiQfRjnBcVehU6hhwsg==}
+ '@wordpress/browserslist-config@6.33.1-next.36001005c.0':
+ resolution: {integrity: sha512-9T44GQxkKpbQPilvS02j7/C521n36/PUlIIodpk2/KjTW9FE/touygGjyA5Rh3mJwLzdPAVGCe+ZwgJhqDc+8Q==}
engines: {node: '>=18.12.0', npm: '>=8.19.2'}
'@wordpress/commands@0.29.0':
@@ -10629,8 +10632,8 @@ packages:
peerDependencies:
webpack: ^5.0.0
- '@wordpress/dependency-extraction-webpack-plugin@6.31.1-next.233ccab9b.0':
- resolution: {integrity: sha512-Yh9kgLxzOnWHGRw7S7vwfMr9F0rX/D8uZJoJ7OMQ//3J3NjRqh89GrPHBJaGBB5Elm5nBsLzBLglJPXwabTkCA==}
+ '@wordpress/dependency-extraction-webpack-plugin@6.33.1-next.36001005c.0':
+ resolution: {integrity: sha512-RIVGzoNsAGce3ZI4wl71mcR+tZibLkcCArOWKm2snkImdtX7X87PHzeirOBpB4rPKbrq06Y0urha1u0eh8kA9A==}
engines: {node: '>=18.12.0', npm: '>=8.19.2'}
peerDependencies:
webpack: ^5.0.0
@@ -11876,14 +11879,14 @@ packages:
resolution: {integrity: sha512-KkVhXK9s5Ftly2Z0BJfQR7m3Z4WB+8/+w0Tj86Cztz3NJk3iFF51Tes5zAD8GhDJ4SelwGW5ghALV51coTjrWA==}
engines: {node: '>=18.12.0', npm: '>=8.19.2'}
- '@wordpress/warning@3.30.0':
- resolution: {integrity: sha512-ZtkpSe3DhtUzIrwf+5slGkJJCxy1xn56fZ6atUaJWRbjsKnIZlTcPgahPUJZ2bugsGS5BlmDEuVI8C4NUdbwvQ==}
- engines: {node: '>=18.12.0', npm: '>=8.19.2'}
-
'@wordpress/warning@3.32.0':
resolution: {integrity: sha512-6dPNKfJAOXijIMi9k/QdS/IQvHXcl5ErNM10y5dIhhLDuGmsZlQER06VrVmQIVAkbsmL49OfrqkqMOQidp61JA==}
engines: {node: '>=18.12.0', npm: '>=8.19.2'}
+ '@wordpress/warning@3.34.0':
+ resolution: {integrity: sha512-WemuVXjaekzCDsWbDPj/RZSy44mIjPIy35DaoJgfLcgkXMH2GRBRSomhZMkWyGatD39vdXm0nqe95LsLDqrwCg==}
+ engines: {node: '>=18.12.0', npm: '>=8.19.2'}
+
'@wordpress/warning@3.8.1':
resolution: {integrity: sha512-xlo0Xw1jiyiE6nh43NAtQMAL05VDk837kY2xfjsus6wD597TeWFpj6gmcRMH25FZULTUHDB2EPfLviWXqOgUfg==}
engines: {node: '>=18.12.0', npm: '>=8.19.2'}
@@ -15017,6 +15020,7 @@ packages:
eslint-plugin-markdown@2.2.1:
resolution: {integrity: sha512-FgWp4iyYvTFxPwfbxofTvXxgzPsDuSKHQy2S+a8Ve6savbujey+lgrFFbXQA0HPygISpRYWYBjooPzhYSF81iA==}
engines: {node: ^8.10.0 || ^10.12.0 || >= 12.0.0}
+ deprecated: Please use @eslint/markdown instead
peerDependencies:
eslint: '>=6.0.0'
@@ -20716,6 +20720,7 @@ packages:
engines: {node: '>=0.6.0', teleport: '>=0.2.0'}
deprecated: |-
You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.
+
(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)
qqjs@0.3.11:
@@ -33551,23 +33556,23 @@ snapshots:
'@babel/preset-typescript': 7.25.7(@babel/core@7.25.7)
'@babel/runtime': 7.25.7
'@wordpress/browserslist-config': 6.30.0
- '@wordpress/warning': 3.30.0
+ '@wordpress/warning': 3.32.0
browserslist: 4.24.4
core-js: 3.40.0
react: 18.3.1
transitivePeerDependencies:
- supports-color
- '@wordpress/babel-preset-default@8.31.1-next.233ccab9b.0':
+ '@wordpress/babel-preset-default@8.33.1-next.36001005c.0':
dependencies:
'@babel/core': 7.25.7
+ '@babel/plugin-syntax-import-attributes': 7.26.0(@babel/core@7.25.7)
'@babel/plugin-transform-react-jsx': 7.25.7(@babel/core@7.25.7)
'@babel/plugin-transform-runtime': 7.25.7(@babel/core@7.25.7)
'@babel/preset-env': 7.25.7(@babel/core@7.25.7)
'@babel/preset-typescript': 7.25.7(@babel/core@7.25.7)
- '@babel/runtime': 7.25.7
- '@wordpress/browserslist-config': 6.31.1-next.233ccab9b.0
- '@wordpress/warning': 3.32.0
+ '@wordpress/browserslist-config': 6.33.1-next.36001005c.0
+ '@wordpress/warning': 3.34.0
browserslist: 4.24.2
core-js: 3.40.0
react: 18.3.1
@@ -33753,7 +33758,7 @@ snapshots:
'@wordpress/token-list': 3.20.0
'@wordpress/upload-media': 0.5.0(@babel/core@7.25.7)(@emotion/is-prop-valid@1.2.1)(@types/react@18.3.16)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(webpack-virtual-modules@0.6.1)(webpack@5.97.1)
'@wordpress/url': 4.20.0
- '@wordpress/warning': 3.30.0
+ '@wordpress/warning': 3.32.0
'@wordpress/wordcount': 4.20.0
change-case: 4.1.2
clsx: 2.1.1
@@ -33814,7 +33819,7 @@ snapshots:
'@wordpress/style-engine': 2.10.0
'@wordpress/token-list': 3.10.0
'@wordpress/url': 4.19.1
- '@wordpress/warning': 3.30.0
+ '@wordpress/warning': 3.32.0
'@wordpress/wordcount': 4.20.0
change-case: 4.1.2
clsx: 2.1.1
@@ -34070,7 +34075,7 @@ snapshots:
'@wordpress/private-apis': 1.20.0
'@wordpress/rich-text': 7.16.0(react@18.3.1)
'@wordpress/shortcode': 4.10.0
- '@wordpress/warning': 3.30.0
+ '@wordpress/warning': 3.32.0
change-case: 4.1.2
colord: 2.9.3
fast-deep-equal: 3.1.3
@@ -34101,7 +34106,7 @@ snapshots:
'@wordpress/private-apis': 1.20.0
'@wordpress/rich-text': 7.20.0(react@18.3.1)
'@wordpress/shortcode': 4.20.0
- '@wordpress/warning': 3.30.0
+ '@wordpress/warning': 3.32.0
change-case: 4.1.2
colord: 2.9.3
fast-deep-equal: 3.1.3
@@ -34123,7 +34128,7 @@ snapshots:
'@wordpress/browserslist-config@6.30.0': {}
- '@wordpress/browserslist-config@6.31.1-next.233ccab9b.0': {}
+ '@wordpress/browserslist-config@6.33.1-next.36001005c.0': {}
'@wordpress/commands@0.29.0(@emotion/is-prop-valid@1.2.1)(@types/react@18.3.16)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
@@ -34596,7 +34601,7 @@ snapshots:
'@wordpress/primitives': 4.11.0(react@18.3.1)
'@wordpress/private-apis': 1.20.0
'@wordpress/rich-text': 7.16.0(react@18.3.1)
- '@wordpress/warning': 3.30.0
+ '@wordpress/warning': 3.32.0
change-case: 4.1.2
clsx: 2.1.1
colord: 2.9.3
@@ -34650,7 +34655,7 @@ snapshots:
'@wordpress/primitives': 4.20.0(react@18.3.1)
'@wordpress/private-apis': 1.20.0
'@wordpress/rich-text': 7.16.0(react@18.3.1)
- '@wordpress/warning': 3.30.0
+ '@wordpress/warning': 3.32.0
change-case: 4.1.2
clsx: 2.1.1
colord: 2.9.3
@@ -34704,7 +34709,7 @@ snapshots:
'@wordpress/primitives': 4.20.0(react@18.3.1)
'@wordpress/private-apis': 1.20.0
'@wordpress/rich-text': 7.20.0(react@18.3.1)
- '@wordpress/warning': 3.30.0
+ '@wordpress/warning': 3.32.0
change-case: 4.1.2
clsx: 2.1.1
colord: 2.9.3
@@ -34758,7 +34763,7 @@ snapshots:
'@wordpress/primitives': 4.21.0(react@18.3.1)
'@wordpress/private-apis': 1.21.0
'@wordpress/rich-text': 7.21.0(react@18.3.1)
- '@wordpress/warning': 3.30.0
+ '@wordpress/warning': 3.32.0
change-case: 4.1.2
clsx: 2.1.1
colord: 2.9.3
@@ -35094,7 +35099,7 @@ snapshots:
'@wordpress/sync': 1.10.0
'@wordpress/undo-manager': 1.10.0
'@wordpress/url': 4.19.1
- '@wordpress/warning': 3.30.0
+ '@wordpress/warning': 3.32.0
change-case: 4.1.2
equivalent-key-map: 0.2.2
fast-deep-equal: 3.1.3
@@ -35128,7 +35133,7 @@ snapshots:
'@wordpress/sync': 1.20.0
'@wordpress/undo-manager': 1.20.0
'@wordpress/url': 4.20.0
- '@wordpress/warning': 3.30.0
+ '@wordpress/warning': 3.32.0
change-case: 4.1.2
equivalent-key-map: 0.2.2
fast-deep-equal: 3.1.3
@@ -35366,7 +35371,7 @@ snapshots:
json2php: 0.0.7
webpack: 5.97.1(@swc/core@1.3.100)(webpack-cli@5.1.4)
- '@wordpress/dependency-extraction-webpack-plugin@6.31.1-next.233ccab9b.0(webpack@5.97.1)':
+ '@wordpress/dependency-extraction-webpack-plugin@6.33.1-next.36001005c.0(webpack@5.97.1)':
dependencies:
json2php: 0.0.7
webpack: 5.97.1(@swc/core@1.3.100)(esbuild@0.18.20)(webpack-cli@5.1.4)
@@ -36252,7 +36257,7 @@ snapshots:
'@wordpress/primitives': 4.11.0(react@18.3.1)
'@wordpress/private-apis': 1.8.1
'@wordpress/url': 4.19.1
- '@wordpress/warning': 3.30.0
+ '@wordpress/warning': 3.32.0
change-case: 4.1.2
client-zip: 2.4.5
react: 18.3.1
@@ -36285,7 +36290,7 @@ snapshots:
'@wordpress/primitives': 4.11.0(react@18.3.1)
'@wordpress/private-apis': 1.8.1
'@wordpress/url': 4.19.1
- '@wordpress/warning': 3.30.0
+ '@wordpress/warning': 3.32.0
change-case: 4.1.2
client-zip: 2.4.5
react: 18.3.1
@@ -38285,10 +38290,10 @@ snapshots:
'@wordpress/warning@3.21.0': {}
- '@wordpress/warning@3.30.0': {}
-
'@wordpress/warning@3.32.0': {}
+ '@wordpress/warning@3.34.0': {}
+
'@wordpress/warning@3.8.1': {}
'@wordpress/widgets@3.24.0(@babel/helper-module-imports@7.25.9)(@babel/types@7.26.0)(@emotion/is-prop-valid@1.2.1)(@types/react@18.3.16)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':