Skip to content

Commit 4de5682

Browse files
committed
feat: Use Trusted Types in MyFeedElement - simplify JSON feed (#62)(#29)
1 parent 6034dac commit 4de5682

6 files changed

Lines changed: 57 additions & 19 deletions

File tree

demo/my-trusted-types.html

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,31 @@
11
<!doctype html><html lang="en">
22

3+
<meta
4+
http-equiv="Content-Security-Policy"
5+
content="require-trusted-types-for 'script'; trusted-types allowAnchor allowAnchorListPlus"
6+
>
7+
38
<link rel="stylesheet" href="style/app.v2.css">
49
<style>
5-
#myDiv { line-height: 1.7; }
10+
#myDiv {
11+
background: #eee;
12+
border: 1px solid silver;
13+
line-height: 1.8;
14+
margin: 2rem 0;
15+
padding: 1rem;
16+
}
617
</style>
718

819
<title>Trusted Types test</title>
920

1021
<h1>Trusted Types test</h1>
1122

23+
<p>Uses a <tt>Content-Security-Policy</tt> header with <tt>require-trusted-types-for</tt>.
24+
And, uses the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API">Trusted Types API</a>
25+
to sanitize input HTML data.
26+
</p>
27+
28+
1229
<div id="myDiv">Loading Trusted Types polyfill...</div>
1330

1431

@@ -56,8 +73,11 @@ <h1>Trusted Types test</h1>
5673
const escaped = myTrustedTypes.createHTML('allowAnchor', uncleanHTML);
5774

5875
console.assert(escaped instanceof TrustedHTML, 'Expecting trustedHTML');
59-
console.log('Trusted HTML?', myTrustedTypes, escaped.toString()); // true
76+
console.log('Trusted HTML?', myTrustedTypes, escaped); // true
6077

61-
elem.innerHTML = escaped.toString();
78+
// elem.innerHTML = 'Hello world!'; // "my-trusted-types.html:64 This document requires 'TrustedHTML' assignment."
79+
elem.innerHTML = escaped;
6280
})();
6381
</script>
82+
83+
</html>

