Browse Source

fix: don't redefine `browser` if reinjected + init simplifications

tophf 6 years ago
parent
commit
917c14042e

+ 13 - 4
scripts/webpack.conf.js

@@ -62,15 +62,24 @@ const modify = (extra, init) => modifyWebpackConfig(
   },
 );
 
+// avoid running webpack bootstrap in a potentially hacked environment
+// after documentElement was replaced which triggered reinjection of content scripts
+const skipReinjectionHeader = `if (window[Symbol.for('${INIT_FUNC_NAME}')] !== 1)`;
+const skipReinjectionConfig = (config, test) => config.plugins.push(
+  new WrapperWebpackPlugin({
+    header: skipReinjectionHeader,
+    ...test && { test },
+  }));
+
 module.exports = Promise.all([
-  modify(),
+  modify(null, config => skipReinjectionConfig(config, /^browser\.js$/)),
   modify({
     pages: {
       injected: {
         entry: './src/injected',
       },
     },
-  }),
+  }, skipReinjectionConfig),
   modify({
     pages: {
       'injected-web': {
@@ -81,8 +90,8 @@ module.exports = Promise.all([
     config.output.libraryTarget = 'commonjs2';
     config.plugins.push(
       new WrapperWebpackPlugin({
-        header: `\
-          window.${INIT_FUNC_NAME} = function () {
+        header: `${skipReinjectionHeader}
+          window[Symbol.for('${INIT_FUNC_NAME}')] = function () {
             var module = { exports: {} };
           `,
         footer: `

+ 4 - 1
src/common/browser.js

@@ -2,7 +2,10 @@
 // for DOM elements with 'id' attribute which is a standard feature, more info:
 // https://github.com/mozilla/webextension-polyfill/pull/153
 // https://html.spec.whatwg.org/multipage/window-object.html#named-access-on-the-window-object
-if (!global.browser?.runtime?.sendMessage) {
+if (!global.browser?.runtime?.sendMessage
+// Also don't redefine our `browser` on content script reinjection due to new documentElement
+// because `chrome` was already deleted by us and now it can be spoofed by a userscript
+&& window[Symbol.for(process.env.INIT_FUNC_NAME)] !== 1) {
   const { chrome, Promise } = global;
   const wrapAPIs = (source, meta = {}) => {
     return Object.entries(source)

+ 4 - 2
src/injected/content/index.js

@@ -17,7 +17,9 @@ const menus = {};
 // Make sure to call obj::method() in code that may run after INJECT_CONTENT userscripts
 const { split } = String.prototype;
 
-export default async function initialize(contentId, webId) {
+(async () => {
+  const contentId = getUniqId();
+  const webId = getUniqId();
   // injecting right now before site scripts can mangle globals or intercept our contentId
   // except for XML documents as their appearance breaks, but first we're sending
   // a request for the data because injectPageSandbox takes ~5ms
@@ -32,7 +34,7 @@ export default async function initialize(contentId, webId) {
   if (data.scripts) injectScripts(contentId, webId, data, isXml);
   getPopup();
   setBadge();
-}
+})();
 
 bridge.addBackgroundHandlers({
   Command(data) {

+ 5 - 3
src/injected/content/inject.js

@@ -8,14 +8,16 @@ import {
 import { sendCmd } from '#/common';
 
 import {
-  forEach, join, append, createElementNS, NS_HTML,
+  forEach, join, append, createElementNS, defineProperty, NS_HTML,
   charCodeAt, fromCharCode,
 } from '../utils/helpers';
 import bridge from './bridge';
 
 // Firefox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1408996
-const VMInitInjection = window[process.env.INIT_FUNC_NAME];
-delete window[process.env.INIT_FUNC_NAME];
+const VMInitInjection = window[Symbol.for(process.env.INIT_FUNC_NAME)];
+// To avoid running repeatedly due to new `document.documentElement`
+// (the symbol is undeletable so a userscript can't fool us on reinjection)
+defineProperty(window, Symbol.for(process.env.INIT_FUNC_NAME), { value: 1 });
 
 const { encodeURIComponent } = global;
 const { replace } = String.prototype;

+ 23 - 33
src/injected/index.js

@@ -1,41 +1,31 @@
-import { getUniqId, sendCmd } from './utils';
-import { addEventListener, describeProperty, match } from './utils/helpers';
-import initialize from './content';
-
-(function main() {
-  // Avoid running repeatedly due to new `document.documentElement`
-  const VM_KEY = '__Violentmonkey';
-  // Literal `1` guards against <html id="__Violentmonkey">, more info in browser.js
-  if (window[VM_KEY] === 1) return;
-  window[VM_KEY] = 1;
-
-  function initBridge() {
-    const contentId = getUniqId();
-    const webId = getUniqId();
-    initialize(contentId, webId);
-  }
-
-  initBridge();
+import { sendCmd } from './utils';
+import './content';
 
+// Script installation
+// Firefox does not support `onBeforeRequest` for `file:`
+if (global.location.pathname.endsWith('.user.js')) {
   const { go } = History.prototype;
+  const { document, history } = global;
   const { querySelector } = Document.prototype;
-  const { get: getReadyState } = describeProperty(Document.prototype, 'readyState');
-  // For installation
-  // Firefox does not support `onBeforeRequest` for `file:`
-  async function checkJS() {
+  const referrer = document.referrer;
+  (async () => {
+    if (document.readyState !== 'complete') {
+      await new Promise(resolve => {
+        global.addEventListener('load', resolve, { once: true });
+      });
+    }
+    // plain text shouldn't have a <title>
     if (!document::querySelector('title')) {
-      // plain text
       await sendCmd('ConfirmInstall', {
         code: document.body.textContent,
-        url: window.location.href,
-        from: document.referrer,
+        url: global.location.href,
+        from: referrer,
       });
-      if (window.history.length > 1) window.history::go(-1);
-      else sendCmd('TabClose');
+      if (history.length > 1) {
+        history::go(-1);
+      } else {
+        sendCmd('TabClose');
+      }
     }
-  }
-  if (window.location.pathname::match(/\.user\.js$/)) {
-    if (document::getReadyState() === 'complete') checkJS();
-    else window::addEventListener('load', checkJS, { once: true });
-  }
-}());
+  })();
+}