| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496 |
- /* global BG: true, onRuntimeMessage, applyOnMessage, handleUpdate, handleDelete */
- /* global FIREFOX: true */
- 'use strict';
- // keep message channel open for sendResponse in chrome.runtime.onMessage listener
- const KEEP_CHANNEL_OPEN = true;
- const CHROME = Boolean(chrome.app) && parseInt(navigator.userAgent.match(/Chrom\w+\/(?:\d+\.){2}(\d+)|$/)[1]);
- const OPERA = CHROME && parseFloat(navigator.userAgent.match(/\bOPR\/(\d+\.\d+)|$/)[1]);
- const ANDROID = !chrome.windows;
- let FIREFOX = !CHROME && parseFloat(navigator.userAgent.match(/\bFirefox\/(\d+\.\d+)|$/)[1]);
- if (!CHROME && !chrome.browserAction.openPopup) {
- // in FF pre-57 legacy addons can override useragent so we assume the worst
- // until we know for sure in the async getBrowserInfo()
- // (browserAction.openPopup was added in 57)
- FIREFOX = 50;
- browser.runtime.getBrowserInfo().then(info => {
- FIREFOX = parseFloat(info.version);
- document.documentElement.classList.add('moz-appearance-bug', FIREFOX && FIREFOX < 54);
- });
- }
- const URLS = {
- ownOrigin: chrome.runtime.getURL(''),
- optionsUI: [
- chrome.runtime.getURL('options.html'),
- 'chrome://extensions/?options=' + chrome.runtime.id,
- ],
- configureCommands:
- OPERA ? 'opera://settings/configureCommands'
- : 'chrome://extensions/configureCommands',
- // CWS cannot be scripted in chromium, see ChromeExtensionsClient::IsScriptableURL
- // https://cs.chromium.org/chromium/src/chrome/common/extensions/chrome_extensions_client.cc
- browserWebStore:
- FIREFOX ? 'https://addons.mozilla.org/' :
- OPERA ? 'https://addons.opera.com/' :
- 'https://chrome.google.com/webstore/',
- // Chrome 61.0.3161+ doesn't run content scripts on NTP https://crrev.com/2978953002/
- // TODO: remove when "minimum_chrome_version": "61" or higher
- chromeProtectsNTP: CHROME >= 3161,
- supported: url => (
- url.startsWith('http') && !url.startsWith(URLS.browserWebStore) ||
- url.startsWith('ftp') ||
- url.startsWith('file') ||
- url.startsWith(URLS.ownOrigin) ||
- !URLS.chromeProtectsNTP && url.startsWith('chrome://newtab/')
- ),
- };
- let BG = chrome.extension.getBackgroundPage();
- if (BG && !BG.getStyles && BG !== window) {
- // own page like editor/manage is being loaded on browser startup
- // before the background page has been fully initialized;
- // it'll be resolved in onBackgroundReady() instead
- BG = null;
- }
- if (!BG || BG !== window) {
- if (FIREFOX) {
- document.documentElement.classList.add('firefox');
- } else if (OPERA) {
- document.documentElement.classList.add('opera');
- } else if (chrome.app && navigator.userAgent.includes('Vivaldi')) {
- document.documentElement.classList.add('vivaldi');
- }
- // TODO: remove once our manifest's minimum_chrome_version is 50+
- // Chrome 49 doesn't report own extension pages in webNavigation apparently
- if (CHROME && CHROME < 2661) {
- getActiveTab().then(BG.updateIcon);
- }
- }
- const FIREFOX_NO_DOM_STORAGE = FIREFOX && !tryCatch(() => localStorage);
- if (FIREFOX_NO_DOM_STORAGE) {
- // may be disabled via dom.storage.enabled
- Object.defineProperty(window, 'localStorage', {value: {}});
- Object.defineProperty(window, 'sessionStorage', {value: {}});
- }
- function notifyAllTabs(msg) {
- const originalMessage = msg;
- 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
- msg = Object.assign({}, msg, {
- style: getStyleWithNoCode(msg.style)
- });
- }
- const affectsAll = !msg.affects || msg.affects.all;
- const affectsOwnOriginOnly = !affectsAll && (msg.affects.editor || msg.affects.manager);
- const affectsTabs = affectsAll || affectsOwnOriginOnly;
- const affectsIcon = affectsAll || msg.affects.icon;
- const affectsPopup = affectsAll || msg.affects.popup;
- const affectsSelf = affectsPopup || msg.prefs;
- if (affectsTabs || affectsIcon) {
- 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)) {
- msg.tabId = tab.id;
- sendMessage(msg, ignoreChromeError);
- }
- if (affectsIcon && BG) {
- BG.updateIcon(tab);
- }
- };
- // list all tabs including chrome-extension:// which can be ours
- Promise.all([
- queryTabs(affectsOwnOriginOnly ? {url: URLS.ownOrigin + '*'} : {}),
- getActiveTab(),
- ]).then(([tabs, activeTab]) => {
- const activeTabId = activeTab && activeTab.id;
- for (const tab of tabs) {
- invokeOrPostpone(tab.id === activeTabId, notifyTab, tab);
- }
- });
- }
- // notify self: the message no longer is sent to the origin in new Chrome
- if (typeof onRuntimeMessage !== 'undefined') {
- onRuntimeMessage(originalMessage);
- }
- // notify apply.js on own pages
- if (typeof applyOnMessage !== 'undefined') {
- applyOnMessage(originalMessage);
- }
- // notify background page and all open popups
- if (affectsSelf) {
- msg.tabId = undefined;
- sendMessage(msg, ignoreChromeError);
- }
- }
- function sendMessage(msg, callback) {
- /*
- Promise mode [default]:
- - rejects on receiving {__ERROR__: message} created by background.js::onRuntimeMessage
- - automatically suppresses chrome.runtime.lastError because it's autogenerated
- by browserAction.setText which lacks a callback param in chrome API
- Standard callback mode:
- - enabled by passing a second param
- */
- const {tabId, frameId} = msg;
- if (tabId >= 0 && FIREFOX) {
- // FF: reroute all tabs messages to styleViaAPI
- const msgForBG = BG === window ? msg : BG.deepCopy(msg);
- const sender = {tab: {id: tabId}, frameId};
- const task = BG.styleViaAPI.process(msgForBG, sender);
- return callback ? task.then(callback) : task;
- }
- const fn = tabId >= 0 ? chrome.tabs.sendMessage : chrome.runtime.sendMessage;
- const args = tabId >= 0 ? [tabId, msg, {frameId}] : [msg];
- if (callback) {
- fn(...args, callback);
- } else {
- return new Promise((resolve, reject) => {
- fn(...args, r => {
- const err = r && r.__ERROR__;
- (err ? reject : resolve)(err || r);
- chrome.runtime.lastError; // eslint-disable-line no-unused-expressions
- });
- });
- }
- }
- function queryTabs(options = {}) {
- return new Promise(resolve =>
- chrome.tabs.query(options, tabs =>
- resolve(tabs)));
- }
- function getTab(id) {
- return new Promise(resolve =>
- chrome.tabs.get(id, tab =>
- !chrome.runtime.lastError && resolve(tab)));
- }
- function getOwnTab() {
- return new Promise(resolve =>
- chrome.tabs.getCurrent(tab => resolve(tab)));
- }
- function getActiveTab() {
- return queryTabs({currentWindow: true, active: true})
- .then(tabs => tabs[0]);
- }
- function getActiveTabRealURL() {
- return getActiveTab()
- .then(getTabRealURL);
- }
- function getTabRealURL(tab) {
- return new Promise(resolve => {
- if (tab.url !== 'chrome://newtab/' || URLS.chromeProtectsNTP) {
- resolve(tab.url);
- } else {
- chrome.webNavigation.getFrame({tabId: tab.id, frameId: 0, processId: -1}, frame => {
- resolve(frame && frame.url || '');
- });
- }
- });
- }
- // opens a tab or activates the already opened one,
- // reuses the New Tab page if it's focused now
- function openURL({url, index, openerTabId, currentWindow = true}) {
- if (!url.includes('://')) {
- url = chrome.runtime.getURL(url);
- }
- return new Promise(resolve => {
- // [some] chromium forks don't handle their fake branded protocols
- url = url.replace(/^(opera|vivaldi)/, 'chrome');
- // FF doesn't handle moz-extension:// URLs (bug)
- // API doesn't handle the hash-fragment part
- const urlQuery = url.startsWith('moz-extension') ? undefined : url.replace(/#.*/, '');
- queryTabs({url: urlQuery, currentWindow}).then(tabs => {
- for (const tab of tabs) {
- if (tab.url === url) {
- activateTab(tab).then(resolve);
- return;
- }
- }
- getActiveTab().then(tab => {
- const chromeInIncognito = tab && tab.incognito && url.startsWith('chrome');
- if (tab && (tab.url === 'chrome://newtab/' || tab.url === 'about:newtab') && !chromeInIncognito) {
- // update current NTP, except for chrome:// or chrome-extension:// in incognito
- chrome.tabs.update({url}, resolve);
- } else {
- // create a new tab
- const options = {url, index};
- // FF57+ supports openerTabId, but not in Android (indicated by the absence of chrome.windows)
- if (tab && (!FIREFOX || FIREFOX >= 57 && chrome.windows) && !chromeInIncognito) {
- options.openerTabId = tab.id;
- }
- chrome.tabs.create(options, resolve);
- }
- });
- });
- });
- }
- function activateTab(tab) {
- return Promise.all([
- new Promise(resolve => {
- chrome.tabs.update(tab.id, {active: true}, resolve);
- }),
- chrome.windows && new Promise(resolve => {
- chrome.windows.update(tab.windowId, {focused: true}, resolve);
- }),
- ]);
- }
- function stringAsRegExp(s, flags) {
- return new RegExp(s.replace(/[{}()[\]\\.+*?^$|]/g, '\\$&'), flags);
- }
- function ignoreChromeError() {
- chrome.runtime.lastError; // eslint-disable-line no-unused-expressions
- }
- 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, flags) {
- try {
- return new RegExp(regexp, flags);
- } catch (e) {}
- }
- function tryJSONparse(jsonString) {
- try {
- return JSON.parse(jsonString);
- } catch (e) {}
- }
- 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);
- fn(...args);
- },
- unregister(fn) {
- clearTimeout(debounce.timers.get(fn));
- debounce.timers.delete(fn);
- },
- });
- function deepCopy(obj) {
- return obj !== null && obj !== undefined && typeof obj === 'object'
- ? deepMerge(typeof obj.slice === 'function' ? [] : {}, obj)
- : obj;
- }
- function deepMerge(target, ...args) {
- const isArray = typeof target.slice === 'function';
- for (const obj of args) {
- if (isArray && obj !== null && obj !== undefined) {
- for (const element of obj) {
- target.push(deepCopy(element));
- }
- continue;
- }
- for (const k in obj) {
- const value = obj[k];
- if (k in target && typeof value === 'object' && value !== null) {
- 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);
- }
- };
- }
- function onBackgroundReady() {
- return BG && BG.getStyles ? Promise.resolve() : new Promise(function ping(resolve) {
- sendMessage({method: 'healthCheck'}, health => {
- if (health !== undefined) {
- BG = chrome.extension.getBackgroundPage();
- resolve();
- } else {
- setTimeout(ping, 0, resolve);
- }
- });
- });
- }
- // in case Chrome haven't yet loaded the bg page and displays our page like edit/manage
- function getStylesSafe(options) {
- return onBackgroundReady()
- .then(() => BG.getStyles(options));
- }
- function saveStyleSafe(style) {
- return onBackgroundReady()
- .then(() => BG.saveStyle(BG.deepCopy(style)))
- .then(savedStyle => {
- if (style.notify === false) {
- handleUpdate(savedStyle, style);
- }
- return savedStyle;
- });
- }
- function deleteStyleSafe({id, notify = true} = {}) {
- return onBackgroundReady()
- .then(() => BG.deleteStyle({id, notify}))
- .then(() => {
- if (!notify) {
- handleDelete(id);
- }
- return id;
- });
- }
- function download(url, {
- method = url.includes('?') ? 'POST' : 'GET',
- body = url.includes('?') ? url.slice(url.indexOf('?')) : null,
- requiredStatusCode = 200,
- timeout = 10e3,
- headers = {
- 'Content-type': 'application/x-www-form-urlencoded',
- },
- } = {}) {
- return new Promise((resolve, reject) => {
- url = new URL(url);
- if (url.protocol === 'file:' && FIREFOX) {
- // https://stackoverflow.com/questions/42108782/firefox-webextensions-get-local-files-content-by-path
- // FIXME: add FetchController when it is available.
- const timer = setTimeout(reject, timeout, new Error('Timeout fetching ' + url.href));
- fetch(url.href, {mode: 'same-origin'})
- .then(r => {
- clearTimeout(timer);
- return r.status === 200 ? r.text() : Promise.reject(r.status);
- })
- .catch(reject)
- .then(resolve);
- return;
- }
- const xhr = new XMLHttpRequest();
- xhr.timeout = timeout;
- xhr.onloadend = event => {
- if (event.type !== 'error' && (
- xhr.status === requiredStatusCode || !requiredStatusCode ||
- url.protocol === 'file:')) {
- resolve(xhr.responseText);
- } else {
- reject(xhr.status);
- }
- };
- xhr.onerror = xhr.onloadend;
- xhr.open(method, url.href, true);
- for (const key in headers) {
- xhr.setRequestHeader(key, headers[key]);
- }
- xhr.send(body);
- });
- }
- function invokeOrPostpone(isInvoke, fn, ...args) {
- return isInvoke
- ? fn(...args)
- : setTimeout(invokeOrPostpone, 0, true, fn, ...args);
- }
- function openEditor(id) {
- let url = '/edit.html';
- if (id) {
- url += `?id=${id}`;
- }
- if (chrome.windows && prefs.get('openEditInWindow')) {
- chrome.windows.create(Object.assign({url}, prefs.get('windowPosition')));
- } else {
- openURL({url});
- }
- }
- function closeCurrentTab() {
- // https://bugzilla.mozilla.org/show_bug.cgi?id=1409375
- getOwnTab().then(tab => {
- if (tab) {
- chrome.tabs.remove(tab.id);
- }
- });
- }
|