diff --git a/aio/angular.json b/aio/angular.json index 513d7eef7f42..8d07acea8ef9 100644 --- a/aio/angular.json +++ b/aio/angular.json @@ -138,6 +138,12 @@ }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", + "options": { + "browserTarget": "site:build", + "headers": { + "Content-Security-Policy-Report-Only": "require-trusted-types-for 'script'; trusted-types angular angular#unsafe-bypass analytics google#safe" + } + }, "configurations": { "next": { "browserTarget": "site:build:next" diff --git a/aio/package.json b/aio/package.json index 1d2c4fc02375..b0841675eeed 100644 --- a/aio/package.json +++ b/aio/package.json @@ -98,8 +98,10 @@ "@angular/platform-browser-dynamic": "12.0.5", "@angular/router": "12.0.5", "@angular/service-worker": "12.0.5", + "@types/trusted-types": "^2.0.0", "@webcomponents/custom-elements": "1.4.3", "rxjs": "^6.6.7", + "safevalues": "^0.1.3", "tslib": "^2.2.0", "zone.js": "~0.11.4" }, diff --git a/aio/src/app/app.module.ts b/aio/src/app/app.module.ts index 4a27d5b5629f..ebeb33eb3639 100644 --- a/aio/src/app/app.module.ts +++ b/aio/src/app/app.module.ts @@ -1,3 +1,4 @@ +/// import { BrowserModule } from '@angular/platform-browser'; import { ErrorHandler, NgModule } from '@angular/core'; import { HttpClientModule } from '@angular/common/http'; @@ -46,6 +47,17 @@ import { SwUpdatesModule } from 'app/sw-updates/sw-updates.module'; import { environment } from '../environments/environment'; +import {scriptUrl} from 'safevalues'; +import {concatHtmls} from 'safevalues/builders/html_builders'; +import {assertIsTemplateObject} from 'safevalues/implementation/safe_string_literal'; + +import {htmlFromStringKnownToSatisfyTypeContract} from 'safevalues/unsafe/reviewed'; + +function svg(constantSvg: TemplateStringsArray): TrustedHTML { + assertIsTemplateObject(constantSvg, false, 'This needs to be static'); + return htmlFromStringKnownToSatisfyTypeContract(constantSvg[0], 'static SVG markup'); +} + // These are the hardcoded inline svg sources to be used by the `` component. // tslint:disable: max-line-length export const svgIconProviders = [ @@ -53,11 +65,12 @@ export const svgIconProviders = [ provide: SVG_ICONS, useValue: { name: 'close', - svgSource: - '' + - '' + - '' + - '', + svgSource: concatHtmls( + svg``, + svg``, + svg``, + svg``, + ), }, multi: true, }, @@ -65,11 +78,12 @@ export const svgIconProviders = [ provide: SVG_ICONS, useValue: { name: 'insert_comment', - svgSource: - '' + - '' + - '' + - '', + svgSource: concatHtmls( + svg``, + svg``, + svg``, + svg``, + ), }, multi: true, }, @@ -77,10 +91,11 @@ export const svgIconProviders = [ provide: SVG_ICONS, useValue: { name: 'keyboard_arrow_right', - svgSource: - '' + - '' + - '', + svgSource: concatHtmls( + svg``, + svg``, + svg``, + ), }, multi: true, }, @@ -88,10 +103,11 @@ export const svgIconProviders = [ provide: SVG_ICONS, useValue: { name: 'menu', - svgSource: - '' + - '' + - '', + svgSource: concatHtmls( + svg``, + svg``, + svg``, + ), }, multi: true, }, @@ -102,15 +118,16 @@ export const svgIconProviders = [ useValue: { namespace: 'logos', name: 'github', - svgSource: - '' + - '' + - '', + svgSource: concatHtmls( + svg``, + svg``, + svg``, + ), }, multi: true, }, @@ -119,14 +136,15 @@ export const svgIconProviders = [ useValue: { namespace: 'logos', name: 'twitter', - svgSource: - '' + - '' + - '', + svgSource: concatHtmls( + svg``, + svg``, + svg``, + ), }, multi: true, }, @@ -135,12 +153,13 @@ export const svgIconProviders = [ useValue: { namespace: 'logos', name: 'youtube', - svgSource: - '' + - '' + - '', + svgSource: concatHtmls( + svg``, + svg``, + svg``, + ), }, multi: true, }, @@ -160,7 +179,10 @@ export const svgIconProviders = [ MatToolbarModule, SwUpdatesModule, SharedModule, - ServiceWorkerModule.register('/ngsw-worker.js', {enabled: environment.production}), + ServiceWorkerModule.register( + // Make sure service worker is loaded with a TrustedScriptURL + scriptUrl`/ngsw-worker.js` as unknown as string, + {enabled: environment.production}), ], declarations: [ AppComponent, diff --git a/aio/src/app/custom-elements/code/code-example.component.ts b/aio/src/app/custom-elements/code/code-example.component.ts index c7219edac1fd..6314c2c25655 100644 --- a/aio/src/app/custom-elements/code/code-example.component.ts +++ b/aio/src/app/custom-elements/code/code-example.component.ts @@ -1,6 +1,7 @@ /* tslint:disable component-selector */ import { Component, HostBinding, ElementRef, ViewChild, Input, AfterViewInit } from '@angular/core'; import { CodeComponent } from './code.component'; +import { htmlFromStringKnownToSatisfyTypeContract } from 'safevalues/unsafe/reviewed'; /** * An embeddable code block that displays nicely formatted code. @@ -85,7 +86,8 @@ export class CodeExampleComponent implements AfterViewInit { ngAfterViewInit() { const contentElem = this.content.nativeElement; - this.aioCode.code = contentElem.innerHTML; - contentElem.innerHTML = ''; // Remove DOM nodes that are no longer needed. + this.aioCode.code = htmlFromStringKnownToSatisfyTypeContract( + contentElem.innerHTML, 'existing innerHTML content'); + contentElem.textContent = ''; // Remove DOM nodes that are no longer needed. } } diff --git a/aio/src/app/custom-elements/code/code-tabs.component.ts b/aio/src/app/custom-elements/code/code-tabs.component.ts index 3ed2cc6d1157..1aa78c6afcb3 100644 --- a/aio/src/app/custom-elements/code/code-tabs.component.ts +++ b/aio/src/app/custom-elements/code/code-tabs.component.ts @@ -1,10 +1,11 @@ /* tslint:disable component-selector */ import { AfterViewInit, Component, ElementRef, Input, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core'; +import {htmlFromStringKnownToSatisfyTypeContract} from 'safevalues/unsafe/reviewed'; import { CodeComponent } from './code.component'; export interface TabInfo { class: string; - code: string; + code: TrustedHTML; path: string; region: string; @@ -67,7 +68,7 @@ export class CodeTabsComponent implements OnInit, AfterViewInit { // NOTE: // In IE11, doing this also empties the `` nodes captured in `codeExamples` ¯\_(ツ)_/¯ // Only remove the unnecessary nodes after having captured the `` contents. - contentElem.innerHTML = ''; + contentElem.innerText = ''; } ngAfterViewInit() { @@ -80,7 +81,8 @@ export class CodeTabsComponent implements OnInit, AfterViewInit { private getTabInfo(tabContent: Element): TabInfo { return { class: tabContent.getAttribute('class') || '', - code: tabContent.innerHTML, + code: htmlFromStringKnownToSatisfyTypeContract( + tabContent.innerHTML, 'existing innerHTML content'), path: tabContent.getAttribute('path') || '', region: tabContent.getAttribute('region') || '', diff --git a/aio/src/app/custom-elements/code/code.component.ts b/aio/src/app/custom-elements/code/code.component.ts index a3b40df5aecd..2cb1db5c4e16 100644 --- a/aio/src/app/custom-elements/code/code.component.ts +++ b/aio/src/app/custom-elements/code/code.component.ts @@ -5,6 +5,8 @@ import { PrettyPrinter } from './pretty-printer.service'; import { MatSnackBar } from '@angular/material/snack-bar'; import { Observable, of } from 'rxjs'; import { tap } from 'rxjs/operators'; +import {htmlEscape, unwrapHtmlForSink} from 'safevalues'; +import {htmlFromStringKnownToSatisfyTypeContract} from 'safevalues/unsafe/reviewed'; /** * Formatted Code Block @@ -48,17 +50,19 @@ export class CodeComponent implements OnChanges { private codeText: string; /** Code that should be formatted with current inputs and displayed in the view. */ - set code(code: string) { + set code(code: TrustedHTML) { this._code = code; - if (!this._code || !this._code.trim()) { + if (!this._code.toString() || !this._code.toString().trim()) { this.showMissingCodeMessage(); } else { this.formatDisplayedCode(); } } - get code(): string { return this._code; } - _code: string; + get code(): TrustedHTML { + return this._code; + } + _code: TrustedHTML; /** Whether the copy button should be shown. */ @Input() hideCopy: boolean; @@ -130,15 +134,16 @@ export class CodeComponent implements OnChanges { /** Sets the message showing that the code could not be found. */ private showMissingCodeMessage() { const src = this.path ? this.path + (this.region ? '#' + this.region : '') : ''; - const srcMsg = src ? ` for\n${src}` : '.'; - this.setCodeHtml(`

