From 7ab689f537e8e56a35232735d459b03c8a5f3ffa Mon Sep 17 00:00:00 2001 From: Artem Zatsarynnyi Date: Thu, 12 Sep 2019 18:13:00 +0300 Subject: [PATCH] Clipboard Plugin API Signed-off-by: Artem Zatsarynnyi --- .../src/browser/browser-clipboard-service.ts | 111 ++++++++++++++++++ .../core/src/browser/clipboard-service.ts | 23 ++++ .../browser/window/browser-window-module.ts | 3 + .../electron-clipboard-service.ts | 32 +++++ .../window/electron-window-module.ts | 3 + .../plugin-ext/src/common/plugin-api-rpc.ts | 8 +- .../src/hosted/browser/worker/worker-main.ts | 5 +- .../src/hosted/node/plugin-host-rpc.ts | 5 +- .../src/main/browser/clipboard-main.ts | 38 ++++++ .../src/main/browser/main-context.ts | 4 + .../plugin-ext/src/plugin/clipboard-ext.ts | 37 ++++++ .../plugin-ext/src/plugin/plugin-context.ts | 6 +- packages/plugin/src/theia.d.ts | 23 ++++ 13 files changed, 293 insertions(+), 5 deletions(-) create mode 100644 packages/core/src/browser/browser-clipboard-service.ts create mode 100644 packages/core/src/browser/clipboard-service.ts create mode 100644 packages/core/src/electron-browser/electron-clipboard-service.ts create mode 100644 packages/plugin-ext/src/main/browser/clipboard-main.ts create mode 100644 packages/plugin-ext/src/plugin/clipboard-ext.ts diff --git a/packages/core/src/browser/browser-clipboard-service.ts b/packages/core/src/browser/browser-clipboard-service.ts new file mode 100644 index 0000000000000..82be81cc4dbb2 --- /dev/null +++ b/packages/core/src/browser/browser-clipboard-service.ts @@ -0,0 +1,111 @@ +/******************************************************************************** + * Copyright (C) 2019 RedHat and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { injectable, inject } from 'inversify'; +import { isFirefox } from './browser'; +import { ClipboardService } from './clipboard-service'; +import { ILogger } from '../common/logger'; +import { MessageService } from '../common/message-service'; + +export interface NavigatorClipboard { + readText(): Promise; + writeText(value: string): Promise; +} +export interface PermissionStatus { + state: 'granted' | 'prompt' | 'denied' +} +export interface NavigatorPermissions { + query(options: { name: string }): Promise +} + +@injectable() +export class BrowserClipboardService implements ClipboardService { + + @inject(MessageService) + protected readonly messageService: MessageService; + + @inject(ILogger) + protected readonly logger: ILogger; + + async readText(): Promise { + let permission; + try { + permission = await this.queryPermission('clipboard-read'); + } catch (e) { + this.logger.error('Failed checking a clipboard-read permission.', e); + // in FireFox, Clipboard API isn't gated with the permissions + try { + return await this.getClipboardAPI().readText(); + } catch (e) { + this.logger.error('Failed reading clipboard content.', e); + if (isFirefox) { + this.messageService.warn(`Clipboard API is not available. + It can be enabled by 'dom.events.testing.asyncClipboard' preference on 'about:config' page. Then reload Theia. + Note, it will allow FireFox getting full access to the system clipboard.`); + } + throw new Error('Failed reading clipboard content.'); + } + } + if (permission.state === 'denied') { + // most likely, the user intentionally denied the access + this.messageService.error("Access to the clipboard is denied. Check your browser's permission."); + throw new Error('Access to the clipboard is denied.'); + } + return this.getClipboardAPI().readText(); + } + + async writeText(value: string): Promise { + let permission; + try { + permission = await this.queryPermission('clipboard-write'); + } catch (e) { + this.logger.error('Failed checking a clipboard-write permission.', e); + // in FireFox, Clipboard API isn't gated with the permissions + try { + await this.getClipboardAPI().writeText(value); + return; + } catch (e) { + this.logger.error('Failed writing to the clipboard.', e); + if (isFirefox) { + this.messageService.warn(`Clipboard API is not available. + It can be enabled by 'dom.events.testing.asyncClipboard' preference on 'about:config' page. Then reload Theia. + Note, it will allow FireFox getting full access to the system clipboard.`); + } + throw new Error('Failed writing the the clipboard.'); + } + } + if (permission.state === 'denied') { + // most likely, the user intentionally denied the access + this.messageService.error("Access to the clipboard is denied. Check your browser's permission."); + throw new Error('Access to the clipboard is denied.'); + } + return this.getClipboardAPI().writeText(value); + } + + protected async queryPermission(name: string): Promise { + if ('permissions' in navigator) { + return (navigator['permissions']).query({ name: name }); + } + throw new Error('Permissions API unavailable'); + } + + protected getClipboardAPI(): NavigatorClipboard { + if ('clipboard' in navigator) { + return (navigator['clipboard']); + } + throw new Error('Async Clipboard API unavailable'); + } +} diff --git a/packages/core/src/browser/clipboard-service.ts b/packages/core/src/browser/clipboard-service.ts new file mode 100644 index 0000000000000..5079bab7d79aa --- /dev/null +++ b/packages/core/src/browser/clipboard-service.ts @@ -0,0 +1,23 @@ +/******************************************************************************** + * Copyright (C) 2019 RedHat and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { MaybePromise } from '../common/types'; + +export const ClipboardService = Symbol('ClipboardService'); +export interface ClipboardService { + readText(): MaybePromise; + writeText(value: string): MaybePromise; +} diff --git a/packages/core/src/browser/window/browser-window-module.ts b/packages/core/src/browser/window/browser-window-module.ts index f4bbbc3eaf76d..dd201ba404f2c 100644 --- a/packages/core/src/browser/window/browser-window-module.ts +++ b/packages/core/src/browser/window/browser-window-module.ts @@ -18,9 +18,12 @@ import { ContainerModule } from 'inversify'; import { WindowService } from '../../browser/window/window-service'; import { DefaultWindowService } from '../../browser/window/default-window-service'; import { FrontendApplicationContribution } from '../frontend-application'; +import { ClipboardService } from '../clipboard-service'; +import { BrowserClipboardService } from '../browser-clipboard-service'; export default new ContainerModule(bind => { bind(DefaultWindowService).toSelf().inSingletonScope(); bind(WindowService).toService(DefaultWindowService); bind(FrontendApplicationContribution).toService(DefaultWindowService); + bind(ClipboardService).to(BrowserClipboardService).inSingletonScope(); }); diff --git a/packages/core/src/electron-browser/electron-clipboard-service.ts b/packages/core/src/electron-browser/electron-clipboard-service.ts new file mode 100644 index 0000000000000..192e96ed5dd07 --- /dev/null +++ b/packages/core/src/electron-browser/electron-clipboard-service.ts @@ -0,0 +1,32 @@ +/******************************************************************************** + * Copyright (C) 2019 RedHat and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { clipboard } from 'electron'; +import { injectable } from 'inversify'; +import { ClipboardService } from '../browser/clipboard-service'; + +@injectable() +export class ElectronClipboardService implements ClipboardService { + + readText(): string { + return clipboard.readText(); + } + + writeText(value: string): void { + clipboard.writeText(value); + } + +} diff --git a/packages/core/src/electron-browser/window/electron-window-module.ts b/packages/core/src/electron-browser/window/electron-window-module.ts index 5b11e207d4a94..edad94a14c24e 100644 --- a/packages/core/src/electron-browser/window/electron-window-module.ts +++ b/packages/core/src/electron-browser/window/electron-window-module.ts @@ -18,8 +18,11 @@ import { ContainerModule } from 'inversify'; import { WindowService } from '../../browser/window/window-service'; import { ElectronWindowService } from './electron-window-service'; import { FrontendApplicationContribution } from '../../browser/frontend-application'; +import { ElectronClipboardService } from '../electron-clipboard-service'; +import { ClipboardService } from '../../browser/clipboard-service'; export default new ContainerModule(bind => { bind(WindowService).to(ElectronWindowService).inSingletonScope(); bind(FrontendApplicationContribution).toService(WindowService); + bind(ClipboardService).to(ElectronClipboardService).inSingletonScope(); }); diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index 99cecc367c01d..1205eb132a65f 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -1255,6 +1255,11 @@ export interface FileSystemMain { $unregisterProvider(handle: number): void; } +export interface ClipboardMain { + $readText(): Promise; + $writeText(value: string): Promise; +} + export const PLUGIN_RPC_CONTEXT = { COMMAND_REGISTRY_MAIN: >createProxyIdentifier('CommandRegistryMain'), QUICK_OPEN_MAIN: createProxyIdentifier('QuickOpenMain'), @@ -1280,7 +1285,8 @@ export const PLUGIN_RPC_CONTEXT = { FILE_SYSTEM_MAIN: createProxyIdentifier('FileSystemMain'), SCM_MAIN: createProxyIdentifier('ScmMain'), DECORATIONS_MAIN: createProxyIdentifier('DecorationsMain'), - WINDOW_MAIN: createProxyIdentifier('WindowMain') + WINDOW_MAIN: createProxyIdentifier('WindowMain'), + CLIPBOARD_MAIN: >createProxyIdentifier('ClipboardMain') }; export const MAIN_RPC_CONTEXT = { diff --git a/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts b/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts index 6a4f1ef131a99..d0a04cf39e70f 100644 --- a/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts +++ b/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts @@ -28,6 +28,7 @@ import { EditorsAndDocumentsExtImpl } from '../../../plugin/editors-and-document import { WorkspaceExtImpl } from '../../../plugin/workspace'; import { MessageRegistryExt } from '../../../plugin/message-registry'; import { WorkerEnvExtImpl } from './worker-env-ext'; +import { ClipboardExt } from '../../../plugin/clipboard-ext'; // tslint:disable-next-line:no-any const ctx = self as any; @@ -55,6 +56,7 @@ const messageRegistryExt = new MessageRegistryExt(rpc); const workspaceExt = new WorkspaceExtImpl(rpc, editorsAndDocuments, messageRegistryExt); const preferenceRegistryExt = new PreferenceRegistryExtImpl(rpc, workspaceExt); const debugExt = createDebugExtStub(rpc); +const clipboardExt = new ClipboardExt(rpc); const pluginManager = new PluginManagerExtImpl({ // tslint:disable-next-line:no-any @@ -133,7 +135,8 @@ const apiFactory = createAPIFactory( preferenceRegistryExt, editorsAndDocuments, workspaceExt, - messageRegistryExt + messageRegistryExt, + clipboardExt ); let defaultApi: typeof theia; diff --git a/packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts b/packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts index ad49960d2e1f4..59a19938d406b 100644 --- a/packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts +++ b/packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts @@ -26,6 +26,7 @@ import { EditorsAndDocumentsExtImpl } from '../../plugin/editors-and-documents'; import { WorkspaceExtImpl } from '../../plugin/workspace'; import { MessageRegistryExt } from '../../plugin/message-registry'; import { EnvNodeExtImpl } from '../../plugin/node/env-node-ext'; +import { ClipboardExt } from '../../plugin/clipboard-ext'; /** * Handle the RPC calls. @@ -47,6 +48,7 @@ export class PluginHostRPC { const messageRegistryExt = new MessageRegistryExt(this.rpc); const workspaceExt = new WorkspaceExtImpl(this.rpc, editorsAndDocumentsExt, messageRegistryExt); const preferenceRegistryExt = new PreferenceRegistryExtImpl(this.rpc, workspaceExt); + const clipboardExt = new ClipboardExt(this.rpc); this.pluginManager = this.createPluginManager(envExt, preferenceRegistryExt, this.rpc); this.rpc.set(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT, this.pluginManager); this.rpc.set(MAIN_RPC_CONTEXT.EDITORS_AND_DOCUMENTS_EXT, editorsAndDocumentsExt); @@ -61,7 +63,8 @@ export class PluginHostRPC { preferenceRegistryExt, editorsAndDocumentsExt, workspaceExt, - messageRegistryExt + messageRegistryExt, + clipboardExt ); } diff --git a/packages/plugin-ext/src/main/browser/clipboard-main.ts b/packages/plugin-ext/src/main/browser/clipboard-main.ts new file mode 100644 index 0000000000000..eccb10948ed30 --- /dev/null +++ b/packages/plugin-ext/src/main/browser/clipboard-main.ts @@ -0,0 +1,38 @@ +/******************************************************************************** + * Copyright (C) 2019 RedHat and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { interfaces } from 'inversify'; +import { ClipboardMain } from '../../common'; +import { ClipboardService } from '@theia/core/lib/browser/clipboard-service'; + +export class ClipboardMainImpl implements ClipboardMain { + + protected readonly clipboardService: ClipboardService; + + constructor(container: interfaces.Container) { + this.clipboardService = container.get(ClipboardService); + } + + async $readText(): Promise { + const result = await this.clipboardService.readText(); + return result; + } + + async $writeText(value: string): Promise { + await this.clipboardService.writeText(value); + } + +} diff --git a/packages/plugin-ext/src/main/browser/main-context.ts b/packages/plugin-ext/src/main/browser/main-context.ts index 72e606cf3ca57..28f216b9c73e0 100644 --- a/packages/plugin-ext/src/main/browser/main-context.ts +++ b/packages/plugin-ext/src/main/browser/main-context.ts @@ -40,6 +40,7 @@ import { DebugMainImpl } from './debug/debug-main'; import { FileSystemMainImpl } from './file-system-main'; import { ScmMainImpl } from './scm-main'; import { DecorationsMainImpl } from './decorations/decorations-main'; +import { ClipboardMainImpl } from './clipboard-main'; export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container): void { const commandRegistryMain = new CommandRegistryMainImpl(rpc, container); @@ -113,4 +114,7 @@ export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container const windowMain = new WindowStateMain(rpc, container); rpc.set(PLUGIN_RPC_CONTEXT.WINDOW_MAIN, windowMain); + + const clipboardMain = new ClipboardMainImpl(container); + rpc.set(PLUGIN_RPC_CONTEXT.CLIPBOARD_MAIN, clipboardMain); } diff --git a/packages/plugin-ext/src/plugin/clipboard-ext.ts b/packages/plugin-ext/src/plugin/clipboard-ext.ts new file mode 100644 index 0000000000000..56cf2e878a1ae --- /dev/null +++ b/packages/plugin-ext/src/plugin/clipboard-ext.ts @@ -0,0 +1,37 @@ +/******************************************************************************** + * Copyright (C) 2019 RedHat and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import * as theia from '@theia/plugin'; +import { RPCProtocol } from '../common/rpc-protocol'; +import { PLUGIN_RPC_CONTEXT, ClipboardMain } from '../common'; + +export class ClipboardExt implements theia.Clipboard { + + protected readonly proxy: ClipboardMain; + + constructor(rpc: RPCProtocol) { + this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.CLIPBOARD_MAIN); + } + + readText(): Promise { + return this.proxy.$readText(); + } + + writeText(value: string): Promise { + return this.proxy.$writeText(value); + } + +} diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index 4c651b1b8e4d8..fb7d461b269ba 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -139,6 +139,7 @@ import { ScmExtImpl } from './scm'; import { DecorationProvider, LineChange } from '@theia/plugin'; import { DecorationsExtImpl } from './decorations'; import { TextEditorExt } from './text-editor'; +import { ClipboardExt } from './clipboard-ext'; export function createAPIFactory( rpc: RPCProtocol, @@ -148,7 +149,8 @@ export function createAPIFactory( preferenceRegistryExt: PreferenceRegistryExtImpl, editorsAndDocumentsExt: EditorsAndDocumentsExtImpl, workspaceExt: WorkspaceExtImpl, - messageRegistryExt: MessageRegistryExt + messageRegistryExt: MessageRegistryExt, + clipboard: ClipboardExt ): PluginAPIFactory { const commandRegistry = rpc.set(MAIN_RPC_CONTEXT.COMMAND_REGISTRY_EXT, new CommandRegistryImpl(rpc)); @@ -491,7 +493,7 @@ export function createAPIFactory( get machineId(): string { return envExt.machineId; }, get sessionId(): string { return envExt.sessionId; }, get uriScheme(): string { return envExt.uriScheme; }, - + clipboard, getEnvVariable(envVarName: string): PromiseLike { return envExt.getEnvVariable(envVarName); }, diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index 5763b96478035..03e1ad8edcc8b 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -2969,6 +2969,24 @@ declare module '@theia/plugin' { deserializeWebviewPanel(webviewPanel: WebviewPanel, state: any): PromiseLike; } + /** + * The clipboard provides read and write access to the system's clipboard. + */ + export interface Clipboard { + + /** + * Read the current clipboard contents as text. + * @returns A thenable that resolves to a string. + */ + readText(): PromiseLike; + + /** + * Writes text into the clipboard. + * @returns A thenable that resolves when writing happened. + */ + writeText(value: string): PromiseLike; + } + /** * A uri handler is responsible for handling system-wide [uris](#Uri). * @@ -4751,6 +4769,11 @@ declare module '@theia/plugin' { */ export const language: string; + /** + * The system clipboard. + */ + export const clipboard: Clipboard; + /** * A unique identifier for the computer. */