diff options
| -rw-r--r-- | .eslintrc.json | 2 | ||||
| -rw-r--r-- | ext/js/background/backend.js | 204 | ||||
| -rw-r--r-- | ext/js/background/offscreen-proxy.js | 172 | 
3 files changed, 223 insertions, 155 deletions
| diff --git a/.eslintrc.json b/.eslintrc.json index e37ef133..f1a79770 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -420,6 +420,8 @@                  "ext/js/yomitan.js",                  "ext/js/accessibility/accessibility-controller.js",                  "ext/js/background/backend.js", +                "ext/js/background/offscreen.js", +                "ext/js/background/offscreen-proxy.js",                  "ext/js/background/profile-conditions-util.js",                  "ext/js/background/request-builder.js",                  "ext/js/background/script-manager.js", diff --git a/ext/js/background/backend.js b/ext/js/background/backend.js index c29e5ee6..bf4841f8 100644 --- a/ext/js/background/backend.js +++ b/ext/js/background/backend.js @@ -35,6 +35,7 @@ import {Translator} from '../language/translator.js';  import {AudioDownloader} from '../media/audio-downloader.js';  import {MediaUtil} from '../media/media-util.js';  import {yomitan} from '../yomitan.js'; +import {OffscreenProxy, DictionaryDatabaseProxy, TranslatorProxy, ClipboardReaderProxy} from './offscreen-proxy.js';  import {ProfileConditionsUtil} from './profile-conditions-util.js';  import {RequestBuilder} from './request-builder.js';  import {ScriptManager} from './script-manager.js'; @@ -66,23 +67,10 @@ export class Backend {                  richContentPasteTargetSelector: '#clipboard-rich-content-paste-target'              });          } else { -            this._dictionaryDatabase = { -                prepare: () => this._sendMessagePromise({action: 'databasePrepareOffscreen'}), -                getDictionaryInfo: () => this._sendMessagePromise({action: 'getDictionaryInfoOffscreen'}), -                purge: () => this._sendMessagePromise({action: 'databasePurgeOffscreen'}), -                getMedia: this._getMediaOffscreen.bind(this) -            }; -            this._translator = { -                prepare: (deinflectionReasons) => this._sendMessagePromise({action: 'translatorPrepareOffscreen', params: {deinflectionReasons}}), -                findKanji: this._findKanjiOffscreen.bind(this), -                findTerms: this._findTermsOffscreen.bind(this), -                getTermFrequencies: this._getTermFrequenciesOffscreen.bind(this), -                clearDatabaseCaches: () => this._sendMessagePromise({action: 'clearDatabaseCachesOffscreen'}) -            }; -            this._clipboardReader = { -                getText: this._getTextOffscreen.bind(this), -                getImage: this._getImageOffscreen.bind(this) -            }; +            this._offscreen = new OffscreenProxy(); +            this._dictionaryDatabase = new DictionaryDatabaseProxy(this._offscreen); +            this._translator = new TranslatorProxy(this._offscreen); +            this._clipboardReader = new ClipboardReaderProxy(this._offscreen);          }          this._clipboardMonitor = new ClipboardMonitor({ @@ -119,8 +107,6 @@ export 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)}], @@ -243,7 +229,7 @@ export class Backend {              await this._requestBuilder.prepare();              await this._environment.prepare();              if (chrome.offscreen) { -                await this._setupOffscreenDocument(); +                await this._offscreen.prepare();              }              this._clipboardReader.browser = this._environment.getInfo().browser; @@ -767,6 +753,49 @@ export class Backend {          }      } +    _onApiOpenCrossFramePort({targetTabId, targetFrameId}, sender) { +        const sourceTabId = (sender && sender.tab ? sender.tab.id : null); +        if (typeof sourceTabId !== 'number') { +            throw new Error('Port does not have an associated tab ID'); +        } +        const sourceFrameId = sender.frameId; +        if (typeof sourceFrameId !== 'number') { +            throw new Error('Port does not have an associated frame ID'); +        } + +        const sourceDetails = { +            name: 'cross-frame-communication-port', +            otherTabId: targetTabId, +            otherFrameId: targetFrameId +        }; +        const targetDetails = { +            name: 'cross-frame-communication-port', +            otherTabId: sourceTabId, +            otherFrameId: sourceFrameId +        }; +        let sourcePort = chrome.tabs.connect(sourceTabId, {frameId: sourceFrameId, name: JSON.stringify(sourceDetails)}); +        let targetPort = chrome.tabs.connect(targetTabId, {frameId: targetFrameId, name: JSON.stringify(targetDetails)}); + +        const cleanup = () => { +            this._checkLastError(chrome.runtime.lastError); +            if (targetPort !== null) { +                targetPort.disconnect(); +                targetPort = null; +            } +            if (sourcePort !== null) { +                sourcePort.disconnect(); +                sourcePort = null; +            } +        }; + +        sourcePort.onMessage.addListener((message) => { targetPort.postMessage(message); }); +        targetPort.onMessage.addListener((message) => { sourcePort.postMessage(message); }); +        sourcePort.onDisconnect.addListener(cleanup); +        targetPort.onDisconnect.addListener(cleanup); + +        return {targetTabId, targetFrameId}; +    } +      // Command handlers      async _onCommandOpenSearchPage(params) { @@ -1637,20 +1666,6 @@ export 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); @@ -2252,125 +2267,4 @@ export class Backend {          }          return results;      } - -    async _getMediaOffscreen(targets) { -        const serializedMedia = await this._sendMessagePromise({action: 'databaseGetMediaOffscreen', params: {targets}}); -        const media = serializedMedia.map((m) => ({...m, content: ArrayBufferUtil.base64ToArrayBuffer(m.content)})); -        return media; -    } - -    async _findKanjiOffscreen(text, findKanjiOptions) { -        const enabledDictionaryMapList = [...findKanjiOptions.enabledDictionaryMap]; -        const modifiedKanjiOptions = { -            ...findKanjiOptions, -            enabledDictionaryMap: enabledDictionaryMapList -        }; -        return this._sendMessagePromise({action: 'findKanjiOffscreen', params: {text, findKanjiOptions: modifiedKanjiOptions}}); -    } - -    async _findTermsOffscreen(mode, text, findTermsOptions) { -        const {enabledDictionaryMap, excludeDictionaryDefinitions, textReplacements} = findTermsOptions; -        const enabledDictionaryMapList = [...enabledDictionaryMap]; -        const excludeDictionaryDefinitionsList = excludeDictionaryDefinitions ? [...excludeDictionaryDefinitions] : null; -        const textReplacementsSerialized = textReplacements.map((group) => { -            if (!group) { -                return group; -            } -            return group.map((opt) => ({...opt, pattern: opt.pattern.toString()})); -        }); -        const modifiedFindTermsOptions = { -            ...findTermsOptions, -            enabledDictionaryMap: enabledDictionaryMapList, -            excludeDictionaryDefinitions: excludeDictionaryDefinitionsList, -            textReplacementsOptions: textReplacementsSerialized -        }; -        return this._sendMessagePromise({action: 'findTermsOffscreen', params: {mode, text, findTermsOptions: modifiedFindTermsOptions}}); -    } - -    async _getTermFrequenciesOffscreen(termReadingList, dictionaries) { -        return this._sendMessagePromise({action: 'getTermFrequenciesOffscreen', params: {termReadingList, dictionaries}}); -    } - -    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') { -            throw new Error('Port does not have an associated tab ID'); -        } -        const sourceFrameId = sender.frameId; -        if (typeof sourceFrameId !== 'number') { -            throw new Error('Port does not have an associated frame ID'); -        } - -        const sourceDetails = { -            name: 'cross-frame-communication-port', -            otherTabId: targetTabId, -            otherFrameId: targetFrameId -        }; -        const targetDetails = { -            name: 'cross-frame-communication-port', -            otherTabId: sourceTabId, -            otherFrameId: sourceFrameId -        }; -        let sourcePort = chrome.tabs.connect(sourceTabId, {frameId: sourceFrameId, name: JSON.stringify(sourceDetails)}); -        let targetPort = chrome.tabs.connect(targetTabId, {frameId: targetFrameId, name: JSON.stringify(targetDetails)}); - -        const cleanup = () => { -            this._checkLastError(chrome.runtime.lastError); -            if (targetPort !== null) { -                targetPort.disconnect(); -                targetPort = null; -            } -            if (sourcePort !== null) { -                sourcePort.disconnect(); -                sourcePort = null; -            } -        }; - -        sourcePort.onMessage.addListener((message) => { targetPort.postMessage(message); }); -        targetPort.onMessage.addListener((message) => { sourcePort.postMessage(message); }); -        sourcePort.onDisconnect.addListener(cleanup); -        targetPort.onDisconnect.addListener(cleanup); - -        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-proxy.js b/ext/js/background/offscreen-proxy.js new file mode 100644 index 00000000..ae414b99 --- /dev/null +++ b/ext/js/background/offscreen-proxy.js @@ -0,0 +1,172 @@ +/* + * 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/>. + */ + +import {deserializeError, isObject} from '../core.js'; +import {ArrayBufferUtil} from '../data/sandbox/array-buffer-util.js'; + +export class OffscreenProxy { +    constructor() { +        this._creatingOffscreen = null; +    } + +    // https://developer.chrome.com/docs/extensions/reference/offscreen/ +    async prepare() { +        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; +    } + +    sendMessagePromise(...args) { +        return new Promise((resolve, reject) => { +            const callback = (response) => { +                try { +                    resolve(this._getMessageResponseResult(response)); +                } catch (error) { +                    reject(error); +                } +            }; + +            chrome.runtime.sendMessage(...args, callback); +        }); +    } + +    _getMessageResponseResult(response) { +        let error = chrome.runtime.lastError; +        if (error) { +            throw new Error(error.message); +        } +        if (!isObject(response)) { +            throw new Error('Offscreen document did not respond'); +        } +        error = response.error; +        if (error) { +            throw deserializeError(error); +        } +        return response.result; +    } +} + +export class DictionaryDatabaseProxy { +    constructor(offscreen) { +        this._offscreen = offscreen; +    } + +    prepare() { +        return this._offscreen.sendMessagePromise({action: 'databasePrepareOffscreen'}); +    } + +    getDictionaryInfo() { +        return this._offscreen.sendMessagePromise({action: 'getDictionaryInfoOffscreen'}); +    } + +    purge() { +        return this._offscreen.sendMessagePromise({action: 'databasePurgeOffscreen'}); +    } + +    async getMedia(targets) { +        const serializedMedia = await this._offscreen.sendMessagePromise({action: 'databaseGetMediaOffscreen', params: {targets}}); +        const media = serializedMedia.map((m) => ({...m, content: ArrayBufferUtil.base64ToArrayBuffer(m.content)})); +        return media; +    } +} + +export class TranslatorProxy { +    constructor(offscreen) { +        this._offscreen = offscreen; +    } + +    prepare(deinflectionReasons) { +        return this._offscreen.sendMessagePromise({action: 'translatorPrepareOffscreen', params: {deinflectionReasons}}); +    } + +    async findKanji(text, findKanjiOptions) { +        const enabledDictionaryMapList = [...findKanjiOptions.enabledDictionaryMap]; +        const modifiedKanjiOptions = { +            ...findKanjiOptions, +            enabledDictionaryMap: enabledDictionaryMapList +        }; +        return this._offscreen.sendMessagePromise({action: 'findKanjiOffscreen', params: {text, findKanjiOptions: modifiedKanjiOptions}}); +    } + +    async findTerms(mode, text, findTermsOptions) { +        const {enabledDictionaryMap, excludeDictionaryDefinitions, textReplacements} = findTermsOptions; +        const enabledDictionaryMapList = [...enabledDictionaryMap]; +        const excludeDictionaryDefinitionsList = excludeDictionaryDefinitions ? [...excludeDictionaryDefinitions] : null; +        const textReplacementsSerialized = textReplacements.map((group) => { +            if (!group) { +                return group; +            } +            return group.map((opt) => ({...opt, pattern: opt.pattern.toString()})); +        }); +        const modifiedFindTermsOptions = { +            ...findTermsOptions, +            enabledDictionaryMap: enabledDictionaryMapList, +            excludeDictionaryDefinitions: excludeDictionaryDefinitionsList, +            textReplacementsOptions: textReplacementsSerialized +        }; +        return this._offscreen.sendMessagePromise({action: 'findTermsOffscreen', params: {mode, text, findTermsOptions: modifiedFindTermsOptions}}); +    } + +    async getTermFrequencies(termReadingList, dictionaries) { +        return this._offscreen.sendMessagePromise({action: 'getTermFrequenciesOffscreen', params: {termReadingList, dictionaries}}); +    } + +    clearDatabaseCaches() { +        return this._offscreen.sendMessagePromise({action: 'clearDatabaseCachesOffscreen'}); +    } +} + +export class ClipboardReaderProxy { +    constructor(offscreen) { +        this._offscreen = offscreen; +    } + +    async getText(useRichText) { +        return this._offscreen.sendMessagePromise({action: 'clipboardGetTextOffscreen', params: {useRichText}}); +    } + +    async getImage() { +        return this._offscreen.sendMessagePromise({action: 'clipboardGetImageOffscreen'}); +    } +} |