Browse Source

feat: support inject-into=content

Gerald 7 years ago
parent
commit
f1fd86b376

+ 1 - 0
scripts/webpack.base.conf.js

@@ -25,6 +25,7 @@ const baseConfig = [
     plugins: [
       new webpack.DefinePlugin({
         'process.env.INIT_FUNC_NAME': JSON.stringify(INIT_FUNC_NAME),
+        'process.env.DEBUG': JSON.stringify(process.env.DEBUG || false),
       }),
     ],
   },

+ 6 - 0
src/background/index.js

@@ -216,6 +216,12 @@ const commands = {
   CheckPosition() {
     return sortScripts();
   },
+  InjectScript(code, src) {
+    return browser.tabs.executeScript(src.tab.id, {
+      code: `${code};0`,
+      runAt: 'document_start',
+    });
+  },
 };
 
 initialize()

+ 9 - 11
src/background/utils/db.js

@@ -2,7 +2,9 @@ import {
   i18n, request, buffer2string, getFullUrl, isRemote, getRnd4,
 } from '#/common';
 import { objectGet, objectSet } from '#/common/object';
-import { getNameURI, parseMeta, newScript } from './script';
+import {
+  getNameURI, parseMeta, newScript, getDefaultCustom,
+} from './script';
 import { testScript, testBlacklist } from './tester';
 import { register } from './init';
 import patchDB from './patch-db';
@@ -154,6 +156,12 @@ function initialize() {
         storeInfo.position = Math.max(storeInfo.position, getInt(objectGet(value, 'props.position')));
       }
     });
+    scripts.forEach(script => {
+      script.custom = {
+        ...getDefaultCustom(),
+        ...script.custom,
+      };
+    });
     Object.assign(store, {
       scripts,
       storeInfo,
@@ -186,16 +194,6 @@ export function normalizePosition() {
       objectSet(item, positionKey, position);
       updates.push(item);
     }
-    // XXX patch v2.8.0
-    if (typeof item.custom.origInclude === 'undefined') {
-      item.custom = Object.assign({
-        origInclude: true,
-        origExclude: true,
-        origMatch: true,
-        origExcludeMatch: true,
-      }, item.custom);
-      if (!updates.includes(item)) updates.push(item);
-    }
   });
   store.storeInfo.position = store.scripts.length;
   const { length } = updates;

+ 10 - 6
src/background/utils/script.js

@@ -73,6 +73,15 @@ export function parseMeta(code) {
   return meta;
 }
 
