/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; import * as path from 'path'; import { MarkdownEngine } from '../markdownEngine'; import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); import { Logger } from '../logger'; import { ContentSecurityPolicyArbiter, MarkdownPreviewSecurityLevel } from '../security'; import { MarkdownPreviewConfigurationManager, MarkdownPreviewConfiguration } from './previewConfig'; import { MarkdownContributionProvider } from '../markdownExtensions'; /** * 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): string { return value.replace(/"/g, '"'); } 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, previewConfigurations: MarkdownPreviewConfigurationManager, initialLine: number | undefined = undefined, state?: any ): Promise { const sourceUri = markdownDocument.uri; const config = previewConfigurations.loadAndCacheConfiguration(sourceUri); const initialData = { source: sourceUri.toString(), line: initialLine, lineCount: markdownDocument.lineCount, scrollPreviewWithEditor: config.scrollPreviewWithEditor, scrollEditorWithPreview: config.scrollEditorWithPreview, doubleClickToSwitchToEditor: config.doubleClickToSwitchToEditor, disableSecurityWarnings: this.cspArbiter.shouldDisableSecurityWarnings() }; this.logger.log('provideTextDocumentContent', initialData); // Content Security Policy const nonce = new Date().getTime() + '' + new Date().getMilliseconds(); const csp = this.getCspForResource(sourceUri, nonce); const body = await this.engine.render(markdownDocument); return ` ${csp} ${this.getStyles(sourceUri, nonce, config, state)} ${body}
${this.getScripts(nonce)} `; } 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(mediaFile: string): string { return vscode.Uri.file(this.context.asAbsolutePath(path.join('media', mediaFile))) .with({ scheme: 'vscode-resource' }) .toString(); } private fixHref(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 vscode.Uri.file(href) .with({ scheme: 'vscode-resource' }) .toString(); } // Use a workspace relative path if there is a workspace const root = vscode.workspace.getWorkspaceFolder(resource); if (root) { return vscode.Uri.file(path.join(root.uri.fsPath, href)) .with({ scheme: 'vscode-resource' }) .toString(); } // Otherwise look relative to the markdown file return vscode.Uri.file(path.join(path.dirname(resource.fsPath), href)) .with({ scheme: 'vscode-resource' }) .toString(); } private computeCustomStyleSheetIncludes(resource: vscode.Uri, config: MarkdownPreviewConfiguration): string { if (Array.isArray(config.styles)) { return config.styles.map(style => { return ``; }).join('\n'); } return ''; } private getSettingsOverrideStyles(nonce: string, config: MarkdownPreviewConfiguration): string { return ``; } private getImageStabilizerStyles(state?: any) { let ret = '\n'; return ret; } private getStyles(resource: vscode.Uri, nonce: string, config: MarkdownPreviewConfiguration, state?: any): string { const baseStyles = this.contributionProvider.contributions.previewStyles .map(resource => ``) .join('\n'); return `${baseStyles} ${this.getSettingsOverrideStyles(nonce, config)} ${this.computeCustomStyleSheetIncludes(resource, config)} ${this.getImageStabilizerStyles(state)}`; } private getScripts(nonce: string): string { return this.contributionProvider.contributions.previewScripts .map(resource => ``) .join('\n'); } private getCspForResource(resource: vscode.Uri, nonce: string): string { switch (this.cspArbiter.getSecurityLevelForResource(resource)) { case MarkdownPreviewSecurityLevel.AllowInsecureContent: return ``; case MarkdownPreviewSecurityLevel.AllowInsecureLocalContent: return ``; case MarkdownPreviewSecurityLevel.AllowScriptsAndAllContent: return ''; case MarkdownPreviewSecurityLevel.Strict: default: return ``; } } }