From 53aa239da3cb7e022ce3ce988100c8c7acfb9a72 Mon Sep 17 00:00:00 2001 From: tophf Date: Sat, 2 Sep 2017 18:36:32 +0300 Subject: [PATCH] fallback to chrome.storage when IndexedDB is dysfunctional --- _locales/en/messages.json | 2 +- background/background.js | 4 -- background/storage.js | 94 ++++++++++++++++++++++++++++++++++++++- js/dom.js | 68 ++++++++++------------------ manage/manage.js | 5 ++- msgbox/dysfunctional.css | 4 +- msgbox/dysfunctional.js | 7 +-- 7 files changed, 123 insertions(+), 61 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 7e89af27..6122067f 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -140,7 +140,7 @@ "description": "Label for the style editor's CSS theme." }, "dysfunctional": { - "message": "Stylus cannot function because Firefox is either in private mode or is applying its website cookies policy to IndexedDB storage used by Stylus, which erroneously marks the secure moz-extension:// origin as insecure even though WebExtensions aren't websites and Stylus doesn't use cookies.\n\n1. Open Firefox options\n2. Go to 'Privacy & Security'\n3. Set 'History' mode to 'Use custom settings'\n4. Click 'Exceptions'\n5. Paste our manifest URL and click 'Allow'\n6. Click 'Save settings'\n7. Uncheck 'Always use private browsing mode'\n\nThe actual manifest URL is shown below.\nYou can also find it on about:debugging page.", + "message": "Stylus cannot function in private windows because Firefox disallows direct connection to the internal background page context of the extension.", "description": "Displayed in Firefox when its settings make Stylus dysfunctional" }, "dysfunctionalBackgroundConnection": { diff --git a/background/background.js b/background/background.js index 51fbe3d8..cab15047 100644 --- a/background/background.js +++ b/background/background.js @@ -5,10 +5,6 @@ // eslint-disable-next-line no-var var browserCommands, contextMenus; -// ************************************************************************* -// preload the DB -tryCatch(getStyles); - // ************************************************************************* // register all listeners chrome.runtime.onMessage.addListener(onRuntimeMessage); diff --git a/background/storage.js b/background/storage.js index c4a389cd..f9e88609 100644 --- a/background/storage.js +++ b/background/storage.js @@ -40,6 +40,11 @@ var chromeLocal = { chrome.storage.local.set(data, () => resolve(data)); }); }, + remove(keyOrKeys) { + return new Promise(resolve => { + chrome.storage.local.remove(keyOrKeys, resolve); + }); + }, getValue(key) { return chromeLocal.get(key).then(data => data[key]); }, @@ -77,8 +82,54 @@ var chromeSync = { } }; +// eslint-disable-next-line no-var +var dbExec = dbExecIndexedDB; -function dbExec(method, data) { +// we use chrome.storage.local fallback if IndexedDB doesn't save data, +// which, once detected on the first run, is remembered in chrome.storage.local +// for reliablility and in localStorage for fast synchronous access +// (FF may block localStorage depending on its privacy options) +do { + const fallback = () => { + dbExec = dbExecChromeStorage; + chromeLocal.set({dbInChromeStorage: true}); + localStorage.dbInChromeStorage = 'true'; + ignoreChromeError(); + getStyles(); + }; + const fallbackSet = localStorage.dbInChromeStorage; + if (fallbackSet === 'true' || !tryCatch(() => indexedDB)) { + fallback(); + break; + } else if (fallbackSet === 'false') { + getStyles(); + break; + } + chromeLocal.get('dbInChromeStorage') + .then(data => + data && data.dbInChromeStorage && Promise.reject()) + .then(() => dbExecIndexedDB('getAllKeys', IDBKeyRange.lowerBound(1), 1)) + .then(({target}) => ( + (target.result || [])[0] ? + Promise.reject('ok') : + dbExecIndexedDB('get', -1))) + .then(({target}) => ( + (target.result || {}).id === -1 ? + dbExecIndexedDB('delete', -1).then(() => 'ok') : + Promise.reject())) + .catch(result => { + if (result === 'ok') { + chromeLocal.set({dbInChromeStorage: false}); + localStorage.dbInChromeStorage = 'false'; + getStyles(); + } else { + fallback(); + } + }); +} while (0); + + +function dbExecIndexedDB(method, ...args) { return new Promise((resolve, reject) => { Object.assign(indexedDB.open('stylish', 2), { onsuccess(event) { @@ -88,7 +139,7 @@ function dbExec(method, data) { } else { const transaction = database.transaction(['styles'], 'readwrite'); const store = transaction.objectStore('styles'); - Object.assign(store[method](data), { + Object.assign(store[method](...args), { onsuccess: event => resolve(event, store, transaction, database), onerror: reject, }); @@ -111,6 +162,45 @@ function dbExec(method, data) { } +function dbExecChromeStorage(method, data) { + const STYLE_KEY_PREFIX = 'style-'; + switch (method) { + case 'get': + return chromeLocal.getValue(STYLE_KEY_PREFIX + data) + .then(result => ({target: {result}})); + + case 'put': + if (!data.id) { + return getStyles().then(() => { + data.id = 1; + for (const style of cachedStyles.list) { + data.id = Math.max(data.id, style.id + 1); + } + return dbExecChromeStorage('put', data); + }); + } + return chromeLocal.setValue(STYLE_KEY_PREFIX + data.id, data) + .then(() => (chrome.runtime.lastError ? Promise.reject() : data.id)); + + case 'delete': + return chromeLocal.remove(STYLE_KEY_PREFIX + data); + + case 'getAll': + return chromeLocal.get(null).then(storage => { + const styles = []; + for (const key in storage) { + if (key.startsWith(STYLE_KEY_PREFIX) && + Number(key.substr(STYLE_KEY_PREFIX.length))) { + styles.push(storage[key]); + } + } + return {target: {result: styles}}; + }); + } + return Promise.reject(); +} + + function getStyles(options) { if (cachedStyles.list) { return Promise.resolve(filterStyles(options)); diff --git a/js/dom.js b/js/dom.js index 659c2e4d..ed21d634 100644 --- a/js/dom.js +++ b/js/dom.js @@ -38,20 +38,31 @@ for (const type of [NodeList, NamedNodeMap, HTMLCollection, HTMLAllCollection]) // add favicon in Firefox // eslint-disable-next-line no-unused-expressions -navigator.userAgent.includes('Firefox') && setTimeout(() => { - dieOnDysfunction(); - const iconset = ['', 'light/'][prefs.get('iconset')] || ''; - for (const size of [38, 32, 19, 16]) { - document.head.appendChild($element({ - tag: 'link', - rel: 'icon', - href: `/images/icon/${iconset}${size}.png`, - sizes: size + 'x' + size, - })); - } +if (navigator.userAgent.includes('Firefox')) { + chrome.windows.getCurrent(wnd => { + if (!BG && wnd.incognito) { + // private windows can't get bg page + location.href = '/msgbox/dysfunctional.html'; + throw 0; + } + }); + setTimeout(() => { + if (!window.prefs) { + return; + } + const iconset = ['', 'light/'][prefs.get('iconset')] || ''; + for (const size of [38, 32, 19, 16]) { + document.head.appendChild($element({ + tag: 'link', + rel: 'icon', + href: `/images/icon/${iconset}${size}.png`, + sizes: size + 'x' + size, + })); + } + }); // set hyphenation language document.documentElement.setAttribute('lang', chrome.i18n.getUILanguage()); -}); +} function onDOMready() { @@ -259,36 +270,3 @@ function $element(opt) { } return element; } - - -function dieOnDysfunction() { - function die() { - location.href = '/msgbox/dysfunctional.html'; - throw 0; - } - (() => { - try { - return indexedDB; - } catch (e) { - die(); - } - })(); - Object.assign(indexedDB.open('test'), { - onerror: die, - onupgradeneeded: indexedDB.deleteDatabase('test'), - }); - // TODO: fallback to sendMessage in FF since private windows can't get bg page - chrome.windows.getCurrent(wnd => wnd.incognito && die()); - // check if privacy settings were fixed but the extension wasn't reloaded, - // use setTimeout to auto-cancel if already dead - setTimeout(() => { - const bg = chrome.extension.getBackgroundPage(); - if (bg && !(bg.cachedStyles || {}).list) { - chrome.storage.local.get('reloaded', data => { - if (!data || Date.now() - (data.reloaded || 0) > 10e3) { - chrome.storage.local.set({reloaded: Date.now()}, () => chrome.runtime.reload()); - } - }); - } - }); -} diff --git a/manage/manage.js b/manage/manage.js index 59108ba2..202da0fc 100644 --- a/manage/manage.js +++ b/manage/manage.js @@ -555,7 +555,10 @@ function dieOnNullBackground() { title: 'Stylus', className: 'danger center', contents: t('dysfunctionalBackgroundConnection'), - onshow: () => $('#message-box-close-icon').remove(), + onshow: () => { + $('#message-box-close-icon').remove(); + window.removeEventListener('keydown', messageBox.listeners.key, true); + } }); document.documentElement.style.pointerEvents = 'none'; }); diff --git a/msgbox/dysfunctional.css b/msgbox/dysfunctional.css index 05e51e71..1c4e00e7 100644 --- a/msgbox/dysfunctional.css +++ b/msgbox/dysfunctional.css @@ -1,6 +1,6 @@ html { height: 100vh; - min-height: 450px; + min-height: 12em; display: flex; align-items: center; justify-content: center; @@ -16,7 +16,7 @@ html { body { margin: 2em; color: white; - max-width: 600px; + max-width: 20em; } div { diff --git a/msgbox/dysfunctional.js b/msgbox/dysfunctional.js index c1659a89..7f7ba6b2 100644 --- a/msgbox/dysfunctional.js +++ b/msgbox/dysfunctional.js @@ -1,8 +1,3 @@ 'use strict'; -document.body.textContent = - chrome.i18n.getMessage('dysfunctional'); -document.body.appendChild(document.createElement('div')).textContent = - chrome.runtime.getURL('manifest.json'); -// set hyphenation language -document.documentElement.setAttribute('lang', chrome.i18n.getUILanguage()); +document.body.textContent = chrome.i18n.getMessage('dysfunctional');