diff options
| -rw-r--r-- | .eslintrc.json | 40 | ||||
| -rw-r--r-- | dev/data/manifest-variants.json | 21 | ||||
| -rw-r--r-- | ext/bg/js/backend.js | 123 | ||||
| -rw-r--r-- | ext/bg/js/native-simple-dom-parser.js | 2 | ||||
| -rw-r--r-- | ext/mixed/js/core.js | 9 | ||||
| -rw-r--r-- | ext/sw.js | 45 | ||||
| -rw-r--r-- | test/test-sw.js | 80 | 
7 files changed, 282 insertions, 38 deletions
| diff --git a/.eslintrc.json b/.eslintrc.json index 3723d97d..486d7bd1 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -156,6 +156,46 @@                  "node": true,                  "webextensions": false              } +        }, +        { +            "files": [ +                "ext/mixed/js/core.js", +                "ext/mixed/js/yomichan.js", +                "ext/mixed/js/environment.js", +                "ext/mixed/js/japanese.js", +                "ext/mixed/js/cache-map.js", +                "ext/mixed/js/dictionary-data-util.js", +                "ext/mixed/js/object-property-accessor.js", +                "ext/bg/js/anki.js", +                "ext/bg/js/audio-downloader.js", +                "ext/bg/js/clipboard-monitor.js", +                "ext/bg/js/clipboard-reader.js", +                "ext/bg/js/database.js", +                "ext/bg/js/deinflector.js", +                "ext/bg/js/dictionary-database.js", +                "ext/bg/js/json-schema.js", +                "ext/bg/js/mecab.js", +                "ext/bg/js/media-utility.js", +                "ext/bg/js/options.js", +                "ext/bg/js/profile-conditions.js", +                "ext/bg/js/request-builder.js", +                "ext/bg/js/native-simple-dom-parser.js", +                "ext/bg/js/text-source-map.js", +                "ext/bg/js/translator.js", +                "ext/bg/js/backend.js", +                "ext/bg/js/background-main.js" +            ], +            "env": { +                "browser": false, +                "serviceworker": true, +                "es2017": true, +                "webextensions": true +            }, +            "globals": { +                "FileReader": "readonly", +                "Intl": "readonly", +                "crypto": "readonly" +            }          }      ]  } diff --git a/dev/data/manifest-variants.json b/dev/data/manifest-variants.json index 3f65d86f..0532ab9b 100644 --- a/dev/data/manifest-variants.json +++ b/dev/data/manifest-variants.json @@ -131,6 +131,27 @@              ]          },          { +            "name": "chrome-mv3", +            "fileName": "yomichan-chrome-mv3.zip", +            "modifications": [ +                {"action": "set",    "path": ["manifest_version"], "value": 3}, +                {"action": "move",   "path": ["browser_action"], "newPath": ["action"]}, +                {"action": "delete", "path": ["background", "page"]}, +                {"action": "delete", "path": ["background", "persistent"]}, +                {"action": "set",    "path": ["background", "service_worker"], "value": "sw.js"}, +                {"action": "move",   "path": ["content_security_policy"], "newPath": ["content_security_policy_old"]}, +                {"action": "set",    "path": ["content_security_policy"], "value": {}}, +                {"action": "move",   "path": ["content_security_policy_old"], "newPath": ["content_security_policy", "extension_pages"]}, +                {"action": "move",   "path": ["sandbox", "content_security_policy"], "newPath": ["content_security_policy", "sandbox"]}, +                {"action": "remove", "path": ["permissions"], "item": "<all_urls>"}, +                {"action": "set",    "path": ["host_permissions"], "value": ["<all_urls>"], "after": "optional_permissions"}, +                {"action": "add",    "path": ["permissions"], "items": ["scripting"]}, +                {"action": "move",   "path": ["web_accessible_resources"], "newPath": ["web_accessible_resources_old"]}, +                {"action": "set",    "path": ["web_accessible_resources"], "value": [{"resources": [], "matches": ["<all_urls>"]}], "after": "web_accessible_resources_old"}, +                {"action": "move",   "path": ["web_accessible_resources_old"], "newPath": ["web_accessible_resources", 0, "resources"]} +            ] +        }, +        {              "name": "firefox",              "fileName": "yomichan-firefox.xpi",              "modifications": [ diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index fb11ba2e..322d9400 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -47,6 +47,7 @@ class Backend {          this._mecab = new Mecab();          this._mediaUtility = new MediaUtility();          this._clipboardReader = new ClipboardReader({ +            // eslint-disable-next-line no-undef              document: (typeof document === 'object' && document !== null ? document : null),              pasteTargetSelector: '#clipboard-paste-target',              imagePasteTargetSelector: '#clipboard-image-paste-target', @@ -551,43 +552,7 @@ class Backend {      }      _onApiInjectStylesheet({type, value}, sender) { -        if (!sender.tab) { -            return Promise.reject(new Error('Invalid tab')); -        } - -        const tabId = sender.tab.id; -        const frameId = sender.frameId; -        const details = ( -            type === 'file' ? -            { -                file: value, -                runAt: 'document_start', -                cssOrigin: 'author', -                allFrames: false, -                matchAboutBlank: true -            } : -            { -                code: value, -                runAt: 'document_start', -                cssOrigin: 'user', -                allFrames: false, -                matchAboutBlank: true -            } -        ); -        if (typeof frameId === 'number') { -            details.frameId = frameId; -        } - -        return new Promise((resolve, reject) => { -            chrome.tabs.insertCSS(tabId, details, () => { -                const e = chrome.runtime.lastError; -                if (e) { -                    reject(new Error(e.message)); -                } else { -                    resolve(); -                } -            }); -        }); +        return this._injectStylesheet(type, value, sender);      }      async _onApiGetStylesheetContent({url}) { @@ -1772,4 +1737,88 @@ class Backend {              });          });      } + +    _injectStylesheet(type, value, target) { +        if (isObject(chrome.tabs) && typeof chrome.tabs.insertCSS === 'function') { +            return this._injectStylesheetMV2(type, value, target); +        } else if (isObject(chrome.scripting) && typeof chrome.scripting.insertCSS === 'function') { +            return this._injectStylesheetMV3(type, value, target); +        } else { +            return Promise.reject(new Error('insertCSS function not available')); +        } +    } + +    _injectStylesheetMV2(type, value, target) { +        return new Promise((resolve, reject) => { +            if (!target.tab) { +                reject(new Error('Invalid tab')); +                return; +            } + +            const tabId = target.tab.id; +            const frameId = target.frameId; +            const details = ( +                type === 'file' ? +                { +                    file: value, +                    runAt: 'document_start', +                    cssOrigin: 'author', +                    allFrames: false, +                    matchAboutBlank: true +                } : +                { +                    code: value, +                    runAt: 'document_start', +                    cssOrigin: 'user', +                    allFrames: false, +                    matchAboutBlank: true +                } +            ); +            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, value, target) { +        return new Promise((resolve, reject) => { +            if (!target.tab) { +                reject(new Error('Invalid tab')); +                return; +            } + +            const tabId = target.tab.id; +            const frameId = target.frameId; +            const details = ( +                type === 'file' ? +                {origin: chrome.scripting.StyleOrigin.AUTHOR, files: [value]} : +                {origin: chrome.scripting.StyleOrigin.USER,   css: value} +            ); +            details.target = { +                tabId, +                allFrames: false +            }; +            if (typeof frameId === 'number') { +                details.target.frameIds = [frameId]; +            } + +            chrome.scripting.insertCSS(details, () => { +                const e = chrome.runtime.lastError; +                if (e) { +                    reject(new Error(e.message)); +                } else { +                    resolve(); +                } +            }); +        }); +    }  } diff --git a/ext/bg/js/native-simple-dom-parser.js b/ext/bg/js/native-simple-dom-parser.js index 4e0d89ea..c1752bc4 100644 --- a/ext/bg/js/native-simple-dom-parser.js +++ b/ext/bg/js/native-simple-dom-parser.js @@ -17,6 +17,8 @@  class NativeSimpleDOMParser {      constructor(content) { +        // TODO : Remove +        // eslint-disable-next-line no-undef          this._document = new DOMParser().parseFromString(content, 'text/html');      } diff --git a/ext/mixed/js/core.js b/ext/mixed/js/core.js index 72ab7474..aa894e01 100644 --- a/ext/mixed/js/core.js +++ b/ext/mixed/js/core.js @@ -304,7 +304,12 @@ function promiseTimeout(delay, resolveValue) {  }  function promiseAnimationFrame(timeout=null) { -    return new Promise((resolve) => { +    return new Promise((resolve, reject) => { +        if (typeof cancelAnimationFrame !== 'function' || typeof requestAnimationFrame !== 'function') { +            reject(new Error('Animation not supported in this context')); +            return; +        } +          let timer = null;          let frameRequest = null;          const onFrame = (time) => { @@ -318,12 +323,14 @@ function promiseAnimationFrame(timeout=null) {          const onTimeout = () => {              timer = null;              if (frameRequest !== null) { +                // eslint-disable-next-line no-undef                  cancelAnimationFrame(frameRequest);                  frameRequest = null;              }              resolve({time: timeout, timeout: true});          }; +        // eslint-disable-next-line no-undef          frameRequest = requestAnimationFrame(onFrame);          if (typeof timeout === 'number') {              timer = setTimeout(onTimeout, timeout); diff --git a/ext/sw.js b/ext/sw.js new file mode 100644 index 00000000..1b377ae0 --- /dev/null +++ b/ext/sw.js @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2020  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/>. + */ + +self.importScripts( +    '/mixed/lib/wanakana.min.js', +    '/mixed/js/core.js', +    '/mixed/js/yomichan.js', +    '/mixed/js/environment.js', +    '/mixed/js/japanese.js', +    '/mixed/js/cache-map.js', +    '/mixed/js/dictionary-data-util.js', +    '/mixed/js/object-property-accessor.js', +    '/bg/js/anki.js', +    '/bg/js/audio-downloader.js', +    '/bg/js/clipboard-monitor.js', +    '/bg/js/clipboard-reader.js', +    '/bg/js/database.js', +    '/bg/js/deinflector.js', +    '/bg/js/dictionary-database.js', +    '/bg/js/json-schema.js', +    '/bg/js/mecab.js', +    '/bg/js/media-utility.js', +    '/bg/js/options.js', +    '/bg/js/profile-conditions.js', +    '/bg/js/request-builder.js', +    '/bg/js/native-simple-dom-parser.js', +    '/bg/js/text-source-map.js', +    '/bg/js/translator.js', +    '/bg/js/backend.js', +    '/bg/js/background-main.js' +); diff --git a/test/test-sw.js b/test/test-sw.js new file mode 100644 index 00000000..a2cb9df4 --- /dev/null +++ b/test/test-sw.js @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2020  Yomichan Authors + * Author: 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/>. + */ + +const fs = require('fs'); +const path = require('path'); +const {JSDOM} = require('jsdom'); +const {VM} = require('../dev/vm'); +const assert = require('assert'); + + +function getAllHtmlScriptPaths(fileName) { +    const domSource = fs.readFileSync(fileName, {encoding: 'utf8'}); +    const dom = new JSDOM(domSource); +    const {window} = dom; +    const {document} = window; +    try { +        const scripts = document.querySelectorAll('script'); +        return [...scripts].map(({src}) => src); +    } finally { +        window.close(); +    } +} + + +function main() { +    try { +        // Verify that sw.js scripts match background.html scripts +        const rootDir = path.join(__dirname, '..'); +        const extDirName = 'ext'; +        const extDir = path.join(rootDir, extDirName); + +        const scripts = getAllHtmlScriptPaths(path.join(extDir, 'bg', 'background.html')); +        const importedScripts = []; + +        const importScripts = (...scripts2) => { +            importedScripts.push(...scripts2); +        }; + +        const vm = new VM({importScripts}); +        vm.context.self = vm.context; +        vm.execute(['sw.js']); + +        vm.assert.deepStrictEqual(scripts, importedScripts); + +        // Verify that eslint config lists files correctly +        const expectedSwRulesFiles = scripts.filter((src) => !src.startsWith('/mixed/lib/')).map((src) => `${extDirName}${src}`); +        const eslintConfig = JSON.parse(fs.readFileSync(path.join(rootDir, '.eslintrc.json'), {encoding: 'utf8'})); +        const swRules = eslintConfig.overrides.find((item) => ( +            typeof item.env === 'object' && +            item.env !== null && +            item.env.serviceworker === true +        )); +        assert.ok(typeof swRules !== 'undefined'); +        assert.ok(Array.isArray(swRules.files)); +        assert.deepStrictEqual(swRules.files, expectedSwRulesFiles); +    } catch (e) { +        console.error(e); +        process.exit(-1); +        return; +    } +    process.exit(0); +} + + +if (require.main === module) { main(); } |