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 (Important)</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="<all_urls>"><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