提交 7f3d3d83 编写于 作者: M Matt Bierner

Add vscode.env.webviewResourceRoot api

Fixes #72155

Adds a constant to the api that tracks the root path for resources inside of webviews. This is required because we will not be able to use `vscode-resource:` uris on the web. Our current approach is to rewrite the html we are given but there are almost certainly going to be cases where we don't get this quite right.

Adopts the new api for the markdown preview
上级 6520c22b
......@@ -4,7 +4,8 @@
"description": "%description%",
"version": "1.0.0",
"icon": "icon.png",
"publisher": "vscode",
"publisher": "vscode",
"enableProposedApi": true,
"license": "MIT",
"aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217",
"engines": {
......
......@@ -19,7 +19,7 @@ const settings = getSettings();
const vscode = acquireVsCodeApi();
// Set VS Code state
let state = getData('data-state');
let state = getData<{ line: number }>('data-state');
vscode.setState(state);
const messaging = createPosterForVsCode(vscode);
......@@ -131,8 +131,8 @@ document.addEventListener('click', event => {
if (node.getAttribute('href').startsWith('#')) {
break;
}
if (node.href.startsWith('file://') || node.href.startsWith('vscode-resource:')) {
const [path, fragment] = node.href.replace(/^(file:\/\/|vscode-resource:)/i, '').split('#');
if (node.href.startsWith('file://') || node.href.startsWith('vscode-resource:') || node.href.startsWith(settings.webviewResourceRoot)) {
const [path, fragment] = node.href.replace(/^(file:\/\/|vscode-resource:)/i, '').replace(new RegExp(`^${escapeRegExp(settings.webviewResourceRoot)}`)).split('#');
messaging.postMessage('clickLink', { path, fragment });
event.preventDefault();
event.stopPropagation();
......@@ -157,4 +157,8 @@ if (settings.scrollEditorWithPreview) {
}
}
}, 50));
}
function escapeRegExp(text: string) {
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
}
\ No newline at end of file
......@@ -4,18 +4,19 @@
*--------------------------------------------------------------------------------------------*/
export interface PreviewSettings {
source: string;
line: number;
lineCount: number;
scrollPreviewWithEditor?: boolean;
scrollEditorWithPreview: boolean;
disableSecurityWarnings: boolean;
doubleClickToSwitchToEditor: boolean;
readonly source: string;
readonly line: number;
readonly lineCount: number;
readonly scrollPreviewWithEditor?: boolean;
readonly scrollEditorWithPreview: boolean;
readonly disableSecurityWarnings: boolean;
readonly doubleClickToSwitchToEditor: boolean;
readonly webviewResourceRoot: string;
}
let cachedSettings: PreviewSettings | undefined = undefined;
export function getData(key: string): PreviewSettings {
export function getData<T = {}>(key: string): T {
const element = document.getElementById('vscode-markdown-preview-data');
if (element) {
const data = element.getAttribute(key);
......
......@@ -14,6 +14,7 @@ import { Logger } from '../logger';
import { ContentSecurityPolicyArbiter, MarkdownPreviewSecurityLevel } from '../security';
import { MarkdownPreviewConfigurationManager, MarkdownPreviewConfiguration } from './previewConfig';
import { MarkdownContributionProvider } from '../markdownExtensions';
import { toResoruceUri } from '../util/resources';
/**
* Strings used inside the markdown preview.
......@@ -63,7 +64,8 @@ export class MarkdownContentProvider {
scrollPreviewWithEditor: config.scrollPreviewWithEditor,
scrollEditorWithPreview: config.scrollEditorWithPreview,
doubleClickToSwitchToEditor: config.doubleClickToSwitchToEditor,
disableSecurityWarnings: this.cspArbiter.shouldDisableSecurityWarnings()
disableSecurityWarnings: this.cspArbiter.shouldDisableSecurityWarnings(),
webviewResourceRoot: vscode.env.webviewResourceRoot,
};
this.logger.log('provideTextDocumentContent', initialData);
......@@ -84,7 +86,7 @@ export class MarkdownContentProvider {
data-state="${escapeAttribute(JSON.stringify(state || {}))}">
<script src="${this.extensionResourcePath('pre.js')}" nonce="${nonce}"></script>
${this.getStyles(sourceUri, nonce, config, state)}
<base href="${markdownDocument.uri.with({ scheme: 'vscode-resource' }).toString(true)}">
<base href="${toResoruceUri(markdownDocument.uri)}">
</head>
<body class="vscode-body ${config.scrollBeyondLastLine ? 'scrollBeyondLastLine' : ''} ${config.wordWrap ? 'wordWrap' : ''} ${config.markEditorSelection ? 'showEditorSelection' : ''}">
${body}
......@@ -108,8 +110,7 @@ export class MarkdownContentProvider {
}
private extensionResourcePath(mediaFile: string): string {
return vscode.Uri.file(this.context.asAbsolutePath(path.join('media', mediaFile)))
.with({ scheme: 'vscode-resource' })
return toResoruceUri(vscode.Uri.file(this.context.asAbsolutePath(path.join('media', mediaFile))))
.toString();
}
......@@ -124,23 +125,17 @@ export class MarkdownContentProvider {
// Assume it must be a local file
if (path.isAbsolute(href)) {
return vscode.Uri.file(href)
.with({ scheme: 'vscode-resource' })
.toString();
return toResoruceUri(vscode.Uri.file(href)).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();
return toResoruceUri(vscode.Uri.file(path.join(root.uri.fsPath, href))).toString();
}
// Otherwise look relative to the markdown file
return vscode.Uri.file(path.join(path.dirname(resource.fsPath), href))
.with({ scheme: 'vscode-resource' })
.toString();
return toResoruceUri(vscode.Uri.file(path.join(path.dirname(resource.fsPath), href))).toString();
}
private computeCustomStyleSheetIncludes(resource: vscode.Uri, config: MarkdownPreviewConfiguration): string {
......@@ -197,17 +192,17 @@ export class MarkdownContentProvider {
private getCspForResource(resource: vscode.Uri, nonce: string): string {
switch (this.cspArbiter.getSecurityLevelForResource(resource)) {
case MarkdownPreviewSecurityLevel.AllowInsecureContent:
return `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self' vscode-resource: http: https: data:; media-src 'self' vscode-resource: http: https: data:; script-src 'nonce-${nonce}'; style-src 'self' vscode-resource: 'unsafe-inline' http: https: data:; font-src 'self' vscode-resource: http: https: data:;">`;
return `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self' ${vscode.env.webviewResourceRoot} http: https: data:; media-src 'self' ${vscode.env.webviewResourceRoot} http: https: data:; script-src 'nonce-${nonce}'; style-src 'self' ${vscode.env.webviewResourceRoot} 'unsafe-inline' http: https: data:; font-src 'self' ${vscode.env.webviewResourceRoot} http: https: data:;">`;
case MarkdownPreviewSecurityLevel.AllowInsecureLocalContent:
return `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self' vscode-resource: https: data: http://localhost:* http://127.0.0.1:*; media-src 'self' vscode-resource: https: data: http://localhost:* http://127.0.0.1:*; script-src 'nonce-${nonce}'; style-src 'self' vscode-resource: 'unsafe-inline' https: data: http://localhost:* http://127.0.0.1:*; font-src 'self' vscode-resource: https: data: http://localhost:* http://127.0.0.1:*;">`;
return `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self' ${vscode.env.webviewResourceRoot} https: data: http://localhost:* http://127.0.0.1:*; media-src 'self' ${vscode.env.webviewResourceRoot} https: data: http://localhost:* http://127.0.0.1:*; script-src 'nonce-${nonce}'; style-src 'self' ${vscode.env.webviewResourceRoot} 'unsafe-inline' https: data: http://localhost:* http://127.0.0.1:*; font-src 'self' ${vscode.env.webviewResourceRoot} https: data: http://localhost:* http://127.0.0.1:*;">`;
case MarkdownPreviewSecurityLevel.AllowScriptsAndAllContent:
return '';
case MarkdownPreviewSecurityLevel.Strict:
default:
return `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self' vscode-resource: https: data:; media-src 'self' vscode-resource: https: data:; script-src 'nonce-${nonce}'; style-src 'self' vscode-resource: 'unsafe-inline' https: data:; font-src 'self' vscode-resource: https: data:;">`;
return `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self' ${vscode.env.webviewResourceRoot} https: data:; media-src 'self' ${vscode.env.webviewResourceRoot} https: data:; script-src 'nonce-${nonce}'; style-src 'self' ${vscode.env.webviewResourceRoot} 'unsafe-inline' https: data:; font-src 'self' ${vscode.env.webviewResourceRoot} https: data:;">`;
}
}
}
......@@ -7,10 +7,10 @@ import * as vscode from 'vscode';
import * as path from 'path';
import { Disposable } from './util/dispose';
import * as arrays from './util/arrays';
import { toResoruceUri } from './util/resources';
const resolveExtensionResource = (extension: vscode.Extension<any>, resourcePath: string): vscode.Uri => {
return vscode.Uri.file(path.join(extension.extensionPath, resourcePath))
.with({ scheme: 'vscode-resource' });
return toResoruceUri(vscode.Uri.file(path.join(extension.extensionPath, resourcePath)));
};
const resolveExtensionResources = (extension: vscode.Extension<any>, resourcePaths: unknown): vscode.Uri[] => {
......
/*---------------------------------------------------------------------------------------------
* 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';
const rootUri = vscode.Uri.parse(vscode.env.webviewResourceRoot);
export function toResoruceUri(uri: vscode.Uri): vscode.Uri {
return rootUri.with({
path: rootUri.path + uri.path,
query: uri.query,
fragment: uri.fragment,
});
}
......@@ -251,7 +251,7 @@ suite('Webview tests', () => {
});
</script>`);
const workspaceRootUri = vscode.Uri.file(vscode.workspace.rootPath!).with({ scheme: 'vscode-resource' });
const workspaceRootUri = vscode.env.webviewResourceRoot + vscode.Uri.file(vscode.workspace.rootPath!).path;
{
const imagePath = workspaceRootUri.toString() + '/image.png';
......
......@@ -153,4 +153,5 @@ export interface IEnvironmentService {
driverVerbose: boolean;
webviewEndpoint?: string;
readonly webviewResourceRoot: string;
}
......@@ -266,6 +266,10 @@ export class EnvironmentService implements IEnvironmentService {
get driverHandle(): string | undefined { return this._args['driver']; }
get driverVerbose(): boolean { return !!this._args['driver-verbose']; }
get webviewResourceRoot(): string {
return 'vscode-resource:';
}
constructor(private _args: ParsedArgs, private _execPath: string) {
if (!process.env['VSCODE_LOGS']) {
const key = toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '');
......
......@@ -1464,4 +1464,18 @@ declare module 'vscode' {
}
//#endregion
//#region Webview Resource Roots
export namespace env {
/**
* Root url from which local resources are loaded inside of webviews.
*
* This is `vscode-resource:` when vscode is run on the desktop. When vscode is run
* on the web, this points to a server endpoint.
*/
export const webviewResourceRoot: string;
}
//#endregion
}
......@@ -58,6 +58,7 @@ export interface IEnvironment {
extensionTestsLocationURI?: URI;
globalStorageHome: URI;
userHome: URI;
webviewResourceRoot: string;
}
export interface IStaticWorkspaceData {
......
......@@ -259,7 +259,11 @@ export function createApiFactory(
},
openExternal(uri: URI) {
return extHostWindow.openUri(uri, { allowTunneling: !!initData.remoteAuthority });
}
},
get webviewResourceRoot() {
checkProposedApiEnabled(extension);
return initData.environment.webviewResourceRoot;
},
};
if (!initData.environment.extensionTestsLocationURI) {
// allow to patch env-function when running tests
......
......@@ -133,4 +133,8 @@ export class BrowserWorkbenchEnvironmentService implements IEnvironmentService {
driverHandle?: string;
driverVerbose: boolean;
webviewEndpoint?: string;
get webviewResourceRoot(): string {
return this.webviewEndpoint ? this.webviewEndpoint + '/vscode-resource' : 'vscode-resource:';
}
}
......@@ -189,7 +189,8 @@ export class RemoteExtensionHostClient extends Disposable implements IExtensionH
extensionDevelopmentLocationURI: this._environmentService.extensionDevelopmentLocationURI,
extensionTestsLocationURI: this._environmentService.extensionTestsLocationURI,
globalStorageHome: remoteExtensionHostData.globalStorageHome,
userHome: remoteExtensionHostData.userHome
userHome: remoteExtensionHostData.userHome,
webviewResourceRoot: this._environmentService.webviewResourceRoot,
},
workspace: this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? null : {
configuration: workspace.configuration,
......
......@@ -394,7 +394,8 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
extensionDevelopmentLocationURI: this._environmentService.extensionDevelopmentLocationURI,
extensionTestsLocationURI: this._environmentService.extensionTestsLocationURI,
globalStorageHome: URI.file(this._environmentService.globalStorageHome),
userHome: URI.file(this._environmentService.userHome)
userHome: URI.file(this._environmentService.userHome),
webviewResourceRoot: this._environmentService.webviewResourceRoot,
},
workspace: this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? undefined : {
configuration: withNullAsUndefined(workspace.configuration),
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册