Skip to content

Commit

Permalink
Transform text/uri-list mime values in data transfers
Browse files Browse the repository at this point in the history
Fixes microsoft#165636

This passes any `text/uri-list` mime values in a datatransfer through our uri transformers. Because these uri values are stored as a list of strings, they are not automatically transformed
  • Loading branch information
mjbvz committed Nov 8, 2022
1 parent 8642c74 commit 3798acf
Show file tree
Hide file tree
Showing 10 changed files with 85 additions and 47 deletions.
15 changes: 15 additions & 0 deletions src/vs/base/common/dataTransfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { distinct } from 'vs/base/common/arrays';
import { URI } from 'vs/base/common/uri';
import { generateUuid } from 'vs/base/common/uuid';

Expand Down Expand Up @@ -92,3 +93,17 @@ export class VSDataTransfer {
return mimeType.toLowerCase();
}
}


export const UriList = Object.freeze({
// http://amundsen.com/hypermedia/urilist/
create: (entries: ReadonlyArray<string | URI>): string => {
return distinct(entries.map(x => x.toString())).join('\r\n');
},
split: (str: string): string[] => {
return str.split('\r\n');
},
parse: (str: string): string[] => {
return UriList.split(str).filter(value => !value.startsWith('#'));
}
});
13 changes: 1 addition & 12 deletions src/vs/editor/browser/dnd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import { DataTransfers } from 'vs/base/browser/dnd';
import { distinct } from 'vs/base/common/arrays';
import { createFileDataTransferItem, createStringDataTransferItem, IDataTransferItem, VSDataTransfer } from 'vs/base/common/dataTransfer';
import { createFileDataTransferItem, createStringDataTransferItem, IDataTransferItem, UriList, VSDataTransfer } from 'vs/base/common/dataTransfer';
import { Mimes } from 'vs/base/common/mime';
import { URI } from 'vs/base/common/uri';
import { CodeDataTransfers, extractEditorsDropData, FileAdditionalNativeProperties } from 'vs/platform/dnd/browser/dnd';
Expand Down Expand Up @@ -64,13 +63,3 @@ export function addExternalEditorsDropData(dataTransfer: VSDataTransfer, dragEve
dataTransfer.delete(internal);
}
}

