/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as path from 'path'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import { Logger } from '../logger'; import { MarkdownEngine } from '../markdownEngine'; import { MarkdownContributionProvider } from '../markdownExtensions'; import { ContentSecurityPolicyArbiter, MarkdownPreviewSecurityLevel } from '../security'; import { WebviewResourceProvider } from '../util/resources'; import { MarkdownPreviewConfiguration, MarkdownPreviewConfigurationManager } from './previewConfig'; const localize = nls.loadMessageBundle(); /** * Strings used inside the markdown preview. * * Stored here and then injected in the preview so that they * can be localized using our normal localization process. */ const previewStrings = { cspAlertMessageText: localize( 'preview.securityMessage.text', 'Some content has been disabled in this document'), cspAlertMessageTitle: localize( 'preview.securityMessage.title', 'Potentially unsafe or insecure content has been disabled in the markdown preview. Change the Markdown preview security setting to allow insecure content or enable scripts'), cspAlertMessageLabel: localize( 'preview.securityMessage.label', 'Content Disabled Security Warning') }; function escapeAttribute(value: string | vscode.Uri): string { return value.toString().replace(/"/g, '"'); } export interface MarkdownContentProviderOutput { html: string; containingImages: { src: string }[]; } export class MarkdownContentProvider { constructor( private readonly engine: MarkdownEngine, private readonly context: vscode.ExtensionContext, private readonly cspArbiter: ContentSecurityPolicyArbiter, private readonly contributionProvider: MarkdownContributionProvider, private readonly logger: Logger ) { } public async provideTextDocumentContent( markdownDocument: vscode.TextDocument, resourceProvider: WebviewResourceProvider, previewConfigurations: MarkdownPreviewConfigurationManager, initialLine: number | undefined = undefined, state?: any ): Promise { const sourceUri = markdownDocument.uri; const config = previewConfigurations.loadAndCacheConfiguration(sourceUri); const initialData = { source: sourceUri.toString(), fragment: state?.fragment || markdownDocument.uri.fragment || undefined, line: initialLine, lineCount: markdownDocument.lineCount, scrollPreviewWithEditor: config.scrollPreviewWithEditor, scrollEditorWithPreview: config.scrollEditorWithPreview, doubleClickToSwitchToEditor: config.doubleClickToSwitchToEditor, disableSecurityWarnings: this.cspArbiter.shouldDisableSecurityWarnings(), webviewResourceRoot: resourceProvider.asWebviewUri(markdownDocument.uri).toString(), }; this.logger.log('provideTextDocumentContent', initialData); // Content Security Policy const nonce = new Date().getTime() + '' + new Date().getMilliseconds(); const csp = this.getCsp(resourceProvider, sourceUri, nonce); const body = await this.engine.render(markdownDocument); const html = ` ${csp} ${this.getStyles(resourceProvider, sourceUri, config, state)} ${body.html}
${this.getScripts(resourceProvider, nonce)} `; return { html, containingImages: body.containingImages, }; } public provideFileNotFoundContent( resource: vscode.Uri, ): string { const resourcePath = path.basename(resource.fsPath); const body = localize('preview.notFound', '{0} cannot be found', resourcePath); return ` ${body} `; } private extensionResourcePath(resourceProvider: WebviewResourceProvider, mediaFile: string): string { const webviewResource = resourceProvider.asWebviewUri( vscode.Uri.joinPath(this.context.extensionUri, 'media', mediaFile)); return webviewResource.toString(); } private fixHref(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, href: string): string { if (!href) { return href; } if (href.startsWith('http:') || href.startsWith('https:') || href.startsWith('file:')) { return href; } // Assume it must be a local file if (path.isAbsolute(href)) { return resourceProvider.asWebviewUri(vscode.Uri.file(href)).toString(); } // Use a workspace relative path if there is a workspace const root = vscode.workspace.getWorkspaceFolder(resource); if (root) { return resourceProvider.asWebviewUri(vscode.Uri.joinPath(root.uri, href)).toString(); } // Otherwise look relative to the markdown file return resourceProvider.asWebviewUri(vscode.Uri.file(path.join(path.dirname(resource.fsPath), href))).toString(); } private computeCustomStyleSheetIncludes(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, config: MarkdownPreviewConfiguration): string { if (!Array.isArray(config.styles)) { return ''; } const out: string[] = []; for (const style of config.styles) { out.push(``); } return out.join('\n'); } private getSettingsOverrideStyles(config: MarkdownPreviewConfiguration): string { return [ config.fontFamily ? `--markdown-font-family: ${config.fontFamily};` : '', isNaN(config.fontSize) ? '' : `--markdown-font-size: ${config.fontSize}px;`, isNaN(config.lineHeight) ? '' : `--markdown-line-height: ${config.lineHeight};`, ].join(' '); } private getImageStabilizerStyles(state?: any) { let ret = '\n'; return ret; } private getStyles(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, config: MarkdownPreviewConfiguration, state?: any): string { const baseStyles: string[] = []; for (const resource of this.contributionProvider.contributions.previewStyles) { baseStyles.push(``); } return `${baseStyles.join('\n')} ${this.computeCustomStyleSheetIncludes(resourceProvider, resource, config)} ${this.getImageStabilizerStyles(state)}`; } private getScripts(resourceProvider: WebviewResourceProvider, nonce: string): string { const out: string[] = []; for (const resource of this.contributionProvider.contributions.previewScripts) { out.push(``); } return out.join('\n'); } private getCsp( provider: WebviewResourceProvider, resource: vscode.Uri, nonce: string ): string { const rule = provider.cspSource; switch (this.cspArbiter.getSecurityLevelForResource(resource)) { case MarkdownPreviewSecurityLevel.AllowInsecureContent: return ``; case MarkdownPreviewSecurityLevel.AllowInsecureLocalContent: return ``; case MarkdownPreviewSecurityLevel.AllowScriptsAndAllContent: return ''; case MarkdownPreviewSecurityLevel.Strict: default: return ``; } } }