previewContentProvider.ts 9.2 KB
Newer Older
1 2 3 4 5 6
/*---------------------------------------------------------------------------------------------
 *  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';
M
Matt Bierner 已提交
7
import * as vscode from 'vscode';
8
import * as nls from 'vscode-nls';
9
import { Logger } from '../logger';
M
Matt Bierner 已提交
10
import { MarkdownEngine } from '../markdownEngine';
M
Matt Bierner 已提交
11
import { MarkdownContributionProvider } from '../markdownExtensions';
M
Matt Bierner 已提交
12 13 14 15 16
import { ContentSecurityPolicyArbiter, MarkdownPreviewSecurityLevel } from '../security';
import { WebviewResourceProvider } from '../util/resources';
import { MarkdownPreviewConfiguration, MarkdownPreviewConfigurationManager } from './previewConfig';

const localize = nls.loadMessageBundle();
17

M
Matt Bierner 已提交
18 19 20 21 22 23
/**
 * 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.
 */
24
const previewStrings = {
M
Matt Bierner 已提交
25 26 27 28 29 30 31 32 33 34 35
	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')
36 37
};

M
Matt Bierner 已提交
38 39
function escapeAttribute(value: string | vscode.Uri): string {
	return value.toString().replace(/"/g, '"');
M
Matt Bierner 已提交
40 41
}

M
Matt Bierner 已提交
42
export class MarkdownContentProvider {
43
	constructor(
M
Matt Bierner 已提交
44 45 46
		private readonly engine: MarkdownEngine,
		private readonly context: vscode.ExtensionContext,
		private readonly cspArbiter: ContentSecurityPolicyArbiter,
M
Matt Bierner 已提交
47
		private readonly contributionProvider: MarkdownContributionProvider,
M
Matt Bierner 已提交
48
		private readonly logger: Logger
M
Matt Bierner 已提交
49
	) { }
50

51 52
	public async provideTextDocumentContent(
		markdownDocument: vscode.TextDocument,
M
Matt Bierner 已提交
53
		resourceProvider: WebviewResourceProvider,
54
		previewConfigurations: MarkdownPreviewConfigurationManager,
55 56
		initialLine: number | undefined = undefined,
		state?: any
57 58 59 60 61 62 63 64 65 66
	): Promise<string> {
		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,
67
			disableSecurityWarnings: this.cspArbiter.shouldDisableSecurityWarnings(),
M
Matt Bierner 已提交
68
			webviewResourceRoot: resourceProvider.toWebviewResource(markdownDocument.uri).toString(),
69 70 71 72 73 74
		};

		this.logger.log('provideTextDocumentContent', initialData);

		// Content Security Policy
		const nonce = new Date().getTime() + '' + new Date().getMilliseconds();
M
Matt Bierner 已提交
75
		const csp = this.getCsp(resourceProvider, sourceUri, nonce);
76

77
		const body = await this.engine.render(markdownDocument);
78
		return `<!DOCTYPE html>
M
Matt Bierner 已提交
79
			<html style="${escapeAttribute(this.getSettingsOverrideStyles(config))}">
80 81 82
			<head>
				<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
				${csp}
83
				<meta id="vscode-markdown-preview-data"
M
Matt Bierner 已提交
84 85 86
					data-settings="${escapeAttribute(JSON.stringify(initialData))}"
					data-strings="${escapeAttribute(JSON.stringify(previewStrings))}"
					data-state="${escapeAttribute(JSON.stringify(state || {}))}">
M
Matt Bierner 已提交
87 88 89
				<script src="${this.extensionResourcePath(resourceProvider, 'pre.js')}" nonce="${nonce}"></script>
				${this.getStyles(resourceProvider, sourceUri, config, state)}
				<base href="${resourceProvider.toWebviewResource(markdownDocument.uri)}">
90 91 92 93
			</head>
			<body class="vscode-body ${config.scrollBeyondLastLine ? 'scrollBeyondLastLine' : ''} ${config.wordWrap ? 'wordWrap' : ''} ${config.markEditorSelection ? 'showEditorSelection' : ''}">
				${body}
				<div class="code-line" data-line="${markdownDocument.lineCount}"></div>
M
Matt Bierner 已提交
94
				${this.getScripts(resourceProvider, nonce)}
95 96 97 98
			</body>
			</html>`;
	}

M
Styling  
Matt Bierner 已提交
99
	public provideFileNotFoundContent(
100
		resource: vscode.Uri,
M
Styling  
Matt Bierner 已提交
101
	): string {
102
		const resourcePath = path.basename(resource.fsPath);
P
pkoushik 已提交
103
		const body = localize('preview.notFound', '{0} cannot be found', resourcePath);
104 105 106
		return `<!DOCTYPE html>
			<html>
			<body class="vscode-body">
P
pkoushik 已提交
107
				${body}
108 109 110 111
			</body>
			</html>`;
	}

M
Matt Bierner 已提交
112 113 114 115
	private extensionResourcePath(resourceProvider: WebviewResourceProvider, mediaFile: string): string {
		const webviewResource = resourceProvider.toWebviewResource(
			vscode.Uri.file(this.context.asAbsolutePath(path.join('media', mediaFile))));
		return webviewResource.toString();
116 117
	}

M
Matt Bierner 已提交
118
	private fixHref(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, href: string): string {
M
Matt Bierner 已提交
119 120 121
		if (!href) {
			return href;
		}
122

123 124
		if (href.startsWith('http:') || href.startsWith('https:') || href.startsWith('file:')) {
			return href;
M
Matt Bierner 已提交
125
		}
126

127 128
		// Assume it must be a local file
		if (path.isAbsolute(href)) {
M
Matt Bierner 已提交
129
			return resourceProvider.toWebviewResource(vscode.Uri.file(href)).toString();
M
Matt Bierner 已提交
130
		}
131

M
Matt Bierner 已提交
132
		// Use a workspace relative path if there is a workspace
133
		const root = vscode.workspace.getWorkspaceFolder(resource);
134
		if (root) {
M
Matt Bierner 已提交
135
			return resourceProvider.toWebviewResource(vscode.Uri.file(path.join(root.uri.fsPath, href))).toString();
136
		}
M
Matt Bierner 已提交
137

M
Matt Bierner 已提交
138
		// Otherwise look relative to the markdown file
M
Matt Bierner 已提交
139
		return resourceProvider.toWebviewResource(vscode.Uri.file(path.join(path.dirname(resource.fsPath), href))).toString();
140 141
	}

M
Matt Bierner 已提交
142 143 144
	private computeCustomStyleSheetIncludes(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, config: MarkdownPreviewConfiguration): string {
		if (!Array.isArray(config.styles)) {
			return '';
145
		}
M
Matt Bierner 已提交
146 147 148 149 150
		const out: string[] = [];
		for (const style of config.styles) {
			out.push(`<link rel="stylesheet" class="code-user-style" data-source="${escapeAttribute(style)}" href="${escapeAttribute(this.fixHref(resourceProvider, resource, style))}" type="text/css" media="screen">`);
		}
		return out.join('\n');
151 152
	}

M
Matt Bierner 已提交
153 154 155 156 157 158
	private getSettingsOverrideStyles(config: MarkdownPreviewConfiguration): string {
		return [
			config.fontFamily ? `--vscode-markdown-font-family: ${config.fontFamily};` : '',
			isNaN(config.fontSize) ? '' : `--vscode-markdown-font-size: ${config.fontSize}px;`,
			isNaN(config.lineHeight) ? '' : `--vscode-markdown-line-height: ${config.lineHeight};`,
		].join(' ');
159 160
	}

161 162 163 164 165 166 167 168 169 170 171 172 173 174 175
	private getImageStabilizerStyles(state?: any) {
		let ret = '<style>\n';
		if (state && state.imageInfo) {
			state.imageInfo.forEach((imgInfo: any) => {
				ret += `#${imgInfo.id}.loading {
					height: ${imgInfo.height}px;
					width: ${imgInfo.width}px;
				}\n`;
			});
		}
		ret += '</style>\n';

		return ret;
	}

M
Matt Bierner 已提交
176 177 178 179 180
	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(`<link rel="stylesheet" type="text/css" href="${escapeAttribute(resourceProvider.toWebviewResource(resource))}">`);
		}
