aboutsummaryrefslogtreecommitdiff
path: root/ext
diff options
context:
space:
mode:
authorDarius Jahandarie <djahandarie@gmail.com>2023-10-22 06:41:56 +0000
committerGitHub <noreply@github.com>2023-10-22 06:41:56 +0000
commit264bfe8c64e78016551f35df497eb6cd65f2965d (patch)
tree73ec5415972a7e6cdf0b213b0896344ad0a194b2 /ext
parentaaa570a89ce4b285b42f26258de24b1bde4a76c3 (diff)
parent379fdcf2280939c72e1be4e4f38567149a108873 (diff)
Merge pull request #278 from jbukl/offscreen-clipboard
Fix chromium clipboard access
Diffstat (limited to 'ext')
-rw-r--r--ext/js/background/backend.js80
-rw-r--r--ext/js/background/offscreen-main.js25
-rw-r--r--ext/js/background/offscreen.js74
-rw-r--r--ext/js/display/search-display-controller.js2
-rw-r--r--ext/offscreen.html39
5 files changed, 213 insertions, 7 deletions
diff --git a/ext/js/background/backend.js b/ext/js/background/backend.js
index 565f4abf..308ae4d5 100644
--- a/ext/js/background/backend.js
+++ b/ext/js/background/backend.js
@@ -57,12 +57,21 @@ class Backend {
});
this._anki = new AnkiConnect();
this._mecab = new Mecab();
- this._clipboardReader = new ClipboardReader({
- // eslint-disable-next-line no-undef
- document: (typeof document === 'object' && document !== null ? document : null),
- pasteTargetSelector: '#clipboard-paste-target',
- richContentPasteTargetSelector: '#clipboard-rich-content-paste-target'
- });
+
+ if (!chrome.offscreen) {
+ this._clipboardReader = new ClipboardReader({
+ // eslint-disable-next-line no-undef
+ document: (typeof document === 'object' && document !== null ? document : null),
+ pasteTargetSelector: '#clipboard-paste-target',
+ richContentPasteTargetSelector: '#clipboard-rich-content-paste-target'
+ });
+ } else {
+ this._clipboardReader = {
+ getText: this._getTextOffscreen.bind(this),
+ getImage: this._getImageOffscreen.bind(this)
+ };
+ }
+
this._clipboardMonitor = new ClipboardMonitor({
japaneseUtil: this._japaneseUtil,
clipboardReader: this._clipboardReader
@@ -97,6 +106,8 @@ class Backend {
this._permissions = null;
this._permissionsUtil = new PermissionsUtil();
+ this._creatingOffscreen = null;
+
this._messageHandlers = new Map([
['requestBackendReadySignal', {async: false, contentScript: true, handler: this._onApiRequestBackendReadySignal.bind(this)}],
['optionsGet', {async: false, contentScript: true, handler: this._onApiOptionsGet.bind(this)}],
@@ -218,6 +229,9 @@ class Backend {
await this._requestBuilder.prepare();
await this._environment.prepare();
+ if (chrome.offscreen) {
+ await this._setupOffscreenDocument();
+ }
this._clipboardReader.browser = this._environment.getInfo().browser;
try {
@@ -1610,6 +1624,20 @@ class Backend {
return await (json ? response.json() : response.text());
}
+ _sendMessagePromise(...args) {
+ return new Promise((resolve, reject) => {
+ const callback = (response) => {
+ try {
+ resolve(this._getMessageResponseResult(response));
+ } catch (error) {
+ reject(error);
+ }
+ };
+
+ chrome.runtime.sendMessage(...args, callback);
+ });
+ }
+
_sendMessageIgnoreResponse(...args) {
const callback = () => this._checkLastError(chrome.runtime.lastError);
chrome.runtime.sendMessage(...args, callback);
@@ -2220,6 +2248,14 @@ class Backend {
return results;
}
+ async _getTextOffscreen(useRichText) {
+ return this._sendMessagePromise({action: 'clipboardGetTextOffscreen', params: {useRichText}});
+ }
+
+ async _getImageOffscreen() {
+ return this._sendMessagePromise({action: 'clipboardGetImageOffscreen'});
+ }
+
_onApiOpenCrossFramePort({targetTabId, targetFrameId}, sender) {
const sourceTabId = (sender && sender.tab ? sender.tab.id : null);
if (typeof sourceTabId !== 'number') {
@@ -2262,4 +2298,36 @@ class Backend {
return {targetTabId, targetFrameId};
}
+
+ // https://developer.chrome.com/docs/extensions/reference/offscreen/
+ async _setupOffscreenDocument() {
+ if (await this._hasOffscreenDocument()) {
+ return;
+ }
+ if (this._creatingOffscreen) {
+ await this._creatingOffscreen;
+ return;
+ }
+
+ this._creatingOffscreen = chrome.offscreen.createDocument({
+ url: 'offscreen.html',
+ reasons: ['CLIPBOARD'],
+ justification: 'Access to the clipboard'
+ });
+ await this._creatingOffscreen;
+ this._creatingOffscreen = null;
+ }
+ async _hasOffscreenDocument() {
+ const offscreenUrl = chrome.runtime.getURL('offscreen.html');
+ if (!chrome.runtime.getContexts) { // chrome version <116
+ const matchedClients = await clients.matchAll();
+ return await matchedClients.some((client) => client.url === offscreenUrl);
+ }
+
+ const contexts = await chrome.runtime.getContexts({
+ contextTypes: ['OFFSCREEN_DOCUMENT'],
+ documentUrls: [offscreenUrl]
+ });
+ return !!contexts.length;
+ }
}
diff --git a/ext/js/background/offscreen-main.js b/ext/js/background/offscreen-main.js
new file mode 100644
index 00000000..808e7766
--- /dev/null
+++ b/ext/js/background/offscreen-main.js
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2023 Yomitan Authors
+ * Copyright (C) 2020-2022 Yomichan Authors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+/* global
+ * Offscreen
+ */
+
+(() => {
+ new Offscreen();
+})();
diff --git a/ext/js/background/offscreen.js b/ext/js/background/offscreen.js
new file mode 100644
index 00000000..bc41d189
--- /dev/null
+++ b/ext/js/background/offscreen.js
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2023 Yomitan Authors
+ * Copyright (C) 2016-2022 Yomichan Authors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+/* global
+ * ClipboardReader
+ */
+
+/**
+ * This class controls the core logic of the extension, including API calls
+ * and various forms of communication between browser tabs and external applications.
+ */
+class Offscreen {
+ /**
+ * Creates a new instance.
+ */
+ constructor() {
+ this._clipboardReader = new ClipboardReader({
+ // eslint-disable-next-line no-undef
+ document: (typeof document === 'object' && document !== null ? document : null),
+ pasteTargetSelector: '#clipboard-paste-target',
+ richContentPasteTargetSelector: '#clipboard-rich-content-paste-target'
+ });
+
+ this._messageHandlers = new Map([
+ ['clipboardGetTextOffscreen', {async: true, contentScript: true, handler: this._getTextHandler.bind(this)}],
+ ['clipboardGetImageOffscreen', {async: true, contentScript: true, handler: this._getImageHandler.bind(this)}]
+ ]);
+
+ const onMessage = this._onMessage.bind(this);
+ chrome.runtime.onMessage.addListener(onMessage);
+ }
+
+ _getTextHandler({useRichText}) {
+ return this._clipboardReader.getText(useRichText);
+ }
+
+ _getImageHandler() {
+ return this._clipboardReader.getImage();
+ }
+
+ _onMessage({action, params}, sender, callback) {
+ const messageHandler = this._messageHandlers.get(action);
+ if (typeof messageHandler === 'undefined') { return false; }
+ this._validatePrivilegedMessageSender(sender);
+
+ return invokeMessageHandler(messageHandler, params, callback, sender);
+ }
+
+ _validatePrivilegedMessageSender(sender) {
+ let {url} = sender;
+ if (typeof url === 'string' && yomichan.isExtensionUrl(url)) { return; }
+ const {tab} = url;
+ if (typeof tab === 'object' && tab !== null) {
+ ({url} = tab);
+ if (typeof url === 'string' && yomichan.isExtensionUrl(url)) { return; }
+ }
+ throw new Error('Invalid message sender');
+ }
+}
diff --git a/ext/js/display/search-display-controller.js b/ext/js/display/search-display-controller.js
index 25d9d6c2..5a271e05 100644
--- a/ext/js/display/search-display-controller.js
+++ b/ext/js/display/search-display-controller.js
@@ -44,7 +44,7 @@ class SearchDisplayController {
this._clipboardMonitor = new ClipboardMonitor({
japaneseUtil,
clipboardReader: {
- getText: async () => (await yomichan.api.clipboardGet())
+ getText: yomichan.api.clipboardGet.bind(yomichan.api)
}
});
this._messageHandlers = new Map();
diff --git a/ext/offscreen.html b/ext/offscreen.html
new file mode 100644
index 00000000..f773e5b1
--- /dev/null
+++ b/ext/offscreen.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width,initial-scale=1">
+ <title>Offscreen</title>
+ <link rel="icon" type="image/png" href="/images/icon16.png" sizes="16x16">
+ <link rel="icon" type="image/png" href="/images/icon19.png" sizes="19x19">
+ <link rel="icon" type="image/png" href="/images/icon32.png" sizes="32x32">
+ <link rel="icon" type="image/png" href="/images/icon38.png" sizes="38x38">
+ <link rel="icon" type="image/png" href="/images/icon48.png" sizes="48x48">
+ <link rel="icon" type="image/png" href="/images/icon64.png" sizes="64x64">
+ <link rel="icon" type="image/png" href="/images/icon128.png" sizes="128x128">
+ <link rel="stylesheet" type="text/css" href="/css/background.css">
+</head>
+<body>
+
+<textarea id="clipboard-paste-target"></textarea>
+
+<!-- Scripts -->
+<script src="/js/core.js"></script>
+
+<script src="/js/yomichan.js"></script>
+
+<script src="/js/comm/clipboard-reader.js"></script>
+
+<script src="/js/background/offscreen.js"></script>
+<script src="/js/background/offscreen-main.js"></script>
+
+<!--
+ Due to a Firefox bug, this next element is purposefully terminated incorrectly.
+ This element must appear directly inside the <body> element, and it must not be closed properly.
+ https://bugzilla.mozilla.org/show_bug.cgi?id=1603985
+-->
+<!-- [html-validate-disable close-order] -->
+<div id="clipboard-rich-content-paste-target" contenteditable="true">
+
+</body>
+</html>