demo/style/app.v2.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ h2 {
7575
body {
7676
color: #222;
7777
font-family: sans-serif;
78+
line-height: 1.6;
7879
margin: auto;
7980
max-width: 36rem;
8081
padding: 0 1rem;

src/build/feed.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,13 @@ function metaDataToFeed (data) {
5858
const tags = status ? status.split(/, ?/) : [];
5959
const url = `${GH_URL}/${fileName}`;
6060
// eslint-disable-next-line camelcase
61-
const content_html = `<p part="p">${desc}</p>
61+
const content_html = `<p>${desc}</p>
6262
<ul>
63-
<li><i part="k i">Demo:</i> <a href="${demoUrl || '#'}">${demoUrl}</a>
64-
<li><i part="k i">Status:</i> ${status || ''}
65-
<li><i part="k i">className:</i> <code>${className}</code>
66-
<li><i part="k i">parentClass:</i> <code>${parentClass}</code>
67-
<li><i part="k i">tagName:</i> <code>&lt;${tagName}></code>
63+
<li><x>Demo:</x> <a href="${demoUrl || '#'}">${demoUrl}</a>
64+
<li><x>Status:</x> ${status || ''}
65+
<li><x>className:</x> <code>${className}</code>
66+
<li><x>parentClass:</x> <code>${parentClass}</code>
67+
<li><x>tagName:</x> <code>&lt;${tagName}></code>
6868
</ul>`;
6969

7070
return { id, title, url, tags, content_html, _ext: it }; /* eslint-disable-line camelcase */

src/components/MyAnalyticsElement.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ const { HTMLElement, localStorage } = window;
33
/**
44
* Embed analytics without cookies.
55
*
6-
* Easily embed Google Analytics - use <tt>localStorage</tt> instead of cookies, and anonyise the IP address.
6+
* Easily embed Google Analytics - use <code>localStorage</code> instead of cookies, and anonyise the IP address.
77
*
88
* @copyright © Nick Freear, 28-June-2021.
99
*

src/components/MyFeedElement.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { appendTemplate, safeUrl, strip } from '../util/attachTemplate.js';
2+
import MyTrustedTypes from '../util/MyTrustedTypes.js';
23
// Was: import MyElement from '../MyElement.js';
34

45
const { fetch, HTMLElement, Request, location } = window;
@@ -15,6 +16,8 @@ const { fetch, HTMLElement, Request, location } = window;
1516
* @since 1.3.0
1617
*/
1718
export class MyFeedElement extends HTMLElement {
19+
#trustedTypes;
20+
1821
static getTag () { return 'my-feed'; }
1922

2023
get href () {
@@ -45,6 +48,7 @@ export class MyFeedElement extends HTMLElement {
4548
}
4649

4750
async connectedCallback () {
51+
await this.#loadTrustedTypes();
4852
const { data, resp, req } = await this.#fetchFeed();
4953

5054
const FILTERED = this.#filterItems(data.items);
@@ -82,14 +86,17 @@ export class MyFeedElement extends HTMLElement {
8286
return filtered;
8387
}
8488

89+
get #policyId () { return 'allowAnchorListPlus'; }
90+
8591
#makeListItem (item, open) {
92+
const createHTML = (s) => this.#trustedTypes.createHTML(this.#policyId , s);
8693
const { skip, guid, link, pubDate, title, url, time, tags, content, content_html } = item; /* eslint-disable-line camelcase */
8794

8895
if (skip) return '<template><!-- skip --></template>';
8996

9097
// @TODO: security! - _saferHtml()
9198
const CONTENT = content || content_html || null; /* eslint-disable-line camelcase */
92-
const DETAILS = CONTENT ? `<details part="details" ${open ? 'open' : ''}><summary part="summary">More</summary>${CONTENT}</details>` : null;
99+
const DETAILS = CONTENT ? `<details part="details" ${open ? 'open' : ''}><summary part="summary">More</summary>${createHTML(CONTENT)}</details>` : null;
93100
// Be liberal in what we accept - 'link' or 'url'.
94101
// console.debug('makeListItem:', item);
95102
return `<template>
@@ -119,4 +126,9 @@ export class MyFeedElement extends HTMLElement {
119126
const feedUrl = this.toJson ? this.#rssToJsonService + encodeURIComponent(PARSED.href) : PARSED.href;
120127
return new Request(feedUrl);
121128
}
129+
130+
async #loadTrustedTypes () {
131+
this.#trustedTypes = new MyTrustedTypes();
132+
await this.#trustedTypes.load();
133+
}
122134
}

src/util/MyTrustedTypes.js

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ export class MyTrustedTypes {
1919
return {
2020
lessThan: /</g,
2121
allTags: /</g,
22-
allowAnchorLink: /<(?!a href=|\/a>)/ig,
22+
allowAnchorLink: /<(?!a href=|\/a>)/ig, // Negative lookahead assertion.
2323
notAnchorLink: /<(?!a href=|\/a>)/ig,
24+
allowAnchorListPlus: /<(?!a href=|\/a|\/?ul|\/?li|\/?p|\/?x|\/?code>)/ig,
2425
javascriptColon: /javascript:/ig,
2526
onEvent: /on(\w+)[ ]*=/ig,
2627
};
@@ -49,18 +50,22 @@ export class MyTrustedTypes {
4950

5051
this.#policies.push({
5152
id: 'allowAnchor',
52-
policy: trustedTypes.createPolicy('myEscapePolicy', {
53-
// createHTML: (string) => string.replace(/</g, "&lt;"),
54-
/* createHTML: (str) => str.replace(/on(\w+)=/ig, 'on-ZZ-$1=')
55-
.replace(/<(\/?)(script|object|embed|i?frame|style)/ig, '&lt;$1$2')
56-
.replace(/javascript:/ig, 'ZZ-scrip:'), */
53+
policy: trustedTypes.createPolicy('allowAnchor', {
54+
// createHTML: (str) => str.replace(/<(\/?)(script|object|embed|i?frame|style)/ig, '&lt;$1$2')
5755

5856
createHTML: (str) => str.replace(this.#RE.allowAnchorLink, this.#replace.lessThanEntity)
59-
// .replace(/<([^a][^ ]|\/[^a])/ig, '&lt;$1') // /<(?!(a|b) )/ig
60-
// .replace(/<\/([^a])/ig, '&lt;/$1')
6157
.replace(this.#RE.javascriptColon, this.#replace.cleanJavascriptColon)
6258
.replace(this.#RE.onEvent, this.#replace.cleanOnEvent)
6359
})
60+
});
61+
62+
this.#policies.push({
63+
id: 'allowAnchorListPlus',
64+
policy: trustedTypes.createPolicy('allowAnchorListPlus', {
65+
createHTML: (str) => str.replace(this.#RE.onEvent, this.#replace.cleanOnEvent)
66+
.replace(this.#RE.javascriptColon, this.#replace.cleanJavascriptColon)
67+
.replace(this.#RE.allowAnchorListPlus, this.#replace.lessThanEntity)
68+
})
6469
}); // push.
6570

6671
return this;

0 commit comments

Comments
 (0)