Add vscode-webview-resource protocol (#97777)
* Add vscode-webview-resource protocol Adds a new protocol (`vscode-webview-resource`) for loading resources insides of webviews. This replaces the existing `vscode-resource` protocol and is registered on the main thread instead of in each renderer This change also adds some rewriting logic to update any `vscode-resource:` references found in the main html to use `vscode-webview-resource` instead. * Move webview protcol provider to own file * Remove registration of vscode-resource scheme * Remove use or parition for each webview Now that we have a single shared protocol handler, we do not need to run each webview in its own partition * Fix rewriting csp to use new protocol * Update src/vs/code/electron-main/app.ts Co-authored-by: Benjamin Pasero <benjpas@microsoft.com> Co-authored-by: Benjamin Pasero <benjpas@microsoft.com>
This commit is contained in:
parent
946d994d1b
commit
307cb32f30
10
src/main.js
10
src/main.js
|
@ -79,7 +79,15 @@ setCurrentWorkingDirectory();
|
|||
|
||||
// Register custom schemes with privileges
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{ scheme: 'vscode-resource', privileges: { secure: true, supportFetchAPI: true, corsEnabled: true } }
|
||||
{
|
||||
scheme: 'vscode-webview-resource',
|
||||
privileges: {
|
||||
secure: true,
|
||||
standard: true,
|
||||
supportFetchAPI: true,
|
||||
corsEnabled: true,
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
||||
// Global app listeners
|
||||
|
|
|
@ -59,6 +59,8 @@ export namespace Schemas {
|
|||
export const vscodeSettings = 'vscode-settings';
|
||||
|
||||
export const webviewPanel = 'webview-panel';
|
||||
|
||||
export const vscodeWebviewResource = 'vscode-webview-resource';
|
||||
}
|
||||
|
||||
class RemoteAuthoritiesImpl {
|
||||
|
|
|
@ -81,6 +81,8 @@ import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common
|
|||
import { StorageKeysSyncRegistryChannel } from 'vs/platform/userDataSync/common/userDataSyncIpc';
|
||||
import { INativeEnvironmentService } from 'vs/platform/environment/node/environmentService';
|
||||
import { mnemonicButtonLabel, getPathLabel } from 'vs/base/common/labels';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { WebviewProtocolProvider } from 'vs/platform/webview/electron-main/webviewProtocolProvider';
|
||||
|
||||
export class CodeApplication extends Disposable {
|
||||
private windowsMainService: IWindowsMainService | undefined;
|
||||
|
@ -89,6 +91,7 @@ export class CodeApplication extends Disposable {
|
|||
constructor(
|
||||
private readonly mainIpcServer: Server,
|
||||
private readonly userEnv: IProcessEnvironment,
|
||||
@IFileService fileService: IFileService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
@IEnvironmentService private readonly environmentService: INativeEnvironmentService,
|
||||
|
@ -99,6 +102,8 @@ export class CodeApplication extends Disposable {
|
|||
super();
|
||||
|
||||
this.registerListeners();
|
||||
|
||||
this._register(new WebviewProtocolProvider(fileService));
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
|
|
|
@ -8,10 +8,9 @@ import { sep } from 'vs/base/common/path';
|
|||
import { URI } from 'vs/base/common/uri';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
|
||||
import { getWebviewContentMimeType } from 'vs/workbench/contrib/webview/common/mimeTypes';
|
||||
import { isUNC } from 'vs/base/common/extpath';
|
||||
|
||||
export const WebviewResourceScheme = 'vscode-resource';
|
||||
import { getWebviewContentMimeType } from 'vs/platform/webview/common/mimeTypes';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
|
||||
export namespace WebviewResourceResponse {
|
||||
export enum Type { Success, Failed, AccessDenied }
|
||||
|
@ -77,7 +76,7 @@ export async function loadLocalResource(
|
|||
}
|
||||
|
||||
function normalizeRequestPath(requestUri: URI) {
|
||||
if (requestUri.scheme !== WebviewResourceScheme) {
|
||||
if (requestUri.scheme !== Schemas.vscodeWebviewResource) {
|
||||
return requestUri;
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ipcMain as ipc, IpcMainEvent, MimeTypedBuffer, protocol } from 'electron';
|
||||
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { loadLocalResource, WebviewResourceResponse } from 'vs/platform/webview/common/resourceLoader';
|
||||
|
||||
export interface RegisterWebviewMetadata {
|
||||
readonly extensionLocation: URI | undefined;
|
||||
readonly localResourceRoots: readonly URI[];
|
||||
}
|
||||
|
||||
type ErrorCallback = (response: MimeTypedBuffer | { error: number }) => void;
|
||||
|
||||
|
||||
export class WebviewProtocolProvider extends Disposable {
|
||||
|
||||
private readonly webviewMetadata = new Map<string, {
|
||||
readonly extensionLocation: URI | undefined;
|
||||
readonly localResourceRoots: URI[];
|
||||
}>();
|
||||
|
||||
constructor(
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
) {
|
||||
super();
|
||||
|
||||
ipc.on('vscode:registerWebview', (event: IpcMainEvent, id: string, data: RegisterWebviewMetadata) => {
|
||||
this.webviewMetadata.set(id, {
|
||||
extensionLocation: data.extensionLocation ? URI.from(data.extensionLocation) : undefined,
|
||||
localResourceRoots: data.localResourceRoots.map((x: UriComponents) => URI.from(x)),
|
||||
});
|
||||
|
||||
event.sender.send(`vscode:didRegisterWebview-${id}`);
|
||||
});
|
||||
|
||||
ipc.on('vscode:unregisterWebview', (_event: IpcMainEvent, id: string) => {
|
||||
this.webviewMetadata.delete(id);
|
||||
});
|
||||
|
||||
|
||||
protocol.registerBufferProtocol(Schemas.vscodeWebviewResource, async (request, callback): Promise<void> => {
|
||||
try {
|
||||
const uri = URI.parse(request.url);
|
||||
const resource = URI.parse(uri.path.replace(/^\/(\w+)/, '$1:'));
|
||||
|
||||
const id = uri.authority;
|
||||
const metadata = this.webviewMetadata.get(id);
|
||||
if (metadata) {
|
||||
const result = await loadLocalResource(resource, this.fileService, metadata.extensionLocation, () => metadata.localResourceRoots);
|
||||
if (result.type === WebviewResourceResponse.Type.Success) {
|
||||
return callback({
|
||||
data: Buffer.from(result.data.buffer),
|
||||
mimeType: result.mimeType
|
||||
});
|
||||
}
|
||||
|
||||
if (result.type === WebviewResourceResponse.Type.AccessDenied) {
|
||||
console.error('Webview: Cannot load resource outside of protocol root');
|
||||
return (callback as ErrorCallback)({ error: -10 /* ACCESS_DENIED: https://cs.chromium.org/chromium/src/net/base/net_error_list.h */ });
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
|
||||
return (callback as ErrorCallback)({ error: -2 });
|
||||
});
|
||||
|
||||
this._register(toDisposable(() => protocol.unregisterProtocol(Schemas.vscodeWebviewResource)));
|
||||
}
|
||||
}
|
2
src/vs/vscode.d.ts
vendored
2
src/vs/vscode.d.ts
vendored
|
@ -6610,7 +6610,7 @@ declare module 'vscode' {
|
|||
readonly enableCommandUris?: boolean;
|
||||
|
||||
/**
|
||||
* Root paths from which the webview can load local (filesystem) resources using the `vscode-resource:` scheme.
|
||||
* Root paths from which the webview can load local (filesystem) resources using uris from `asWebviewUri`
|
||||
*
|
||||
* Default to the root folders of the current workspace plus the extension's install directory.
|
||||
*
|
||||
|
|
|
@ -94,8 +94,7 @@ export class MainThreadEditorInsets implements MainThreadEditorInsetsShape {
|
|||
}, {
|
||||
allowScripts: options.enableScripts,
|
||||
localResourceRoots: options.localResourceRoots ? options.localResourceRoots.map(uri => URI.revive(uri)) : undefined
|
||||
});
|
||||
webview.extension = { id: extensionId, location: URI.revive(extensionLocation) };
|
||||
}, { id: extensionId, location: URI.revive(extensionLocation) });
|
||||
|
||||
const webviewZone = new EditorWebviewZone(editor, line, height, webview);
|
||||
|
||||
|
|
|
@ -97,9 +97,8 @@ export class CustomEditorInputFactory extends WebviewEditorInputFactory {
|
|||
const webview = webviewService.createWebviewOverlay(data.id, {
|
||||
enableFindWidget: data.options.enableFindWidget,
|
||||
retainContextWhenHidden: data.options.retainContextWhenHidden
|
||||
}, data.options);
|
||||
}, data.options, data.extension);
|
||||
webview.state = data.state;
|
||||
webview.extension = data.extension;
|
||||
return webview;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -231,7 +231,7 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ
|
|||
|
||||
const id = generateUuid();
|
||||
const webview = new Lazy(() => {
|
||||
return this.webviewService.createWebviewOverlay(id, { customClasses: options?.customClasses }, {});
|
||||
return this.webviewService.createWebviewOverlay(id, { customClasses: options?.customClasses }, {}, undefined);
|
||||
});
|
||||
const input = this.instantiationService.createInstance(CustomEditorInput, resource, viewType, id, webview, {});
|
||||
if (typeof group !== 'undefined') {
|
||||
|
|
|
@ -588,7 +588,7 @@ export class ExtensionEditor extends BaseEditor {
|
|||
|
||||
const webview = this.contentDisposables.add(this.webviewService.createWebviewOverlay('extensionEditor', {
|
||||
enableFindWidget: true,
|
||||
}, {}));
|
||||
}, {}, undefined));
|
||||
|
||||
webview.claim(this);
|
||||
webview.layoutWebviewOverElement(template.content);
|
||||
|
|
|
@ -19,7 +19,8 @@ import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewMod
|
|||
import { IOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService';
|
||||
import { IWebviewService, WebviewElement } from 'vs/workbench/contrib/webview/browser/webview';
|
||||
import { WebviewResourceScheme } from 'vs/workbench/contrib/webview/common/resourceLoader';
|
||||
import { asWebviewUri } from 'vs/workbench/contrib/webview/common/webviewUri';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
|
||||
export interface IDimensionMessage {
|
||||
__vscode_notebook_message: boolean;
|
||||
|
@ -144,7 +145,8 @@ export class BackLayerWebView extends Disposable {
|
|||
@IWebviewService readonly webviewService: IWebviewService,
|
||||
@IOpenerService readonly openerService: IOpenerService,
|
||||
@INotebookService private readonly notebookService: INotebookService,
|
||||
@IEnvironmentService private readonly environmentService: IEnvironmentService
|
||||
@IEnvironmentService private readonly environmentService: IEnvironmentService,
|
||||
@IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService,
|
||||
) {
|
||||
super();
|
||||
this.element = document.createElement('div');
|
||||
|
@ -155,7 +157,7 @@ export class BackLayerWebView extends Disposable {
|
|||
this.element.style.margin = `0px 0 0px ${CELL_MARGIN + CELL_RUN_GUTTER}px`;
|
||||
|
||||
const pathsPath = getPathFromAmdModule(require, 'vs/loader.js');
|
||||
const loader = URI.file(pathsPath).with({ scheme: WebviewResourceScheme });
|
||||
const loader = asWebviewUri(this.workbenchEnvironmentService, this.id, URI.file(pathsPath));
|
||||
|
||||
let coreDependencies = '';
|
||||
let resolveFunc: () => void;
|
||||
|
@ -560,13 +562,14 @@ ${loaderJs}
|
|||
private _createInset(webviewService: IWebviewService, content: string) {
|
||||
const rootPath = URI.file(path.dirname(getPathFromAmdModule(require, '')));
|
||||
this.localResourceRootsCache = [...this.notebookService.getNotebookProviderResourceRoots(), rootPath];
|
||||
|
||||
const webview = webviewService.createWebviewElement(this.id, {
|
||||
enableFindWidget: false,
|
||||
}, {
|
||||
allowMultipleAPIAcquire: true,
|
||||
allowScripts: true,
|
||||
localResourceRoots: this.localResourceRootsCache
|
||||
});
|
||||
}, undefined);
|
||||
webview.html = content;
|
||||
return webview;
|
||||
}
|
||||
|
@ -742,7 +745,7 @@ ${loaderJs}
|
|||
if (this.environmentService.isExtensionDevelopment && (preloadResource.scheme === 'http' || preloadResource.scheme === 'https')) {
|
||||
return preloadResource;
|
||||
}
|
||||
return preloadResource.with({ scheme: WebviewResourceScheme });
|
||||
return asWebviewUri(this.workbenchEnvironmentService, this.id, preloadResource);
|
||||
});
|
||||
extensionLocations.push(rendererInfo.extensionLocation);
|
||||
preloadResources.forEach(e => {
|
||||
|
|
|
@ -59,13 +59,13 @@ export abstract class BaseWebview<T extends HTMLElement> extends Disposable {
|
|||
|
||||
protected content: WebviewContent;
|
||||
|
||||
public extension: WebviewExtensionDescription | undefined;
|
||||
|
||||
constructor(
|
||||
// TODO: matb, this should not be protected. The only reason it needs to be is that the base class ends up using it in the call to createElement
|
||||
protected readonly id: string,
|
||||
options: WebviewOptions,
|
||||
contentOptions: WebviewContentOptions,
|
||||
public readonly extension: WebviewExtensionDescription | undefined,
|
||||
private readonly webviewThemeDataProvider: WebviewThemeDataProvider,
|
||||
@ITelemetryService private readonly _telemetryService: ITelemetryService,
|
||||
@IEnvironmentService private readonly _environementService: IEnvironmentService,
|
||||
|
@ -79,7 +79,7 @@ export abstract class BaseWebview<T extends HTMLElement> extends Disposable {
|
|||
state: undefined
|
||||
};
|
||||
|
||||
this._element = this.createElement(options);
|
||||
this._element = this.createElement(options, contentOptions);
|
||||
|
||||
this._ready = new Promise(resolve => {
|
||||
const subscription = this._register(this.on(WebviewMessageChannels.webviewReady, () => {
|
||||
|
@ -187,7 +187,7 @@ export abstract class BaseWebview<T extends HTMLElement> extends Disposable {
|
|||
|
||||
protected abstract readonly extraContentOptions: { readonly [key: string]: string };
|
||||
|
||||
protected abstract createElement(options: WebviewOptions): T;
|
||||
protected abstract createElement(options: WebviewOptions, contentOptions: WebviewContentOptions): T;
|
||||
|
||||
protected abstract on<T = unknown>(channel: string, handler: (data: T) => void): IDisposable;
|
||||
|
||||
|
|
|
@ -28,7 +28,6 @@ export class DynamicWebviewEditorOverlay extends Disposable implements WebviewOv
|
|||
private _html: string = '';
|
||||
private _initialScrollProgress: number = 0;
|
||||
private _state: string | undefined = undefined;
|
||||
private _extension: WebviewExtensionDescription | undefined;
|
||||
|
||||
private _contentOptions: WebviewContentOptions;
|
||||
private _options: WebviewOptions;
|
||||
|
@ -42,6 +41,7 @@ export class DynamicWebviewEditorOverlay extends Disposable implements WebviewOv
|
|||
private readonly id: string,
|
||||
initialOptions: WebviewOptions,
|
||||
initialContentOptions: WebviewContentOptions,
|
||||
public readonly extension: WebviewExtensionDescription | undefined,
|
||||
@ILayoutService private readonly _layoutService: ILayoutService,
|
||||
@IWebviewService private readonly _webviewService: IWebviewService,
|
||||
@IContextKeyService private readonly _contextKeyService: IContextKeyService
|
||||
|
@ -109,11 +109,10 @@ export class DynamicWebviewEditorOverlay extends Disposable implements WebviewOv
|
|||
|
||||
private show() {
|
||||
if (!this._webview.value) {
|
||||
const webview = this._webviewService.createWebviewElement(this.id, this._options, this._contentOptions);
|
||||
const webview = this._webviewService.createWebviewElement(this.id, this._options, this._contentOptions, this.extension);
|
||||
this._webview.value = webview;
|
||||
webview.state = this._state;
|
||||
webview.html = this._html;
|
||||
webview.extension = this._extension;
|
||||
if (this._options.tryRestoreScrollPosition) {
|
||||
webview.initialScrollProgress = this._initialScrollProgress;
|
||||
}
|
||||
|
@ -175,12 +174,6 @@ export class DynamicWebviewEditorOverlay extends Disposable implements WebviewOv
|
|||
this.withWebview(webview => webview.contentOptions = value);
|
||||
}
|
||||
|
||||
public get extension() { return this._extension; }
|
||||
public set extension(value) {
|
||||
this._extension = value;
|
||||
this.withWebview(webview => webview.extension = value);
|
||||
}
|
||||
|
||||
private readonly _onDidFocus = this._register(new Emitter<void>());
|
||||
public readonly onDidFocus: Event<void> = this._onDidFocus.event;
|
||||
|
||||
|
|
|
@ -94,6 +94,10 @@
|
|||
postMessage: hostMessaging.postMessage.bind(hostMessaging),
|
||||
onMessage: hostMessaging.onMessage.bind(hostMessaging),
|
||||
ready: workerReady,
|
||||
fakeLoad: true
|
||||
fakeLoad: true,
|
||||
rewriteCSP: (csp, endpoint) => {
|
||||
const endpointUrl = new URL(endpoint);
|
||||
csp.setAttribute('content', csp.replace(/(vscode-webview-resource|vscode-resource):(?=(\s|;|$))/g, endpointUrl.origin));
|
||||
}
|
||||
});
|
||||
}());
|
||||
}());
|
||||
|
|
|
@ -11,7 +11,8 @@
|
|||
* focusIframeOnCreate?: boolean,
|
||||
* ready?: Promise<void>,
|
||||
* onIframeLoaded?: (iframe: HTMLIFrameElement) => void,
|
||||
* fakeLoad: boolean
|
||||
* fakeLoad: boolean,
|
||||
* rewriteCSP: (existingCSP: string, endpoint?: string) => string,
|
||||
* }} WebviewHost
|
||||
*/
|
||||
|
||||
|
@ -360,14 +361,10 @@
|
|||
if (!csp) {
|
||||
host.postMessage('no-csp-found');
|
||||
} else {
|
||||
// Rewrite vscode-resource in csp
|
||||
if (data.endpoint) {
|
||||
try {
|
||||
const endpointUrl = new URL(data.endpoint);
|
||||
csp.setAttribute('content', csp.getAttribute('content').replace(/vscode-resource:(?=(\s|;|$))/g, endpointUrl.origin));
|
||||
} catch (e) {
|
||||
console.error('Could not rewrite csp');
|
||||
}
|
||||
try {
|
||||
csp.setAttribute('content', host.rewriteCSP(csp.getAttribute('content'), data.endpoint));
|
||||
} catch (e) {
|
||||
console.error('Could not rewrite csp');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -40,12 +40,14 @@ export interface IWebviewService {
|
|||
id: string,
|
||||
options: WebviewOptions,
|
||||
contentOptions: WebviewContentOptions,
|
||||
extension: WebviewExtensionDescription | undefined,
|
||||
): WebviewElement;
|
||||
|
||||
createWebviewOverlay(
|
||||
id: string,
|
||||
options: WebviewOptions,
|
||||
contentOptions: WebviewContentOptions,
|
||||
extension: WebviewExtensionDescription | undefined,
|
||||
): WebviewOverlay;
|
||||
|
||||
setIcons(id: string, value: WebviewIcons | undefined): void;
|
||||
|
|
|
@ -5,19 +5,19 @@
|
|||
|
||||
import { addDisposableListener } from 'vs/base/browser/dom';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { isWeb } from 'vs/base/common/platform';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { ITunnelService } from 'vs/platform/remote/common/tunnel';
|
||||
import { Webview, WebviewContentOptions, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { loadLocalResource, WebviewResourceResponse } from 'vs/platform/webview/common/resourceLoader';
|
||||
import { BaseWebview, WebviewMessageChannels } from 'vs/workbench/contrib/webview/browser/baseWebviewElement';
|
||||
import { Webview, WebviewContentOptions, WebviewExtensionDescription, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview';
|
||||
import { WebviewPortMappingManager } from 'vs/workbench/contrib/webview/common/portMapping';
|
||||
import { loadLocalResource, WebviewResourceResponse } from 'vs/workbench/contrib/webview/common/resourceLoader';
|
||||
import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/common/themeing';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { BaseWebview, WebviewMessageChannels } from 'vs/workbench/contrib/webview/browser/baseWebviewElement';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { isWeb } from 'vs/base/common/platform';
|
||||
|
||||
export class IFrameWebview extends BaseWebview<HTMLIFrameElement> implements Webview {
|
||||
private readonly _portMappingManager: WebviewPortMappingManager;
|
||||
|
@ -26,6 +26,7 @@ export class IFrameWebview extends BaseWebview<HTMLIFrameElement> implements Web
|
|||
id: string,
|
||||
options: WebviewOptions,
|
||||
contentOptions: WebviewContentOptions,
|
||||
extension: WebviewExtensionDescription | undefined,
|
||||
webviewThemeDataProvider: WebviewThemeDataProvider,
|
||||
@ITunnelService tunnelService: ITunnelService,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
|
@ -34,7 +35,7 @@ export class IFrameWebview extends BaseWebview<HTMLIFrameElement> implements Web
|
|||
@IEnvironmentService environementService: IEnvironmentService,
|
||||
@IWorkbenchEnvironmentService workbenchEnvironmentService: IWorkbenchEnvironmentService,
|
||||
) {
|
||||
super(id, options, contentOptions, webviewThemeDataProvider, telemetryService, environementService, workbenchEnvironmentService);
|
||||
super(id, options, contentOptions, extension, webviewThemeDataProvider, telemetryService, environementService, workbenchEnvironmentService);
|
||||
|
||||
if (!this.useExternalEndpoint && (!workbenchEnvironmentService.options || typeof workbenchEnvironmentService.webviewExternalEndpoint !== 'string')) {
|
||||
throw new Error('To use iframe based webviews, you must configure `environmentService.webviewExternalEndpoint`');
|
||||
|
@ -58,7 +59,7 @@ export class IFrameWebview extends BaseWebview<HTMLIFrameElement> implements Web
|
|||
}));
|
||||
}
|
||||
|
||||
protected createElement(options: WebviewOptions) {
|
||||
protected createElement(options: WebviewOptions, contentOptions: WebviewContentOptions) {
|
||||
const element = document.createElement('iframe');
|
||||
element.className = `webview ${options.customClasses || ''}`;
|
||||
element.sandbox.add('allow-scripts', 'allow-same-origin', 'allow-forms');
|
||||
|
@ -93,7 +94,7 @@ export class IFrameWebview extends BaseWebview<HTMLIFrameElement> implements Web
|
|||
|
||||
private preprocessHtml(value: string): string {
|
||||
return value
|
||||
.replace(/(["'])vscode-resource:(\/\/([^\s\/'"]+?)(?=\/))?([^\s'"]+?)(["'])/gi, (match, startQuote, _1, scheme, path, endQuote) => {
|
||||
.replace(/(["'])(?:vscode-resource|vscode-webview-resource):(\/\/([^\s\/'"]+?)(?=\/))?([^\s'"]+?)(["'])/gi, (match, startQuote, _1, scheme, path, endQuote) => {
|
||||
if (scheme) {
|
||||
return `${startQuote}${this.externalEndpoint}/vscode-resource/${scheme}${path}${endQuote}`;
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IWebviewService, WebviewContentOptions, WebviewOverlay, WebviewElement, WebviewIcons, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview';
|
||||
import { IWebviewService, WebviewContentOptions, WebviewOverlay, WebviewElement, WebviewIcons, WebviewOptions, WebviewExtensionDescription } from 'vs/workbench/contrib/webview/browser/webview';
|
||||
import { IFrameWebview } from 'vs/workbench/contrib/webview/browser/webviewElement';
|
||||
import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/common/themeing';
|
||||
import { DynamicWebviewEditorOverlay } from './dynamicWebviewEditorOverlay';
|
||||
|
@ -27,17 +27,19 @@ export class WebviewService implements IWebviewService {
|
|||
createWebviewElement(
|
||||
id: string,
|
||||
options: WebviewOptions,
|
||||
contentOptions: WebviewContentOptions
|
||||
contentOptions: WebviewContentOptions,
|
||||
extension: WebviewExtensionDescription | undefined,
|
||||
): WebviewElement {
|
||||
return this._instantiationService.createInstance(IFrameWebview, id, options, contentOptions, this._webviewThemeDataProvider);
|
||||
return this._instantiationService.createInstance(IFrameWebview, id, options, contentOptions, extension, this._webviewThemeDataProvider);
|
||||
}
|
||||
|
||||
createWebviewOverlay(
|
||||
id: string,
|
||||
options: WebviewOptions,
|
||||
contentOptions: WebviewContentOptions,
|
||||
extension: WebviewExtensionDescription | undefined,
|
||||
): WebviewOverlay {
|
||||
return this._instantiationService.createInstance(DynamicWebviewEditorOverlay, id, options, contentOptions);
|
||||
return this._instantiationService.createInstance(DynamicWebviewEditorOverlay, id, options, contentOptions, extension);
|
||||
}
|
||||
|
||||
setIcons(id: string, iconPath: WebviewIcons | undefined): void {
|
||||
|
|
|
@ -307,14 +307,12 @@ export class WebviewEditorService implements IWebviewWorkbenchService {
|
|||
private createWebviewElement(
|
||||
id: string,
|
||||
extension: WebviewExtensionDescription | undefined,
|
||||
options: WebviewInputOptions
|
||||
options: WebviewInputOptions,
|
||||
) {
|
||||
const webview = this._webviewService.createWebviewOverlay(id, {
|
||||
return this._webviewService.createWebviewOverlay(id, {
|
||||
enableFindWidget: options.enableFindWidget,
|
||||
retainContextWhenHidden: options.retainContextWhenHidden
|
||||
}, options);
|
||||
webview.extension = extension;
|
||||
return webview;
|
||||
}, options, extension);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
25
src/vs/workbench/contrib/webview/common/webviewUri.ts
Normal file
25
src/vs/workbench/contrib/webview/common/webviewUri.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
|
||||
|
||||
export function asWebviewUri(
|
||||
environmentService: IWorkbenchEnvironmentService,
|
||||
uuid: string,
|
||||
resource: URI,
|
||||
): URI {
|
||||
const uri = environmentService.webviewResourceRoot
|
||||
// Make sure we preserve the scheme of the resource but convert it into a normal path segment
|
||||
// The scheme is important as we need to know if we are requesting a local or a remote resource.
|
||||
.replace('{{resource}}', resource.scheme + withoutScheme(resource))
|
||||
.replace('{{uuid}}', uuid);
|
||||
return URI.parse(uri);
|
||||
}
|
||||
|
||||
function withoutScheme(resource: URI): string {
|
||||
return resource.toString().replace(/^\S+?:/, '');
|
||||
}
|
|
@ -62,7 +62,10 @@
|
|||
isMouseDown = false;
|
||||
});
|
||||
newFrame.contentWindow.addEventListener('mousemove', tryDispatchSyntheticMouseEvent);
|
||||
}
|
||||
},
|
||||
rewriteCSP: (csp) => {
|
||||
return csp.replace(/vscode-resource:(?=(\s|;|$))/g, 'vscode-webview-resource:');
|
||||
},
|
||||
};
|
||||
|
||||
host.onMessage('devtools-opened', () => {
|
||||
|
|
|
@ -3,12 +3,13 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { FindInPageOptions, OnBeforeRequestListenerDetails, OnHeadersReceivedListenerDetails, Response, WebContents, WebviewTag } from 'electron';
|
||||
import { FindInPageOptions, OnBeforeRequestListenerDetails, OnHeadersReceivedListenerDetails, Response, WebContents, WebviewTag, ipcRenderer } from 'electron';
|
||||
import { addDisposableListener } from 'vs/base/browser/dom';
|
||||
import { ThrottledDelayer } from 'vs/base/common/async';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { once } from 'vs/base/common/functional';
|
||||
import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { isMacintosh } from 'vs/base/common/platform';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import * as modes from 'vs/editor/common/modes';
|
||||
|
@ -21,9 +22,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
|||
import { BaseWebview, WebviewMessageChannels } from 'vs/workbench/contrib/webview/browser/baseWebviewElement';
|
||||
import { Webview, WebviewContentOptions, WebviewExtensionDescription, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview';
|
||||
import { WebviewPortMappingManager } from 'vs/workbench/contrib/webview/common/portMapping';
|
||||
import { WebviewResourceScheme } from 'vs/workbench/contrib/webview/common/resourceLoader';
|
||||
import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/common/themeing';
|
||||
import { registerFileProtocol } from 'vs/workbench/contrib/webview/electron-browser/webviewProtocols';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { WebviewFindDelegate, WebviewFindWidget } from '../browser/webviewFindWidget';
|
||||
|
||||
|
@ -116,32 +115,29 @@ class WebviewSession extends Disposable {
|
|||
|
||||
class WebviewProtocolProvider extends Disposable {
|
||||
|
||||
private _resolve!: () => void;
|
||||
private _reject!: () => void;
|
||||
|
||||
public readonly ready: Promise<void>;
|
||||
|
||||
constructor(
|
||||
handle: WebviewTagHandle,
|
||||
getExtensionLocation: () => URI | undefined,
|
||||
private readonly id: string,
|
||||
extension: WebviewExtensionDescription | undefined,
|
||||
getLocalResourceRoots: () => ReadonlyArray<URI>,
|
||||
fileService: IFileService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.ready = new Promise((resolve, reject) => {
|
||||
this._resolve = resolve;
|
||||
this._reject = reject;
|
||||
ipcRenderer.send('vscode:registerWebview', id, {
|
||||
extensionLocation: extension?.location.toJSON(),
|
||||
localResourceRoots: getLocalResourceRoots().map(x => x.toJSON()),
|
||||
});
|
||||
|
||||
this._register(handle.onFirstLoad(contents => {
|
||||
try {
|
||||
registerFileProtocol(contents, WebviewResourceScheme, fileService, getExtensionLocation(), getLocalResourceRoots);
|
||||
this._resolve();
|
||||
} catch {
|
||||
this._reject();
|
||||
}
|
||||
}));
|
||||
this.ready = new Promise((resolve) => {
|
||||
ipcRenderer.once(`vscode:didRegisterWebview-${id}`, () => resolve());
|
||||
});
|
||||
}
|
||||
|
||||
dispose() {
|
||||
super.dispose();
|
||||
|
||||
ipcRenderer.send('vscode:unregisterWebview', this.id);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -236,7 +232,7 @@ export class ElectronWebviewBasedWebview extends BaseWebview<WebviewTag> impleme
|
|||
private _webviewFindWidget: WebviewFindWidget | undefined;
|
||||
private _findStarted: boolean = false;
|
||||
|
||||
public extension: WebviewExtensionDescription | undefined;
|
||||
public readonly extension: WebviewExtensionDescription | undefined;
|
||||
private readonly _protocolProvider: WebviewProtocolProvider;
|
||||
|
||||
private readonly _domReady: Promise<void>;
|
||||
|
@ -247,6 +243,7 @@ export class ElectronWebviewBasedWebview extends BaseWebview<WebviewTag> impleme
|
|||
id: string,
|
||||
options: WebviewOptions,
|
||||
contentOptions: WebviewContentOptions,
|
||||
extension: WebviewExtensionDescription | undefined,
|
||||
private readonly _webviewThemeDataProvider: WebviewThemeDataProvider,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IFileService fileService: IFileService,
|
||||
|
@ -256,15 +253,12 @@ export class ElectronWebviewBasedWebview extends BaseWebview<WebviewTag> impleme
|
|||
@IWorkbenchEnvironmentService workbenchEnvironmentService: IWorkbenchEnvironmentService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
) {
|
||||
super(id, options, contentOptions, _webviewThemeDataProvider, telemetryService, environementService, workbenchEnvironmentService);
|
||||
super(id, options, contentOptions, extension, _webviewThemeDataProvider, telemetryService, environementService, workbenchEnvironmentService);
|
||||
|
||||
const webviewAndContents = this._register(new WebviewTagHandle(this.element!));
|
||||
const session = this._register(new WebviewSession(webviewAndContents));
|
||||
|
||||
this._protocolProvider = new WebviewProtocolProvider(webviewAndContents,
|
||||
() => this.extension?.location,
|
||||
() => (this.content.options.localResourceRoots || []),
|
||||
fileService);
|
||||
this._protocolProvider = new WebviewProtocolProvider(id, extension, () => (this.content.options.localResourceRoots || []));
|
||||
this._register(this._protocolProvider);
|
||||
|
||||
this._register(new WebviewPortMappingProvider(
|
||||
|
@ -345,7 +339,6 @@ export class ElectronWebviewBasedWebview extends BaseWebview<WebviewTag> impleme
|
|||
element.focus = () => {
|
||||
this.doFocus();
|
||||
};
|
||||
element.setAttribute('partition', `webview${Date.now()}`);
|
||||
element.setAttribute('webpreferences', 'contextIsolation=yes');
|
||||
element.className = `webview ${options.customClasses || ''}`;
|
||||
|
||||
|
@ -362,6 +355,21 @@ export class ElectronWebviewBasedWebview extends BaseWebview<WebviewTag> impleme
|
|||
|
||||
protected readonly extraContentOptions = {};
|
||||
|
||||
public set html(value: string) {
|
||||
super.html = this.preprocessHtml(value);
|
||||
}
|
||||
|
||||
private preprocessHtml(value: string): string {
|
||||
return value
|
||||
.replace(/(["'])vscode-resource:(\/\/([^\s\/'"]+?)(?=\/))?([^\s'"]+?)(["'])/gi, (match, startQuote, _1, scheme, path, endQuote) => {
|
||||
if (scheme) {
|
||||
return `${startQuote}${Schemas.vscodeWebviewResource}:${this.id}//${scheme}${path}${endQuote}`;
|
||||
}
|
||||
return `${startQuote}${Schemas.vscodeWebviewResource}:${this.id}//file${path}${endQuote}`;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public mountTo(parent: HTMLElement) {
|
||||
if (!this.element) {
|
||||
return;
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as electron from 'electron';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { loadLocalResource, WebviewResourceResponse } from 'vs/workbench/contrib/webview/common/resourceLoader';
|
||||
|
||||
export function registerFileProtocol(
|
||||
contents: electron.WebContents,
|
||||
protocol: string,
|
||||
fileService: IFileService,
|
||||
extensionLocation: URI | undefined,
|
||||
getRoots: () => ReadonlyArray<URI>
|
||||
) {
|
||||
contents.session.protocol.registerBufferProtocol(protocol, async (request, callback: any) => {
|
||||
try {
|
||||
const result = await loadLocalResource(URI.parse(request.url), fileService, extensionLocation, getRoots);
|
||||
if (result.type === WebviewResourceResponse.Type.Success) {
|
||||
return callback({
|
||||
data: Buffer.from(result.data.buffer),
|
||||
mimeType: result.mimeType
|
||||
});
|
||||
}
|
||||
if (result.type === WebviewResourceResponse.Type.AccessDenied) {
|
||||
console.error('Webview: Cannot load resource outside of protocol root');
|
||||
return callback({ error: -10 /* ACCESS_DENIED: https://cs.chromium.org/chromium/src/net/base/net_error_list.h */ });
|
||||
}
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
|
||||
return callback({ error: -2 /* FAILED: https://cs.chromium.org/chromium/src/net/base/net_error_list.h */ });
|
||||
});
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { DynamicWebviewEditorOverlay } from 'vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay';
|
||||
import { IWebviewService, WebviewContentOptions, WebviewOverlay, WebviewElement, WebviewIcons, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview';
|
||||
import { IWebviewService, WebviewContentOptions, WebviewElement, WebviewExtensionDescription, WebviewIcons, WebviewOptions, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview';
|
||||
import { IFrameWebview } from 'vs/workbench/contrib/webview/browser/webviewElement';
|
||||
import { WebviewIconManager } from 'vs/workbench/contrib/webview/browser/webviewIconManager';
|
||||
import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/common/themeing';
|
||||
|
@ -29,13 +29,14 @@ export class ElectronWebviewService implements IWebviewService {
|
|||
createWebviewElement(
|
||||
id: string,
|
||||
options: WebviewOptions,
|
||||
contentOptions: WebviewContentOptions
|
||||
contentOptions: WebviewContentOptions,
|
||||
extension: WebviewExtensionDescription | undefined,
|
||||
): WebviewElement {
|
||||
const useExternalEndpoint = this._configService.getValue<string>('webview.experimental.useExternalEndpoint');
|
||||
if (useExternalEndpoint) {
|
||||
return this._instantiationService.createInstance(IFrameWebview, id, options, contentOptions, this._webviewThemeDataProvider);
|
||||
return this._instantiationService.createInstance(IFrameWebview, id, options, contentOptions, extension, this._webviewThemeDataProvider);
|
||||
} else {
|
||||
return this._instantiationService.createInstance(ElectronWebviewBasedWebview, id, options, contentOptions, this._webviewThemeDataProvider);
|
||||
return this._instantiationService.createInstance(ElectronWebviewBasedWebview, id, options, contentOptions, extension, this._webviewThemeDataProvider);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -43,8 +44,9 @@ export class ElectronWebviewService implements IWebviewService {
|
|||
id: string,
|
||||
options: WebviewOptions,
|
||||
contentOptions: WebviewContentOptions,
|
||||
extension: WebviewExtensionDescription | undefined,
|
||||
): WebviewOverlay {
|
||||
return this._instantiationService.createInstance(DynamicWebviewEditorOverlay, id, options, contentOptions);
|
||||
return this._instantiationService.createInstance(DynamicWebviewEditorOverlay, id, options, contentOptions, extension);
|
||||
}
|
||||
|
||||
setIcons(id: string, iconPath: WebviewIcons | undefined): void {
|
||||
|
|
|
@ -42,10 +42,10 @@ export class NativeWorkbenchEnvironmentService extends EnvironmentService implem
|
|||
}
|
||||
|
||||
@memoize
|
||||
get webviewResourceRoot(): string { return 'vscode-resource://{{resource}}'; }
|
||||
get webviewResourceRoot(): string { return `${Schemas.vscodeWebviewResource}://{{uuid}}/{{resource}}`; }
|
||||
|
||||
@memoize
|
||||
get webviewCspSource(): string { return 'vscode-resource:'; }
|
||||
get webviewCspSource(): string { return `${Schemas.vscodeWebviewResource}:`; }
|
||||
|
||||
@memoize
|
||||
get userRoamingDataHome(): URI { return this.appSettingsHome.with({ scheme: Schemas.userData }); }
|
||||
|
|
Loading…
Reference in a new issue