From 5d306150b01749d60d00b92f903bb6707420bf76 Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 17 Dec 2020 23:13:33 +0300 Subject: [PATCH] use executeScript for early data injection --- background/style-via-webrequest.js | 93 ++++++++++++++---------------- content/apply.js | 6 +- manage/import-export.js | 11 +--- manifest.json | 3 - options/options.js | 13 ----- tools/zip.js | 19 +----- 6 files changed, 52 insertions(+), 93 deletions(-) diff --git a/background/style-via-webrequest.js b/background/style-via-webrequest.js index 00e2e89b..3fd5e077 100644 --- a/background/style-via-webrequest.js +++ b/background/style-via-webrequest.js @@ -11,96 +11,91 @@ define(async require => { const rxHOST = /^('none'|(https?:\/\/)?[^']+?[^:'])$/; // strips CSP sources covered by * const blobUrlPrefix = 'blob:' + chrome.runtime.getURL('/'); const stylesToPass = {}; - const enabled = {}; + const state = {}; await prefs.initializing; - prefs.subscribe([idXHR, idOFF, idCSP], toggle, {runNow: true}); + prefs.subscribe([idXHR, idOFF, idCSP], toggle); + toggle(); function toggle() { - const csp = prefs.get(idCSP) && !prefs.get(idOFF); - const xhr = prefs.get(idXHR) && !prefs.get(idOFF) && Boolean(chrome.declarativeContent); - if (xhr === enabled.xhr && csp === enabled.csp) { + const off = prefs.get(idOFF); + const csp = prefs.get(idCSP) && !off; + const xhr = prefs.get(idXHR) && !off; + if (xhr === state.xhr && csp === state.csp && off === state.off) { return; } - // Need to unregister first so that the optional EXTRA_HEADERS is properly registered + const reqFilter = { + urls: ['*://*/*'], + types: ['main_frame', 'sub_frame'], + }; + chrome.webNavigation.onCommitted.removeListener(injectData); chrome.webRequest.onBeforeRequest.removeListener(prepareStyles); chrome.webRequest.onHeadersReceived.removeListener(modifyHeaders); if (xhr || csp) { - const reqFilter = { - urls: [''], - types: ['main_frame', 'sub_frame'], - }; - chrome.webRequest.onBeforeRequest.addListener(prepareStyles, reqFilter); + // We unregistered it above so that the optional EXTRA_HEADERS is properly re-registered chrome.webRequest.onHeadersReceived.addListener(modifyHeaders, reqFilter, [ 'blocking', 'responseHeaders', xhr && chrome.webRequest.OnHeadersReceivedOptions.EXTRA_HEADERS, ].filter(Boolean)); } - if (enabled.xhr !== xhr) { - enabled.xhr = xhr; - toggleEarlyInjection(); + if (!off) { + chrome.webRequest.onBeforeRequest.addListener(prepareStyles, reqFilter); + chrome.webNavigation.onCommitted.addListener(injectData, {url: [{urlPrefix: 'http'}]}); } - enabled.csp = csp; - } - - /** Runs content scripts earlier than document_start */ - function toggleEarlyInjection() { - const api = chrome.declarativeContent; - if (!api) return; - api.onPageChanged.removeRules([idXHR], async () => { - if (enabled.xhr) { - api.onPageChanged.addRules([{ - id: idXHR, - conditions: [ - new api.PageStateMatcher({ - pageUrl: {urlContains: '://'}, - }), - ], - actions: [ - new api.RequestContentScript({ - js: chrome.runtime.getManifest().content_scripts[0].js, - allFrames: true, - }), - ], - }]); - } - }); + state.csp = csp; + state.off = off; + state.xhr = xhr; } /** @param {chrome.webRequest.WebRequestBodyDetails} req */ async function prepareStyles(req) { const sections = await API.styles.getSectionsByUrl(req.url); if (!isEmptyObj(sections)) { - stylesToPass[req.requestId] = !enabled.xhr || makeObjectUrl(sections); - setTimeout(cleanUp, 600e3, req.requestId); + stylesToPass[req.url] = JSON.stringify(sections); + setTimeout(cleanUp, 600e3, req.url); } } - function makeObjectUrl(sections) { - const blob = new Blob([JSON.stringify(sections)]); + function injectData(req) { + const str = stylesToPass[req.url]; + if (str) { + chrome.tabs.executeScript(req.tabId, { + frameId: req.frameId, + runAt: 'document_start', + code: `(${data => { + if (self.INJECTED !== 1) { // storing data only if apply.js hasn't run yet + window[Symbol.for('styles')] = data; + } + }})(${str})`, + }); + } + } + + function makeObjectUrl(data) { + const blob = new Blob([data]); return URL.createObjectURL(blob).slice(blobUrlPrefix.length); } /** @param {chrome.webRequest.WebResponseHeadersDetails} req */ function modifyHeaders(req) { const {responseHeaders} = req; - const id = stylesToPass[req.requestId]; - if (!id) { + const str = stylesToPass[req.url]; + if (!str) { return; } - if (enabled.xhr) { + if (state.xhr) { responseHeaders.push({ name: 'Set-Cookie', - value: `${chrome.runtime.id}=${id}`, + value: `${chrome.runtime.id}=${makeObjectUrl(str)}`, }); } - const csp = enabled.csp && + const csp = state.csp && responseHeaders.find(h => h.name.toLowerCase() === 'content-security-policy'); if (csp) { patchCsp(csp); } - if (enabled.xhr || csp) { + if (state.xhr || csp) { return {responseHeaders}; } } diff --git a/content/apply.js b/content/apply.js index de80bb2b..f2fe75ee 100644 --- a/content/apply.js +++ b/content/apply.js @@ -58,8 +58,12 @@ define(require => { if (STYLE_VIA_API) { await API.styleViaAPI({method: 'styleApply'}); } else { - const styles = chrome.app && !chrome.tabs && getStylesViaXhr() || + const SYM = Symbol.for('styles'); + const styles = + window[SYM] || + chrome.app && !chrome.tabs && getStylesViaXhr() || await API.styles.getSectionsByUrl(getMatchUrl(), null, true); + delete window[SYM]; if (styles.disableAll) { delete styles.disableAll; styleInjector.toggle(false); diff --git a/manage/import-export.js b/manage/import-export.js index e6e02f58..418d1d34 100644 --- a/manage/import-export.js +++ b/manage/import-export.js @@ -6,11 +6,7 @@ const STYLUS_BACKUP_FILE_EXT = '.json'; define(require => { const {API} = require('/js/msg'); const {isEmptyObj} = require('/js/polyfill'); - const { - CHROME, - deepEqual, - tryJSONparse, - } = require('/js/toolbox'); + const {deepEqual, tryJSONparse} = require('/js/toolbox'); const t = require('/js/localization'); const prefs = require('/js/prefs'); const { @@ -260,11 +256,6 @@ define(require => { } async function importOptions() { - // Must acquire the permission before setting the pref - if (CHROME && !chrome.declarativeContent && - stats.options.names.find(_ => _.name === 'styleViaXhr' && _.isValid && _.val)) { - await browser.permissions.request({permissions: ['declarativeContent']}); - } const oldStorage = await chromeSync.get(); for (const {name, val, isValid, isPref} of stats.options.names) { if (isValid) { diff --git a/manifest.json b/manifest.json index 2d0ccd59..7e012858 100644 --- a/manifest.json +++ b/manifest.json @@ -23,9 +23,6 @@ "identity", "" ], - "optional_permissions": [ - "declarativeContent" - ], "background": { "scripts": [ "js/polyfill.js", diff --git a/options/options.js b/options/options.js index ae6d2c40..360734b6 100644 --- a/options/options.js +++ b/options/options.js @@ -53,19 +53,6 @@ define(require => { $('[data-cmd="open-keyboard"]').classList.remove('chromium-only'); } - if (CHROME && !chrome.declarativeContent) { - // Show the option as disabled until the permission is actually granted - const el = $('#styleViaXhr'); - prefs.initializing.then(() => { - el.checked = false; - }); - el.on('click', () => { - if (el.checked) { - chrome.permissions.request({permissions: ['declarativeContent']}, ignoreChromeError); - } - }); - } - // actions $('#options-close-icon').onclick = () => { top.dispatchEvent(new CustomEvent('closeOptions')); diff --git a/tools/zip.js b/tools/zip.js index 5d975536..f6bfc0fd 100644 --- a/tools/zip.js +++ b/tools/zip.js @@ -3,13 +3,11 @@ const fs = require('fs'); const archiver = require('archiver'); -const manifest = require('../manifest.json'); -function createZip({isFirefox} = {}) { - const fileName = `stylus${isFirefox ? '-firefox' : ''}.zip`; +function createZip() { + const fileName = 'stylus.zip'; const ignore = [ '.*', // dot files/folders (glob, not regexp) - 'vendor/codemirror/lib/**', // get unmodified copy from node_modules 'node_modules/**', 'tools/**', 'package.json', @@ -39,19 +37,7 @@ function createZip({isFirefox} = {}) { }); archive.pipe(file); - if (isFirefox) { - const name = 'manifest.json'; - const keyOpt = 'optional_permissions'; - ignore.unshift(name); - manifest[keyOpt] = manifest[keyOpt].filter(p => p !== 'declarativeContent'); - if (!manifest[keyOpt].length) { - delete manifest[keyOpt]; - } - archive.append(JSON.stringify(manifest, null, ' '), {name, stats: fs.lstatSync(name)}); - } archive.glob('**', {ignore}); - // Don't use modified codemirror.js (see "update-libraries.js") - archive.directory('node_modules/codemirror/lib', 'vendor/codemirror/lib'); archive.finalize(); }); } @@ -59,7 +45,6 @@ function createZip({isFirefox} = {}) { (async () => { try { await createZip(); - await createZip({isFirefox: true}); console.log('\x1b[32m%s\x1b[0m', 'Stylus zip complete'); } catch (err) { console.error(err);