Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'

This commit is contained in:
Joe Previte
2020-12-15 15:52:33 -07:00
4649 changed files with 1311795 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { getMediaMime, MIME_UNKNOWN } from 'vs/base/common/mime';
import { extname } from 'vs/base/common/path';
import { URI } from 'vs/base/common/uri';
const webviewMimeTypes = new Map([
['.svg', 'image/svg+xml'],
['.txt', 'text/plain'],
['.css', 'text/css'],
['.js', 'application/javascript'],
['.json', 'application/json'],
['.html', 'text/html'],
['.htm', 'text/html'],
['.xhtml', 'application/xhtml+xml'],
['.oft', 'font/otf'],
['.xml', 'application/xml'],
]);
export function getWebviewContentMimeType(resource: URI): string {
const ext = extname(resource.fsPath).toLowerCase();
return webviewMimeTypes.get(ext) || getMediaMime(resource.fsPath) || MIME_UNKNOWN;
}

View File

@@ -0,0 +1,130 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { VSBufferReadableStream } from 'vs/base/common/buffer';
import { CancellationToken } from 'vs/base/common/cancellation';
import { isUNC } from 'vs/base/common/extpath';
import { Schemas } from 'vs/base/common/network';
import { sep } from 'vs/base/common/path';
import { URI } from 'vs/base/common/uri';
import { IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { IRequestService } from 'vs/platform/request/common/request';
import { getWebviewContentMimeType } from 'vs/platform/webview/common/mimeTypes';
export const webviewPartitionId = 'webview';
export namespace WebviewResourceResponse {
export enum Type { Success, Failed, AccessDenied }
export class StreamSuccess {
readonly type = Type.Success;
constructor(
public readonly stream: VSBufferReadableStream,
public readonly mimeType: string
) { }
}
export const Failed = { type: Type.Failed } as const;
export const AccessDenied = { type: Type.AccessDenied } as const;
export type StreamResponse = StreamSuccess | typeof Failed | typeof AccessDenied;
}
interface FileReader {
readFileStream(resource: URI): Promise<VSBufferReadableStream>;
}
export async function loadLocalResource(
requestUri: URI,
options: {
extensionLocation: URI | undefined;
roots: ReadonlyArray<URI>;
remoteConnectionData?: IRemoteConnectionData | null;
rewriteUri?: (uri: URI) => URI,
},
fileReader: FileReader,
requestService: IRequestService,
): Promise<WebviewResourceResponse.StreamResponse> {
let resourceToLoad = getResourceToLoad(requestUri, options.roots);
if (!resourceToLoad) {
return WebviewResourceResponse.AccessDenied;
}
const mime = getWebviewContentMimeType(requestUri); // Use the original path for the mime
// Perform extra normalization if needed
if (options.rewriteUri) {
resourceToLoad = options.rewriteUri(resourceToLoad);
}
if (resourceToLoad.scheme === Schemas.http || resourceToLoad.scheme === Schemas.https) {
const response = await requestService.request({ url: resourceToLoad.toString(true) }, CancellationToken.None);
if (response.res.statusCode === 200) {
return new WebviewResourceResponse.StreamSuccess(response.stream, mime);
}
return WebviewResourceResponse.Failed;
}
try {
const contents = await fileReader.readFileStream(resourceToLoad);
return new WebviewResourceResponse.StreamSuccess(contents, mime);
} catch (err) {
console.log(err);
return WebviewResourceResponse.Failed;
}
}
function getResourceToLoad(
requestUri: URI,
roots: ReadonlyArray<URI>
): URI | undefined {
const normalizedPath = normalizeRequestPath(requestUri);
for (const root of roots) {
if (containsResource(root, normalizedPath)) {
return normalizedPath;
}
}
return undefined;
}
function normalizeRequestPath(requestUri: URI) {
if (requestUri.scheme === Schemas.vscodeWebviewResource) {
// The `vscode-webview-resource` scheme has the following format:
//
// vscode-webview-resource://id/scheme//authority?/path
//
// Encode requestUri.path so that URI.parse can properly parse special characters like '#', '?', etc.
const resourceUri = URI.parse(encodeURIComponent(requestUri.path).replace(/%2F/gi, '/').replace(/^\/([a-z0-9\-]+)(\/{1,2})/i, (_: string, scheme: string, sep: string) => {
if (sep.length === 1) {
return `${scheme}:///`; // Add empty authority.
} else {
return `${scheme}://`; // Url has own authority.
}
}));
return resourceUri.with({
query: requestUri.query,
fragment: requestUri.fragment
});
} else {
return requestUri;
}
}
function containsResource(root: URI, resource: URI): boolean {
let rootPath = root.fsPath + (root.fsPath.endsWith(sep) ? '' : sep);
let resourceFsPath = resource.fsPath;
if (isUNC(root.fsPath) && isUNC(resource.fsPath)) {
rootPath = rootPath.toLowerCase();
resourceFsPath = resourceFsPath.toLowerCase();
}
return resourceFsPath.startsWith(rootPath);
}

View File

@@ -0,0 +1,31 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { VSBuffer } from 'vs/base/common/buffer';
import { UriComponents } from 'vs/base/common/uri';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { IWebviewPortMapping } from 'vs/platform/webview/common/webviewPortMapping';
export const IWebviewManagerService = createDecorator<IWebviewManagerService>('webviewManagerService');
export interface IWebviewManagerService {
_serviceBrand: unknown;
registerWebview(id: string, windowId: number, metadata: RegisterWebviewMetadata): Promise<void>;
unregisterWebview(id: string): Promise<void>;
updateWebviewMetadata(id: string, metadataDelta: Partial<RegisterWebviewMetadata>): Promise<void>;
didLoadResource(requestId: number, content: VSBuffer | undefined): void;
setIgnoreMenuShortcuts(webContentsId: number, enabled: boolean): Promise<void>;
}
export interface RegisterWebviewMetadata {
readonly extensionLocation: UriComponents | undefined;
readonly localResourceRoots: readonly UriComponents[];
readonly remoteConnectionData: IRemoteConnectionData | null;
readonly portMappings: readonly IWebviewPortMapping[];
}

View File

@@ -0,0 +1,81 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IDisposable } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import { URI } from 'vs/base/common/uri';
import { IAddress } from 'vs/platform/remote/common/remoteAgentConnection';
import { extractLocalHostUriMetaDataForPortMapping, ITunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel';
export interface IWebviewPortMapping {
webviewPort: number;
extensionHostPort: number;
}
/**
* Manages port mappings for a single webview.
*/
export class WebviewPortMappingManager implements IDisposable {
private readonly _tunnels = new Map<number, Promise<RemoteTunnel>>();
constructor(
private readonly _getExtensionLocation: () => URI | undefined,
private readonly _getMappings: () => readonly IWebviewPortMapping[],
private readonly tunnelService: ITunnelService
) { }
public async getRedirect(resolveAuthority: IAddress | null | undefined, url: string): Promise<string | undefined> {
const uri = URI.parse(url);
const requestLocalHostInfo = extractLocalHostUriMetaDataForPortMapping(uri);
if (!requestLocalHostInfo) {
return undefined;
}
for (const mapping of this._getMappings()) {
if (mapping.webviewPort === requestLocalHostInfo.port) {
const extensionLocation = this._getExtensionLocation();
if (extensionLocation && extensionLocation.scheme === Schemas.vscodeRemote) {
const tunnel = resolveAuthority && await this.getOrCreateTunnel(resolveAuthority, mapping.extensionHostPort);
if (tunnel) {
if (tunnel.tunnelLocalPort === mapping.webviewPort) {
return undefined;
}
return encodeURI(uri.with({
authority: `127.0.0.1:${tunnel.tunnelLocalPort}`,
}).toString(true));
}
}
if (mapping.webviewPort !== mapping.extensionHostPort) {
return encodeURI(uri.with({
authority: `${requestLocalHostInfo.address}:${mapping.extensionHostPort}`
}).toString(true));
}
}
}
return undefined;
}
dispose() {
for (const tunnel of this._tunnels.values()) {
tunnel.then(tunnel => tunnel.dispose());
}
this._tunnels.clear();
}
private getOrCreateTunnel(remoteAuthority: IAddress, remotePort: number): Promise<RemoteTunnel> | undefined {
const existing = this._tunnels.get(remotePort);
if (existing) {
return existing;
}
const tunnel = this.tunnelService.openTunnel({ getAddress: async () => remoteAuthority }, undefined, remotePort);
if (tunnel) {
this._tunnels.set(remotePort, tunnel);
}
return tunnel;
}
}

View File

@@ -0,0 +1,86 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { webContents } from 'electron';
import { VSBuffer } from 'vs/base/common/buffer';
import { Disposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { IFileService } from 'vs/platform/files/common/files';
import { ITunnelService } from 'vs/platform/remote/common/tunnel';
import { IRequestService } from 'vs/platform/request/common/request';
import { IWebviewManagerService, RegisterWebviewMetadata } from 'vs/platform/webview/common/webviewManagerService';
import { WebviewPortMappingProvider } from 'vs/platform/webview/electron-main/webviewPortMappingProvider';
import { WebviewProtocolProvider } from 'vs/platform/webview/electron-main/webviewProtocolProvider';
import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows';
export class WebviewMainService extends Disposable implements IWebviewManagerService {
declare readonly _serviceBrand: undefined;
private readonly protocolProvider: WebviewProtocolProvider;
private readonly portMappingProvider: WebviewPortMappingProvider;
constructor(
@IFileService fileService: IFileService,
@IRequestService requestService: IRequestService,
@ITunnelService tunnelService: ITunnelService,
@IWindowsMainService windowsMainService: IWindowsMainService,
) {
super();
this.protocolProvider = this._register(new WebviewProtocolProvider(fileService, requestService, windowsMainService));
this.portMappingProvider = this._register(new WebviewPortMappingProvider(tunnelService));
}
public async registerWebview(id: string, windowId: number, metadata: RegisterWebviewMetadata): Promise<void> {
const extensionLocation = metadata.extensionLocation ? URI.from(metadata.extensionLocation) : undefined;
this.protocolProvider.registerWebview(id, {
...metadata,
windowId: windowId,
extensionLocation,
localResourceRoots: metadata.localResourceRoots.map(x => URI.from(x))
});
this.portMappingProvider.registerWebview(id, {
extensionLocation,
mappings: metadata.portMappings,
resolvedAuthority: metadata.remoteConnectionData,
});
}
public async unregisterWebview(id: string): Promise<void> {
this.protocolProvider.unregisterWebview(id);
this.portMappingProvider.unregisterWebview(id);
}
public async updateWebviewMetadata(id: string, metaDataDelta: Partial<RegisterWebviewMetadata>): Promise<void> {
const extensionLocation = metaDataDelta.extensionLocation ? URI.from(metaDataDelta.extensionLocation) : undefined;
this.protocolProvider.updateWebviewMetadata(id, {
...metaDataDelta,
extensionLocation,
localResourceRoots: metaDataDelta.localResourceRoots?.map(x => URI.from(x)),
});
this.portMappingProvider.updateWebviewMetadata(id, {
...metaDataDelta,
extensionLocation,
});
}
public async setIgnoreMenuShortcuts(webContentsId: number, enabled: boolean): Promise<void> {
const contents = webContents.fromId(webContentsId);
if (!contents) {
throw new Error(`Invalid webContentsId: ${webContentsId}`);
}
if (!contents.isDestroyed()) {
contents.setIgnoreMenuShortcuts(enabled);
}
}
public async didLoadResource(requestId: number, content: VSBuffer | undefined): Promise<void> {
this.protocolProvider.didLoadResource(requestId, content);
}
}

View File

@@ -0,0 +1,89 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { OnBeforeRequestListenerDetails, session } from 'electron';
import { Disposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { IAddress } from 'vs/platform/remote/common/remoteAgentConnection';
import { ITunnelService } from 'vs/platform/remote/common/tunnel';
import { webviewPartitionId } from 'vs/platform/webview/common/resourceLoader';
import { IWebviewPortMapping, WebviewPortMappingManager } from 'vs/platform/webview/common/webviewPortMapping';
interface OnBeforeRequestListenerDetails_Extended extends OnBeforeRequestListenerDetails {
readonly lastCommittedOrigin?: string;
}
interface PortMappingData {
readonly extensionLocation: URI | undefined;
readonly mappings: readonly IWebviewPortMapping[];
readonly resolvedAuthority: IAddress | null | undefined;
}
export class WebviewPortMappingProvider extends Disposable {
private readonly _webviewData = new Map<string, {
readonly manager: WebviewPortMappingManager;
metadata: PortMappingData;
}>();
constructor(
@ITunnelService private readonly _tunnelService: ITunnelService,
) {
super();
const sess = session.fromPartition(webviewPartitionId);
sess.webRequest.onBeforeRequest({
urls: [
'*://localhost:*/*',
'*://127.0.0.1:*/*',
'*://0.0.0.0:*/*',
]
}, async (details: OnBeforeRequestListenerDetails_Extended, callback) => {
let origin: URI;
try {
origin = URI.parse(details.lastCommittedOrigin!);
} catch {
return callback({});
}
const webviewId = origin.authority;
const entry = this._webviewData.get(webviewId);
if (!entry) {
return callback({});
}
const redirect = await entry.manager.getRedirect(entry.metadata.resolvedAuthority, details.url);
return callback(redirect ? { redirectURL: redirect } : {});
});
}
public async registerWebview(id: string, metadata: PortMappingData): Promise<void> {
const manager = new WebviewPortMappingManager(
() => this._webviewData.get(id)?.metadata.extensionLocation,
() => this._webviewData.get(id)?.metadata.mappings || [],
this._tunnelService);
this._webviewData.set(id, { metadata, manager });
}
public unregisterWebview(id: string): void {
const existing = this._webviewData.get(id);
if (existing) {
existing.manager.dispose();
this._webviewData.delete(id);
}
}
public async updateWebviewMetadata(id: string, metadataDelta: Partial<PortMappingData>): Promise<void> {
const entry = this._webviewData.get(id);
if (entry) {
this._webviewData.set(id, {
...entry,
...metadataDelta,
});
}
}
}

View File

@@ -0,0 +1,241 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { protocol, session } from 'electron';
import { Readable } from 'stream';
import { bufferToStream, VSBuffer, VSBufferReadableStream } from 'vs/base/common/buffer';
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { FileAccess, Schemas } from 'vs/base/common/network';
import { URI } from 'vs/base/common/uri';
import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files';
import { IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { IRequestService } from 'vs/platform/request/common/request';
import { loadLocalResource, webviewPartitionId, WebviewResourceResponse } from 'vs/platform/webview/common/resourceLoader';
import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows';
interface WebviewMetadata {
readonly windowId: number;
readonly extensionLocation: URI | undefined;
readonly localResourceRoots: readonly URI[];
readonly remoteConnectionData: IRemoteConnectionData | null;
}
export class WebviewProtocolProvider extends Disposable {
private static validWebviewFilePaths = new Map([
['/index.html', 'index.html'],
['/electron-browser/index.html', 'index.html'],
['/main.js', 'main.js'],
['/host.js', 'host.js'],
]);
private readonly webviewMetadata = new Map<string, WebviewMetadata>();
private requestIdPool = 1;
private readonly pendingResourceReads = new Map<number, { resolve: (content: VSBuffer | undefined) => void }>();
constructor(
@IFileService private readonly fileService: IFileService,
@IRequestService private readonly requestService: IRequestService,
@IWindowsMainService readonly windowsMainService: IWindowsMainService,
) {
super();
const sess = session.fromPartition(webviewPartitionId);
// Register the protocol loading webview html
const webviewHandler = this.handleWebviewRequest.bind(this);
protocol.registerFileProtocol(Schemas.vscodeWebview, webviewHandler);
sess.protocol.registerFileProtocol(Schemas.vscodeWebview, webviewHandler);
// Register the protocol loading webview resources both inside the webview and at the top level
const webviewResourceHandler = this.handleWebviewResourceRequest.bind(this);
protocol.registerStreamProtocol(Schemas.vscodeWebviewResource, webviewResourceHandler);
sess.protocol.registerStreamProtocol(Schemas.vscodeWebviewResource, webviewResourceHandler);
this._register(toDisposable(() => {
protocol.unregisterProtocol(Schemas.vscodeWebviewResource);
sess.protocol.unregisterProtocol(Schemas.vscodeWebviewResource);
protocol.unregisterProtocol(Schemas.vscodeWebview);
sess.protocol.unregisterProtocol(Schemas.vscodeWebview);
}));
}
private streamToNodeReadable(stream: VSBufferReadableStream): Readable {
return new class extends Readable {
private listening = false;
_read(size?: number): void {
if (!this.listening) {
this.listening = true;
// Data
stream.on('data', data => {
try {
if (!this.push(data.buffer)) {
stream.pause(); // pause the stream if we should not push anymore
}
} catch (error) {
this.emit(error);
}
});
// End
stream.on('end', () => {
try {
this.push(null); // signal EOS
} catch (error) {
this.emit(error);
}
});
// Error
stream.on('error', error => this.emit('error', error));
}
// ensure the stream is flowing
stream.resume();
}
_destroy(error: Error | null, callback: (error: Error | null) => void): void {
stream.destroy();
callback(null);
}
};
}
public async registerWebview(id: string, metadata: WebviewMetadata): Promise<void> {
this.webviewMetadata.set(id, metadata);
}
public unregisterWebview(id: string): void {
this.webviewMetadata.delete(id);
}
public async updateWebviewMetadata(id: string, metadataDelta: Partial<WebviewMetadata>): Promise<void> {
const entry = this.webviewMetadata.get(id);
if (entry) {
this.webviewMetadata.set(id, {
...entry,
...metadataDelta,
});
}
}
private async handleWebviewRequest(request: Electron.Request, callback: any) {
try {
const uri = URI.parse(request.url);
const entry = WebviewProtocolProvider.validWebviewFilePaths.get(uri.path);
if (typeof entry === 'string') {
const relativeResourcePath = uri.path.startsWith('/electron-browser')
? `vs/workbench/contrib/webview/electron-browser/pre/${entry}`
: `vs/workbench/contrib/webview/browser/pre/${entry}`;
const url = FileAccess.asFileUri(relativeResourcePath, require);
return callback(decodeURIComponent(url.fsPath));
}
} catch {
// noop
}
callback({ error: -10 /* ACCESS_DENIED - https://cs.chromium.org/chromium/src/net/base/net_error_list.h?l=32 */ });
}
private async handleWebviewResourceRequest(
request: Electron.Request,
callback: (stream?: NodeJS.ReadableStream | Electron.StreamProtocolResponse | undefined) => void
) {
try {
const uri = URI.parse(request.url);
const id = uri.authority;
const metadata = this.webviewMetadata.get(id);
if (metadata) {
// Try to further rewrite remote uris so that they go to the resolved server on the main thread
let rewriteUri: undefined | ((uri: URI) => URI);
if (metadata.remoteConnectionData) {
rewriteUri = (uri) => {
if (metadata.remoteConnectionData) {
if (uri.scheme === Schemas.vscodeRemote || (metadata.extensionLocation?.scheme === Schemas.vscodeRemote)) {
return URI.parse(`http://${metadata.remoteConnectionData.host}:${metadata.remoteConnectionData.port}`).with({
path: '/vscode-remote-resource',
query: `tkn=${metadata.remoteConnectionData.connectionToken}&path=${encodeURIComponent(uri.path)}`,
});
}
}
return uri;
};
}
const fileService = {
readFileStream: async (resource: URI): Promise<VSBufferReadableStream> => {
if (resource.scheme === Schemas.file) {
return (await this.fileService.readFileStream(resource)).value;
}
// Unknown uri scheme. Try delegating the file read back to the renderer
// process which should have a file system provider registered for the uri.
const window = this.windowsMainService.getWindowById(metadata.windowId);
if (!window) {
throw new FileOperationError('Could not find window for resource', FileOperationResult.FILE_NOT_FOUND);
}
const requestId = this.requestIdPool++;
const p = new Promise<VSBuffer | undefined>(resolve => {
this.pendingResourceReads.set(requestId, { resolve });
});
window.send(`vscode:loadWebviewResource-${id}`, requestId, uri);
const result = await p;
if (!result) {
throw new FileOperationError('Could not read file', FileOperationResult.FILE_NOT_FOUND);
}
return bufferToStream(result);
}
};
const result = await loadLocalResource(uri, {
extensionLocation: metadata.extensionLocation,
roots: metadata.localResourceRoots,
remoteConnectionData: metadata.remoteConnectionData,
rewriteUri,
}, fileService, this.requestService);
if (result.type === WebviewResourceResponse.Type.Success) {
return callback({
statusCode: 200,
data: this.streamToNodeReadable(result.stream),
headers: {
'Content-Type': result.mimeType,
'Access-Control-Allow-Origin': '*',
}
});
}
if (result.type === WebviewResourceResponse.Type.AccessDenied) {
console.error('Webview: Cannot load resource outside of protocol root');
return callback({ data: null, statusCode: 401 });
}
}
} catch {
// noop
}
return callback({ data: null, statusCode: 404 });
}
public didLoadResource(requestId: number, content: VSBuffer | undefined) {
const pendingRead = this.pendingResourceReads.get(requestId);
if (!pendingRead) {
throw new Error('Unknown request');
}
this.pendingResourceReads.delete(requestId);
pendingRead.resolve(content);
}
}