summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/ci.yml3
-rw-r--r--.github/workflows/publish-firefox-development.yml3
-rw-r--r--SECURITY.md5
-rw-r--r--dev/data/manifest-variants.json38
-rw-r--r--dev/translator-vm.js4
-rw-r--r--docs/permissions.md9
-rw-r--r--ext/css/structured-content.css2
-rw-r--r--ext/js/background/backend.js84
-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/js/language/translator.js6
-rw-r--r--ext/offscreen.html39
-rw-r--r--package-lock.json12
14 files changed, 281 insertions, 25 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4a767541..0254cbaa 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -45,7 +45,8 @@ jobs:
run: npm run build
- name: Validate manifest.json of the extension
- uses: cardinalby/schema-validator-action@c2da05377e89dd0c9b7be9420da0b3534b1efcce # pin@v1
+ uses: cardinalby/schema-validator-action@76c68bfc941bd2dc82859f2528984999d1df36a4 # v3.1.0
with:
file: ext/manifest.json
schema: "https://json.schemastore.org/chrome-manifest.json"
+ fixSchemas: true
diff --git a/.github/workflows/publish-firefox-development.yml b/.github/workflows/publish-firefox-development.yml
index 32b0532a..c956b76c 100644
--- a/.github/workflows/publish-firefox-development.yml
+++ b/.github/workflows/publish-firefox-development.yml
@@ -75,7 +75,7 @@ jobs:
"updates": [
{
"version": "${{ github.event.release.name }}",
- "update_link": "${{ steps.uploadReleaseAsset.browser_download_url }}"
+ "update_link": "${{ steps.uploadReleaseAsset.outputs.browser_download_url }}"
}
]
}
@@ -84,6 +84,7 @@ jobs:
EOF
- name: Commit files
+ continue-on-error: true
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 00000000..2f42f913
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,5 @@
+# Security Policy
+
+## Reporting a Vulnerability
+
+Please report vulnerabilties using GitHub's built-in functionality: https://github.com/themoeway/yomitan/security/advisories/new
diff --git a/dev/data/manifest-variants.json b/dev/data/manifest-variants.json
index 26d91d26..07c98b91 100644
--- a/dev/data/manifest-variants.json
+++ b/dev/data/manifest-variants.json
@@ -80,9 +80,9 @@
"storage",
"clipboardWrite",
"unlimitedStorage",
- "webRequest",
"declarativeNetRequest",
- "scripting"
+ "scripting",
+ "offscreen"
],
"optional_permissions": [
"clipboardRead",
@@ -228,7 +228,7 @@
"value": {
"gecko": {
"id": "{cb7c0bec-7085-4f84-8422-7b55a7c4467c}",
- "strict_min_version": "102.0"
+ "strict_min_version": "115.0"
}
}
},
@@ -249,11 +249,36 @@
]
},
{
+ "action": "add",
+ "path": [
+ "permissions"
+ ],
+ "items": [
+ "webRequest"
+ ]
+ },
+ {
+ "action": "add",
+ "path": [
+ "permissions"
+ ],
+ "items": [
+ "webRequestBlocking"
+ ]
+ },
+ {
"action": "remove",
"path": [
"permissions"
],
"item": "declarativeNetRequest"
+ },
+ {
+ "action": "remove",
+ "path": [
+ "permissions"
+ ],
+ "item": "offscreen"
}
],
"excludeFiles": [
@@ -329,6 +354,13 @@
"item": "webRequestBlocking"
},
{
+ "action": "remove",
+ "path": [
+ "permissions"
+ ],
+ "item": "offscreen"
+ },
+ {
"action": "delete",
"path": [
"content_scripts",
diff --git a/dev/translator-vm.js b/dev/translator-vm.js
index e3b700ff..2a51ab8c 100644
--- a/dev/translator-vm.js
+++ b/dev/translator-vm.js
@@ -95,8 +95,8 @@ class TranslatorVM extends DatabaseVM {
japaneseUtil: this._japaneseUtil,
database: dictionaryDatabase
});
- const deinflectionReasions = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'ext', 'data/deinflect.json')));
- this._translator.prepare(deinflectionReasions);
+ const deinflectionReasons = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'ext', 'data/deinflect.json')));
+ this._translator.prepare(deinflectionReasons);
// Assign properties
this._ankiNoteDataCreator = new AnkiNoteDataCreator(this._japaneseUtil);
diff --git a/docs/permissions.md b/docs/permissions.md
index b337bb31..10046210 100644
--- a/docs/permissions.md
+++ b/docs/permissions.md
@@ -9,12 +9,12 @@
`unlimitedStorage` is used to help prevent web browsers from unexpectedly
deleting dictionary data.
-* `webRequest` and `webRequestBlocking` _(Manifest V2 only)_ <br>
+* `webRequest` and `webRequestBlocking` _(Firefox only)_ <br>
Yomichan uses these permissions to ensure certain requests have valid and secure headers.
This sometimes involves removing or changing the `Origin` request header,
as this can be used to fingerprint browser configuration.
-* `declarativeNetRequest` _(Manifest V3 only)_ <br>
+* `declarativeNetRequest` _(Chrome only)_ <br>
Yomichan uses this permission to ensure certain requests have valid and secure headers.
This sometimes involves removing or changing the `Origin` request header,
as this can be used to fingerprint browser configuration.
@@ -23,6 +23,11 @@
Yomichan will sometimes need to inject stylesheets into webpages in order to
properly display the search popup.
+* `offscreen` __(Chrome only)_ <br>
+ Yomitan uses this permission to create a secondary backend document that has DOM access, given that Manifest v3
+ service workers do not. Service workers can then reach out to out to this document in order to complete
+ actions that require access to DOM APIs, such as any that require clipboard access.
+
* `clipboardWrite` <br>
Yomichan supports simulating the `Ctrl+C` (copy to clipboard) keyboard shortcut
when a definitions popup is open and focused.
diff --git a/ext/css/structured-content.css b/ext/css/structured-content.css
index 9c894008..1d4677b5 100644
--- a/ext/css/structured-content.css
+++ b/ext/css/structured-content.css
@@ -201,7 +201,7 @@
/* Links */
.gloss-link-text {
- vertical-align: middle;
+ vertical-align: baseline;
}
.gloss-link-external-icon {
display: inline-block;
diff --git a/ext/js/background/backend.js b/ext/js/background/backend.js
index 58387fd4..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 {
@@ -226,8 +240,8 @@ class Backend {
log.error(e);
}
- const deinflectionReasions = await this._fetchAsset('/data/deinflect.json', true);
- this._translator.prepare(deinflectionReasions);
+ const deinflectionReasons = await this._fetchAsset('/data/deinflect.json', true);
+ this._translator.prepare(deinflectionReasons);
await this._optionsUtil.prepare();
this._defaultAnkiFieldTemplates = (await this._fetchAsset('/data/templates/default-anki-field-templates.handlebars')).trim();
@@ -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/js/language/translator.js b/ext/js/language/translator.js
index edb38bfb..3b47cc51 100644
--- a/ext/js/language/translator.js
+++ b/ext/js/language/translator.js
@@ -93,6 +93,12 @@ class Translator {
}
if (mode === 'simple') {
+ if (sortFrequencyDictionary !== null) {
+ const sortDictionaryMap = [sortFrequencyDictionary]
+ .filter((key) => enabledDictionaryMap.has(key))
+ .reduce((subMap, key) => subMap.set(key, enabledDictionaryMap.get(key)), new Map());
+ await this._addTermMeta(dictionaryEntries, sortDictionaryMap);
+ }
this._clearTermTags(dictionaryEntries);
} else {
await this._addTermMeta(dictionaryEntries, enabledDictionaryMap);
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>
diff --git a/package-lock.json b/package-lock.json
index 88207e0e..61b77b09 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4001,9 +4001,9 @@
}
},
"node_modules/postcss": {
- "version": "8.4.29",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz",
- "integrity": "sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==",
+ "version": "8.4.31",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
+ "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"dev": true,
"funding": [
{
@@ -8696,9 +8696,9 @@
"dev": true
},
"postcss": {
- "version": "8.4.29",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz",
- "integrity": "sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==",
+ "version": "8.4.31",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
+ "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"dev": true,
"requires": {
"nanoid": "^3.3.6",