Browse Source

refactor bridges

Gerald 8 years ago
parent
commit
f221118fda

+ 1 - 1
scripts/webpack.conf.js

@@ -60,7 +60,7 @@ targets.push(Object.assign({}, base, {
 
 targets.push(Object.assign({}, base, {
   entry: {
-    injected: 'src/injected.js',
+    injected: 'src/injected/index.js',
     browser: 'src/browser.js',
   },
   plugins: [

+ 0 - 814
src/injected.js

@@ -1,814 +0,0 @@
-(function injectContent() {
-  // Avoid running repeatedly due to new `document.documentElement`
-  if (window.VM) return;
-  window.VM = 1;
-
-  function getUniqId() {
-    return Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
-  }
-
-  /**
-  * http://www.webtoolkit.info/javascript-utf8.html
-  */
-  function utf8decode(utftext) {
-    /* eslint-disable no-bitwise */
-    let string = '';
-    let i = 0;
-    let c1 = 0;
-    let c2 = 0;
-    let c3 = 0;
-    while (i < utftext.length) {
-      c1 = utftext.charCodeAt(i);
-      if (c1 < 128) {
-        string += String.fromCharCode(c1);
-        i += 1;
-      } else if (c1 > 191 && c1 < 224) {
-        c2 = utftext.charCodeAt(i + 1);
-        string += String.fromCharCode(((c1 & 31) << 6) | (c2 & 63));
-        i += 2;
-      } else {
-        c2 = utftext.charCodeAt(i + 1);
-        c3 = utftext.charCodeAt(i + 2);
-        string += String.fromCharCode(((c1 & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
-        i += 3;
-      }
-    }
-    return string;
-    /* eslint-enable no-bitwise */
-  }
-
-  function sendMessage(data) {
-    return browser.runtime.sendMessage(data);
-  }
-
-  const badge = {
-    number: 0,
-    ready: false,
-    willSet: false,
-  };
-  function getBadge() {
-    badge.willSet = true;
-    setBadge();
-  }
-  function setBadge() {
-    if (badge.ready && badge.willSet) {
-      // XXX: only scripts run in top level window are counted
-      if (top === window) sendMessage({ cmd: 'SetBadge', data: badge.number });
-    }
-  }
-  function handleWeb(obj) {
-    const comm = this;
-    const handlers = {
-      LoadScripts(data) {
-        comm.onLoadScripts(data);
-      },
-      Command(data) {
-        const func = comm.command[data];
-        if (func) func();
-      },
-      GotRequestId(id) {
-        const req = comm.qrequests.shift();
-        req.start(req, id);
-      },
-      HttpRequested(res) {
-        const req = comm.requests[res.id];
-        if (req) req.callback(req, res);
-      },
-      UpdateValues(data) {
-        const { values } = comm;
-        if (values && values[data.uri]) values[data.uri] = data.values;
-      },
-      NotificationClicked(id) {
-        const options = comm.notif[id];
-        if (options) {
-          const { onclick } = options;
-          if (onclick) onclick();
-        }
-      },
-      NotificationClosed(id) {
-        const options = comm.notif[id];
-        if (options) {
-          delete comm.notif[id];
-          const { ondone } = options;
-          if (ondone) ondone();
-        }
-      },
-      // advanced inject
-      Injected(id) {
-        const item = comm.ainject[id];
-        const func = window[`VM_${id}`];
-        delete window[`VM_${id}`];
-        delete comm.ainject[id];
-        if (item && func) comm.runCode(item[0], func, item[1], item[2]);
-      },
-    };
-    const handle = handlers[obj.cmd];
-    if (handle) handle(obj.data);
-  }
-
-  /**
-  * @desc Wrap methods to prevent unexpected modifications.
-  */
-  function getWrapper() {
-    // http://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects
-    // http://developer.mozilla.org/docs/Web/API/Window
-    const comm = this;
-    const wrapper = {};
-    comm.forEach([
-      // `eval` should be called directly so that it is run in current scope
-      'eval',
-    ], name => {
-      wrapper[name] = window[name];
-    });
-    comm.forEach([
-      // 'uneval',
-      'isFinite',
-      'isNaN',
-      'parseFloat',
-      'parseInt',
-      'decodeURI',
-      'decodeURIComponent',
-      'encodeURI',
-      'encodeURIComponent',
-
-      'addEventListener',
-      'alert',
-      'atob',
-      'blur',
-      'btoa',
-      'clearInterval',
-      'clearTimeout',
-      'close',
-      'confirm',
-      'dispatchEvent',
-      'fetch',
-      'find',
-      'focus',
-      'getComputedStyle',
-      'getSelection',
-      'matchMedia',
-      'moveBy',
-      'moveTo',
-      'open',
-      'openDialog',
-      'postMessage',
-      'print',
-      'prompt',
-      'removeEventListener',
-      'requestAnimationFrame',
-      'resizeBy',
-      'resizeTo',
-      'scroll',
-      'scrollBy',
-      'scrollByLines',
-      'scrollByPages',
-      'scrollTo',
-      'setInterval',
-      'setTimeout',
-      'stop',
-    ], name => {
-      const method = window[name];
-      if (method) {
-        wrapper[name] = (...args) => method.apply(window, args);
-      }
-    });
-    function defineProtectedProperty(name) {
-      let modified = false;
-      let value;
-      Object.defineProperty(wrapper, name, {
-        get() {
-          if (!modified) value = window[name];
-          return value === window ? wrapper : value;
-        },
-        set(val) {
-          modified = true;
-          value = val;
-        },
-      });
-    }
-    function defineReactedProperty(name) {
-      Object.defineProperty(wrapper, name, {
-        get() {
-          const value = window[name];
-          return value === window ? wrapper : value;
-        },
-        set(val) {
-          window[name] = val;
-        },
-      });
-    }
-    // Wrap properties
-    comm.forEach(comm.props, name => {
-      if (name in wrapper) return;
-      if (name.slice(0, 2) === 'on') defineReactedProperty(name);
-      else defineProtectedProperty(name);
-    });
-    return wrapper;
-  }
-  function initHandler(src, dest) {
-    const comm = this;
-    comm.sid = comm.vmid + src;
-    comm.did = comm.vmid + dest;
-    const handle = comm[`handle_${src}`];
-    document.addEventListener(comm.sid, e => {
-      const data = JSON.parse(e.detail);
-      handle.call(comm, data);
-    }, false);
-    comm.load = comm.noop;
-    comm.checkLoad = comm.noop;
-  }
-  function postData(id, data) {
-    // Firefox issue: data must be stringified to avoid cross-origin problem
-    const e = new CustomEvent(id, { detail: JSON.stringify(data) });
-    document.dispatchEvent(e);
-  }
-  function getRequest(arg) {
-    const comm = this;
-    init();
-    return comm.getRequest(arg);
-    function init() {
-      comm.requests = {};
-      comm.qrequests = [];
-      comm.getRequest = details => {
-        const req = {
-          details,
-          callback,
-          start,
-          req: {
-            abort: reqAbort,
-          },
-        };
-        details.url = getFullUrl(details.url);
-        comm.qrequests.push(req);
-        comm.post({ cmd: 'GetRequestId' });
-        return req.req;
-      };
-    }
-    function reqAbort() {
-      comm.post({ cmd: 'AbortRequest', data: this.id });
-    }
-    function parseData(req, details) {
-      if (req.resType) {
-        // blob or arraybuffer
-        let data = req.data.response.split(',');
-        const mimetype = data[0].match(/^data:(.*?);base64$/);
-        if (!mimetype) {
-          // invalid
-          req.data.response = null;
-        } else {
-          data = window.atob(data[1]);
-          const arr = new window.Uint8Array(data.length);
-          for (let i = 0; i < data.length; i += 1) arr[i] = data.charCodeAt(i);
-          if (details.responseType === 'blob') {
-            // blob
-            return new Blob([arr], { type: mimetype });
-          }
-          // arraybuffer
-          return arr.buffer;
-        }
-      } else if (details.responseType === 'json') {
-        // json
-        return JSON.parse(req.data.response);
-      } else {
-        // text
-        return req.data.response;
-      }
-    }
-    // request object functions
-    function callback(req, res) {
-      const cb = req.details[`on${res.type}`];
-      if (cb) {
-        if (res.data.response) {
-          if (!req.data) req.data = [parseData(res, req.details)];
-          res.data.response = req.data[0];
-        }
-        res.data.context = req.details.context;
-        cb(res.data);
-      }
-      if (res.type === 'loadend') delete comm.requests[req.id];
-    }
-    function start(req, id) {
-      const { details } = req;
-      const data = {
-        id,
-        method: details.method,
-        url: details.url,
-        data: details.data,
-        // async: !details.synchronous,
-        user: details.user,
-        password: details.password,
-        headers: details.headers,
-        overrideMimeType: details.overrideMimeType,
-      };
-      req.id = id;
-      comm.requests[id] = req;
-      if (comm.includes(['arraybuffer', 'blob'], details.responseType)) {
-        data.responseType = 'blob';
-      }
-      comm.post({ cmd: 'HttpRequest', data });
-    }
-    function getFullUrl(url) {
-      const a = document.createElement('a');
-      a.setAttribute('href', url);
-      return a.href;
-    }
-  }
-  function wrapGM(script, cache) {
-    // Add GM functions
-    // Reference: http://wiki.greasespot.net/Greasemonkey_Manual:API
-    const comm = this;
-    const gm = {};
-    const grant = script.meta.grant || [];
-    const urls = {};
-    if (!grant.length || (grant.length === 1 && grant[0] === 'none')) {
-      // @grant none
-      grant.pop();
-    } else {
-      gm.window = comm.getWrapper();
-    }
-    if (!comm.includes(grant, 'unsafeWindow')) grant.push('unsafeWindow');
-    if (!comm.includes(grant, 'GM_info')) grant.push('GM_info');
-    const resources = script.meta.resources || {};
-    const dataEncoders = {
-      o: val => JSON.stringify(val),
-      '': val => val.toString(),
-    };
-    const dataDecoders = {
-      n: val => Number(val),
-      b: val => val === 'true',
-      o: val => JSON.parse(val),
-      '': val => val,
-    };
-    const gmFunctions = {
-      unsafeWindow: { value: window },
-      GM_info: {
-        get() {
-          const matches = script.code.match(/\/\/\s+==UserScript==\s+([\s\S]*?)\/\/\s+==\/UserScript==\s/);
-          const obj = {
-            scriptMetaStr: matches ? matches[1] : '',
-            scriptWillUpdate: !!script.update,
-            scriptHandler: 'Violentmonkey',
-            version: comm.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],
-              })),
-              'run-at': script.meta['run-at'] || '',
-              unwrap: false,  // deprecated, always `false`
-              version: script.meta.version || '',
-            },
-          };
-          return obj;
-        },
-      },
-      GM_deleteValue: {
-        value(key) {
-          const values = getValues();
-          delete values[key];
-          saveValues();
-        },
-      },
-      GM_getValue: {
-        value(key, def) {
-          const values = getValues();
-          const raw = values[key];
-          if (raw) {
-            const type = raw[0];
-            const handle = dataDecoders[type] || dataDecoders[''];
-            let value = raw.slice(1);
-            try {
-              value = handle(value);
-            } catch (e) {
-              console.warn(e);
-            }
-            return value;
-          }
-          return def;
-        },
-      },
-      GM_listValues: {
-        value() {
-          return Object.keys(getValues());
-        },
-      },
-      GM_setValue: {
-        value(key, val) {
-          const type = (typeof val)[0];
-          const handle = dataEncoders[type] || dataEncoders[''];
-          let value = val;
-          value = type + handle(value);
-          const values = getValues();
-          values[key] = value;
-          saveValues();
-        },
-      },
-      GM_getResourceText: {
-        value(name) {
-          if (name in resources) {
-            const uri = resources[name];
-            const raw = cache[uri];
-            const text = raw && comm.utf8decode(window.atob(raw));
-            return text;
-          }
-        },
-      },
-      GM_getResourceURL: {
-        value(name) {
-          if (name in resources) {
-            const key = resources[name];
-            let blobUrl = urls[key];
-            if (!blobUrl) {
-              const raw = cache[key];
-              if (raw) {
-                // Binary string is not supported by blob constructor,
-                // so we have to transform it into array buffer.
-                const bin = window.atob(raw);
-                const arr = new window.Uint8Array(bin.length);
-                for (let i = 0; i < bin.length; i += 1) arr[i] = bin.charCodeAt(i);
-                const blob = new Blob([arr]);
-                blobUrl = URL.createObjectURL(blob);
-                urls[key] = blobUrl;
-              } else {
-                blobUrl = key;
-              }
-            }
-            return blobUrl;
-          }
-        },
-      },
-      GM_addStyle: {
-        value(data) {
-          comm.post({ cmd: 'AddStyle', data });
-        },
-      },
-      GM_log: {
-        value(data) {
-          console.log(`[Violentmonkey][${script.meta.name || 'No name'}]`, data);  // eslint-disable-line no-console
-        },
-      },
-      GM_openInTab: {
-        value(url, background) {
-          comm.post({ cmd: 'OpenTab', data: { url, active: !background } });
-        },
-      },
-      GM_registerMenuCommand: {
-        value(cap, func, acc) {
-          comm.command[cap] = func;
-          comm.post({ cmd: 'RegisterMenu', data: [cap, acc] });
-        },
-      },
-      GM_xmlhttpRequest: {
-        value(details) {
-          return comm.getRequest(details);
-        },
-      },
-      GM_notification: {
-        value(text, title, image, onclick) {
-          const options = typeof text === 'object' ? text : {
-            text,
-            title,
-            image,
-            onclick,
-          };
-          if (!options.text) {
-            throw new Error('GM_notification: `text` is required!');
-          }
-          const id = (comm.notif[''] || 0) + 1;
-          comm.notif[''] = id;
-          comm.notif[id] = options;
-          comm.post({
-            cmd: 'Notification',
-            data: {
-              id,
-              text: options.text,
-              title: options.title,
-              image: options.image,
-            },
-          });
-        },
-      },
-      GM_setClipboard: {
-        value(data, type) {
-          comm.post({
-            cmd: 'SetClipboard',
-            data: { type, data },
-          });
-        },
-      },
-    };
-    comm.forEach(grant, name => {
-      const prop = gmFunctions[name];
-      if (prop) addProperty(name, prop, gm);
-    });
-    return gm;
-    function getValues() {
-      return comm.values[script.uri];
-    }
-    function propertyToString() {
-      return '[Violentmonkey property]';
-    }
-    function addProperty(name, prop, obj) {
-      if ('value' in prop) prop.writable = false;
-      prop.configurable = false;
-      Object.defineProperty(obj, name, prop);
-      if (typeof obj[name] === 'function') obj[name].toString = propertyToString;
-    }
-    function saveValues() {
-      comm.post({
-        cmd: 'SetValue',
-        data: {
-          uri: script.uri,
-          values: getValues(),
-        },
-      });
-    }
-  }
-  function onLoadScripts(data) {
-    const comm = this;
-    const start = [];
-    const idle = [];
-    const end = [];
-    comm.command = {};
-    comm.notif = {};
-    comm.ainject = {};
-    comm.version = data.version;
-    comm.values = {};
-    // reset load and checkLoad
-    comm.load = () => {
-      run(end);
-      setTimeout(run, 0, idle);
-    };
-    comm.checkLoad = () => {
-      if (!comm.state && comm.includes(['interactive', 'complete'], document.readyState)) comm.state = 1;
-      if (comm.state) comm.load();
-    };
-    const listMap = {
-      'document-start': start,
-      'document-idle': idle,
-      'document-end': end,
-    };
-    comm.forEach(data.scripts, script => {
-      comm.values[script.uri] = data.values[script.uri] || {};
-      if (script && script.enabled) {
-        const list = listMap[script.custom['run-at'] || script.meta['run-at']] || end;
-        list.push(script);
-      }
-    });
-    run(start);
-    comm.checkLoad();
-    function buildCode(script) {
-      const requireKeys = script.meta.require || [];
-      const wrapper = comm.wrapGM(script, data.cache);
-      // Must use Object.getOwnPropertyNames to list unenumerable properties
-      const wrapperKeys = Object.getOwnPropertyNames(wrapper);
-      const wrapperInit = comm.map(wrapperKeys, name => `this["${name}"]=${name}`).join(';');
-      const codeSlices = [`${wrapperInit};with(this)!function(){`];
-      comm.forEach(requireKeys, key => {
-        const requireCode = data.require[key];
-        if (requireCode) {
-          codeSlices.push(requireCode);
-          // Add `;` to a new line in case script ends with comment lines
-          codeSlices.push(';');
-        }
-      });
-      // wrap code to make 'use strict' work
-      codeSlices.push(`!function(){${script.code}\n}.call(this)`);
-      codeSlices.push('}.call(this);');
-      const code = codeSlices.join('\n');
-      const name = script.custom.name || script.meta.name || script.id;
-      const args = comm.map(wrapperKeys, key => wrapper[key]);
-      const thisObj = wrapper.window || wrapper;
-      const id = comm.getUniqId();
-      comm.ainject[id] = [name, args, thisObj];
-      comm.post({ cmd: 'Inject', data: [id, wrapperKeys, code] });
-    }
-    function run(list) {
-      while (list.length) buildCode(list.shift());
-    }
-  }
-  // Communicator
-  const comm = {
-    vmid: `VM_${getUniqId()}`,
-    state: 0,
-    utf8decode,
-    getUniqId,
-    props: Object.getOwnPropertyNames(window),
-    noop() {},
-
-    // Array functions
-    // Notice: avoid using prototype functions since they may be changed by page scripts
-    forEach(arr, func) {
-      const length = arr && arr.length;
-      for (let i = 0; i < length; i += 1) func(arr[i], i, arr);
-    },
-    includes(arr, item) {
-      const length = arr && arr.length;
-      for (let i = 0; i < length; i += 1) {
-        if (arr[i] === item) return true;
-      }
-      return false;
-    },
-    map(arr, func) {
-      const res = [];
-      this.forEach(arr, (item, i) => {
-        res.push(func(item, i, arr));
-      });
-      return res;
-    },
-
-    init: initHandler,
-    postData,
-    post(data) {
-      return this.postData(this.did, data);
-    },
-    runCode(name, func, args, thisObj) {
-      if (process.env.DEBUG) {
-        console.log(`Run script: ${name}`); // eslint-disable-line no-console
-      }
-      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);
-      }
-    },
-    getRequest,
-    getWrapper,
-    wrapGM,
-    onLoadScripts,
-  };
-
-  const ids = [];
-  const menus = [];
-  function injectScript(data) {
-    // data: [id, wrapperKeys, code]
-    const func = (scriptId, destId, cb, post) => {
-      Object.defineProperty(window, `VM_${scriptId}`, {
-        value: cb,
-        configurable: true,
-      });
-      post(destId, { cmd: 'Injected', data: scriptId });
-    };
-    inject(`!${func.toString()}(${JSON.stringify(data[0])},${JSON.stringify(comm.did)},function(${data[1].join(',')}){${data[2]}},${postData.toString()})`);
-  }
-  function handleContent(req) {
-    if (!req) {
-      console.error('[Violentmonkey] Invalid data! There might be unsupported data format.');
-      return;
-    }
-    const handlers = {
-      GetRequestId: getRequestId,
-      HttpRequest: httpRequest,
-      AbortRequest: abortRequest,
-      Inject: injectScript,
-      OpenTab(data) {
-        sendMessage({ cmd: 'OpenTab', data });
-      },
-      SetValue(data) {
-        sendMessage({ cmd: 'SetValue', data });
-      },
-      RegisterMenu(data) {
-        if (window.top === window) menus.push(data);
-        getPopup();
-      },
-      AddStyle(css) {
-        if (document.head) {
-          const style = document.createElement('style');
-          style.textContent = css;
-          document.head.appendChild(style);
-        }
-      },
-      Notification: onNotificationCreate,
-      SetClipboard(data) {
-        sendMessage({ cmd: 'SetClipboard', data });
-      },
-    };
-    const handle = handlers[req.cmd];
-    if (handle) handle(req.data);
-  }
-  function getPopup() {
-    // XXX: only scripts run in top level window are counted
-    if (top === window) {
-      sendMessage({
-        cmd: 'SetPopup',
-        data: { ids, menus },
-      })
-      .catch(comm.noop);
-    }
-  }
-
-  const notifications = {};
-  function onNotificationCreate(options) {
-    sendMessage({ cmd: 'Notification', data: options })
-    .then(nid => { notifications[nid] = options.id; });
-  }
-  function onNotificationClick(nid) {
-    const id = notifications[nid];
-    if (id) comm.post({ cmd: 'NotificationClicked', data: id });
-  }
-  function onNotificationClose(nid) {
-    const id = notifications[nid];
-    if (id) {
-      comm.post({ cmd: 'NotificationClosed', data: id });
-      delete notifications[nid];
-    }
-  }
-
-  // Messages
-  browser.runtime.onMessage.addListener((req, src) => {
-    const handlers = {
-      Command(data) {
-        comm.post({ cmd: 'Command', data });
-      },
-      GetPopup: getPopup,
-      GetBadge: getBadge,
-      HttpRequested: httpRequested,
-      UpdateValues(data) {
-        comm.post({ cmd: 'UpdateValues', data });
-      },
-      NotificationClick: onNotificationClick,
-      NotificationClose: onNotificationClose,
-    };
-    const handle = handlers[req.cmd];
-    if (handle) handle(req.data, src);
-  });
-
-  // Requests
-  const requests = {};
-  function getRequestId() {
-    sendMessage({ cmd: 'GetRequestId' })
-    .then(id => {
-      requests[id] = 1;
-      comm.post({ cmd: 'GotRequestId', data: id });
-    });
-  }
-  function httpRequest(details) {
-    sendMessage({ cmd: 'HttpRequest', data: details });
-  }
-  function httpRequested(data) {
-    if (requests[data.id]) {
-      if (data.type === 'loadend') delete requests[data.id];
-      comm.post({ cmd: 'HttpRequested', data });
-    }
-  }
-  function abortRequest(id) {
-    sendMessage({ cmd: 'AbortRequest', data: id });
-  }
-
-  function objEncode(obj) {
-    const list = Object.keys(obj).map(name => {
-      const value = obj[name];
-      const jsonKey = JSON.stringify(name);
-      if (typeof value === 'function') return `${jsonKey}:${value.toString()}`;
-      return `${jsonKey}:${JSON.stringify(value)}`;
-    });
-    return `{${list.join(',')}}`;
-  }
-  function inject(code) {
-    const script = document.createElement('script');
-    const doc = document.body || document.documentElement;
-    script.textContent = code;
-    doc.appendChild(script);
-    try {
-      doc.removeChild(script);
-    } catch (e) {
-      // ignore if body is changed and script is detached
-    }
-  }
-  function loadScripts(data) {
-    comm.forEach(data.scripts, script => {
-      ids.push(script.id);
-      if (script.enabled) badge.number += 1;
-    });
-    comm.post({ cmd: 'LoadScripts', data });
-    badge.ready = true;
-    getPopup();
-    setBadge();
-  }
-  function initCommunicator() {
-    const contentId = getUniqId();
-    const webId = getUniqId();
-    comm[`handle_${webId}`] = handleWeb;
-    const initWeb = (comm, webId, contentId) => { // eslint-disable-line no-shadow
-      comm.init(webId, contentId);
-      document.addEventListener('DOMContentLoaded', () => {
-        comm.state = 1;
-        comm.load();
-      }, false);
-      comm.checkLoad();
-    };
-    inject(`(${initWeb.toString()})(${objEncode(comm)},${JSON.stringify(webId)},${JSON.stringify(contentId)})`);
-    comm[`handle_${contentId}`] = handleContent;
-    comm.init(contentId, webId);
-    sendMessage({ cmd: 'GetInjected', data: location.href }).then(loadScripts);
-  }
-  initCommunicator();
-}());

+ 55 - 0
src/injected/bridge.js

@@ -0,0 +1,55 @@
+/**
+ * All functions to be injected into web page must be independent.
+ * They must be assigned to `bridge` so that they can be serialized.
+ */
+import { noop, getUniqId, postData } from './utils';
+
+function post(data) {
+  const bridge = this;
+  bridge.postData(bridge.destId, data);
+}
+
+function bindEvents(src, dest) {
+  const bridge = this;
+  const { vmid } = bridge;
+  const srcId = vmid + src;
+  bridge.destId = vmid + dest;
+  document.addEventListener(srcId, e => {
+    const data = JSON.parse(e.detail);
+    bridge.handle(data);
+  }, false);
+}
+
+// Array functions
+// Notice: avoid using prototype functions since they may be changed by page scripts
+function forEach(arr, func) {
+  const length = arr && arr.length;
+  for (let i = 0; i < length; i += 1) func(arr[i], i, arr);
+}
+function includes(arr, item) {
+  const length = arr && arr.length;
+  for (let i = 0; i < length; i += 1) {
+    if (arr[i] === item) return true;
+  }
+  return false;
+}
+function map(arr, func) {
+  const bridge = this;
+  const res = [];
+  bridge.forEach(arr, (item, i) => {
+    res.push(func(item, i, arr));
+  });
+  return res;
+}
+
+export default {
+  postData,
+  post,
+  getUniqId,
+  forEach,
+  includes,
+  map,
+  noop,
+  bindEvents,
+  vmid: `VM_${getUniqId()}`,
+};

+ 86 - 0
src/injected/content.js

@@ -0,0 +1,86 @@
+import base from './bridge';
+import { getRequestId, httpRequest, abortRequest } from './requests';
+import { inject, postData, sendMessage, noop } from './utils';
+import { onNotificationCreate } from './notification';
+
+const ids = [];
+const menus = [];
+
+const bridge = Object.assign({
+  initialize,
+  getPopup,
+  ids,
+  menus,
+  handle: handleContent,
+}, base);
+
+export default bridge;
+
+function getPopup() {
+  // XXX: only scripts run in top level window are counted
+  if (top === window) {
+    sendMessage({
+      cmd: 'SetPopup',
+      data: { ids, menus },
+    })
+    .catch(noop);
+  }
+}
+
+function injectScript(data) {
+  const [id, wrapperKeys, code] = data;
+  const func = (scriptId, cb, post, destId) => {
+    Object.defineProperty(window, `VM_${scriptId}`, {
+      value: cb,
+      configurable: true,
+    });
+    post(destId, { cmd: 'Injected', data: scriptId });
+  };
+  const args = [
+    JSON.stringify(id),
+    `function(${wrapperKeys.join(',')}){${code}}`,
+    postData.toString(),
+    JSON.stringify(bridge.destId),
+  ];
+  inject(`!${func.toString()}(${args.join(',')})`);
+}
+
+function handleContent(req) {
+  if (!req) {
+    console.error('[Violentmonkey] Invalid data! There might be unsupported data format.');
+    return;
+  }
+  const handlers = {
+    GetRequestId: getRequestId,
+    HttpRequest: httpRequest,
+    AbortRequest: abortRequest,
+    Inject: injectScript,
+    OpenTab(data) {
+      sendMessage({ cmd: 'OpenTab', data });
+    },
+    SetValue(data) {
+      sendMessage({ cmd: 'SetValue', data });
+    },
+    RegisterMenu(data) {
+      if (window.top === window) menus.push(data);
+      getPopup();
+    },
+    AddStyle(css) {
+      if (document.head) {
+        const style = document.createElement('style');
+        style.textContent = css;
+        document.head.appendChild(style);
+      }
+    },
+    Notification: onNotificationCreate,
+    SetClipboard(data) {
+      sendMessage({ cmd: 'SetClipboard', data });
+    },
+  };
+  const handle = handlers[req.cmd];
+  if (handle) handle(req.data);
+}
+
+function initialize(src, dest) {
+  bridge.bindEvents(src, dest);
+}

+ 79 - 0
src/injected/index.js

@@ -0,0 +1,79 @@
+import { httpRequested } from './requests';
+import { inject, objEncode, getUniqId, sendMessage } from './utils';
+import { onNotificationClick, onNotificationClose } from './notification';
+import bridge from './content';
+import webBridgeObj from './web';
+
+(function main() {
+  // Avoid running repeatedly due to new `document.documentElement`
+  if (window.VM) return;
+  window.VM = 1;
+
+  const badge = {
+    number: 0,
+    ready: false,
+    willSet: false,
+  };
+  function getBadge() {
+    badge.willSet = true;
+    setBadge();
+  }
+  function setBadge() {
+    if (badge.ready && badge.willSet) {
+      // XXX: only scripts run in top level window are counted
+      if (top === window) sendMessage({ cmd: 'SetBadge', data: badge.number });
+    }
+  }
+
+  // Messages
+  browser.runtime.onMessage.addListener((req, src) => {
+    const handlers = {
+      Command(data) {
+        bridge.post({ cmd: 'Command', data });
+      },
+      GetPopup: bridge.getPopup,
+      GetBadge: getBadge,
+      HttpRequested: httpRequested,
+      UpdateValues(data) {
+        bridge.post({ cmd: 'UpdateValues', data });
+      },
+      NotificationClick: onNotificationClick,
+      NotificationClose: onNotificationClose,
+    };
+    const handle = handlers[req.cmd];
+    if (handle) handle(req.data, src);
+  });
+
+  function initWeb(webBridge, webId, contentId, props) {
+    webBridge.initialize(webId, contentId, props);
+    document.addEventListener('DOMContentLoaded', () => {
+      webBridge.state = 1;
+      webBridge.load();
+    }, false);
+    webBridge.checkLoad();
+  }
+  function initBridge() {
+    const contentId = getUniqId();
+    const webId = getUniqId();
+    const args = [
+      objEncode(webBridgeObj),
+      JSON.stringify(webId),
+      JSON.stringify(contentId),
+      JSON.stringify(Object.getOwnPropertyNames(window)),
+    ];
+    inject(`(${initWeb.toString()})(${args.join(',')})`);
+    bridge.initialize(contentId, webId);
+    sendMessage({ cmd: 'GetInjected', data: location.href })
+    .then(data => {
+      bridge.forEach(data.scripts, script => {
+        bridge.ids.push(script.id);
+        if (script.enabled) badge.number += 1;
+      });
+      bridge.post({ cmd: 'LoadScripts', data });
+      badge.ready = true;
+      bridge.getPopup();
+      setBadge();
+    });
+  }
+  initBridge();
+}());

+ 20 - 0
src/injected/notification.js

@@ -0,0 +1,20 @@
+import { sendMessage } from './utils';
+import bridge from './content';
+
+const notifications = {};
+
+export function onNotificationCreate(options) {
+  sendMessage({ cmd: 'Notification', data: options })
+  .then(nid => { notifications[nid] = options.id; });
+}
+export function onNotificationClick(nid) {
+  const id = notifications[nid];
+  if (id) bridge.post({ cmd: 'NotificationClicked', data: id });
+}
+export function onNotificationClose(nid) {
+  const id = notifications[nid];
+  if (id) {
+    bridge.post({ cmd: 'NotificationClosed', data: id });
+    delete notifications[nid];
+  }
+}

+ 24 - 0
src/injected/requests.js

@@ -0,0 +1,24 @@
+import { sendMessage } from './utils';
+import bridge from './content';
+
+const requests = {};
+
+export function getRequestId() {
+  sendMessage({ cmd: 'GetRequestId' })
+  .then(id => {
+    requests[id] = 1;
+    bridge.post({ cmd: 'GotRequestId', data: id });
+  });
+}
+export function httpRequest(details) {
+  sendMessage({ cmd: 'HttpRequest', data: details });
+}
+export function httpRequested(data) {
+  if (requests[data.id]) {
+    if (data.type === 'loadend') delete requests[data.id];
+    bridge.post({ cmd: 'HttpRequested', data });
+  }
+}
+export function abortRequest(id) {
+  sendMessage({ cmd: 'AbortRequest', data: id });
+}

+ 37 - 0
src/injected/utils.js

@@ -0,0 +1,37 @@
+export function postData(destId, data) {
+  // Firefox issue: data must be stringified to avoid cross-origin problem
+  const e = new CustomEvent(destId, { detail: JSON.stringify(data) });
+  document.dispatchEvent(e);
+}
+
+export function sendMessage(data) {
+  return browser.runtime.sendMessage(data);
+}
+
+export function inject(code) {
+  const script = document.createElement('script');
+  const doc = document.body || document.documentElement;
+  script.textContent = code;
+  doc.appendChild(script);
+  try {
+    doc.removeChild(script);
+  } catch (e) {
+    // ignore if body is changed and script is detached
+  }
+}
+
+export function objEncode(obj) {
+  const list = Object.keys(obj).map(name => {
+    const value = obj[name];
+    const jsonKey = JSON.stringify(name);
+    if (typeof value === 'function') return `${jsonKey}:${value.toString()}`;
+    return `${jsonKey}:${JSON.stringify(value)}`;
+  });
+  return `{${list.join(',')}}`;
+}
+
+export function getUniqId() {
+  return Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
+}
+
+export function noop() {}

+ 592 - 0
src/injected/web.js

@@ -0,0 +1,592 @@
+/**
+ * All functions to be injected into web page must be independent.
+ * They must be assigned to `bridge` so that they can be serialized.
+ */
+import base from './bridge';
+
+/**
+ * http://www.webtoolkit.info/javascript-utf8.html
+ */
+function utf8decode(utftext) {
+  /* eslint-disable no-bitwise */
+  let string = '';
+  let i = 0;
+  let c1 = 0;
+  let c2 = 0;
+  let c3 = 0;
+  while (i < utftext.length) {
+    c1 = utftext.charCodeAt(i);
+    if (c1 < 128) {
+      string += String.fromCharCode(c1);
+      i += 1;
+    } else if (c1 > 191 && c1 < 224) {
+      c2 = utftext.charCodeAt(i + 1);
+      string += String.fromCharCode(((c1 & 31) << 6) | (c2 & 63));
+      i += 2;
+    } else {
+      c2 = utftext.charCodeAt(i + 1);
+      c3 = utftext.charCodeAt(i + 2);
+      string += String.fromCharCode(((c1 & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
+      i += 3;
+    }
+  }
+  return string;
+  /* eslint-enable no-bitwise */
+}
+
+function onLoadScripts(data) {
+  const bridge = this;
+  const start = [];
+  const idle = [];
+  const end = [];
+  bridge.command = {};
+  bridge.notif = {};
+  bridge.ainject = {};
+  bridge.version = data.version;
+  bridge.values = {};
+  // reset load and checkLoad
+  bridge.load = () => {
+    run(end);
+    setTimeout(run, 0, idle);
+  };
+  bridge.checkLoad = () => {
+    if (!bridge.state && bridge.includes(['interactive', 'complete'], document.readyState)) bridge.state = 1;
+    if (bridge.state) bridge.load();
+  };
+  const listMap = {
+    'document-start': start,
+    'document-idle': idle,
+    'document-end': end,
+  };
+  bridge.forEach(data.scripts, script => {
+    bridge.values[script.uri] = data.values[script.uri] || {};
+    if (script && script.enabled) {
+      const list = listMap[script.custom['run-at'] || script.meta['run-at']] || end;
+      list.push(script);
+    }
+  });
+  run(start);
+  bridge.checkLoad();
+  function buildCode(script) {
+    const requireKeys = script.meta.require || [];
+    const wrapper = bridge.wrapGM(script, data.cache);
+    // Must use Object.getOwnPropertyNames to list unenumerable properties
+    const wrapperKeys = Object.getOwnPropertyNames(wrapper);
+    const wrapperInit = bridge.map(wrapperKeys, name => `this["${name}"]=${name}`).join(';');
+    const codeSlices = [`${wrapperInit};with(this)!function(){`];
+    bridge.forEach(requireKeys, key => {
+      const requireCode = data.require[key];
+      if (requireCode) {
+        codeSlices.push(requireCode);
+        // Add `;` to a new line in case script ends with comment lines
+        codeSlices.push(';');
+      }
+    });
+    // wrap code to make 'use strict' work
+    codeSlices.push(`!function(){${script.code}\n}.call(this)`);
+    codeSlices.push('}.call(this);');
+    const code = codeSlices.join('\n');
+    const name = script.custom.name || script.meta.name || script.id;
+    const args = bridge.map(wrapperKeys, key => wrapper[key]);
+    const thisObj = wrapper.window || wrapper;
+    const id = bridge.getUniqId();
+    bridge.ainject[id] = [name, args, thisObj];
+    bridge.post({ cmd: 'Inject', data: [id, wrapperKeys, code] });
+  }
+  function run(list) {
+    while (list.length) buildCode(list.shift());
+  }
+}
+
+function handleWeb(obj) {
+  const bridge = this;
+  const handlers = {
+    LoadScripts(data) {
+      bridge.onLoadScripts(data);
+    },
+    Command(data) {
+      const func = bridge.command[data];
+      if (func) func();
+    },
+    GotRequestId(id) {
+      const req = bridge.qrequests.shift();
+      req.start(req, id);
+    },
+    HttpRequested(res) {
+      const req = bridge.requests[res.id];
+      if (req) req.callback(req, res);
+    },
+    UpdateValues(data) {
+      const { values } = bridge;
+      if (values && values[data.uri]) values[data.uri] = data.values;
+    },
+    NotificationClicked(id) {
+      const options = bridge.notif[id];
+      if (options) {
+        const { onclick } = options;
+        if (onclick) onclick();
+      }
+    },
+    NotificationClosed(id) {
+      const options = bridge.notif[id];
+      if (options) {
+        delete bridge.notif[id];
+        const { ondone } = options;
+        if (ondone) ondone();
+      }
+    },
+    // advanced inject
+    Injected(id) {
+      const item = bridge.ainject[id];
+      const func = window[`VM_${id}`];
+      delete window[`VM_${id}`];
+      delete bridge.ainject[id];
+      if (item && func) bridge.runCode(item[0], func, item[1], item[2]);
+    },
+  };
+  const handle = handlers[obj.cmd];
+  if (handle) handle(obj.data);
+}
+
+function runCode(name, func, args, thisObj) {
+  if (process.env.DEBUG) {
+    console.log(`Run script: ${name}`); // eslint-disable-line no-console
+  }
+  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);
+  }
+}
+
+function getRequest(arg) {
+  const bridge = this;
+  init();
+  return bridge.getRequest(arg);
+  function init() {
+    bridge.requests = {};
+    bridge.qrequests = [];
+    bridge.getRequest = details => {
+      const req = {
+        details,
+        callback,
+        start,
+        req: {
+          abort: reqAbort,
+        },
+      };
+      details.url = getFullUrl(details.url);
+      bridge.qrequests.push(req);
+      bridge.post({ cmd: 'GetRequestId' });
+      return req.req;
+    };
+  }
+  function reqAbort() {
+    bridge.post({ cmd: 'AbortRequest', data: this.id });
+  }
+  function parseData(req, details) {
+    if (req.resType) {
+      // blob or arraybuffer
+      let data = req.data.response.split(',');
+      const mimetype = data[0].match(/^data:(.*?);base64$/);
+      if (!mimetype) {
+        // invalid
+        req.data.response = null;
+      } else {
+        data = window.atob(data[1]);
+        const arr = new window.Uint8Array(data.length);
+        for (let i = 0; i < data.length; i += 1) arr[i] = data.charCodeAt(i);
+        if (details.responseType === 'blob') {
+          // blob
+          return new Blob([arr], { type: mimetype });
+        }
+        // arraybuffer
+        return arr.buffer;
+      }
+    } else if (details.responseType === 'json') {
+      // json
+      return JSON.parse(req.data.response);
+    } else {
+      // text
+      return req.data.response;
+    }
+  }
+  // request object functions
+  function callback(req, res) {
+    const cb = req.details[`on${res.type}`];
+    if (cb) {
+      if (res.data.response) {
+        if (!req.data) req.data = [parseData(res, req.details)];
+        res.data.response = req.data[0];
+      }
+      res.data.context = req.details.context;
+      cb(res.data);
+    }
+    if (res.type === 'loadend') delete bridge.requests[req.id];
+  }
+  function start(req, id) {
+    const { details } = req;
+    const data = {
+      id,
+      method: details.method,
+      url: details.url,
+      data: details.data,
+      // async: !details.synchronous,
+      user: details.user,
+      password: details.password,
+      headers: details.headers,
+      overrideMimeType: details.overrideMimeType,
+    };
+    req.id = id;
+    bridge.requests[id] = req;
+    if (bridge.includes(['arraybuffer', 'blob'], details.responseType)) {
+      data.responseType = 'blob';
+    }
+    bridge.post({ cmd: 'HttpRequest', data });
+  }
+  function getFullUrl(url) {
+    const a = document.createElement('a');
+    a.setAttribute('href', url);
+    return a.href;
+  }
+}
+
+function wrapGM(script, cache) {
+  // Add GM functions
+  // Reference: http://wiki.greasespot.net/Greasemonkey_Manual:API
+  const bridge = this;
+  const gm = {};
+  const grant = script.meta.grant || [];
+  const urls = {};
+  if (!grant.length || (grant.length === 1 && grant[0] === 'none')) {
+    // @grant none
+    grant.pop();
+  } else {
+    gm.window = bridge.getWrapper();
+  }
+  if (!bridge.includes(grant, 'unsafeWindow')) grant.push('unsafeWindow');
+  if (!bridge.includes(grant, 'GM_info')) grant.push('GM_info');
+  const resources = script.meta.resources || {};
+  const dataEncoders = {
+    o: val => JSON.stringify(val),
+    '': val => val.toString(),
+  };
+  const dataDecoders = {
+    n: val => Number(val),
+    b: val => val === 'true',
+    o: val => JSON.parse(val),
+    '': val => val,
+  };
+  const gmFunctions = {
+    unsafeWindow: { value: window },
+    GM_info: {
+      get() {
+        const matches = script.code.match(/\/\/\s+==UserScript==\s+([\s\S]*?)\/\/\s+==\/UserScript==\s/);
+        const obj = {
+          scriptMetaStr: matches ? matches[1] : '',
+          scriptWillUpdate: !!script.update,
+          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],
+            })),
+            'run-at': script.meta['run-at'] || '',
+            unwrap: false,  // deprecated, always `false`
+            version: script.meta.version || '',
+          },
+        };
+        return obj;
+      },
+    },
+    GM_deleteValue: {
+      value(key) {
+        const values = getValues();
+        delete values[key];
+        saveValues();
+      },
+    },
+    GM_getValue: {
+      value(key, def) {
+        const values = getValues();
+        const raw = values[key];
+        if (raw) {
+          const type = raw[0];
+          const handle = dataDecoders[type] || dataDecoders[''];
+          let value = raw.slice(1);
+          try {
+            value = handle(value);
+          } catch (e) {
+            console.warn(e);
+          }
+          return value;
+        }
+        return def;
+      },
+    },
+    GM_listValues: {
+      value() {
+        return Object.keys(getValues());
+      },
+    },
+    GM_setValue: {
+      value(key, val) {
+        const type = (typeof val)[0];
+        const handle = dataEncoders[type] || dataEncoders[''];
+        let value = val;
+        value = type + handle(value);
+        const values = getValues();
+        values[key] = value;
+        saveValues();
+      },
+    },
+    GM_getResourceText: {
+      value(name) {
+        if (name in resources) {
+          const uri = resources[name];
+          const raw = cache[uri];
+          const text = raw && utf8decode(window.atob(raw));
+          return text;
+        }
+      },
+    },
+    GM_getResourceURL: {
+      value(name) {
+        if (name in resources) {
+          const key = resources[name];
+          let blobUrl = urls[key];
+          if (!blobUrl) {
+            const raw = cache[key];
+            if (raw) {
+              // Binary string is not supported by blob constructor,
+              // so we have to transform it into array buffer.
+              const bin = window.atob(raw);
+              const arr = new window.Uint8Array(bin.length);
+              for (let i = 0; i < bin.length; i += 1) arr[i] = bin.charCodeAt(i);
+              const blob = new Blob([arr]);
+              blobUrl = URL.createObjectURL(blob);
+              urls[key] = blobUrl;
+            } else {
+              blobUrl = key;
+            }
+          }
+          return blobUrl;
+        }
+      },
+    },
+    GM_addStyle: {
+      value(data) {
+        bridge.post({ cmd: 'AddStyle', data });
+      },
+    },
+    GM_log: {
+      value(data) {
+        console.log(`[Violentmonkey][${script.meta.name || 'No name'}]`, data);  // eslint-disable-line no-console
+      },
+    },
+    GM_openInTab: {
+      value(url, background) {
+        bridge.post({ cmd: 'OpenTab', data: { url, active: !background } });
+      },
+    },
+    GM_registerMenuCommand: {
+      value(cap, func, acc) {
+        bridge.command[cap] = func;
+        bridge.post({ cmd: 'RegisterMenu', data: [cap, acc] });
+      },
+    },
+    GM_xmlhttpRequest: {
+      value(details) {
+        return bridge.getRequest(details);
+      },
+    },
+    GM_notification: {
+      value(text, title, image, onclick) {
+        const options = typeof text === 'object' ? text : {
+          text,
+          title,
+          image,
+          onclick,
+        };
+        if (!options.text) {
+          throw new Error('GM_notification: `text` is required!');
+        }
+        const id = (bridge.notif[''] || 0) + 1;
+        bridge.notif[''] = id;
+        bridge.notif[id] = options;
+        bridge.post({
+          cmd: 'Notification',
+          data: {
+            id,
+            text: options.text,
+            title: options.title,
+            image: options.image,
+          },
+        });
+      },
+    },
+    GM_setClipboard: {
+      value(data, type) {
+        bridge.post({
+          cmd: 'SetClipboard',
+          data: { type, data },
+        });
+      },
+    },
+  };
+  bridge.forEach(grant, name => {
+    const prop = gmFunctions[name];
+    if (prop) addProperty(name, prop, gm);
+  });
+  return gm;
+  function getValues() {
+    return bridge.values[script.uri];
+  }
+  function propertyToString() {
+    return '[Violentmonkey property]';
+  }
+  function addProperty(name, prop, obj) {
+    if ('value' in prop) prop.writable = false;
+    prop.configurable = false;
+    Object.defineProperty(obj, name, prop);
+    if (typeof obj[name] === 'function') obj[name].toString = propertyToString;
+  }
+  function saveValues() {
+    bridge.post({
+      cmd: 'SetValue',
+      data: {
+        uri: script.uri,
+        values: getValues(),
+      },
+    });
+  }
+}
+
+/**
+ * @desc Wrap methods to prevent unexpected modifications.
+ */
+function getWrapper() {
+  // http://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects
+  // http://developer.mozilla.org/docs/Web/API/Window
+  const bridge = this;
+  const wrapper = {};
+  bridge.forEach([
+    // `eval` should be called directly so that it is run in current scope
+    'eval',
+  ], name => {
+    wrapper[name] = window[name];
+  });
+  bridge.forEach([
+    // 'uneval',
+    'isFinite',
+    'isNaN',
+    'parseFloat',
+    'parseInt',
+    'decodeURI',
+    'decodeURIComponent',
+    'encodeURI',
+    'encodeURIComponent',
+
+    'addEventListener',
+    'alert',
+    'atob',
+    'blur',
+    'btoa',
+    'clearInterval',
+    'clearTimeout',
+    'close',
+    'confirm',
+    'dispatchEvent',
+    'fetch',
+    'find',
+    'focus',
+    'getComputedStyle',
+    'getSelection',
+    'matchMedia',
+    'moveBy',
+    'moveTo',
+    'open',
+    'openDialog',
+    'postMessage',
+    'print',
+    'prompt',
+    'removeEventListener',
+    'requestAnimationFrame',
+    'resizeBy',
+    'resizeTo',
+    'scroll',
+    'scrollBy',
+    'scrollByLines',
+    'scrollByPages',
+    'scrollTo',
+    'setInterval',
+    'setTimeout',
+    'stop',
+  ], name => {
+    const method = window[name];
+    if (method) {
+      wrapper[name] = (...args) => method.apply(window, args);
+    }
+  });
+  function defineProtectedProperty(name) {
+    let modified = false;
+    let value;
+    Object.defineProperty(wrapper, name, {
+      get() {
+        if (!modified) value = window[name];
+        return value === window ? wrapper : value;
+      },
+      set(val) {
+        modified = true;
+        value = val;
+      },
+    });
+  }
+  function defineReactedProperty(name) {
+    Object.defineProperty(wrapper, name, {
+      get() {
+        const value = window[name];
+        return value === window ? wrapper : value;
+      },
+      set(val) {
+        window[name] = val;
+      },
+    });
+  }
+  // Wrap properties
+  bridge.forEach(bridge.props, name => {
+    if (name in wrapper) return;
+    if (name.slice(0, 2) === 'on') defineReactedProperty(name);
+    else defineProtectedProperty(name);
+  });
+  return wrapper;
+}
+
+function initialize(src, dest, props) {
+  const bridge = this;
+  bridge.props = props;
+  bridge.load = bridge.noop;
+  bridge.checkLoad = bridge.noop;
+  bridge.bindEvents(src, dest);
+}
+
+export default Object.assign({
+  utf8decode,
+  getRequest,
+  wrapGM,
+  getWrapper,
+  onLoadScripts,
+  runCode,
+  initialize,
+  state: 0,
+  handle: handleWeb,
+}, base);