From 4b7f91fa5f43ba6023f1c9991348b56b3e26a11b Mon Sep 17 00:00:00 2001
From: praschke <stel@comfy.monster>
Date: Wed, 16 Aug 2023 11:37:13 +0100
Subject: fix script and style injection in Firefox

Firefox added the scripting API in 102. This should fix the majority
of warnings listed in #96:

- insertCSS
- executeScript
- getRegisteredContentScripts
- contentScripts.register
- registerContentScripts
- unregisterContentScripts
---
 ext/js/background/backend.js        |   4 +-
 ext/js/background/script-manager.js | 111 ++++--------------------------------
 2 files changed, 13 insertions(+), 102 deletions(-)

(limited to 'ext/js')

diff --git a/ext/js/background/backend.js b/ext/js/background/backend.js
index db6cfada..dd233abb 100644
--- a/ext/js/background/backend.js
+++ b/ext/js/background/backend.js
@@ -593,7 +593,7 @@ class Backend {
     async _onApiInjectStylesheet({type, value}, sender) {
         const {frameId, tab} = sender;
         if (!isObject(tab)) { throw new Error('Invalid tab'); }
-        return await this._scriptManager.injectStylesheet(type, value, tab.id, frameId, false, true, 'document_start');
+        return await this._scriptManager.injectStylesheet(type, value, tab.id, frameId, false);
     }
 
     async _onApiGetStylesheetContent({url}) {
@@ -790,7 +790,7 @@ class Backend {
         if (typeof tabId !== 'number') { throw new Error('Sender has invalid tab ID'); }
         const {frameId} = sender;
         for (const file of files) {
-            await this._scriptManager.injectScript(file, tabId, frameId, false, true, 'document_start');
+            await this._scriptManager.injectScript(file, tabId, frameId, false);
         }
     }
 
diff --git a/ext/js/background/script-manager.js b/ext/js/background/script-manager.js
index 722a46f0..a0aed2a3 100644
--- a/ext/js/background/script-manager.js
+++ b/ext/js/background/script-manager.js
@@ -36,14 +36,10 @@ class ScriptManager {
      * @param {number} tabId The id of the tab to inject into.
      * @param {number} [frameId] The id of the frame to inject into.
      * @param {boolean} [allFrames] Whether or not the stylesheet should be injected into all frames.
-     * @param {boolean} [matchAboutBlank] Whether or not the stylesheet should be injected into about:blank frames.
-     * @param {string} [runAt] The time to inject the stylesheet at.
      * @returns {Promise<void>}
      */
-    injectStylesheet(type, content, tabId, frameId, allFrames, matchAboutBlank, runAt) {
-        if (isObject(chrome.tabs) && typeof chrome.tabs.insertCSS === 'function') {
-            return this._injectStylesheetMV2(type, content, tabId, frameId, allFrames, matchAboutBlank, runAt);
-        } else if (isObject(chrome.scripting) && typeof chrome.scripting.insertCSS === 'function') {
+    injectStylesheet(type, content, tabId, frameId, allFrames) {
+        if (isObject(chrome.scripting) && typeof chrome.scripting.insertCSS === 'function') {
             return this._injectStylesheetMV3(type, content, tabId, frameId, allFrames);
         } else {
             return Promise.reject(new Error('Stylesheet injection not supported'));
@@ -56,14 +52,10 @@ class ScriptManager {
      * @param {number} tabId The id of the tab to inject into.
      * @param {number} [frameId] The id of the frame to inject into.
      * @param {boolean} [allFrames] Whether or not the script should be injected into all frames.
-     * @param {boolean} [matchAboutBlank] Whether or not the script should be injected into about:blank frames.
-     * @param {string} [runAt] The time to inject the script at.
      * @returns {Promise<{frameId: number, result: object}>} The id of the frame and the result of the script injection.
      */
-    injectScript(file, tabId, frameId, allFrames, matchAboutBlank, runAt) {
-        if (isObject(chrome.tabs) && typeof chrome.tabs.executeScript === 'function') {
-            return this._injectScriptMV2(file, tabId, frameId, allFrames, matchAboutBlank, runAt);
-        } else if (isObject(chrome.scripting) && typeof chrome.scripting.executeScript === 'function') {
+    injectScript(file, tabId, frameId, allFrames) {
+        if (isObject(chrome.scripting) && typeof chrome.scripting.executeScript === 'function') {
             return this._injectScriptMV3(file, tabId, frameId, allFrames);
         } else {
             return Promise.reject(new Error('Script injection not supported'));
@@ -122,19 +114,6 @@ class ScriptManager {
             throw new Error('Registration already exists');
         }
 
-        // Firefox
-        if (
-            typeof browser === 'object' && browser !== null &&
-            isObject(browser.contentScripts) &&
-            typeof browser.contentScripts.register === 'function'
-        ) {
-            const details2 = this._convertContentScriptRegistrationDetails(details, id, true);
-            const registration = await browser.contentScripts.register(details2);
-            this._contentScriptRegistrations.set(id, registration);
-            return;
-        }
-
-        // Chrome
         if (isObject(chrome.scripting) && typeof chrome.scripting.registerContentScripts === 'function') {
             const details2 = this._convertContentScriptRegistrationDetails(details, id, false);
             await new Promise((resolve, reject) => {
@@ -161,18 +140,17 @@ class ScriptManager {
      * @returns {Promise<boolean>} `true` if the content script was unregistered, `false` otherwise.
      */
     async unregisterContentScript(id) {
-        // Chrome
         if (isObject(chrome.scripting) && typeof chrome.scripting.unregisterContentScripts === 'function') {
             this._contentScriptRegistrations.delete(id);
             try {
-                await this._unregisterContentScriptChrome(id);
+                await this._unregisterContentScriptMV3(id);
                 return true;
             } catch (e) {
                 return false;
             }
         }
 
-        // Firefox or fallback
+        // Fallback
         const registration = this._contentScriptRegistrations.get(id);
         if (typeof registration === 'undefined') { return false; }
         this._contentScriptRegistrations.delete(id);
@@ -187,19 +165,7 @@ class ScriptManager {
      * @returns {string[]} An array of the required permissions, which may be empty.
      */
     getRequiredContentScriptRegistrationPermissions() {
-        if (
-            // Firefox
-            (
-                typeof browser === 'object' && browser !== null &&
-                isObject(browser.contentScripts) &&
-                typeof browser.contentScripts.register === 'function'
-            ) ||
-            // Chrome
-            (
-                isObject(chrome.scripting) &&
-                typeof chrome.scripting.registerContentScripts === 'function'
-            )
-        ) {
+        if (isObject(chrome.scripting) && typeof chrome.scripting.registerContentScripts === 'function') {
             return [];
         }
 
@@ -209,39 +175,6 @@ class ScriptManager {
 
     // Private
 
-    _injectStylesheetMV2(type, content, tabId, frameId, allFrames, matchAboutBlank, runAt) {
-        return new Promise((resolve, reject) => {
-            const details = (
-                type === 'file' ?
-                {
-                    file: content,
-                    runAt,
-                    cssOrigin: 'author',
-                    allFrames,
-                    matchAboutBlank
-                } :
-                {
-                    code: content,
-                    runAt,
-                    cssOrigin: 'user',
-                    allFrames,
-                    matchAboutBlank
-                }
-            );
-            if (typeof frameId === 'number') {
-                details.frameId = frameId;
-            }
-            chrome.tabs.insertCSS(tabId, details, () => {
-                const e = chrome.runtime.lastError;
-                if (e) {
-                    reject(new Error(e.message));
-                } else {
-                    resolve();
-                }
-            });
-        });
-    }
-
     _injectStylesheetMV3(type, content, tabId, frameId, allFrames) {
         return new Promise((resolve, reject) => {
             const details = (
@@ -267,27 +200,6 @@ class ScriptManager {
         });
     }
 
-    _injectScriptMV2(file, tabId, frameId, allFrames, matchAboutBlank, runAt) {
-        return new Promise((resolve, reject) => {
-            const details = {
-                allFrames,
-                frameId,
-                file,
-                matchAboutBlank,
-                runAt
-            };
-            chrome.tabs.executeScript(tabId, details, (results) => {
-                const e = chrome.runtime.lastError;
-                if (e) {
-                    reject(new Error(e.message));
-                } else {
-                    const result = results[0];
-                    resolve({frameId, result});
-                }
-            });
-        });
-    }
-
     _injectScriptMV3(file, tabId, frameId, allFrames) {
         return new Promise((resolve, reject) => {
             const details = {
@@ -310,7 +222,7 @@ class ScriptManager {
         });
     }
 
-    _unregisterContentScriptChrome(id) {
+    _unregisterContentScriptMV3(id) {
         return new Promise((resolve, reject) => {
             chrome.scripting.unregisterContentScripts({ids: [id]}, () => {
                 const e = chrome.runtime.lastError;
@@ -407,7 +319,7 @@ class ScriptManager {
         const {urlRegex} = details;
         if (urlRegex !== null && !urlRegex.test(url)) { return; }
 
-        let {allFrames, css, js, matchAboutBlank, runAt} = details;
+        let {allFrames, css, js, runAt} = details;
 
         if (isWebNavigation) {
             if (allFrames) {
@@ -425,14 +337,13 @@ class ScriptManager {
 
         const promises = [];
         if (Array.isArray(css)) {
-            const runAtCss = (typeof runAt === 'string' ? runAt : 'document_start');
             for (const file of css) {
-                promises.push(this.injectStylesheet('file', file, tabId, frameId, allFrames, matchAboutBlank, runAtCss));
+                promises.push(this.injectStylesheet('file', file, tabId, frameId, allFrames));
             }
         }
         if (Array.isArray(js)) {
             for (const file of js) {
-                promises.push(this.injectScript(file, tabId, frameId, allFrames, matchAboutBlank, runAt));
+                promises.push(this.injectScript(file, tabId, frameId, allFrames));
             }
         }
         await Promise.all(promises);
-- 
cgit v1.2.3


From bc8425ec6b82ac2c8aa761ee4a94e2b6afedcad2 Mon Sep 17 00:00:00 2001
From: praschke <stel@comfy.monster>
Date: Wed, 16 Aug 2023 11:44:45 +0100
Subject: fix: StyleOrigin enum absent in Firefox

the API accepts string literals, which is all this enum provides. This
should fix two warnings in #96.
---
 ext/js/background/script-manager.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

(limited to 'ext/js')

diff --git a/ext/js/background/script-manager.js b/ext/js/background/script-manager.js
index a0aed2a3..694b64db 100644
--- a/ext/js/background/script-manager.js
+++ b/ext/js/background/script-manager.js
@@ -179,8 +179,8 @@ class ScriptManager {
         return new Promise((resolve, reject) => {
             const details = (
                 type === 'file' ?
-                {origin: chrome.scripting.StyleOrigin.AUTHOR, files: [content]} :
-                {origin: chrome.scripting.StyleOrigin.USER,   css: content}
+                {origin: 'AUTHOR', files: [content]} :
+                {origin: 'USER',   css: content}
             );
             details.target = {
                 tabId,
-- 
cgit v1.2.3


From 660aa2a7cf3b5771d02114a454555cd9785e759e Mon Sep 17 00:00:00 2001
From: praschke <stel@comfy.monster>
Date: Wed, 16 Aug 2023 11:49:39 +0100
Subject: fix: window.getSelection() can return null on Firefox

---
 ext/js/language/text-scanner.js | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

(limited to 'ext/js')

diff --git a/ext/js/language/text-scanner.js b/ext/js/language/text-scanner.js
index 6fa7a454..af5cc8fe 100644
--- a/ext/js/language/text-scanner.js
+++ b/ext/js/language/text-scanner.js
@@ -145,7 +145,8 @@ class TextScanner extends EventDispatcher {
 
         if (value) {
             this._hookEvents();
-            this._userHasNotSelectedAnythingManually = window.getSelection().isCollapsed;
+            const selection = window.getSelection();
+            this._userHasNotSelectedAnythingManually = (selection === null) ? true : selection.isCollapsed;
         }
     }
 
-- 
cgit v1.2.3


From 07333a2807fc23875a3ffa34f97ea0ff1e44d3d6 Mon Sep 17 00:00:00 2001
From: praschke <stel@comfy.monster>
Date: Thu, 17 Aug 2023 14:19:41 +0100
Subject: remove broken fetch(1, 2)

this line serves no purpose. the commit it was introduced in has the
message 'Document RequestBuilder' and is the only non-documentary line
in the commit.

related to #204.
---
 ext/js/background/request-builder.js | 1 -
 1 file changed, 1 deletion(-)

(limited to 'ext/js')

diff --git a/ext/js/background/request-builder.js b/ext/js/background/request-builder.js
index eca7d0d3..663e242b 100644
--- a/ext/js/background/request-builder.js
+++ b/ext/js/background/request-builder.js
@@ -54,7 +54,6 @@ class RequestBuilder {
      * @returns {Promise<Response>} The response of the `fetch` call.
      */
     async fetchAnonymous(url, init) {
-        fetch(1, 2);
         if (isObject(chrome.declarativeNetRequest)) {
             return await this._fetchAnonymousDeclarative(url, init);
         }
-- 
cgit v1.2.3


From b5752a451e93cc58b281552fc64125f807e32e15 Mon Sep 17 00:00:00 2001
From: Ewan Fox <ewan@ewanfox.com>
Date: Sun, 20 Aug 2023 05:02:48 +0100
Subject: Add Intuitive Permission Toggle to Welcome Page (#214)

---
 .../settings/recommended-permissions-controller.js | 74 ++++++++++++++++++++++
 ext/js/pages/welcome-main.js                       |  4 ++
 ext/welcome.html                                   | 15 +++++
 3 files changed, 93 insertions(+)
 create mode 100644 ext/js/pages/settings/recommended-permissions-controller.js

(limited to 'ext/js')

diff --git a/ext/js/pages/settings/recommended-permissions-controller.js b/ext/js/pages/settings/recommended-permissions-controller.js
new file mode 100644
index 00000000..3d25d5eb
--- /dev/null
+++ b/ext/js/pages/settings/recommended-permissions-controller.js
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2023  Yomitan Authors
+ * Copyright (C) 2021-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/>.
+ */
+
+class RecommendedPermissionsController {
+    constructor(settingsController) {
+        this._settingsController = settingsController;
+        this._originToggleNodes = null;
+        this._eventListeners = new EventListenerCollection();
+        this._errorContainer = null;
+    }
+
+    async prepare() {
+        this._originToggleNodes = document.querySelectorAll('.recommended-permissions-toggle');
+        this._errorContainer = document.querySelector('#recommended-permissions-error');
+        for (const node of this._originToggleNodes) {
+            node.addEventListener('change', this._onOriginToggleChange.bind(this), false);
+        }
+
+        this._settingsController.on('permissionsChanged', this._onPermissionsChanged.bind(this));
+        await this._updatePermissions();
+    }
+
+    // Private
+
+    _onPermissionsChanged({permissions}) {
+        this._eventListeners.removeAllEventListeners();
+        const originsSet = new Set(permissions.origins);
+        for (const node of this._originToggleNodes) {
+            node.checked = originsSet.has(node.dataset.origin);
+        }
+    }
+
+    _onOriginToggleChange(e) {
+        const node = e.currentTarget;
+        const value = node.checked;
+        node.checked = !value;
+
+        const {origin} = node.dataset;
+        this._setOriginPermissionEnabled(origin, value);
+    }
+
+    async _updatePermissions() {
+        const permissions = await this._settingsController.permissionsUtil.getAllPermissions();
+        this._onPermissionsChanged({permissions});
+    }
+
+    async _setOriginPermissionEnabled(origin, enabled) {
+        let added = false;
+        try {
+            added = await this._settingsController.permissionsUtil.setPermissionsGranted({origins: [origin]}, enabled);
+        } catch (e) {
+            this._errorContainer.hidden = false;
+            this._errorContainer.textContent = e.message;
+        }
+        if (!added) { return false; }
+        await this._updatePermissions();
+        return true;
+    }
+}
diff --git a/ext/js/pages/welcome-main.js b/ext/js/pages/welcome-main.js
index eb8bd675..521ce2c2 100644
--- a/ext/js/pages/welcome-main.js
+++ b/ext/js/pages/welcome-main.js
@@ -23,6 +23,7 @@
  * ExtensionContentController
  * GenericSettingController
  * ModalController
+ * RecommendedPermissionsController
  * ScanInputsSimpleController
  * SettingsController
  * SettingsDisplayController
@@ -77,6 +78,9 @@ async function setupGenericSettingsController(genericSettingController) {
         const simpleScanningInputController = new ScanInputsSimpleController(settingsController);
         simpleScanningInputController.prepare();
 
+        const recommendedPermissionsController = new RecommendedPermissionsController(settingsController);
+        recommendedPermissionsController.prepare();
+
         await Promise.all(preparePromises);
 
         document.documentElement.dataset.loaded = 'true';
diff --git a/ext/welcome.html b/ext/welcome.html
index bfa3cefd..8a01d06f 100644
--- a/ext/welcome.html
+++ b/ext/welcome.html
@@ -98,6 +98,20 @@
         </div>
     </div>
 
+    <h2>Recommended Permissions &#40;Important&#41;</h2>
+    <div class="settings-group">
+        <div class="settings-item"><div class="settings-item-inner">
+            <div class="settings-item-left">
+                <div class="settings-item-label">Enable recommended permissions</div>
+                <div class="settings-item-description">This will allow Yomitan to scan text from most sites. Further configuration is available on the <a href="/permissions.html" rel="noopener">Permissions page</a>.</div>
+            </div>
+            <div class="settings-item-right">
+                <label class="toggle"><input type="checkbox" class="recommended-permissions-toggle" data-origin="&lt;all_urls&gt;"><span class="toggle-body"><span class="toggle-track"></span><span class="toggle-knob"></span></span></label>
+            </div>
+            <div id="recommended-permissions-error" class="margin-above danger-text" hidden></div>
+        </div></div>
+    </div>
+
     <h2>Basic customization</h2>
     <div class="settings-group">
         <div class="settings-item"><div class="settings-item-inner">
@@ -424,6 +438,7 @@
 <script src="/js/pages/settings/generic-setting-controller.js"></script>
 <script src="/js/pages/settings/modal.js"></script>
 <script src="/js/pages/settings/modal-controller.js"></script>
+<script src="/js/pages/settings/recommended-permissions-controller.js"></script>
 <script src="/js/pages/settings/scan-inputs-simple-controller.js"></script>
 <script src="/js/pages/settings/settings-controller.js"></script>
 <script src="/js/pages/settings/settings-display-controller.js"></script>
-- 
cgit v1.2.3