Browse Source

Add requests

Gerald 10 years ago
parent
commit
053989c4b7
5 changed files with 1117 additions and 929 deletions
  1. 563 0
      src/background/db.js
  2. 2 0
      src/background/index.html
  3. 366 929
      src/background/main.js
  4. 182 0
      src/background/requests.js
  5. 4 0
      src/common.js

+ 563 - 0
src/background/db.js

@@ -0,0 +1,563 @@
+function VMDB() {
+  var _this = this;
+  _this.initialized = _this.openDB().then(_this.initPosition.bind(_this));
+}
+
+VMDB.prototype.openDB = function () {
+  var _this = this;
+  return new Promise(function (resolve, reject) {
+    var request = indexedDB.open('Violentmonkey', 1);
+    request.onsuccess = function (e) {
+      _this.db = request.result;
+    };
+    request.onerror = function (e) {
+      var err = e.target.error;
+      console.log('IndexedDB error: ' + err.message);
+      reject(err);
+    };
+    request.onupgradeneeded = function (e) {
+      var r = e.currentTarget.result;
+      // scripts: id uri custom meta enabled update code position
+      var o = r.createObjectStore('scripts', {
+        keyPath: 'id',
+        autoIncrement: true,
+      });
+      o.createIndex('uri', 'uri', {unique: true});
+      o.createIndex('update', 'update', {unique: false});
+      // position should be unique at last
+      o.createIndex('position', 'position', {unique: false});
+      // require: uri code
+      o = r.createObjectStore('require', {keyPath: 'uri'});
+      // cache: uri data
+      o = r.createObjectStore('cache', {keyPath: 'uri'});
+      // values: uri values
+      o = r.createObjectStore('values', {keyPath: 'uri'});
+    };
+  });
+};
+
+VMDB.prototype.initPosition = function () {
+  var _this = this;
+  _this.position = 0;
+  var o = _this.db.transaction('scripts', 'readwrite').objectStore('scripts');
+  return new Promise(function (resolve, reject) {
+    o.index('position').openCursor(null, 'prev').onsuccess = function (e) {
+      var result = e.target.result;
+      if (result && _this.position < result.key)
+        _this.position = result.key;
+      resolve();
+    };
+  });
+};
+
+VMDB.prototype.getScript = function (id, os) {
+  os = os || this.db.transaction('scripts').objectStore('scripts');
+  return new Promise(function (resolve, reject) {
+    os.get(id).onsuccess = function (e) {
+      resolve(e.target.result);
+    };
+  });
+};
+
+VMDB.prototype.queryScript = function (id, meta) {
+  return id ? this.getScript(id)
+  : new Promise(function (resolve, reject) {
+    var uri = getNameURI({meta: meta});
+    if (uri !== '::')
+      this.db.transaction('scripts').objectStore('scripts')
+      .index('uri').get(uri).onsuccess = function (e) {
+        resolve(e.target.result);
+      };
+    else
+      resolve(newScript());
+  });
+};
+
+VMDB.prototype.getScriptData = function (id) {
+  return this.getScript(id).then(function (script) {
+    if (!script) return Promise.reject();
+    var data = getMeta(script);
+    data.code = script.code;
+    return data;
+  });
+};
+
+VMDB.prototype.getScriptInfos = function (ids) {
+  var _this = this;
+  var os = _this.db.transaction('scripts').objectStore('scripts');
+  return Promise.all(ids.map(function (id) {
+    return _this.getScript(id, os);
+  })).then(function (scripts) {
+    return scripts.filter(function (x) {return x;}).map(getMeta);
+  });
+};
+
+VMDB.prototype.getScriptsByURL = function (url) {
+  function getScripts() {
+    return new Promise(function (resolve, reject) {
+      var data = {
+        scripts: [],
+        uris: [],
+      };
+      var require = {};
+      var cache = {};
+      var o = db.transaction('scripts').objectStore('scripts');
+      o.index('position').openCursor().onsuccess = function (e) {
+        var result = e.target.result;
+        if (result) {
+          var value = result.value;
+          if (testURL(url, value)) {
+            data.scripts.push(value);
+            data.uris.push(value.uri);
+            value.meta.require.forEach(function (key) {
+              require[key] = 1;
+            });
+            for (var k in value.meta.resources)
+              cache[value.meta.resources[k]] = 1;
+          }
+          result.continue();
+        } else {
+          data.require = Object.getOwnPropertyNames(require);
+          data.cache = Object.getOwnPropertyNames(cache);
+          resolve(data);
+        }
+      };
+    });
+  }
+  function getRequire(uris) {
+    var o = db.transaction('require').objectStore('require');
+    return Promise.all(uris.map(function (uri) {
+      return new Promise(function (resolve, reject) {
+        o.get(uri).onsuccess = function (e) {
+          resolve(e.target.result);
+        };
+      });
+    })).then(function (data) {
+      return data.reduce(function (result, value, i) {
+        if (value) result[uris[i]] = value.code;
+        return result;
+      }, {});
+    });
+  }
+  function getValues(uris) {
+    var o = db.transaction('values').objectStore('values');
+    return Promise.all(uris.map(function (uri) {
+      return new Promise(function (resolve, reject) {
+        o.get(uri).onsuccess = function (e) {
+          resolve(e.target.result);
+        };
+      });
+    })).then(function (data) {
+      return data.reduce(function (result, value, i) {
+        if (value) result[uris[i]] = value.values;
+        return result;
+      }, {});
+    });
+  }
+  var _this = this;
+  var db = _this.db;
+  return getScripts().then(function (data) {
+    return Promise.all([
+      getRequire(data.require),
+      getValues(data.uris),
+      _this.getCacheB64(data.cache),
+    ]).then(function (res) {
+      return {
+        scripts: data.scripts,
+        require: res[0],
+        values: res[1],
+        cache: res[2],
+      };
+    });
+  });
+};
+
+VMDB.prototype.getData = function () {
+  function getScripts() {
+    return new Promise(function (resolve, reject) {
+      var data = {
+        scripts: [],
+      };
+      var cache = {};
+      var o = db.transaction('scripts').objectStore('scripts');
+      o.index('position').openCursor().onsuccess = function (e) {
+        var result = e.target.result;
+        if (result) {
+          var value = result.value;
+          if (isRemote(value.meta.icon)) cache[value.meta.icon] = 1;
+          data.scripts.push(getMeta(value));
+          result.continue();
+        } else {
+          data.cache = Object.getOwnPropertyNames(cache);
+          resolve(data);
+        }
+      }
+    });
+  }
+  function getCache(uris) {
+    return _this.getCacheB64(uris).then(function (cache) {
+      for (var k in cache)
+        cache[k] = 'data:image/png;base64,' + cache[k];
+      return cache;
+    });
+  }
+  var _this = this;
+  var db = _this.db;
+  return getScripts().then(function (data) {
+    return getCache(data.cache).then(function (cache) {
+      return {
+        scripts: data.scripts,
+        cache: cache,
+      };
+    });
+  });
+};
+
+VMDB.prototype.removeScript = function (id) {
+	var o = this.db.transaction('scripts', 'readwrite').objectStore('scripts');
+	o.delete(id);
+  return Promise.resolve();
+};
+
+VMDB.prototype.moveScript = function (id, offset) {
+  var o = this.db.transaction('scripts', 'readwrite').objectStore('scripts');
+  return this.getScript(id).then(function (script) {
+    var pos = script.position;
+    var range, order;
+    if (offset < 0) {
+      range = IDBKeyRange.upperBound(pos, true);
+      order = 'prev';
+      offset = -offset;
+    } else {
+      range = IDBKeyRange.lowerBound(pos, true);
+      order = 'next';
+    }
+    return new Promise(function (resolve, reject) {
+      o.index('position').openCursor(range, order).onsuccess = function (e) {
+        var result = e.target.result;
+        if (result) {
+          offset --;
+          var value = result.value;
+          value.position = pos;
+          pos = result.key;
+          result.update(value);
+          if (offset) result.continue();
+          else {
+            script.position = pos;
+            o.put(script).onsuccess = function () {
+              resolve();
+            };
+          }
+        }
+      };
+    });
+  });
+};
+
+VMDB.prototype.getCacheB64 = function (urls) {
+  var o = this.db.transaction('cache').objectStore('cache');
+  return Promise.all(urls.map(function (url) {
+    return new Promise(function (resolve, reject) {
+      o.get(url).onsuccess = function (e) {
+        resolve(e.target.result);
+      };
+    });
+  })).then(function (data) {
+    return data.reduce(function (map, value, i) {
+      map[urls[i]] = value;
+    }, {});
+  });
+};
+
+VMDB.prototype.saveCache = function (url, data) {
+  var o = this.db.transaction('cache', 'readwrite').objectStore('cache');
+  return new Promise(function (resolve, reject) {
+    o.put({uri: url, data: data}).onsuccess = function () {
+      resolve();
+    };
+  });
+};
+
+VMDB.prototype.saveRequire = function (url, data) {
+  var o = this.db.transaction('require', 'readwrite').objectStore('require');
+  return new Promise(function (resolve, reject) {
+    o.put({uri: url, code: data}).onsuccess = function () {
+      resolve();
+    };
+  });
+};
+
+VMDB.prototype.saveScript = function (script) {
+  script.enabled = script.enabled ? 1 : 0;
+  script.update = script.update ? 1 : 0;
+  if (!script.position) script.position = ++ this.position;
+  var o = this.db.transaction('scripts', 'readwrite').objectStore('scripts');
+  return new Promise(function (resolve, reject) {
+    o.put(script).onsuccess = function () {
+      resolve();
+    };
+  });
+};
+
+VMDB.prototype.fetch = function (url, type, headers) {
+  var xhr = new XMLHttpRequest;
+  xhr.open('GET', url, true);
+  if (type) xhr.responseType = type;
+  if (headers) for (var k in headers)
+    xhr.setRequestHeader(k, headers[k]);
+  return new Promise(function (resolve, reject) {
+    xhr.onload = function () {
+      resolve(this);
+    };
+    xhr.onerror = function () {
+      reject(this);
+    };
+    xhr.send();
+  });
+};
+
+VMDB.prototype.fetchCache = function () {
+  var requests = {};
+  return function (url, check) {
+    var _this = this;
+    return requests[url]
+    || (requests[url] = _this.fetch(url, 'blob').then(function (res) {
+      return check ? check(res.response) : res.response;
+    }).then(function (data) {
+      return new Promise(function (resolve, reject) {
+        var reader = new FileReader;
+        reader.onload = function (e) {
+          _this.saveCache(url, window.btoa(this.result)).then(function () {
+            delete requests[url];
+            resolve();
+          });
+        };
+        reader.onerror = function (e) {
+          reject(e);
+        };
+        reader.readAsBinaryString(data);
+      });
+    }));
+  };
+}();
+
+VMDB.prototype.fetchRequire = function () {
+  var requests = {};
+  return function (url) {
+    var _this = this;
+    return requests[url]
+    || (requests[url] = _this.fetch(url).then(function (res) {
+      return _this.saveRequire(url, res.responseText);
+    }).then(function () {
+      delete requests[url];
+    }));
+  };
+}();
+
+VMDB.prototype.setValue = function (uri, values) {
+  var o = this.db.transaction('values', 'readwrite').objectStore('values');
+  return new Promise(function (resolve, reject) {
+    o.put({uri: uri, values: values}).onsuccess = function () {
+      resolve();
+    };
+  });
+};
+
+VMDB.prototype.updateScriptInfo = function (id, data) {
+  var o = this.db.transaction('scripts', 'readwrite').objectStore('scripts');
+  return new Promise(function (resolve, reject) {
+    o.get(id).onsuccess = function (e) {
+      var script = e.target.result;
+      if (!script) return reject();
+      for (var k in data)
+        if (k in script) script[k] = data[k];
+      o.put(script).onsuccess = function (e) {
+        resolve(getMeta(script));
+      };
+    };
+  });
+};
+
+VMDB.prototype.getExportData = function (ids, withValues) {
+  function getScripts(ids) {
+    var o = db.transaction('scripts').objectStore('scripts');
+    return Promise.all(ids.map(function (id) {
+      return new Promise(function (resolve, reject) {
+        o.get(id).onsuccess = function (e) {
+          resolve(e.target.result);
+        };
+      });
+    })).then(function (data) {
+      return data.filter(function (x) {return x;});
+    });
+  }
+  function getValues(uris) {
+    var o = db.transaction('values').objectStore('values');
+    return Promise.all(uris.map(function (uri) {
+      return new Promise(function (resolve, reject) {
+        o.get(uri).onsuccess = function (e) {
+          resolve(e.target.result);
+        };
+      });
+    })).then(function (data) {
+      return data.reduce(function (result, value, i) {
+        if (value) result[uris[i]] = value.values;
+        return result;
+      }, {});
+    });
+  }
+  var db = this.db;
+  return getScripts(ids).then(function (scripts) {
+    var res = {
+      scripts: scripts,
+    };
+    return withValues
+    ? getValues(scripts.map(function (script) {return script.uri;})).then(function (values) {
+      res.values = values;
+      return res;
+    }) : res;
+  });
+};
+
+// Common functions
+
+function parseMeta(code) {
+	// initialize meta, specify those with multiple values allowed
+	var meta = {
+		include: [],
+		exclude: [],
+		match: [],
+		require: [],
+		resource: [],
+		grant: [],
+	};
+	var flag = -1;
+	code.replace(/(?:^|\n)\/\/\s*([@=]\S+)(.*)/g, function(value, group1, group2) {
+		if (flag < 0 && group1 == '==UserScript==')
+			// start meta
+			flag = 1;
+		else if(flag > 0 && group1 == '==/UserScript==')
+			// end meta
+			flag = 0;
+		if (flag == 1 && group1[0] == '@') {
+			var key = group1.slice(1);
+			var val = group2.replace(/^\s+|\s+$/g, '');
+			var value = meta[key];
+      // multiple values allowed
+			if (value && value.push) value.push(val);
+      // only first value will be stored
+			else if (!(key in meta)) meta[key] = val;
+		}
+	});
+	meta.resources = {};
+	meta.resource.forEach(function(line) {
+		var pair = line.match(/^(\w\S*)\s+(.*)/);
+		if (pair) meta.resources[pair[1]] = pair[2];
+	});
+	delete meta.resource;
+	// @homepageURL: compatible with @homepage
+	if (!meta.homepageURL && meta.homepage) meta.homepageURL = meta.homepage;
+	return meta;
+}
+
+function newScript() {
+	var script = {
+		custom: {},
+		enabled: 1,
+		update: 1,
+		code: '// ==UserScript==\n// @name New Script\n// ==/UserScript==\n',
+	};
+	script.meta = parseMeta(script.code);
+	return script;
+}
+
+function getMeta(script) {
+	return {
+		id: script.id,
+		custom: script.custom,
+		meta: script.meta,
+		enabled: script.enabled,
+		update: script.update,
+	};
+}
+
+function getNameURI(script) {
+	var ns = script.meta.namespace || '';
+	var name = script.meta.name || '';
+	var nameURI = escape(ns) + ':' + escape(name) + ':';
+	if (!ns && !name) nameURI += script.id || '';
+	return nameURI;
+}
+
+var tester = function () {
+  function testURL(url, script) {
+    var custom = script.custom;
+    var meta = script.meta;
+    var inc = [], exc = [], mat = [];
+    var ok = true;
+    if (custom._match !== false && meta.match) mat = mat.concat(meta.match);
+    if (custom.match) mat = mat.concat(custom.match);
+    if (custom._include !== false && meta.include) inc = inc.concat(meta.include);
+    if (custom.include) inc = inc.concat(custom.include);
+    if (custom._exclude !== false && meta.exclude) exc = exc.concat(meta.exclude);
+    if (custom.exclude) exc = exc.concat(custom.exclude);
+    if (mat.length) {
+      // @match
+      var urlParts = url.match(match_reg);
+      ok = mat.some(function (str) {
+        return matchTest(str, urlParts);
+      });
+    } else {
+      // @include
+      ok = inc.some(function (str) {
+        return autoReg(str).test(url);
+      });
+    }
+    // exclude
+    ok = ok && !exc.some(function (str) {
+      return autoReg(str).test(url);
+    });
+    return ok;
+  }
+
+  function str2RE(str) {
+    return RegExp('^' + str.replace(/([.?\/])/g, '\\$1').replace(/\*/g, '.*?') + '$');
+  }
+
+  function autoReg(str) {
+    if (/^\/.*\/$/.test(str))
+      return RegExp(str.slice(1, -1));	// Regular-expression
+    else
+      return str2RE(str);	// String with wildcards
+  }
+
+  var match_reg = /(.*?):\/\/([^\/]*)\/(.*)/;
+  function matchTest(str, urlParts) {
+    if (str == '<all_urls>') return true;
+    var parts = str.match(match_reg);
+    var ok = !!parts;
+    // scheme
+    ok = ok && (
+      // exact match
+      parts[1] == urlParts[1]
+      // * = http | https
+      || parts[1] == '*' && /^https?$/i.test(urlParts[1])
+    );
+    // host
+    ok = ok && (
+      // * matches all
+      parts[2] == '*'
+      // exact match
+      || parts[2] == urlParts[2]
+      // *.example.com
+      || /^\*\.[^*]*$/.test(parts[2]) && str2RE(parts[2]).test(urlParts[2])
+    );
+    // pathname
+    ok = ok && str2RE(parts[3]).test(urlParts[3]);
+    return ok;
+  }
+
+  return {
+    testURL: testURL,
+  };
+}();

