mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
docs(docs-infra): sanitize markdown tooltip in Code editor
(cherry picked from commit 0f960a5514)
This commit is contained in:
parent
3c41e74fdd
commit
9ea8cb6eea
3 changed files with 72 additions and 10 deletions
|
|
@ -19,6 +19,8 @@ import {NodeRuntimeSandbox} from '../node-runtime-sandbox.service';
|
|||
import {TypingsLoader} from '../typings-loader.service';
|
||||
|
||||
import {FileAndContentRecord} from '@angular/docs';
|
||||
import {DomSanitizer} from '@angular/platform-browser';
|
||||
import {NodeRuntimeState} from '../node-runtime-state.service';
|
||||
import {CODE_EDITOR_EXTENSIONS} from './constants/code-editor-extensions';
|
||||
import {LANGUAGES} from './constants/code-editor-languages';
|
||||
import {getAutocompleteExtension} from './extensions/autocomplete';
|
||||
|
|
@ -26,10 +28,9 @@ import {getDiagnosticsExtension} from './extensions/diagnostics';
|
|||
import {getTooltipExtension} from './extensions/tooltip';
|
||||
import {DiagnosticsState} from './services/diagnostics-state.service';
|
||||
import {TsVfsWorkerActions} from './workers/enums/actions';
|
||||
import {TYPESCRIPT_VFS_WORKER_FACTORY} from './workers/factory-provider';
|
||||
import {CodeChangeRequest} from './workers/interfaces/code-change-request';
|
||||
import {ActionMessage} from './workers/interfaces/message';
|
||||
import {NodeRuntimeState} from '../node-runtime-state.service';
|
||||
import {TYPESCRIPT_VFS_WORKER_FACTORY} from './workers/factory-provider';
|
||||
|
||||
export interface EditorFile {
|
||||
filename: string;
|
||||
|
|
@ -81,6 +82,7 @@ export class CodeMirrorEditor {
|
|||
private readonly typingsLoader = inject(TypingsLoader);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly diagnosticsState = inject(DiagnosticsState);
|
||||
private readonly domSanitizer = inject(DomSanitizer);
|
||||
private readonly tsVfsWorkerFactory = inject(TYPESCRIPT_VFS_WORKER_FACTORY);
|
||||
private tsVfsWorker: Worker | null = null;
|
||||
|
||||
|
|
@ -448,7 +450,12 @@ export class CodeMirrorEditor {
|
|||
this.sendRequestToTsVfs,
|
||||
this.diagnosticsState,
|
||||
),
|
||||
getTooltipExtension(this.eventManager$, this.currentFile, this.sendRequestToTsVfs),
|
||||
getTooltipExtension(
|
||||
this.eventManager$,
|
||||
this.currentFile,
|
||||
this.sendRequestToTsVfs,
|
||||
this.domSanitizer,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
41
adev/src/app/editor/code-editor/extensions/tooltip.spec.ts
Normal file
41
adev/src/app/editor/code-editor/extensions/tooltip.spec.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {DomSanitizer} from '@angular/platform-browser';
|
||||
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {getMarkedHtmlFromString, getTagsHtml} from './tooltip';
|
||||
|
||||
describe('getMarkedHtmlFromString', () => {
|
||||
it('sanitizes markdown HTML content before assigning to innerHTML', () => {
|
||||
const markdownContent = 'hello <img src=x onerror="alert(1)" />';
|
||||
const domSanitizer = TestBed.inject(DomSanitizer);
|
||||
|
||||
const result = getMarkedHtmlFromString(markdownContent, domSanitizer);
|
||||
|
||||
expect(result.innerHTML.trim()).toBe('<p>hello <img src="x"></p>');
|
||||
expect(result.innerHTML).not.toContain('onerror');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTagsHtml', () => {
|
||||
it('sanitizes JSDoc tag content before assigning to innerHTML', () => {
|
||||
const tags = [
|
||||
{
|
||||
name: 'example',
|
||||
text: [{text: 'hello <img src=x onerror="alert(1)" />'}],
|
||||
},
|
||||
] as any[];
|
||||
const domSanitizer = TestBed.inject(DomSanitizer);
|
||||
|
||||
const result = getTagsHtml(tags, domSanitizer);
|
||||
|
||||
expect(result.innerHTML).toContain('@example');
|
||||
expect(result.innerHTML).not.toContain('onerror');
|
||||
});
|
||||
});
|
||||
|
|
@ -6,7 +6,9 @@
|
|||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {Signal} from '@angular/core';
|
||||
import {SecurityContext, Signal} from '@angular/core';
|
||||
import {DomSanitizer} from '@angular/platform-browser';
|
||||
|
||||
import {Tooltip, hoverTooltip} from '@codemirror/view';
|
||||
import {marked} from 'marked';
|
||||
import {Subject, filter, take} from 'rxjs';
|
||||
|
|
@ -23,6 +25,7 @@ export const getTooltipExtension = (
|
|||
emitter: Subject<ActionMessage<DisplayTooltipResponse>>,
|
||||
currentFile: Signal<EditorFile>,
|
||||
sendRequestToTsVfs: (request: ActionMessage<DisplayTooltipRequest>) => void,
|
||||
domSanitizer: DomSanitizer,
|
||||
) => {
|
||||
return hoverTooltip(
|
||||
async (_, pos: number): Promise<Tooltip | null> => {
|
||||
|
|
@ -58,9 +61,9 @@ export const getTooltipExtension = (
|
|||
|
||||
// use documentation if available as it's more informative than tags
|
||||
if (documentation?.[0]?.text) {
|
||||
tooltip.appendChild(getMarkedHtmlFromString(documentation[0]?.text));
|
||||
tooltip.appendChild(getMarkedHtmlFromString(documentation[0]?.text, domSanitizer));
|
||||
} else if (tags?.length) {
|
||||
tooltip.appendChild(getTagsHtml(tags));
|
||||
tooltip.appendChild(getTagsHtml(tags, domSanitizer));
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -91,9 +94,13 @@ function forceTooltipScrollTop() {
|
|||
}
|
||||
}
|
||||
|
||||
function getMarkedHtmlFromString(content: string): HTMLDivElement {
|
||||
export function getMarkedHtmlFromString(
|
||||
content: string,
|
||||
domSanitizer: DomSanitizer,
|
||||
): HTMLDivElement {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.innerHTML = marked(content) as string;
|
||||
const sanitizedHtml = renderAndSanitizeMarkdownToHtml(content, domSanitizer);
|
||||
wrapper.innerHTML = sanitizedHtml;
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
|
@ -123,7 +130,7 @@ function getHtmlFromDisplayParts(displayParts: ts.SymbolDisplayPart[]): HTMLDivE
|
|||
return wrapper;
|
||||
}
|
||||
|
||||
function getTagsHtml(tags: ts.JSDocTagInfo[]): HTMLDivElement {
|
||||
export function getTagsHtml(tags: ts.JSDocTagInfo[], domSanitizer: DomSanitizer): HTMLDivElement {
|
||||
const tagsWrapper = document.createElement('div');
|
||||
|
||||
let contentString = '';
|
||||
|
|
@ -138,7 +145,14 @@ function getTagsHtml(tags: ts.JSDocTagInfo[]): HTMLDivElement {
|
|||
}
|
||||
}
|
||||
|
||||
tagsWrapper.innerHTML = marked(contentString) as string;
|
||||
const sanitizedHtml = renderAndSanitizeMarkdownToHtml(contentString, domSanitizer);
|
||||
tagsWrapper.innerHTML = sanitizedHtml;
|
||||
|
||||
return tagsWrapper;
|
||||
}
|
||||
|
||||
function renderAndSanitizeMarkdownToHtml(content: string, domSanitizer: DomSanitizer): string {
|
||||
const markedHtml = marked(content) as string;
|
||||
const sanitizedHtml = domSanitizer.sanitize(SecurityContext.HTML, markedHtml) ?? '';
|
||||
return sanitizedHtml;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue