ScriptManager (#2021)

* Create ScriptManager class

* Use ScriptManager in Backend
This commit is contained in:
toasted-nutbread 2021-11-22 19:29:20 -05:00 committed by GitHub
parent b0a0184334
commit cedf6b25c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 172 additions and 144 deletions

View File

@ -186,6 +186,7 @@
"ext/js/background/environment.js", "ext/js/background/environment.js",
"ext/js/background/profile-conditions-util.js", "ext/js/background/profile-conditions-util.js",
"ext/js/background/request-builder.js", "ext/js/background/request-builder.js",
"ext/js/background/script-manager.js",
"ext/js/comm/anki.js", "ext/js/comm/anki.js",
"ext/js/comm/clipboard-monitor.js", "ext/js/comm/clipboard-monitor.js",
"ext/js/comm/clipboard-reader.js", "ext/js/comm/clipboard-reader.js",

View File

@ -27,6 +27,7 @@
<script src="/js/background/environment.js"></script> <script src="/js/background/environment.js"></script>
<script src="/js/background/profile-conditions-util.js"></script> <script src="/js/background/profile-conditions-util.js"></script>
<script src="/js/background/request-builder.js"></script> <script src="/js/background/request-builder.js"></script>
<script src="/js/background/script-manager.js"></script>
<script src="/js/comm/anki.js"></script> <script src="/js/comm/anki.js"></script>
<script src="/js/comm/clipboard-monitor.js"></script> <script src="/js/comm/clipboard-monitor.js"></script>
<script src="/js/comm/clipboard-reader.js"></script> <script src="/js/comm/clipboard-reader.js"></script>

View File

@ -31,6 +31,7 @@
* PermissionsUtil * PermissionsUtil
* ProfileConditionsUtil * ProfileConditionsUtil
* RequestBuilder * RequestBuilder
* ScriptManager
* StringUtil * StringUtil
* Translator * Translator
* wanakana * wanakana
@ -67,6 +68,7 @@ class Backend {
requestBuilder: this._requestBuilder requestBuilder: this._requestBuilder
}); });
this._optionsUtil = new OptionsUtil(); this._optionsUtil = new OptionsUtil();
this._scriptManager = new ScriptManager();
this._searchPopupTabId = null; this._searchPopupTabId = null;
this._searchPopupTabCreatePromise = null; this._searchPopupTabCreatePromise = null;
@ -559,8 +561,10 @@ class Backend {
return Promise.resolve({tabId, frameId}); return Promise.resolve({tabId, frameId});
} }
_onApiInjectStylesheet({type, value}, sender) { async _onApiInjectStylesheet({type, value}, sender) {
return this._injectStylesheet(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);
} }
async _onApiGetStylesheetContent({url}) { async _onApiGetStylesheetContent({url}) {
@ -746,7 +750,7 @@ class Backend {
_onApiDocumentStart(params, sender) { _onApiDocumentStart(params, sender) {
const {tab, frameId, url} = sender; const {tab, frameId, url} = sender;
if (typeof url !== 'string' || typeof tab !== 'object' || tab === null) { return; } if (typeof url !== 'string' || typeof tab !== 'object' || tab === null) { return; }
this._updateTabAccessibility(url, tab, frameId); this._updateTabAccessibility(url, tab.id, frameId);
} }
async _onApiGetTermFrequencies({termReadingList, dictionaries}) { async _onApiGetTermFrequencies({termReadingList, dictionaries}) {
@ -2085,145 +2089,6 @@ 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();
}
});
});
}
_injectScript(file, tabId, frameId) {
if (isObject(chrome.tabs) && typeof chrome.tabs.executeScript === 'function') {
return this._injectScriptMV2(file, tabId, frameId);
} else if (isObject(chrome.scripting) && typeof chrome.scripting.executeScript === 'function') {
return this._injectScriptMV3(file, tabId, frameId);
} else {
return Promise.reject(new Error('executeScript function not available'));
}
}
_injectScriptMV2(file, tabId, frameId) {
return new Promise((resolve, reject) => {
const details = {
allFrames: false,
frameId,
file,
matchAboutBlank: true,
runAt: 'document_start'
};
const callback = (results) => {
const e = chrome.runtime.lastError;
if (e) {
reject(new Error(e.message));
} else {
const result = results[0];
resolve({frameId, result});
}
};
chrome.tabs.executeScript(tabId, details, callback);
});
}
_injectScriptMV3(file, tabId, frameId) {
return new Promise((resolve, reject) => {
const details = {
files: [file],
target: {
allFrames: false,
frameIds: [frameId],
tabId
}
};
const callback = (results) => {
const e = chrome.runtime.lastError;
if (e) {
reject(new Error(e.message));
} else {
const {frameId: frameId2, result} = results[0];
resolve({frameId: frameId2, result});
}
};
chrome.scripting.executeScript(details, callback);
});
}
_getTabById(tabId) { _getTabById(tabId) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
chrome.tabs.get( chrome.tabs.get(
@ -2275,7 +2140,7 @@ class Backend {
} }
} }
async _updateTabAccessibility(url, tab, frameId) { async _updateTabAccessibility(url, tabId, frameId) {
let file = null; let file = null;
switch (new URL(url).hostname) { switch (new URL(url).hostname) {
@ -2291,7 +2156,7 @@ class Backend {
if (file === null) { return; } if (file === null) { return; }
return await this._injectScript(file, tab.id, frameId); await this._scriptManager.injectScript(file, tabId, frameId);
} }
async _getNormalizedDictionaryDatabaseMedia(targets) { async _getNormalizedDictionaryDatabaseMedia(targets) {

View File

@ -0,0 +1,160 @@
/*
* Copyright (C) 2021 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/>.
*/
/**
* This class is used to manage script injection into content tabs.
*/
class ScriptManager {
/**
* Injects a stylesheet into a specific tab and frame.
* @param {string} type The type of content to inject; either 'file' or 'code'.
* @param {string} content The content to inject.
* If type is 'file', this argument should be a path to a file.
* If type is 'code', this argument should be the CSS content.
* @param {number} tabId The id of the tab to inject into.
* @param {number} frameId The id of the frame to inject into.
* @returns {Promise<void>}
*/
injectStylesheet(type, content, tabId, frameId) {
if (isObject(chrome.tabs) && typeof chrome.tabs.insertCSS === 'function') {
return this._injectStylesheetMV2(type, content, tabId, frameId);
} else if (isObject(chrome.scripting) && typeof chrome.scripting.insertCSS === 'function') {
return this._injectStylesheetMV3(type, content, tabId, frameId);
} else {
return Promise.reject(new Error('Stylesheet injection not supported'));
}
}
/**
* Injects a script into a specific tab and frame.
* @param {string} file The path to a file to inject.
* @param {number} tabId The id of the tab to inject into.
* @param {number} frameId The id of the frame to inject into.
* @returns {Promise<{frameId: number, result: object}>} The id of the frame and the result of the script injection.
*/
injectScript(file, tabId, frameId) {
if (isObject(chrome.tabs) && typeof chrome.tabs.executeScript === 'function') {
return this._injectScriptMV2(file, tabId, frameId);
} else if (isObject(chrome.scripting) && typeof chrome.scripting.executeScript === 'function') {
return this._injectScriptMV3(file, tabId, frameId);
} else {
return Promise.reject(new Error('Script injection not supported'));
}
}
// Private
_injectStylesheetMV2(type, content, tabId, frameId) {
return new Promise((resolve, reject) => {
const details = (
type === 'file' ?
{
file: content,
runAt: 'document_start',
cssOrigin: 'author',
allFrames: false,
matchAboutBlank: true
} :
{
code: content,
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, content, tabId, frameId) {
return new Promise((resolve, reject) => {
const details = (
type === 'file' ?
{origin: chrome.scripting.StyleOrigin.AUTHOR, files: [content]} :
{origin: chrome.scripting.StyleOrigin.USER, css: content}
);
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();
}
});
});
}
_injectScriptMV2(file, tabId, frameId) {
return new Promise((resolve, reject) => {
const details = {
allFrames: false,
frameId,
file,
matchAboutBlank: true,
runAt: 'document_start'
};
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) {
return new Promise((resolve, reject) => {
const details = {
files: [file],
target: {
allFrames: false,
frameIds: [frameId],
tabId
}
};
chrome.scripting.executeScript(details, (results) => {
const e = chrome.runtime.lastError;
if (e) {
reject(new Error(e.message));
} else {
const {frameId: frameId2, result} = results[0];
resolve({frameId: frameId2, result});
}
});
});
}
}

View File

@ -24,6 +24,7 @@ self.importScripts(
'/js/background/environment.js', '/js/background/environment.js',
'/js/background/profile-conditions-util.js', '/js/background/profile-conditions-util.js',
'/js/background/request-builder.js', '/js/background/request-builder.js',
'/js/background/script-manager.js',
'/js/comm/anki.js', '/js/comm/anki.js',
'/js/comm/clipboard-monitor.js', '/js/comm/clipboard-monitor.js',
'/js/comm/clipboard-reader.js', '/js/comm/clipboard-reader.js',