Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit cb00190

Browse files
committed
add csp trusted types policy
1 parent f3c8e75 commit cb00190

File tree

5 files changed

+140
-42
lines changed

5 files changed

+140
-42
lines changed

README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,65 @@ completer.addEventListener('auto-complete-change', function(event) {
120120
})
121121
```
122122
123+
### CSP Trusted Types
124+
125+
You can call
126+
`setCSPTrustedTypesPolicy(policy: TrustedTypePolicy | Promise<TrustedTypePolicy> | null)`
127+
from JavaScript to set a
128+
[CSP trusted types policy](https://web.dev/trusted-types/), which can perform
129+
(synchronous) filtering or rejection of the `fetch` response before it is
130+
inserted into the page:
131+
132+
```ts
133+
import AutoCompleteElement from 'auto-complete-element'
134+
import DOMPurify from 'dompurify' // Using https://github.com/cure53/DOMPurify
135+
136+
// This policy removes all HTML markup except links.
137+
const policy = trustedTypes.createPolicy('links-only', {
138+
createHTML: (htmlText: string) => {
139+
return DOMPurify.sanitize(htmlText, {
140+
ALLOWED_TAGS: ['a'],
141+
ALLOWED_ATTR: ['href'],
142+
RETURN_TRUSTED_TYPE: true
143+
})
144+
}
145+
})
146+
AutoCompleteElement.setCSPTrustedTypesPolicy(policy)
147+
```
148+
149+
The policy has access to the `fetch` response object. Due to platform
150+
constraints, only synchronous information from the response (in addition to the
151+
HTML text body) can be used in the policy:
152+
153+
```ts
154+
import AutoCompleteElement from 'auto-complete-element'
155+
156+
const policy = trustedTypes.createPolicy('require-server-header', {
157+
createHTML: (htmlText: string, response: Response) => {
158+
if (response.headers.get('X-Server-Sanitized') !== 'sanitized=true') {
159+
// Note: this will reject the contents, but the error may be caught before it shows in the JS console.
160+
throw new Error('Rejecting HTML that was not marked by the server as sanitized.')
161+
}
162+
return htmlText
163+
}
164+
})
165+
AutoCompleteElement.setCSPTrustedTypesPolicy(policy)
166+
```
167+
168+
Note that:
169+
170+
- Only a single policy can be set, shared by all `AutoCompleteElement` fetches.
171+
- You should call `setCSPTrustedTypesPolicy()` ahead of any other load of
172+
`auto-complete` element in your code.
173+
- If your policy itself requires asynchronous work to construct, you can also
174+
pass a `Promise<TrustedTypePolicy>`.
175+
- Pass `null` to remove the policy.
176+
- Not all browsers
177+
[support the trusted types API in JavaScript](https://caniuse.com/mdn-api_trustedtypes).
178+
You may want to use the
179+
[recommended tinyfill](https://github.com/w3c/trusted-types#tinyfill) to
180+
construct a policy without causing issues in other browsers.
181+
123182
## Browser support
124183
125184
Browsers without native [custom element support][support] require a [polyfill][].

src/auto-complete-element.ts

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,27 @@
11
import Autocomplete from './autocomplete.js'
22
import AutocompleteEvent from './auto-complete-event.js'
3-
import {fragment} from './send.js'
43

54
const state = new WeakMap()
65

6+
export interface CSPTrustedTypesPolicy {
7+
createHTML: (s: string, response: Response) => CSPTrustedHTMLToStringable
8+
}
9+
10+
// Note: basically every object (and some primitives) in JS satisfy this
11+
// `CSPTrustedHTMLToStringable` interface, but this is the most compatible shape
12+
// we can use.
13+
interface CSPTrustedHTMLToStringable {
14+
toString: () => string
15+
}
16+
17+
let cspTrustedTypesPolicyPromise: Promise<CSPTrustedTypesPolicy> | null = null
18+
719
// eslint-disable-next-line custom-elements/file-name-matches-element
820
export default class AutocompleteElement extends HTMLElement {
21+
static setCSPTrustedTypesPolicy(policy: CSPTrustedTypesPolicy | Promise<CSPTrustedTypesPolicy> | null): void {
22+
cspTrustedTypesPolicyPromise = policy === null ? policy : Promise.resolve(policy)
23+
}
24+
925
#forElement: HTMLElement | null = null
1026
get forElement(): HTMLElement | null {
1127
if (this.#forElement?.isConnected) {
@@ -87,6 +103,7 @@ export default class AutocompleteElement extends HTMLElement {
87103
}
88104
}
89105

106+
// HEAD
90107
get fetchOnEmpty(): boolean {
91108
return this.hasAttribute('fetch-on-empty')
92109
}
@@ -96,7 +113,27 @@ export default class AutocompleteElement extends HTMLElement {
96113
}
97114

98115
fetchResult = fragment
99-
116+
//
117+
#requestController?: AbortController
118+
async fetchResult(url: URL): Promise<string | CSPTrustedHTMLToStringable> {
119+
this.#requestController?.abort()
120+
const {signal} = (this.#requestController = new AbortController())
121+
const res = await fetch(url.toString(), {
122+
signal,
123+
headers: {
124+
Accept: 'text/fragment+html',
125+
},
126+
})
127+
if (!res.ok) {
128+
throw new Error(await res.text())
129+
}
130+
if (cspTrustedTypesPolicyPromise) {
131+
const cspTrustedTypesPolicy = await cspTrustedTypesPolicyPromise
132+
return cspTrustedTypesPolicy.createHTML(await res.text(), res)
133+
}
134+
return await res.text()
135+
}
136+
//f21528e (add csp trusted types policy)
100137
static get observedAttributes(): string[] {
101138
return ['open', 'value', 'for']
102139
}
@@ -122,8 +159,8 @@ export default class AutocompleteElement extends HTMLElement {
122159
this.dispatchEvent(
123160
new AutocompleteEvent('auto-complete-change', {
124161
bubbles: true,
125-
relatedTarget: autocomplete.input
126-
})
162+
relatedTarget: autocomplete.input,
163+
}),
127164
)
128165
break
129166
}

src/autocomplete.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,10 +202,10 @@ export default class Autocomplete {
202202

203203
this.container.dispatchEvent(new CustomEvent('loadstart'))
204204
this.container
205-
.fetchResult(this.input, url.toString())
205+
.fetchResult(url)
206206
.then(html => {
207207
// eslint-disable-next-line github/no-inner-html
208-
this.results.innerHTML = html
208+
this.results.innerHTML = html as string
209209
this.identifyOptions()
210210
const allNewOptions = this.results.querySelectorAll('[role="option"]')
211211
const hasResults = !!allNewOptions.length

src/send.ts

Lines changed: 0 additions & 33 deletions
This file was deleted.

test/test.js

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ describe('auto-complete element', function () {
6767
value = event.target.value
6868
relatedTarget = event.relatedTarget
6969
},
70-
{once: true}
70+
{once: true},
7171
)
7272

7373
assert.isTrue(keydown(input, 'Enter'))
@@ -370,6 +370,41 @@ describe('auto-complete element', function () {
370370
assert.equal(5, list.children.length)
371371
})
372372
})
373+
374+
describe('trustedHTML', () => {
375+
beforeEach(function () {
376+
document.body.innerHTML = `
377+
<div id="mocha-fixture">
378+
<auto-complete src="/search" for="popup" data-autoselect="true">
379+
<input type="text">
380+
<ul id="popup"></ul>
381+
<div id="popup-feedback"></div>
382+
</auto-complete>
383+
</div>
384+
`
385+
})
386+
387+
it('calls trusted types policy, passing response to it', async function () {
388+
const calls = []
389+
const html = '<li><strong>replacement</strong></li>'
390+
window.AutocompleteElement.setCSPTrustedTypesPolicy({
391+
createHTML(str, res) {
392+
calls.push([str, res])
393+
return html
394+
},
395+
})
396+
const container = document.querySelector('auto-complete')
397+
const popup = container.querySelector('#popup')
398+
const input = container.querySelector('input')
399+
400+
triggerInput(input, 'hub')
401+
await once(container, 'loadend')
402+
403+
assert.equal(calls.length, 1)
404+
assert.equal(popup.children.length, 1)
405+
assert.equal(popup.innerHTML, html)
406+
})
407+
})
373408
})
374409

375410
function waitForElementToChange(el) {
@@ -402,15 +437,15 @@ const keyCodes = {
402437
ArrowUp: 38,
403438
Enter: 13,
404439
Escape: 27,
405-
Tab: 9
440+
Tab: 9,
406441
}
407442

408443
function keydown(element, key, alt = false) {
409444
const e = {
410445
shiftKey: false,
411446
altKey: alt,
412447
ctrlKey: false,
413-
metaKey: false
448+
metaKey: false,
414449
}
415450

416451
key = key.replace(/\b(Ctrl|Alt|Meta)\+/g, function (_, type) {

0 commit comments

Comments
 (0)