+ 2 - 0
src/background/index.html

@@ -6,6 +6,8 @@
 	</head>
   <body>
 		<script src="/common.js"></script>
+    <script src="db.js"></script>
+    <script src="requests.js"></script>
 		<script src="main.js"></script>
   </body>
 </html>

+ 366 - 929
src/background/main.js

@@ -1,634 +1,343 @@
-'use strict';
-var db;
+var vmdb = new VMDB;
 var port = null;
-var position = 0;
 var vm_ver = chrome.app.getDetails().version;
 
-function compareVersion(version1, version2) {
-	version1 = (version1 || '').split('.');
-	version2 = (version2 || '').split('.');
-	for ( var i = 0; i < version1.length || i < version2.length; i ++ ) {
-		var delta = (parseInt(version1[i], 10) || 0) - (parseInt(version2[i], 10) || 0);
-		if(delta) return delta < 0 ? -1 : 1;
-	}
-	return 0;
-}
+vmdb.initialized.then(function () {
+  var commands = {
+    NewScript: function (data, src) {
+      return Promise.resolve(newScript());
+    },
+    RemoveScript: removeScript,
+    GetData: getData,
+    GetInjected: getInjected,
+    CheckUpdate: checkUpdate,
+    CheckUpdateAll: checkUpdateAll,
+    UpdateMeta: updateMeta,
+    SetValue: setValue,
+    ExportZip: exportZip,
+    ParseScript: parseScript,
+    GetScript: getScript,
+    GetMetas: getMetas,
+    SetBadge: setBadge,
+    AutoUpdate: autoUpdate,
+    Vacuum: vacuum,
+    Move: move,
+    ParseMeta: function(o, src, callback) {callback(parseMeta(o));},
+    GetRequestId: getRequestId,
+    HttpRequest: httpRequest,
+    AbortRequest: abortRequest,
+  };
+  chrome.runtime.onMessage.addListener(function (req, src, callback) {
+    var func = commands[req.cmd];
+    if (func) {
+      var res = func(req.data, src);
+      if (res === false) return;
+      var finish = function () {
+        try {
+          callback.apply(null, arguments);
+        } catch (e) {
+          // callback fails if not given in content page
+        }
+      };
+      res.then(finish, finish);
+      return true;
+    }
+  });
+  setTimeout(autoUpdate, 2e4);
+});
 
-function getUniqId() {
-	return Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
-}
+// Common functions
 
-function notify(options) {
-	chrome.notifications.create(options.id || 'ViolentMonkey', {
-		type: 'basic',
-		iconUrl: '/images/icon128.png',
-		title: options.title + ' - ' + _.i18n('extName'),
-		message: options.body,
-		isClickable: options.isClickable,
-	});
-}
-
-function initDb(callback) {
-	var request = indexedDB.open('Violentmonkey', 1);
-	request.onsuccess = function (e) {
-		// initiate db
-		db = request.result;
-		if (callback) callback();
-	};
-	request.onerror = function (e) {
-		console.log('IndexedDB error: ' + e.target.error.message);
-	};
-	request.onupgradeneeded = function (e) {
-		var r = e.currentTarget.result;
-		// scripts: id uri custom meta enabled update code position
-		var o = r.createObjectStore('scripts', {
-			keyPath: 'id',
-			autoIncrement: true,
-		});
-		o.createIndex('uri', 'uri', {unique: true});
-		o.createIndex('update', 'update', {unique: false});
-		// position should be unique at last
-		o.createIndex('position', 'position', {unique: false});
-		// require: uri code
-		o = r.createObjectStore('require', {keyPath: 'uri'});
-		// cache: uri data
-		o = r.createObjectStore('cache', {keyPath: 'uri'});
-		// values: uri values
-		o = r.createObjectStore('values', {keyPath: 'uri'});
-	};
+function compareVersion(version1, version2) {
+  version1 = (version1 || '').split('.');
+  version2 = (version2 || '').split('.');
+  for ( var i = 0; i < version1.length || i < version2.length; i ++ ) {
+    var delta = (parseInt(version1[i], 10) || 0) - (parseInt(version2[i], 10) || 0);
+    if(delta) return delta < 0 ? -1 : 1;
+  }
+  return 0;
 }
 
-function getNameURI(script) {
-	var ns = script.meta.namespace || '';
-	var name = script.meta.name || '';
-	var nameURI = escape(ns) + ':' + escape(name) + ':';
-	if (!ns && !name) nameURI += script.id;
-	return nameURI;
+function notify(options) {
+  chrome.notifications.create(options.id || 'ViolentMonkey', {
+    type: 'basic',
+    iconUrl: '/images/icon128.png',
+    title: options.title + ' - ' + _.i18n('extName'),
+    message: options.body,
+    isClickable: options.isClickable,
+  });
 }
 
 function isRemote(url){
-	return url && !/^data:/.test(url);
+  return url && !/^data:/.test(url);
 }
 
 function getMeta(script) {
-	return {
-		id: script.id,
-		custom: script.custom,
-		meta: script.meta,
-		enabled: script.enabled,
-		update: script.update,
-	};
-}
-
-function parseMeta(code) {
-	// initialize meta, specify those with multiple values allowed
-	var meta = {
-		include: [],
-		exclude: [],
-		match: [],
-		require: [],
-		resource: [],
-		grant: [],
-	};
-	var flag = -1;
-	code.replace(/(?:^|\n)\/\/\s*([@=]\S+)(.*)/g, function(value, group1, group2) {
-		if (flag < 0 && group1 == '==UserScript==')
-			// start meta
-			flag = 1;
-		else if(flag > 0 && group1 == '==/UserScript==')
-			// end meta
-			flag = 0;
-		if(flag == 1 && group1[0] == '@') {
-			var key = group1.slice(1);
-			var val = group2.replace(/^\s+|\s+$/g, '');
-			var value = meta[key];
-			if(value && value.push) value.push(val);	// multiple values allowed
-			else if(!(key in meta)) meta[key] = val;	// only first value will be stored
-		}
-	});
-	meta.resources = {};
-	meta.resource.forEach(function(line) {
-		var pair = line.match(/^(\w\S*)\s+(.*)/);
-		if(pair) meta.resources[pair[1]] = pair[2];
-	});
-	delete meta.resource;
-	// @homepageURL: compatible with @homepage
-	if(! meta.homepageURL && meta.homepage) meta.homepageURL = meta.homepage;
-	return meta;
-}
-
-function newScript() {
-	var script = {
-		custom: {},
-		enabled: 1,
-		update: 1,
-		code: '// ==UserScript==\n// @name New Script\n// ==/UserScript==\n',
-	};
-	script.meta = parseMeta(script.code);
-	return script;
-}
-
-function removeScript(id, src, callback) {
-	var o = db.transaction('scripts', 'readwrite').objectStore('scripts');
-	o.delete(id);
-	if (callback) callback();
+  return {
+    id: script.id,
+    custom: script.custom,
+    meta: script.meta,
+    enabled: script.enabled,
+    update: script.update,
+  };
 }
 
 function vacuum(o, src, callback) {
-	function init(){
-		var o = db.transaction('scripts').objectStore('scripts');
-		o.index('position').openCursor().onsuccess = function (e) {
-			var r = e.target.result;
-			if (r) {
-				var script = r.value;
-				ids.push(script.id);
-				script.meta.require.forEach(function (item) {require[item] = 1;});
-				for(var i in script.meta.resources) cache[script.meta.resources[i]] = 1;
-				if(isRemote(script.meta.icon)) cache[script.meta.icon] = 1;
-				values[script.uri] = 1;
-				r.continue();
-			} else vacuumPosition();
-		};
-	}
-	function vacuumPosition() {
-		var id = ids.shift();
-		if (id) {
-			var o = db.transaction('scripts','readwrite').objectStore('scripts');
-			o.get(id).onsuccess = function (e) {
-				var r = e.target.result;
-				r.position = ++ _pos;
-				o.put(r).onsuccess = vacuumPosition;
-			};
-		} else {
-			position = _pos;
-			vacuumDB('require', require);
-			vacuumDB('cache', cache);
-			vacuumDB('values', values);
-		}
-	}
-	function vacuumDB(dbName, dict) {
-		working ++;
-		// the database must have a keyPath of 'uri'
-		var o = db.transaction(dbName, 'readwrite').objectStore(dbName);
-		o.openCursor().onsuccess = function (e) {
-			var r = e.target.result;
-			if (r) {
-				var v = r.value;
-				if (!dict[v.uri]) o.delete(v.uri);
-				else dict[v.uri] ++;	// keep
-				r.continue();
-			} else finish();
-		};
-	}
-	function finish() {
-		if(! -- working) {
-			for(var i in require)
-				if(require[i] == 1) fetchRequire(i);
-			for(i in cache)
-				if(cache[i] == 1) fetchCache(i);
-			callback();
-		}
-	}
-	var ids = [];
-	var cache = {};
-	var require = {};
-	var values = {};
-	var working = 0;
-	var _pos=0;
-	init();
-	return true;
-}
-
-function move(data, src, callback) {
-	var o = db.transaction('scripts', 'readwrite').objectStore('scripts');
-	o.get(data.id).onsuccess = function (e) {
-		var script = e.target.result;
-		var pos = script.position;
-		var range, order;
-		if (data.offset < 0) {
-			range = IDBKeyRange.upperBound(pos, true);
-			order = 'prev';
-			data.offset = -data.offset;
-		} else {
-			range = IDBKeyRange.lowerBound(pos, true);
-			order = 'next';
-		}
-		o.index('position').openCursor(range, order).onsuccess = function (e) {
-			var r = e.target.result;
-			if (r) {
-				data.offset --;
-				var v = r.value;
-				v.position = pos;
-				pos = r.key;
-				r.update(v);
-				if (data.offset) r.continue();
-				else {
-					script.position = pos;
-					o.put(script);
-				}
-			}
-		};
-	};
-	if(callback) callback();
-}
-
-function str2RE(str) {
-	return RegExp('^' + str.replace(/(\.|\?|\/)/g, '\\$1').replace(/\*/g, '.*?') + '$');
-}
-
-function autoReg(str) {
-	if (/^\/.*\/$/.test(str))
-		return RegExp(str.slice(1, -1));	// Regular-expression
-	else
-		return str2RE(str);	// String with wildcards
-}
-
-var match_reg = /(.*?):\/\/([^\/]*)\/(.*)/;
-function matchTest(str, urlParts){
-	if (str == '<all_urls>') return true;
-	var parts = str.match(match_reg);
-	return !!(parts &&
-		// scheme
-		(
-			parts[1] == urlParts[1] ||	// exact match
-			parts[1] == '*' && /^https?$/i.test(urlParts[1])	// * = http|https
-		) &&
-		// host
-		(
-			parts[2] == '*' ||	// * matches all
-			parts[2] == urlParts[2] ||	// exact match
-			/^\*\.[^*]*$/.test(parts[2]) && str2RE(parts[2]).test(urlParts[2])	// *.example.com
-		) &&
-		// pathname
-		str2RE(parts[3]).test(urlParts[3])
-	);
-}
-
-function testURL(url, script){
-	var custom = script.custom;
-	var meta = script.meta;
-	var inc = [], exc = [], mat = [];
-	var ok = true;
-	if(custom._match !== false && meta.match) mat = mat.concat(meta.match);
-	if(custom.match) mat = mat.concat(custom.match);
-	if(custom._include !== false && meta.include) inc = inc.concat(meta.include);
-	if(custom.include) inc = inc.concat(custom.include);
-	if(custom._exclude !== false && meta.exclude) exc = exc.concat(meta.exclude);
-	if(custom.exclude) exc = exc.concat(custom.exclude);
-	// @match
-	if(mat.length) {
-		var urlParts = url.match(match_reg);
-		mat.some(function(str) {
-			return (ok = matchTest(str, urlParts));
-		});
-	}
-	// @include
-	else inc.some(function(str) {
-		return (ok = autoReg(str).test(url));
-	});
-	// exclude
-	if(ok) exc.some(function(str) {
-		ok = ! autoReg(str).test(url);
-		return ! ok;
-	});
-	return ok;
-}
-
-// get full data of a single script for user edit
-function getScript(id, src, callback) {
-	var o = db.transaction('scripts').objectStore('scripts');
-	o.get(id).onsuccess = function (e) {
-		var v = e.target.result;
-		if(v) {
-			var script = getMeta(v);
-			script.code = v.code;
-			if(callback) callback(script);
-		}
-	};
-	return true;
-}
-
-// get meta data of scripts for popup menu
-function getMetas(ids, src, callback) {
-	function get(){
-		var id = ids.shift();
-		if (id)
-			o.get(id).onsuccess = function (e) {
-				var v=e.target.result;
-				if (v) data.push(getMeta(v));
-				get();
-			};
-		else callback(data);
-	}
-	var o = db.transaction('scripts').objectStore('scripts');
-	var data=[];
-	get();
-	return true;
+  function init(){
+    var o = db.transaction('scripts').objectStore('scripts');
+    o.index('position').openCursor().onsuccess = function (e) {
+      var r = e.target.result;
+      if (r) {
+        var script = r.value;
+        ids.push(script.id);
+        script.meta.require.forEach(function (item) {require[item] = 1;});
+        for(var i in script.meta.resources) cache[script.meta.resources[i]] = 1;
+        if(isRemote(script.meta.icon)) cache[script.meta.icon] = 1;
+        values[script.uri] = 1;
+        r.continue();
+      } else vacuumPosition();
+    };
+  }
+  function vacuumPosition() {
+    var id = ids.shift();
+    if (id) {
+      var o = db.transaction('scripts','readwrite').objectStore('scripts');
+      o.get(id).onsuccess = function (e) {
+        var r = e.target.result;
+        r.position = ++ _pos;
+        o.put(r).onsuccess = vacuumPosition;
+      };
+    } else {
+      position = _pos;
+      vacuumDB('require', require);
+      vacuumDB('cache', cache);
+      vacuumDB('values', values);
+    }
+  }
+  function vacuumDB(dbName, dict) {
+    working ++;
+    // the database must have a keyPath of 'uri'
+    var o = db.transaction(dbName, 'readwrite').objectStore(dbName);
+    o.openCursor().onsuccess = function (e) {
+      var r = e.target.result;
+      if (r) {
+        var v = r.value;
+        if (!dict[v.uri]) o.delete(v.uri);
+        else dict[v.uri] ++;  // keep
+        r.continue();
+      } else finish();
+    };
+  }
+  function finish() {
+    if(! -- working) {
+      for(var i in require)
+        if(require[i] == 1) fetchRequire(i);
+      for(i in cache)
+        if(cache[i] == 1) fetchCache(i);
+      callback();
+    }
+  }
+  var ids = [];
+  var cache = {};
+  var require = {};
+  var values = {};
+  var working = 0;
+  var _pos=0;
+  init();
+  return true;
 }
 
 var badges = {};
 function setBadge(num, src, callback) {
-	var o;
-	if(src.id in badges) o = badges[src.id];
-	else badges[src.id] = o = {num: 0};
-	o.num += num;
-	chrome.browserAction.setBadgeBackgroundColor({color: '#808', tabId: src.tab.id});
-	chrome.browserAction.setBadgeText({
-		text: o.num ? o.num.toString() : '',
-		tabId: src.tab.id,
-	});
-	if(o.timer) clearTimeout(o.timer);
-	o.timer = setTimeout(function(){delete badges[src.id];}, 300);
-	callback();
-}
-
-function getCacheB64(ids, callback) {
-	function loop() {
-		var id = ids.pop();
-		if (id)
-			o.get(id).onsuccess = function(e){
-				var v = e.target.result;
-				if(v) data[id] = v.data;
-				loop();
-			};
-		else callback(data);
-	}
-	var o = db.transaction('cache').objectStore('cache');
-	var data = {};
-	loop();
+  var o;
+  if(src.id in badges) o = badges[src.id];
+  else badges[src.id] = o = {num: 0};
+  o.num += num;
+  chrome.browserAction.setBadgeBackgroundColor({color: '#808', tabId: src.tab.id});
+  chrome.browserAction.setBadgeText({
+    text: o.num ? o.num.toString() : '',
+    tabId: src.tab.id,
+  });
+  if(o.timer) clearTimeout(o.timer);
+  o.timer = setTimeout(function(){delete badges[src.id];}, 300);
+  callback();
 }
 
 function getInjected(url, src, callback) {
-	function getScripts(){
-		var o = db.transaction('scripts').objectStore('scripts');
-		var require = {};
-		var cache = {};
-		var values = [];
-		o.index('position').openCursor().onsuccess = function (e) {
-			var r = e.target.result;
-			if (r) {
-				var v = r.value;
-				if (testURL(url, v)) {
-					data.scripts.push(v);
-					values.push(v.uri);
-					v.meta.require.forEach(function(i){require[i] = 1;});
-					for(var i in v.meta.resources) cache[v.meta.resources[i]] = 1;
-				}
-				r.continue();
-			} else {
-				count = 3;
-				getRequire(Object.getOwnPropertyNames(require));
-				getCacheB64(Object.getOwnPropertyNames(cache), function (cache) {
-					data.cache = cache;
-					finish();
-				});
-				getValues(values);
-			}
-		};
-	}
-	function getRequire(require) {
-		function loop() {
-			var uri = require.pop();
-			if(uri)
-				o.get(uri).onsuccess = function(e) {
-					var r = e.target.result;
-					if (r) data.require[uri] = r.code;
-					loop();
-				};
-			else finish();
-		}
-		var o = db.transaction('require').objectStore('require');
-		loop();
-	}
-	function getValues(values) {
-		function loop(){
-			var uri = values.pop();
-			if (uri)
-				o.get(uri).onsuccess = function (e) {
-					var v = e.target.result;
-					if (v) data.values[uri] = v.values;
-					loop();
-				};
-			else finish();
-		}
-		var o = db.transaction('values').objectStore('values');
-		loop();
-	}
-	function finish(){
-		if (! -- count) {
-			callback(data);
-			if(src.url == src.tab.url)
-				chrome.tabs.sendMessage(src.tab.id, {cmd: 'GetBadge'});
-		}
-	}
-	var data = {
-		scripts: [],
-		values: {},
-		require: {},
-		injectMode: _.options.get('injectMode'),
-		version: vm_ver,
-	};
-	var count = 1;
-	if (data.isApplied = _.options.get('isApplied')) getScripts();
-	else finish();
-	return true;
+  function getScripts(){
+    var o = db.transaction('scripts').objectStore('scripts');
+    var require = {};
+    var cache = {};
+    var values = [];
+    o.index('position').openCursor().onsuccess = function (e) {
+      var r = e.target.result;
+      if (r) {
+        var v = r.value;
+        if (testURL(url, v)) {
+          data.scripts.push(v);
+          values.push(v.uri);
+          v.meta.require.forEach(function(i){require[i] = 1;});
+          for(var i in v.meta.resources) cache[v.meta.resources[i]] = 1;
+        }
+        r.continue();
+      } else {
+        count = 3;
+        getRequire(Object.getOwnPropertyNames(require));
+        getCacheB64(Object.getOwnPropertyNames(cache), function (cache) {
+          data.cache = cache;
+          finish();
+        });
+        getValues(values);
+      }
+    };
+  }
+  function getRequire(require) {
+    function loop() {
+      var uri = require.pop();
+      if(uri)
+        o.get(uri).onsuccess = function(e) {
+          var r = e.target.result;
+          if (r) data.require[uri] = r.code;
+          loop();
+        };
+      else finish();
+    }
+    var o = db.transaction('require').objectStore('require');
+    loop();
+  }
+  function getValues(values) {
+    function loop(){
+      var uri = values.pop();
+      if (uri)
+        o.get(uri).onsuccess = function (e) {
+          var v = e.target.result;
+          if (v) data.values[uri] = v.values;
+          loop();
+        };
+      else finish();
+    }
+    var o = db.transaction('values').objectStore('values');
+    loop();
+  }
+  function finish(){
+    if (! -- count) {
+      callback(data);
+      if(src.url == src.tab.url)
+        chrome.tabs.sendMessage(src.tab.id, {cmd: 'GetBadge'});
+    }
+  }
+  var data = {
+    scripts: [],
+    values: {},
+    require: {},
+    injectMode: _.options.get('injectMode'),
+    version: vm_ver,
+  };
+  var count = 1;
+  if (data.isApplied = _.options.get('isApplied')) getScripts();
+  else finish();
+  return true;
 }
 
 function fetchURL(url, cb, type, headers) {
   var req = new XMLHttpRequest();
   req.open('GET', url, true);
   if (type) req.responseType = type;
-	if (headers) for(var i in headers)
-		req.setRequestHeader(i, headers[i]);
+  if (headers) for(var i in headers)
+    req.setRequestHeader(i, headers[i]);
   if(cb) req.onloadend = cb;
   req.send();
 }
 
-function saveCache(url, data, callback) {
-	var o = db.transaction('cache', 'readwrite').objectStore('cache');
-	o.put({uri: url, data: data}).onsuccess = callback;
-}
-
-function saveRequire(url, data, callback) {
-	var o = db.transaction('require', 'readwrite').objectStore('require');
-	o.put({uri: url, code: data}).onsuccess = callback;
-}
-
-var u_cache = {};
-function fetchCache(url, check) {
-	function saveResult(blob) {
-		var r = new FileReader();
-		r.onload = function(e) {
-			saveCache(url, window.btoa(this.result), function () {delete u_cache[url];});
-		};
-		r.readAsBinaryString(blob);
-	}
-	if (!u_cache[url]) {
-		u_cache[url] = 1;
-		fetchURL(url, function() {
-			if (this.status != 200) return;
-			if (check) check(this.response, saveResult);
-			else saveResult(this.response);
-		}, 'blob');
-	}
-}
-
-var u_require = {};
-function fetchRequire(url) {
-	if(u_require[url]) return;
-	u_require[url] = 1;
-	fetchURL(url, function(){
-		if (this.status == 200)
-			saveRequire(url, this.responseText, function(){delete u_require[url];});
-	});
-}
-
 function updateItem(data) {
-	if (port) try {
-		port.postMessage(data);
-	} catch(e) {
-		port = null;
-		console.log(e);
-	}
-}
-
-function queryScript(id, meta, callback) {
-	var o = db.transaction('scripts').objectStore('scripts');
-	function finish(script) {
-		if(!script) script = newScript();
-		if(callback) callback(script);
-	}
-	function queryMeta() {
-		var uri = getNameURI({id: '', meta: meta});
-		if (uri != '::')
-			o.index('uri').get(uri).onsuccess = function (e) {
-				finish(e.target.result);
-			};
-		else finish();
-	}
-	function queryId() {
-		if (id)
-			o.get(id).onsuccess = function(e) {
-				var script = e.target.result;
-				if(script) finish(script);
-				else queryMeta();
-			};
-		else queryMeta();
-	}
-	queryId();
-}
-
-function saveScript(script) {
-	var o = db.transaction('scripts', 'readwrite').objectStore('scripts');
-	script.enabled = script.enabled ? 1 : 0;
-	script.update = script.update ? 1 : 0;
-	if (!script.position) script.position = ++ position;
-	return o.put(script);
+  if (port) try {
+    port.postMessage(data);
+  } catch(e) {
+    port = null;
+    console.log(e);
+  }
 }
 
 function parseScript(data, src, callback) {
-	function finish() {
-		updateItem(ret);
-		if (callback) callback(ret);
-	}
-	var ret = {
-		cmd: 'update',
-		data: {
-			message: 'message' in data ? data.message : _.i18n('msgUpdated'),
-		},
-	};
-	if (data.status && data.status != 200 || data.code == '') {
-		// net error
-		ret.cmd = 'error';
-		ret.data.message = _.i18n('msgErrorFetchingScript');
-		finish();
-	} else {
-		// store script
-		var meta = parseMeta(data.code);
-		queryScript(data.id, meta, function(script) {
-			if (!script.id) {
-				ret.cmd = 'add';
-				ret.data.message = _.i18n('msgInstalled');
-			}
-			// add additional data for import and user edit
-			if (data.more)
-				for(var i in data.more)
-					if(i in script) script[i] = data.more[i];
-			script.meta = meta;
-			script.code = data.code;
-			script.uri = getNameURI(script);
-			// use referer page as default homepage
-			if (data.from && !script.meta.homepageURL && !script.custom.homepageURL && !/^(file|data):/.test(data.from))
-				script.custom.homepageURL = data.from;
-			if (data.url && !/^(file|data):/.test(data.url))
-				script.custom.lastInstallURL = data.url;
-			saveScript(script).onsuccess = function(e) {
-				script.id = e.target.result;
-				_.assign(ret.data, getMeta(script));
-				finish();
-				if (!meta.grant.length && !_.options.get('ignoreGrant'))
-					notify({
-						id: 'VM-NoGrantWarning',
-						title: _.i18n('Warning'),
-						body: _.i18n('msgWarnGrant', [meta.name||_.i18n('labelNoName')]),
-						isClickable: true,
-					});
-			};
-		});
-		// @require
-		meta.require.forEach(function (url) {
-			var cache = data.require && data.require[url];
-			if(cache) saveRequire(url, cache);
-			else fetchRequire(url);
-		});
-		// @resource
-		for(var i in meta.resources) {
-			var url = meta.resources[i];
-			var cache = data.resources && data.resources[url];
-			if(cache) saveCache(url, cache);
-			else fetchCache(url);
-		}
-		// @icon
-		if(isRemote(meta.icon)) fetchCache(meta.icon, function (blob, cb) {
-			var free = function() {
-				URL.revokeObjectURL(url);
-			};
-			var url = URL.createObjectURL(blob);
-			var image = new Image;
-			image.onload = function() {
-				free();
-				cb(blob);
-			};
-			image.onerror = function() {
-				free();
-			};
-			image.src = url;
-		});
-	}
-	return true;
-}
-
-function setValue(data, src, callback) {
-	var o = db.transaction('values', 'readwrite').objectStore('values');
-	o.put({uri:data.uri, values:data.values});
-	if (callback) callback();
-}
-
-function updateMeta(meta, src, callback) {
-	var o = db.transaction('scripts', 'readwrite').objectStore('scripts');
-	o.get(meta.id).onsuccess = function (e) {
-		var script = e.target.result;
-		if (!script) return;
-		for (var i in meta)
-			if (i in script) script[i] = meta[i];
-		o.put(script).onsuccess = function (e) {
-			updateItem({
-				cmd: 'update',
-				data: getMeta(script),
-			});
-		};
-	};
-	if (callback) callback();
+  function finish() {
+    updateItem(ret);
+    if (callback) callback(ret);
+  }
+  var ret = {
+    cmd: 'update',
+    data: {
+      message: 'message' in data ? data.message : _.i18n('msgUpdated'),
+    },
+  };
+  if (data.status && data.status != 200 || data.code == '') {
+    // net error
+    ret.cmd = 'error';
+    ret.data.message = _.i18n('msgErrorFetchingScript');
+    finish();
+  } else {
+    // store script
+    var meta = parseMeta(data.code);
+    queryScript(data.id, meta, function(script) {
+      if (!script.id) {
+        ret.cmd = 'add';
+        ret.data.message = _.i18n('msgInstalled');
+      }
+      // add additional data for import and user edit
+      if (data.more)
+        for(var i in data.more)
+          if(i in script) script[i] = data.more[i];
+      script.meta = meta;
+      script.code = data.code;
+      script.uri = getNameURI(script);
+      // use referer page as default homepage
+      if (data.from && !script.meta.homepageURL && !script.custom.homepageURL && !/^(file|data):/.test(data.from))
+        script.custom.homepageURL = data.from;
+      if (data.url && !/^(file|data):/.test(data.url))
+        script.custom.lastInstallURL = data.url;
+      saveScript(script).onsuccess = function(e) {
+        script.id = e.target.result;
+        Object.assign(ret.data, getMeta(script));
+        finish();
+        if (!meta.grant.length && !_.options.get('ignoreGrant'))
+          notify({
+            id: 'VM-NoGrantWarning',
+            title: _.i18n('Warning'),
+            body: _.i18n('msgWarnGrant', [meta.name||_.i18n('labelNoName')]),
+            isClickable: true,
+          });
+      };
+    });
+    // @require
+    meta.require.forEach(function (url) {
+      var cache = data.require && data.require[url];
+      if(cache) saveRequire(url, cache);
+      else fetchRequire(url);
+    });
+    // @resource
+    for(var i in meta.resources) {
+      var url = meta.resources[i];
+      var cache = data.resources && data.resources[url];
+      if(cache) saveCache(url, cache);
+      else fetchCache(url);
+    }
+    // @icon
+    if(isRemote(meta.icon)) fetchCache(meta.icon, function (blob, cb) {
+      var free = function() {
+        URL.revokeObjectURL(url);
+      };
+      var url = URL.createObjectURL(blob);
+      var image = new Image;
+      image.onload = function() {
+        free();
+        cb(blob);
+      };
+      image.onerror = function() {
+        free();
+      };
+      image.src = url;
+    });
+  }
+  return true;
 }
 
 var _update = {};
@@ -638,377 +347,105 @@ function realCheckUpdate(script) {
       ret.data.message = _.i18n('msgUpdating');
       fetchURL(downloadURL, function(){
         parseScript({
-					id: script.id,
+          id: script.id,
           status: this.status,
           code: this.responseText,
         });
       });
     } else ret.data.message = '<span class=new>' + _.i18n('msgNewVersion') + '</span>';
     updateItem(ret);
-		finish();
+    finish();
   }
-	function finish(){
-		delete _update[script.id];
-	}
-	if (_update[script.id]) return;
-	_update[script.id] = 1;
+  function finish(){
+    delete _update[script.id];
+  }
+  if (_update[script.id]) return;
+  _update[script.id] = 1;
   var ret = {
-		cmd: 'update',
-		data: {
-			id: script.id,
-			updating: true,
-		},
-	};
-	var downloadURL =
-		script.custom.downloadURL ||
-		script.meta.downloadURL ||
-		script.custom.lastInstallURL;
-	var updateURL =
-		script.custom.updateURL ||
-		script.meta.updateURL ||
-		downloadURL;
+    cmd: 'update',
+    data: {
+      id: script.id,
+      updating: true,
+    },
+  };
+  var downloadURL =
+    script.custom.downloadURL ||
+    script.meta.downloadURL ||
+    script.custom.lastInstallURL;
+  var updateURL =
+    script.custom.updateURL ||
+    script.meta.updateURL ||
+    downloadURL;
   if(updateURL) {
     ret.data.message = _.i18n('msgCheckingForUpdate');
-		updateItem(ret);
+    updateItem(ret);
     fetchURL(updateURL, function() {
       ret.data.message = _.i18n('msgErrorFetchingUpdateInfo');
       if (this.status == 200)
-				try {
-					var meta = parseMeta(this.responseText);
-					if(compareVersion(script.meta.version, meta.version) < 0)
-						return update();
-					ret.data.message = _.i18n('msgNoUpdate');
-				} catch(e) {}
-			ret.data.updating = false;
+        try {
+          var meta = parseMeta(this.responseText);
+          if(compareVersion(script.meta.version, meta.version) < 0)
+            return update();
+          ret.data.message = _.i18n('msgNoUpdate');
+        } catch(e) {}
+      ret.data.updating = false;
       updateItem(ret);
-			finish();
+      finish();
     }, null, {
-			Accept:'text/x-userscript-meta',
-		});
+      Accept:'text/x-userscript-meta',
+    });
   } else finish();
 }
 
 function checkUpdate(id, src, callback) {
-	var o = db.transaction('scripts').objectStore('scripts');
-	o.get(id).onsuccess = function (e) {
-		var script = e.target.result;
-		if(script) realCheckUpdate(script);
-		if(callback) callback();
-	};
-	return true;
+  var o = db.transaction('scripts').objectStore('scripts');
+  o.get(id).onsuccess = function (e) {
+    var script = e.target.result;
+    if(script) realCheckUpdate(script);
+    if(callback) callback();
+  };
+  return true;
 }
 
 function checkUpdateAll(e, src, callback) {
-	_.options.set('lastUpdate', Date.now());
-	var o = db.transaction('scripts').objectStore('scripts');
-	o.index('update').openCursor(1).onsuccess = function (e) {
-		var r = e.target.result;
-		if (r) {
-			realCheckUpdate(r.value);
-			r.continue();
-		} else if(callback) callback();
-	};
-	return true;
+  _.options.set('lastUpdate', Date.now());
+  var o = db.transaction('scripts').objectStore('scripts');
+  o.index('update').openCursor(1).onsuccess = function (e) {
+    var r = e.target.result;
+    if (r) {
+      realCheckUpdate(r.value);
+      r.continue();
+    } else if(callback) callback();
+  };
+  return true;
 }
 
 var _autoUpdate = false;
 function autoUpdate(data, src, callback) {
   function check() {
-		if(_.options.get('autoUpdate')) {
-			if (Date.now() - _.options.get('lastUpdate') >= 864e5)
-				checkUpdateAll();
-			setTimeout(check, 36e5);
-		} else _autoUpdate = false;
+    if(_.options.get('autoUpdate')) {
+      if (Date.now() - _.options.get('lastUpdate') >= 864e5)
+        checkUpdateAll();
+      setTimeout(check, 36e5);
+    } else _autoUpdate = false;
   }
   if (!_autoUpdate) {
-		_autoUpdate = true;
-		check();
-	}
-	if (callback) callback();
-}
-
-function getData(e, src, callback) {
-	function getScripts() {
-		var o = db.transaction('scripts').objectStore('scripts');
-		var cache = {};
-		o.index('position').openCursor().onsuccess = function (e) {
-			var r = e.target.result;
-			if (r) {
-				var script = r.value;
-				if(isRemote(script.meta.icon))
-					cache[script.meta.icon] = 1;
-				data.scripts.push(getMeta(script));
-				r.continue();
-			} else getCache(Object.getOwnPropertyNames(cache));
-		};
-	}
-	function getCache(uris) {
-		getCacheB64(uris, function(cache) {
-			for(var i in cache)
-				cache[i] = 'data:image/png;base64,' + cache[i];
-			data.cache = cache;
-			callback(data);
-		});
-	}
-	var data = {scripts: []};
-	getScripts();
-	return true;
-}
-
-function exportZip(exp, src, callback) {
-	function getScripts() {
-		function loop() {
-			var id = exp.data.shift();
-			if (id)
-				o.get(id).onsuccess = function (e) {
-					var script = e.target.result;
-					if(script) {
-						data.scripts.push(script);
-						if(exp.values) values.push(script.uri);
-					}
-					loop();
-				};
-			else getValues(values);
-		}
-		var values = [];
-		var o = db.transaction('scripts').objectStore('scripts');
-		loop();
-	}
-	function getValues(values) {
-		function loop() {
-			var uri = values.shift();
-			if (uri)
-				o.get(uri).onsuccess = function (e) {
-					var v = e.target.result;
-					if (v) data.values[uri] = v.values;
-					loop();
-				};
-			else finish();
-		}
-		if(exp.values) {
-			var o = db.transaction('values').objectStore('values');
-			data.values = {};
-			loop();
-		} else finish();
-	}
-	function finish() {
-		callback(data);
-	}
-	var data = {scripts: []};
-	getScripts();
-	return true;
-}
-
-// Requests
-var requests = {};
-var requests_dict = {};
-var special_headers = ['user-agent', 'referer', 'origin', 'host'];
-function getRequestId(data, src, callback) {
-  var id = getUniqId();
-  requests[id] = {
-		xhr: new XMLHttpRequest(),
-	};
-	callback(id);
-}
-function httpRequest(details, src, callback) {
-  function reqCallback(evt) {
-		function finish() {
-			chrome.tabs.sendMessage(src.tab.id, {
-				cmd: 'HttpRequested',
-				data: {
-					id: details.id,
-					type: evt.type,
-					resType: xhr.responseType,
-					data: data
-				}
-			});
-		}
-		var data = {
-			finalUrl: req.finalUrl,
-			readyState: xhr.readyState,
-			responseHeaders: xhr.getAllResponseHeaders(),
-			status: xhr.status,
-			statusText: xhr.statusText
-		};
-		try {
-			data.responseText = xhr.responseText;
-		} catch(e) {}
-		if(xhr.response && xhr.responseType == 'blob') {
-			var r = new FileReader();
-			r.onload = function(e) {
-				data.response = r.result;
-				finish();
-			};
-			r.readAsDataURL(xhr.response);
-		} else {
-			// default `null` for blob and '' for text
-			data.response = xhr.response;
-			finish();
-		}
-		if(evt.type == 'loadend') {
-			if(req.coreId) delete requests_dict[req.coreId];
-			delete requests[details.id];
-		}
-  }
-  var req = requests[details.id];
-	if (!req) return;
-	var xhr = req.xhr;
-  try {
-		// details.async = true;
-    xhr.open(details.method, details.url, true, details.user, details.password);
-		xhr.setRequestHeader('VM-Verify', details.id);
-    if(details.headers)
-			for(var i in details.headers) {
-				var v = details.headers[i];
-				if(special_headers.indexOf(i.toLowerCase()) >= 0)
-					xhr.setRequestHeader('VM-' + i, v);
-				else xhr.setRequestHeader(i, v);
-			}
-		if(details.responseType)
-			xhr.responseType = 'blob';
-    if(details.overrideMimeType)
-			xhr.overrideMimeType(details.overrideMimeType);
-    ['abort', 'error', 'load', 'loadend', 'progress', 'readystatechange', 'timeout'].forEach(function (i) {
-      xhr['on'+i] = reqCallback;
-    });
-		req.finalUrl = details.url;
-		xhr.send(details.data);
-  } catch(e) {
-		console.log(e);
+    _autoUpdate = true;
+    check();
   }
-}
-function abortRequest(id) {
-  var req = requests[id];
-  if(req) req.xhr.abort();
-	if(req.coreId) delete requests_dict[req.coreId];
-  delete requests[id];
+  if (callback) callback();
 }
 
 chrome.runtime.onConnect.addListener(function (_port) {
-	port = _port;
-	_port.onDisconnect.addListener(function () {port = null;});
+  port = _port;
+  _port.onDisconnect.addListener(function () {port = null;});
 });
 