+export function getDefaultCustom() {
+  return {
+    origInclude: true,
+    origExclude: true,
+    origMatch: true,
+    origExcludeMatch: true,
+  };
+}
+
 export function newScript(data) {
   const state = {
     url: '*://*/*',
@@ -84,12 +93,7 @@ export function newScript(data) {
     return value == null ? str : value;
   });
   const script = {
-    custom: {
-      origInclude: true,
-      origExclude: true,
-      origMatch: true,
-      origExcludeMatch: true,
-    },
+    custom: getDefaultCustom(),
     config: {
       enabled: 1,
       shouldUpdate: 1,

+ 1 - 0
src/common/browser.js

@@ -118,6 +118,7 @@ const meta = {
     remove: wrapAsync,
     sendMessage: wrapAsync,
     update: wrapAsync,
+    executeScript: wrapAsync,
   },
   webRequest: true,
 };

+ 3 - 0
src/common/consts.js

@@ -0,0 +1,3 @@
+export const INJECT_PAGE = 'page';
+export const INJECT_CONTENT = 'content';
+export const INJECT_AUTO = 'auto';

+ 88 - 9
src/injected/content/index.js

@@ -1,5 +1,6 @@
 import { isFirefox } from '#/common/ua';
 import { getUniqId } from '#/common';
+import { INJECT_PAGE, INJECT_CONTENT, INJECT_AUTO } from '#/common/consts';
 import {
   bindEvents, sendMessage, inject, attachFunction,
 } from '../utils';
@@ -13,6 +14,9 @@ import dirtySetClipboard from './clipboard';
 
 const IS_TOP = window.top === window;
 
+// Firefox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1408996
+const VMInitInjection = window[process.env.INIT_FUNC_NAME];
+
 const ids = [];
 const enabledIds = [];
 const menus = {};
@@ -46,6 +50,7 @@ const bgHandlers = {
 export default function initialize(contentId, webId) {
   bridge.post = bindEvents(contentId, webId, onHandle);
   bridge.destId = webId;
+  const injectable = checkInjectable();
 
   browser.runtime.onMessage.addListener((req, src) => {
     const handle = bgHandlers[req.cmd];
@@ -60,6 +65,10 @@ export default function initialize(contentId, webId) {
     },
   })
   .then(data => {
+    const scriptLists = {
+      [INJECT_PAGE]: [],
+      [INJECT_CONTENT]: [],
+    };
     if (data.scripts) {
       data.scripts = data.scripts.filter(script => {
         ids.push(script.props.id);
@@ -69,19 +78,82 @@ export default function initialize(contentId, webId) {
         }
         return false;
       });
+      data.scripts.forEach(script => {
+        let injectInto = script.custom.injectInto || script.meta.injectInto || data.injectInto;
+        if (injectInto === INJECT_AUTO) {
+          injectInto = injectable ? INJECT_PAGE : INJECT_CONTENT;
+        }
+        const list = scriptLists[injectInto];
+        if (list) list.push(script);
+      });
     }
     getPopup();
     setBadge();
-    const needInject = data.scripts && data.scripts.length;
-    if (needInject) {
-      bridge.ready.then(() => {
-        bridge.post({ cmd: 'LoadScripts', data });
-      });
+    if (scriptLists[INJECT_PAGE].length || scriptLists[INJECT_CONTENT].length) {
+      injectScripts(contentId, webId, data, scriptLists);
     }
-    return needInject;
   });
 }
 
+function checkInjectable() {
+  const id = getUniqId('VM-');
+  const detect = domId => {
+    const span = document.createElement('span');
+    span.id = domId;
+    document.documentElement.appendChild(span);
+  };
+  inject(`(${detect.toString()})(${JSON.stringify(id)})`);
+  const span = document.querySelector(`#${id}`);
+  const injectable = !!span;
+  if (span) span.parentNode.removeChild(span);
+  return injectable;
+}
+
+function injectScripts(contentId, webId, data, scriptLists) {
+  const props = {};
+  [
+    Object.getOwnPropertyNames(window),
+    Object.getOwnPropertyNames(global),
+  ].forEach(keys => {
+    keys.forEach(key => { props[key] = 1; });
+  });
+  const args = [
+    webId,
+    contentId,
+    Object.keys(props),
+  ];
+
+  const injectPage = scriptLists[INJECT_PAGE];
+  const injectContent = scriptLists[INJECT_CONTENT];
+  if (injectContent.length) {
+    VMInitInjection()(...args, INJECT_CONTENT);
+    bridge.ready.then(() => {
+      bridge.post({
+        cmd: 'LoadScripts',
+        data: {
+          ...data,
+          mode: INJECT_CONTENT,
+          scripts: injectContent,
+        },
+      });
+    });
+  }
+  if (injectPage.length) {
+    // Avoid using Function::apply in case it is shimmed
+    inject(`(${VMInitInjection.toString()}())(${args.map(arg => JSON.stringify(arg)).join(',')})`);
+    bridge.ready.then(() => {
+      bridge.post({
+        cmd: 'LoadScripts',
+        data: {
+          ...data,
+          mode: INJECT_PAGE,
+          scripts: injectPage,
+        },
+      });
+    });
+  }
+}
+
 const handlers = {
   GetRequestId: getRequestId,
   HttpRequest: httpRequest,
@@ -159,7 +231,7 @@ function getPopup() {
 }
 
 function injectScript(data) {
-  const [vId, wrapperKeys, code, vCallbackId] = data;
+  const [vId, code, vCallbackId, mode] = data;
   const func = (attach, id, cb, callbackId) => {
     attach(id, cb);
     const callback = window[callbackId];
@@ -168,9 +240,16 @@ function injectScript(data) {
   const args = [
     attachFunction.toString(),
     JSON.stringify(vId),
-    `function(${wrapperKeys.join(',')}){${code}}`,
+    code,
     JSON.stringify(vCallbackId),
   ];
   const injectedCode = `!${func.toString()}(${args.join(',')})`;
-  inject(injectedCode);
+  if (mode === INJECT_CONTENT) {
+    sendMessage({
+      cmd: 'InjectScript',
+      data: injectedCode,
+    });
+  } else {
+    inject(injectedCode);
+  }
 }

+ 5 - 28
src/injected/index.js

@@ -1,39 +1,16 @@
-import { inject, getUniqId, sendMessage } from './utils';
+import { getUniqId, sendMessage } from './utils';
 import initialize from './content';
 
 (function main() {
   // Avoid running repeatedly due to new `document.documentElement`
-  if (window.VM) return;
-  window.VM = 1;
-
-  // Firefox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1408996
-  const VMInitInjection = window[process.env.INIT_FUNC_NAME];
+  const VM_KEY = '__Violentmonkey';
+  if (window[VM_KEY]) return;
+  window[VM_KEY] = 1;
 
   function initBridge() {
     const contentId = getUniqId();
     const webId = getUniqId();
-    initialize(contentId, webId).then(needInject => {
-      if (needInject) {
-        doInject(contentId, webId);
-      }
-    });
-  }
-
-  function doInject(contentId, webId) {
-    const props = {};
-    [
-      Object.getOwnPropertyNames(window),
-      Object.getOwnPropertyNames(global),
-    ].forEach(keys => {
-      keys.forEach(key => { props[key] = 1; });
-    });
-    const args = [
-      webId,
-      contentId,
-      Object.keys(props),
-    ];
-    // Avoid using Function::apply in case it is shimmed
-    inject(`(${VMInitInjection.toString()}())(${args.map(arg => JSON.stringify(arg)).join(',')})`);
+    initialize(contentId, webId);
   }
 
   initBridge();

+ 29 - 16
src/injected/web/index.js

@@ -1,3 +1,4 @@
+import { INJECT_PAGE, INJECT_CONTENT } from '#/common/consts';
 import {
   getUniqId, bindEvents, attachFunction, cache2blobUrl,
 } from '../utils';
@@ -17,8 +18,14 @@ import { onDownload } from './download';
 
 let state = 0;
 
-export default function initialize(webId, contentId, props) {
+export default function initialize(
+  webId,
+  contentId,
+  props,
+  mode = INJECT_PAGE,
+) {
   bridge.props = props;
+  bridge.mode = mode;
   bridge.post = bindEvents(webId, contentId, onHandle);
   document.addEventListener('DOMContentLoaded', () => {
     state = 1;
@@ -75,6 +82,7 @@ function onHandle(obj) {
 }
 
 function onLoadScripts(data) {
+  if (data.mode !== bridge.mode) return;
   const start = [];
   const idle = [];
   const end = [];
@@ -96,9 +104,7 @@ function onLoadScripts(data) {
   };
   if (data.scripts) {
     forEach(data.scripts, script => {
-      // XXX: use camelCase since v2.6.3
-      const runAt = script.custom.runAt || script.custom['run-at']
-        || script.meta.runAt || script.meta['run-at'];
+      const runAt = script.custom.runAt || script.meta.runAt;
       const list = listMap[runAt] || end;
       list.push(script);
       store.values[script.props.id] = data.values[script.props.id];
@@ -114,7 +120,8 @@ function onLoadScripts(data) {
     const requireKeys = script.meta.require || [];
     const pathMap = script.custom.pathMap || {};
     const code = data.code[script.props.id] || '';
-    const { wrapper, thisObj } = wrapGM(script, code, data.cache);
+    const unsafeWindow = bridge.mode === INJECT_CONTENT ? global : window;
+    const { wrapper, thisObj } = wrapGM(script, code, data.cache, unsafeWindow);
     // Must use Object.getOwnPropertyNames to list unenumerable properties
     const argNames = Object.getOwnPropertyNames(wrapper);
     const wrapperInit = map(argNames, name => `this["${name}"]=${name}`).join(';');
@@ -130,7 +137,7 @@ function onLoadScripts(data) {
     // wrap code to make 'use strict' work
     codeSlices.push(`!function(){${code}\n}.call(this)`);
     codeSlices.push('}.call(this);');
-    const codeConcat = codeSlices.join('\n');
+    const codeConcat = `function(${argNames.join(',')}){${codeSlices.join('\n')}}`;
     const name = script.custom.name || script.meta.name || script.props.id;
     const args = map(argNames, key => wrapper[key]);
     const id = getUniqId('VMin');
@@ -139,20 +146,19 @@ function onLoadScripts(data) {
       const func = window[id];
       if (func) runCode(name, func, args, thisObj);
     });
-    bridge.post({ cmd: 'Inject', data: [id, argNames, codeConcat, fnId] });
+    bridge.post({ cmd: 'Inject', data: [id, codeConcat, fnId, bridge.mode] });
   }
   function run(list) {
     while (list.length) buildCode(list.shift());
   }
 }
 
-function wrapGM(script, code, cache) {
+function wrapGM(script, code, cache, unsafeWindow) {
   // Add GM functions
   // Reference: http://wiki.greasespot.net/Greasemonkey_Manual:API
   const gm = {};
   const grant = script.meta.grant || [];
   const urls = {};
-  const unsafeWindow = window;
   let thisObj = gm;
   if (!grant.length || (grant.length === 1 && grant[0] === 'none')) {
     // @grant none
@@ -181,6 +187,7 @@ function wrapGM(script, code, cache) {
     scriptWillUpdate: !!script.config.shouldUpdate,
     scriptHandler: 'Violentmonkey',
     version: bridge.version,
+    injectInto: bridge.mode,
     script: {
       description: script.meta.description || '',
       excludes: [...script.meta.exclude],
@@ -218,7 +225,7 @@ function wrapGM(script, code, cache) {
           try {
             if (handle) val = handle(val);
           } catch (e) {
-            if (process.env.DEBUG) console.warn(e);
+            if (process.env.DEBUG) log('warn', 'GM_getValue', e);
           }
           return val;
         }
@@ -287,8 +294,7 @@ function wrapGM(script, code, cache) {
     },
     GM_log: {
       value(...args) {
-        // eslint-disable-next-line no-console
-        console.log(`[Violentmonkey][${script.meta.name || 'No name'}]`, ...args);
+        log('log', [script.meta.name || 'No name'], ...args);
       },
     },
     GM_openInTab: {
@@ -479,16 +485,23 @@ function getWrapper(unsafeWindow) {
   return wrapper;
 }
 
+function log(level, tags, ...args) {
+  const tagList = ['Violentmonkey'];
+  if (tags) tagList.push(...tags);
+  const prefix = tagList.map(tag => `[${tag}]`).join('');
+  console[level](prefix, ...args);
+}
+
 function runCode(name, func, args, thisObj) {
   if (process.env.DEBUG) {
-    console.log(`Run script: ${name}`); // eslint-disable-line no-console
+    log('info', [bridge.mode], name);
   }
   try {
     func.apply(thisObj, args);
   } catch (e) {
-    let msg = `Error running script: ${name}\n${e}`;
-    if (e.message) msg = `${msg}\n${e.message}`;
-    console.error(msg);
+    let message = `\n${e}`;
+    if (e.message) message = `${message}\n${e.message}`;
+    log('error', [bridge.mode, name], message);
   }
 }
 

+ 1 - 1
src/injected/web/requests.js

@@ -19,7 +19,7 @@ export function onRequestCreate(details) {
 
 export function onRequestStart(id) {
   const req = queue.shift();
-  start(req, id);
+  if (req) start(req, id);
 }
 
 export function onRequestCallback(res) {

+ 1 - 1
src/options/views/edit/index.vue

@@ -137,7 +137,7 @@ export default {
         match: fromList(custom.match),
         exclude: fromList(custom.exclude),
         excludeMatch: fromList(custom.excludeMatch),
-        runAt: custom.runAt || custom['run-at'] || '',
+        runAt: custom.runAt || '',
       });
       this.settings = settings;
       this.$nextTick(() => {