2017-04-11 10:51:40 +00:00
|
|
|
/* global BG: true, onRuntimeMessage, applyOnMessage, handleUpdate, handleDelete */
|
2017-03-26 02:30:59 +00:00
|
|
|
'use strict';
|
|
|
|
|
2017-03-14 23:27:52 +00:00
|
|
|
// keep message channel open for sendResponse in chrome.runtime.onMessage listener
|
|
|
|
const KEEP_CHANNEL_OPEN = true;
|
2017-04-17 19:43:59 +00:00
|
|
|
|
2017-04-09 06:43:51 +00:00
|
|
|
const FIREFOX = /Firefox/.test(navigator.userAgent);
|
|
|
|
const OPERA = /OPR/.test(navigator.userAgent);
|
2017-04-17 19:43:59 +00:00
|
|
|
|
2017-04-09 06:43:51 +00:00
|
|
|
const URLS = {
|
|
|
|
ownOrigin: chrome.runtime.getURL(''),
|
2017-04-17 19:43:59 +00:00
|
|
|
|
2017-04-11 10:51:40 +00:00
|
|
|
optionsUI: [
|
2017-04-09 06:43:51 +00:00
|
|
|
chrome.runtime.getURL('options/index.html'),
|
|
|
|
'chrome://extensions/?options=' + chrome.runtime.id,
|
2017-04-11 10:51:40 +00:00
|
|
|
],
|
2017-04-17 19:43:59 +00:00
|
|
|
|
2017-04-11 10:51:40 +00:00
|
|
|
configureCommands:
|
|
|
|
OPERA ? 'opera://settings/configureCommands'
|
|
|
|
: 'chrome://extensions/configureCommands',
|
2017-04-17 19:43:59 +00:00
|
|
|
|
2017-04-12 13:56:41 +00:00
|
|
|
// CWS cannot be scripted in chromium, see ChromeExtensionsClient::IsScriptableURL
|
|
|
|
// https://cs.chromium.org/chromium/src/chrome/common/extensions/chrome_extensions_client.cc
|
2017-05-19 07:23:04 +00:00
|
|
|
chromeWebStore: FIREFOX ? 'https://addons.mozilla.org/' : (
|
|
|
|
OPERA ? 'https://addons.opera.com/' : 'https://chrome.google.com/webstore/'
|
|
|
|
),
|
2017-04-17 19:43:59 +00:00
|
|
|
|
|
|
|
supported: new RegExp(
|
|
|
|
'^(file|ftps?|http)://|' +
|
2017-05-19 07:23:04 +00:00
|
|
|
`^https://${FIREFOX ? '(?!addons\\.mozilla\\.org)' : (
|
|
|
|
OPERA ? '(?!addons\\.opera\\.com)' : '(?!chrome\\.google\\.com/webstore)'
|
|
|
|
)}|` +
|
2017-04-17 19:43:59 +00:00
|
|
|
'^' + chrome.runtime.getURL('')),
|
2017-04-09 06:43:51 +00:00
|
|
|
};
|
|
|
|
|
2017-04-11 10:51:40 +00:00
|
|
|
let BG = chrome.extension.getBackgroundPage();
|
2017-03-14 23:27:52 +00:00
|
|
|
|
2017-04-11 10:51:40 +00:00
|
|
|
if (!BG || BG != window) {
|
|
|
|
document.documentElement.classList.toggle('firefox', FIREFOX);
|
|
|
|
document.documentElement.classList.toggle('opera', OPERA);
|
2017-04-17 16:17:28 +00:00
|
|
|
// TODO: remove once our manifest's minimum_chrome_version is 50+
|
|
|
|
// Chrome 49 doesn't report own extension pages in webNavigation apparently
|
|
|
|
if (navigator.userAgent.includes('Chrome/49.')) {
|
|
|
|
getActiveTab().then(BG.updateIcon);
|
|
|
|
}
|
2017-04-11 10:51:40 +00:00
|
|
|
}
|
2017-03-21 01:32:38 +00:00
|
|
|
|
2017-04-11 10:51:40 +00:00
|
|
|
function notifyAllTabs(msg) {
|
|
|
|
const originalMessage = msg;
|
2017-04-12 17:24:05 +00:00
|
|
|
if (msg.method == 'styleUpdated' || msg.method == 'styleAdded') {
|
|
|
|
// apply/popup/manage use only meta for these two methods,
|
|
|
|
// editor may need the full code but can fetch it directly,
|
|
|
|
// so we send just the meta to avoid spamming lots of tabs with huge styles
|
2017-04-11 10:51:40 +00:00
|
|
|
msg = Object.assign({}, msg, {
|
|
|
|
style: getStyleWithNoCode(msg.style)
|
Improve style caching, cache requests too, add code:false mode
Previously, when a cache was invalidated and every tab/iframe issued a getStyles request, we previous needlessly accessed IndexedDB for each of these requests. It happened because 1) the global cachedStyles was created only at the end of the async DB-reading, 2) and each style record is retrieved asynchronously so the single threaded JS engine interleaved all these operations. It could easily span a few seconds when many tabs are open and you have like 100 styles.
Now, in getStyles: all requests issued while cachedStyles is being populated are queued and invoked at the end.
Now, in filterStyles: all requests are cached using the request's options combined in a string as a key. It also helps on each navigation because we monitor page loading process at different stages: before, when committed, history traversal, requesting applicable styles by a content script. Icon badge update also may issue a copy of the just issued request by one of the navigation listeners.
Now, the caches are invalidated smartly: style add/update/delete/toggle only purges filtering cache, and modifies style cache in-place without re-reading the entire IndexedDB.
Now, code:false mode for manage page that only needs style meta. It reduces the transferred message size 10-100 times thus reducing the overhead caused by to internal JSON-fication in the extensions API.
Also fast&direct getStylesSafe for own pages; code cosmetics
2017-03-17 22:50:35 +00:00
|
|
|
});
|
2017-03-26 02:30:59 +00:00
|
|
|
}
|
2017-04-11 10:51:40 +00:00
|
|
|
const affectsAll = !msg.affects || msg.affects.all;
|
2017-04-12 17:24:05 +00:00
|
|
|
const affectsOwnOriginOnly = !affectsAll && (msg.affects.editor || msg.affects.manager);
|
|
|
|
const affectsTabs = affectsAll || affectsOwnOriginOnly;
|
2017-04-11 10:51:40 +00:00
|
|
|
const affectsIcon = affectsAll || msg.affects.icon;
|
|
|
|
const affectsPopup = affectsAll || msg.affects.popup;
|
2017-04-16 10:20:37 +00:00
|
|
|
const affectsSelf = affectsPopup || msg.prefs;
|
2017-04-07 02:42:24 +00:00
|
|
|
if (affectsTabs || affectsIcon) {
|
2017-06-17 05:49:12 +00:00
|
|
|
const notifyTab = tab => {
|
|
|
|
// own pages will be notified via runtime.sendMessage later
|
|
|
|
if ((affectsTabs || URLS.optionsUI.includes(tab.url))
|
|
|
|
&& !(affectsSelf && tab.url.startsWith(URLS.ownOrigin))
|
|
|
|
// skip lazy-loaded aka unloaded tabs that seem to start loading on message in FF
|
|
|
|
&& (!FIREFOX || tab.width)) {
|
|
|
|
chrome.tabs.sendMessage(tab.id, msg);
|
|
|
|
}
|
|
|
|
if (affectsIcon && BG) {
|
|
|
|
BG.updateIcon(tab);
|
|
|
|
}
|
|
|
|
};
|
2017-04-11 10:51:40 +00:00
|
|
|
// list all tabs including chrome-extension:// which can be ours
|
2017-06-17 10:00:10 +00:00
|
|
|
Promise.all([
|
|
|
|
queryTabs(affectsOwnOriginOnly ? {url: URLS.ownOrigin + '*'} : {}),
|
|
|
|
getActiveTab(),
|
|
|
|
]).then(([tabs, activeTab]) => {
|
|
|
|
const activeTabId = activeTab && activeTab.id;
|
|
|
|
for (const tab of tabs) {
|
2017-06-27 12:15:49 +00:00
|
|
|
invokeOrPostpone(tab.id === activeTabId, notifyTab, tab);
|
2017-06-17 10:00:10 +00:00
|
|
|
}
|
2017-04-07 02:42:24 +00:00
|
|
|
});
|
|
|
|
}
|
2017-03-26 02:30:59 +00:00
|
|
|
// notify self: the message no longer is sent to the origin in new Chrome
|
2017-04-11 10:51:40 +00:00
|
|
|
if (typeof onRuntimeMessage != 'undefined') {
|
|
|
|
onRuntimeMessage(originalMessage);
|
2017-04-07 02:42:24 +00:00
|
|
|
}
|
2017-04-11 10:51:40 +00:00
|
|
|
// notify apply.js on own pages
|
|
|
|
if (typeof applyOnMessage != 'undefined') {
|
|
|
|
applyOnMessage(originalMessage);
|
2017-04-11 03:51:32 +00:00
|
|
|
}
|
2017-04-11 10:51:40 +00:00
|
|
|
// notify background page and all open popups
|
2017-04-16 10:20:37 +00:00
|
|
|
if (affectsSelf) {
|
2017-04-11 10:51:40 +00:00
|
|
|
chrome.runtime.sendMessage(msg);
|
2017-03-26 02:30:59 +00:00
|
|
|
}
|
2012-04-16 01:56:12 +00:00
|
|
|
}
|
2015-05-21 09:34:44 +00:00
|
|
|
|
2017-03-21 01:32:38 +00:00
|
|
|
|
2017-06-17 10:00:10 +00:00
|
|
|
function queryTabs(options = {}) {
|
|
|
|
return new Promise(resolve =>
|
|
|
|
chrome.tabs.query(options, tabs =>
|
|
|
|
resolve(tabs)));
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-04-23 10:54:20 +00:00
|
|
|
function getTab(id) {
|
|
|
|
return new Promise(resolve =>
|
|
|
|
chrome.tabs.get(id, tab =>
|
|
|
|
!chrome.runtime.lastError && resolve(tab)));
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-06-27 09:39:51 +00:00
|
|
|
function getOwnTab() {
|
|
|
|
return new Promise(resolve =>
|
|
|
|
chrome.tabs.getCurrent(tab => resolve(tab)));
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-03-21 01:32:38 +00:00
|
|
|
function getActiveTab() {
|
2017-06-17 10:00:10 +00:00
|
|
|
return queryTabs({currentWindow: true, active: true})
|
|
|
|
.then(tabs => tabs[0]);
|
2015-05-21 09:34:44 +00:00
|
|
|
}
|
|
|
|
|
2017-03-21 01:32:38 +00:00
|
|
|
|
|
|
|
function getActiveTabRealURL() {
|
2017-03-26 02:30:59 +00:00
|
|
|
return getActiveTab()
|
|
|
|
.then(getTabRealURL);
|
2017-03-21 01:32:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getTabRealURL(tab) {
|
2017-03-26 02:30:59 +00:00
|
|
|
return new Promise(resolve => {
|
|
|
|
if (tab.url != 'chrome://newtab/') {
|
|
|
|
resolve(tab.url);
|
|
|
|
} else {
|
|
|
|
chrome.webNavigation.getFrame({tabId: tab.id, frameId: 0, processId: -1}, frame => {
|
2017-03-25 21:14:41 +00:00
|
|
|
resolve(frame && frame.url || '');
|
2017-03-26 02:30:59 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
2015-05-21 09:34:44 +00:00
|
|
|
}
|
|
|
|
|
2017-03-21 01:32:38 +00:00
|
|
|
|
2017-03-25 06:17:14 +00:00
|
|
|
// opens a tab or activates the already opened one,
|
|
|
|
// reuses the New Tab page if it's focused now
|
|
|
|
function openURL({url, currentWindow = true}) {
|
2017-03-26 02:30:59 +00:00
|
|
|
if (!url.includes('://')) {
|
|
|
|
url = chrome.runtime.getURL(url);
|
|
|
|
}
|
|
|
|
return new Promise(resolve => {
|
2017-03-29 10:00:19 +00:00
|
|
|
// [some] chromium forks don't handle their fake branded protocols
|
|
|
|
url = url.replace(/^(opera|vivaldi)/, 'chrome');
|
2017-06-06 07:33:45 +00:00
|
|
|
// FF doesn't handle moz-extension:// URLs (bug)
|
2017-03-27 02:35:10 +00:00
|
|
|
// API doesn't handle the hash-fragment part
|
2017-06-06 07:33:45 +00:00
|
|
|
const urlQuery = url.startsWith('moz-extension') ? undefined : url.replace(/#.*/, '');
|
2017-06-17 10:00:10 +00:00
|
|
|
queryTabs({url: urlQuery, currentWindow}).then(tabs => {
|
2017-03-27 02:35:10 +00:00
|
|
|
for (const tab of tabs) {
|
|
|
|
if (tab.url == url) {
|
|
|
|
activateTab(tab).then(resolve);
|
|
|
|
return;
|
|
|
|
}
|
2017-03-26 02:30:59 +00:00
|
|
|
}
|
2017-04-09 06:43:51 +00:00
|
|
|
getActiveTab().then(tab => {
|
2017-06-03 13:52:58 +00:00
|
|
|
if (tab && tab.url == 'chrome://newtab/'
|
2017-06-03 13:57:52 +00:00
|
|
|
// prevent redirecting incognito NTP to a chrome URL as it crashes Chrome
|
|
|
|
&& (!url.startsWith('chrome') || !tab.incognito)) {
|
2017-04-09 06:43:51 +00:00
|
|
|
chrome.tabs.update({url}, resolve);
|
|
|
|
} else {
|
|
|
|
chrome.tabs.create(tab && !FIREFOX ? {url, openerTabId: tab.id} : {url}, resolve);
|
|
|
|
}
|
|
|
|
});
|
2017-03-26 02:30:59 +00:00
|
|
|
});
|
|
|
|
});
|
2017-03-21 01:32:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-03-25 06:17:14 +00:00
|
|
|
function activateTab(tab) {
|
2017-03-26 02:30:59 +00:00
|
|
|
return Promise.all([
|
|
|
|
new Promise(resolve => {
|
|
|
|
chrome.tabs.update(tab.id, {active: true}, resolve);
|
|
|
|
}),
|
|
|
|
new Promise(resolve => {
|
|
|
|
chrome.windows.update(tab.windowId, {focused: true}, resolve);
|
|
|
|
}),
|
|
|
|
]);
|
2017-03-25 06:17:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-03-15 12:41:39 +00:00
|
|
|
function stringAsRegExp(s, flags) {
|
2017-03-26 02:30:59 +00:00
|
|
|
return new RegExp(s.replace(/[{}()[\]/\\.+?^$:=*!|]/g, '\\$&'), flags);
|
2017-03-15 12:41:39 +00:00
|
|
|
}
|
|
|
|
|
2017-03-21 01:32:38 +00:00
|
|
|
|
2017-04-11 10:51:40 +00:00
|
|
|
function ignoreChromeError() {
|
|
|
|
chrome.runtime.lastError; // eslint-disable-line no-unused-expressions
|
2017-03-15 12:41:39 +00:00
|
|
|
}
|
2017-03-16 13:36:33 +00:00
|
|
|
|
2017-03-21 01:32:38 +00:00
|
|
|
|
2017-04-11 10:51:40 +00:00
|
|
|
function getStyleWithNoCode(style) {
|
|
|
|
const stripped = Object.assign({}, style, {sections: []});
|
|
|
|
for (const section of style.sections) {
|
|
|
|
stripped.sections.push(Object.assign({}, section, {code: null}));
|
|
|
|
}
|
|
|
|
return stripped;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// js engine can't optimize the entire function if it contains try-catch
|
|
|
|
// so we should keep it isolated from normal code in a minimal wrapper
|
|
|
|
// Update: might get fixed in V8 TurboFan in the future
|
|
|
|
function tryCatch(func, ...args) {
|
|
|
|
try {
|
|
|
|
return func(...args);
|
|
|
|
} catch (e) {}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function tryRegExp(regexp) {
|
|
|
|
try {
|
|
|
|
return new RegExp(regexp);
|
|
|
|
} catch (e) {}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function tryJSONparse(jsonString) {
|
|
|
|
try {
|
|
|
|
return JSON.parse(jsonString);
|
|
|
|
} catch (e) {}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-04-22 18:02:49 +00:00
|
|
|
const debounce = Object.assign((fn, delay, ...args) => {
|
|
|
|
clearTimeout(debounce.timers.get(fn));
|
|
|
|
debounce.timers.set(fn, setTimeout(debounce.run, delay, fn, ...args));
|
|
|
|
}, {
|
|
|
|
timers: new Map(),
|
|
|
|
run(fn, ...args) {
|
|
|
|
debounce.timers.delete(fn);
|
2017-04-11 10:51:40 +00:00
|
|
|
fn(...args);
|
2017-04-22 18:02:49 +00:00
|
|
|
},
|
|
|
|
unregister(fn) {
|
|
|
|
clearTimeout(debounce.timers.get(fn));
|
|
|
|
debounce.timers.delete(fn);
|
|
|
|
},
|
|
|
|
});
|
2017-04-11 10:51:40 +00:00
|
|
|
|
|
|
|
|
|
|
|
function deepCopy(obj) {
|
2017-04-11 13:13:56 +00:00
|
|
|
return obj !== null && obj !== undefined && typeof obj == 'object'
|
|
|
|
? deepMerge(typeof obj.slice == 'function' ? [] : {}, obj)
|
|
|
|
: obj;
|
2017-04-11 10:51:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function deepMerge(target, ...args) {
|
2017-04-11 13:13:56 +00:00
|
|
|
const isArray = typeof target.slice == 'function';
|
2017-04-11 10:51:40 +00:00
|
|
|
for (const obj of args) {
|
2017-04-11 13:13:56 +00:00
|
|
|
if (isArray && obj !== null && obj !== undefined) {
|
|
|
|
for (const element of obj) {
|
|
|
|
target.push(deepCopy(element));
|
|
|
|
}
|
|
|
|
continue;
|
|
|
|
}
|
2017-04-11 10:51:40 +00:00
|
|
|
for (const k in obj) {
|
|
|
|
const value = obj[k];
|
2017-04-11 13:13:56 +00:00
|
|
|
if (k in target && typeof value == 'object' && value !== null) {
|
2017-04-11 10:51:40 +00:00
|
|
|
deepMerge(target[k], value);
|
|
|
|
} else {
|
|
|
|
target[k] = deepCopy(value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return target;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function sessionStorageHash(name) {
|
|
|
|
return {
|
|
|
|
name,
|
|
|
|
value: tryCatch(JSON.parse, sessionStorage[name]) || {},
|
|
|
|
set(k, v) {
|
|
|
|
this.value[k] = v;
|
|
|
|
this.updateStorage();
|
|
|
|
},
|
|
|
|
unset(k) {
|
|
|
|
delete this.value[k];
|
|
|
|
this.updateStorage();
|
|
|
|
},
|
|
|
|
updateStorage() {
|
|
|
|
sessionStorage[this.name] = JSON.stringify(this.value);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-05-23 19:31:40 +00:00
|
|
|
function onBackgroundReady() {
|
2017-06-18 12:20:11 +00:00
|
|
|
return BG && BG.getStyles ? Promise.resolve() : new Promise(function ping(resolve) {
|
2017-04-11 10:51:40 +00:00
|
|
|
chrome.runtime.sendMessage({method: 'healthCheck'}, health => {
|
|
|
|
if (health !== undefined) {
|
|
|
|
BG = chrome.extension.getBackgroundPage();
|
2017-05-23 19:31:40 +00:00
|
|
|
resolve();
|
2017-04-11 10:51:40 +00:00
|
|
|
} else {
|
2017-06-18 12:20:11 +00:00
|
|
|
setTimeout(ping, 0, resolve);
|
2017-04-11 10:51:40 +00:00
|
|
|
}
|
|
|
|
});
|
2017-06-18 12:20:11 +00:00
|
|
|
});
|
2017-04-11 10:51:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// in case Chrome haven't yet loaded the bg page and displays our page like edit/manage
|
|
|
|
function getStylesSafe(options) {
|
2017-05-23 19:31:40 +00:00
|
|
|
return onBackgroundReady()
|
|
|
|
.then(() => BG.getStyles(options));
|
2017-04-11 10:51:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function saveStyleSafe(style) {
|
2017-05-23 19:31:40 +00:00
|
|
|
return onBackgroundReady()
|
|
|
|
.then(() => BG.saveStyle(BG.deepCopy(style)))
|
2017-04-11 10:51:40 +00:00
|
|
|
.then(savedStyle => {
|
|
|
|
if (style.notify === false) {
|
|
|
|
handleUpdate(savedStyle, style);
|
|
|
|
}
|
|
|
|
return savedStyle;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function deleteStyleSafe({id, notify = true} = {}) {
|
2017-05-23 19:31:40 +00:00
|
|
|
return onBackgroundReady()
|
|
|
|
.then(() => BG.deleteStyle({id, notify}))
|
2017-04-11 10:51:40 +00:00
|
|
|
.then(() => {
|
|
|
|
if (!notify) {
|
|
|
|
handleDelete(id);
|
|
|
|
}
|
|
|
|
return id;
|
|
|
|
});
|
2017-03-29 00:57:21 +00:00
|
|
|
}
|
2017-04-20 01:46:04 +00:00
|
|
|
|
|
|
|
|
|
|
|
function download(url) {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const xhr = new XMLHttpRequest();
|
|
|
|
xhr.timeout = 10e3;
|
|
|
|
xhr.onloadend = () => (xhr.status == 200
|
|
|
|
? resolve(xhr.responseText)
|
|
|
|
: reject(xhr.status));
|
|
|
|
const [mainUrl, query] = url.split('?');
|
|
|
|
xhr.open(query ? 'POST' : 'GET', mainUrl, true);
|
|
|
|
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
|
|
|
|
xhr.send(query);
|
|
|
|
});
|
|
|
|
}
|
2017-06-27 09:55:47 +00:00
|
|
|
|
|
|
|
|
|
|
|
function doTimeout(ms = 0, ...args) {
|
|
|
|
return ms > 0
|
|
|
|
? () => new Promise(resolve => setTimeout(resolve, ms, ...args))
|
|
|
|
: new Promise(resolve => setTimeout(resolve, 0, ...args));
|
|
|
|
}
|
2017-06-27 12:15:49 +00:00
|
|
|
|
|
|
|
|
|
|
|
function invokeOrPostpone(isInvoke, fn, ...args) {
|
|
|
|
return isInvoke
|
|
|
|
? fn(...args)
|
|
|
|
: setTimeout(invokeOrPostpone, 0, true, fn, ...args);
|
|
|
|
}
|