181

M
Matt Bierner 已提交
182 183
		return `${baseStyles.join('\n')}
			${this.computeCustomStyleSheetIncludes(resourceProvider, resource, config)}
184
			${this.getImageStabilizerStyles(state)}`;
185 186
	}

M
Matt Bierner 已提交
187 188 189 190 191 192 193 194 195
	private getScripts(resourceProvider: WebviewResourceProvider, nonce: string): string {
		const out: string[] = [];
		for (const resource of this.contributionProvider.contributions.previewScripts) {
			out.push(`<script async
				src="${escapeAttribute(resourceProvider.toWebviewResource(resource))}"
				nonce="${nonce}"
				charset="UTF-8"></script>`);
		}
		return out.join('\n');
196 197
	}

M
Matt Bierner 已提交
198 199
	private getCsp(
		provider: WebviewResourceProvider,
200 201 202
		resource: vscode.Uri,
		nonce: string
	): string {
203
		const rule = provider.cspSource;
204 205
		switch (this.cspArbiter.getSecurityLevelForResource(resource)) {
			case MarkdownPreviewSecurityLevel.AllowInsecureContent:
M
Matt Bierner 已提交
206
				return `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self' ${rule} http: https: data:; media-src 'self' ${rule} http: https: data:; script-src 'nonce-${nonce}'; style-src 'self' ${rule} 'unsafe-inline' http: https: data:; font-src 'self' ${rule} http: https: data:;">`;
207

208
			case MarkdownPreviewSecurityLevel.AllowInsecureLocalContent:
M
Matt Bierner 已提交
209
				return `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self' ${rule} https: data: http://localhost:* http://127.0.0.1:*; media-src 'self' ${rule} https: data: http://localhost:* http://127.0.0.1:*; script-src 'nonce-${nonce}'; style-src 'self' ${rule} 'unsafe-inline' https: data: http://localhost:* http://127.0.0.1:*; font-src 'self' ${rule} https: data: http://localhost:* http://127.0.0.1:*;">`;
210

211
			case MarkdownPreviewSecurityLevel.AllowScriptsAndAllContent:
212
				return '<meta http-equiv="Content-Security-Policy" content="">';
213 214 215

			case MarkdownPreviewSecurityLevel.Strict:
			default:
M
Matt Bierner 已提交
216
				return `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self' ${rule} https: data:; media-src 'self' ${rule} https: data:; script-src 'nonce-${nonce}'; style-src 'self' ${rule} 'unsafe-inline' https: data:; font-src 'self' ${rule} https: data:;">`;
217 218
		}
	}
219
}