summaryrefslogtreecommitdiff
path: root/ext
diff options
context:
space:
mode:
authorjbukl <noreply@github.com>2023-10-20 02:37:47 -0400
committerjbukl <noreply@github.com>2023-10-20 15:25:52 -0400
commit9a39d0a7e2896edd4a6deebad00b8550cfffc15b (patch)
treeab4aa25d9c89856066ae9d1c347da3ad9bb6ea4e /ext
parentc3be9af7b6f00dad7107fcdae60a8004cc81936a (diff)
fix: chromium clipboard access
on chromium, backend calls to clipboardGet are forwarded to an offscreen script
Diffstat (limited to 'ext')
-rw-r--r--ext/css/offscreen.css30
-rw-r--r--ext/js/background/backend.js73
-rw-r--r--ext/js/display/search-display-controller.js2
-rw-r--r--ext/js/offscreen/offscreen-main.js25
-rw-r--r--ext/js/offscreen/offscreen.js70
-rw-r--r--ext/offscreen.html40
6 files changed, 233 insertions, 7 deletions
diff --git a/ext/css/offscreen.css b/ext/css/offscreen.css
new file mode 100644
index 00000000..ab283025
--- /dev/null
+++ b/ext/css/offscreen.css
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2023 Yomitan Authors
+ * Copyright (C) 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/>.
+ */
+
+/* stylelint-disable declaration-no-important */
+#clipboard-rich-content-paste-target * {
+ background-image: none !important;
+ list-style-image: none !important;
+ content: none !important;
+ cursor: auto !important;
+ border-image-source: none !important;
+ offset-path: none !important;
+ -webkit-mask-image: none !important;
+ mask-image: none !important;
+}
+/* stylelint-enable declaration-no-important */
diff --git a/ext/js/background/backend.js b/ext/js/background/backend.js
index 565f4abf..57565eec 100644
--- a/ext/js/background/backend.js
+++ b/ext/js/background/backend.js
@@ -57,12 +57,19 @@ 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'
- });
+
+ this._clipboardReader = {
+ getText: this._getTextOffscreen.bind(this)
+ };
+ if (!chrome || !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'
+ });
+ }
+
this._clipboardMonitor = new ClipboardMonitor({
japaneseUtil: this._japaneseUtil,
clipboardReader: this._clipboardReader
@@ -97,6 +104,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)}],
@@ -557,6 +566,21 @@ class Backend {
return this._clipboardReader.getText(false);
}
+ async _getTextOffscreen(useRichText) {
+ await this._setupOffscreenDocument();
+ return new Promise((resolve, reject) => {
+ const callback = (response) => {
+ try {
+ resolve(this._getMessageResponseResult(response));
+ } catch (error) {
+ reject(error);
+ }
+ };
+
+ chrome.runtime.sendMessage({action: 'clipboardGetOffscreen', params: {useRichText}}, callback);
+ });
+ }
+
async _onApiGetDisplayTemplatesHtml() {
return await this._fetchAsset('/display-templates.html');
}
@@ -2262,4 +2286,41 @@ class Backend {
return {targetTabId, targetFrameId};
}
+
+ // https://developer.chrome.com/docs/extensions/reference/offscreen/
+ async _setupOffscreenDocument() {
+ // Check all windows controlled by the service worker to see if one
+ // of them is the offscreen document with the given path
+ if (await this._hasOffscreenDocument()) {
+ return;
+ }
+
+ // create offscreen document
+ if (this._creatingOffscreen) {
+ await this._creatingOffscreen;
+ } else {
+ this._creatingOffscreen = chrome.offscreen.createDocument({
+ url: 'offscreen.html',
+ reasons: ['CLIPBOARD'],
+ justification: 'reason for needing the document'
+ });
+ await this._creatingOffscreen;
+ this._creatingOffscreen = null;
+ }
+ }
+ async _hasOffscreenDocument() {
+ const offscreenUrl = chrome.runtime.getURL('offscreen.html');
+ if (chrome.runtime.getContexts) {
+ const contexts = await chrome.runtime.getContexts({
+ contextTypes: ['OFFSCREEN_DOCUMENT'],
+ documentUrls: [offscreenUrl]
+ });
+ return Boolean(contexts.length);
+ } else {
+ const matchedClients = await clients.matchAll();
+ return await matchedClients.some((client) => {
+ client.url.includes(chrome.runtime.id);
+ });
+ }
+ }
}
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/js/offscreen/offscreen-main.js b/ext/js/offscreen/offscreen-main.js
new file mode 100644
index 00000000..808e7766
--- /dev/null
+++ b/ext/js/offscreen/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/offscreen/offscreen.js b/ext/js/offscreen/offscreen.js
new file mode 100644
index 00000000..1ff9aae3
--- /dev/null
+++ b/ext/js/offscreen/offscreen.js
@@ -0,0 +1,70 @@
+/*
+ * 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
+ * Environment
+ */
+
+/**
+ * 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([
+ ['clipboardGetOffscreen', {async: true, contentScript: true, handler: this._getTextHandler.bind(this)}]
+ ]);
+
+ const onMessage = this._onMessage.bind(this);
+ chrome.runtime.onMessage.addListener(onMessage);
+ }
+
+ _getTextHandler({useRichText}) {
+ return this._clipboardReader.getText(useRichText);
+ }
+
+ _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/offscreen.html b/ext/offscreen.html
new file mode 100644
index 00000000..85576998
--- /dev/null
+++ b/ext/offscreen.html
@@ -0,0 +1,40 @@
+<!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/extension/environment.js"></script>
+
+<script src="/js/offscreen/offscreen.js"></script>
+<script src="/js/offscreen/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>