-function initPosition(callback) {
-	position = 0;
-	var o = db.transaction('scripts', 'readwrite').objectStore('scripts');
-	o.index('position').openCursor(null, 'prev').onsuccess = function (e) {
-		var r = e.target.result;
-		if(r && position < r.key) position = r.key;
-		if(callback) callback();
-	};
-}
-
-function initMessages() {
-	chrome.runtime.onMessage.addListener(function(req, src, callback) {
-		var maps = {
-			NewScript: function (o, src, callback) {callback(newScript());},
-			RemoveScript: removeScript,
-			GetData: getData,
-			GetInjected: getInjected,
-			CheckUpdate: checkUpdate,
-			CheckUpdateAll: checkUpdateAll,
-			UpdateMeta: updateMeta,
-			SetValue: setValue,
-			ExportZip: exportZip,
-			ParseScript: parseScript,
-			GetScript: getScript,
-			GetMetas: getMetas,
-			SetBadge: setBadge,
-			AutoUpdate: autoUpdate,
-			Vacuum: vacuum,
-			Move: move,
-			ParseMeta: function(o, src, callback) {callback(parseMeta(o));},
-			GetRequestId: getRequestId,
-			HttpRequest: httpRequest,
-			AbortRequest: abortRequest,
-		};
-		var func = maps[req.cmd];
-		if(func) return func(req.data, src, function() {
-			// if callback function is not given in content page, callback will fail
-			try {
-				callback.apply(null, arguments);
-			} catch(e) {}
-		});
-	});
-}
-
-initDb(function() {
-	initPosition(initMessages);
-	chrome.browserAction.setIcon({
-		path: '/images/icon19' + (_.options.get('isApplied') ? '' : 'w') + '.png',
-	});
-	setTimeout(autoUpdate, 2e4);
-});
-
-// Confirm page
-chrome.webRequest.onBeforeRequest.addListener(function(req) {
-	// onBeforeRequest is fired for local files too
-	if(/\.user\.js([\?#]|$)/.test(req.url)) {
-		var x = new XMLHttpRequest();
-		x.open('GET', req.url, false);
-		x.send();
-		if((!x.status || x.status == 200) && !/^\s*</.test(x.responseText)) {
-			if(req.tabId < 0)
-				chrome.tabs.create({
-					url: chrome.extension.getURL('/options/index.html') + '#confirm/' + encodeURIComponent(req.url),
-				});
-			else
-				chrome.tabs.get(req.tabId, function(t){
-					chrome.tabs.create({
-						url: chrome.extension.getURL('/options/index.html') + '#confirm/' + encodeURIComponent(req.url) + '/' + encodeURIComponent(t.url),
-					});
-				});
-			return {redirectUrl: 'javascript:history.back()'};
-		}
-	}
-}, {
-	urls: ['<all_urls>'],
-	types: ['main_frame'],
-}, ['blocking']);
-
-// Modifications on headers
-chrome.webRequest.onBeforeSendHeaders.addListener(function(details) {
-	var headers = details.requestHeaders;
-	var new_headers = [];
-	var vm_headers = {};
-	headers.forEach(function(header) {
-		if(header.name.substr(0,3) == 'VM-')
-			vm_headers[header.name.substr(3)] = header.value;
-		else
-			new_headers.push(header);
-	});
-	var verify = vm_headers['Verify'];
-	if(verify) {
-		var req = requests[verify];
-		if(req) {
-			delete vm_headers['Verify'];
-			requests_dict[details.requestId] = verify;
-			req.coreId = details.requestId;
-			for(var i in vm_headers)
-				if(special_headers.indexOf(i.toLowerCase()) >= 0)
-					new_headers.push({name: i, value: vm_headers[i]});
-		}
-	}
-	return {requestHeaders: new_headers};
-}, {
-	urls: ['<all_urls>'],
-	types: ['xmlhttprequest'],
-}, ["blocking", "requestHeaders"]);
-
-// Watch URL redirects
-chrome.webRequest.onBeforeRedirect.addListener(function(details) {
-	var verify = requests_dict[details.requestId]
-	if (verify) {
-		var req = requests[verify];
-		if (req) req.finalUrl = details.redirectUrl;
-	}
-}, {
-	urls: ['<all_urls>'],
-	types: ['xmlhttprequest'],
+chrome.browserAction.setIcon({
+  path: '/images/icon19' + (_.options.get('isApplied') ? '' : 'w') + '.png',
 });
 
 chrome.notifications.onClicked.addListener(function(id) {
-	if(id == 'VM-NoGrantWarning')
-		chrome.tabs.create({url: 'http://wiki.greasespot.net/@grant'});
+  if(id == 'VM-NoGrantWarning')
+    chrome.tabs.create({url: 'http://wiki.greasespot.net/@grant'});
 });

+ 182 - 0
src/background/requests.js

@@ -0,0 +1,182 @@
+var requests = function () {
+  var requests = {};
+  var verify = {};
+  var special_headers = [
+    'user-agent',
+    'referer',
+    'origin',
+    'host',
+  ];
+
+  function getRequestId() {
+    var id = _.getUniqId();
+    requests[id] = {
+      id: id,
+      xhr: new XMLHttpRequest,
+    };
+    return id;
+  }
+
+  function xhrCallbackWrapper(req) {
+    var lastPromise = Promise.resolve();
+    var xhr = req.xhr;
+    return function (evt) {
+      var res = {
+        id: req.id,
+        type: evt.type,
+        resType: xhr.responseType,
+      };
+      var data = res.data = {
+        finalUrl: req.finalUrl,
+        readyState: xhr.readyState,
+        responseHeaders: xhr.getAllResponseHeaders(),
+        status: xhr.status,
+        statusText: xhr.statusText,
+      };
+      try {
+        data.responseText = xhr.responseText;
+      } catch (e) {}
+      if (evt.type === 'loadend') clearRequest(req);
+      return lastPromise = lastPromise.then(function () {
+        return new Promise(function (resolve, reject) {
+          if (xhr.response && xhr.responseType === 'blob') {
+            var reader = new FileReader;
+            reader.onload = function (e) {
+              data.response = this.result;
+              resolve();
+            };
+            reader.readAsDataURL(xhr.response);
+          } else {
+            // default `null` for blob and '' for text
+            data.response = xhr.response;
+            resolve();
+          }
+        });
+      }).then(function () {
+        req.cb && req.cb(res);
+      });
+    };
+  }
+
+  function httpRequest(details, cb) {
+    var req = requests[details.id];
+    if (!req || req.cb) return;
+    req.cb = cb;
+    var xhr = req.xhr;
+    try {
+      xhr.open(details.method, details.url, true, details.user, details.password);
+      xhr.setRequestHeader('VM-Verify', details.id);
+      if (details.headers) {
+        for (var k in details.headers) {
+          xhr.setRequestHeader(
+            ~special_headers.indexOf(k.toLowerCase()) ? 'VM-' + k : k,
+            details.headers[k]
+          );
+        }
+      }
+      if (details.responseType)
+        xhr.responseType = 'blob';
+      if (details.overrideMimeType)
+        xhr.overrideMimeType(details.overrideMimeType);
+      var callback = xhrCallbackWrapper(req);
+      [
+        'abort',
+        'error',
+        'load',
+        'loadend',
+        'progress',
+        'readystatechange',
+        'timeout',
+      ].forEach(function (evt) {
+        xhr['on' + evt] = callback;
+      });
+      req.finalUrl = details.url;
+      xhr.send(details.data);
+    } catch (e) {
+      console.log(e);
+    }
+  }
+
+  function clearRequest(req) {
+    if (req.coreId) delete verify[req.coreId];
+    delete requests[req.id];
+  }
+
+  function abortRequest(id) {
+    var req = requests[id];
+    if (req) req.xhr.abort();
+    clearRequest(req);
+  }
+
+  return {
+    getRequestId: getRequestId,
+    abortRequest: abortRequest,
+    httpRequest: httpRequest,
+  };
+}();
+
+// Watch URL redirects
+chrome.webRequest.onBeforeRedirect.addListener(function (details) {
+	var reqId = verify[details.requestId];
+	if (reqId) {
+		var req = requests[reqId];
+		if (req) req.finalUrl = details.redirectUrl;
+	}
+}, {
+	urls: ['<all_urls>'],
+	types: ['xmlhttprequest'],
+});
+
+// Modifications on headers
+chrome.webRequest.onBeforeSendHeaders.addListener(function (details) {
+	var headers = details.requestHeaders;
+	var new_headers = [];
+	var vm_headers = {};
+	headers.forEach(function (header) {
+		if (header.name.substr(0, 3) == 'VM-')
+			vm_headers[header.name.slice(3)] = header.value;
+		else
+			new_headers.push(header);
+	});
+	var reqId = vm_headers['Verify'];
+	if (reqId) {
+		var req = requests[reqId];
+		if (req) {
+			delete vm_headers['Verify'];
+			verify[details.requestId] = reqId;
+			req.coreId = details.requestId;
+			for (var i in vm_headers)
+				if (~special_headers.indexOf(i.toLowerCase()))
+					new_headers.push({name: i, value: vm_headers[i]});
+		}
+	}
+	return {requestHeaders: new_headers};
+}, {
+	urls: ['<all_urls>'],
+	types: ['xmlhttprequest'],
+}, ['blocking', 'requestHeaders']);
+
+chrome.webRequest.onBeforeRequest.addListener(function (req) {
+	// onBeforeRequest is fired for local files too
+	if (/\.user\.js([\?#]|$)/.test(req.url)) {
+		var x = new XMLHttpRequest();
+		x.open('GET', req.url, false);
+		x.send();
+		if ((!x.status || x.status == 200) && !/^\s*</.test(x.responseText)) {
+			if (req.tabId < 0)
+				chrome.tabs.create({
+					url: chrome.extension.getURL('/options/index.html') + '#confirm/' + encodeURIComponent(req.url),
+				});
+			else
+				chrome.tabs.get(req.tabId, function (t) {
+					chrome.tabs.create({
+						url: chrome.extension.getURL('/options/index.html') + '#confirm/' + encodeURIComponent(req.url) + '/' + encodeURIComponent(t.url),
+					});
+				});
+			return {redirectUrl: 'javascript:history.back()'};
+		}
+	}
+}, {
+	urls: ['<all_urls>'],
+	types: ['main_frame'],
+}, ['blocking']);

+ 4 - 0
src/common.js

@@ -65,6 +65,10 @@ _.zfill = function (num, length) {
   return num;
 };
 
+_.getUniqId = function () {
+	return Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
+};
+
 /*
 function format() {
   var args = arguments;