API.* groups + async'ify

* API.styles.*
* API.usercss.*
* API.sync.*
* API.worker.*
* API.updater.*
* simplify db: resolve with result
* remove API.download
* simplify download()
* remove noCode param as it wastes more time/memory on copying
* styleManager: switch style<->data names to reflect their actual contents
* inline method bodies to avoid indirection and enable better autocomplete/hint/jump support in IDE
This commit is contained in:
tophf 2020-11-18 21:19:32 +03:00
parent 06823bd5b4
commit 86623a9aab
41 changed files with 1367 additions and 1574 deletions

View File

@ -4,6 +4,7 @@
importScripts('/js/worker-util.js');
const {loadScript} = workerUtil;
/** @namespace ApiWorker */
workerUtil.createAPI({
parseMozFormat(arg) {
loadScript('/vendor-overwrites/csslint/parserlib.js', '/js/moz-parser.js');

View File

@ -1,49 +1,30 @@
/* global download prefs openURL FIREFOX CHROME
URLS ignoreChromeError chromeLocal semverCompare
styleManager msg navigatorUtil workerUtil contentScripts sync
findExistingTab activateTab isTabReplaceable getActiveTab
/* global
activateTab
API
chromeLocal
findExistingTab
FIREFOX
getActiveTab
isTabReplaceable
msg
openURL
prefs
semverCompare
URLS
workerUtil
*/
'use strict';
// eslint-disable-next-line no-var
var backgroundWorker = workerUtil.createWorker({
url: '/background/background-worker.js',
});
//#region API
// eslint-disable-next-line no-var
var browserCommands, contextMenus;
Object.assign(API, {
// *************************************************************************
// browser commands
browserCommands = {
openManage,
openOptions: () => openManage({options: true}),
styleDisableAll(info) {
prefs.set('disableAll', info ? info.checked : !prefs.get('disableAll'));
},
reload: () => chrome.runtime.reload(),
};
window.API_METHODS = Object.assign(window.API_METHODS || {}, {
deleteStyle: styleManager.deleteStyle,
editSave: styleManager.editSave,
findStyle: styleManager.findStyle,
getAllStyles: styleManager.getAllStyles, // used by importer
getSectionsByUrl: styleManager.getSectionsByUrl,
getStyle: styleManager.get,
getStylesByUrl: styleManager.getStylesByUrl,
importStyle: styleManager.importStyle,
importManyStyles: styleManager.importMany,
installStyle: styleManager.installStyle,
styleExists: styleManager.styleExists,
toggleStyle: styleManager.toggleStyle,
addInclusion: styleManager.addInclusion,
removeInclusion: styleManager.removeInclusion,
addExclusion: styleManager.addExclusion,
removeExclusion: styleManager.removeExclusion,
/** @type {ApiWorker} */
worker: workerUtil.createWorker({
url: '/background/background-worker.js',
}),
/** @returns {string} */
getTabUrlPrefix() {
const {url} = this.sender.tab;
if (url.startsWith(URLS.ownOrigin)) {
@ -52,21 +33,68 @@ window.API_METHODS = Object.assign(window.API_METHODS || {}, {
return url.match(/^([\w-]+:\/+[^/#]+)/)[1];
},
download(msg) {
delete msg.method;
return download(msg.url, msg);
},
parseCss({code}) {
return backgroundWorker.parseMozFormat({code});
},
/** @returns {Prefs} */
getPrefs: () => prefs.values,
setPref: (key, value) => prefs.set(key, value),
setPref(key, value) {
prefs.set(key, value);
},
openEditor,
/**
* Opens the editor or activates an existing tab
* @param {{
id?: number
domain?: string
'url-prefix'?: string
}} params
* @returns {Promise<chrome.tabs.Tab>}
*/
openEditor(params) {
const u = new URL(chrome.runtime.getURL('edit.html'));
u.search = new URLSearchParams(params);
return openURL({
url: `${u}`,
currentWindow: null,
newWindow: prefs.get('openEditInWindow') && Object.assign({},
prefs.get('openEditInWindow.popup') && {type: 'popup'},
prefs.get('windowPosition')),
});
},
/* Same as openURL, the only extra prop in `opts` is `message` - it'll be sent when the tab is ready,
which is needed in the popup, otherwise another extension could force the tab to open in foreground
thus auto-closing the popup (in Chrome at least) and preventing the sendMessage code from running */
/** @returns {Promise<chrome.tabs.Tab>} */
async openManage({options = false, search, searchMode} = {}) {
let url = chrome.runtime.getURL('manage.html');
if (search) {
url += `?search=${encodeURIComponent(search)}&searchMode=${searchMode}`;
}
if (options) {
url += '#stylus-options';
}
let tab = await findExistingTab({
url,
currentWindow: null,
ignoreHash: true,
ignoreSearch: true,
});
if (tab) {
await activateTab(tab);
if (url !== (tab.pendingUrl || tab.url)) {
await msg.sendTab(tab.id, {method: 'pushState', url}).catch(console.error);
}
return tab;
}
tab = await getActiveTab();
return isTabReplaceable(tab, url)
? activateTab(tab, {url})
: browser.tabs.create({url});
},
/**
* Same as openURL, the only extra prop in `opts` is `message` - it'll be sent
* when the tab is ready, which is needed in the popup, otherwise another
* extension could force the tab to open in foreground thus auto-closing the
* popup (in Chrome at least) and preventing the sendMessage code from running
* @returns {Promise<chrome.tabs.Tab>}
*/
async openURL(opts) {
const tab = await openURL(opts);
if (opts.message) {
@ -86,54 +114,49 @@ window.API_METHODS = Object.assign(window.API_METHODS || {}, {
}));
}
},
});
optionsCustomizeHotkeys() {
return browserCommands.openOptions()
.then(() => new Promise(resolve => setTimeout(resolve, 500)))
.then(() => msg.broadcastExtension({method: 'optionsCustomizeHotkeys'}));
//#endregion
//#region browserCommands
const browserCommands = {
openManage: () => API.openManage(),
openOptions: () => API.openManage({options: true}),
styleDisableAll(info) {
prefs.set('disableAll', info ? info.checked : !prefs.get('disableAll'));
},
syncStart: sync.start,
syncStop: sync.stop,
syncNow: sync.syncNow,
getSyncStatus: sync.getStatus,
syncLogin: sync.login,
openManage,
});
// *************************************************************************
// register all listeners
msg.on(onRuntimeMessage);
// tell apply.js to refresh styles for non-committed navigation
navigatorUtil.onUrlChange(({tabId, frameId}, type) => {
if (type !== 'committed') {
msg.sendTab(tabId, {method: 'urlChanged'}, {frameId})
.catch(msg.ignoreError);
}
});
if (FIREFOX) {
// FF misses some about:blank iframes so we inject our content script explicitly
navigatorUtil.onDOMContentLoaded(webNavIframeHelperFF, {
url: [
{urlEquals: 'about:blank'},
],
reload: () => chrome.runtime.reload(),
};
if (chrome.commands) {
chrome.commands.onCommand.addListener(command => browserCommands[command]());
}
if (FIREFOX && browser.commands && browser.commands.update) {
// register hotkeys in FF
const hotkeyPrefs = Object.keys(prefs.defaults).filter(k => k.startsWith('hotkey.'));
prefs.subscribe(hotkeyPrefs, (name, value) => {
try {
name = name.split('.')[1];
if (value.trim()) {
browser.commands.update({name, shortcut: value});
} else {
browser.commands.reset(name);
}
} catch (e) {}
});
}
if (chrome.contextMenus) {
chrome.contextMenus.onClicked.addListener((info, tab) =>
contextMenus[info.menuItemId].click(info, tab));
}
//#endregion
//#region Init
if (chrome.commands) {
// Not available in Firefox - https://bugzilla.mozilla.org/show_bug.cgi?id=1240350
chrome.commands.onCommand.addListener(command => browserCommands[command]());
}
msg.on((msg, sender) => {
if (msg.method === 'invokeAPI') {
const fn = msg.path.reduce((res, name) => res && res[name], API);
if (!fn) throw new Error(`Unknown API.${msg.path.join('.')}`);
const res = fn.apply({msg, sender}, msg.args);
return res === undefined ? null : res;
}
});
// *************************************************************************
chrome.runtime.onInstalled.addListener(({reason, previousVersion}) => {
if (reason !== 'update') return;
if (semverCompare(previousVersion, '1.5.13') <= 0) {
@ -150,188 +173,6 @@ chrome.runtime.onInstalled.addListener(({reason, previousVersion}) => {
}
});
// *************************************************************************
// context menus
contextMenus = {
'show-badge': {
title: 'menuShowBadge',
click: info => prefs.set(info.menuItemId, info.checked),
},
'disableAll': {
title: 'disableAllStyles',
click: browserCommands.styleDisableAll,
},
'open-manager': {
title: 'openStylesManager',
click: browserCommands.openManage,
},
'open-options': {
title: 'openOptions',
click: browserCommands.openOptions,
},
'reload': {
presentIf: async () => (await browser.management.getSelf()).installType === 'development',
title: 'reload',
click: browserCommands.reload,
},
'editor.contextDelete': {
presentIf: () => !FIREFOX && prefs.get('editor.contextDelete'),
title: 'editDeleteText',
type: 'normal',
contexts: ['editable'],
documentUrlPatterns: [URLS.ownOrigin + 'edit*'],
click: (info, tab) => {
msg.sendTab(tab.id, {method: 'editDeleteText'}, undefined, 'extension')
.catch(msg.ignoreError);
},
},
};
async function createContextMenus(ids) {
for (const id of ids) {
let item = contextMenus[id];
if (item.presentIf && !await item.presentIf()) {
continue;
}
item = Object.assign({id}, item);
delete item.presentIf;
item.title = chrome.i18n.getMessage(item.title);
if (!item.type && typeof prefs.defaults[id] === 'boolean') {
item.type = 'checkbox';
item.checked = prefs.get(id);
}
if (!item.contexts) {
item.contexts = ['browser_action'];
}
delete item.click;
chrome.contextMenus.create(item, ignoreChromeError);
}
}
if (chrome.contextMenus) {
// "Delete" item in context menu for browsers that don't have it
if (CHROME &&
// looking at the end of UA string
/(Vivaldi|Safari)\/[\d.]+$/.test(navigator.userAgent) &&
// skip forks with Flash as those are likely to have the menu e.g. CentBrowser
!Array.from(navigator.plugins).some(p => p.name === 'Shockwave Flash')) {
prefs.defaults['editor.contextDelete'] = true;
}
// circumvent the bug with disabling check marks in Chrome 62-64
const toggleCheckmark = CHROME >= 62 && CHROME <= 64 ?
(id => chrome.contextMenus.remove(id, () => createContextMenus([id]) + ignoreChromeError())) :
((id, checked) => chrome.contextMenus.update(id, {checked}, ignoreChromeError));
const togglePresence = (id, checked) => {
if (checked) {
createContextMenus([id]);
} else {
chrome.contextMenus.remove(id, ignoreChromeError);
}
};
const keys = Object.keys(contextMenus);
prefs.subscribe(keys.filter(id => typeof prefs.defaults[id] === 'boolean'), toggleCheckmark);
prefs.subscribe(keys.filter(id => contextMenus[id].presentIf && id in prefs.defaults), togglePresence);
createContextMenus(keys);
}
// reinject content scripts when the extension is reloaded/updated. Firefox
// would handle this automatically.
if (!FIREFOX) {
setTimeout(contentScripts.injectToAllTabs, 0);
}
// register hotkeys
if (FIREFOX && browser.commands && browser.commands.update) {
const hotkeyPrefs = Object.keys(prefs.defaults).filter(k => k.startsWith('hotkey.'));
prefs.subscribe(hotkeyPrefs, (name, value) => {
try {
name = name.split('.')[1];
if (value.trim()) {
browser.commands.update({name, shortcut: value});
} else {
browser.commands.reset(name);
}
} catch (e) {}
});
}
msg.broadcast({method: 'backgroundReady'});
function webNavIframeHelperFF({tabId, frameId}) {
if (!frameId) return;
msg.sendTab(tabId, {method: 'ping'}, {frameId})
.catch(() => false)
.then(pong => {
if (pong) return;
// insert apply.js to iframe
const files = chrome.runtime.getManifest().content_scripts[0].js;
for (const file of files) {
chrome.tabs.executeScript(tabId, {
frameId,
file,
matchAboutBlank: true,
}, ignoreChromeError);
}
});
}
function onRuntimeMessage(msg, sender) {
if (msg.method !== 'invokeAPI') {
return;
}
const fn = window.API_METHODS[msg.name];
if (!fn) {
throw new Error(`unknown API: ${msg.name}`);
}
const res = fn.apply({msg, sender}, msg.args);
return res === undefined ? null : res;
}
function openEditor(params) {
/* Open the editor. Activate if it is already opened
params: {
id?: Number,
domain?: String,
'url-prefix'?: String
}
*/
const u = new URL(chrome.runtime.getURL('edit.html'));
u.search = new URLSearchParams(params);
return openURL({
url: `${u}`,
currentWindow: null,
newWindow: prefs.get('openEditInWindow') && Object.assign({},
prefs.get('openEditInWindow.popup') && {type: 'popup'},
prefs.get('windowPosition')),
});
}
async function openManage({options = false, search, searchMode} = {}) {
let url = chrome.runtime.getURL('manage.html');
if (search) {
url += `?search=${encodeURIComponent(search)}&searchMode=${searchMode}`;
}
if (options) {
url += '#stylus-options';
}
let tab = await findExistingTab({
url,
currentWindow: null,
ignoreHash: true,
ignoreSearch: true,
});
if (tab) {
await activateTab(tab);
if (url !== (tab.pendingUrl || tab.url)) {
await msg.sendTab(tab.id, {method: 'pushState', url}).catch(console.error);
}
return tab;
}
tab = await getActiveTab();
return isTabReplaceable(tab, url)
? activateTab(tab, {url})
: browser.tabs.create({url});
}
//#endregion

View File

@ -1,8 +1,18 @@
/* global msg ignoreChromeError URLS */
/* exported contentScripts */
/* global
FIREFOX
ignoreChromeError
msg
URLS
*/
'use strict';
const contentScripts = (() => {
/*
Reinject content scripts when the extension is reloaded/updated.
Firefox handles this automatically.
*/
// eslint-disable-next-line no-unused-expressions
!FIREFOX && (() => {
const NTP = 'chrome://newtab/';
const ALL_URLS = '<all_urls>';
const SCRIPTS = chrome.runtime.getManifest().content_scripts;
@ -18,21 +28,7 @@ const contentScripts = (() => {
const busyTabs = new Set();
let busyTabsTimer;
// expose version on greasyfork/sleazyfork 1) info page and 2) code page
const urlMatches = '/scripts/\\d+[^/]*(/code)?([?#].*)?$';
chrome.webNavigation.onCommitted.addListener(({tabId}) => {
chrome.tabs.executeScript(tabId, {
file: '/content/install-hook-greasyfork.js',
runAt: 'document_start',
});
}, {
url: [
{hostEquals: 'greasyfork.org', urlMatches},
{hostEquals: 'sleazyfork.org', urlMatches},
],
});
return {injectToTab, injectToAllTabs};
setTimeout(injectToAllTabs);
function injectToTab({url, tabId, frameId = null}) {
for (const script of SCRIPTS) {

107
background/context-menus.js Normal file
View File

@ -0,0 +1,107 @@
/* global
browserCommands
CHROME
FIREFOX
ignoreChromeError
msg
prefs
URLS
*/
'use strict';
// eslint-disable-next-line no-unused-expressions
chrome.contextMenus && (() => {
const contextMenus = {
'show-badge': {
title: 'menuShowBadge',
click: info => prefs.set(info.menuItemId, info.checked),
},
'disableAll': {
title: 'disableAllStyles',
click: browserCommands.styleDisableAll,
},
'open-manager': {
title: 'openStylesManager',
click: browserCommands.openManage,
},
'open-options': {
title: 'openOptions',
click: browserCommands.openOptions,
},
'reload': {
presentIf: async () => (await browser.management.getSelf()).installType === 'development',
title: 'reload',
click: browserCommands.reload,
},
'editor.contextDelete': {
presentIf: () => !FIREFOX && prefs.get('editor.contextDelete'),
title: 'editDeleteText',
type: 'normal',
contexts: ['editable'],
documentUrlPatterns: [URLS.ownOrigin + 'edit*'],
click: (info, tab) => {
msg.sendTab(tab.id, {method: 'editDeleteText'}, undefined, 'extension')
.catch(msg.ignoreError);
},
},
};
// "Delete" item in context menu for browsers that don't have it
if (CHROME &&
// looking at the end of UA string
/(Vivaldi|Safari)\/[\d.]+$/.test(navigator.userAgent) &&
// skip forks with Flash as those are likely to have the menu e.g. CentBrowser
!Array.from(navigator.plugins).some(p => p.name === 'Shockwave Flash')) {
prefs.defaults['editor.contextDelete'] = true;
}
const keys = Object.keys(contextMenus);
prefs.subscribe(keys.filter(id => typeof prefs.defaults[id] === 'boolean'),
CHROME >= 62 && CHROME <= 64 ? toggleCheckmarkBugged : toggleCheckmark);
prefs.subscribe(keys.filter(id => contextMenus[id].presentIf && id in prefs.defaults),
togglePresence);
createContextMenus(keys);
chrome.contextMenus.onClicked.addListener((info, tab) =>
contextMenus[info.menuItemId].click(info, tab));
async function createContextMenus(ids) {
for (const id of ids) {
let item = contextMenus[id];
if (item.presentIf && !await item.presentIf()) {
continue;
}
item = Object.assign({id}, item);
delete item.presentIf;
item.title = chrome.i18n.getMessage(item.title);
if (!item.type && typeof prefs.defaults[id] === 'boolean') {
item.type = 'checkbox';
item.checked = prefs.get(id);
}
if (!item.contexts) {
item.contexts = ['browser_action'];
}
delete item.click;
chrome.contextMenus.create(item, ignoreChromeError);
}
}
function toggleCheckmark(id, checked) {
chrome.contextMenus.update(id, {checked}, ignoreChromeError);
}
/** Circumvents the bug with disabling check marks in Chrome 62-64 */
async function toggleCheckmarkBugged(id) {
await browser.contextMenus.remove(id).catch(ignoreChromeError);
createContextMenus([id]);
}
function togglePresence(id, checked) {
if (checked) {
createContextMenus([id]);
} else {
chrome.contextMenus.remove(id, ignoreChromeError);
}
}
})();

View File

@ -35,20 +35,9 @@ function createChromeStorageDB() {
}),
};
return {exec};
function exec(method, ...args) {
if (METHODS[method]) {
return METHODS[method](...args)
.then(result => {
if (method === 'putMany' && result.map) {
return result.map(r => ({target: {result: r}}));
}
return {target: {result}};
});
}
return Promise.reject(new Error(`unknown DB method ${method}`));
}
return {
exec: (method, ...args) => METHODS[method](...args),
};
function prepareInc() {
if (INC) return Promise.resolve();

View File

@ -33,18 +33,17 @@ const db = (() => {
case false: break;
default: await testDB();
}
return useIndexedDB();
chromeLocal.setValue(FALLBACK, false);
return dbExecIndexedDB;
}
async function testDB() {
let e = await dbExecIndexedDB('getAllKeys', IDBKeyRange.lowerBound(1), 1);
// throws if result is null
e = e.target.result[0];
e = e[0]; // throws if result is null
const id = `${performance.now()}.${Math.random()}.${Date.now()}`;
await dbExecIndexedDB('put', {id});
e = await dbExecIndexedDB('get', id);
// throws if result or id is null
await dbExecIndexedDB('delete', e.target.result.id);
await dbExecIndexedDB('delete', e.id); // throws if `e` or id is null
}
function useChromeStorage(err) {
@ -56,11 +55,6 @@ const db = (() => {
return createChromeStorageDB().exec;
}
function useIndexedDB() {
chromeLocal.setValue(FALLBACK, false);
return dbExecIndexedDB;
}
async function dbExecIndexedDB(method, ...args) {
const mode = method.startsWith('get') ? 'readonly' : 'readwrite';
const store = (await open()).transaction([STORE], mode).objectStore(STORE);
@ -70,8 +64,9 @@ const db = (() => {
function storeRequest(store, method, ...args) {
return new Promise((resolve, reject) => {
/** @type {IDBRequest} */
const request = store[method](...args);
request.onsuccess = resolve;
request.onsuccess = () => resolve(request.result);
request.onerror = reject;
});
}

View File

@ -1,4 +1,4 @@
/* global prefs debounce iconUtil FIREFOX CHROME VIVALDI tabManager navigatorUtil API_METHODS */
/* global prefs debounce iconUtil FIREFOX CHROME VIVALDI tabManager navigatorUtil API */
/* exported iconManager */
'use strict';
@ -27,7 +27,7 @@ const iconManager = (() => {
refreshAllIcons();
});
Object.assign(API_METHODS, {
Object.assign(API, {
/** @param {(number|string)[]} styleIds
* @param {boolean} [lazyBadge=false] preventing flicker during page load */
updateIconBadge(styleIds, {lazyBadge} = {}) {
@ -53,7 +53,7 @@ const iconManager = (() => {
function onPortDisconnected({sender}) {
if (tabManager.get(sender.tab.id, 'styleIds')) {
API_METHODS.updateIconBadge.call({sender}, [], {lazyBadge: true});
API.updateIconBadge.call({sender}, [], {lazyBadge: true});
}
}

View File

@ -1,75 +1,103 @@
/* global CHROME URLS */
/* exported navigatorUtil */
/* global
CHROME
FIREFOX
ignoreChromeError
msg
URLS
*/
'use strict';
const navigatorUtil = (() => {
const handler = {
urlChange: null,
};
return extendNative({onUrlChange});
(() => {
/** @type {Set<function(data: Object, type: string)>} */
const listeners = new Set();
/** @type {NavigatorUtil} */
const navigatorUtil = window.navigatorUtil = new Proxy({
onUrlChange(fn) {
listeners.add(fn);
},
}, {
get(target, prop) {
return target[prop] ||
(target = chrome.webNavigation[prop]).addListener.bind(target);
},
});
function onUrlChange(fn) {
initUrlChange();
handler.urlChange.push(fn);
navigatorUtil.onCommitted(onNavigation.bind('committed'));
navigatorUtil.onHistoryStateUpdated(onFakeNavigation.bind('history'));
navigatorUtil.onReferenceFragmentUpdated(onFakeNavigation.bind('hash'));
navigatorUtil.onCommitted(runGreasyforkContentScript, {
// expose style version on greasyfork/sleazyfork 1) info page and 2) code page
url: ['greasyfork', 'sleazyfork'].map(host => ({
hostEquals: host + '.org',
urlMatches: '/scripts/\\d+[^/]*(/code)?([?#].*)?$',
})),
});
if (FIREFOX) {
navigatorUtil.onDOMContentLoaded(runMainContentScripts, {
url: [{
urlEquals: 'about:blank',
}],
});
}
function initUrlChange() {
if (handler.urlChange) {
return;
/** @this {string} type */
async function onNavigation(data) {
if (CHROME &&
URLS.chromeProtectsNTP &&
data.url.startsWith('https://www.google.') &&
data.url.includes('/_/chrome/newtab?')) {
// Modern Chrome switched to WebUI NTP so this is obsolete, but there may be exceptions
// TODO: investigate, and maybe use a separate listener for CHROME <= ver
const tab = await browser.tabs.get(data.tabId);
const url = tab.pendingUrl || tab.url;
if (url === 'chrome://newtab/') {
data.url = url;
}
}
handler.urlChange = [];
chrome.webNavigation.onCommitted.addListener(data =>
fixNTPUrl(data)
.then(() => executeCallbacks(handler.urlChange, data, 'committed'))
.catch(console.error)
);
chrome.webNavigation.onHistoryStateUpdated.addListener(data =>
fixNTPUrl(data)
.then(() => executeCallbacks(handler.urlChange, data, 'historyStateUpdated'))
.catch(console.error)
);
chrome.webNavigation.onReferenceFragmentUpdated.addListener(data =>
fixNTPUrl(data)
.then(() => executeCallbacks(handler.urlChange, data, 'referenceFragmentUpdated'))
.catch(console.error)
);
listeners.forEach(fn => fn(data, this));
}
function fixNTPUrl(data) {
if (
!CHROME ||
!URLS.chromeProtectsNTP ||
!data.url.startsWith('https://www.google.') ||
!data.url.includes('/_/chrome/newtab?')
) {
return Promise.resolve();
}
return browser.tabs.get(data.tabId)
.then(tab => {
const url = tab.pendingUrl || tab.url;
if (url === 'chrome://newtab/') {
data.url = url;
}
});
/** @this {string} type */
function onFakeNavigation(data) {
onNavigation.call(this, data);
msg.sendTab(data.tabId, {method: 'urlChanged'}, {frameId: data.frameId})
.catch(msg.ignoreError);
}
function executeCallbacks(callbacks, data, type) {
for (const cb of callbacks) {
cb(data, type);
/** FF misses some about:blank iframes so we inject our content script explicitly */
async function runMainContentScripts({tabId, frameId}) {
if (frameId &&
!await msg.sendTab(tabId, {method: 'ping'}, {frameId}).catch(ignoreChromeError)) {
for (const file of chrome.runtime.getManifest().content_scripts[0].js) {
chrome.tabs.executeScript(tabId, {
frameId,
file,
matchAboutBlank: true,
}, ignoreChromeError);
}
}
}
function extendNative(target) {
return new Proxy(target, {
get: (target, prop) => {
if (target[prop]) {
return target[prop];
}
return chrome.webNavigation[prop].addListener.bind(chrome.webNavigation[prop]);
},
function runGreasyforkContentScript({tabId}) {
chrome.tabs.executeScript(tabId, {
file: '/content/install-hook-greasyfork.js',
runAt: 'document_start',
});
}
})();
/**
* @typedef NavigatorUtil
* @property {NavigatorUtilEvent} onBeforeNavigate
* @property {NavigatorUtilEvent} onCommitted
* @property {NavigatorUtilEvent} onCompleted
* @property {NavigatorUtilEvent} onCreatedNavigationTarget
* @property {NavigatorUtilEvent} onDOMContentLoaded
* @property {NavigatorUtilEvent} onErrorOccurred
* @property {NavigatorUtilEvent} onHistoryStateUpdated
* @property {NavigatorUtilEvent} onReferenceFragmentUpdated
* @property {NavigatorUtilEvent} onTabReplaced
*/
/**
* @typedef {function(cb: function, filters: WebNavigationEventFilter?)} NavigatorUtilEvent
*/

View File

@ -1,3 +1,4 @@
/* global API */
'use strict';
(() => {
@ -40,7 +41,7 @@
.then(res => res.json());
};
window.API_METHODS = Object.assign(window.API_METHODS || {}, {
API.openusercss = {
/**
* This function can be used to retrieve a theme object from the
* GraphQL API, set above
@ -98,5 +99,5 @@
}
}
`),
});
};
})();

View File

@ -1,8 +1,7 @@
/* global
API_METHODS
API
debounce
stringAsRegExp
styleManager
tryRegExp
usercss
*/
@ -50,16 +49,16 @@
* @param {number[]} [params.ids] - if not specified, all styles are searched
* @returns {number[]} - array of matched styles ids
*/
API_METHODS.searchDB = async ({query, mode = 'all', ids}) => {
API.searchDB = async ({query, mode = 'all', ids}) => {
let res = [];
if (mode === 'url' && query) {
res = (await styleManager.getStylesByUrl(query)).map(r => r.data.id);
res = (await API.styles.getByUrl(query)).map(r => r.style.id);
} else if (mode in MODES) {
const modeHandler = MODES[mode];
const m = /^\/(.+?)\/([gimsuy]*)$/.exec(query);
const rx = m && tryRegExp(m[1], m[2]);
const test = rx ? rx.test.bind(rx) : makeTester(query);
res = (await styleManager.getAllStyles())
res = (await API.styles.getAll())
.filter(style =>
(!ids || ids.includes(style.id)) &&
(!query || modeHandler(style, test)))

View File

@ -1,7 +1,16 @@
/* eslint no-eq-null: 0, eqeqeq: [2, "smart"] */
/* global createCache db calcStyleDigest db tryRegExp styleCodeEmpty styleSectionGlobal
getStyleWithNoCode msg prefs sync URLS */
/* exported styleManager */
/* global
API
calcStyleDigest
createCache
db
msg
prefs
stringAsRegExp
styleCodeEmpty
styleSectionGlobal
tryRegExp
URLS
*/
'use strict';
/*
@ -13,41 +22,34 @@ The live preview feature relies on `runtime.connect` and `port.onDisconnect`
to cleanup the temporary code. See /edit/live-preview.js.
*/
/** @type {styleManager} */
const styleManager = (() => {
const preparing = prepare();
/* exported styleManager */
const styleManager = API.styles = (() => {
/* styleId => {
data: styleData,
preview: styleData,
appliesTo: Set<url>
} */
const styles = new Map();
//#region Declarations
const ready = init();
/**
* @typedef StyleMapData
* @property {StyleObj} style
* @property {?StyleObj} [preview]
* @property {Set<string>} appliesTo - urls
*/
/** @type {Map<number,StyleMapData>} */
const dataMap = new Map();
const uuidIndex = new Map();
/* url => {
maybeMatch: Set<styleId>,
sections: Object<styleId => {
id: styleId,
code: Array<String>
}>
} */
/** @typedef {Object<styleId,{id: number, code: string[]}>} StyleSectionsToApply */
/** @type {Map<string,{maybeMatch: Set<styleId>, sections: StyleSectionsToApply}>} */
const cachedStyleForUrl = createCache({
onDeleted: (url, cache) => {
for (const section of Object.values(cache.sections)) {
const style = styles.get(section.id);
if (style) {
style.appliesTo.delete(url);
}
const data = id2data(section.id);
if (data) data.appliesTo.delete(url);
}
},
});
const BAD_MATCHER = {test: () => false};
const compileRe = createCompiler(text => `^(${text})$`);
const compileSloppyRe = createCompiler(text => `^${text}$`);
const compileExclusion = createCompiler(buildExclusion);
const DUMMY_URL = {
hash: '',
host: '',
@ -62,287 +64,256 @@ const styleManager = (() => {
searchParams: new URLSearchParams(),
username: '',
};
const MISSING_PROPS = {
name: style => `ID: ${style.id}`,
_id: () => uuidv4(),
_rev: () => Date.now(),
};
const DELETE_IF_NULL = ['id', 'customName'];
//#endregion
handleLivePreviewConnections();
chrome.runtime.onConnect.addListener(handleLivePreview);
//#region Public surface
// Sorted alphabetically
return {
return Object.assign(/** @namespace styleManager */{
compareRevision,
}, ensurePrepared(/** @namespace styleManager */{
get,
getByUUID,
getSectionsByUrl,
putByUUID,
installStyle,
deleteStyle,
deleteByUUID,
editSave,
findStyle,
importStyle,
importMany,
toggleStyle,
getAllStyles, // used by import-export
getStylesByUrl, // used by popup
styleExists,
addExclusion,
removeExclusion,
addInclusion,
removeInclusion,
}));
function handleLivePreviewConnections() {
chrome.runtime.onConnect.addListener(port => {
if (port.name !== 'livePreview') {
return;
/** @returns {Promise<number>} style id */
async delete(id, reason) {
await ready;
const data = id2data(id);
await db.exec('delete', id);
if (reason !== 'sync') {
API.sync.delete(data.style._id, Date.now());
}
let id;
port.onMessage.addListener(data => {
if (!id) {
id = data.id;
}
const style = styles.get(id);
style.preview = data;
broadcastStyleUpdated(style.preview, 'editPreview');
for (const url of data.appliesTo) {
const cache = cachedStyleForUrl.get(url);
if (cache) delete cache.sections[id];
}
dataMap.delete(id);
uuidIndex.delete(data.style._id);
await msg.broadcast({
method: 'styleDeleted',
style: {id},
});
port.onDisconnect.addListener(() => {
port = null;
if (id) {
const style = styles.get(id);
if (!style) {
// maybe deleted
return;
return id;
},
/** @returns {Promise<number>} style id */
async deleteByUUID(_id, rev) {
await ready;
const id = uuidIndex.get(_id);
const oldDoc = id && id2style(id);
if (oldDoc && compareRevision(oldDoc._rev, rev) <= 0) {
// FIXME: does it make sense to set reason to 'sync' in deleteByUUID?
return API.styles.delete(id, 'sync');
}
},
/** @returns {Promise<StyleObj>} */
async editSave(style) {
await ready;
style = mergeWithMapped(style);
style.updateDate = Date.now();
return handleSave(await saveStyle(style), 'editSave');
},
/** @returns {Promise<?StyleObj>} */
async find(filter) {
await ready;
const filterEntries = Object.entries(filter);
for (const {style} of dataMap.values()) {
if (filterEntries.every(([key, val]) => style[key] === val)) {
return style;
}
}
return null;
},
/** @returns {Promise<StyleObj[]>} */
async getAll() {
await ready;
return Array.from(dataMap.values(), data2style);
},
/** @returns {Promise<StyleObj>} */
async getByUUID(uuid) {
await ready;
return id2style(uuidIndex.get(uuid));
},
/** @returns {Promise<StyleSectionsToApply>} */
async getSectionsByUrl(url, id, isInitialApply) {
await ready;
let cache = cachedStyleForUrl.get(url);
if (!cache) {
cache = {
sections: {},
maybeMatch: new Set(),
};
buildCache(cache, url, dataMap.values());
cachedStyleForUrl.set(url, cache);
} else if (cache.maybeMatch.size) {
buildCache(cache, url, Array.from(cache.maybeMatch, id2data).filter(Boolean));
}
const res = id
? cache.sections[id] ? {[id]: cache.sections[id]} : {}
: cache.sections;
// Avoiding flicker of needlessly applied styles by providing both styles & pref in one API call
return isInitialApply && prefs.get('disableAll')
? Object.assign({disableAll: true}, res)
: res;
},
/** @returns {Promise<StyleObj>} */
async get(id) {
await ready;
return id2style(id);
},
/** @returns {Promise<StylesByUrlResult[]>} */
async getByUrl(url, id = null) {
await ready;
// FIXME: do we want to cache this? Who would like to open popup rapidly
// or search the DB with the same URL?
const result = [];
const styles = id
? [id2style(id)].filter(Boolean)
: Array.from(dataMap.values(), data2style);
const query = createMatchQuery(url);
for (const style of styles) {
let excluded = false;
let sloppy = false;
let sectionMatched = false;
const match = urlMatchStyle(query, style);
// TODO: enable this when the function starts returning false
// if (match === false) {
// continue;
// }
if (match === 'excluded') {
excluded = true;
}
for (const section of style.sections) {
if (styleSectionGlobal(section) && styleCodeEmpty(section.code)) {
continue;
}
style.preview = null;
broadcastStyleUpdated(style.data, 'editPreviewEnd');
}
});
});
}
function escapeRegExp(text) {
// https://github.com/lodash/lodash/blob/0843bd46ef805dd03c0c8d804630804f3ba0ca3c/lodash.js#L152
return text.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
}
function get(id, noCode = false) {
const data = styles.get(id).data;
return noCode ? getStyleWithNoCode(data) : data;
}
function getByUUID(uuid) {
const id = uuidIndex.get(uuid);
if (id) {
return get(id);
}
}
function getAllStyles() {
return [...styles.values()].map(s => s.data);
}
function compareRevision(rev1, rev2) {
return rev1 - rev2;
}
function putByUUID(doc) {
const id = uuidIndex.get(doc._id);
if (id) {
doc.id = id;
} else {
delete doc.id;
}
const oldDoc = id && styles.has(id) && styles.get(id).data;
let diff = -1;
if (oldDoc) {
diff = compareRevision(oldDoc._rev, doc._rev);
if (diff > 0) {
sync.put(oldDoc._id, oldDoc._rev);
return;
}
}
if (diff < 0) {
return db.exec('put', doc)
.then(event => {
doc.id = event.target.result;
uuidIndex.set(doc._id, doc.id);
return handleSave(doc, 'sync');
});
}
}
function toggleStyle(id, enabled) {
const style = styles.get(id);
const data = Object.assign({}, style.data, {enabled});
return saveStyle(data)
.then(newData => handleSave(newData, 'toggle', false))
.then(() => id);
}
// used by install-hook-userstyles.js
function findStyle(filter, noCode = false) {
for (const style of styles.values()) {
if (filterMatch(filter, style.data)) {
return noCode ? getStyleWithNoCode(style.data) : style.data;
}
}
return null;
}
function styleExists(filter) {
return [...styles.values()].some(s => filterMatch(filter, s.data));
}
function filterMatch(filter, target) {
for (const key of Object.keys(filter)) {
if (filter[key] !== target[key]) {
return false;
}
}
return true;
}
function importStyle(data) {
// FIXME: is it a good idea to save the data directly?
return saveStyle(data)
.then(newData => handleSave(newData, 'import'));
}
function importMany(items) {
items.forEach(beforeSave);
return db.exec('putMany', items)
.then(events => {
for (let i = 0; i < items.length; i++) {
afterSave(items[i], events[i].target.result);
}
return Promise.all(items.map(i => handleSave(i, 'import')));
});
}
function installStyle(data, reason = null) {
const style = styles.get(data.id);
if (!style) {
data = Object.assign(createNewStyle(), data);
} else {
data = Object.assign({}, style.data, data);
}
if (!reason) {
reason = style ? 'update' : 'install';
}
let url = !data.url && data.updateUrl;
if (url) {
const usoId = URLS.extractUsoArchiveId(url);
url = usoId && `${URLS.usoArchive}?style=${usoId}` ||
URLS.extractGreasyForkId(url) && url.match(/^.*?\/\d+/)[0];
if (url) data.url = data.installationUrl = url;
}
// FIXME: update updateDate? what about usercss config?
return calcStyleDigest(data)
.then(digest => {
data.originalDigest = digest;
return saveStyle(data);
})
.then(newData => handleSave(newData, reason));
}
function editSave(data) {
const style = styles.get(data.id);
if (style) {
data = Object.assign({}, style.data, data);
} else {
data = Object.assign(createNewStyle(), data);
}
data.updateDate = Date.now();
return saveStyle(data)
.then(newData => handleSave(newData, 'editSave'));
}
function addIncludeExclude(id, rule, type) {
const data = Object.assign({}, styles.get(id).data);
if (!data[type]) {
data[type] = [];
}
if (data[type].includes(rule)) {
throw new Error('The rule already exists');
}
data[type] = data[type].concat([rule]);
return saveStyle(data)
.then(newData => handleSave(newData, 'styleSettings'));
}
function removeIncludeExclude(id, rule, type) {
const data = Object.assign({}, styles.get(id).data);
if (!data[type]) {
return;
}
if (!data[type].includes(rule)) {
return;
}
data[type] = data[type].filter(r => r !== rule);
return saveStyle(data)
.then(newData => handleSave(newData, 'styleSettings'));
}
function addExclusion(id, rule) {
return addIncludeExclude(id, rule, 'exclusions');
}
function removeExclusion(id, rule) {
return removeIncludeExclude(id, rule, 'exclusions');
}
function addInclusion(id, rule) {
return addIncludeExclude(id, rule, 'inclusions');
}
function removeInclusion(id, rule) {
return removeIncludeExclude(id, rule, 'inclusions');
}
function deleteStyle(id, reason) {
const style = styles.get(id);
const rev = Date.now();
return db.exec('delete', id)
.then(() => {
if (reason !== 'sync') {
sync.delete(style.data._id, rev);
}
for (const url of style.appliesTo) {
const cache = cachedStyleForUrl.get(url);
if (cache) {
delete cache.sections[id];
const match = urlMatchSection(query, section);
if (match) {
if (match === 'sloppy') {
sloppy = true;
}
sectionMatched = true;
break;
}
}
styles.delete(id);
uuidIndex.delete(style.data._id);
return msg.broadcast({
method: 'styleDeleted',
style: {id},
});
})
.then(() => id);
if (sectionMatched) {
result.push(/** @namespace StylesByUrlResult */{style, excluded, sloppy});
}
}
return result;
},
/** @returns {Promise<StyleObj[]>} */
async importMany(items) {
await ready;
items.forEach(beforeSave);
const events = await db.exec('putMany', items);
return Promise.all(items.map((item, i) => {
afterSave(item, events[i]);
return handleSave(item, 'import');
}));
},
/** @returns {Promise<StyleObj>} */
async import(data) {
await ready;
return handleSave(await saveStyle(data), 'import');
},
/** @returns {Promise<StyleObj>} */
async install(style, reason = null) {
await ready;
reason = reason || dataMap.has(style.id) ? 'update' : 'install';
style = mergeWithMapped(style);
const url = !style.url && style.updateUrl && (
URLS.extractUsoArchiveInstallUrl(style.updateUrl) ||
URLS.extractGreasyForkInstallUrl(style.updateUrl)
);
if (url) style.url = style.installationUrl = url;
style.originalDigest = await calcStyleDigest(style);
// FIXME: update updateDate? what about usercss config?
return handleSave(await saveStyle(style), reason);
},
/** @returns {Promise<?StyleObj>} */
async putByUUID(doc) {
await ready;
const id = uuidIndex.get(doc._id);
if (id) {
doc.id = id;
} else {
delete doc.id;
}
const oldDoc = id && id2style(id);
let diff = -1;
if (oldDoc) {
diff = compareRevision(oldDoc._rev, doc._rev);
if (diff > 0) {
API.sync.put(oldDoc._id, oldDoc._rev);
return;
}
}
if (diff < 0) {
doc.id = await db.exec('put', doc);
uuidIndex.set(doc._id, doc.id);
return handleSave(doc, 'sync');
}
},
/** @returns {Promise<number>} style id */
async toggle(id, enabled) {
await ready;
const style = Object.assign({}, id2style(id), {enabled});
handleSave(await saveStyle(style), 'toggle', false);
return id;
},
// using bind() to skip step-into when debugging
/** @returns {Promise<StyleObj>} */
addExclusion: addIncludeExclude.bind(null, 'exclusions'),
/** @returns {Promise<StyleObj>} */
addInclusion: addIncludeExclude.bind(null, 'inclusions'),
/** @returns {Promise<?StyleObj>} */
removeExclusion: removeIncludeExclude.bind(null, 'exclusions'),
/** @returns {Promise<?StyleObj>} */
removeInclusion: removeIncludeExclude.bind(null, 'inclusions'),
};
//#endregion
//#region Implementation
/** @returns {StyleMapData} */
function id2data(id) {
return dataMap.get(id);
}
function deleteByUUID(_id, rev) {
const id = uuidIndex.get(_id);
const oldDoc = id && styles.has(id) && styles.get(id).data;
if (oldDoc && compareRevision(oldDoc._rev, rev) <= 0) {
// FIXME: does it make sense to set reason to 'sync' in deleteByUUID?
return deleteStyle(id, 'sync');
}
/** @returns {?StyleObj} */
function id2style(id) {
return (dataMap.get(id) || {}).style;
}
function ensurePrepared(methods) {
const prepared = {};
for (const [name, fn] of Object.entries(methods)) {
prepared[name] = (...args) =>
preparing.then(() => fn(...args));
}
return prepared;
/** @returns {?StyleObj} */
function data2style(data) {
return data && data.style;
}
/** @returns {StyleObj} */
function createNewStyle() {
return {
return /** @namespace StyleObj */{
enabled: true,
updateUrl: null,
md5Url: null,
@ -352,43 +323,105 @@ const styleManager = (() => {
};
}
function broadcastStyleUpdated(data, reason, method = 'styleUpdated', codeIsUpdated = true) {
const style = styles.get(data.id);
/** @returns {void} */
function storeInMap(style) {
dataMap.set(style.id, {
style,
appliesTo: new Set(),
});
}
/** @returns {StyleObj} */
function mergeWithMapped(style) {
return Object.assign({},
id2style(style.id) || createNewStyle(),
style);
}
function handleLivePreview(port) {
if (port.name !== 'livePreview') {
return;
}
let id;
port.onMessage.addListener(style => {
if (!id) id = style.id;
const data = id2data(id);
data.preview = style;
broadcastStyleUpdated(style, 'editPreview');
});
port.onDisconnect.addListener(() => {
port = null;
if (id) {
const data = id2data(id);
if (data) {
data.preview = null;
broadcastStyleUpdated(data.style, 'editPreviewEnd');
}
}
});
}
function compareRevision(rev1, rev2) {
return rev1 - rev2;
}
async function addIncludeExclude(type, id, rule) {
await ready;
const style = Object.assign({}, id2style(id));
const list = style[type] || (style[type] = []);
if (list.includes(rule)) {
throw new Error('The rule already exists');
}
style[type] = list.concat([rule]);
return handleSave(await saveStyle(style), 'styleSettings');
}
async function removeIncludeExclude(type, id, rule) {
await ready;
const style = Object.assign({}, id2style(id));
const list = style[type];
if (!list || !list.includes(rule)) {
return;
}
style[type] = list.filter(r => r !== rule);
return handleSave(await saveStyle(style), 'styleSettings');
}
function broadcastStyleUpdated(style, reason, method = 'styleUpdated', codeIsUpdated = true) {
const {id} = style;
const data = id2data(id);
const excluded = new Set();
const updated = new Set();
for (const [url, cache] of cachedStyleForUrl.entries()) {
if (!style.appliesTo.has(url)) {
cache.maybeMatch.add(data.id);
if (!data.appliesTo.has(url)) {
cache.maybeMatch.add(id);
continue;
}
const code = getAppliedCode(createMatchQuery(url), data);
if (!code) {
excluded.add(url);
delete cache.sections[data.id];
} else {
const code = getAppliedCode(createMatchQuery(url), style);
if (code) {
updated.add(url);
cache.sections[data.id] = {
id: data.id,
code,
};
cache.sections[id] = {id, code};
} else {
excluded.add(url);
delete cache.sections[id];
}
}
style.appliesTo = updated;
data.appliesTo = updated;
return msg.broadcast({
method,
style: {
id: data.id,
md5Url: data.md5Url,
enabled: data.enabled,
},
reason,
codeIsUpdated,
style: {
id,
md5Url: style.md5Url,
enabled: style.enabled,
},
});
}
function beforeSave(style) {
if (!style.name) {
throw new Error('style name is empty');
throw new Error('Style name is empty');
}
for (const key of DELETE_IF_NULL) {
if (style[key] == null) {
@ -407,114 +440,29 @@ const styleManager = (() => {
style.id = newId;
}
uuidIndex.set(style._id, style.id);
sync.put(style._id, style._rev);
API.sync.put(style._id, style._rev);
}
function saveStyle(style) {
async function saveStyle(style) {
beforeSave(style);
return db.exec('put', style)
.then(event => {
afterSave(style, event.target.result);
return style;
});
const newId = await db.exec('put', style);
afterSave(style, newId);
return style;
}
function handleSave(data, reason, codeIsUpdated) {
const style = styles.get(data.id);
let method;
if (!style) {
styles.set(data.id, {
appliesTo: new Set(),
data,
});
method = 'styleAdded';
function handleSave(style, reason, codeIsUpdated) {
const data = id2data(style.id);
const method = data ? 'styleUpdated' : 'styleAdded';
if (!data) {
storeInMap(style);
} else {
style.data = data;
method = 'styleUpdated';
data.style = style;
}
broadcastStyleUpdated(data, reason, method, codeIsUpdated);
return data;
broadcastStyleUpdated(style, reason, method, codeIsUpdated);
return style;
}
// get styles matching a URL, including sloppy regexps and excluded items.
function getStylesByUrl(url, id = null) {
// FIXME: do we want to cache this? Who would like to open popup rapidly
// or search the DB with the same URL?
const result = [];
const datas = !id ? [...styles.values()].map(s => s.data) :
styles.has(id) ? [styles.get(id).data] : [];
const query = createMatchQuery(url);
for (const data of datas) {
let excluded = false;
let sloppy = false;
let sectionMatched = false;
const match = urlMatchStyle(query, data);
// TODO: enable this when the function starts returning false
// if (match === false) {
// continue;
// }
if (match === 'excluded') {
excluded = true;
}
for (const section of data.sections) {
if (styleSectionGlobal(section) && styleCodeEmpty(section.code)) {
continue;
}
const match = urlMatchSection(query, section);
if (match) {
if (match === 'sloppy') {
sloppy = true;
}
sectionMatched = true;
break;
}
}
if (sectionMatched) {
result.push({data, excluded, sloppy});
}
}
return result;
}
function getSectionsByUrl(url, id, isInitialApply) {
let cache = cachedStyleForUrl.get(url);
if (!cache) {
cache = {
sections: {},
maybeMatch: new Set(),
};
buildCache(styles.values());
cachedStyleForUrl.set(url, cache);
} else if (cache.maybeMatch.size) {
buildCache(
[...cache.maybeMatch]
.filter(i => styles.has(i))
.map(i => styles.get(i))
);
}
const res = id
? cache.sections[id] ? {[id]: cache.sections[id]} : {}
: cache.sections;
// Avoiding flicker of needlessly applied styles by providing both styles & pref in one API call
return isInitialApply && prefs.get('disableAll')
? Object.assign({disableAll: true}, res)
: res;
function buildCache(styleList) {
const query = createMatchQuery(url);
for (const {appliesTo, data, preview} of styleList) {
const code = getAppliedCode(query, preview || data);
if (code) {
cache.sections[data.id] = {
id: data.id,
code,
};
appliesTo.add(url);
}
}
}
}
function getAppliedCode(query, data) {
if (urlMatchStyle(query, data) !== true) {
return;
@ -528,60 +476,45 @@ const styleManager = (() => {
return code.length && code;
}
function prepare() {
const ADD_MISSING_PROPS = {
name: style => `ID: ${style.id}`,
_id: () => uuidv4(),
_rev: () => Date.now(),
};
return db.exec('getAll')
.then(event => event.target.result || [])
.then(styleList => {
// setup missing _id, _rev
const updated = [];
for (const style of styleList) {
if (addMissingProperties(style)) {
updated.push(style);
}
}
if (updated.length) {
return db.exec('putMany', updated)
.then(() => styleList);
}
return styleList;
})
.then(styleList => {
for (const style of styleList) {
fixUsoMd5Issue(style);
styles.set(style.id, {
appliesTo: new Set(),
data: style,
});
uuidIndex.set(style._id, style.id);
}
});
function addMissingProperties(style) {
let touched = false;
for (const key in ADD_MISSING_PROPS) {
if (!style[key]) {
style[key] = ADD_MISSING_PROPS[key](style);
touched = true;
}
}
// upgrade the old way of customizing local names
const {originalName} = style;
if (originalName) {
touched = true;
if (originalName !== style.name) {
style.customName = style.name;
style.name = originalName;
}
delete style.originalName;
}
return touched;
async function init() {
const styles = await db.exec('getAll') || [];
const updated = styles.filter(style =>
addMissingProps(style) +
addCustomName(style));
if (updated.length) {
await db.exec('putMany', updated);
}
for (const style of styles) {
fixUsoMd5Issue(style);
storeInMap(style);
uuidIndex.set(style._id, style.id);
}
}
function addMissingProps(style) {
let res = 0;
for (const key in MISSING_PROPS) {
if (!style[key]) {
style[key] = MISSING_PROPS[key](style);
res = 1;
}
}
return res;
}
/** Upgrades the old way of customizing local names */
function addCustomName(style) {
let res = 0;
const {originalName} = style;
if (originalName) {
res = 1;
if (originalName !== style.name) {
style.customName = style.name;
style.name = originalName;
}
delete style.originalName;
}
return res;
}
function urlMatchStyle(query, style) {
@ -652,7 +585,8 @@ const styleManager = (() => {
}
function compileGlob(text) {
return escapeRegExp(text).replace(/\\\\\\\*|\\\*/g, m => m.length > 2 ? m : '.*');
return stringAsRegExp(text, '', true)
.replace(/\\\\\\\*|\\\*/g, m => m.length > 2 ? m : '.*');
}
function buildExclusion(text) {
@ -706,6 +640,18 @@ const styleManager = (() => {
};
}
function buildCache(cache, url, styleList) {
const query = createMatchQuery(url);
for (const {style, appliesTo, preview} of styleList) {
const code = getAppliedCode(query, preview || style);
if (code) {
const id = style.id;
cache.sections[id] = {id, code};
appliesTo.add(url);
}
}
}
function createURL(url) {
try {
return new URL(url);
@ -726,4 +672,5 @@ const styleManager = (() => {
function hex4dashed(num, i) {
return (num + 0x10000).toString(16).slice(-4) + (i >= 1 && i <= 4 ? '-' : '');
}
//#endregion
})();

View File

@ -1,7 +1,7 @@
/* global API_METHODS styleManager CHROME prefs */
/* global API CHROME prefs */
'use strict';
API_METHODS.styleViaAPI = !CHROME && (() => {
API.styleViaAPI = !CHROME && (() => {
const ACTIONS = {
styleApply,
styleDeleted,
@ -37,7 +37,7 @@ API_METHODS.styleViaAPI = !CHROME && (() => {
throw new Error('we do not count styles for frames');
}
const {frameStyles} = getCachedData(tab.id, frameId);
API_METHODS.updateIconBadge.call({sender}, Object.keys(frameStyles));
API.updateIconBadge.call({sender}, Object.keys(frameStyles));
}
function styleApply({id = null, ignoreUrlCheck = false}, {tab, frameId, url}) {
@ -48,7 +48,7 @@ API_METHODS.styleViaAPI = !CHROME && (() => {
if (id === null && !ignoreUrlCheck && frameStyles.url === url) {
return NOP;
}
return styleManager.getSectionsByUrl(url, id).then(sections => {
return API.styles.getSectionsByUrl(url, id).then(sections => {
const tasks = [];
for (const section of Object.values(sections)) {
const styleId = section.id;

View File

@ -1,4 +1,8 @@
/* global API CHROME prefs */
/* global
API
CHROME
prefs
*/
'use strict';
// eslint-disable-next-line no-unused-expressions
@ -67,14 +71,14 @@ CHROME && (async () => {
}
/** @param {chrome.webRequest.WebRequestBodyDetails} req */
function prepareStyles(req) {
API.getSectionsByUrl(req.url).then(sections => {
if (Object.keys(sections).length) {
stylesToPass[req.requestId] = !enabled.xhr ? true :
URL.createObjectURL(new Blob([JSON.stringify(sections)])).slice(blobUrlPrefix.length);
setTimeout(cleanUp, 600e3, req.requestId);
}
});
async function prepareStyles(req) {
const sections = await API.styles.getSectionsByUrl(req.url);
if (Object.keys(sections).length) {
stylesToPass[req.requestId] = !enabled.xhr ? true :
URL.createObjectURL(new Blob([JSON.stringify(sections)]))
.slice(blobUrlPrefix.length);
setTimeout(cleanUp, 600e3, req.requestId);
}
}
/** @param {chrome.webRequest.WebResponseHeadersDetails} req */

View File

@ -1,13 +1,23 @@
/* global dbToCloud styleManager chromeLocal prefs tokenManager msg */
/* global
API
chromeLocal
dbToCloud
msg
prefs
styleManager
tokenManager
*/
/* exported sync */
'use strict';
const sync = (() => {
const sync = API.sync = (() => {
const SYNC_DELAY = 1; // minutes
const SYNC_INTERVAL = 30; // minutes
/** @typedef API.sync.Status */
const status = {
/** @type {'connected'|'connecting'|'disconnected'|'disconnecting'} */
state: 'disconnected',
syncing: false,
progress: null,
@ -18,21 +28,30 @@ const sync = (() => {
let currentDrive;
const ctrl = dbToCloud.dbToCloud({
onGet(id) {
return styleManager.getByUUID(id);
return API.styles.getByUUID(id);
},
onPut(doc) {
return styleManager.putByUUID(doc);
return API.styles.putByUUID(doc);
},
onDelete(id, rev) {
return styleManager.deleteByUUID(id, rev);
return API.styles.deleteByUUID(id, rev);
},
onFirstSync() {
return styleManager.getAllStyles()
.then(styles => {
styles.forEach(i => ctrl.put(i._id, i._rev));
});
async onFirstSync() {
for (const i of await API.styles.getAll()) {
ctrl.put(i._id, i._rev);
}
},
onProgress(e) {
if (e.phase === 'start') {
status.syncing = true;
} else if (e.phase === 'end') {
status.syncing = false;
status.progress = null;
} else {
status.progress = e;
}
emitStatusChange();
},
onProgress,
compareRevision(a, b) {
return styleManager.compareRevision(a, b);
},
@ -46,55 +65,126 @@ const sync = (() => {
},
});
const initializing = prefs.initializing.then(() => {
prefs.subscribe(['sync.enabled'], onPrefChange);
onPrefChange(null, prefs.get('sync.enabled'));
const ready = prefs.initializing.then(() => {
prefs.subscribe('sync.enabled',
(_, val) => val === 'none'
? sync.stop()
: sync.start(val, true),
{now: true});
});
chrome.alarms.onAlarm.addListener(info => {
if (info.name === 'syncNow') {
syncNow().catch(console.error);
sync.syncNow();
}
});
return Object.assign({
getStatus: () => status,
}, ensurePrepared({
start,
stop,
put: (...args) => {
if (!currentDrive) return;
schedule();
return ctrl.put(...args);
},
delete: (...args) => {
// Sorted alphabetically
return {
async delete(...args) {
await ready;
if (!currentDrive) return;
schedule();
return ctrl.delete(...args);
},
syncNow,
login,
}));
function ensurePrepared(obj) {
return Object.entries(obj).reduce((o, [key, fn]) => {
o[key] = (...args) =>
initializing.then(() => fn(...args));
return o;
}, {});
}
/**
* @returns {Promise<API.sync.Status>}
*/
async getStatus() {
return status;
},
function onProgress(e) {
if (e.phase === 'start') {
status.syncing = true;
} else if (e.phase === 'end') {
status.syncing = false;
status.progress = null;
} else {
status.progress = e;
}
emitStatusChange();
}
async login(name = prefs.get('sync.enabled')) {
await ready;
try {
await tokenManager.getToken(name, true);
} catch (err) {
if (/Authorization page could not be loaded/i.test(err.message)) {
// FIXME: Chrome always fails at the first login so we try again
await tokenManager.getToken(name);
}
throw err;
}
status.login = true;
emitStatusChange();
},
async put(...args) {
await ready;
if (!currentDrive) return;
schedule();
return ctrl.put(...args);
},
async start(name, fromPref = false) {
await ready;
if (currentDrive) {
return;
}
currentDrive = getDrive(name);
ctrl.use(currentDrive);
status.state = 'connecting';
status.currentDriveName = currentDrive.name;
status.login = true;
emitStatusChange();
try {
if (!fromPref) {
await sync.login(name).catch(handle401Error);
}
await sync.syncNow();
status.errorMessage = null;
} catch (err) {
status.errorMessage = err.message;
// FIXME: should we move this logic to options.js?
if (!fromPref) {
console.error(err);
return sync.stop();
}
}
prefs.set('sync.enabled', name);
status.state = 'connected';
schedule(SYNC_INTERVAL);
emitStatusChange();
},
async stop() {
await ready;
if (!currentDrive) {
return;
}
chrome.alarms.clear('syncNow');
status.state = 'disconnecting';
emitStatusChange();
try {
await ctrl.stop();
await tokenManager.revokeToken(currentDrive.name);
await chromeLocal.remove(`sync/state/${currentDrive.name}`);
} catch (e) {
}
currentDrive = null;
prefs.set('sync.enabled', 'none');
status.state = 'disconnected';
status.currentDriveName = null;
status.login = false;
emitStatusChange();
},
async syncNow() {
await ready;
if (!currentDrive) {
return Promise.reject(new Error('cannot sync when disconnected'));
}
try {
await (ctrl.isInit() ? ctrl.syncNow() : ctrl.start()).catch(handle401Error);
status.errorMessage = null;
} catch (err) {
status.errorMessage = err.message;
}
emitStatusChange();
},
};
function schedule(delay = SYNC_DELAY) {
chrome.alarms.create('syncNow', {
@ -103,106 +193,25 @@ const sync = (() => {
});
}
function onPrefChange(key, value) {
if (value === 'none') {
stop().catch(console.error);
} else {
start(value, true).catch(console.error);
}
}
function withFinally(p, cleanup) {
return p.then(
result => {
cleanup(undefined, result);
return result;
},
err => {
cleanup(err);
throw err;
}
);
}
function syncNow() {
if (!currentDrive) {
return Promise.reject(new Error('cannot sync when disconnected'));
}
return withFinally(
(ctrl.isInit() ? ctrl.syncNow() : ctrl.start())
.catch(handle401Error),
err => {
status.errorMessage = err ? err.message : null;
emitStatusChange();
}
);
}
function handle401Error(err) {
async function handle401Error(err) {
let emit;
if (err.code === 401) {
return tokenManager.revokeToken(currentDrive.name)
.catch(console.error)
.then(() => {
status.login = false;
emitStatusChange();
throw err;
});
await tokenManager.revokeToken(currentDrive.name).catch(console.error);
emit = true;
} else if (/User interaction required|Requires user interaction/i.test(err.message)) {
emit = true;
}
if (/User interaction required|Requires user interaction/i.test(err.message)) {
if (emit) {
status.login = false;
emitStatusChange();
}
throw err;
return Promise.reject(err);
}
function emitStatusChange() {
msg.broadcastExtension({method: 'syncStatusUpdate', status});
}
function login(name = prefs.get('sync.enabled')) {
return tokenManager.getToken(name, true)
.catch(err => {
if (/Authorization page could not be loaded/i.test(err.message)) {
// FIXME: Chrome always fails at the first login so we try again
return tokenManager.getToken(name);
}
throw err;
})
.then(() => {
status.login = true;
emitStatusChange();
});
}
function start(name, fromPref = false) {
if (currentDrive) {
return Promise.resolve();
}
currentDrive = getDrive(name);
ctrl.use(currentDrive);
status.state = 'connecting';
status.currentDriveName = currentDrive.name;
status.login = true;
emitStatusChange();
return withFinally(
(fromPref ? Promise.resolve() : login(name))
.catch(handle401Error)
.then(() => syncNow()),
err => {
status.errorMessage = err ? err.message : null;
// FIXME: should we move this logic to options.js?
if (err && !fromPref) {
console.error(err);
return stop();
}
prefs.set('sync.enabled', name);
schedule(SYNC_INTERVAL);
status.state = 'connected';
emitStatusChange();
}
);
}
function getDrive(name) {
if (name === 'dropbox' || name === 'google' || name === 'onedrive') {
return dbToCloud.drive[name]({
@ -211,26 +220,4 @@ const sync = (() => {
}
throw new Error(`unknown cloud name: ${name}`);
}
function stop() {
if (!currentDrive) {
return Promise.resolve();
}
chrome.alarms.clear('syncNow');
status.state = 'disconnecting';
emitStatusChange();
return withFinally(
ctrl.stop()
.then(() => tokenManager.revokeToken(currentDrive.name))
.then(() => chromeLocal.remove(`sync/state/${currentDrive.name}`)),
() => {
currentDrive = null;
prefs.set('sync.enabled', 'none');
status.state = 'disconnected';
status.currentDriveName = null;
status.login = false;
emitStatusChange();
}
);
}
})();

View File

@ -1,27 +1,23 @@
/* global
API_METHODS
API
calcStyleDigest
chromeLocal
debounce
download
getStyleWithNoCode
ignoreChromeError
prefs
semverCompare
styleJSONseemsValid
styleManager
styleSectionsEqual
tryJSONparse
usercss
*/
'use strict';
(() => {
const STATES = {
const STATES = /** @namespace UpdaterStates */{
UPDATED: 'updated',
SKIPPED: 'skipped',
UNREACHABLE: 'server unreachable',
// details for SKIPPED status
EDITED: 'locally edited',
MAYBE_EDITED: 'may be locally edited',
@ -32,20 +28,22 @@
ERROR_JSON: 'error: JSON is invalid',
ERROR_VERSION: 'error: version is older than installed style',
};
const ALARM_NAME = 'scheduledUpdate';
const MIN_INTERVAL_MS = 60e3;
const RETRY_ERRORS = [
503, // service unavailable
429, // too many requests
];
let lastUpdateTime;
let checkingAll = false;
let logQueue = [];
let logLastWriteTime = 0;
const retrying = new Set();
API_METHODS.updateCheckAll = checkAllStyles;
API_METHODS.updateCheck = checkStyle;
API_METHODS.getUpdaterStates = () => STATES;
API.updater = {
checkAllStyles,
checkStyle,
getStates: () => STATES,
};
chromeLocal.getValue('lastUpdateTime').then(val => {
lastUpdateTime = val || Date.now();
@ -53,191 +51,159 @@
chrome.alarms.onAlarm.addListener(onAlarm);
});
return {checkAllStyles, checkStyle, STATES};
function checkAllStyles({
async function checkAllStyles({
save = true,
ignoreDigest,
observe,
} = {}) {
resetInterval();
checkingAll = true;
retrying.clear();
const port = observe && chrome.runtime.connect({name: 'updater'});
return styleManager.getAllStyles().then(styles => {
styles = styles.filter(style => style.updateUrl);
if (port) port.postMessage({count: styles.length});
log('');
log(`${save ? 'Scheduled' : 'Manual'} update check for ${styles.length} styles`);
return Promise.all(
styles.map(style =>
checkStyle({style, port, save, ignoreDigest})));
}).then(() => {
if (port) port.postMessage({done: true});
if (port) port.disconnect();
log('');
checkingAll = false;
retrying.clear();
});
const styles = (await API.styles.getAll())
.filter(style => style.updateUrl);
if (port) port.postMessage({count: styles.length});
log('');
log(`${save ? 'Scheduled' : 'Manual'} update check for ${styles.length} styles`);
await Promise.all(
styles.map(style =>
checkStyle({style, port, save, ignoreDigest})));
if (port) port.postMessage({done: true});
if (port) port.disconnect();
log('');
checkingAll = false;
}
function checkStyle({
id,
style,
port,
save = true,
ignoreDigest,
}) {
/*
Original style digests are calculated in these cases:
* style is installed or updated from server
* style is checked for an update and its code is equal to the server code
/**
* @param {{
id?: number
style?: StyleObj
port?: chrome.runtime.Port
save?: boolean = true
ignoreDigest?: boolean
}} opts
* @returns {{
style: StyleObj
updated?: boolean
error?: any
STATES: UpdaterStates
}}
Update check proceeds in these cases:
* style has the original digest and it's equal to the current digest
* [ignoreDigest: true] style doesn't yet have the original digest but we ignore it
* [ignoreDigest: none/false] style doesn't yet have the original digest
so we compare the code to the server code and if it's the same we save the digest,
otherwise we skip the style and report MAYBE_EDITED status
Original style digests are calculated in these cases:
* style is installed or updated from server
* non-usercss style is checked for an update and styleSectionsEqual considers it unchanged
'ignoreDigest' option is set on the second manual individual update check on the manage page.
*/
return fetchStyle()
.then(() => {
if (!ignoreDigest) {
return calcStyleDigest(style)
.then(checkIfEdited);
}
})
.then(() => {
if (style.usercssData) {
return maybeUpdateUsercss();
}
return maybeUpdateUSO();
})
.then(maybeSave)
.then(reportSuccess)
.catch(reportFailure);
Update check proceeds in these cases:
* style has the original digest and it's equal to the current digest
* [ignoreDigest: true] style doesn't yet have the original digest but we ignore it
* [ignoreDigest: none/false] style doesn't yet have the original digest
so we compare the code to the server code and if it's the same we save the digest,
otherwise we skip the style and report MAYBE_EDITED status
function fetchStyle() {
if (style) {
return Promise.resolve();
}
return styleManager.get(id)
.then(style_ => {
style = style_;
});
'ignoreDigest' option is set on the second manual individual update check on the manage page.
*/
async function checkStyle(opts) {
const {
id,
style = await API.styles.get(id),
ignoreDigest,
port,
save,
} = opts;
const ucd = style.usercssData;
let res, state;
try {
await checkIfEdited();
res = {
style: await (ucd ? updateUsercss : updateUSO)().then(maybeSave),
updated: true,
};
state = STATES.UPDATED;
} catch (err) {
const error = err === 0 && STATES.UNREACHABLE ||
err && err.message ||
err;
res = {error, style, STATES};
state = `${STATES.SKIPPED} (${error})`;
}
log(`${state} #${style.id} ${style.customName || style.name}`);
if (port) port.postMessage(res);
return res;
function reportSuccess(saved) {
log(STATES.UPDATED + ` #${style.id} ${style.customName || style.name}`);
const info = {updated: true, style: saved};
if (port) port.postMessage(info);
return info;
}
function reportFailure(error) {
if ((
error === 503 || // Service Unavailable
error === 429 // Too Many Requests
) && !retrying.has(id)) {
retrying.add(id);
return new Promise(resolve => {
setTimeout(() => {
resolve(checkStyle({id, style, port, save, ignoreDigest}));
}, 1000);
});
}
error = error === 0 ? 'server unreachable' : error;
// UserCSS metadata error returns an object; e.g. "Invalid @var color..."
if (typeof error === 'object' && error.message) {
error = error.message;
}
log(STATES.SKIPPED + ` (${error}) #${style.id} ${style.customName || style.name}`);
const info = {error, STATES, style: getStyleWithNoCode(style)};
if (port) port.postMessage(info);
return info;
}
function checkIfEdited(digest) {
if (style.originalDigest && style.originalDigest !== digest) {
async function checkIfEdited() {
if (!ignoreDigest &&
style.originalDigest &&
style.originalDigest !== await calcStyleDigest(style)) {
return Promise.reject(STATES.EDITED);
}
}
function maybeUpdateUSO() {
return download(style.md5Url).then(md5 => {
if (!md5 || md5.length !== 32) {
return Promise.reject(STATES.ERROR_MD5);
}
if (md5 === style.originalMd5 && style.originalDigest && !ignoreDigest) {
return Promise.reject(STATES.SAME_MD5);
}
// USO can't handle POST requests for style json
return download(style.updateUrl, {body: null})
.then(text => {
const style = tryJSONparse(text);
if (style) {
// USO may not provide a correctly updated originalMd5 (#555)
style.originalMd5 = md5;
}
return style;
});
});
}
function maybeUpdateUsercss() {
// TODO: when sourceCode is > 100kB use http range request(s) for version check
return download(style.updateUrl).then(text =>
usercss.buildMeta(text).then(json => {
const {usercssData: {version}} = style;
const {usercssData: {version: newVersion}} = json;
switch (Math.sign(semverCompare(version, newVersion))) {
case 0:
// re-install is invalid in a soft upgrade
if (!ignoreDigest) {
const sameCode = text === style.sourceCode;
return Promise.reject(sameCode ? STATES.SAME_CODE : STATES.SAME_VERSION);
}
break;
case 1:
// downgrade is always invalid
return Promise.reject(STATES.ERROR_VERSION);
}
return usercss.buildCode(json);
})
);
}
function maybeSave(json = {}) {
// usercss is already validated while building
if (!json.usercssData && !styleJSONseemsValid(json)) {
async function updateUSO() {
const md5 = await tryDownload(style.md5Url);
if (!md5 || md5.length !== 32) {
return Promise.reject(STATES.ERROR_MD5);
}
if (md5 === style.originalMd5 && style.originalDigest && !ignoreDigest) {
return Promise.reject(STATES.SAME_MD5);
}
const json = await tryDownload(style.updateUrl, {responseType: 'json'});
if (!styleJSONseemsValid(json)) {
return Promise.reject(STATES.ERROR_JSON);
}
// USO may not provide a correctly updated originalMd5 (#555)
json.originalMd5 = md5;
return json;
}
async function updateUsercss() {
// TODO: when sourceCode is > 100kB use http range request(s) for version check
const text = await tryDownload(style.updateUrl);
const json = await usercss.buildMeta(text);
const delta = semverCompare(json.usercssData.version, ucd.version);
if (!delta && !ignoreDigest) {
// re-install is invalid in a soft upgrade
const sameCode = text === style.sourceCode;
return Promise.reject(sameCode ? STATES.SAME_CODE : STATES.SAME_VERSION);
}
if (delta < 0) {
// downgrade is always invalid
return Promise.reject(STATES.ERROR_VERSION);
}
return usercss.buildCode(json);
}
async function maybeSave(json) {
json.id = style.id;
json.updateDate = Date.now();
// keep current state
delete json.customName;
delete json.enabled;
const newStyle = Object.assign({}, style, json);
if (!style.usercssData && styleSectionsEqual(json, style)) {
// update digest even if save === false as there might be just a space added etc.
return styleManager.installStyle(newStyle)
.then(saved => {
style.originalDigest = saved.originalDigest;
return Promise.reject(STATES.SAME_CODE);
});
// update digest even if save === false as there might be just a space added etc.
if (!ucd && styleSectionsEqual(json, style)) {
style.originalDigest = (await API.styles.install(newStyle)).originalDigest;
return Promise.reject(STATES.SAME_CODE);
}
if (!style.originalDigest && !ignoreDigest) {
return Promise.reject(STATES.MAYBE_EDITED);
}
return !save ? newStyle :
(ucd ? API.usercss : API.styles).install(newStyle);
}
return save ?
API_METHODS[json.usercssData ? 'installUsercss' : 'installStyle'](newStyle) :
newStyle;
async function tryDownload(url, params) {
let {retryDelay = 1000} = opts;
while (true) {
try {
return await download(url, params);
} catch (code) {
if (!RETRY_ERRORS.includes(code) ||
retryDelay > MIN_INTERVAL_MS) {
return Promise.reject(code);
}
}
retryDelay *= 1.25;
await new Promise(resolve => setTimeout(resolve, retryDelay));
}
}
}

View File

@ -0,0 +1,81 @@
/* global
API
deepCopy
usercss
*/
'use strict';
API.usercss = {
async build({
styleId,
sourceCode,
vars,
checkDup,
metaOnly,
assignVars,
}) {
let style = await usercss.buildMeta(sourceCode);
const dup = (checkDup || assignVars) &&
await API.usercss.find(styleId ? {id: styleId} : style);
if (!metaOnly) {
if (vars || assignVars) {
await usercss.assignVars(style, vars ? {usercssData: {vars}} : dup);
}
style = await usercss.buildCode(style);
}
return {style, dup};
},
async buildMeta(style) {
if (style.usercssData) {
return style;
}
// allow sourceCode to be normalized
const {sourceCode} = style;
delete style.sourceCode;
return Object.assign(await usercss.buildMeta(sourceCode), style);
},
async configVars(id, vars) {
let style = deepCopy(await API.styles.get(id));
style.usercssData.vars = vars;
style = await usercss.buildCode(style);
style = await API.styles.install(style, 'config');
return style.usercssData.vars;
},
async editSave(style) {
return API.styles.editSave(await API.usercss.parse(style));
},
async find(styleOrData) {
if (styleOrData.id) {
return API.styles.get(styleOrData.id);
}
const {name, namespace} = styleOrData.usercssData || styleOrData;
for (const dup of await API.styles.getAll()) {
const data = dup.usercssData;
if (data &&
data.name === name &&
data.namespace === namespace) {
return dup;
}
}
},
async install(style) {
return API.styles.install(await API.usercss.parse(style));
},
async parse(style) {
style = await API.usercss.buildMeta(style);
// preserve style.vars during update
const dup = await API.usercss.find(style);
if (dup) {
style.id = dup.id;
await usercss.assignVars(style, dup);
}
return usercss.buildCode(style);
},
};

View File

@ -1,132 +0,0 @@
/* global API_METHODS usercss styleManager deepCopy */
/* exported usercssHelper */
'use strict';
const usercssHelper = (() => {
API_METHODS.installUsercss = installUsercss;
API_METHODS.editSaveUsercss = editSaveUsercss;
API_METHODS.configUsercssVars = configUsercssVars;
API_METHODS.buildUsercss = build;
API_METHODS.buildUsercssMeta = buildMeta;
API_METHODS.findUsercss = find;
function buildMeta(style) {
if (style.usercssData) {
return Promise.resolve(style);
}
// allow sourceCode to be normalized
const {sourceCode} = style;
delete style.sourceCode;
return usercss.buildMeta(sourceCode)
.then(newStyle => Object.assign(newStyle, style));
}
function assignVars(style) {
return find(style)
.then(dup => {
if (dup) {
style.id = dup.id;
// preserve style.vars during update
return usercss.assignVars(style, dup)
.then(() => style);
}
return style;
});
}
/**
* Parse the source, find the duplication, and build sections with variables
* @param _
* @param {String} _.sourceCode
* @param {Boolean=} _.checkDup
* @param {Boolean=} _.metaOnly
* @param {Object} _.vars
* @param {Boolean=} _.assignVars
* @returns {Promise<{style, dup:Boolean?}>}
*/
function build({
styleId,
sourceCode,
checkDup,
metaOnly,
vars,
assignVars = false,
}) {
return usercss.buildMeta(sourceCode)
.then(style => {
const findDup = checkDup || assignVars ?
find(styleId ? {id: styleId} : style) : Promise.resolve();
return Promise.all([
metaOnly ? style : doBuild(style, findDup),
findDup,
]);
})
.then(([style, dup]) => ({style, dup}));
function doBuild(style, findDup) {
if (vars || assignVars) {
const getOld = vars ? Promise.resolve({usercssData: {vars}}) : findDup;
return getOld
.then(oldStyle => usercss.assignVars(style, oldStyle))
.then(() => usercss.buildCode(style));
}
return usercss.buildCode(style);
}
}
// Build the style within aditional properties then inherit variable values
// from the old style.
function parse(style) {
return buildMeta(style)
.then(buildMeta)
.then(assignVars)
.then(usercss.buildCode);
}
// FIXME: simplify this to `installUsercss(sourceCode)`?
function installUsercss(style) {
return parse(style)
.then(styleManager.installStyle);
}
// FIXME: simplify this to `editSaveUsercss({sourceCode, exclusions})`?
function editSaveUsercss(style) {
return parse(style)
.then(styleManager.editSave);
}
function configUsercssVars(id, vars) {
return styleManager.get(id)
.then(style => {
const newStyle = deepCopy(style);
newStyle.usercssData.vars = vars;
return usercss.buildCode(newStyle);
})
.then(style => styleManager.installStyle(style, 'config'))
.then(style => style.usercssData.vars);
}
/**
* @param {Style|{name:string, namespace:string}} styleOrData
* @returns {Style}
*/
function find(styleOrData) {
if (styleOrData.id) {
return styleManager.get(styleOrData.id);
}
const {name, namespace} = styleOrData.usercssData || styleOrData;
return styleManager.getAllStyles().then(styleList => {
for (const dup of styleList) {
const data = dup.usercssData;
if (!data) continue;
if (data.name === name &&
data.namespace === namespace) {
return dup;
}
}
});
}
})();

View File

@ -1,5 +1,5 @@
/* global
API_METHODS
API
download
openURL
tabManager
@ -25,7 +25,7 @@
isContentTypeText((await fetch(url, {method: 'HEAD'})).headers.get('content-type'))
) && download(url);
API_METHODS.getUsercssInstallCode = url => {
API.usercss.getInstallCode = url => {
// when the installer tab is reloaded after the cache is expired, this will throw intentionally
const {code, timer} = installCodeCache[url];
clearInstallCode(url);

View File

@ -60,7 +60,7 @@ self.INJECTED !== 1 && (() => {
await API.styleViaAPI({method: 'styleApply'});
} else {
const styles = chrome.app && !chrome.tabs && getStylesViaXhr() ||
await API.getSectionsByUrl(getMatchUrl(), null, true);
await API.styles.getSectionsByUrl(getMatchUrl(), null, true);
if (styles.disableAll) {
delete styles.disableAll;
styleInjector.toggle(false);
@ -117,7 +117,7 @@ self.INJECTED !== 1 && (() => {
case 'styleUpdated':
if (request.style.enabled) {
API.getSectionsByUrl(getMatchUrl(), request.style.id)
API.styles.getSectionsByUrl(getMatchUrl(), request.style.id)
.then(sections => {
if (!sections[request.style.id]) {
styleInjector.remove(request.style.id);
@ -132,13 +132,13 @@ self.INJECTED !== 1 && (() => {
case 'styleAdded':
if (request.style.enabled) {
API.getSectionsByUrl(getMatchUrl(), request.style.id)
API.styles.getSectionsByUrl(getMatchUrl(), request.style.id)
.then(styleInjector.apply);
}
break;
case 'urlChanged':
API.getSectionsByUrl(getMatchUrl())
API.styles.getSectionsByUrl(getMatchUrl())
.then(styleInjector.replace);
break;

View File

@ -13,7 +13,7 @@ if (window.INJECTED_GREASYFORK !== 1) {
e.data.name &&
e.data.type === 'style-version-query') {
removeEventListener('message', onMessage);
const style = await API.findUsercss(e.data) || {};
const style = await API.usercss.find(e.data) || {};
const {version} = style.usercssData || {};
postMessage({type: 'style-version', version}, '*');
}

View File

@ -34,7 +34,7 @@
&& event.data.type === 'ouc-is-installed'
&& allowedOrigins.includes(event.origin)
) {
API.findUsercss({
API.usercss.find({
name: event.data.name,
namespace: event.data.namespace,
}).then(style => {
@ -129,7 +129,7 @@
&& event.data.type === 'ouc-install-usercss'
&& allowedOrigins.includes(event.origin)
) {
API.installUsercss({
API.usercss.install({
name: event.data.title,
sourceCode: event.data.code,
}).then(style => {

View File

@ -1,19 +1,21 @@
'use strict';
// preventing reregistration if reinjected by tabs.executeScript for whatever reason, just in case
if (typeof self.oldCode !== 'string') {
self.oldCode = (document.querySelector('body > pre') || document.body).textContent;
if (typeof window.oldCode !== 'string') {
window.oldCode = (document.querySelector('body > pre') || document.body).textContent;
chrome.runtime.onConnect.addListener(port => {
if (port.name !== 'downloadSelf') return;
port.onMessage.addListener(({id, force}) => {
fetch(location.href, {mode: 'same-origin'})
.then(r => r.text())
.then(code => ({id, code: force || code !== self.oldCode ? code : null}))
.catch(error => ({id, error: error.message || `${error}`}))
.then(msg => {
port.postMessage(msg);
if (msg.code != null) self.oldCode = msg.code;
});
port.onMessage.addListener(async ({id, force}) => {
const msg = {id};
try {
const code = await (await fetch(location.href, {mode: 'same-origin'})).text();
if (code !== window.oldCode || force) {
msg.code = window.oldCode = code;
}
} catch (error) {
msg.error = error.message || `${error}`;
}
port.postMessage(msg);
});
// FF keeps content scripts connected on navigation https://github.com/openstyles/stylus/issues/864
addEventListener('pagehide', () => port.disconnect(), {once: true});
@ -21,4 +23,4 @@ if (typeof self.oldCode !== 'string') {
}
// passing the result to tabs.executeScript
self.oldCode; // eslint-disable-line no-unused-expressions
window.oldCode; // eslint-disable-line no-unused-expressions

View File

@ -24,7 +24,7 @@
let currentMd5;
const md5Url = getMeta('stylish-md5-url') || `https://update.userstyles.org/${styleId}.md5`;
Promise.all([
API.findStyle({md5Url}),
API.styles.find({md5Url}),
getResource(md5Url),
onDOMready(),
]).then(checkUpdatability);
@ -154,9 +154,9 @@
function doInstall() {
let oldStyle;
return API.findStyle({
return API.styles.find({
md5Url: getMeta('stylish-md5-url') || location.href,
}, true)
})
.then(_oldStyle => {
oldStyle = _oldStyle;
return oldStyle ?
@ -187,7 +187,7 @@
return;
}
// Update originalMd5 since USO changed it (2018-11-11) to NOT match the current md5
return API.installStyle(Object.assign(json, addProps, {originalMd5: currentMd5}))
return API.styles.install(Object.assign(json, addProps, {originalMd5: currentMd5}))
.then(style => {
if (!isNew && style.updateUrl.includes('?')) {
enableUpdateButton(true);
@ -218,20 +218,15 @@
return e ? e.getAttribute('href') : null;
}
function getResource(url, options) {
if (url.startsWith('#')) {
return Promise.resolve(document.getElementById(url.slice(1)).textContent);
async function getResource(url, type = 'text') {
try {
return url.startsWith('#')
? document.getElementById(url.slice(1)).textContent
: await (await fetch(url))[type];
} catch (error) {
alert('Error\n' + error.message);
return Promise.reject(error);
}
return API.download(Object.assign({
url,
timeout: 60e3,
// USO can't handle POST requests for style json
body: null,
}, options))
.catch(error => {
alert('Error' + (error ? '\n' + error : ''));
throw error;
});
}
// USO providing md5Url as "https://update.update.userstyles.org/#####.md5"
@ -244,7 +239,7 @@
}
function getStyleJson() {
return getResource(getStyleURL(), {responseType: 'json'})
return getResource(getStyleURL(), 'json')
.then(style => {
if (!style || !Array.isArray(style.sections) || style.sections.length) {
return style;
@ -254,7 +249,7 @@
return style;
}
return getResource(getMeta('stylish-update-url'))
.then(code => API.parseCss({code}))
.then(code => API.worker.parseMozFormat({code}))
.then(result => {
style.sections = result.sections;
return style;

View File

@ -110,7 +110,7 @@ lazyInit();
async function initStyle() {
const params = new URLSearchParams(location.search);
const id = Number(params.get('id'));
style = id ? await API.getStyle(id) : initEmptyStyle(params);
style = id ? await API.styles.get(id) : initEmptyStyle(params);
// switching the mode here to show the correct page ASAP, usually before DOMContentLoaded
editor.isUsercss = Boolean(style.usercssData || !style.id && prefs.get('newStyleAsUsercss'));
document.documentElement.classList.toggle('usercss', editor.isUsercss);
@ -426,26 +426,18 @@ function lazyInit() {
}
function onRuntimeMessage(request) {
const {style} = request;
switch (request.method) {
case 'styleUpdated':
if (
editor.style.id === request.style.id &&
!['editPreview', 'editPreviewEnd', 'editSave', 'config']
.includes(request.reason)
) {
Promise.resolve(
request.codeIsUpdated === false ?
request.style : API.getStyle(request.style.id)
)
.then(newStyle => {
editor.replaceStyle(newStyle, request.codeIsUpdated);
});
if (editor.style.id === style.id &&
!['editPreview', 'editPreviewEnd', 'editSave', 'config'].includes(request.reason)) {
Promise.resolve(request.codeIsUpdated === false ? style : API.styles.get(style.id))
.then(newStyle => editor.replaceStyle(newStyle, request.codeIsUpdated));
}
break;
case 'styleDeleted':
if (editor.style.id === request.style.id) {
if (editor.style.id === style.id) {
closeCurrentTab();
break;
}
break;
case 'editDeleteText':

View File

@ -117,7 +117,7 @@ function SectionsEditor() {
if (!validate(newStyle)) {
return;
}
newStyle = await API.editSave(newStyle);
newStyle = await API.styles.editSave(newStyle);
destroyRemovedSections();
sessionStore.justEditedStyleId = newStyle.id;
editor.replaceStyle(newStyle, false);
@ -384,7 +384,7 @@ function SectionsEditor() {
t('importPreprocessor'), 'pre-line',
t('importPreprocessorTitle'))
) {
const {sections, errors} = await API.parseCss({code});
const {sections, errors} = await API.worker.parseMozFormat({code});
// shouldn't happen but just in case
if (!sections.length || errors.length) {
throw errors;
@ -403,7 +403,7 @@ function SectionsEditor() {
async function getPreprocessor(code) {
try {
return (await API.buildUsercssMeta({sourceCode: code})).usercssData.preprocessor;
return (await API.usercss.buildMeta({sourceCode: code})).usercssData.preprocessor;
} catch (e) {}
}

View File

@ -97,7 +97,7 @@ function SourceEditor() {
}
function preprocess(style) {
return API.buildUsercss({
return API.usercss.build({
styleId: style.id,
sourceCode: style.sourceCode,
assignVars: true,
@ -231,7 +231,7 @@ function SourceEditor() {
if (!dirty.isDirty()) return;
const code = cm.getValue();
return ensureUniqueStyle(code)
.then(() => API.editSaveUsercss({
.then(() => API.usercss.editSave({
id: style.id,
enabled: style.enabled,
sourceCode: code,
@ -265,7 +265,7 @@ function SourceEditor() {
function ensureUniqueStyle(code) {
return style.id ? Promise.resolve() :
API.buildUsercss({
API.usercss.build({
sourceCode: code,
checkDup: true,
metaOnly: true,

View File

@ -176,7 +176,7 @@
function initSourceCode(sourceCode) {
cm.setValue(sourceCode);
cm.refresh();
API.buildUsercss({sourceCode, checkDup: true})
API.usercss.build({sourceCode, checkDup: true})
.then(init)
.catch(err => {
$('#header').classList.add('meta-init-error');
@ -248,7 +248,7 @@
data.version,
]))
).then(ok => ok &&
API.installUsercss(style)
API.usercss.install(style)
.then(install)
.catch(err => messageBox.alert(t('styleInstallFailed', err), 'pre'))
);
@ -317,7 +317,7 @@
let sequence = null;
if (tabId < 0) {
getData = DirectDownloader();
sequence = API.getUsercssInstallCode(initialUrl)
sequence = API.usercss.getInstallCode(initialUrl)
.then(code => code || getData())
.catch(getData);
} else {
@ -372,19 +372,20 @@
cm.setValue(code);
cm.setCursor(cursor);
cm.scrollTo(scrollInfo.left, scrollInfo.top);
return API.installUsercss({id, sourceCode: code})
return API.usercss.install({id, sourceCode: code})
.then(updateMeta)
.catch(showError);
});
}
function DirectDownloader() {
let oldCode = null;
const passChangedCode = code => {
const isSame = code === oldCode;
oldCode = code;
return isSame ? null : code;
return async () => {
const code = await download(initialUrl);
if (oldCode !== code) {
oldCode = code;
return code;
}
};
return () => download(initialUrl).then(passChangedCode);
}
function PortDownloader() {
const resolvers = new Map();

View File

@ -84,10 +84,13 @@ const URLS = {
url &&
url.startsWith(URLS.usoArchiveRaw) &&
parseInt(url.match(/\/(\d+)\.user\.css|$/)[1]),
extractUsoArchiveInstallUrl: url => {
const id = URLS.extractUsoArchiveId(url);
return id ? `${URLS.usoArchive}?style=${id}` : '';
},
extractGreasyForkId: url =>
/^https:\/\/(?:greasy|sleazy)fork\.org\/scripts\/(\d+)[^/]*\/code\/[^/]*\.user\.css$/.test(url) &&
RegExp.$1,
extractGreasyForkInstallUrl: url =>
/^(https:\/\/(?:greasy|sleazy)fork\.org\/scripts\/\d+)[^/]*\/code\/[^/]*\.user\.css$|$/.exec(url)[1],
supported: url => (
url.startsWith('http') ||
@ -98,9 +101,7 @@ const URLS = {
),
};
if (chrome.extension.getBackgroundPage && chrome.extension.getBackgroundPage() === window) {
window.API_METHODS = {};
} else {
if (!chrome.extension.getBackgroundPage || chrome.extension.getBackgroundPage() !== window) {
const cls = FIREFOX ? 'firefox' : OPERA ? 'opera' : VIVALDI ? 'vivaldi' : '';
if (cls) document.documentElement.classList.add(cls);
}
@ -226,8 +227,9 @@ function activateTab(tab, {url, index, openerTabId} = {}) {
}
function stringAsRegExp(s, flags) {
return new RegExp(s.replace(/[{}()[\]\\.+*?^$|]/g, '\\$&'), flags);
function stringAsRegExp(s, flags, asString) {
s = s.replace(/[{}()[\]\\.+*?^$|]/g, '\\$&');
return asString ? s : new RegExp(s, flags);
}
@ -371,70 +373,49 @@ function download(url, {
requiredStatusCode = 200,
timeout = 60e3, // connection timeout, USO is that bad
loadTimeout = 2 * 60e3, // data transfer timeout (counted from the first remote response)
headers = {
'Content-type': 'application/x-www-form-urlencoded',
},
headers,
} = {}) {
const queryPos = url.indexOf('?');
if (queryPos > 0 && body === undefined) {
method = 'POST';
body = url.slice(queryPos);
url = url.slice(0, queryPos);
/* USO can't handle POST requests for style json and XHR/fetch can't handle super long URL
* so we need to collapse all long variables and expand them in the response */
const queryPos = url.startsWith(URLS.uso) ? url.indexOf('?') : -1;
if (queryPos >= 0) {
if (body === undefined) {
method = 'POST';
body = url.slice(queryPos);
url = url.slice(0, queryPos);
}
if (headers === undefined) {
headers = {
'Content-type': 'application/x-www-form-urlencoded',
};
}
}
// * USO can't handle POST requests for style json
// * XHR/fetch can't handle long URL
// So we need to collapse all long variables and expand them in the response
const usoVars = [];
return new Promise((resolve, reject) => {
let xhr;
const xhr = new XMLHttpRequest();
const u = new URL(collapseUsoVars(url));
const onTimeout = () => {
if (xhr) xhr.abort();
xhr.abort();
reject(new Error('Timeout fetching ' + u.href));
};
let timer = setTimeout(onTimeout, timeout);
const switchTimer = () => {
clearTimeout(timer);
timer = loadTimeout && setTimeout(onTimeout, loadTimeout);
};
if (u.protocol === 'file:' && FIREFOX) { // TODO: maybe remove this since FF68+ can't do it anymore
// https://stackoverflow.com/questions/42108782/firefox-webextensions-get-local-files-content-by-path
// FIXME: add FetchController when it is available.
fetch(u.href, {mode: 'same-origin'})
.then(r => {
switchTimer();
return r.status === 200 ? r.text() : Promise.reject(r.status);
})
.catch(reject)
.then(text => {
clearTimeout(timer);
resolve(text);
});
return;
}
xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState >= XMLHttpRequest.HEADERS_RECEIVED) {
xhr.onreadystatechange = null;
switchTimer();
clearTimeout(timer);
timer = loadTimeout && setTimeout(onTimeout, loadTimeout);
}
};
xhr.onloadend = event => {
clearTimeout(timer);
if (event.type !== 'error' && (
xhr.status === requiredStatusCode || !requiredStatusCode ||
u.protocol === 'file:')) {
resolve(expandUsoVars(xhr.response));
} else {
reject(xhr.status);
}
};
xhr.onerror = xhr.onloadend;
xhr.onload = () =>
xhr.status === requiredStatusCode || !requiredStatusCode || u.protocol === 'file:'
? resolve(expandUsoVars(xhr.response))
: reject(xhr.status);
xhr.onerror = () => reject(xhr.status);
xhr.onloadend = () => clearTimeout(timer);
xhr.responseType = responseType;
xhr.open(method, u.href, true);
for (const key in headers) {
xhr.setRequestHeader(key, headers[key]);
xhr.open(method, u.href);
for (const [name, value] of Object.entries(headers || {})) {
xhr.setRequestHeader(name, value);
}
xhr.send(body);
});

View File

@ -130,27 +130,30 @@ window.INJECTED !== 1 && (() => {
},
};
window.API = new Proxy({}, {
get(target, name) {
// using a named function for convenience when debugging
return async function invokeAPI(...args) {
if (!bg && chrome.tabs) {
bg = await browser.runtime.getBackgroundPage().catch(() => {});
}
const message = {method: 'invokeAPI', name, args};
// content scripts and probably private tabs
if (!bg) {
return msg.send(message);
}
// in FF, the object would become a dead object when the window
// is closed, so we have to clone the object into background.
const res = bg.msg._execute(TARGETS.extension, bg.deepCopy(message), {
frameId: 0, // false in case of our Options frame but we really want to fetch styles early
tab: NEEDS_TAB_IN_SENDER.includes(name) && await getOwnTab(),
url: location.href,
});
return deepCopy(await res);
};
const apiHandler = !isBg && {
get({PATH}, name) {
const fn = () => {};
fn.PATH = [...PATH, name];
return new Proxy(fn, apiHandler);
},
});
async apply({PATH: path}, thisObj, args) {
if (!bg && chrome.tabs) {
bg = await browser.runtime.getBackgroundPage().catch(() => {});
}
const message = {method: 'invokeAPI', path, args};
// content scripts and probably private tabs
if (!bg) {
return msg.send(message);
}
// in FF, the object would become a dead object when the window
// is closed, so we have to clone the object into background.
const res = bg.msg._execute(TARGETS.extension, bg.deepCopy(message), {
frameId: 0, // false in case of our Options frame but we really want to fetch styles early
tab: NEEDS_TAB_IN_SENDER.includes(path.join('.')) && await getOwnTab(),
url: location.href,
});
return deepCopy(await res);
},
};
window.API = isBg ? {} : new Proxy({PATH: []}, apiHandler);
})();

View File

@ -6,7 +6,7 @@
window.INJECTED !== 1 && (() => {
const STORAGE_KEY = 'settings';
const clone = msg.isBg ? deepCopy : (val => JSON.parse(JSON.stringify(val)));
const defaults = {
const defaults = /** @namespace Prefs */{
'openEditInWindow': false, // new editor opens in a own browser window
'openEditInWindow.popup': false, // new editor opens in a simplified browser window without omnibox
'windowPosition': {}, // detached window position

View File

@ -1,4 +1,4 @@
/* global backgroundWorker */
/* global API */
/* exported usercss */
'use strict';
@ -33,7 +33,7 @@ const usercss = (() => {
throw new Error('can not find metadata');
}
return backgroundWorker.parseUsercssMeta(match[0], match.index)
return API.worker.parseUsercssMeta(match[0], match.index)
.catch(err => {
if (err.code) {
const args = ERR_ARGS_IS_LIST.has(err.code) ? drawList(err.args) : err.args;
@ -68,7 +68,7 @@ const usercss = (() => {
*/
function buildCode(style, allowErrors) {
const match = style.sourceCode.match(RX_META);
return backgroundWorker.compileUsercss(
return API.worker.compileUsercss(
style.usercssData.preprocessor,
style.sourceCode.slice(0, match.index) + style.sourceCode.slice(match.index + match[0].length),
style.usercssData.vars
@ -95,7 +95,7 @@ const usercss = (() => {
vars[key].value = oldVars[key].value;
}
}
return backgroundWorker.nullifyInvalidVars(vars)
return API.worker.nullifyInvalidVars(vars)
.then(vars => {
style.usercssData.vars = vars;
});

View File

@ -128,7 +128,7 @@ function configDialog(style) {
return;
}
if (!bgStyle) {
API.getStyle(style.id, true)
API.styles.get(style.id)
.catch(() => ({}))
.then(bgStyle => save({anyChangeIsDirty}, bgStyle));
return;
@ -182,7 +182,7 @@ function configDialog(style) {
return;
}
saving = true;
return API.configUsercssVars(style.id, style.usercssData.vars)
return API.usercss.configVars(style.id, style.usercssData.vars)
.then(newVars => {
varsInitial = getInitialValues(newVars);
vars.forEach(va => onchange({target: va.input, justSaved: true}));

View File

@ -109,7 +109,7 @@ function importFromFile({fileTypeFilter, file} = {}) {
async function importFromString(jsonString) {
const json = tryJSONparse(jsonString);
const oldStyles = Array.isArray(json) && json.length ? await API.getAllStyles() : [];
const oldStyles = Array.isArray(json) && json.length ? await API.styles.getAll() : [];
const oldStylesById = new Map(oldStyles.map(style => [style.id, style]));
const oldStylesByName = new Map(oldStyles.map(style => [style.name.trim(), style]));
const items = [];
@ -126,7 +126,7 @@ async function importFromString(jsonString) {
await Promise.all(json.map(analyze));
bulkChangeQueue.length = 0;
bulkChangeQueue.time = performance.now();
(await API.importManyStyles(items))
(await API.styles.importMany(items))
.forEach((style, i) => updateStats(style, infos[i]));
return done();
@ -290,10 +290,10 @@ async function importFromString(jsonString) {
];
let tasks = Promise.resolve();
for (const id of newIds) {
tasks = tasks.then(() => API.deleteStyle(id));
tasks = tasks.then(() => API.styles.delete(id));
const oldStyle = oldStylesById.get(id);
if (oldStyle) {
tasks = tasks.then(() => API.importStyle(oldStyle));
tasks = tasks.then(() => API.styles.import(oldStyle));
}
}
// taskUI is superfast and updates style list only in this page,
@ -338,7 +338,7 @@ async function exportToFile() {
Object.assign({
[prefs.STORAGE_KEY]: prefs.values,
}, await chromeSync.getLZValues()),
...await API.getAllStyles(),
...await API.styles.getAll(),
];
const text = JSON.stringify(data, null, ' ');
const type = 'application/json';

View File

@ -94,7 +94,7 @@ const handleEvent = {};
(async () => {
const query = router.getSearch('search');
const [styles, ids, el] = await Promise.all([
API.getAllStyles(),
API.styles.getAll(),
query && API.searchDB({query, mode: router.getSearch('searchMode')}),
waitForSelector('#installed'), // needed to avoid flicker due to an extra frame and layout shift
prefs.initializing,
@ -469,7 +469,7 @@ Object.assign(handleEvent, {
},
toggle(event, entry) {
API.toggleStyle(entry.styleId, this.matches('.enable') || this.checked);
API.styles.toggle(entry.styleId, this.matches('.enable') || this.checked);
},
check(event, entry) {
@ -481,7 +481,7 @@ Object.assign(handleEvent, {
event.preventDefault();
const json = entry.updatedCode;
json.id = entry.styleId;
API[json.usercssData ? 'installUsercss' : 'installStyle'](json);
(json.usercssData ? API.usercss : API.styles).install(json);
},
delete(event, entry) {
@ -496,7 +496,7 @@ Object.assign(handleEvent, {
})
.then(({button}) => {
if (button === 0) {
API.deleteStyle(id);
API.styles.delete(id);
}
});
const deleteButton = $('#message-box-buttons > button');
@ -599,7 +599,7 @@ function handleBulkChange() {
}
function handleUpdateForId(id, opts) {
return API.getStyle(id).then(style => {
return API.styles.get(id).then(style => {
handleUpdate(style, opts);
bulkChangeQueue.time = performance.now();
});
@ -697,7 +697,7 @@ function switchUI({styleOnly} = {}) {
let iconsMissing = iconsEnabled && !$('.applies-to img');
if (changed.enabled || (iconsMissing && !createStyleElement.parts)) {
installed.textContent = '';
API.getAllStyles().then(showStyles);
API.styles.getAll().then(showStyles);
return;
}
if (changed.sliders && newUI.enabled) {

View File

@ -53,7 +53,7 @@ function checkUpdateAll() {
chrome.runtime.onConnect.removeListener(onConnect);
});
API.updateCheckAll({
API.updater.checkAllStyles({
save: false,
observe: true,
ignoreDigest,
@ -98,7 +98,7 @@ function checkUpdate(entry, {single} = {}) {
$('.update-note', entry).textContent = t('checkingForUpdate');
$('.check-update', entry).title = '';
if (single) {
API.updateCheck({
API.updater.checkStyle({
save: false,
id: entry.styleId,
ignoreDigest: entry.classList.contains('update-problem'),
@ -221,7 +221,7 @@ function showUpdateHistory(event) {
let deleted = false;
Promise.all([
chromeLocal.getValue('updateLog'),
API.getUpdaterStates(),
API.updater.getStates(),
]).then(([lines = [], states]) => {
logText = lines.join('\n');
messageBox({

View File

@ -52,12 +52,13 @@
"background/tab-manager.js",
"background/icon-manager.js",
"background/background.js",
"background/usercss-helper.js",
"background/usercss-api-helper.js",
"background/usercss-install-helper.js",
"background/style-via-api.js",
"background/style-via-webrequest.js",
"background/search-db.js",
"background/update.js",
"background/context-menus.js",
"background/openusercss-api.js"
]
},

View File

@ -1,7 +1,25 @@
/* global messageBox msg setupLivePrefs enforceInputRange
$ $$ $create $createLink
FIREFOX OPERA CHROME URLS openURL prefs t API ignoreChromeError
CHROME_HAS_BORDER_BUG capitalize */
/* global
$
$$
$create
$createLink
API
capitalize
CHROME
CHROME_HAS_BORDER_BUG
enforceInputRange
FIREFOX
getEventKeyName
ignoreChromeError
messageBox
msg
openURL
OPERA
prefs
setupLivePrefs
t
URLS
*/
'use strict';
setupLivePrefs();
@ -44,7 +62,7 @@ if (CHROME && !chrome.declarativeContent) {
prefs.initializing.then(() => {
el.checked = false;
});
el.addEventListener('click', () => {
el.on('click', () => {
if (el.checked) {
chrome.permissions.request({permissions: ['declarativeContent']}, ignoreChromeError);
}
@ -101,84 +119,75 @@ document.onclick = e => {
// sync to cloud
(() => {
const cloud = document.querySelector('.sync-options .cloud-name');
const connectButton = document.querySelector('.sync-options .connect');
const disconnectButton = document.querySelector('.sync-options .disconnect');
const syncButton = document.querySelector('.sync-options .sync-now');
const statusText = document.querySelector('.sync-options .sync-status');
const loginButton = document.querySelector('.sync-options .sync-login');
const elCloud = $('.sync-options .cloud-name');
const elStart = $('.sync-options .connect');
const elStop = $('.sync-options .disconnect');
const elSyncNow = $('.sync-options .sync-now');
const elStatus = $('.sync-options .sync-status');
const elLogin = $('.sync-options .sync-login');
/** @type {API.sync.Status} */
let status = {};
msg.onExtension(e => {
if (e.method === 'syncStatusUpdate') {
status = e.status;
updateButtons();
setStatus(e.status);
}
});
API.sync.getStatus()
.then(setStatus);
API.getSyncStatus()
.then(_status => {
status = _status;
updateButtons();
elCloud.on('change', updateButtons);
for (const [btn, fn] of [
[elStart, () => API.sync.start(elCloud.value)],
[elStop, API.sync.stop],
[elSyncNow, API.sync.syncNow],
[elLogin, API.sync.login],
]) {
btn.on('click', e => {
if (getEventKeyName(e) === 'L') {
fn();
}
});
function validClick(e) {
return e.button === 0 && !e.ctrl && !e.alt && !e.shift;
}
cloud.addEventListener('change', updateButtons);
function setStatus(newStatus) {
status = newStatus;
updateButtons();
}
function updateButtons() {
const isConnected = status.state === 'connected';
const isDisconnected = status.state === 'disconnected';
if (status.currentDriveName) {
cloud.value = status.currentDriveName;
elCloud.value = status.currentDriveName;
}
cloud.disabled = status.state !== 'disconnected';
connectButton.disabled = status.state !== 'disconnected' || cloud.value === 'none';
disconnectButton.disabled = status.state !== 'connected' || status.syncing;
syncButton.disabled = status.state !== 'connected' || status.syncing;
statusText.textContent = getStatusText();
loginButton.style.display = status.state === 'connected' && !status.login ? '' : 'none';
for (const [el, enable] of [
[elCloud, isDisconnected],
[elStart, isDisconnected && elCloud.value !== 'none'],
[elStop, isConnected && !status.syncing],
[elSyncNow, isConnected && !status.syncing],
]) {
el.disabled = !enable;
}
elStatus.textContent = getStatusText();
elLogin.hidden = !isConnected || status.login;
}
function getStatusText() {
// chrome.i18n.getMessage is used instead of t() because calculated ids may be absent
let res;
if (status.syncing) {
if (status.progress) {
const {phase, loaded, total} = status.progress;
return chrome.i18n.getMessage(`optionsSyncStatus${capitalize(phase)}`, [loaded + 1, total]) ||
`${phase} ${loaded} / ${total}`;
}
return chrome.i18n.getMessage('optionsSyncStatusSyncing') || 'syncing';
const {phase, loaded, total} = status.progress || {};
res = phase
? chrome.i18n.getMessage(`optionsSyncStatus${capitalize(phase)}`, [loaded + 1, total]) ||
`${phase} ${loaded} / ${total}`
: t('optionsSyncStatusSyncing');
} else {
const {state, errorMessage} = status;
res = (state === 'connected' || state === 'disconnected') && errorMessage ||
chrome.i18n.getMessage(`optionsSyncStatus${capitalize(state)}`) || state;
}
if ((status.state === 'connected' || status.state === 'disconnected') && status.errorMessage) {
return status.errorMessage;
}
return chrome.i18n.getMessage(`optionsSyncStatus${capitalize(status.state)}`) || status.state;
return res;
}
connectButton.addEventListener('click', e => {
if (validClick(e)) {
API.syncStart(cloud.value).catch(console.error);
}
});
disconnectButton.addEventListener('click', e => {
if (validClick(e)) {
API.syncStop().catch(console.error);
}
});
syncButton.addEventListener('click', e => {
if (validClick(e)) {
API.syncNow().catch(console.error);
}
});
loginButton.addEventListener('click', e => {
if (validClick(e)) {
API.syncLogin().catch(console.error);
}
});
})();
function checkUpdates() {
@ -193,7 +202,7 @@ function checkUpdates() {
chrome.runtime.onConnect.removeListener(onConnect);
});
API.updateCheckAll({observe: true});
API.updater.checkAllStyles({observe: true});
function observer(info) {
if ('count' in info) {
@ -223,7 +232,7 @@ function setupRadioButtons() {
// group all radio-inputs by name="prefName" attribute
for (const el of $$('input[type="radio"][name]')) {
(sets[el.name] = sets[el.name] || []).push(el);
el.addEventListener('change', onChange);
el.on('change', onChange);
}
// select the input corresponding to the actual pref value
for (const name in sets) {

View File

@ -89,7 +89,7 @@ const hotkeys = (() => {
if (!match && $('input', entry).checked !== enable || entry.classList.contains(match)) {
results.push(entry.id);
task = task
.then(() => API.toggleStyle(entry.styleId, enable))
.then(() => API.styles.toggle(entry.styleId, enable))
.then(() => {
entry.classList.toggle('enabled', enable);
entry.classList.toggle('disabled', !enable);

View File

@ -81,7 +81,7 @@ const initializing = (async () => {
/* Merges the extra props from API into style data.
* When `id` is specified returns a single object otherwise an array */
async function getStyleDataMerged(url, id) {
const styles = (await API.getStylesByUrl(url, id))
.map(r => Object.assign(r.data, r));
const styles = (await API.styles.getByUrl(url, id))
.map(r => Object.assign(r.style, r));
return id ? styles[0] : styles;
}

View File

@ -143,7 +143,7 @@ async function initPopup(frames) {
switch (e.target.dataset.cmd) {
case 'ok':
hideModal(this, {animate: true});
API.deleteStyle(Number(id));
API.styles.delete(Number(id));
break;
case 'cancel':
showModal($('.menu', $.entry(id)), '.menu-close');
@ -464,20 +464,19 @@ Object.assign(handleEvent, {
event.preventDefault();
},
toggle(event) {
async toggle(event) {
// when fired on checkbox, prevent the parent label from seeing the event, see #501
event.stopPropagation();
API
.toggleStyle(handleEvent.getClickedStyleId(event), this.checked)
.then(() => resortEntries());
await API.styles.toggle(handleEvent.getClickedStyleId(event), this.checked);
resortEntries();
},
toggleExclude(event, type) {
const entry = handleEvent.getClickedStyleElement(event);
if (event.target.checked) {
API.addExclusion(entry.styleMeta.id, getExcludeRule(type));
API.styles.addExclusion(entry.styleMeta.id, getExcludeRule(type));
} else {
API.removeExclusion(entry.styleMeta.id, getExcludeRule(type));
API.styles.removeExclusion(entry.styleMeta.id, getExcludeRule(type));
}
},
@ -503,7 +502,7 @@ Object.assign(handleEvent, {
configure(event) {
const {styleId, styleIsUsercss} = handleEvent.getClickedStyleElement(event);
if (styleIsUsercss) {
API.getStyle(styleId, true).then(style => {
API.styles.get(styleId).then(style => {
hotkeys.setState(false);
configDialog(style).then(() => {
hotkeys.setState(true);

View File

@ -149,7 +149,7 @@ window.addEventListener('showStyles:done', () => {
addEventListener('styleAdded', async ({detail: {style}}) => {
restoreScrollPosition();
const usoId = calcUsoId(style) ||
calcUsoId(await API.getStyle(style.id, true));
calcUsoId(await API.styles.get(style.id));
if (usoId && results.find(r => r.i === usoId)) {
renderActionButtons(usoId, style.id);
}
@ -194,7 +194,7 @@ window.addEventListener('showStyles:done', () => {
results = await search({retry});
}
if (results.length) {
const installedStyles = await API.getAllStyles();
const installedStyles = await API.styles.getAll();
const allUsoIds = new Set(installedStyles.map(calcUsoId));
results = results.filter(r => !allUsoIds.has(r.i));
}
@ -419,7 +419,7 @@ window.addEventListener('showStyles:done', () => {
const updateUrl = `${URLS.usoArchiveRaw}usercss/${id}.user.css`;
try {
const sourceCode = await download(updateUrl);
const style = await API.installUsercss({sourceCode, updateUrl});
const style = await API.usercss.install({sourceCode, updateUrl});
renderFullInfo(entry, style);
} catch (reason) {
error(`Error while downloading usoID:${id}\nReason: ${reason}`);
@ -432,7 +432,7 @@ window.addEventListener('showStyles:done', () => {
function uninstall() {
const entry = this.closest('.search-result');
saveScrollPosition(entry);
API.deleteStyle(entry._result.installedStyleId);
API.styles.delete(entry._result.installedStyleId);
}
function saveScrollPosition(entry) {