From 50bee5e1760f61be51c84ad9a9af9ce19d847920 Mon Sep 17 00:00:00 2001 From: wdeveloper16 Date: Fri, 24 Apr 2026 09:07:58 +0200 Subject: [PATCH] :sparkles: Add clipboard:read/write permissions to plugin system (#6980) (#9053) * :sparkles: Add clipboard:read/write permissions to plugin system (#6980) * :wrench: Fix prettier formatting in clipboard permission files --------- Co-authored-by: wdeveloper16 Co-authored-by: Andrey Antukh --- .../src/app/main/ui/workspace/plugins.cljs | 15 +++- frontend/src/app/plugins/register.cljs | 5 +- frontend/translations/en.po | 8 +++ .../plugins-runtime/src/lib/api/openUI.api.ts | 26 ++++++- .../src/lib/create-modal.spec.ts | 69 +++++++++++++++++++ .../plugins-runtime/src/lib/create-modal.ts | 10 +++ .../src/lib/modal/plugin-modal.spec.ts | 29 ++++++++ .../src/lib/modal/plugin-modal.ts | 11 ++- .../src/lib/models/manifest.schema.ts | 2 + .../src/lib/plugin-manager.spec.ts | 2 + .../plugins-runtime/src/lib/plugin-manager.ts | 18 ++++- 11 files changed, 188 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/plugins.cljs b/frontend/src/app/main/ui/workspace/plugins.cljs index fc319bf925..f4068c585a 100644 --- a/frontend/src/app/main/ui/workspace/plugins.cljs +++ b/frontend/src/app/main/ui/workspace/plugins.cljs @@ -302,7 +302,20 @@ [:div {:class (stl/css :permissions-list-entry)} deprecated-icon/oauth-1 [:p {:class (stl/css :permissions-list-text)} - (tr "workspace.plugins.permissions.allow-localstorage")]])]) + (tr "workspace.plugins.permissions.allow-localstorage")]]) + + (cond + (contains? permissions "clipboard:write") + [:div {:class (stl/css :permissions-list-entry)} + deprecated-icon/oauth-1 + [:p {:class (stl/css :permissions-list-text)} + (tr "workspace.plugins.permissions.clipboard-write")]] + + (contains? permissions "clipboard:read") + [:div {:class (stl/css :permissions-list-entry)} + deprecated-icon/oauth-1 + [:p {:class (stl/css :permissions-list-text)} + (tr "workspace.plugins.permissions.clipboard-read")]])]) (mf/defc plugins-permissions-dialog {::mf/register modal/components diff --git a/frontend/src/app/plugins/register.cljs b/frontend/src/app/plugins/register.cljs index e3792f3fc9..df9afb6380 100644 --- a/frontend/src/app/plugins/register.cljs +++ b/frontend/src/app/plugins/register.cljs @@ -54,7 +54,10 @@ (conj "library:read") (contains? permissions "comment:write") - (conj "comment:read")) + (conj "comment:read") + + (contains? permissions "clipboard:write") + (conj "clipboard:read")) plugin-url (u/uri plugin-url) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 41308a2694..5de7250a59 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -7646,6 +7646,14 @@ msgstr "" msgid "workspace.plugins.permissions.allow-download" msgstr "Start file downloads." +#: src/app/main/ui/workspace/plugins.cljs +msgid "workspace.plugins.permissions.clipboard-read" +msgstr "Read the contents of your clipboard." + +#: src/app/main/ui/workspace/plugins.cljs +msgid "workspace.plugins.permissions.clipboard-write" +msgstr "Read and write to your clipboard." + #: src/app/main/ui/workspace/plugins.cljs:287 msgid "workspace.plugins.permissions.allow-localstorage" msgstr "Store data in the browser." diff --git a/plugins/libs/plugins-runtime/src/lib/api/openUI.api.ts b/plugins/libs/plugins-runtime/src/lib/api/openUI.api.ts index dbbeba184c..3e56ea15bb 100644 --- a/plugins/libs/plugins-runtime/src/lib/api/openUI.api.ts +++ b/plugins/libs/plugins-runtime/src/lib/api/openUI.api.ts @@ -10,7 +10,27 @@ export const openUIApi = z z.enum(['dark', 'light']), openUISchema.optional(), z.boolean().optional(), + z.boolean().optional(), + z.boolean().optional(), ) - .implement((title, url, theme, options, allowDownloads) => { - return createModal(title, url, theme, options, allowDownloads); - }); + .implement( + ( + title, + url, + theme, + options, + allowDownloads, + allowClipboardRead, + allowClipboardWrite, + ) => { + return createModal( + title, + url, + theme, + options, + allowDownloads, + allowClipboardRead, + allowClipboardWrite, + ); + }, + ); diff --git a/plugins/libs/plugins-runtime/src/lib/create-modal.spec.ts b/plugins/libs/plugins-runtime/src/lib/create-modal.spec.ts index 80d28fe56c..4cefab63c2 100644 --- a/plugins/libs/plugins-runtime/src/lib/create-modal.spec.ts +++ b/plugins/libs/plugins-runtime/src/lib/create-modal.spec.ts @@ -104,4 +104,73 @@ describe('createModal', () => { expect(modal.wrapper.style.width).toEqual('200px'); expect(modal.wrapper.style.height).toEqual('200px'); }); + + it('should set allow-clipboard-read attribute when allowClipboardRead is true', () => { + const theme: Theme = 'light'; + + createModal( + 'Test Modal', + 'https://example.com', + theme, + undefined, + false, + true, + false, + ); + + expect(modalMock.setAttribute).toHaveBeenCalledWith( + 'allow-clipboard-read', + 'true', + ); + expect(modalMock.setAttribute).not.toHaveBeenCalledWith( + 'allow-clipboard-write', + 'true', + ); + }); + + it('should set allow-clipboard-write attribute when allowClipboardWrite is true', () => { + const theme: Theme = 'light'; + + createModal( + 'Test Modal', + 'https://example.com', + theme, + undefined, + false, + false, + true, + ); + + expect(modalMock.setAttribute).toHaveBeenCalledWith( + 'allow-clipboard-write', + 'true', + ); + expect(modalMock.setAttribute).not.toHaveBeenCalledWith( + 'allow-clipboard-read', + 'true', + ); + }); + + it('should set both clipboard attributes when both are true', () => { + const theme: Theme = 'light'; + + createModal( + 'Test Modal', + 'https://example.com', + theme, + undefined, + false, + true, + true, + ); + + expect(modalMock.setAttribute).toHaveBeenCalledWith( + 'allow-clipboard-read', + 'true', + ); + expect(modalMock.setAttribute).toHaveBeenCalledWith( + 'allow-clipboard-write', + 'true', + ); + }); }); diff --git a/plugins/libs/plugins-runtime/src/lib/create-modal.ts b/plugins/libs/plugins-runtime/src/lib/create-modal.ts index 01422c76b6..d6cd2d1623 100644 --- a/plugins/libs/plugins-runtime/src/lib/create-modal.ts +++ b/plugins/libs/plugins-runtime/src/lib/create-modal.ts @@ -10,6 +10,8 @@ export function createModal( theme: Theme, options?: OpenUIOptions, allowDownloads?: boolean, + allowClipboardRead?: boolean, + allowClipboardWrite?: boolean, ) { const modal = document.createElement('plugin-modal') as PluginModalElement; @@ -44,6 +46,14 @@ export function createModal( modal.setAttribute('allow-downloads', 'true'); } + if (allowClipboardRead) { + modal.setAttribute('allow-clipboard-read', 'true'); + } + + if (allowClipboardWrite) { + modal.setAttribute('allow-clipboard-write', 'true'); + } + document.body.appendChild(modal); return modal; diff --git a/plugins/libs/plugins-runtime/src/lib/modal/plugin-modal.spec.ts b/plugins/libs/plugins-runtime/src/lib/modal/plugin-modal.spec.ts index d7b774c2c3..fb19f291ab 100644 --- a/plugins/libs/plugins-runtime/src/lib/modal/plugin-modal.spec.ts +++ b/plugins/libs/plugins-runtime/src/lib/modal/plugin-modal.spec.ts @@ -99,6 +99,35 @@ describe('PluginModalElement', () => { modal.remove(); }); + it('should set iframe allow attribute for clipboard permissions', () => { + const modal = document.createElement('plugin-modal'); + modal.setAttribute('title', 'Test modal'); + modal.setAttribute('iframe-src', 'about:blank'); + modal.setAttribute('allow-clipboard-read', 'true'); + modal.setAttribute('allow-clipboard-write', 'true'); + document.body.appendChild(modal); + + const iframe = modal.shadowRoot?.querySelector('iframe'); + expect(iframe).toBeTruthy(); + expect(iframe?.allow).toContain('clipboard-read'); + expect(iframe?.allow).toContain('clipboard-write'); + + modal.remove(); + }); + + it('should not set clipboard allow attributes when permissions are absent', () => { + const modal = document.createElement('plugin-modal'); + modal.setAttribute('title', 'Test modal'); + modal.setAttribute('iframe-src', 'about:blank'); + document.body.appendChild(modal); + + const iframe = modal.shadowRoot?.querySelector('iframe'); + expect(iframe).toBeTruthy(); + expect(iframe?.allow).toBe(''); + + modal.remove(); + }); + it('should dispatch close event when close button is clicked', () => { const modal = document.createElement('plugin-modal'); modal.setAttribute('title', 'Test modal'); diff --git a/plugins/libs/plugins-runtime/src/lib/modal/plugin-modal.ts b/plugins/libs/plugins-runtime/src/lib/modal/plugin-modal.ts index c61ad7fce5..53ea472494 100644 --- a/plugins/libs/plugins-runtime/src/lib/modal/plugin-modal.ts +++ b/plugins/libs/plugins-runtime/src/lib/modal/plugin-modal.ts @@ -52,6 +52,10 @@ export class PluginModalElement extends HTMLElement { const title = this.getAttribute('title'); const iframeSrc = this.getAttribute('iframe-src'); const allowDownloads = this.getAttribute('allow-downloads') || false; + const allowClipboardRead = + this.getAttribute('allow-clipboard-read') || false; + const allowClipboardWrite = + this.getAttribute('allow-clipboard-write') || false; if (!title || !iframeSrc) { throw new Error('title and iframe-src attributes are required'); @@ -95,7 +99,12 @@ export class PluginModalElement extends HTMLElement { const iframe = document.createElement('iframe'); iframe.src = iframeSrc; - iframe.allow = ''; + + const allowList: string[] = []; + if (allowClipboardRead) allowList.push('clipboard-read'); + if (allowClipboardWrite) allowList.push('clipboard-write'); + iframe.allow = allowList.join('; '); + iframe.sandbox.add( 'allow-scripts', 'allow-forms', diff --git a/plugins/libs/plugins-runtime/src/lib/models/manifest.schema.ts b/plugins/libs/plugins-runtime/src/lib/models/manifest.schema.ts index bd5895c02b..16d4fd5e28 100644 --- a/plugins/libs/plugins-runtime/src/lib/models/manifest.schema.ts +++ b/plugins/libs/plugins-runtime/src/lib/models/manifest.schema.ts @@ -19,6 +19,8 @@ export const manifestSchema = z.object({ 'comment:write', 'allow:downloads', 'allow:localstorage', + 'clipboard:read', + 'clipboard:write', ]), ), }); diff --git a/plugins/libs/plugins-runtime/src/lib/plugin-manager.spec.ts b/plugins/libs/plugins-runtime/src/lib/plugin-manager.spec.ts index 206f43ba34..d53f4f5296 100644 --- a/plugins/libs/plugins-runtime/src/lib/plugin-manager.spec.ts +++ b/plugins/libs/plugins-runtime/src/lib/plugin-manager.spec.ts @@ -123,6 +123,8 @@ describe('createPluginManager', () => { 'light', { width: 400, height: 300 }, true, + false, + false, ); expect(mockModal.setTheme).toHaveBeenCalledWith('light'); expect(mockModal.addEventListener).toHaveBeenCalledWith( diff --git a/plugins/libs/plugins-runtime/src/lib/plugin-manager.ts b/plugins/libs/plugins-runtime/src/lib/plugin-manager.ts index 0b4035794a..8b811f55eb 100644 --- a/plugins/libs/plugins-runtime/src/lib/plugin-manager.ts +++ b/plugins/libs/plugins-runtime/src/lib/plugin-manager.ts @@ -27,6 +27,14 @@ export async function createPluginManager( (s) => s === 'allow:downloads', ); + const allowClipboardRead = !!manifest.permissions.find( + (s) => s === 'clipboard:read', + ); + + const allowClipboardWrite = !!manifest.permissions.find( + (s) => s === 'clipboard:write', + ); + const themeChangeId = context.addListener('themechange', (theme: Theme) => { modal?.setTheme(theme); }); @@ -91,7 +99,15 @@ export async function createPluginManager( return; } - modal = openUIApi(name, modalUrl, theme, options, allowDownloads); + modal = openUIApi( + name, + modalUrl, + theme, + options, + allowDownloads, + allowClipboardRead, + allowClipboardWrite, + ); modal.setTheme(theme);