Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow to render TrustedHTML objects similar to SafeString #1567

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions packages/@glimmer-workspace/integration-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@simple-dom/document": "^1.4.0",
"@simple-dom/serializer": "^1.4.0",
"@simple-dom/void-map": "^1.4.0",
"@types/trusted-types": "^2.0.7",
"js-reporters": "^2.1.0",
"qunit": "^2.19.4",
"simple-html-tokenizer": "^0.5.11"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { TrustedTypePolicy, TrustedTypesWindow } from 'trusted-types/lib';

import { jitSuite, RenderTest, test } from '..';

let policy: TrustedTypePolicy | undefined;
if (typeof window !== 'undefined') {
let trustedTypes = (window as unknown as TrustedTypesWindow).trustedTypes;
if (trustedTypes?.createPolicy) {
policy = trustedTypes.createPolicy('test', {
createHTML: (s: string) => s,
createScript: (s: string) => s,
createScriptURL: (s: string) => s,
});
}
}

export class TrustedHTMLTests extends RenderTest {
static suiteName = 'TrustedHTML';

@test
'renders TrustedHTML similar to SafeString'() {
if (!policy) return;

let html = '<b>test\'"&quot;</b>';
this.registerHelper('trustedHTML', () => {
return policy?.createHTML(html);
});

this.render('<div>{{trustedHTML}}</div>');
this.assertHTML('<div><b>test\'""</b></div>');
this.assertStableRerender();
}

@test
'renders TrustedHTML in attribute context as string'() {
crypto marked this conversation as resolved.
Show resolved Hide resolved
if (!policy) return;

let html = '<b>test\'"&quot;</b>';
this.registerHelper('trustedHTML', () => {
return policy?.createHTML(html);
});

// To keep rendering behavior consistent with SafeString
// trustedHTML is not encoded or decoded in attribute value context.
// It is set as string, that means result value can contain HTML enitites.
// TrustedHTML value must not escape from HTML attribute value context to prevent XSS.
this.render('<a title="{{trustedHTML}}">{{trustedHTML}}</a>');
this.assertHTML('<a title="<b>test\'&quot;&amp;quot;</b>"><b>test\'""</b></a>');
this.assertStableRerender();
}
}

jitSuite(TrustedHTMLTests);
3 changes: 2 additions & 1 deletion packages/@glimmer/interfaces/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"test:types": "tsc --noEmit"
},
"dependencies": {
"@simple-dom/interface": "^1.4.0"
"@simple-dom/interface": "^1.4.0",
"@types/trusted-types": "^2.0.7"
},
"devDependencies": {
"@glimmer-workspace/build-support": "workspace:^",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ export function StdAppend(
op(Op.AppendSafeHTML);
});

when(ContentType.TrustedHTML, () => {
op(Op.AssertSame);
op(Op.AppendHTML);
crypto marked this conversation as resolved.
Show resolved Hide resolved
});

when(ContentType.Fragment, () => {
op(Op.AssertSame);
op(Op.AppendDocumentFragment);
Expand Down
11 changes: 10 additions & 1 deletion packages/@glimmer/runtime/lib/compiled/opcodes/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@ import { isObject } from '@glimmer/util';
import { ContentType, CurriedType, Op } from '@glimmer/vm';

import { isCurriedType } from '../../curried-value';
import { isEmpty, isFragment, isNode, isSafeString, shouldCoerce } from '../../dom/normalize';
import {
isEmpty,
isFragment,
isNode,
isSafeString,
isTrustedHTML,
shouldCoerce,
} from '../../dom/normalize';
import { APPEND_OPCODES } from '../../opcodes';
import DynamicTextContent from '../../vm/content/text';
import { CheckReference } from './-debug-strip';
Expand All @@ -32,6 +39,8 @@ function toContentType(value: unknown) {
return ContentType.Helper;
} else if (isSafeString(value)) {
return ContentType.SafeString;
} else if (isTrustedHTML(value)) {
return ContentType.TrustedHTML;
} else if (isFragment(value)) {
return ContentType.Fragment;
} else if (isNode(value)) {
Expand Down
13 changes: 13 additions & 0 deletions packages/@glimmer/runtime/lib/dom/normalize.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Dict, SimpleDocumentFragment, SimpleNode } from '@glimmer/interfaces';
import type { TrustedHTML, TrustedTypesWindow } from 'trusted-types/lib';

export interface SafeString {
toHTML(): string;
Expand Down Expand Up @@ -43,6 +44,18 @@ export function isEmpty(value: unknown): boolean {
return value === null || value === undefined || typeof (value as Dict).toString !== 'function';
}

let isHTML: ((value: unknown) => boolean) | undefined;
if (typeof window !== 'undefined') {
let trustedTypes = (window as unknown as TrustedTypesWindow).trustedTypes;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a good way to progressively allow the feature for browsers that have it and not break in browsers that don't

if (trustedTypes?.isHTML) {
isHTML = trustedTypes?.isHTML.bind(trustedTypes);
}
}

export function isTrustedHTML(value: unknown): value is TrustedHTML {
return isHTML ? isHTML(value) : false;
}

export function isSafeString(value: unknown): value is SafeString {
return typeof value === 'object' && value !== null && typeof (value as any).toHTML === 'function';
}
Expand Down
3 changes: 2 additions & 1 deletion packages/@glimmer/runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@
"@glimmer/util": "workspace:^",
"@glimmer/validator": "workspace:^",
"@glimmer/vm": "workspace:^",
"@glimmer/wire-format": "workspace:^"
"@glimmer/wire-format": "workspace:^",
"@types/trusted-types": "^2.0.7"
},
"devDependencies": {
"@glimmer-workspace/build-support": "workspace:^",
Expand Down
1 change: 1 addition & 0 deletions packages/@glimmer/vm/lib/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export const ContentType = {
Fragment: 5,
Node: 6,
Other: 8,
TrustedHTML: 9,
} as const;
Loading
Loading