diff --git a/background/sync.js b/background/sync.js index cb03f4f0..052784d8 100644 --- a/background/sync.js +++ b/background/sync.js @@ -38,14 +38,11 @@ const sync = (() => { }, getState(drive) { const key = `sync/state/${drive.name}`; - return chromeLocal.get(key) - .then(obj => obj[key]); + return chromeLocal.getValue(key); }, setState(drive, state) { const key = `sync/state/${drive.name}`; - return chromeLocal.set({ - [key]: state - }); + return chromeLocal.setValue(key, state); } }); diff --git a/background/token-manager.js b/background/token-manager.js index 755fd9fa..b99b38b5 100644 --- a/background/token-manager.js +++ b/background/token-manager.js @@ -1,12 +1,8 @@ -/* global chromeLocal promisifyChrome webextLaunchWebAuthFlow FIREFOX */ +/* global chromeLocal webextLaunchWebAuthFlow FIREFOX */ /* exported tokenManager */ 'use strict'; const tokenManager = (() => { - promisifyChrome({ - 'windows': ['create', 'update', 'remove'], - 'tabs': ['create', 'update', 'remove'] - }); const AUTH = { dropbox: { flow: 'token', @@ -93,24 +89,20 @@ const tokenManager = (() => { }); } - function revokeToken(name) { + async function revokeToken(name) { const provider = AUTH[name]; const k = buildKeys(name); - return revoke() - .then(() => chromeLocal.remove(k.LIST)); - - function revoke() { - if (!provider.revoke) { - return Promise.resolve(); + if (provider.revoke) { + try { + const token = await chromeLocal.getValue(k.TOKEN); + if (token) { + await provider.revoke(token); + } + } catch (e) { + console.error(e); } - return chromeLocal.get(k.TOKEN) - .then(obj => { - if (obj[k.TOKEN]) { - return provider.revoke(obj[k.TOKEN]); - } - }) - .catch(console.error); } + await chromeLocal.remove(k.LIST); } function refreshToken(name, k, obj) { diff --git a/background/update.js b/background/update.js index c458426e..ec55a9e5 100644 --- a/background/update.js +++ b/background/update.js @@ -264,9 +264,9 @@ debounce(flushQueue, text && checkingAll ? 1000 : 0); } - function flushQueue(lines) { + async function flushQueue(lines) { if (!lines) { - chromeLocal.getValue('updateLog', []).then(flushQueue); + flushQueue(await chromeLocal.getValue('updateLog') || []); return; } const time = Date.now() - logLastWriteTime > 11e3 ? diff --git a/edit/edit.js b/edit/edit.js index 1a5c4326..c1de3dd0 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -151,11 +151,10 @@ lazyInit(); if (onBoundsChanged) { // * movement is reported even if the window wasn't resized // * fired just once when done so debounce is not needed - onBoundsChanged.addListener(wnd => { + onBoundsChanged.addListener(async wnd => { // getting the current window id as it may change if the user attached/detached the tab - chrome.windows.getCurrent(ownWnd => { - if (wnd.id === ownWnd.id) saveWindowPos(); - }); + const {id} = await browser.windows.getCurrent(); + if (id === wnd.id) saveWindowPos(); }); } window.on('resize', () => { @@ -325,7 +324,15 @@ lazyInit(); /* Stuff not needed for the main init so we can let it run at its own tempo */ function lazyInit() { let ownTabId; - getOwnTab().then(async tab => { + // not using `await` so we don't block the subsequent code + getOwnTab().then(patchHistoryBack); + // no windows on android + if (chrome.windows) { + restoreWindowSize(); + detectWindowedState(); + chrome.tabs.onAttached.addListener(onAttached); + } + async function patchHistoryBack(tab) { ownTabId = tab.id; // use browser history back when 'back to manage' is clicked if (sessionStorageHash('manageStylesHistory').value[ownTabId] === location.href) { @@ -336,29 +343,23 @@ function lazyInit() { history.back(); }; } - }); - // no windows on android - if (!chrome.windows) { - return; } - // resize on 'undo close' - const pos = tryJSONparse(sessionStorage.windowPos); - delete sessionStorage.windowPos; - if (pos && pos.left != null && chrome.windows) { - chrome.windows.update(chrome.windows.WINDOW_ID_CURRENT, pos); + /** resize on 'undo close' */ + function restoreWindowSize() { + const pos = tryJSONparse(sessionStorage.windowPos); + delete sessionStorage.windowPos; + if (pos && pos.left != null && chrome.windows) { + chrome.windows.update(chrome.windows.WINDOW_ID_CURRENT, pos); + } } - // detect isWindowed - if (prefs.get('openEditInWindow') && history.length === 1) { - chrome.tabs.query({currentWindow: true}, tabs => { - if (tabs.length === 1) { - chrome.windows.getAll(windows => { - isWindowed = windows.length > 1; // not modifying the main browser window - }); - } - }); + async function detectWindowedState() { + isWindowed = + prefs.get('openEditInWindow') && + history.length === 1 && + browser.windows.getAll().length > 1 && + (await browser.tabs.query({currentWindow: true})).length === 1; } - // toggle openEditInWindow - chrome.tabs.onAttached.addListener((tabId, info) => { + async function onAttached(tabId, info) { if (tabId !== ownTabId) { return; } @@ -366,16 +367,15 @@ function lazyInit() { prefs.set('openEditInWindow', false); return; } - chrome.windows.get(info.newWindowId, {populate: true}, win => { - // If there's only one tab in this window, it's been dragged to new window - const openEditInWindow = win.tabs.length === 1; - if (openEditInWindow && FIREFOX) { - // FF-only because Chrome retardedly resets the size during dragging - chrome.windows.update(info.newWindowId, prefs.get('windowPosition')); - } - prefs.set('openEditInWindow', openEditInWindow); - }); - }); + const win = await browser.windows.get(info.newWindowId, {populate: true}); + // If there's only one tab in this window, it's been dragged to new window + const openEditInWindow = win.tabs.length === 1; + // FF-only because Chrome retardedly resets the size during dragging + if (openEditInWindow && FIREFOX) { + chrome.windows.update(info.newWindowId, prefs.get('windowPosition')); + } + prefs.set('openEditInWindow', openEditInWindow); + } } function onRuntimeMessage(request) { diff --git a/install-usercss/install-usercss.js b/install-usercss/install-usercss.js index 8ce11002..fa6c6aa3 100644 --- a/install-usercss/install-usercss.js +++ b/install-usercss/install-usercss.js @@ -398,11 +398,13 @@ r.resolve(code); } }); - port.onDisconnect.addListener(() => { - chrome.tabs.get(tabId, tab => - !chrome.runtime.lastError && tab.url === initialUrl - ? location.reload() - : closeCurrentTab()); + port.onDisconnect.addListener(async () => { + const tab = await browser.tabs.get(tabId); + if (!chrome.runtime.lastError && tab.url === initialUrl) { + location.reload(); + } else { + closeCurrentTab(); + } }); return (opts = {}) => new Promise((resolve, reject) => { const id = performance.now(); diff --git a/js/messaging.js b/js/messaging.js index fb898aef..ba7ae2ed 100644 --- a/js/messaging.js +++ b/js/messaging.js @@ -1,7 +1,6 @@ /* exported getTab getActiveTab onTabReady stringAsRegExp openURL ignoreChromeError getStyleWithNoCode tryRegExp sessionStorageHash download deepEqual closeCurrentTab capitalize CHROME_HAS_BORDER_BUG */ -/* global promisifyChrome */ 'use strict'; const CHROME = Boolean(chrome.app) && parseInt(navigator.userAgent.match(/Chrom\w+\/(\d+)|$/)[1]); @@ -92,10 +91,6 @@ if (chrome.extension.getBackgroundPage && chrome.extension.getBackgroundPage() = if (cls) document.documentElement.classList.add(cls); } -promisifyChrome({ - tabs: ['create', 'get', 'getCurrent', 'move', 'query', 'update'], - windows: ['create', 'update'], // Android doesn't have chrome.windows -}); // FF57+ supports openerTabId, but not in Android // (detecting FF57 by the feature it added, not navigator.ua which may be spoofed in about:config) const openerTabIdSupported = (!FIREFOX || window.AbortController) && chrome.windows != null; diff --git a/js/msg.js b/js/msg.js index b2c9203f..0469b269 100644 --- a/js/msg.js +++ b/js/msg.js @@ -1,13 +1,8 @@ -/* global promisifyChrome */ /* global deepCopy getOwnTab URLS */ // not used in content scripts 'use strict'; // eslint-disable-next-line no-unused-expressions window.INJECTED !== 1 && (() => { - promisifyChrome({ - runtime: ['sendMessage', 'getBackgroundPage'], - tabs: ['sendMessage', 'query'], - }); const TARGETS = Object.assign(Object.create(null), { all: ['both', 'tab', 'extension'], extension: ['both', 'extension'], diff --git a/js/polyfill.js b/js/polyfill.js index e299957f..32b19b05 100644 --- a/js/polyfill.js +++ b/js/polyfill.js @@ -5,52 +5,65 @@ self.INJECTED !== 1 && (() => { //#region for content scripts and our extension pages - if (!window.browser || !browser.runtime) { - const createTrap = (base, parent) => { - const target = typeof base === 'function' ? () => {} : {}; - target.isTrap = true; - return new Proxy(target, { - get: (target, prop) => { - if (target[prop]) return target[prop]; - if (base[prop] && (typeof base[prop] === 'object' || typeof base[prop] === 'function')) { - target[prop] = createTrap(base[prop], base); - return target[prop]; - } - return base[prop]; - }, - apply: (target, thisArg, args) => base.apply(parent, args) - }); + if (!((window.browser || {}).runtime || {}).sendMessage) { + /* Auto-promisifier with a fallback to direct call on signature error. + The fallback isn't used now since we call all synchronous methods via `chrome` */ + const directEvents = ['addListener', 'removeListener', 'hasListener', 'hasListeners']; + // generated by tools/chrome-api-no-cb.js + const directMethods = { + alarms: ['create'], + extension: ['getBackgroundPage', 'getExtensionTabs', 'getURL', 'getViews', 'setUpdateUrlData'], + i18n: ['getMessage', 'getUILanguage'], + identity: ['getRedirectURL'], + runtime: ['connect', 'connectNative', 'getManifest', 'getURL', 'reload', 'restart'], + tabs: ['connect'], }; - window.browser = createTrap(chrome, null); + const promisify = function (fn, ...args) { + let res; + try { + let resolve, reject; + /* Some callbacks have 2 parameters so we're resolving as an array in that case. + For example, chrome.runtime.requestUpdateCheck and chrome.webRequest.onAuthRequired */ + args.push((...results) => + chrome.runtime.lastError ? + reject(new Error(chrome.runtime.lastError.message)) : + resolve(results.length <= 1 ? results[0] : results)); + fn.apply(this, args); + res = new Promise((...rr) => ([resolve, reject] = rr)); + } catch (err) { + if (!err.message.includes('No matching signature')) { + throw err; + } + args.pop(); + res = fn.apply(this, args); + } + return res; + }; + const proxify = (src, srcName, target, key) => { + let res = src[key]; + if (res && typeof res === 'object') { + res = createProxy(res, key); // eslint-disable-line no-use-before-define + } else if (typeof res === 'function') { + res = (directMethods[srcName] || directEvents).includes(key) + ? res.bind(src) + : promisify.bind(src, res); + } + target[key] = res; + return res; + }; + const createProxy = (src, srcName) => + new Proxy({}, { + get(target, key) { + return target[key] || proxify(src, srcName, target, key); + }, + }); + window.browser = createProxy(chrome); } - /* Promisifies the specified `chrome` methods into `browser`. - The definitions is an object like this: { - 'storage.sync': ['get', 'set'], // if deeper than one level, combine the path via `.` - windows: ['create', 'update'], // items and sub-objects will only be created if present in `chrome` - } */ - window.promisifyChrome = definitions => { - for (const [scopeName, methods] of Object.entries(definitions)) { - const path = scopeName.split('.'); - const src = path.reduce((obj, p) => obj && obj[p], chrome); - if (!src) continue; - const dst = path.reduce((obj, p) => obj[p] || (obj[p] = {}), browser); - for (const name of methods) { - const fn = src[name]; - if (!fn || dst[name] && !dst[name].isTrap) continue; - dst[name] = (...args) => new Promise((resolve, reject) => - fn.call(src, ...args, (...results) => - chrome.runtime.lastError ? - reject(chrome.runtime.lastError) : - resolve(results.length <= 1 ? results[0] : results))); - // a couple of callbacks have 2 parameters (we don't use those methods, but just in case) - } - } - }; + //#endregion if (!chrome.tabs) return; - //#endregion //#region for our extension pages for (const storage of ['localStorage', 'sessionStorage']) { @@ -77,5 +90,6 @@ self.INJECTED !== 1 && (() => { } }; } + //#endregion })(); diff --git a/js/prefs.js b/js/prefs.js index 4ecdafc3..ea290534 100644 --- a/js/prefs.js +++ b/js/prefs.js @@ -1,4 +1,4 @@ -/* global promisifyChrome msg API */ +/* global msg API */ /* global deepCopy debounce */ // not used in content scripts 'use strict'; @@ -114,11 +114,6 @@ window.INJECTED !== 1 && (() => { any: new Set(), specific: {}, }; - if (msg.isBg) { - promisifyChrome({ - 'storage.sync': ['get', 'set'], - }); - } // getPrefs may fail on browser startup in the active tab as it loads before the background script const initializing = (msg.isBg ? readStorage() : API.getPrefs().catch(readStorage)) .then(setAll); @@ -236,11 +231,8 @@ window.INJECTED !== 1 && (() => { } function readStorage() { - /* Using a non-promisified call since this code may also run in a content script - when API.getPrefs occasionally fails during browser startup in the active tab */ - return new Promise(resolve => - chrome.storage.sync.get(STORAGE_KEY, data => - resolve(data[STORAGE_KEY]))); + return browser.storage.sync.get(STORAGE_KEY) + .then(data => data[STORAGE_KEY]); } function updateStorage() { diff --git a/js/storage-util.js b/js/storage-util.js index 42e2931a..2ff5587a 100644 --- a/js/storage-util.js +++ b/js/storage-util.js @@ -1,72 +1,51 @@ -/* global loadScript tryJSONparse promisifyChrome */ -/* exported chromeLocal chromeSync */ +/* global loadScript tryJSONparse */ 'use strict'; -promisifyChrome({ - 'storage.local': ['get', 'remove', 'set'], - 'storage.sync': ['get', 'remove', 'set'], -}); - -const [chromeLocal, chromeSync] = (() => { - return [ - createWrapper('local'), - createWrapper('sync'), - ]; - - function createWrapper(name) { - const storage = browser.storage[name]; - const wrapper = { - get: storage.get.bind(storage), - set: data => storage.set(data).then(() => data), - remove: storage.remove.bind(storage), - - /** - * @param {String} key - * @param {Any} [defaultValue] - * @returns {Promise} - */ - getValue: (key, defaultValue) => - wrapper.get( - defaultValue !== undefined ? - {[key]: defaultValue} : - key - ).then(data => data[key]), - - setValue: (key, value) => wrapper.set({[key]: value}), - - getLZValue: key => wrapper.getLZValues([key]).then(data => data[key]), - getLZValues: (keys = Object.values(wrapper.LZ_KEY)) => - Promise.all([ - wrapper.get(keys), - loadLZStringScript(), - ]).then(([data = {}, LZString]) => { - for (const key of keys) { - const value = data[key]; - data[key] = value && tryJSONparse(LZString.decompressFromUTF16(value)); - } - return data; - }), - setLZValue: (key, value) => - loadLZStringScript().then(LZString => - wrapper.set({ - [key]: LZString.compressToUTF16(JSON.stringify(value)), - })), - - loadLZStringScript, - }; - return wrapper; - } - - function loadLZStringScript() { - return window.LZString ? - Promise.resolve(window.LZString) : - loadScript('/vendor/lz-string-unsafe/lz-string-unsafe.min.js').then(() => - (window.LZString = window.LZString || window.LZStringUnsafe)); - } +(() => { + /** @namespace StorageExtras */ + const StorageExtras = { + async getValue(key) { + return (await this.get(key))[key]; + }, + async setValue(key, value) { + await this.set({[key]: value}); + }, + async getLZValue(key) { + return (await this.getLZValues([key]))[key]; + }, + async getLZValues(keys = Object.values(this.LZ_KEY)) { + const [data, LZString] = await Promise.all([ + this.get(keys), + this.getLZString(), + ]); + for (const key of keys) { + const value = data[key]; + data[key] = value && tryJSONparse(LZString.decompressFromUTF16(value)); + } + return data; + }, + async setLZValue(key, value) { + const LZString = await this.getLZString(); + return this.setValue(key, LZString.compressToUTF16(JSON.stringify(value))); + }, + async getLZString() { + if (!window.LZString) { + await loadScript('/vendor/lz-string-unsafe/lz-string-unsafe.min.js'); + window.LZString = window.LZString || window.LZStringUnsafe; + } + return window.LZString; + }, + }; + /** @namespace StorageExtrasSync */ + const StorageExtrasSync = { + LZ_KEY: { + csslint: 'editorCSSLintConfig', + stylelint: 'editorStylelintConfig', + usercssTemplate: 'usercssTemplate', + }, + }; + /** @type {chrome.storage.StorageArea|StorageExtras} */ + window.chromeLocal = Object.assign(browser.storage.local, StorageExtras); + /** @type {chrome.storage.StorageArea|StorageExtras|StorageExtrasSync} */ + window.chromeSync = Object.assign(browser.storage.sync, StorageExtras, StorageExtrasSync); })(); - -chromeSync.LZ_KEY = { - csslint: 'editorCSSLintConfig', - stylelint: 'editorStylelintConfig', - usercssTemplate: 'usercssTemplate', -}; diff --git a/manage/import-export.js b/manage/import-export.js index 7c29d53c..9cc20790 100644 --- a/manage/import-export.js +++ b/manage/import-export.js @@ -257,8 +257,7 @@ async function importFromString(jsonString) { // Must acquire the permission before setting the pref if (CHROME && !chrome.declarativeContent && stats.options.names.find(_ => _.name === 'styleViaXhr' && _.isValid && _.val)) { - await new Promise(resolve => - chrome.permissions.request({permissions: ['declarativeContent']}, resolve)); + await browser.permissions.request({permissions: ['declarativeContent']}); } const oldStorage = await chromeSync.get(); for (const {name, val, isValid, isPref} of stats.options.names) { diff --git a/popup/popup.js b/popup/popup.js index bc9df1b2..7a7fc9a5 100644 --- a/popup/popup.js +++ b/popup/popup.js @@ -88,28 +88,23 @@ function toggleSideBorders(state = prefs.get('popup.borders')) { } } -function initTabUrls() { - return getActiveTab() - .then((tab = {}) => - FIREFOX && tab.status === 'loading' && tab.url === ABOUT_BLANK - ? waitForTabUrlFF(tab) - : tab) - .then(tab => new Promise(resolve => - chrome.webNavigation.getAllFrames({tabId: tab.id}, frames => - resolve({frames, tab})))) - .then(({frames, tab}) => { - let url = tab.pendingUrl || tab.url || ''; // new Chrome uses pendingUrl while connecting - frames = sortTabFrames(frames); - if (url === 'chrome://newtab/' && !URLS.chromeProtectsNTP) { - url = frames[0].url || ''; - } - if (!URLS.supported(url)) { - url = ''; - frames.length = 1; - } - tabURL = frames[0].url = url; - return frames; - }); +async function initTabUrls() { + let tab = await getActiveTab(); + if (FIREFOX && tab.status === 'loading' && tab.url === ABOUT_BLANK) { + tab = await waitForTabUrlFF(tab); + } + let frames = await browser.webNavigation.getAllFrames({tabId: tab.id}); + let url = tab.pendingUrl || tab.url || ''; // new Chrome uses pendingUrl while connecting + frames = sortTabFrames(frames); + if (url === 'chrome://newtab/' && !URLS.chromeProtectsNTP) { + url = frames[0].url || ''; + } + if (!URLS.supported(url)) { + url = ''; + frames.length = 1; + } + tabURL = frames[0].url = url; + return frames; } /** @param {chrome.webNavigation.GetAllFrameResultDetails[]} frames */ diff --git a/tools/chrome-api-no-cb.js b/tools/chrome-api-no-cb.js new file mode 100644 index 00000000..eacaeecf --- /dev/null +++ b/tools/chrome-api-no-cb.js @@ -0,0 +1,73 @@ +#!/usr/bin/env node +/* + Generates a list of callbackless chrome.* API methods from chromium source + to be used in polyfill.js +*/ +'use strict'; + +const manifest = require('../manifest.json'); +const fetch = require('make-fetch-happen'); + +(async () => { + manifest.permissions.push('extension', 'i18n', 'runtime'); + const FN_NO_CB = /\bstatic (\w+) (\w+)(?![^)]*callback)\(\s*([^)]*)\)/g; + const BASE = 'https://github.com/chromium/chromium/raw/master/'; + const PATHS = [ + [BASE + 'extensions/common/api/', 'schema.gni'], + [BASE + 'chrome/common/extensions/api/', 'api_sources.gni'], + ]; + console.debug('Downloading...'); + const schemas = await Promise.all(PATHS.map(([path, name]) => fetchText(path + name))); + const files = {}; + schemas.forEach((text, i) => { + const path = PATHS[i][0]; + text.match(/\w+\.(idl|json)/g).forEach(name => { + files[name] = path; + }); + }); + const resList = []; + const resObj = {}; + await Promise.all(Object.entries(files).map(processApi)); + Object.entries(resObj) + .sort(([a], [b]) => a < b ? -1 : a > b) + .forEach(([key, val]) => { + delete resObj[key]; + resObj[key] = val; + val.sort(); + }); + console.log(resList.sort().join('\n')); + console.log(JSON.stringify(resObj)); + + async function fetchText(file) { + return (await fetch(file)).text(); + } + + async function processApi([file, path]) { + const [name, ext] = file.split('.'); + const api = manifest.permissions.find(p => + name === p.replace(/([A-Z])/g, s => '_' + s.toLowerCase()) || + name === p.replace(/\./g, '_')); + if (!api) return; + const text = await fetchText(path + file); + const noCmt = text.replace(/^\s*\/\/.*$/gm, ''); + if (ext === 'idl') { + const fnBlock = (noCmt.split(/\n\s*interface Functions {\s*/)[1] || '') + .split(/\n\s*interface \w+ {/)[0]; + for (let m; (m = FN_NO_CB.exec(fnBlock));) { + const [, type, name, params] = m; + resList.push(`chrome.${api}.${name}(${params.replace(/\n\s*/g, ' ')}): ${type}`); + (resObj[api] || (resObj[api] = [])).push(name); + } + } else { + for (const fn of JSON.parse(noCmt)[0].functions || []) { + const last = fn.parameters[fn.parameters.length - 1]; + if (!fn.returns_async && (!last || last.type !== 'function')) { + resList.push(`chrome.${api}.${fn.name}(${ + fn.parameters.map(p => `${p.optional ? '?' : ''}${p.name}: ${p.type}`).join(', ') + })`); + (resObj[api] || (resObj[api] = [])).push(fn.name); + } + } + } + } +})();