export const UriList = Object.freeze({
// http://amundsen.com/hypermedia/urilist/
create: (entries: ReadonlyArray<string | URI>): string => {
return distinct(entries.map(x => x.toString())).join('\r\n');
},
parse: (str: string): string[] => {
return str.split('\r\n').filter(value => !value.startsWith('#'));
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import { DataTransfers } from 'vs/base/browser/dnd';
import { addDisposableListener } from 'vs/base/browser/dom';
import { CancelablePromise, createCancelablePromise, raceCancellation } from 'vs/base/common/async';
import { CancellationToken } from 'vs/base/common/cancellation';
import { createStringDataTransferItem, VSDataTransfer } from 'vs/base/common/dataTransfer';
import { createStringDataTransferItem, UriList, VSDataTransfer } from 'vs/base/common/dataTransfer';
import { Disposable } from 'vs/base/common/lifecycle';
import { Mimes } from 'vs/base/common/mime';
import { generateUuid } from 'vs/base/common/uuid';
import { toVSDataTransfer, UriList } from 'vs/editor/browser/dnd';
import { toVSDataTransfer } from 'vs/editor/browser/dnd';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { IBulkEditService, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService';
import { EditorOption } from 'vs/editor/common/config/editorOptions';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@

import { raceCancellation } from 'vs/base/common/async';
import { CancellationToken } from 'vs/base/common/cancellation';
import { VSDataTransfer } from 'vs/base/common/dataTransfer';
import { UriList, VSDataTransfer } from 'vs/base/common/dataTransfer';
import { Disposable } from 'vs/base/common/lifecycle';
import { Mimes } from 'vs/base/common/mime';
import { relativePath } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { addExternalEditorsDropData, toVSDataTransfer, UriList } from 'vs/editor/browser/dnd';
import { addExternalEditorsDropData, toVSDataTransfer } from 'vs/editor/browser/dnd';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { registerEditorContribution } from 'vs/editor/browser/editorExtensions';
import { IBulkEditService, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService';
Expand Down
6 changes: 3 additions & 3 deletions src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -920,7 +920,7 @@ class MainThreadPasteEditProvider implements languages.DocumentPasteEditProvider

if (supportsCopy) {
this.prepareDocumentPaste = async (model: ITextModel, selections: readonly IRange[], dataTransfer: VSDataTransfer, token: CancellationToken): Promise<VSDataTransfer | undefined> => {
const dataTransferDto = await typeConvert.DataTransfer.toDataTransferDTO(dataTransfer);
const dataTransferDto = await typeConvert.DataTransfer.from(dataTransfer);
if (token.isCancellationRequested) {
return undefined;
}
Expand All @@ -942,7 +942,7 @@ class MainThreadPasteEditProvider implements languages.DocumentPasteEditProvider
async provideDocumentPasteEdits(model: ITextModel, selections: Selection[], dataTransfer: VSDataTransfer, token: CancellationToken) {
const request = this.dataTransfers.add(dataTransfer);
try {
const d = await typeConvert.DataTransfer.toDataTransferDTO(dataTransfer);
const d = await typeConvert.DataTransfer.from(dataTransfer);
const result = await this._proxy.$providePasteEdits(this.handle, request.id, model.uri, selections, d, token);
if (!result) {
return undefined;
Expand Down Expand Up @@ -975,7 +975,7 @@ class MainThreadDocumentOnDropEditProvider implements languages.DocumentOnDropEd
async provideDocumentOnDropEdits(model: ITextModel, position: IPosition, dataTransfer: VSDataTransfer, token: CancellationToken): Promise<languages.DocumentOnDropEdit | null | undefined> {
const request = this.dataTransfers.add(dataTransfer);
try {
const dataTransferDto = await typeConvert.DataTransfer.toDataTransferDTO(dataTransfer);
const dataTransferDto = await typeConvert.DataTransfer.from(dataTransfer);
const edit = await this._proxy.$provideDocumentOnDropEdits(this.handle, request.id, model.uri, position, dataTransferDto, token);
if (!edit) {
return undefined;
Expand Down
2 changes: 1 addition & 1 deletion src/vs/workbench/api/browser/mainThreadTreeViews.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ class TreeViewDragAndDropController implements ITreeViewDragAndDropController {
operationUuid?: string, sourceTreeId?: string, sourceTreeItemHandles?: string[]): Promise<void> {
const request = this.dataTransfersCache.add(dataTransfer);
try {
return await this._proxy.$handleDrop(this.treeViewId, request.id, await typeConvert.DataTransfer.toDataTransferDTO(dataTransfer), targetTreeItem?.handle, token, operationUuid, sourceTreeId, sourceTreeItemHandles);
return await this._proxy.$handleDrop(this.treeViewId, request.id, await typeConvert.DataTransfer.from(dataTransfer), targetTreeItem?.handle, token, operationUuid, sourceTreeId, sourceTreeItemHandles);
} finally {
request.dispose();
}
Expand Down
20 changes: 11 additions & 9 deletions src/vs/workbench/api/common/extHostLanguageFeatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,7 @@ class DocumentPasteEditProvider {

constructor(
private readonly _proxy: extHostProtocol.MainThreadLanguageFeaturesShape,
private readonly _uriTransformer: IURITransformer,
private readonly _documents: ExtHostDocuments,
private readonly _provider: vscode.DocumentPasteEditProvider,
private readonly _handle: number,
Expand All @@ -500,21 +501,21 @@ class DocumentPasteEditProvider {
const doc = this._documents.getDocument(resource);
const vscodeRanges = ranges.map(range => typeConvert.Range.to(range));

const dataTransfer = typeConvert.DataTransfer.toDataTransfer(dataTransferDto, () => {
const dataTransfer = typeConvert.DataTransfer.to(dataTransferDto, () => {
throw new NotImplementedError();
});
}, this._uriTransformer);
await this._provider.prepareDocumentPaste(doc, vscodeRanges, dataTransfer, token);

return typeConvert.DataTransfer.toDataTransferDTO(dataTransfer);
return typeConvert.DataTransfer.from(dataTransfer, this._uriTransformer);
}

async providePasteEdits(requestId: number, resource: URI, ranges: IRange[], dataTransferDto: extHostProtocol.DataTransferDTO, token: CancellationToken): Promise<undefined | extHostProtocol.IPasteEditDto> {
const doc = this._documents.getDocument(resource);
const vscodeRanges = ranges.map(range => typeConvert.Range.to(range));

const dataTransfer = typeConvert.DataTransfer.toDataTransfer(dataTransferDto, async (id) => {
const dataTransfer = typeConvert.DataTransfer.to(dataTransferDto, async (id) => {
return (await this._proxy.$resolvePasteFileData(this._handle, requestId, id)).buffer;
});
}, this._uriTransformer);

const edit = await this._provider.provideDocumentPasteEdits(doc, vscodeRanges, dataTransfer, token);
if (!edit) {
Expand Down Expand Up @@ -1686,6 +1687,7 @@ class DocumentOnDropEditAdapter {

constructor(
private readonly _proxy: extHostProtocol.MainThreadLanguageFeaturesShape,
private readonly _uriTransformer: IURITransformer,
private readonly _documents: ExtHostDocuments,
private readonly _provider: vscode.DocumentDropEditProvider,
private readonly _handle: number,
Expand All @@ -1694,9 +1696,9 @@ class DocumentOnDropEditAdapter {
async provideDocumentOnDropEdits(requestId: number, uri: URI, position: IPosition, dataTransferDto: extHostProtocol.DataTransferDTO, token: CancellationToken): Promise<extHostProtocol.IDocumentOnDropEditDto | undefined> {
const doc = this._documents.getDocument(uri);
const pos = typeConvert.Position.to(position);
const dataTransfer = typeConvert.DataTransfer.toDataTransfer(dataTransferDto, async (id) => {
const dataTransfer = typeConvert.DataTransfer.to(dataTransferDto, async (id) => {
return (await this._proxy.$resolveDocumentOnDropFileData(this._handle, requestId, id)).buffer;
});
}, this._uriTransformer);

const edit = await this._provider.provideDocumentDropEdits(doc, pos, dataTransfer, token);
if (!edit) {
Expand Down Expand Up @@ -2329,7 +2331,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF

registerDocumentOnDropEditProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.DocumentDropEditProvider) {
const handle = this._nextHandle();
this._adapter.set(handle, new AdapterData(new DocumentOnDropEditAdapter(this._proxy, this._documents, provider, handle), extension));
this._adapter.set(handle, new AdapterData(new DocumentOnDropEditAdapter(this._proxy, this._uriTransformer, this._documents, provider, handle), extension));
this._proxy.$registerDocumentOnDropEditProvider(handle, this._transformDocumentSelector(selector));
return this._createDisposable(handle);
}
Expand All @@ -2343,7 +2345,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF

registerDocumentPasteEditProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.DocumentPasteEditProvider, metadata: vscode.DocumentPasteProviderMetadata): vscode.Disposable {
const handle = this._nextHandle();
this._adapter.set(handle, new AdapterData(new DocumentPasteEditProvider(this._proxy, this._documents, provider, handle), extension));
this._adapter.set(handle, new AdapterData(new DocumentPasteEditProvider(this._proxy, this._uriTransformer, this._documents, provider, handle), extension));
this._proxy.$registerPasteEditProvider(handle, this._transformDocumentSelector(selector), !!provider.prepareDocumentPaste, metadata.pasteMimeTypes);
return this._createDisposable(handle);
}
Expand Down
4 changes: 2 additions & 2 deletions src/vs/workbench/api/common/extHostTreeViews.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape {
return Promise.reject(new Error(localize('treeView.notRegistered', 'No tree view with id \'{0}\' registered.', destinationViewId)));
}

const treeDataTransfer = DataTransfer.toDataTransfer(treeDataTransferDTO, async dataItemIndex => {
const treeDataTransfer = DataTransfer.to(treeDataTransferDTO, async dataItemIndex => {
return (await this._proxy.$resolveDropFileData(destinationViewId, requestId, dataItemIndex)).buffer;
});
if ((sourceViewId === destinationViewId) && sourceTreeItemHandles) {
Expand Down Expand Up @@ -195,7 +195,7 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape {
return;
}

return DataTransfer.toDataTransferDTO(treeDataTransfer);
return DataTransfer.from(treeDataTransfer);
}

async $hasResolve(treeViewId: string): Promise<boolean> {
Expand Down
61 changes: 47 additions & 14 deletions src/vs/workbench/api/common/extHostTypeConverters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@

import { asArray, coalesce, isNonEmptyArray } from 'vs/base/common/arrays';
import { encodeBase64, VSBuffer } from 'vs/base/common/buffer';
import { IDataTransferItem, UriList, VSDataTransfer } from 'vs/base/common/dataTransfer';
import { once } from 'vs/base/common/functional';
import * as htmlContent from 'vs/base/common/htmlContent';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { ResourceSet } from 'vs/base/common/map';
import { marked } from 'vs/base/common/marked/marked';
import { parse } from 'vs/base/common/marshalling';
import { Mimes } from 'vs/base/common/mime';
import { cloneAndChange } from 'vs/base/common/objects';
import { isEmptyObject, isNumber, isString, isUndefinedOrNull, withNullAsUndefined } from 'vs/base/common/types';
import { URI, UriComponents } from 'vs/base/common/uri';
Expand All @@ -19,8 +22,8 @@ import { IPosition } from 'vs/editor/common/core/position';
import * as editorRange from 'vs/editor/common/core/range';
import { ISelection } from 'vs/editor/common/core/selection';
import { IContentDecorationRenderOptions, IDecorationOptions, IDecorationRenderOptions, IThemeDecorationRenderOptions } from 'vs/editor/common/editorCommon';
import * as languages from 'vs/editor/common/languages';
import * as encodedTokenAttributes from 'vs/editor/common/encodedTokenAttributes';
import * as languages from 'vs/editor/common/languages';
import * as languageSelector from 'vs/editor/common/languageSelector';
import { EndOfLineSequence, TrackedRangeStickiness } from 'vs/editor/common/model';
import { ITextEditorOptions } from 'vs/platform/editor/common/editor';
Expand All @@ -39,8 +42,6 @@ import { EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGro
import { ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService';
import type * as vscode from 'vscode';
import * as types from './extHostTypes';
import { once } from 'vs/base/common/functional';
import { IDataTransferItem, VSDataTransfer } from 'vs/base/common/dataTransfer';

export namespace Command {

Expand Down Expand Up @@ -1966,7 +1967,7 @@ export namespace ViewBadge {
}

export namespace DataTransferItem {
export function toDataTransferItem(item: extHostProtocol.DataTransferItemDTO, resolveFileData: () => Promise<Uint8Array>): types.DataTransferItem {
export function to(mime: string, item: extHostProtocol.DataTransferItemDTO, resolveFileData: () => Promise<Uint8Array>, uriTransformer?: IURITransformer): types.DataTransferItem {
const file = item.fileData;
if (file) {
return new class extends types.DataTransferItem {
Expand All @@ -1979,33 +1980,65 @@ export namespace DataTransferItem {
}
}('', item.id);
} else {
if (uriTransformer && mime === Mimes.uriList) {
return new types.DataTransferItem(transformUriList(item.asString, uri => URI.from(uriTransformer.transformIncoming(uri))));
}

return new types.DataTransferItem(item.asString);
}
}

export async function from(mime: string, item: vscode.DataTransferItem | IDataTransferItem, uriTransformer?: IURITransformer): Promise<extHostProtocol.DataTransferItemDTO> {
let stringValue = await item.asString();
const fileValue = item.asFile();

if (uriTransformer && mime === Mimes.uriList && stringValue && !fileValue) {
stringValue = transformUriList(stringValue, uri => uriTransformer.transformOutgoingURI(uri));
}

return {
id: (item as IDataTransferItem | types.DataTransferItem).id,
asString: stringValue,
fileData: fileValue ? { name: fileValue.name, uri: fileValue.uri } : undefined,
};
}

function transformUriList(strValue: string, f: (part: URI) => URI): string {
const parts = UriList.split(strValue).map(part => {
if (part.startsWith('#')) {
return part;
}

try {
const uri = URI.parse(part);
return f(uri);
} catch {
// Noop
}

return part;
});

return UriList.create(parts);
}
}

export namespace DataTransfer {
export function toDataTransfer(value: extHostProtocol.DataTransferDTO, resolveFileData: (itemId: string) => Promise<Uint8Array>): types.DataTransfer {
export function to(value: extHostProtocol.DataTransferDTO, resolveFileData: (itemId: string) => Promise<Uint8Array>, uriTransformer?: IURITransformer): types.DataTransfer {
const init = value.items.map(([type, item]) => {
return [type, DataTransferItem.toDataTransferItem(item, () => resolveFileData(item.id))] as const;
return [type, DataTransferItem.to(type, item, () => resolveFileData(item.id), uriTransformer)] as const;
});
return new types.DataTransfer(init);
}

export async function toDataTransferDTO(value: vscode.DataTransfer | VSDataTransfer): Promise<extHostProtocol.DataTransferDTO> {
export async function from(value: vscode.DataTransfer | VSDataTransfer, uriTransformer?: IURITransformer): Promise<extHostProtocol.DataTransferDTO> {
const newDTO: extHostProtocol.DataTransferDTO = { items: [] };

const promises: Promise<any>[] = [];

value.forEach((value, key) => {
promises.push((async () => {
const stringValue = await value.asString();
const fileValue = value.asFile();
newDTO.items.push([key, {
id: (value as IDataTransferItem | types.DataTransferItem).id,
asString: stringValue,
fileData: fileValue ? { name: fileValue.name, uri: fileValue.uri } : undefined,
}]);
newDTO.items.push([key, await DataTransferItem.from(key, value, uriTransformer)]);
})());
});

Expand Down
3 changes: 1 addition & 2 deletions src/vs/workbench/browser/dnd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { IListDragAndDrop } from 'vs/base/browser/ui/list/list';
import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView';
import { ITreeDragOverReaction } from 'vs/base/browser/ui/tree/tree';
import { coalesce } from 'vs/base/common/arrays';
import { VSDataTransfer } from 'vs/base/common/dataTransfer';
import { UriList, VSDataTransfer } from 'vs/base/common/dataTransfer';
import { Emitter } from 'vs/base/common/event';
import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import { stringify } from 'vs/base/common/marshalling';
Expand All @@ -19,7 +19,6 @@ import { FileAccess, Schemas } from 'vs/base/common/network';
import { isWindows } from 'vs/base/common/platform';
import { basename, isEqual } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { UriList } from 'vs/editor/browser/dnd';
import { CodeDataTransfers, createDraggedEditorInputFromRawResourcesData, Extensions, extractEditorsAndFilesDropData, IDragAndDropContributionRegistry, IDraggedResourceEditorInput, IResourceStat } from 'vs/platform/dnd/browser/dnd';
import { IFileService } from 'vs/platform/files/common/files';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
Expand Down

0 comments on commit 3798acf

Please sign in to comment.