Browse Source

Finish background scripts

Gerald 10 years ago
parent
commit
0f91465513
5 changed files with 576 additions and 538 deletions
  1. 300 227
      src/background/db.js
  2. 1 0
      src/background/index.html
  3. 106 310
      src/background/main.js
  4. 168 0
      src/background/utils.js
  5. 1 1
      src/options/templates/script.html

+ 300 - 227
src/background/db.js

@@ -1,6 +1,7 @@
 function VMDB() {
   var _this = this;
   _this.initialized = _this.openDB().then(_this.initPosition.bind(_this));
+  _this.checkUpdate = _this.checkUpdate.bind(_this);
 }
 
 VMDB.prototype.openDB = function () {
@@ -9,6 +10,7 @@ VMDB.prototype.openDB = function () {
     var request = indexedDB.open('Violentmonkey', 1);
     request.onsuccess = function (e) {
       _this.db = request.result;
+      resolve();
     };
     request.onerror = function (e) {
       var err = e.target.error;
@@ -43,15 +45,15 @@ VMDB.prototype.initPosition = function () {
   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;
+      if (result) _this.position = result.key;
       resolve();
     };
   });
 };
 
-VMDB.prototype.getScript = function (id, os) {
-  os = os || this.db.transaction('scripts').objectStore('scripts');
+VMDB.prototype.getScript = function (id, tx) {
+  tx = tx || this.db.transaction('scripts');
+  var os = tx.objectStore('scripts');
   return new Promise(function (resolve, reject) {
     os.get(id).onsuccess = function (e) {
       resolve(e.target.result);
@@ -59,12 +61,13 @@ VMDB.prototype.getScript = function (id, os) {
   });
 };
 
-VMDB.prototype.queryScript = function (id, meta) {
-  return id ? this.getScript(id)
+VMDB.prototype.queryScript = function (id, meta, tx) {
+  var _this = this;
+  return id ? _this.getScript(id, tx)
   : new Promise(function (resolve, reject) {
-    var uri = getNameURI({meta: meta});
+    var uri = scriptUtils.getNameURI({meta: meta});
     if (uri !== '::')
-      this.db.transaction('scripts').objectStore('scripts')
+      (tx || _this.db.transaction('scripts')).objectStore('scripts')
       .index('uri').get(uri).onsuccess = function (e) {
         resolve(e.target.result);
       };
@@ -76,7 +79,7 @@ VMDB.prototype.queryScript = function (id, meta) {
 VMDB.prototype.getScriptData = function (id) {
   return this.getScript(id).then(function (script) {
     if (!script) return Promise.reject();
-    var data = scriptUtils.getMeta(script);
+    var data = scriptUtils.getScriptInfo(script);
     data.code = script.code;
     return data;
   });
@@ -84,48 +87,40 @@ VMDB.prototype.getScriptData = function (id) {
 
 VMDB.prototype.getScriptInfos = function (ids) {
   var _this = this;
-  var os = _this.db.transaction('scripts').objectStore('scripts');
+  var tx = _this.db.transaction('scripts');
   return Promise.all(ids.map(function (id) {
-    return _this.getScript(id, os);
+    return _this.getScript(id, tx);
   })).then(function (scripts) {
-    return scripts.filter(function (x) {return x;}).map(scriptUtils.getMeta);
+    return scripts.filter(function (x) {return x;}).map(scriptUtils.getScriptInfo);
   });
 };
 
 VMDB.prototype.getScriptsByURL = function (url) {
   function getScripts() {
-    return new Promise(function (resolve, reject) {
+    return _this.getScriptsByIndex('position', null, tx).then(function (scripts) {
       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);
+      data.scripts = scripts.filter(function (script) {
+        if (tester.testURL(url, script)) {
+          data.uris.push(script.uri);
+          script.meta.require.forEach(function (key) {
+            require[key] = 1;
+          });
+          for (var k in script.meta.resources)
+            cache[script.meta.resources[k]] = 1;
+          return true;
         }
-      };
+      });
+      data.require = Object.keys(require);
+      data.cache = Object.keys(cache);
+      return data;
     });
   }
   function getRequire(uris) {
-    var o = db.transaction('require').objectStore('require');
+    var o = tx.objectStore('require');
     return Promise.all(uris.map(function (uri) {
       return new Promise(function (resolve, reject) {
         o.get(uri).onsuccess = function (e) {
@@ -140,7 +135,7 @@ VMDB.prototype.getScriptsByURL = function (url) {
     });
   }
   function getValues(uris) {
-    var o = db.transaction('values').objectStore('values');
+    var o = tx.objectStore('values');
     return Promise.all(uris.map(function (uri) {
       return new Promise(function (resolve, reject) {
         o.get(uri).onsuccess = function (e) {
@@ -155,12 +150,12 @@ VMDB.prototype.getScriptsByURL = function (url) {
     });
   }
   var _this = this;
-  var db = _this.db;
+  var tx = _this.db.transaction(['scripts', 'require', 'values', 'cache']);
   return getScripts().then(function (data) {
     return Promise.all([
       getRequire(data.require),
       getValues(data.uris),
-      _this.getCacheB64(data.cache),
+      _this.getCacheB64(data.cache, tx),
     ]).then(function (res) {
       return {
         scripts: data.scripts,
@@ -174,35 +169,26 @@ VMDB.prototype.getScriptsByURL = function (url) {
 
 VMDB.prototype.getData = function () {
   function getScripts() {
-    return new Promise(function (resolve, reject) {
-      var data = {
-        scripts: [],
-      };
+    return _this.getScriptsByIndex('position', null, tx).then(function (scripts) {
+      var data = {};
       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(scriptUtils.getMeta(value));
-          result.continue();
-        } else {
-          data.cache = Object.getOwnPropertyNames(cache);
-          resolve(data);
-        }
-      }
+      data.scripts = scripts.map(function (script) {
+        if (scriptUtils.isRemote(script.meta.icon)) cache[script.meta.icon] = 1;
+        return scriptUtils.getScriptInfo(script);
+      });
+      data.cache = Object.keys(cache);
+      return data;
     });
   }
   function getCache(uris) {
-    return _this.getCacheB64(uris).then(function (cache) {
+    return _this.getCacheB64(uris, tx).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;
+  var tx = _this.db.transaction(['scripts', 'cache']);
   return getScripts().then(function (data) {
     return getCache(data.cache).then(function (cache) {
       return {
@@ -214,14 +200,19 @@ VMDB.prototype.getData = function () {
 };
 
 VMDB.prototype.removeScript = function (id) {
-	var o = this.db.transaction('scripts', 'readwrite').objectStore('scripts');
-	o.delete(id);
-  return Promise.resolve();
+  var tx = this.db.transaction('scripts', 'readwrite');
+  return new Promise(function (resolve, reject) {
+    var o = tx.objectStore('scripts');
+    o.delete(id).onsuccess = function () {
+      resolve();
+    };
+  });
 };
 
 VMDB.prototype.moveScript = function (id, offset) {
-  var o = this.db.transaction('scripts', 'readwrite').objectStore('scripts');
-  return this.getScript(id).then(function (script) {
+  var tx = this.db.transaction('scripts', 'readwrite');
+  var o = tx.objectStore('scripts');
+  return this.getScript(id, tx).then(function (script) {
     var pos = script.position;
     var range, order;
     if (offset < 0) {
@@ -254,8 +245,9 @@ VMDB.prototype.moveScript = function (id, offset) {
   });
 };
 
-VMDB.prototype.getCacheB64 = function (urls) {
-  var o = this.db.transaction('cache').objectStore('cache');
+VMDB.prototype.getCacheB64 = function (urls, tx) {
+  tx = tx || this.db.transaction('cache');
+  var o = tx.objectStore('cache');
   return Promise.all(urls.map(function (url) {
     return new Promise(function (resolve, reject) {
       o.get(url).onsuccess = function (e) {
@@ -269,8 +261,9 @@ VMDB.prototype.getCacheB64 = function (urls) {
   });
 };
 
-VMDB.prototype.saveCache = function (url, data) {
-  var o = this.db.transaction('cache', 'readwrite').objectStore('cache');
+VMDB.prototype.saveCache = function (url, data, tx) {
+  tx = tx || this.db.transaction('cache', 'readwrite');
+  var o = tx.objectStore('cache');
   return new Promise(function (resolve, reject) {
     o.put({uri: url, data: data}).onsuccess = function () {
       resolve();
@@ -278,8 +271,9 @@ VMDB.prototype.saveCache = function (url, data) {
   });
 };
 
-VMDB.prototype.saveRequire = function (url, data) {
-  var o = this.db.transaction('require', 'readwrite').objectStore('require');
+VMDB.prototype.saveRequire = function (url, data, tx) {
+  tx = tx || this.db.transaction('require', 'readwrite');
+  var o = tx.objectStore('require');
   return new Promise(function (resolve, reject) {
     o.put({uri: url, code: data}).onsuccess = function () {
       resolve();
@@ -287,47 +281,32 @@ VMDB.prototype.saveRequire = function (url, data) {
   });
 };
 
-VMDB.prototype.saveScript = function (script) {
+VMDB.prototype.saveScript = function (script, tx) {
   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]);
+  tx = tx || this.db.transaction('scripts', 'readwrite');
+  var o = tx.objectStore('scripts');
   return new Promise(function (resolve, reject) {
-    xhr.onload = function () {
-      resolve(this);
+    o.put(script).onsuccess = function (e) {
+      script.id = e.target.result;
+      resolve(script);
     };
-    xhr.onerror = function () {
-      reject(this);
-    };
-    xhr.send();
   });
 };
 
 VMDB.prototype.fetchCache = function () {
   var requests = {};
-  return function (url, check) {
+  return function (url, tx, check) {
     var _this = this;
     return requests[url]
-    || (requests[url] = _this.fetch(url, 'blob').then(function (res) {
+    || (requests[url] = scriptUtils.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 () {
+          _this.saveCache(url, window.btoa(this.result), tx).then(function () {
             delete requests[url];
             resolve();
           });
@@ -343,11 +322,11 @@ VMDB.prototype.fetchCache = function () {
 
 VMDB.prototype.fetchRequire = function () {
   var requests = {};
-  return function (url) {
+  return function (url, tx) {
     var _this = this;
     return requests[url]
-    || (requests[url] = _this.fetch(url).then(function (res) {
-      return _this.saveRequire(url, res.responseText);
+    || (requests[url] = scriptUtils.fetch(url).then(function (res) {
+      return _this.saveRequire(url, res.responseText, tx);
     }).then(function () {
       delete requests[url];
     }));
@@ -372,7 +351,7 @@ VMDB.prototype.updateScriptInfo = function (id, data) {
       for (var k in data)
         if (k in script) script[k] = data[k];
       o.put(script).onsuccess = function (e) {
-        resolve(scriptUtils.getMeta(script));
+        resolve(scriptUtils.getScriptInfo(script));
       };
     };
   });
@@ -380,7 +359,7 @@ VMDB.prototype.updateScriptInfo = function (id, data) {
 
 VMDB.prototype.getExportData = function (ids, withValues) {
   function getScripts(ids) {
-    var o = db.transaction('scripts').objectStore('scripts');
+    var o = tx.objectStore('scripts');
     return Promise.all(ids.map(function (id) {
       return new Promise(function (resolve, reject) {
         o.get(id).onsuccess = function (e) {
@@ -392,7 +371,7 @@ VMDB.prototype.getExportData = function (ids, withValues) {
     });
   }
   function getValues(uris) {
-    var o = db.transaction('values').objectStore('values');
+    var o = tx.objectStore('values');
     return Promise.all(uris.map(function (uri) {
       return new Promise(function (resolve, reject) {
         o.get(uri).onsuccess = function (e) {
@@ -406,7 +385,7 @@ VMDB.prototype.getExportData = function (ids, withValues) {
       }, {});
     });
   }
-  var db = this.db;
+  var tx = this.db.transaction(['scripts', 'values']);
   return getScripts(ids).then(function (scripts) {
     var res = {
       scripts: scripts,
@@ -419,142 +398,236 @@ VMDB.prototype.getExportData = function (ids, withValues) {
   });
 };
 
-var scriptUtils = {
-  parseMeta: function (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;
-  },
-  newScript: function () {
-    var script = {
-  		custom: {},
-  		enabled: 1,
-  		update: 1,
-  		code: '// ==UserScript==\n// @name New Script\n// ==/UserScript==\n',
-  	};
-  	script.meta = scriptUtils.parseMeta(script.code);
-  	return script;
-  },
-  getMeta: function (script) {
-    return {
-  		id: script.id,
-  		custom: script.custom,
-  		meta: script.meta,
-  		enabled: script.enabled,
-  		update: script.update,
-  	};
-  },
-  getNameURI: function (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);
+VMDB.prototype.vacuum = function () {
+  function getScripts() {
+    return _this.getScriptsByIndex('position', null, tx).then(function (scripts) {
+      var data = {
+        require: {},
+        cache: {},
+        values: {},
+      };
+      data.ids = scripts.map(function (script) {
+        script.meta.require.forEach(function (uri) {data.require[uri] = 1;});
+        for (var k in script.meta.resources)
+          data.cache[script.meta.resources[k]] = 1;
+        if (scriptUtils.isRemote(script.meta.icon))
+          data.cache[script.meta.icon] = 1;
+        data.values[script.uri] = 1;
+        return script.id;
       });
-    } else {
-      // @include
-      ok = inc.some(function (str) {
-        return autoReg(str).test(url);
+      return data;
+    });
+  }
+  function vacuumPosition(ids) {
+    var o = tx.objectStore('scripts');
+    return ids.reduce(function (res, id, i) {
+      return res.then(function () {
+        return new Promise(function (resolve, reject) {
+          o.get(id).onsuccess = function (e) {
+            var result = e.target.result;
+            result.position = i + 1;
+            o.put(result).onsuccess = function () {
+              resolve();
+            };
+          };
+        });
       });
-    }
-    // exclude
-    ok = ok && !exc.some(function (str) {
-      return autoReg(str).test(url);
+    }, Promise.resolve());
+  }
+  function vacuumCache(dbName, dict) {
+    return new Promise(function (resolve, reject) {
+      var o = tx.objectStore(dbName);
+      o.openCursor().onsuccess = function (e) {
+        var result = e.target.result;
+        if (result) {
+          var value = result.value;
+          (new Promise(function (resolve, reject) {
+            if (!dict[value.uri])
+              o.delete(value.uri).onsuccess = function () {
+                resolve();
+              };
+            else {
+              dict[value.uri] ++;
+              resolve();
+            }
+          })).then(function () {
+            result.continue();
+          });
+        } else resolve();
+      };
     });
-    return ok;
   }
+  var _this = this;
+  var tx = _this.db.transaction(['scripts', 'require', 'cache', 'values'], 'readwrite');
+  return getScripts().then(function (data) {
+    return Promise.all([
+      vacuumPosition(data.ids),
+      vacuumCache('require', data.require),
+      vacuumCache('cache', data.cache),
+      vacuumCache('values', data.values),
+    ]).then(function () {
+      return {
+        require: data.require,
+        cache: data.cache,
+      };
+    });
+  }).then(function (data) {
+    return Promise.all([
+      Object.keys(data.require).map(function (k) {
+        return data.require[k] === 1 && _this.fetchRequire(k, tx);
+      }),
+      Object.keys(data.cache).map(function (k) {
+        return data.cache[k] === 1 && _this.fetchCache(k, tx);
+      }),
+    ]);
+  });
+};
 
-  function str2RE(str) {
-    return RegExp('^' + str.replace(/([.?\/])/g, '\\$1').replace(/\*/g, '.*?') + '$');
-  }
+VMDB.prototype.getScriptsByIndex = function (index, value, tx) {
+  tx = tx || this.db.transaction('scripts');
+  return new Promise(function (resolve, reject) {
+    var o = tx.objectStore('scripts');
+    var list = [];
+    o.index(index).openCursor(value).onsuccess = function (e) {
+      var result = e.target.result;
+      if (result) {
+        list.push(result.value);
+        result.continue();
+      } else resolve(list);
+    };
+  });
+};
 
-  function autoReg(str) {
-    if (/^\/.*\/$/.test(str))
-      return RegExp(str.slice(1, -1));	// Regular-expression
-    else
-      return str2RE(str);	// String with wildcards
-  }
+VMDB.prototype.parseScript = function (data) {
+  var res = {
+    cmd: 'update',
+    data: {
+      message: data.msg == null ? _.i18n('msgUpdated') : data.msg || '',
+    },
+  };
+  var meta = scriptUtils.parseMeta(data.code);
+  var _this = this;
+  var tx = _this.db.transaction(['scripts', 'require'], 'readwrite');
+  // @require
+  meta.require.forEach(function (url) {
+    var cache = data.require && data.require[url];
+    cache ? _this.saveRequire(url, cache, tx) : _this.fetchRequire(url, tx);
+  });
+  // @resource
+  Object.keys(meta.resources).forEach(function (k) {
+    var url = meta.resources[k];
+    var cache = data.resources && data.resources[url];
+    cache ? _this.saveCache(url, cache, tx) : _this.fetchCache(url, tx);
+  });
+  // @icon
+  if (scriptUtils.isRemote(meta.icon))
+    _this.fetchCache(meta.icon, tx, function (blob) {
+      return new Promise(function (resolve, reject) {
+        var url = URL.createObjectURL(blob);
+        var image = new Image;
+        var free = function () {
+          URL.revokeObjectURL(url);
+        };
+        image.onload = function () {
+          free();
+          resolve();
+        };
+        image.onerror = function () {
+          free();
+          reject();
+        };
+        image.src = url;
+      });
+    });
+  return _this.queryScript(data.id, meta, tx).then(function (script) {
+    if (!script.id) {
+      res.cmd = 'add';
+      res.data.message = _.i18n('msgInstalled');
+    }
+    if (data.more) for (var k in data.more)
+      if (k in script) script[k] = data.more[k];
+    script.meta = meta;
+    script.code = data.code;
+    script.uri = scriptUtils.getNameURI(script);
+    // use referer page as default homepage
+    if (!meta.homepageURL && !script.custom.homepageURL && scriptUtils.isRemote(data.from))
+      script.custom.homepageURL = data.from;
+    if (scriptUtils.isRemote(data.url))
+      script.custom.lastInstallURL = data.url;
+    return _this.saveScript(script, tx);
+  }).then(function (script) {
+    Object.assign(res.data, scriptUtils.getScriptInfo(script));
+    return res;
+  });
+};
 
-  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;
+VMDB.prototype.checkUpdate = function () {
+  function check(script) {
+    var res = {
+      cmd: 'update',
+      data: {
+        id: script.id,
+        checking: true,
+      },
+    };
+    var downloadURL = script.custom.downloadURL || script.meta.downloadURL || script.custom.lastInstallURL;
+    var updateURL = script.custom.updateURL || script.meta.updateURL || downloadURL;
+    var okHandler = function (xhr) {
+      var meta = scriptUtils.parseMeta(xhr.responseText);
+      if (scriptUtils.compareVersion(script.meta.version, meta.version) < 0)
+        return resolve();
+      res.data.checking = false;
+      res.data.message = _.i18n('msgNoUpdate');
+      _.messenger.post(res);
+      return Promise.reject();
+    };
+    var errHandler = function (xhr) {
+      res.data.checking = false;
+      res.data.message = _.i18n('msgErrorFetchingUpdateInfo');
+      _.messenger.post(res);
+      return Promise.reject();
+    };
+    var update = function () {
+      if (!downloadURL) {
+        res.data.message = '<span class="new">' + _.i18n('msgNewVersion') + '</span>';
+        _.messenger.post(res);
+        return Promise.reject();
+      }
+      res.data.message = _.i18n('msgUpdating');
+      return scriptUtils.fetch(downloadURL).then(function (xhr) {
+        res.data.checking = false;
+        _.messenger.post(res);
+        return xhr.responseText;
+      }, function (xhr) {
+        res.data.checking = false;
+        res.data.message = _.i18n('msgErrorFetchingScript');
+        _.messenger.post(res);
+        return Promise.reject();
+      });
+    };
+    if (!updateURL) return Promise.reject();
+    res.data.message = _.i18n('msgCheckingForUpdate');
+    _.messenger.post(res);
+    return scriptUtils.fetch(updateURL, null, {
+      Accept: 'text/x-userscript-meta',
+    }).then(okHandler, errHandler).then(update);
   }
 
-  return {
-    testURL: testURL,
+  var processes = {};
+  return function (script) {
+    var _this = this;
+    var promise = processes[script.id];
+    if (!promise)
+      promise = processes[script.id] = check(script).then(function (code) {
+        delete processes[script.id];
+        return _this.parseScript({
+          id: script.id,
+          code: code,
+        });
+      }, function () {
+        delete processes[script.id];
+        //return Promise.reject();
+      });
+    return promise;
   };
 }();

+ 1 - 0
src/background/index.html

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

+ 106 - 310
src/background/main.js

@@ -1,6 +1,5 @@
 var vmdb = new VMDB;
-var port = null;
-var vm_ver = chrome.app.getDetails().version;
+var VM_VER = chrome.app.getDetails().version;
 var commands = {
   NewScript: function (data, src) {
     return Promise.resolve(scriptUtils.newScript());
@@ -15,14 +14,14 @@ var commands = {
     var data = {
       isApplied: _.options.get('isApplied'),
       injectMode: _.options.get('injectMode'),
-      version: vm_ver,
+      version: VM_VER,
     };
     if(src.url == src.tab.url)
       chrome.tabs.sendMessage(src.tab.id, {cmd: 'GetBadge'});
     return data.isApplied
     ? vmdb.getScriptsByURL(url).then(function (res) {
       return Object.assign(data, res);
-    } : Promise.resolve(data);
+    }) : Promise.resolve(data);
   },
   UpdateMeta: function (data, src) {
     return vmdb.updateScriptInfo(data.id, data);
@@ -42,16 +41,56 @@ var commands = {
   Move: function (data, src) {
     return vmdb.moveScript(data.id, data.offset);
   },
-  CheckUpdate: checkUpdate,
-  CheckUpdateAll: checkUpdateAll,
-  ParseScript: parseScript,
-  SetBadge: setBadge,
+  Vacuum: function (data, src) {
+    return vmdb.vacuum();
+  },
+  ParseScript: function (data, src) {
+    return vmdb.parseScript(data).then(function (res) {
+      var meta = res.data.meta;
+      if (!meta.grant.length && !_.options.get('ignoreGrant'))
+        notify({
+          id: 'VM-NoGrantWarning',
+          title: _.i18n('Warning'),
+          body: _.i18n('msgWarnGrant', [meta.name||_.i18n('labelNoName')]),
+          isClickable: true,
+        });
+      _.messenger.post(res);
+    });
+  },
+  CheckUpdate: function (id, src) {
+    vmdb.getScript(id).then(vmdb.checkUpdate);
+    return false;
+  },
+  CheckUpdateAll: function (data, src) {
+    _.options.set('lastUpdate', Date.now());
+    vmdb.getScriptsByIndex('update', 1).then(function (scripts) {
+      return Promise.all(scripts.map(vmdb.checkUpdate));
+    });
+    return false;
+  },
+  ParseMeta: function (code, src) {
+    return Promise.resolve(scriptUtils.parseMeta(code));
+  },
   AutoUpdate: autoUpdate,
-  Vacuum: vacuum,
-  ParseMeta: function(o, src, callback) {callback(parseMeta(o));},
-  GetRequestId: getRequestId,
-  HttpRequest: httpRequest,
-  AbortRequest: abortRequest,
+  GetRequestId: function (data, src) {
+    return Promise.resolve(requests.getRequestId());
+  },
+  HttpRequest: function (details, src) {
+    requests.httpRequest(details, function (res) {
+      _.messenger.send(src.tab.id, {
+        cmd: 'HttpRequested',
+        data: res,
+      });
+    });
+    return false;
+  },
+  AbortRequest: function (id, src) {
+    return Promise.resolve(requests.abortRequest(id));
+  },
+  SetBadge: function (num, src) {
+    setBadge(num, src);
+    return false;
+  },
 };
 
 vmdb.initialized.then(function () {
@@ -76,16 +115,6 @@ vmdb.initialized.then(function () {
 
 // Common functions
 
-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 notify(options) {
   chrome.notifications.create(options.id || 'ViolentMonkey', {
     type: 'basic',
@@ -96,302 +125,69 @@ function notify(options) {
   });
 }
 
-function isRemote(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 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;
-}
-
-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 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(cb) req.onloadend = cb;
-  req.send();
-}
-
-function updateItem(data) {
-  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;
-        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;
+var setBadge = function () {
+  var badges = {};
+  return function (num, src) {
+    var o = badges[src.id];
+    if (!o) o = badges[src.id] = {num: 0};
+    o.num += num;
+    chrome.browserAction.setBadgeBackgroundColor({
+      color: '#808',
+      tabId: src.tab.id,
     });
-  }
-  return true;
-}
-
-var _update = {};
-function realCheckUpdate(script) {
-  function update() {
-    if(downloadURL) {
-      ret.data.message = _.i18n('msgUpdating');
-      fetchURL(downloadURL, function(){
-        parseScript({
-          id: script.id,
-          status: this.status,
-          code: this.responseText,
-        });
-      });
-    } else ret.data.message = '<span class=new>' + _.i18n('msgNewVersion') + '</span>';
-    updateItem(ret);
-    finish();
-  }
-  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;
-  if(updateURL) {
-    ret.data.message = _.i18n('msgCheckingForUpdate');
-    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;
-      updateItem(ret);
-      finish();
-    }, null, {
-      Accept:'text/x-userscript-meta',
+    chrome.browserAction.setBadgeText({
+      text: (o.num || '').toString(),
+      tabId: src.tab.id,
     });
-  } 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;
-}
-
-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();
+    if (o.timer) clearTimeout(o.timer);
+    o.timer = setTimeout(function () {
+      delete badges[src.id];
+    }, 300);
   };
-  return true;
-}
+}();
 
-var _autoUpdate = false;
-function autoUpdate(data, src, callback) {
+var autoUpdate = function () {
   function check() {
-    if(_.options.get('autoUpdate')) {
+    checking = true;
+    return new Promise(function (resolve, reject) {
+      if (!_.options.get('autoUpdate')) return reject();
       if (Date.now() - _.options.get('lastUpdate') >= 864e5)
-        checkUpdateAll();
+        return commands.CheckUpdateAll();
+    }).then(function () {
       setTimeout(check, 36e5);
-    } else _autoUpdate = false;
-  }
-  if (!_autoUpdate) {
-    _autoUpdate = true;
-    check();
+    }, function () {
+      checking = false;
+    });
   }
-  if (callback) callback();
-}
+  var checking;
+  return function () {
+    checking || check();
+  };
+}();
+
+_.messenger = function () {
+  var port;
+  chrome.runtime.onConnect.addListener(function (_port) {
+    port = _port;
+    _port.onDisconnect.addListener(function () {
+      if (port === _port) port = null;
+    });
+  });
 
-chrome.runtime.onConnect.addListener(function (_port) {
-  port = _port;
-  _port.onDisconnect.addListener(function () {port = null;});
-});
+  return {
+    post: function (data) {
+      try {
+        port && port.postMessage(data);
+      } catch (e) {
+        console.log(e);
+        port = null;
+      }
+    },
+    send: function (tabId, data) {
+      chrome.tabs.sendMessage(tabId, data);
+    },
+  };
+}();
 
 chrome.browserAction.setIcon({
   path: '/images/icon19' + (_.options.get('isApplied') ? '' : 'w') + '.png',

+ 168 - 0
src/background/utils.js

@@ -0,0 +1,168 @@
+var scriptUtils = {
+  isRemote: function (url) {
+    return url && !/^(file|data):/.test(url);
+  },
+  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();
+    });
+  },
+  parseMeta: function (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;
+  },
+  newScript: function () {
+    var script = {
+      custom: {},
+      enabled: 1,
+      update: 1,
+      code: '// ==UserScript==\n// @name New Script\n// ==/UserScript==\n',
+    };
+    script.meta = scriptUtils.parseMeta(script.code);
+    return script;
+  },
+  getScriptInfo: function (script) {
+    return {
+      id: script.id,
+      custom: script.custom,
+      meta: script.meta,
+      enabled: script.enabled,
+      update: script.update,
+    };
+  },
+  getNameURI: function (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;
+  },
+  compareVersion: function (ver1, ver2) {
+    ver1 = (ver1 || '').split('.');
+    ver2 = (ver2 || '').split('.');
+    var len1 = ver1.length, len2 = ver2.length;
+    for (var i = 0; i < len1 || i < len2; i ++) {
+      var delta = (parseInt(ver1[i], 10) || 0) - (parseInt(ver2[i], 10) || 0);
+      if (delta) return delta < 0 ? -1 : 1;
+    }
+    return 0;
+  },
+};
+
+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,
+  };
+}();

+ 1 - 1
src/options/templates/script.html

@@ -26,7 +26,7 @@
   <button data-id=remove data-i18n="buttonRemove"></button>
   <%= it.canUpdate ?
   '<button data-id=update data-i18n="buttonUpdate"' + (
-    it.updating ? ' disabled' : ''
+    it.checking ? ' disabled' : ''
   ) + '></button>'
   : '' %>
   <span data-id=message><%= it.message %></span>