diff --git a/.eslintrc.yml b/.eslintrc.yml index 87af0074..0871bcad 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -1,7 +1,7 @@ # https://github.com/eslint/eslint/blob/master/docs/rules/README.md parserOptions: - ecmaVersion: 2015 + ecmaVersion: 2017 env: browser: true diff --git a/_locales/bg/messages.json b/_locales/bg/messages.json index 0e02bbf8..fce7dc43 100644 --- a/_locales/bg/messages.json +++ b/_locales/bg/messages.json @@ -217,10 +217,6 @@ } } }, - "editorStylesButton": { - "message": "Стилове за редактора", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "Включване", "description": "Label for the button to enable a style" @@ -741,4 +737,4 @@ "message": "този адрес", "description": "Text for link in toolbar pop-up to write a new style for the current URL" } -} \ No newline at end of file +} diff --git a/_locales/cs/messages.json b/_locales/cs/messages.json index 51c2f253..4bd340b5 100644 --- a/_locales/cs/messages.json +++ b/_locales/cs/messages.json @@ -305,10 +305,6 @@ } } }, - "editorStylesButton": { - "message": "Najít styly pro editor", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "Povolit", "description": "Label for the button to enable a style" @@ -1306,4 +1302,4 @@ "message": "Nahrávání souboru…", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/de/messages.json b/_locales/de/messages.json index 6521b6f3..32253e1b 100644 --- a/_locales/de/messages.json +++ b/_locales/de/messages.json @@ -309,10 +309,6 @@ } } }, - "editorStylesButton": { - "message": "Editor Styles finden", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "Aktivieren", "description": "Label for the button to enable a style" @@ -1600,4 +1596,4 @@ "message": "Lade Styles hoch...", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 0f42f307..9d8ee6cd 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -250,6 +250,13 @@ "message": "Copy to clipboard", "description": "Tooltip for elements which can be copied" }, + "customNameHint": { + "message": "Enter a custom name here to rename the style in UI without breaking its updates" + }, + "customNameResetHint": { + "message": "Stop using customized name, switch to the style's own name", + "description": "Tooltip of 'x' button shown in editor when changing the name input of a) styles updated from a URL i.e. not locally created, b) UserCSS styles" + }, "dateInstalled": { "message": "Date installed", "description": "Option text for the user to sort the style by install date" @@ -319,10 +326,6 @@ }, "description": "Title of the page for editing styles" }, - "editorStylesButton": { - "message": "Find editor styles", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "Enable", "description": "Label for the button to enable a style" @@ -984,6 +987,12 @@ "optionsAdvancedAutoSwitchSchemeByTime": { "message": "By night time:" }, + "optionsAdvancedStyleViaXhr": { + "message": "Instant inject mode" + }, + "optionsAdvancedStyleViaXhrNote": { + "message": "Enable this if you encounter flashing of unstyled content (FOUC) when browsing, which is especially noticeable with dark themes.\n\nThe technical reason is that Chrome/Chromium postpones asynchronous communication of extensions, in a usually meaningless attempt to improve page load speed, potentially causing styles to be late to apply. To circumvent this, since web extensions are not provided a synchronous API, Stylus provides this option to utilize the \"deprecated\" synchronous XMLHttpRequest web API to fetch applicable styles. There shouldn't be any detrimental effects, since the request is fulfilled within a few milliseconds while the page is still being downloaded from the server.\n\nNevertheless, Chromium will print a warning in devtools' console. Right-clicking a warning, and hiding them, will prevent future warnings from being shown." + }, "optionsBadgeDisabled": { "message": "Background color when disabled" }, @@ -1036,6 +1045,9 @@ "optionsResetButton": { "message": "Reset options" }, + "optionsStylusThemes": { + "message": "Find a Stylus UI theme" + }, "optionsSubheading": { "message": "More Options", "description": "Subheading for options section on manage page." @@ -1147,6 +1159,10 @@ "message": "Action menu", "description": "Tooltip for menu button in popup." }, + "popupOpenEditInPopup": { + "message": "Use a simple window (no omnibox)", + "description": "Label for the checkbox controlling 'edit' action behavior in the popup." + }, "popupOpenEditInWindow": { "message": "Open editor in a new window", "description": "Label for the checkbox controlling 'edit' action behavior in the popup." @@ -1198,6 +1214,10 @@ "message": "Case-sensitive", "description": "Tooltip for the 'Aa' icon that enables case-sensitive search in the editor shown on Ctrl-F" }, + "searchGlobalStyles": { + "message": "Also search global styles", + "description": "Checkbox label in the popup's inline style search, shown when the text to search is entered" + }, "searchNumberOfResults": { "message": "Number of matches", "description": "Tooltip for the number of found search results in the editor shown on Ctrl-F" @@ -1206,6 +1226,10 @@ "message": "Number of matches in code and applies-to values", "description": "Tooltip for the number of found search results in the editor shown on Ctrl-F" }, + "searchStyleQueryHint": { + "message": "Search style names case-insensitively:\nsome words - all words in any order\n\"some phrase\" - this exact phrase without quotes\n2020 - a year like this also shows styles updated in 2020", + "description": "Tooltip shown for the text input in the popup's inline style finder" + }, "searchRegexp": { "message": "Use /re/ syntax for regexp search", "description": "Label after the search input field in the editor shown on Ctrl-F" diff --git a/_locales/es/messages.json b/_locales/es/messages.json index ffb6ba7a..ceb3dd1d 100644 --- a/_locales/es/messages.json +++ b/_locales/es/messages.json @@ -313,10 +313,6 @@ } } }, - "editorStylesButton": { - "message": "Buscar estilos del editor", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "Activar", "description": "Label for the button to enable a style" @@ -1568,4 +1564,4 @@ "message": "Subiendo el archivo....", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/et/messages.json b/_locales/et/messages.json index e2568512..57066641 100644 --- a/_locales/et/messages.json +++ b/_locales/et/messages.json @@ -313,10 +313,6 @@ } } }, - "editorStylesButton": { - "message": "Leia redaktori stiile", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "Luba", "description": "Label for the button to enable a style" @@ -1486,4 +1482,4 @@ "message": "Faili üleslaadimine...", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index d96e9a3a..e2aad9cd 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -321,10 +321,6 @@ } } }, - "editorStylesButton": { - "message": "Trouver des styles pour l’éditeur", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "Activer", "description": "Label for the button to enable a style" @@ -1616,4 +1612,4 @@ "message": "Envoi du fichier…", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/he/messages.json b/_locales/he/messages.json index 22266d83..c9b25a9a 100644 --- a/_locales/he/messages.json +++ b/_locales/he/messages.json @@ -321,10 +321,6 @@ } } }, - "editorStylesButton": { - "message": "מצא עיצובים לעורך", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "אפשר", "description": "Label for the button to enable a style" @@ -1381,4 +1377,4 @@ "message": "מעלה קובץ...", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/hu/messages.json b/_locales/hu/messages.json index 1ab582b4..1195eb83 100644 --- a/_locales/hu/messages.json +++ b/_locales/hu/messages.json @@ -309,10 +309,6 @@ } } }, - "editorStylesButton": { - "message": "A szerkesztő stílusainak keresése", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "Engedélyezés", "description": "Label for the button to enable a style" @@ -1604,4 +1600,4 @@ "message": "Fájl feltöltése...", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/it/messages.json b/_locales/it/messages.json index 1ccff03a..0be3fef9 100644 --- a/_locales/it/messages.json +++ b/_locales/it/messages.json @@ -273,10 +273,6 @@ } } }, - "editorStylesButton": { - "message": "Cerca stili editor", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "Attiva", "description": "Label for the button to enable a style" @@ -1058,4 +1054,4 @@ "message": "questo URL", "description": "Text for link in toolbar pop-up to write a new style for the current URL" } -} \ No newline at end of file +} diff --git a/_locales/ja/messages.json b/_locales/ja/messages.json index 1b79ccb7..b76c741f 100644 --- a/_locales/ja/messages.json +++ b/_locales/ja/messages.json @@ -313,10 +313,6 @@ } } }, - "editorStylesButton": { - "message": "エディタのスタイルを見つける", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "有効化", "description": "Label for the button to enable a style" @@ -1636,4 +1632,4 @@ "message": "スタイルをアップロード中...", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/ko/messages.json b/_locales/ko/messages.json index 9b207cd9..26a35640 100644 --- a/_locales/ko/messages.json +++ b/_locales/ko/messages.json @@ -317,10 +317,6 @@ } } }, - "editorStylesButton": { - "message": "편집기 스타일 찾기", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "활성화", "description": "Label for the button to enable a style" @@ -1636,4 +1632,4 @@ "message": "파일 업로드 중...", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/nl/messages.json b/_locales/nl/messages.json index daa2a292..677c39cd 100644 --- a/_locales/nl/messages.json +++ b/_locales/nl/messages.json @@ -317,10 +317,6 @@ } } }, - "editorStylesButton": { - "message": "Editorstijlen zoeken", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "Inschakelen", "description": "Label for the button to enable a style" @@ -1620,4 +1616,4 @@ "message": "Bestand uploaden...", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/pl/messages.json b/_locales/pl/messages.json index a5f8ad21..f24ce9e9 100644 --- a/_locales/pl/messages.json +++ b/_locales/pl/messages.json @@ -321,10 +321,6 @@ } } }, - "editorStylesButton": { - "message": "Znajdź style edytora", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "Włącz", "description": "Label for the button to enable a style" @@ -1644,4 +1640,4 @@ "message": "Wysyłanie stylów...", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/pt_PT/messages.json b/_locales/pt_PT/messages.json index a54ba31e..cf3cee05 100644 --- a/_locales/pt_PT/messages.json +++ b/_locales/pt_PT/messages.json @@ -301,10 +301,6 @@ } } }, - "editorStylesButton": { - "message": "Encontrar estilos para o editor", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "Ativar", "description": "Label for the button to enable a style" @@ -1224,4 +1220,4 @@ "message": "este URL", "description": "Text for link in toolbar pop-up to write a new style for the current URL" } -} \ No newline at end of file +} diff --git a/_locales/ro/messages.json b/_locales/ro/messages.json index 0a34d235..703d0f54 100644 --- a/_locales/ro/messages.json +++ b/_locales/ro/messages.json @@ -269,10 +269,6 @@ } } }, - "editorStylesButton": { - "message": "Găsiți teme pentru editor", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "Activați", "description": "Label for the button to enable a style" @@ -1140,4 +1136,4 @@ "message": "acest URL", "description": "Text for link in toolbar pop-up to write a new style for the current URL" } -} \ No newline at end of file +} diff --git a/_locales/ru/messages.json b/_locales/ru/messages.json index 69985610..2334e357 100644 --- a/_locales/ru/messages.json +++ b/_locales/ru/messages.json @@ -321,10 +321,6 @@ } } }, - "editorStylesButton": { - "message": "Сменить тему Стилус", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "Включить", "description": "Label for the button to enable a style" @@ -1644,4 +1640,4 @@ "message": "Загрузка файла...", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/sv/messages.json b/_locales/sv/messages.json index 91ff2c39..d800b825 100644 --- a/_locales/sv/messages.json +++ b/_locales/sv/messages.json @@ -309,10 +309,6 @@ } } }, - "editorStylesButton": { - "message": "Hitta redaktörsstilar", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "Aktivera", "description": "Label for the button to enable a style" @@ -1522,4 +1518,4 @@ "message": "Skickar filen...", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/tr/messages.json b/_locales/tr/messages.json index 941cc153..1dedf52b 100644 --- a/_locales/tr/messages.json +++ b/_locales/tr/messages.json @@ -309,10 +309,6 @@ } } }, - "editorStylesButton": { - "message": "Editör stili bul", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "Etkinleştir", "description": "Label for the button to enable a style" @@ -896,4 +892,4 @@ "message": "Dosya Yükleniyor...", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index 50c1336b..04e56f37 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -321,10 +321,6 @@ } } }, - "editorStylesButton": { - "message": "查找编辑器样式", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "启用", "description": "Label for the button to enable a style" @@ -1644,4 +1640,4 @@ "message": "正在上传文件...", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/zh_TW/messages.json b/_locales/zh_TW/messages.json index 0886cb37..2551fb69 100644 --- a/_locales/zh_TW/messages.json +++ b/_locales/zh_TW/messages.json @@ -321,10 +321,6 @@ } } }, - "editorStylesButton": { - "message": "找到編輯器樣式", - "description": "Find styles for the editor" - }, "enableStyleLabel": { "message": "啟用", "description": "Label for the button to enable a style" @@ -1644,4 +1640,4 @@ "message": "正在上傳檔案……", "description": "" } -} \ No newline at end of file +} diff --git a/background/background-worker.js b/background/background-worker.js index 81387aac..1e6126f6 100644 --- a/background/background-worker.js +++ b/background/background-worker.js @@ -12,7 +12,6 @@ createAPI({ compileUsercss, parseUsercssMeta(text, indexOffset = 0) { loadScript( - '/js/polyfill.js', '/vendor/usercss-meta/usercss-meta.min.js', '/vendor-overwrites/colorpicker/colorconverter.js', '/js/meta-parser.js' @@ -21,7 +20,6 @@ createAPI({ }, nullifyInvalidVars(vars) { loadScript( - '/js/polyfill.js', '/vendor/usercss-meta/usercss-meta.min.js', '/vendor-overwrites/colorpicker/colorconverter.js', '/js/meta-parser.js' @@ -31,11 +29,15 @@ createAPI({ }); function compileUsercss(preprocessor, code, vars) { - loadScript('/vendor-overwrites/csslint/parserlib.js', '/js/moz-parser.js'); + loadScript( + '/vendor-overwrites/csslint/parserlib.js', + '/vendor-overwrites/colorpicker/colorconverter.js', + '/js/moz-parser.js' + ); const builder = getUsercssCompiler(preprocessor); vars = simpleVars(vars); return Promise.resolve(builder.preprocess ? builder.preprocess(code, vars) : code) - .then(code => parseMozFormat({code})) + .then(code => parseMozFormat({code, emptyDocument: preprocessor === 'stylus'})) .then(({sections, errors}) => { if (builder.postprocess) { builder.postprocess(sections, vars); @@ -122,28 +124,39 @@ function getUsercssCompiler(preprocessor) { const pool = new Map(); return Promise.resolve(doReplace(source)); - function getValue(name, rgb) { + function getValue(name, rgbName) { if (!vars.hasOwnProperty(name)) { if (name.endsWith('-rgb')) { - return getValue(name.slice(0, -4), true); + return getValue(name.slice(0, -4), name); } return null; } - if (rgb) { - if (vars[name].type === 'color') { - const color = colorConverter.parse(vars[name].value); - if (!color) return null; - const {r, g, b} = color; - return `${r}, ${g}, ${b}`; + const {type, value} = vars[name]; + switch (type) { + case 'color': { + let color = pool.get(rgbName || name); + if (color == null) { + color = colorConverter.parse(value); + if (color) { + if (color.type === 'hsl') { + color = colorConverter.HSVtoRGB(colorConverter.HSLtoHSV(color)); + } + const {r, g, b} = color; + color = rgbName + ? `${r}, ${g}, ${b}` + : `#${(0x1000000 + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; + } + // the pool stores `false` for bad colors to differentiate from a yet unknown color + pool.set(rgbName || name, color || false); + } + return color || null; } - return null; + case 'dropdown': + case 'select': // prevent infinite recursion + pool.set(name, ''); + return doReplace(value); } - if (vars[name].type === 'dropdown' || vars[name].type === 'select') { - // prevent infinite recursion - pool.set(name, ''); - return doReplace(vars[name].value); - } - return vars[name].value; + return value; } function doReplace(text) { diff --git a/background/background.js b/background/background.js index 5dc3b4eb..f3bf9a8e 100644 --- a/background/background.js +++ b/background/background.js @@ -1,8 +1,7 @@ /* global download prefs openURL FIREFOX CHROME - URLS ignoreChromeError usercssHelper + URLS ignoreChromeError chromeLocal semverCompare styleManager msg navigatorUtil workerUtil contentScripts sync - findExistingTab activateTab isTabReplaceable getActiveTab - tabManager colorScheme */ + findExistingTab activateTab isTabReplaceable getActiveTab colorScheme */ 'use strict'; // eslint-disable-next-line no-var @@ -65,11 +64,13 @@ window.API_METHODS = Object.assign(window.API_METHODS || {}, { /* 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 */ - openURL(opts) { - const {message} = opts; - return openURL(opts) // will pass the resolved value untouched when `message` is absent or falsy - .then(message && (tab => tab.status === 'complete' ? tab : onTabReady(tab))) - .then(message && (tab => msg.sendTab(tab.id, opts.message))); + async openURL(opts) { + const tab = await openURL(opts); + if (opts.message) { + await onTabReady(tab); + await msg.sendTab(tab.id, opts.message); + } + return tab; function onTabReady(tab) { return new Promise((resolve, reject) => setTimeout(function ping(numTries = 10, delay = 100) { @@ -112,14 +113,6 @@ navigatorUtil.onUrlChange(({tabId, frameId}, type) => { } }); -tabManager.onUpdate(({tabId, url, oldUrl = ''}) => { - if (usercssHelper.testUrl(url) && !oldUrl.startsWith(URLS.installUsercss)) { - usercssHelper.testContents(tabId, url).then(data => { - if (data.code) usercssHelper.openInstallerPage(tabId, url, data); - }); - } -}); - if (FIREFOX) { // FF misses some about:blank iframes so we inject our content script explicitly navigatorUtil.onDOMContentLoaded(webNavIframeHelperFF, { @@ -140,7 +133,7 @@ if (chrome.commands) { } // ************************************************************************* -chrome.runtime.onInstalled.addListener(({reason}) => { +chrome.runtime.onInstalled.addListener(({reason, previousVersion}) => { // save install type: "admin", "development", "normal", "sideload" or "other" // "normal" = addon installed from webstore chrome.management.getSelf(info => { @@ -157,6 +150,14 @@ chrome.runtime.onInstalled.addListener(({reason}) => { }); // themes may change delete localStorage.codeMirrorThemes; + // inline search cache for USO is not needed anymore, TODO: remove this by the middle of 2021 + if (semverCompare(previousVersion, '1.5.13') <= 0) { + setTimeout(async () => { + const del = Object.keys(await chromeLocal.get()) + .filter(key => key.startsWith('usoSearchCache')); + if (del.length) chromeLocal.remove(del); + }, 15e3); + } }); // ************************************************************************* @@ -298,16 +299,14 @@ function openEditor(params) { 'url-prefix'?: String } */ - const searchParams = new URLSearchParams(); - for (const key in params) { - searchParams.set(key, params[key]); - } - const search = searchParams.toString(); + const u = new URL(chrome.runtime.getURL('edit.html')); + u.search = new URLSearchParams(params); return openURL({ - url: 'edit.html' + (search && `?${search}`), - newWindow: prefs.get('openEditInWindow'), - windowPosition: prefs.get('windowPosition'), - currentWindow: null + url: `${u}`, + currentWindow: null, + newWindow: prefs.get('openEditInWindow') && Object.assign({}, + prefs.get('openEditInWindow.popup') && {type: 'popup'}, + prefs.get('windowPosition')), }); } @@ -329,7 +328,7 @@ function openManage({options = false, search} = {}) { if (tab) { return Promise.all([ activateTab(tab), - tab.url !== url && msg.sendTab(tab.id, {method: 'pushState', url}) + (tab.pendingUrl || tab.url) !== url && msg.sendTab(tab.id, {method: 'pushState', url}) .catch(console.error) ]); } diff --git a/background/content-scripts.js b/background/content-scripts.js index dfd744f2..3aeecd16 100644 --- a/background/content-scripts.js +++ b/background/content-scripts.js @@ -17,6 +17,21 @@ 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}; function injectToTab({url, tabId, frameId = null}) { @@ -58,13 +73,13 @@ const contentScripts = (() => { return browser.tabs.query({}).then(tabs => { for (const tab of tabs) { // skip unloaded/discarded/chrome tabs - if (!tab.width || tab.discarded || !URLS.supported(tab.url)) continue; + if (!tab.width || tab.discarded || !URLS.supported(tab.pendingUrl || tab.url)) continue; // our content scripts may still be pending injection at browser start so it's too early to ping them if (tab.status === 'loading') { trackBusyTab(tab.id, true); } else { injectToTab({ - url: tab.url, + url: tab.pendingUrl || tab.url, tabId: tab.id }); } diff --git a/background/db.js b/background/db.js index 2549a3ce..223d3870 100644 --- a/background/db.js +++ b/background/db.js @@ -1,4 +1,4 @@ -/* global chromeLocal ignoreChromeError workerUtil createChromeStorageDB */ +/* global chromeLocal workerUtil createChromeStorageDB */ /* exported db */ /* Initialize a database. There are some problems using IndexedDB in Firefox: @@ -10,29 +10,18 @@ https://www.reddit.com/r/firefox/comments/7ijuaq/firefox_59_webextensions_can_us 'use strict'; const db = (() => { - let exec; - const preparing = prepare(); - return { - exec: (...args) => - preparing.then(() => exec(...args)) + const DATABASE = 'stylish'; + const STORE = 'styles'; + const FALLBACK = 'dbInChromeStorage'; + const dbApi = { + async exec(...args) { + dbApi.exec = await tryUsingIndexedDB().catch(useChromeStorage); + return dbApi.exec(...args); + }, }; + return dbApi; - function prepare() { - return withPromise(shouldUseIndexedDB).then( - ok => { - if (ok) { - useIndexedDB(); - } else { - useChromeStorage(); - } - }, - err => { - useChromeStorage(err); - } - ); - } - - function shouldUseIndexedDB() { + async function tryUsingIndexedDB() { // we use chrome.storage.local fallback if IndexedDB doesn't save data, // which, once detected on the first run, is remembered in chrome.storage.local // for reliablility and in localStorage for fast synchronous access @@ -42,115 +31,81 @@ const db = (() => { if (typeof indexedDB === 'undefined') { throw new Error('indexedDB is undefined'); } - // test localStorage - const fallbackSet = localStorage.dbInChromeStorage; - if (fallbackSet === 'true') { - return false; + switch (await getFallback()) { + case true: throw null; + case false: break; + default: await testDB(); } - if (fallbackSet === 'false') { - return true; - } - // test storage.local - return chromeLocal.get('dbInChromeStorage') - .then(data => { - if (data && data.dbInChromeStorage) { - return false; - } - return testDBSize() - .then(ok => ok || testDBMutation()); - }); + return useIndexedDB(); } - function withPromise(fn) { - try { - return Promise.resolve(fn()); - } catch (err) { - return Promise.reject(err); - } + async function getFallback() { + return localStorage[FALLBACK] === 'true' ? true : + localStorage[FALLBACK] === 'false' ? false : + chromeLocal.getValue(FALLBACK); } - function testDBSize() { - return dbExecIndexedDB('getAllKeys', IDBKeyRange.lowerBound(1), 1) - .then(event => ( - event.target.result && - event.target.result.length && - event.target.result[0] - )); - } - - function testDBMutation() { - return dbExecIndexedDB('put', {id: -1}) - .then(() => dbExecIndexedDB('get', -1)) - .then(event => { - if (!event.target.result) { - throw new Error('failed to get previously put item'); - } - if (event.target.result.id !== -1) { - throw new Error('item id is wrong'); - } - return dbExecIndexedDB('delete', -1); - }) - .then(() => true); + async function testDB() { + let e = await dbExecIndexedDB('getAllKeys', IDBKeyRange.lowerBound(1), 1); + // throws if result is null + e = e.target.result[0]; + 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); } function useChromeStorage(err) { - exec = createChromeStorageDB().exec; - chromeLocal.set({dbInChromeStorage: true}, ignoreChromeError); + chromeLocal.setValue(FALLBACK, true); if (err) { - chromeLocal.setValue('dbInChromeStorageReason', workerUtil.cloneError(err)); + chromeLocal.setValue(FALLBACK + 'Reason', workerUtil.cloneError(err)); console.warn('Failed to access indexedDB. Switched to storage API.', err); } - localStorage.dbInChromeStorage = 'true'; + localStorage[FALLBACK] = 'true'; + return createChromeStorageDB().exec; } function useIndexedDB() { - exec = dbExecIndexedDB; - chromeLocal.set({dbInChromeStorage: false}, ignoreChromeError); - localStorage.dbInChromeStorage = 'false'; + chromeLocal.setValue(FALLBACK, false); + localStorage[FALLBACK] = 'false'; + return dbExecIndexedDB; } - function dbExecIndexedDB(method, ...args) { - return open().then(database => { - if (!method) { - return database; - } - if (method === 'putMany') { - return putMany(database, ...args); - } - const mode = method.startsWith('get') ? 'readonly' : 'readwrite'; - const transaction = database.transaction(['styles'], mode); - const store = transaction.objectStore('styles'); - return storeRequest(store, method, ...args); + async function dbExecIndexedDB(method, ...args) { + const mode = method.startsWith('get') ? 'readonly' : 'readwrite'; + const store = (await open()).transaction([STORE], mode).objectStore(STORE); + const fn = method === 'putMany' ? putMany : storeRequest; + return fn(store, method, ...args); + } + + function storeRequest(store, method, ...args) { + return new Promise((resolve, reject) => { + const request = store[method](...args); + request.onsuccess = resolve; + request.onerror = reject; }); + } - function storeRequest(store, method, ...args) { - return new Promise((resolve, reject) => { - const request = store[method](...args); - request.onsuccess = resolve; - request.onerror = reject; + function putMany(store, _method, items) { + return Promise.all(items.map(item => storeRequest(store, 'put', item))); + } + + function open() { + return new Promise((resolve, reject) => { + const request = indexedDB.open(DATABASE, 2); + request.onsuccess = () => resolve(request.result); + request.onerror = reject; + request.onupgradeneeded = create; + }); + } + + function create(event) { + if (event.oldVersion === 0) { + event.target.result.createObjectStore(STORE, { + keyPath: 'id', + autoIncrement: true, }); } - - function open() { - return new Promise((resolve, reject) => { - const request = indexedDB.open('stylish', 2); - request.onsuccess = () => resolve(request.result); - request.onerror = reject; - request.onupgradeneeded = event => { - if (event.oldVersion === 0) { - event.target.result.createObjectStore('styles', { - keyPath: 'id', - autoIncrement: true, - }); - } - }; - }); - } - - function putMany(database, items) { - const transaction = database.transaction(['styles'], 'readwrite'); - const store = transaction.objectStore('styles'); - return Promise.all(items.map(item => storeRequest(store, 'put', item))); - } } })(); diff --git a/background/navigator-util.js b/background/navigator-util.js index 67fdc1e7..c1b702c6 100644 --- a/background/navigator-util.js +++ b/background/navigator-util.js @@ -49,8 +49,9 @@ const navigatorUtil = (() => { } return browser.tabs.get(data.tabId) .then(tab => { - if (tab.url === 'chrome://newtab/') { - data.url = tab.url; + const url = tab.pendingUrl || tab.url; + if (url === 'chrome://newtab/') { + data.url = url; } }); } diff --git a/background/search-db.js b/background/search-db.js index 75318304..21ef0572 100644 --- a/background/search-db.js +++ b/background/search-db.js @@ -57,7 +57,7 @@ continue; } for (const part in PARTS) { - const text = style[part]; + const text = part === 'name' ? style.customName || style.name : style[part]; if (text && PARTS[part](text, rx, words, icase)) { results.push(id); break; diff --git a/background/style-manager.js b/background/style-manager.js index cd221738..2339408b 100644 --- a/background/style-manager.js +++ b/background/style-manager.js @@ -1,6 +1,6 @@ /* eslint no-eq-null: 0, eqeqeq: [2, "smart"] */ -/* global createCache db calcStyleDigest db tryRegExp styleCodeEmpty - getStyleWithNoCode msg sync uuidv4 colorScheme */ +/* global createCache db calcStyleDigest db tryRegExp styleCodeEmpty styleSectionGlobal + getStyleWithNoCode msg prefs sync uuidv4 URLS colorScheme */ /* exported styleManager */ 'use strict'; @@ -61,6 +61,8 @@ const styleManager = (() => { username: '' }; + const DELETE_IF_NULL = ['id', 'customName']; + handleLivePreviewConnections(); handleColorScheme(); @@ -237,6 +239,13 @@ const styleManager = (() => { 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 => { @@ -391,8 +400,10 @@ const styleManager = (() => { if (!style.name) { throw new Error('style name is empty'); } - if (style.id == null) { - delete style.id; + for (const key of DELETE_IF_NULL) { + if (style[key] == null) { + delete style[key]; + } } if (!style._id) { style._id = uuidv4(); @@ -460,7 +471,7 @@ const styleManager = (() => { excludedScheme = true; } for (const section of data.sections) { - if (styleCodeEmpty(section.code)) { + if (styleSectionGlobal(section) && styleCodeEmpty(section.code)) { continue; } const match = urlMatchSection(query, section); @@ -484,7 +495,7 @@ const styleManager = (() => { return result; } - function getSectionsByUrl(url, id) { + function getSectionsByUrl(url, id, isInitialApply) { let cache = cachedStyleForUrl.get(url); if (!cache) { cache = { @@ -500,13 +511,13 @@ const styleManager = (() => { .map(i => styles.get(i)) ); } - if (id) { - if (cache.sections[id]) { - return {[id]: cache.sections[id]}; - } - return {}; - } - return cache.sections; + 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); @@ -578,6 +589,16 @@ const styleManager = (() => { 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; } } @@ -605,7 +626,7 @@ const styleManager = (() => { ) { return true; } - if (section.urlPrefixes && section.urlPrefixes.some(p => query.url.startsWith(p))) { + if (section.urlPrefixes && section.urlPrefixes.some(p => p && query.url.startsWith(p))) { return true; } // as per spec the fragment portion is ignored in @-moz-document: @@ -631,15 +652,7 @@ const styleManager = (() => { return 'sloppy'; } // TODO: check for invalid regexps? - if ( - (!section.regexps || !section.regexps.length) && - (!section.urlPrefixes || !section.urlPrefixes.length) && - (!section.urls || !section.urls.length) && - (!section.domains || !section.domains.length) - ) { - return true; - } - return false; + return styleSectionGlobal(section); } function createCompiler(compile) { diff --git a/background/style-via-xhr.js b/background/style-via-xhr.js new file mode 100644 index 00000000..dc7dc1bc --- /dev/null +++ b/background/style-via-xhr.js @@ -0,0 +1,85 @@ +/* global API CHROME prefs */ +'use strict'; + +// eslint-disable-next-line no-unused-expressions +CHROME && (async () => { + const prefId = 'styleViaXhr'; + const blobUrlPrefix = 'blob:' + chrome.runtime.getURL('/'); + const stylesToPass = {}; + + await prefs.initializing; + toggle(prefId, prefs.get(prefId)); + prefs.subscribe([prefId], toggle); + + function toggle(key, value) { + if (!chrome.declarativeContent) { // not yet granted in options page + value = false; + } + if (value) { + const reqFilter = { + urls: [''], + types: ['main_frame', 'sub_frame'], + }; + chrome.webRequest.onBeforeRequest.addListener(prepareStyles, reqFilter); + chrome.webRequest.onHeadersReceived.addListener(passStyles, reqFilter, [ + 'blocking', + 'responseHeaders', + chrome.webRequest.OnHeadersReceivedOptions.EXTRA_HEADERS, + ].filter(Boolean)); + } else { + chrome.webRequest.onBeforeRequest.removeListener(prepareStyles); + chrome.webRequest.onHeadersReceived.removeListener(passStyles); + } + if (!chrome.declarativeContent) { + return; + } + chrome.declarativeContent.onPageChanged.removeRules([prefId], async () => { + if (!value) return; + chrome.declarativeContent.onPageChanged.addRules([{ + id: prefId, + conditions: [ + new chrome.declarativeContent.PageStateMatcher({ + pageUrl: {urlContains: ':'}, + }), + ], + actions: [ + new chrome.declarativeContent.RequestContentScript({ + allFrames: true, + // This runs earlier than document_start + js: chrome.runtime.getManifest().content_scripts[0].js, + }), + ], + }]); + }); + } + + /** @param {chrome.webRequest.WebRequestBodyDetails} req */ + function prepareStyles(req) { + API.getSectionsByUrl(req.url).then(sections => { + const str = JSON.stringify(sections); + if (str !== '{}') { + stylesToPass[req.requestId] = URL.createObjectURL(new Blob([str])).slice(blobUrlPrefix.length); + setTimeout(cleanUp, 600e3, req.requestId); + } + }); + } + + /** @param {chrome.webRequest.WebResponseHeadersDetails} req */ + function passStyles(req) { + const blobId = stylesToPass[req.requestId]; + if (blobId) { + const {responseHeaders} = req; + responseHeaders.push({ + name: 'Set-Cookie', + value: `${chrome.runtime.id}=${prefs.get('disableAll') ? 1 : 0}${blobId}`, + }); + return {responseHeaders}; + } + } + + function cleanUp(key) { + const blobId = stylesToPass[key]; + delete stylesToPass[key]; + if (blobId) URL.revokeObjectURL(blobUrlPrefix + blobId); + } +})(); diff --git a/background/token-manager.js b/background/token-manager.js index 8be21874..755fd9fa 100644 --- a/background/token-manager.js +++ b/background/token-manager.js @@ -1,10 +1,11 @@ -/* global chromeLocal promisifyChrome FIREFOX */ +/* global chromeLocal promisifyChrome webextLaunchWebAuthFlow FIREFOX */ /* exported tokenManager */ 'use strict'; const tokenManager = (() => { promisifyChrome({ - identity: ['launchWebAuthFlow'], + 'windows': ['create', 'update', 'remove'], + 'tabs': ['create', 'update', 'remove'] }); const AUTH = { dropbox: { @@ -36,7 +37,7 @@ const tokenManager = (() => { scopes: ['https://www.googleapis.com/auth/drive.appdata'], revoke: token => { const params = {token}; - return postQuery(`https://accounts.google.com/o/oauth2/revoke?${stringifyQuery(params)}`); + return postQuery(`https://accounts.google.com/o/oauth2/revoke?${new URLSearchParams(params)}`); } }, onedrive: { @@ -136,14 +137,6 @@ const tokenManager = (() => { }); } - function stringifyQuery(obj) { - const search = new URLSearchParams(); - for (const key of Object.keys(obj)) { - search.set(key, obj[key]); - } - return search.toString(); - } - function authUser(name, k, interactive = false) { const provider = AUTH[name]; const state = Math.random().toFixed(8).slice(2); @@ -159,10 +152,11 @@ const tokenManager = (() => { if (provider.authQuery) { Object.assign(query, provider.authQuery); } - const url = `${provider.authURL}?${stringifyQuery(query)}`; - return browser.identity.launchWebAuthFlow({ + const url = `${provider.authURL}?${new URLSearchParams(query)}`; + return webextLaunchWebAuthFlow({ url, - interactive + interactive, + redirect_uri: query.redirect_uri }) .then(url => { const params = new URLSearchParams( @@ -185,7 +179,7 @@ const tokenManager = (() => { code, grant_type: 'authorization_code', client_id: provider.clientId, - redirect_uri: provider.redirect_uri || chrome.identity.getRedirectURL() + redirect_uri: query.redirect_uri }; if (provider.clientSecret) { body.client_secret = provider.clientSecret; @@ -209,11 +203,9 @@ const tokenManager = (() => { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' - } + }, + body: body ? new URLSearchParams(body) : null, }; - if (body) { - options.body = stringifyQuery(body); - } return fetch(url, options) .then(r => { if (r.ok) { diff --git a/background/update.js b/background/update.js index 2a0e02a7..33c2022c 100644 --- a/background/update.js +++ b/background/update.js @@ -116,7 +116,7 @@ } function reportSuccess(saved) { - log(STATES.UPDATED + ` #${style.id} ${style.name}`); + log(STATES.UPDATED + ` #${style.id} ${style.customName || style.name}`); const info = {updated: true, style: saved}; if (port) port.postMessage(info); return info; @@ -139,7 +139,7 @@ if (typeof error === 'object' && error.message) { error = error.message; } - log(STATES.SKIPPED + ` (${error}) #${style.id} ${style.name}`); + log(STATES.SKIPPED + ` (${error}) #${style.id} ${style.customName || style.name}`); const info = {error, STATES, style: getStyleWithNoCode(style)}; if (port) port.postMessage(info); return info; @@ -207,13 +207,6 @@ // keep current state delete json.enabled; - // keep local name customizations - if (style.originalName !== style.name && style.name !== json.name) { - delete json.name; - } else { - json.originalName = json.name; - } - const newStyle = Object.assign({}, style, json); if (styleSectionsEqual(json, style, {checkSource: true})) { // update digest even if save === false as there might be just a space added etc. diff --git a/background/usercss-helper.js b/background/usercss-helper.js index 3f6081f6..00b3a99b 100644 --- a/background/usercss-helper.js +++ b/background/usercss-helper.js @@ -1,15 +1,8 @@ -/* global API_METHODS usercss styleManager deepCopy openURL download URLS */ +/* global API_METHODS usercss styleManager deepCopy */ /* exported usercssHelper */ 'use strict'; const usercssHelper = (() => { - const installCodeCache = {}; - const clearInstallCode = url => delete installCodeCache[url]; - const isResponseText = r => /^text\/(css|plain)(;.*?)?$/i.test(r.headers.get('content-type')); - // in Firefox we have to use a content script to read file:// - const fileLoader = !chrome.app && // not relying on navigator.ua which can be spoofed - (tabId => browser.tabs.executeScript(tabId, {file: '/content/install-hook-usercss.js'}).then(r => r[0])); - API_METHODS.installUsercss = installUsercss; API_METHODS.editSaveUsercss = editSaveUsercss; API_METHODS.configUsercssVars = configUsercssVars; @@ -17,50 +10,6 @@ const usercssHelper = (() => { API_METHODS.buildUsercss = build; API_METHODS.findUsercss = find; - API_METHODS.getUsercssInstallCode = url => { - // when the installer tab is reloaded after the cache is expired, this will throw intentionally - const {code, timer} = installCodeCache[url]; - clearInstallCode(url); - clearTimeout(timer); - return code; - }; - - return { - - testUrl(url) { - return url.includes('.user.') && - /^(https?|file|ftps?):/.test(url) && - /\.user\.(css|styl)$/.test(url.split(/[#?]/, 1)[0]); - }, - - /** @return {Promise<{ code:string, inTab:boolean } | false>} */ - testContents(tabId, url) { - const isFile = url.startsWith('file:'); - const inTab = isFile && Boolean(fileLoader); - return Promise.resolve(isFile || fetch(url, {method: 'HEAD'}).then(isResponseText)) - .then(ok => ok && (inTab ? fileLoader(tabId) : download(url))) - .then(code => /==userstyle==/i.test(code) && {code, inTab}); - }, - - openInstallerPage(tabId, url, {code, inTab} = {}) { - const newUrl = `${URLS.installUsercss}?updateUrl=${encodeURIComponent(url)}`; - if (inTab) { - browser.tabs.get(tabId).then(tab => - openURL({ - url: `${newUrl}&tabId=${tabId}`, - active: tab.active, - index: tab.index + 1, - openerTabId: tabId, - currentWindow: null, - })); - } else { - const timer = setTimeout(clearInstallCode, 10e3, url); - installCodeCache[url] = {code, timer}; - chrome.tabs.update(tabId, {url: newUrl}); - } - }, - }; - function buildMeta(style) { if (style.usercssData) { return Promise.resolve(style); diff --git a/background/usercss-install-helper.js b/background/usercss-install-helper.js new file mode 100644 index 00000000..b854564a --- /dev/null +++ b/background/usercss-install-helper.js @@ -0,0 +1,82 @@ +/* global API_METHODS openURL download URLS tabManager */ +'use strict'; + +(() => { + const installCodeCache = {}; + const clearInstallCode = url => delete installCodeCache[url]; + const isContentTypeText = type => /^text\/(css|plain)(;.*?)?$/i.test(type); + + // in Firefox we have to use a content script to read file:// + const fileLoader = !chrome.app && ( + async tabId => + (await browser.tabs.executeScript(tabId, {file: '/content/install-hook-usercss.js'}))[0]); + + const urlLoader = + async (tabId, url) => ( + url.startsWith('file:') || + tabManager.get(tabId, isContentTypeText.name) || + isContentTypeText((await fetch(url, {method: 'HEAD'})).headers.get('content-type')) + ) && download(url); + + API_METHODS.getUsercssInstallCode = url => { + // when the installer tab is reloaded after the cache is expired, this will throw intentionally + const {code, timer} = installCodeCache[url]; + clearInstallCode(url); + clearTimeout(timer); + return code; + }; + + // Faster installation on known distribution sites to avoid flicker of css text + chrome.webRequest.onBeforeSendHeaders.addListener(({tabId, url}) => { + openInstallerPage(tabId, url, {}); + // Silently suppressing navigation like it never happened + return {redirectUrl: 'javascript:void 0'}; // eslint-disable-line no-script-url + }, { + urls: [ + URLS.usoArchiveRaw + 'usercss/*.user.css', + '*://greasyfork.org/scripts/*/code/*.user.css', + '*://sleazyfork.org/scripts/*/code/*.user.css', + ], + types: ['main_frame'], + }, ['blocking']); + + // Remember Content-Type to avoid re-fetching of the headers in urlLoader as it can be very slow + chrome.webRequest.onHeadersReceived.addListener(({tabId, responseHeaders}) => { + const h = responseHeaders.find(h => h.name.toLowerCase() === 'content-type'); + tabManager.set(tabId, isContentTypeText.name, h && isContentTypeText(h.value) || undefined); + }, { + urls: '%css,%css?*,%styl,%styl?*'.replace(/%/g, '*://*/*.user.').split(','), + types: ['main_frame'], + }, ['responseHeaders']); + + tabManager.onUpdate(async ({tabId, url, oldUrl = ''}) => { + if (url.includes('.user.') && + /^(https?|file|ftps?):/.test(url) && + /\.user\.(css|styl)$/.test(url.split(/[#?]/, 1)[0]) && + !oldUrl.startsWith(URLS.installUsercss)) { + const inTab = url.startsWith('file:') && Boolean(fileLoader); + const code = await (inTab ? fileLoader : urlLoader)(tabId, url); + if (/==userstyle==/i.test(code)) { + openInstallerPage(tabId, url, {code, inTab}); + } + } + }); + + function openInstallerPage(tabId, url, {code, inTab} = {}) { + const newUrl = `${URLS.installUsercss}?updateUrl=${encodeURIComponent(url)}`; + if (inTab) { + browser.tabs.get(tabId).then(tab => + openURL({ + url: `${newUrl}&tabId=${tabId}`, + active: tab.active, + index: tab.index + 1, + openerTabId: tabId, + currentWindow: null, + })); + } else { + const timer = setTimeout(clearInstallCode, 10e3, url); + installCodeCache[url] = {code, timer}; + chrome.tabs.update(tabId, {url: newUrl}); + } + } +})(); diff --git a/content/apply.js b/content/apply.js index 6407de16..7645fb22 100644 --- a/content/apply.js +++ b/content/apply.js @@ -20,6 +20,7 @@ self.INJECTED !== 1 && (() => { /** @type chrome.runtime.Port */ let port; let lazyBadge = IS_FRAME; + let parentDomain; // the popup needs a check as it's not a tab but can be opened in a tab manually for whatever reason if (!IS_TAB) { @@ -42,13 +43,6 @@ self.INJECTED !== 1 && (() => { window.addEventListener(orphanEventId, orphanCheck, true); } - let parentDomain; - - prefs.subscribe(['disableAll'], (key, value) => doDisableAll(value)); - if (IS_FRAME) { - prefs.subscribe(['exposeIframes'], updateExposeIframes); - } - // detect media change in content script // FIXME: move this to background page when following bugs are fixed: // https://bugzilla.mozilla.org/show_bug.cgi?id=1561546 @@ -59,19 +53,52 @@ self.INJECTED !== 1 && (() => { function onInjectorUpdate() { if (!isOrphaned) { updateCount(); - updateExposeIframes(); + const onOff = prefs[styleInjector.list.length ? 'subscribe' : 'unsubscribe']; + onOff(['disableAll'], updateDisableAll); + if (IS_FRAME) { + updateExposeIframes(); + onOff(['exposeIframes'], updateExposeIframes); + } } } - function init() { - return STYLE_VIA_API ? - API.styleViaAPI({method: 'styleApply'}) : - API.getSectionsByUrl(getMatchUrl()).then(styleInjector.apply); + async function init() { + if (STYLE_VIA_API) { + await API.styleViaAPI({method: 'styleApply'}); + } else { + const styles = chrome.app && getStylesViaXhr() || + await API.getSectionsByUrl(getMatchUrl(), null, true); + if (styles.disableAll) { + delete styles.disableAll; + styleInjector.toggle(false); + } + await styleInjector.apply(styles); + } + } + + function getStylesViaXhr() { + if (new RegExp(`(^|\\s|;)${chrome.runtime.id}=\\s*([-\\w]+)\\s*(;|$)`).test(document.cookie)) { + const data = RegExp.$2; + const disableAll = data[0] === '1'; + const url = 'blob:' + chrome.runtime.getURL(data.slice(1)); + document.cookie = `${chrome.runtime.id}=1; max-age=0`; // remove our cookie + let res; + try { + if (!disableAll) { // will get the styles asynchronously + const xhr = new XMLHttpRequest(); + xhr.open('GET', url, false); // synchronous + xhr.send(); + res = JSON.parse(xhr.response); + } + URL.revokeObjectURL(url); + } catch (e) {} + return res; + } } function getMatchUrl() { let matchUrl = location.href; - if (!matchUrl.match(/^(http|file|chrome|ftp)/)) { + if (!chrome.tabs && !matchUrl.match(/^(http|file|chrome|ftp)/)) { // dynamic about: and javascript: iframes don't have an URL yet // so we'll try the parent frame which is guaranteed to have a real URL try { @@ -145,7 +172,7 @@ self.INJECTED !== 1 && (() => { } } - function doDisableAll(disableAll) { + function updateDisableAll(key, disableAll) { if (STYLE_VIA_API) { API.styleViaAPI({method: 'prefChanged', prefs: {disableAll}}); } else { @@ -153,22 +180,18 @@ self.INJECTED !== 1 && (() => { } } - function fetchParentDomain() { - return parentDomain ? - Promise.resolve() : - API.getTabUrlPrefix() - .then(newDomain => { - parentDomain = newDomain; - }); - } - - function updateExposeIframes() { - if (!prefs.get('exposeIframes') || window === parent || !styleInjector.list.length) { - document.documentElement.removeAttribute('stylus-iframe'); + async function updateExposeIframes(key, value = prefs.get('exposeIframes')) { + const attr = 'stylus-iframe'; + const el = document.documentElement; + if (!el) return; // got no styles so styleInjector didn't wait for + if (!value || !styleInjector.list.length) { + el.removeAttribute(attr); } else { - fetchParentDomain().then(() => { - document.documentElement.setAttribute('stylus-iframe', parentDomain); - }); + if (!parentDomain) parentDomain = await API.getTabUrlPrefix(); + // Check first to avoid triggering DOM mutation + if (el.getAttribute(attr) !== parentDomain) { + el.setAttribute(attr, parentDomain); + } } } diff --git a/content/install-hook-greasyfork.js b/content/install-hook-greasyfork.js new file mode 100644 index 00000000..9e125530 --- /dev/null +++ b/content/install-hook-greasyfork.js @@ -0,0 +1,21 @@ +/* global API */ +'use strict'; + +// onCommitted may fire twice +// Note, we're checking against a literal `1`, not just `if (truthy)`, +// because is exposed per HTML spec as a global variable and `window.INJECTED`. + +if (window.INJECTED_GREASYFORK !== 1) { + window.INJECTED_GREASYFORK = 1; + addEventListener('message', async function onMessage(e) { + if (e.origin === location.origin && + e.data && + e.data.name && + e.data.type === 'style-version-query') { + removeEventListener('message', onMessage); + const style = await API.findUsercss(e.data) || {}; + const {version} = style.usercssData || {}; + postMessage({type: 'style-version', version}, '*'); + } + }); +} diff --git a/content/install-hook-usercss.js b/content/install-hook-usercss.js index 80e837ea..6e8be595 100644 --- a/content/install-hook-usercss.js +++ b/content/install-hook-usercss.js @@ -5,16 +5,18 @@ if (typeof self.oldCode !== 'string') { self.oldCode = (document.querySelector('body > pre') || document.body).textContent; chrome.runtime.onConnect.addListener(port => { if (port.name !== 'downloadSelf') return; - port.onMessage.addListener(({id, timer}) => { + port.onMessage.addListener(({id, force}) => { fetch(location.href, {mode: 'same-origin'}) .then(r => r.text()) - .then(code => ({id, code: timer && code === self.oldCode ? null : code})) + .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; }); }); + // FF keeps content scripts connected on navigation https://github.com/openstyles/stylus/issues/864 + addEventListener('pagehide', () => port.disconnect(), {once: true}); }); } diff --git a/content/install-hook-userstyles.js b/content/install-hook-userstyles.js index c0fd8f70..1adc4e89 100644 --- a/content/install-hook-userstyles.js +++ b/content/install-hook-userstyles.js @@ -1,7 +1,11 @@ /* global cloneInto msg API */ 'use strict'; -(() => { +// eslint-disable-next-line no-unused-expressions +/^\/styles\/(\d+)(\/([^/]*))?([?#].*)?$/.test(location.pathname) && (() => { + const styleId = RegExp.$1; + const pageEventId = `${performance.now()}${Math.random()}`; + window.dispatchEvent(new CustomEvent(chrome.runtime.id + '-install')); window.addEventListener(chrome.runtime.id + '-install', orphanCheck, true); @@ -17,35 +21,18 @@ }, '*'); }); - let gotBody = false; let currentMd5; - new MutationObserver(observeDOM).observe(document.documentElement, { - childList: true, - subtree: true, - }); - observeDOM(); + const md5Url = getMeta('stylish-md5-url') || `https://update.userstyles.org/${styleId}.md5`; + Promise.all([ + API.findStyle({md5Url}), + getResource(md5Url), + onDOMready(), + ]).then(checkUpdatability); - function observeDOM() { - if (!gotBody) { - if (!document.body) return; - gotBody = true; - // TODO: remove the following statement when USO pagination title is fixed - document.title = document.title.replace(/^(\d+)&\w+=/, '#$1: '); - const md5Url = getMeta('stylish-md5-url') || location.href; - Promise.all([ - API.findStyle({md5Url}), - getResource(md5Url) - ]) - .then(checkUpdatability); - } - if (document.getElementById('install_button')) { - onDOMready().then(() => { - requestAnimationFrame(() => { - sendEvent(sendEvent.lastEvent); - }); - }); - } - } + document.documentElement.appendChild( + Object.assign(document.createElement('script'), { + textContent: `(${inPageContext})('${pageEventId}')`, + })); function onMessage(msg) { switch (msg.method) { @@ -72,7 +59,7 @@ function checkUpdatability([installedStyle, md5]) { // TODO: remove the following statement when USO is fixed - document.dispatchEvent(new CustomEvent('stylusFixBuggyUSOsettings', { + document.dispatchEvent(new CustomEvent(pageEventId, { detail: installedStyle && installedStyle.updateUrl, })); currentMd5 = md5; @@ -141,7 +128,6 @@ }); } - function onClick(event) { if (onClick.processing || !orphanCheck()) { return; @@ -227,13 +213,11 @@ } } - function getMeta(name) { const e = document.querySelector(`link[rel="${name}"]`); return e ? e.getAttribute('href') : null; } - function getResource(url, options) { if (url.startsWith('#')) { return Promise.resolve(document.getElementById(url.slice(1)).textContent); @@ -280,7 +264,6 @@ .catch(() => null); } - function styleSectionsEqual({sections: a}, {sections: b}) { if (!a || !b) { return undefined; @@ -318,20 +301,12 @@ } } - function onDOMready() { - if (document.readyState !== 'loading') { - return Promise.resolve(); - } - return new Promise(resolve => { - document.addEventListener('DOMContentLoaded', function _() { - document.removeEventListener('DOMContentLoaded', _); - resolve(); - }); - }); + return document.readyState !== 'loading' + ? Promise.resolve() + : new Promise(resolve => document.addEventListener('DOMContentLoaded', resolve, {once: true})); } - function openSettings(countdown = 10e3) { const button = document.querySelector('.customize_button'); if (button) { @@ -349,12 +324,12 @@ } } - function orphanCheck() { - // TODO: switch to install-hook-usercss.js impl, and remove explicit orphanCheck() calls - if (chrome.i18n && chrome.i18n.getUILanguage()) { - return true; - } + try { + if (chrome.i18n.getUILanguage()) { + return true; + } + } catch (e) {} // In Chrome content script is orphaned on an extension update/reload // so we need to detach event listeners window.removeEventListener(chrome.runtime.id + '-install', orphanCheck, true); @@ -366,132 +341,56 @@ } })(); -// run in page context -document.documentElement.appendChild(document.createElement('script')).text = '(' + ( - () => { - document.currentScript.remove(); - - // spoof Stylish extension presence in Chrome - if (window.chrome && chrome.app) { - const realImage = window.Image; - window.Image = function Image(...args) { - return new Proxy(new realImage(...args), { - get(obj, key) { - return obj[key]; - }, - set(obj, key, value) { - if (key === 'src' && /^chrome-extension:/i.test(value)) { - setTimeout(() => typeof obj.onload === 'function' && obj.onload()); - } else { - obj[key] = value; - } - return true; - }, - }); - }; - } - - // USO bug workaround: use the actual style settings in API response - let settings; - const originalResponseJson = Response.prototype.json; - document.addEventListener('stylusFixBuggyUSOsettings', function _({detail}) { - document.removeEventListener('stylusFixBuggyUSOsettings', _); - // TODO: remove .replace(/^\?/, '') when minimum_chrome_version >= 52 (https://crbug.com/601425) - settings = /\?/.test(detail) && new URLSearchParams(new URL(detail).search.replace(/^\?/, '')); - if (!settings) { - Response.prototype.json = originalResponseJson; +function inPageContext(eventId) { + document.currentScript.remove(); + const origMethods = { + json: Response.prototype.json, + byId: document.getElementById, + }; + let vars; + // USO bug workaround: prevent errors in console after install and busy cursor + document.getElementById = id => + origMethods.byId.call(document, id) || + (/^(stylish-code|stylish-installed-style-installed-\w+|post-install-ad|style-install-unknown)$/.test(id) + ? Object.assign(document.createElement('p'), {className: 'afterdownload-ad'}) + : null); + // USO bug workaround: use the actual image data in customized settings + document.addEventListener(eventId, ({detail}) => { + vars = /\?/.test(detail) && new URL(detail).searchParams; + if (!vars) Response.prototype.json = origMethods.json; + }, {once: true}); + Response.prototype.json = async function () { + const json = await origMethods.json.apply(this, arguments); + if (vars && json && Array.isArray(json.style_settings)) { + Response.prototype.json = origMethods.json; + const images = new Map(); + for (const ss of json.style_settings) { + const value = vars.get('ik-' + ss.install_key); + if (value && ss.setting_type === 'image' && ss.style_setting_options) { + let isListed; + for (const opt of ss.style_setting_options) { + isListed |= opt.default = (opt.value === value); + } + images.set(ss.install_key, {url: value, isListed}); + } } - }); - Response.prototype.json = function (...args) { - return originalResponseJson.call(this, ...args).then(json => { - if (!settings || typeof ((json || {}).style_settings || {}).every !== 'function') { - return json; - } - Response.prototype.json = originalResponseJson; - const images = new Map(); - for (const jsonSetting of json.style_settings) { - let value = settings.get('ik-' + jsonSetting.install_key); - if (!value - || !jsonSetting.style_setting_options - || !jsonSetting.style_setting_options[0]) { - continue; - } - if (value.startsWith('ik-')) { - value = value.replace(/^ik-/, ''); - const defaultItem = jsonSetting.style_setting_options.find(item => item.default); - if (!defaultItem || defaultItem.install_key !== value) { - if (defaultItem) { - defaultItem.default = false; - } - jsonSetting.style_setting_options.some(item => { - if (item.install_key === value) { - item.default = true; - return true; - } - }); - } - } else if (jsonSetting.setting_type === 'image') { - jsonSetting.style_setting_options.some(item => { - if (item.default) { - item.default = false; - return true; - } - }); - images.set(jsonSetting.install_key, value); - } else { - const item = jsonSetting.style_setting_options[0]; - if (item.value !== value && item.install_key === 'placeholder') { - item.value = value; - } - } - } - if (images.size) { - new MutationObserver((_, observer) => { - if (!document.getElementById('style-settings')) { - return; - } + if (images.size) { + new MutationObserver((_, observer) => { + if (document.getElementById('style-settings')) { observer.disconnect(); - for (const [name, url] of images.entries()) { + for (const [name, {url, isListed}] of images) { const elRadio = document.querySelector(`input[name="ik-${name}"][value="user-url"]`); - const elUrl = elRadio && document.getElementById(elRadio.id.replace('url-choice', 'user-url')); + const elUrl = elRadio && + document.getElementById(elRadio.id.replace('url-choice', 'user-url')); if (elUrl) { + elRadio.checked = !isListed; elUrl.value = url; } } - }).observe(document, {childList: true, subtree: true}); - } - return json; - }); - }; - } -) + `)('${chrome.runtime.getURL('').slice(0, -1)}')`; - -// TODO: remove the following statement when USO pagination is fixed -if (location.search.includes('category=')) { - document.addEventListener('DOMContentLoaded', function _() { - document.removeEventListener('DOMContentLoaded', _); - new MutationObserver((_, observer) => { - if (!document.getElementById('pagination')) { - return; + } + }).observe(document, {childList: true, subtree: true}); } - observer.disconnect(); - const category = '&' + location.search.match(/category=[^&]+/)[0]; - const links = document.querySelectorAll('#pagination a[href*="page="]:not([href*="category="])'); - for (let i = 0; i < links.length; i++) { - links[i].href += category; - } - }).observe(document, {childList: true, subtree: true}); - }); -} - -if (/^https?:\/\/userstyles\.org\/styles\/\d{3,}/.test(location.href)) { - new MutationObserver((_, observer) => { - const cssButton = document.getElementsByClassName('css_button'); - if (cssButton.length) { - // Click on the "Show CSS Code" button to workaround the JS error - cssButton[0].click(); - cssButton[0].click(); - observer.disconnect(); } - }).observe(document, {childList: true, subtree: true}); + return json; + }; } diff --git a/content/style-injector.js b/content/style-injector.js index 5c9fffd0..8630bf05 100644 --- a/content/style-injector.js +++ b/content/style-injector.js @@ -8,7 +8,6 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({ const PATCH_ID = 'transition-patch'; // styles are out of order if any of these elements is injected between them const ORDERED_TAGS = new Set(['head', 'body', 'frameset', 'style', 'link']); - const IS_OWN_PAGE = Boolean(chrome.tabs); // detect Chrome 65 via a feature it added since browser version can be spoofed const isChromePre65 = chrome.app && typeof Worklet !== 'function'; const docRewriteObserver = RewriteObserver(_sort); @@ -159,7 +158,7 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({ } function _emitUpdate(value) { - _toggleObservers(!IS_OWN_PAGE && list.length); + _toggleObservers(list.length); onUpdate(); return value; } diff --git a/edit.html b/edit.html index 12614c99..5d7d0c00 100644 --- a/edit.html +++ b/edit.html @@ -18,6 +18,22 @@ } + + + + + + + + + + + + + + + + @@ -63,45 +79,27 @@ - - - - - - - - - - - - - - - + - + - + + - - - - - - + @@ -110,8 +108,6 @@ - -