Files
dspace-angular/src/app/shared/utils/markdown.directive.ts
Kim Shepherd 8fb4772b6c Always sanitize HTML in dsMarkdown even if markdown disabled
Instead of setting innerHTML directly to value, sanitize
the value, even if not passing to renderMarkdown/Mathjax
2024-10-01 14:03:46 +02:00

98 lines
2.5 KiB
TypeScript

import {
Directive,
ElementRef,
Inject,
InjectionToken,
Input,
OnDestroy,
OnInit,
SecurityContext,
} from '@angular/core';
import {
DomSanitizer,
SafeHtml,
} from '@angular/platform-browser';
import { Subject } from 'rxjs';
import {
filter,
take,
takeUntil,
} from 'rxjs/operators';
import { environment } from '../../../environments/environment';
import { MathService } from '../../core/shared/math.service';
import { isEmpty } from '../empty.util';
const markdownItLoader = async () => (await import('markdown-it')).default;
type LazyMarkdownIt = ReturnType<typeof markdownItLoader>;
const MARKDOWN_IT = new InjectionToken<LazyMarkdownIt>(
'Lazily loaded MarkdownIt',
{ providedIn: 'root', factory: markdownItLoader },
);
@Directive({
selector: '[dsMarkdown]',
standalone: true,
})
export class MarkdownDirective implements OnInit, OnDestroy {
@Input() dsMarkdown: string;
private alive$ = new Subject<boolean>();
el: HTMLElement;
constructor(
@Inject(MARKDOWN_IT) private markdownIt: LazyMarkdownIt,
protected sanitizer: DomSanitizer,
private mathService: MathService,
private elementRef: ElementRef) {
this.el = elementRef.nativeElement;
}
ngOnInit() {
this.render(this.dsMarkdown);
}
async render(value: string, forcePreview = false): Promise<SafeHtml> {
if (isEmpty(value) || (!environment.markdown.enabled && !forcePreview)) {
this.el.innerHTML = this.sanitizer.sanitize(SecurityContext.HTML, value);
return;
} else {
if (environment.markdown.mathjax) {
this.renderMathjaxThenMarkdown(value);
} else {
this.renderMarkdown(value);
}
}
}
private renderMathjaxThenMarkdown(value: string) {
const sanitized = this.sanitizer.sanitize(SecurityContext.HTML, value);
this.el.innerHTML = sanitized;
this.mathService.ready().pipe(
filter((ready) => ready),
take(1),
takeUntil(this.alive$),
).subscribe(() => {
this.mathService.render(this.el)?.then(_ => {
this.renderMarkdown(this.el.innerHTML, true);
});
});
}
private async renderMarkdown(value: string, alreadySanitized = false) {
const MarkdownIt = await this.markdownIt;
const md = new MarkdownIt({
html: true,
linkify: true,
});
const html = alreadySanitized ? md.render(value) : this.sanitizer.sanitize(SecurityContext.HTML, md.render(value));
this.el.innerHTML = html;
}
ngOnDestroy() {
this.alive$.next(false);
}
}