Add clipboard:read/write permissions to plugin system (#6980) (#9053)

*  Add clipboard:read/write permissions to plugin system (#6980)

* 🔧 Fix prettier formatting in clipboard permission files

---------

Co-authored-by: wdeveloper16 <wdeveloer16@protonmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
This commit is contained in:
wdeveloper16 2026-04-24 09:07:58 +02:00 committed by GitHub
parent e280168de9
commit 50bee5e176
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 188 additions and 7 deletions

View File

@ -302,7 +302,20 @@
[:div {:class (stl/css :permissions-list-entry)} [:div {:class (stl/css :permissions-list-entry)}
deprecated-icon/oauth-1 deprecated-icon/oauth-1
[:p {:class (stl/css :permissions-list-text)} [: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/defc plugins-permissions-dialog
{::mf/register modal/components {::mf/register modal/components

View File

@ -54,7 +54,10 @@
(conj "library:read") (conj "library:read")
(contains? permissions "comment:write") (contains? permissions "comment:write")
(conj "comment:read")) (conj "comment:read")
(contains? permissions "clipboard:write")
(conj "clipboard:read"))
plugin-url plugin-url
(u/uri plugin-url) (u/uri plugin-url)

View File

@ -7646,6 +7646,14 @@ msgstr ""
msgid "workspace.plugins.permissions.allow-download" msgid "workspace.plugins.permissions.allow-download"
msgstr "Start file downloads." 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 #: src/app/main/ui/workspace/plugins.cljs:287
msgid "workspace.plugins.permissions.allow-localstorage" msgid "workspace.plugins.permissions.allow-localstorage"
msgstr "Store data in the browser." msgstr "Store data in the browser."

View File

@ -10,7 +10,27 @@ export const openUIApi = z
z.enum(['dark', 'light']), z.enum(['dark', 'light']),
openUISchema.optional(), openUISchema.optional(),
z.boolean().optional(), z.boolean().optional(),
z.boolean().optional(),
z.boolean().optional(),
) )
.implement((title, url, theme, options, allowDownloads) => { .implement(
return createModal(title, url, theme, options, allowDownloads); (
}); title,
url,
theme,
options,
allowDownloads,
allowClipboardRead,
allowClipboardWrite,
) => {
return createModal(
title,
url,
theme,
options,
allowDownloads,
allowClipboardRead,
allowClipboardWrite,
);
},
);

View File

@ -104,4 +104,73 @@ describe('createModal', () => {
expect(modal.wrapper.style.width).toEqual('200px'); expect(modal.wrapper.style.width).toEqual('200px');
expect(modal.wrapper.style.height).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',
);
});
}); });

View File

@ -10,6 +10,8 @@ export function createModal(
theme: Theme, theme: Theme,
options?: OpenUIOptions, options?: OpenUIOptions,
allowDownloads?: boolean, allowDownloads?: boolean,
allowClipboardRead?: boolean,
allowClipboardWrite?: boolean,
) { ) {
const modal = document.createElement('plugin-modal') as PluginModalElement; const modal = document.createElement('plugin-modal') as PluginModalElement;
@ -44,6 +46,14 @@ export function createModal(
modal.setAttribute('allow-downloads', 'true'); 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); document.body.appendChild(modal);
return modal; return modal;

View File

@ -99,6 +99,35 @@ describe('PluginModalElement', () => {
modal.remove(); 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', () => { it('should dispatch close event when close button is clicked', () => {
const modal = document.createElement('plugin-modal'); const modal = document.createElement('plugin-modal');
modal.setAttribute('title', 'Test modal'); modal.setAttribute('title', 'Test modal');

View File

@ -52,6 +52,10 @@ export class PluginModalElement extends HTMLElement {
const title = this.getAttribute('title'); const title = this.getAttribute('title');
const iframeSrc = this.getAttribute('iframe-src'); const iframeSrc = this.getAttribute('iframe-src');
const allowDownloads = this.getAttribute('allow-downloads') || false; 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) { if (!title || !iframeSrc) {
throw new Error('title and iframe-src attributes are required'); throw new Error('title and iframe-src attributes are required');
@ -95,7 +99,12 @@ export class PluginModalElement extends HTMLElement {
const iframe = document.createElement('iframe'); const iframe = document.createElement('iframe');
iframe.src = iframeSrc; 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( iframe.sandbox.add(
'allow-scripts', 'allow-scripts',
'allow-forms', 'allow-forms',

View File

@ -19,6 +19,8 @@ export const manifestSchema = z.object({
'comment:write', 'comment:write',
'allow:downloads', 'allow:downloads',
'allow:localstorage', 'allow:localstorage',
'clipboard:read',
'clipboard:write',
]), ]),
), ),
}); });

View File

@ -123,6 +123,8 @@ describe('createPluginManager', () => {
'light', 'light',
{ width: 400, height: 300 }, { width: 400, height: 300 },
true, true,
false,
false,
); );
expect(mockModal.setTheme).toHaveBeenCalledWith('light'); expect(mockModal.setTheme).toHaveBeenCalledWith('light');
expect(mockModal.addEventListener).toHaveBeenCalledWith( expect(mockModal.addEventListener).toHaveBeenCalledWith(

View File

@ -27,6 +27,14 @@ export async function createPluginManager(
(s) => s === 'allow:downloads', (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) => { const themeChangeId = context.addListener('themechange', (theme: Theme) => {
modal?.setTheme(theme); modal?.setTheme(theme);
}); });
@ -91,7 +99,15 @@ export async function createPluginManager(
return; return;
} }
modal = openUIApi(name, modalUrl, theme, options, allowDownloads); modal = openUIApi(
name,
modalUrl,
theme,
options,
allowDownloads,
allowClipboardRead,
allowClipboardWrite,
);
modal.setTheme(theme); modal.setTheme(theme);