Browse Source

fix: use browser.tabs.executeScript in Firefox

In Firefox, we don't need to run scripts in the page context
since we can reach unsafeWindow by window.wrappedJSObject.

close #385
Gerald 7 years ago
parent
commit
0d61d1f060

+ 3 - 0
scripts/utils.js

@@ -3,6 +3,7 @@ process.env.NODE_ENV = process.env.NODE_ENV || 'development';
 const isDev = process.env.NODE_ENV === 'development';
 const isProd = process.env.NODE_ENV === 'production';
 const isTest = process.env.NODE_ENV === 'test';
+const INIT_FUNC_NAME = 'VMInitInjection';
 
 function styleLoader({
   loaders = [],
@@ -55,9 +56,11 @@ exports.isTest = isTest;
 exports.styleLoader = styleLoader;
 exports.styleRule = styleRule;
 exports.merge = merge;
+exports.INIT_FUNC_NAME = INIT_FUNC_NAME;
 exports.definitions = {
   'process.env': {
     NODE_ENV: JSON.stringify(process.env.NODE_ENV),
     DEBUG: isDev ? 'true' : 'false', // whether to log message errors
+    INIT_FUNC_NAME: JSON.stringify(INIT_FUNC_NAME),
   },
 };

+ 3 - 3
scripts/webpack.conf.js

@@ -5,7 +5,7 @@ const ExtractTextPlugin = require('extract-text-webpack-plugin');
 const HtmlWebpackPlugin = require('html-webpack-plugin');
 const WrapperWebpackPlugin = require('wrapper-webpack-plugin');
 const base = require('./webpack.base.conf');
-const { isProd, merge } = require('./utils');
+const { isProd, merge, INIT_FUNC_NAME } = require('./utils');
 
 const entry = {
   'background/app': 'src/background/app.js',
@@ -68,13 +68,13 @@ targets.push(merge(base, {
   plugins: [
     new WrapperWebpackPlugin({
       header: `\
-window.VM_initializeWeb = function () {
+window.${INIT_FUNC_NAME} = function () {
   var module = { exports: {} };
 `,
       footer: `
   var exports = module.exports;
   return exports.__esModule ? exports['default'] : exports;
-};`,
+};0;`,
     }),
   ],
 }));

+ 6 - 0
src/background/app.js

@@ -207,6 +207,12 @@ const commands = {
   CheckPosition() {
     return sortScripts();
   },
+  // For Firefox
+  InjectScript(code, src) {
+    return browser.tabs.executeScript(src.tab.id, {
+      code: `${code};0`,
+    });
+  },
 };
 
 initialize()

+ 7 - 1
src/injected/content/index.js

@@ -66,6 +66,7 @@ export default function initialize(contentId, webId) {
         return false;
       });
     }
+    data.isFirefox = isFirefox;
     getPopup();
     setBadge();
     const needInject = data.scripts && data.scripts.length;
@@ -157,5 +158,10 @@ function injectScript(data) {
     `function(${wrapperKeys.join(',')}){${code}}`,
     JSON.stringify(vCallbackId),
   ];
-  inject(`!${func.toString()}(${args.join(',')})`);
+  const injectedCode = `!${func.toString()}(${args.join(',')})`;
+  if (isFirefox) {
+    sendMessage({ cmd: 'InjectScript', data: injectedCode });
+  } else {
+    inject(injectedCode);
+  }
 }

+ 13 - 7
src/injected/index.js

@@ -1,4 +1,5 @@
 import 'src/common/browser';
+import { isFirefox } from 'src/common/ua';
 import { inject, getUniqId, sendMessage } from './utils';
 import initialize from './content';
 
@@ -7,9 +8,6 @@ import initialize from './content';
   if (window.VM) return;
   window.VM = 1;
 
-  // eslint-disable-next-line camelcase
-  const { VM_initializeWeb } = window;
-
   function initBridge() {
     const contentId = getUniqId();
     const webId = getUniqId();
@@ -29,11 +27,19 @@ import initialize from './content';
       keys.forEach(key => { props[key] = 1; });
     });
     const args = [
-      JSON.stringify(webId),
-      JSON.stringify(contentId),
-      JSON.stringify(Object.keys(props)),
+      webId,
+      contentId,
+      Object.keys(props),
     ];
-    inject(`(${VM_initializeWeb.toString()}())(${args.join(',')})`);
+    const init = window[process.env.INIT_FUNC_NAME];
+    if (isFirefox) {
+      // In Firefox, unsafeWindow = window.wrappedJSObject
+      // So we don't need to inject the scripts into page context
+      init()(...args);
+    } else {
+      // Avoid using Function::apply in case it is shimmed
+      inject(`(${init.toString()}())(${args.map(arg => JSON.stringify(arg)).join(',')})`);
+    }
   }
 
   initBridge();

+ 5 - 5
src/injected/utils/helpers.js

@@ -2,7 +2,7 @@
 // Firefox sucks: `isFinite` is not defined on `window`, see violentmonkey/violentmonkey#300
 // eslint-disable-next-line no-restricted-properties
 export const {
-  console, CustomEvent, Promise, isFinite,
+  console, CustomEvent, Promise, isFinite, Uint8Array,
 } = global;
 
 const arrayProto = Array.prototype;
@@ -63,14 +63,14 @@ export function encodeBody(body) {
     }, {}))
     .then(value => ({ cls, value }));
   } else if (includes(['blob', 'file'], cls)) {
-    const bufsize = 8192;
     result = new Promise(resolve => {
       const reader = new FileReader();
       reader.onload = () => {
-        let value = '';
+        // In Firefox, Uint8Array cannot be sliced if its data is read by FileReader
         const array = new Uint8Array(reader.result);
-        for (let i = 0; i < array.length; i += bufsize) {
-          value += fromCharCode(...array.subarray(i, i + bufsize));
+        let value = '';
+        for (let i = 0; i < array.length; i += 1) {
+          value += fromCharCode(array[i]);
         }
         resolve({
           cls,

+ 1 - 37
src/injected/utils/index.js

@@ -16,28 +16,7 @@ function removeElement(id) {
   }
 }
 
-// let doInject;
-// export function inject(code) {
-//   if (!doInject) {
-//     const id = getUniqId('VM-');
-//     const detect = domId => {
-//       const span = document.createElement('span');
-//       span.id = domId;
-//       document.documentElement.appendChild(span);
-//     };
-//     injectViaText(`(${detect.toString()})(${jsonDump(id)})`);
-//     if (removeElement(id)) {
-//       doInject = injectViaText;
-//     } else {
-//       // For Firefox in CSP limited pages
-//       doInject = injectViaBlob;
-//     }
-//   }
-//   doInject(code);
-// }
-
-export const inject = injectViaText;
-function injectViaText(code) {
+export function inject(code) {
   const script = document.createElement('script');
   const id = getUniqId('VM-');
   script.id = id;
@@ -47,21 +26,6 @@ function injectViaText(code) {
   removeElement(id);
 }
 
-// Firefox does not support script injection by `textCode` in CSP limited pages
-// have to inject via blob URL, leading to delayed first injection.
-// This is rejected by Firefox reviewer.
-// function injectViaBlob(code) {
-//   const script = document.createElement('script');
-//   // https://en.wikipedia.org/wiki/Byte_order_mark
-//   const blob = new Blob(['\ufeff', code], { type: 'text/javascript' });
-//   const url = URL.createObjectURL(blob);
-//   script.src = url;
-//   document.documentElement.appendChild(script);
-//   const { parentNode } = script;
-//   if (parentNode) parentNode.removeChild(script);
-//   URL.revokeObjectURL(url);
-// }
-
 export function getUniqId() {
   return Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
 }

+ 47 - 31
src/injected/web/index.js

@@ -76,6 +76,7 @@ function onLoadScripts(data) {
   const idle = [];
   const end = [];
   bridge.version = data.version;
+  bridge.isFirefox = data.isFirefox;
   if (includes([
     'greasyfork.org',
   ], window.location.host)) {
@@ -150,9 +151,11 @@ function wrapGM(script, code, cache) {
   const gm = {};
   const grant = script.meta.grant || [];
   const urls = {};
+  const unsafeWindow = bridge.isFirefox ? window.wrappedJSObject : window;
   if (!grant.length || (grant.length === 1 && grant[0] === 'none')) {
     // @grant none
     grant.pop();
+    gm.window = unsafeWindow;
   } else {
     gm.window = getWrapper();
   }
@@ -169,35 +172,31 @@ function wrapGM(script, code, cache) {
   const pathMap = script.custom.pathMap || {};
   const matches = code.match(/\/\/\s+==UserScript==\s+([\s\S]*?)\/\/\s+==\/UserScript==\s/);
   const metaStr = matches ? matches[1] : '';
-  const gmFunctions = {
-    unsafeWindow: { value: window },
-    GM_info: {
-      get() {
-        const obj = {
-          uuid: script.props.uuid,
-          scriptMetaStr: metaStr,
-          scriptWillUpdate: !!script.config.shouldUpdate,
-          scriptHandler: 'Violentmonkey',
-          version: bridge.version,
-          script: {
-            description: script.meta.description || '',
-            excludes: script.meta.exclude.concat(),
-            includes: script.meta.include.concat(),
-            matches: script.meta.match.concat(),
-            name: script.meta.name || '',
-            namespace: script.meta.namespace || '',
-            resources: Object.keys(resources).map(name => ({
-              name,
-              url: resources[name],
-            })),
-            runAt: script.meta.runAt || '',
-            unwrap: false, // deprecated, always `false`
-            version: script.meta.version || '',
-          },
-        };
-        return obj;
-      },
+  const gmInfo = {
+    uuid: script.props.uuid,
+    scriptMetaStr: metaStr,
+    scriptWillUpdate: !!script.config.shouldUpdate,
+    scriptHandler: 'Violentmonkey',
+    version: bridge.version,
+    script: {
+      description: script.meta.description || '',
+      excludes: script.meta.exclude.concat(),
+      includes: script.meta.include.concat(),
+      matches: script.meta.match.concat(),
+      name: script.meta.name || '',
+      namespace: script.meta.namespace || '',
+      resources: Object.keys(resources).map(name => ({
+        name,
+        url: resources[name],
+      })),
+      runAt: script.meta.runAt || '',
+      unwrap: false, // deprecated, always `false`
+      version: script.meta.version || '',
     },
+  };
+  const gmFunctions = {
+    unsafeWindow: { value: unsafeWindow },
+    GM_info: { value: gmInfo },
     GM_deleteValue: {
       value(key) {
         const value = loadValues();
@@ -365,11 +364,20 @@ function getWrapper() {
   // http://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects
   // http://developer.mozilla.org/docs/Web/API/Window
   const wrapper = {};
+  const defined = {};
+  // Block special objects
+  forEach([
+    'browser',
+  ], name => {
+    wrapper[name] = undefined;
+    defined[name] = 1;
+  });
   forEach([
     // `eval` should be called directly so that it is run in current scope
     'eval',
   ], name => {
-    wrapper[name] = window[name];
+    wrapper[name] = global[name];
+    defined[name] = 1;
   });
   forEach([
     // 'uneval',
@@ -419,9 +427,17 @@ function getWrapper() {
     'setTimeout',
     'stop',
   ], name => {
-    const method = window[name];
+    const method = global[name];
     if (method) {
-      wrapper[name] = (...args) => method.apply(window, args);
+      wrapper[name] = (...args) => method.apply(global, args);
+      defined[name] = 1;
+    }
+  });
+  Object.getOwnPropertyNames(global).forEach(name => {
+    const value = global[name];
+    if (!defined[name] && ['object', 'function'].includes(typeof value)) {
+      wrapper[name] = value;
+      defined[name] = 1;
     }
   });
   function defineProtectedProperty(name) {