The code sample is missing${srcMsg}

`); + const srcMsg = `The code sample is missing${src ? ` for\n${src}` : '.'}`; + this.setCodeHtml(htmlFromStringKnownToSatisfyTypeContract( + `

${htmlEscape(srcMsg)}

`, 'message is properly escaped')); } /** Sets the innerHTML of the code container to the provided code string. */ - private setCodeHtml(formattedCode: string) { + private setCodeHtml(formattedCode: TrustedHTML) { // **Security:** Code example content is provided by docs authors and as such its considered to // be safe for innerHTML purposes. - this.codeContainer.nativeElement.innerHTML = formattedCode; + this.codeContainer.nativeElement.innerHTML = unwrapHtmlForSink(formattedCode); } /** Gets the textContent of the displayed code element. */ @@ -176,10 +181,10 @@ export class CodeComponent implements OnChanges { } } -function leftAlign(text: string): string { +function leftAlign(text: TrustedHTML): TrustedHTML { let indent = Number.MAX_VALUE; - const lines = text.split('\n'); + const lines = text.toString().split('\n'); lines.forEach(line => { const lineIndent = line.search(/\S/); if (lineIndent !== -1) { @@ -187,5 +192,7 @@ function leftAlign(text: string): string { } }); - return lines.map(line => line.substr(indent)).join('\n').trim(); + return htmlFromStringKnownToSatisfyTypeContract( + lines.map(line => line.substr(indent)).join('\n').trim(), + 'safe manipulation of existing trusted HTML'); } diff --git a/aio/src/app/custom-elements/code/pretty-printer.service.ts b/aio/src/app/custom-elements/code/pretty-printer.service.ts index 0f234dd7871d..a25c05ed091f 100644 --- a/aio/src/app/custom-elements/code/pretty-printer.service.ts +++ b/aio/src/app/custom-elements/code/pretty-printer.service.ts @@ -1,11 +1,12 @@ import { Injectable } from '@angular/core'; +import { htmlFromStringKnownToSatisfyTypeContract } from 'safevalues/unsafe/reviewed'; import { from, Observable } from 'rxjs'; import { first, map, share } from 'rxjs/operators'; import { Logger } from 'app/shared/logger.service'; -type PrettyPrintOne = (code: string, language?: string, linenums?: number | boolean) => string; +type PrettyPrintOne = (code: TrustedHTML, language?: string, linenums?: number|boolean) => string; /** * Wrapper around the prettify.js library @@ -45,13 +46,14 @@ export class PrettyPrinter { * - number: do display but start at the given number * @returns Observable - Observable of formatted code */ - formatCode(code: string, language?: string, linenums?: number | boolean) { + formatCode(code: TrustedHTML, language?: string, linenums?: number|boolean) { return this.prettyPrintOne.pipe( map(ppo => { try { - return ppo(code, language, linenums); + return htmlFromStringKnownToSatisfyTypeContract( + ppo(code, language, linenums), 'prettify.js modifies already trusted HTML inline'); } catch (err) { - const msg = `Could not format code that begins '${code.substr(0, 50)}...'.`; + const msg = `Could not format code that begins '${code.toString().substr(0, 50)}...'.`; console.error(msg, err); throw new Error(msg); } diff --git a/aio/src/app/custom-elements/lazy-custom-element.component.ts b/aio/src/app/custom-elements/lazy-custom-element.component.ts index 1b1f4d4e2322..df4c8a989801 100644 --- a/aio/src/app/custom-elements/lazy-custom-element.component.ts +++ b/aio/src/app/custom-elements/lazy-custom-element.component.ts @@ -1,5 +1,7 @@ import { Component, ElementRef, Input, OnInit } from '@angular/core'; import { Logger } from 'app/shared/logger.service'; +import {unwrapHtmlForSink} from 'safevalues'; +import {htmlFromStringKnownToSatisfyTypeContract} from 'safevalues/unsafe/reviewed'; import { ElementsLoader } from './elements-loader'; @Component({ @@ -21,7 +23,9 @@ export class LazyCustomElementComponent implements OnInit { return; } - this.elementRef.nativeElement.innerHTML = `<${this.selector}>`; + this.elementRef.nativeElement.innerHTML = + unwrapHtmlForSink(htmlFromStringKnownToSatisfyTypeContract( + `<${this.selector}>`, 'selector is validated')); this.elementsLoader.loadCustomElement(this.selector); } } diff --git a/aio/src/app/documents/document-contents.ts b/aio/src/app/documents/document-contents.ts index ca0e398db425..db0a79b12f9c 100644 --- a/aio/src/app/documents/document-contents.ts +++ b/aio/src/app/documents/document-contents.ts @@ -1,6 +1,13 @@ -export interface DocumentContents { +export interface UnsafeDocumentContents { /** The unique identifier for this document */ id: string; /** The HTML to display in the doc viewer */ contents: string|null; } + +export interface DocumentContents { + /** The unique identifier for this document */ + id: string; + /** The HTML to display in the doc viewer */ + contents: TrustedHTML|null; +} diff --git a/aio/src/app/documents/document.service.ts b/aio/src/app/documents/document.service.ts index 872f8b62057b..935bf4585883 100644 --- a/aio/src/app/documents/document.service.ts +++ b/aio/src/app/documents/document.service.ts @@ -2,32 +2,33 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { AsyncSubject, Observable, of } from 'rxjs'; -import { catchError, switchMap, tap } from 'rxjs/operators'; +import { catchError, switchMap, tap, map } from 'rxjs/operators'; -import { DocumentContents } from './document-contents'; +import { DocumentContents, UnsafeDocumentContents } from './document-contents'; export { DocumentContents } from './document-contents'; import { LocationService } from 'app/shared/location.service'; import { Logger } from 'app/shared/logger.service'; +import {htmlEscape} from 'safevalues'; +import {htmlFromStringKnownToSatisfyTypeContract} from 'safevalues/unsafe/reviewed'; export const FILE_NOT_FOUND_ID = 'file-not-found'; export const FETCHING_ERROR_ID = 'fetching-error'; export const CONTENT_URL_PREFIX = 'generated/'; export const DOC_CONTENT_URL_PREFIX = CONTENT_URL_PREFIX + 'docs/'; -const FETCHING_ERROR_CONTENTS = (path: string) => ` +const FETCHING_ERROR_CONTENTS = (path: string) => htmlFromStringKnownToSatisfyTypeContract(`
error_outline

Request for document failed

- We are unable to retrieve the "${path}" page at this time. -
+ We are unable to retrieve the "${htmlEscape(path)}" page at this time. Please check your connection and try again later.

-`; +`, 'inline HTML with interpolations escaped'); @Injectable() export class DocumentService { @@ -58,20 +59,28 @@ export class DocumentService { const subject = new AsyncSubject(); this.logger.log('fetching document from', requestPath); - this.http - .get(requestPath, {responseType: 'json'}) - .pipe( - tap(data => { - if (!data || typeof data !== 'object') { - this.logger.log('received invalid data:', data); - throw Error('Invalid data'); - } - }), - catchError((error: HttpErrorResponse) => { - return error.status === 404 ? this.getFileNotFoundDoc(id) : this.getErrorDoc(id, error); - }), - ) - .subscribe(subject); + this.http.get(requestPath, {responseType: 'json'}) + .pipe( + tap(data => { + if (!data || typeof data !== 'object') { + this.logger.log('received invalid data:', data); + throw Error('Invalid data'); + } + }), + map(data => ({ + id: data.id, + contents: data.contents === null ? + null : + htmlFromStringKnownToSatisfyTypeContract( + data.contents, + 'HTML is authored by the documentation team and is fetched directly from the server') + })), + catchError((error: HttpErrorResponse) => { + return error.status === 404 ? this.getFileNotFoundDoc(id) : + this.getErrorDoc(id, error); + }), + ) + .subscribe(subject); return subject.asObservable(); } @@ -84,7 +93,7 @@ export class DocumentService { } else { return of({ id: FILE_NOT_FOUND_ID, - contents: 'Document not found' + contents: htmlFromStringKnownToSatisfyTypeContract('Document not found', 'inline HTML') }); } } diff --git a/aio/src/app/layout/doc-viewer/doc-viewer.component.ts b/aio/src/app/layout/doc-viewer/doc-viewer.component.ts index 26c0528a843a..0271410c50cf 100644 --- a/aio/src/app/layout/doc-viewer/doc-viewer.component.ts +++ b/aio/src/app/layout/doc-viewer/doc-viewer.component.ts @@ -8,6 +8,8 @@ import { DocumentContents, FILE_NOT_FOUND_ID, FETCHING_ERROR_ID } from 'app/docu import { Logger } from 'app/shared/logger.service'; import { TocService } from 'app/shared/toc.service'; import { ElementsLoader } from 'app/custom-elements/elements-loader'; +import {unwrapHtmlForSink} from 'safevalues'; +import {htmlFromStringKnownToSatisfyTypeContract} from 'safevalues/unsafe/reviewed'; // Constants @@ -15,7 +17,8 @@ export const NO_ANIMATIONS = 'no-animations'; // Initialization prevents flicker once pre-rendering is on const initialDocViewerElement = document.querySelector('aio-doc-viewer'); -const initialDocViewerContent = initialDocViewerElement ? initialDocViewerElement.innerHTML : ''; +const initialDocViewerContent = htmlFromStringKnownToSatisfyTypeContract( + initialDocViewerElement?.innerHTML ?? '', 'existing innerHTML content'); @Component({ selector: 'aio-doc-viewer', @@ -69,8 +72,9 @@ export class DocViewerComponent implements OnDestroy { private tocService: TocService, private elementsLoader: ElementsLoader) { this.hostElement = elementRef.nativeElement; + // Security: the initialDocViewerContent comes from the prerendered DOM and is considered to be secure - this.hostElement.innerHTML = initialDocViewerContent; + this.hostElement.innerHTML = unwrapHtmlForSink(initialDocViewerContent); if (this.hostElement.firstElementChild) { this.currViewContainer = this.hostElement.firstElementChild as HTMLElement; @@ -100,7 +104,10 @@ export class DocViewerComponent implements OnDestroy { if (titleEl && needsToc && !embeddedToc) { // Add an embedded ToC if it's needed and there isn't one in the content already. - titleEl.insertAdjacentHTML('afterend', ''); + titleEl.insertAdjacentHTML( + 'afterend', + unwrapHtmlForSink(htmlFromStringKnownToSatisfyTypeContract( + '', 'constant HTML'))); } else if (!needsToc && embeddedToc && embeddedToc.parentNode !== null) { // Remove the embedded Toc if it's there and not needed. // We cannot use ChildNode.remove() because of IE11 @@ -134,9 +141,15 @@ export class DocViewerComponent implements OnDestroy { this.setNoIndex(doc.id === FILE_NOT_FOUND_ID || doc.id === FETCHING_ERROR_ID); return this.void$.pipe( - // Security: `doc.contents` is always authored by the documentation team - // and is considered to be safe. - tap(() => this.nextViewContainer.innerHTML = doc.contents || ''), + tap(() => { + if (doc.contents === null) { + this.nextViewContainer.textContent = ''; + } else { + // Security: `doc.contents` is always authored by the documentation team + // and is considered to be safe. + this.nextViewContainer.innerHTML = unwrapHtmlForSink(doc.contents); + } + }), tap(() => addTitleAndToc = this.prepareTitleAndToc(this.nextViewContainer, doc.id)), switchMap(() => this.elementsLoader.loadContainedCustomElements(this.nextViewContainer)), tap(() => this.docReady.emit()), @@ -145,7 +158,7 @@ export class DocViewerComponent implements OnDestroy { catchError(err => { const errorMessage = `${(err instanceof Error) ? err.stack : err}`; this.logger.error(new Error(`[DocViewer] Error preparing document '${doc.id}': ${errorMessage}`)); - this.nextViewContainer.innerHTML = ''; + this.nextViewContainer.textContent = ''; this.setNoIndex(true); // TODO(gkalpak): Remove this once gathering debug info is no longer needed. @@ -247,7 +260,7 @@ export class DocViewerComponent implements OnDestroy { const prevViewContainer = this.currViewContainer; this.currViewContainer = this.nextViewContainer; this.nextViewContainer = prevViewContainer; - this.nextViewContainer.innerHTML = ''; // Empty to release memory. + this.nextViewContainer.textContent = ''; // Empty to release memory. }), ); } diff --git a/aio/src/app/search/search.service.ts b/aio/src/app/search/search.service.ts index 280ffc714177..7c01db459f12 100644 --- a/aio/src/app/search/search.service.ts +++ b/aio/src/app/search/search.service.ts @@ -3,6 +3,7 @@ import { ConnectableObservable, Observable, race, ReplaySubject, timer } from 'r import { concatMap, first, publishReplay } from 'rxjs/operators'; import { WebWorkerClient } from 'app/shared/web-worker'; import { SearchResults } from 'app/search/interfaces'; +import { scriptUrl, unwrapScriptUrlForSink } from 'safevalues'; @Injectable() export class SearchService { @@ -27,8 +28,7 @@ export class SearchService { .pipe( concatMap(() => { // Create the worker and load the index - // tslint:disable-next-line: whitespace - const worker = new Worker(new URL('./search.worker', import.meta.url), { type: 'module' }); + const worker = new Worker(unwrapScriptUrlForSink(scriptUrl`./search.worker`), { type: 'module'}); this.worker = WebWorkerClient.create(worker, this.zone); return this.worker.sendMessage('load-index'); }), diff --git a/aio/src/app/shared/custom-icon-registry.ts b/aio/src/app/shared/custom-icon-registry.ts index 8ad73b2a2919..930a67cdd4cc 100644 --- a/aio/src/app/shared/custom-icon-registry.ts +++ b/aio/src/app/shared/custom-icon-registry.ts @@ -4,6 +4,7 @@ import { of } from 'rxjs'; import { MatIconRegistry } from '@angular/material/icon'; import { HttpClient } from '@angular/common/http'; import { DomSanitizer } from '@angular/platform-browser'; +import {unwrapHtmlForSink} from 'safevalues'; /** * Use SVG_ICONS (and SvgIconInfo) as "multi" providers to provide the SVG source @@ -22,7 +23,7 @@ export const SVG_ICONS = new InjectionToken>('SvgIcons'); export interface SvgIconInfo { namespace?: string; name: string; - svgSource: string; + svgSource: TrustedHTML; } interface SvgIconMap { @@ -76,7 +77,7 @@ export class CustomIconRegistry extends MatIconRegistry { const div = document.createElement('DIV'); // SECURITY: the source for the SVG icons is provided in code by trusted developers - div.innerHTML = svgIcon.svgSource; + div.innerHTML = unwrapHtmlForSink(svgIcon.svgSource); const svgElement = div.querySelector('svg') as SVGElement; nsIconMap[svgIcon.name] = svgElement; diff --git a/aio/src/app/shared/toc.service.ts b/aio/src/app/shared/toc.service.ts index aea59adc969a..7928fe101117 100644 --- a/aio/src/app/shared/toc.service.ts +++ b/aio/src/app/shared/toc.service.ts @@ -3,6 +3,8 @@ import { Inject, Injectable } from '@angular/core'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { ReplaySubject } from 'rxjs'; import { ScrollSpyInfo, ScrollSpyService } from 'app/shared/scroll-spy.service'; +import {unwrapHtmlForSink} from 'safevalues'; +import {htmlFromStringKnownToSatisfyTypeContract} from 'safevalues/unsafe/reviewed'; export interface TocItem { @@ -62,7 +64,8 @@ export class TocService { // - Mark the HTML as trusted to be used with `[innerHTML]`. private extractHeadingSafeHtml(heading: HTMLHeadingElement) { const div: HTMLDivElement = this.document.createElement('div'); - div.innerHTML = heading.innerHTML; + div.innerHTML = unwrapHtmlForSink( + htmlFromStringKnownToSatisfyTypeContract(heading.innerHTML, 'existing innerHTML content')); // Remove any `.github-links` or `.header-link` elements (along with their content). querySelectorAll(div, '.github-links, .header-link').forEach(removeNode); diff --git a/aio/src/assets/js/prettify.js b/aio/src/assets/js/prettify.js index 3b74b5bdaa3d..de6d24bc000b 100755 --- a/aio/src/assets/js/prettify.js +++ b/aio/src/assets/js/prettify.js @@ -39,8 +39,8 @@ S=/^(DIR|FILE|array|vector|(de|priority_)?queue|(forward_)?list|stack|(const_)?( "\"'"]],[["tag",/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],["pun",/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);t(G([],[["atv",/^[\s\S]+/]]),["uq.val"]);t(y({keywords:H, hashComments:!0,cStyleComments:!0,types:S}),"c cc cpp cxx cyc m".split(" "));t(y({keywords:"null,true,false"}),["json"]);t(y({keywords:P,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:S}),["cs"]);t(y({keywords:O,cStyleComments:!0}),["java"]);t(y({keywords:C,hashComments:!0,multiLineStrings:!0}),["bash","bsh","csh","sh"]);t(y({keywords:Q,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}),["cv","py","python"]);t(y({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END", hashComments:!0,multiLineStrings:!0,regexLiterals:2}),["perl","pl","pm"]);t(y({keywords:R,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb","ruby"]);t(y({keywords:F,cStyleComments:!0,regexLiterals:!0}),["javascript","js","ts","typescript"]);t(y({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,throw,true,try,unless,until,when,while,yes",hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0, -regexLiterals:!0}),["coffee"]);t(G([],[["str",/^[\s\S]+/]]),["regex"]);var Y=E.PR={createSimpleLexer:G,registerLangHandler:t,sourceDecorator:y,PR_ATTRIB_NAME:"atn",PR_ATTRIB_VALUE:"atv",PR_COMMENT:"com",PR_DECLARATION:"dec",PR_KEYWORD:"kwd",PR_LITERAL:"lit",PR_NOCODE:"nocode",PR_PLAIN:"pln",PR_PUNCTUATION:"pun",PR_SOURCE:"src",PR_STRING:"str",PR_TAG:"tag",PR_TYPE:"typ",prettyPrintOne:E.prettyPrintOne=function(a,d,f){f=f||!1;d=d||null;var b=document.createElement("div");b.innerHTML="
"+a+"
"; -b=b.firstChild;f&&L(b,f,!0);M({j:d,m:f,h:b,l:1,a:null,i:null,c:null,g:null});return b.innerHTML},prettyPrint:E.prettyPrint=function(a,d){function f(){for(var b=E.PR_SHOULD_USE_CONTINUATION?c.now()+250:Infinity;p diff --git a/aio/yarn.lock b/aio/yarn.lock index ec39a857a253..0221121cd2aa 100644 --- a/aio/yarn.lock +++ b/aio/yarn.lock @@ -1766,6 +1766,11 @@ resolved "https://registry.yarnpkg.com/@types/stemmer/-/stemmer-1.0.2.tgz#bd8354f50b3c9b87c351d169240e45cf1fa1f5e8" integrity sha512-2gWEIFqVZjjZxo8/TcugCAl7nW9Jd9ArEDpTAc5nH7d+ZUkreHA7GzuFcLZ0sflLrA5b1PZ+2yDyHJcuP9KWWw== +"@types/trusted-types@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.0.tgz#aee6e868fcef74f2b8c71614b6df81a601a42f17" + integrity sha512-I8MnZqNXsOLHsU111oHbn3khtvKMi5Bn4qVFsIWSJcCP1KKDiXX5AEw8UPk0nSopeC+Hvxt6yAy1/a5PailFqg== + "@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e" @@ -10866,6 +10871,11 @@ safeps@7.0.1: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +safevalues@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/safevalues/-/safevalues-0.1.3.tgz#24444ce57bf22bccd62113e79dadec7cdc34976d" + integrity sha512-eWVnJAw0QKjJAniL5re5DTBvcAbwmJ5o+qN1R93dcxSjdFBzyKvfLf/5EUZ7zt+FG58sTfoI0QRu+HZTJFg6Iw== + sass-loader@11.0.1: version "11.0.1" resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-11.0.1.tgz#8672f896593466573b904f47693e0695368e38c9"