error reporting cosmetix
[guerillascript.git] / modules / scriptcache.js
blobfee1f78181abffb463050e8794513c8ea4a1c61e
1 /*
2 * Copyright 2015 Ketmar Dark <ketmar@ketmar.no-ip.org>
3 * Portions copyright 2004-2007 Aaron Boodman
4 * Contributors: See contributors list in install.rdf and CREDITS
6 * Permission is hereby granted, free of charge, to any person obtaining a copy
7 * of this software and associated documentation files (the "Software"), to deal
8 * in the Software without restriction, including without limitation the rights
9 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 * copies of the Software, and to permit persons to whom the Software is
11 * furnished to do so, subject to the following conditions:
13 * Note that this license applies only to the Greasemonkey extension source
14 * files, not to the user scripts which it runs. User scripts are licensed
15 * separately by their authors.
17 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 * SOFTWARE.
25 * The above copyright notice and this permission notice shall be included in all
26 * copies or substantial portions of the Software.
28 ////////////////////////////////////////////////////////////////////////////////
29 //let ioSvc = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
32 ////////////////////////////////////////////////////////////////////////////////
33 function fileReadText (file) {
34 let inputStream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream);
35 inputStream.init(file, -1, -1, null);
36 let scInputStream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(Ci.nsIScriptableInputStream);
37 scInputStream.init(inputStream);
38 let output = scInputStream.read(-1);
39 scInputStream.close();
40 inputStream.close();
41 let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Ci.nsIScriptableUnicodeConverter);
42 converter.charset = "UTF-8";
43 let res = converter.ConvertToUnicode(output);
44 if (typeof(res) != "string") throw new Error("fucked file '"+file.path+"'");
45 // fuck BOM
46 if (res.length >= 3 && res.substr(0, 3) == "\u00EF\u00BB\u00BF") res = res.substr(3);
47 return res;
51 ////////////////////////////////////////////////////////////////////////////////
52 function uri2re (uri) {
53 uri = uri.replace(/^\s+/, "").replace(/\s+$/, "");
54 if (uri == "") return null;
55 if (uri == "*") return null;
56 // regexp?
57 if (uri.length >= 2 && uri.charCodeAt(0) == 47 && uri.charCodeAt(uri.length-1) == 47) {
58 if (uri.length < 3) return null;
59 return new RegExp(uri.substring(1, uri.length-1), "i");
61 // convert glob to regexp
62 let re = "^";
63 for (let f = 0; f < uri.length; ++f) {
64 switch (uri[f]) {
65 case "*": re += ".*?"; break; // any, non-greedy
66 case ".": case "?": case "^": case "$": case "+":
67 case "{": case "}": case "[": case "]": case "|":
68 case "(": case ")": case "\\":
69 re += "\\"+uri[f];
70 break;
71 case " ": break; // ignore spaces
72 default: re += uri[f]; break;
75 re += "$";
76 //if (guerillaOptions.debugCache) conlog("uri:["+uri+"] re: ["+re+"]");
77 return new RegExp(re, "i");
81 ////////////////////////////////////////////////////////////////////////////////
82 function isValidFileName (fname) {
83 if (fname.length < 4) return false;
84 if (fname[0] == "/") return false;
85 if (fname.substr(fname.length-3) != ".js") return false;
86 return true;
90 function normFileName (fname) {
91 fname = fname.replace(/^\s+/, "").replace(/\s+$/, "");
92 let res = [];
93 for (let fn of fname.split("/")) {
94 if (fn.length == 0 || fn == ".") continue;
95 if (fn == "..") {
96 if (res.length > 0) res = res.slice(0, res.length-1);
97 } else {
98 res.push(fn);
101 return res.join("/");
105 // is ".user.js" script?
106 function isGMJS (nfo) { return (/\.user\.js$/.test(nfo.name)); }
109 function buildBaseName (name) {
110 let mt = name.match(/^(.+)\.js$/);
111 let res = (mt ? mt[1] : name+"_unk");
112 let nop = res.match(/([^\/]+)$/);
113 if (nop) res = nop[1];
114 return res;
118 ////////////////////////////////////////////////////////////////////////////////
119 const wholeMetaRE = /^\s*\/\/\s*==UserScript==\s*([\s\S]*?)^\/\/\s*==\/UserScript==/m;
120 const metakvRE = /^\s*@(\S+)\s+(.+?)\s*$/;
121 const metakeyRE = /^\s*@(\S+)\s*$/;
124 // get the stuff between ==UserScript== lines
125 function extractMeta (text) {
126 let mt = text.match(wholeMetaRE);
127 return (mt ? mt[1].replace(/^\s+/, "") : null);
131 // this should actually filter spaces and multiline comments, but let it be noop for now
132 function metaDisabled (text) {
134 let mt = text.match(/^\s*(\S\S)/);
135 return (!mt || mt[1] != "//");
137 return false;
141 function processMeta (nfo, meta) {
142 if (!meta) {
143 nfo.disabled = true;
144 return;
146 let lines = meta.split("\n");
147 for (let lno = 0; lno < lines.length; ++lno) {
148 let line = lines[lno];
149 // remove slashes
150 if (line.length > 1 && line.charCodeAt(0) == 47 && line.charCodeAt(1) == 47) line = line.substr(2);
151 let kv = line.match(metakvRE);
152 if (!kv) {
153 kv = line.match(metakeyRE);
154 if (kv) {
155 switch (kv[1]) {
156 case "unwrap": case "unwrapped": nfo.unwrapped = true; break;
157 case "wrap": case "wrapped": nfo.unwrapped = false; break;
158 case "noframe": case "noframes": nfo.noframes = true; break;
159 case "frame": case "frames": nfo.noframes = false; break;
160 case "disabled": nfo.disabled = true; break;
163 continue;
165 let key = kv[1];
166 let value = kv[2];
167 if (key == "library" || key == "libraries") {
168 let libs = value.split(/\s+/);
169 for (let f = 0; f < libs.length; ++f) {
170 let lib = normFileName(libs[f]);
171 if (lib.length == 0 || !isValidFileName(lib)) continue;
172 let found = false;
173 for (let z = 0; z < nfo.libs.length; ++z) if (nfo.libs[z] == lib) { found = true; break; }
174 if (!found) nfo.libs.push(lib);
176 continue;
178 if (key == "require" || key == "requires") {
179 let reqs = value.split(/\s+/);
180 for (let f = 0; f < reqs.length; ++f) {
181 let req = normFileName(reqs[f]);
182 if (req.length == 0 || !isValidFileName(req)) continue;
183 let found = false;
184 for (let z = 0; z < nfo.reqs.length; ++z) if (nfo.reqs[z] == req) { found = true; break; }
185 if (!found) nfo.reqs.push(req);
187 continue;
189 if (key == "run-at") {
190 if (value == "document-start") nfo.atDocStart = true;
191 continue;
193 // "@grant none" means the same as "@unwrap"
194 if (key == "grant") {
195 switch (value) {
196 case "none": nfo.wasGrantNone = true; break;
197 case "GM_log": nfo.wantLog = true; break;
198 case "unsafeWindow": break;
199 default: if (value) nfo.wasGrantAnything = true; break;
201 continue;
203 if (key != "include" && key != "exclude") continue;
204 if (value == "*") {
205 if (key == "include") nfo.incAll = true; else nfo.excAll = true;
206 continue;
208 if (key == "include" && nfo.incAll) continue;
209 if (key == "exclude" && nfo.excAll) continue;
210 let re = uri2re(value);
211 if (key == "include") nfo.incREs.push(re); else nfo.excREs.push(re);
216 ////////////////////////////////////////////////////////////////////////////////
217 let uniqueCount = 0;
218 let jsdirLMT = 0, libdirLMT = 0;
219 let scCache = {}, libCache = {};
220 let scNameList = new Array(); // alphabetically sorted
222 /* cache element:
223 * string name: script name ("abcd.js")
224 * string path: script path
225 * string text: script text
226 * number lmtime: lastmod time
227 * number upcount: last update (compared with uniqueCount)
228 * RegExp[] incREs: regular expressions for "@include"
229 * RegExp[] excREs: regular expressions for "@exclude"
230 * bool incAll: "@include" has "*"
231 * bool excAll: "@exclude" has "*"
232 * bool unwrapped
233 * bool noframes
234 * bool atDocStart: run at "document-start"
235 * string libs[]: list of required libraries (names on scan, nfos after fixup)
236 * string reqs[]: list of required files (names on scan, nfos after fixup)
237 * bool disabled
238 * bool wasGrantAnything
239 * bool wasGrantNone
240 * bool wantLog
244 ////////////////////////////////////////////////////////////////////////////////
245 function parseUserJSOptions (nfo, isujs) {
246 nfo.incREs = [];
247 nfo.excREs = [];
248 nfo.incAll = nfo.excAll = false;
249 nfo.unwrapped = null;
250 nfo.noframes = false;
251 nfo.atDocStart = false;
252 nfo.libs = [];
253 nfo.reqs = [];
254 nfo.disabled = false;
255 nfo.wasGrantAnything = false;
256 nfo.wasGrantNone = false;
257 nfo.wantLog = isujs && !isGMJS(nfo);
258 if (isujs) {
259 let dis = metaDisabled(nfo.text);
260 processMeta(nfo, extractMeta(nfo.text));
261 if (nfo.wasGrantNone) nfo.unwrapped = true;
262 else if (nfo.wasGrantAnything) nfo.unwrapped = false;
263 // GM scripts without "@grant" should be unwrapped
264 if (isGMJS(nfo)) {
265 if (nfo.unwrapped === null && !nfo.wasGrantAnything) nfo.unwrapped = true;
267 if (!nfo.incAll && nfo.incREs.length == 0) nfo.excAll = true; //nfo.incAll = true;
268 if (nfo.incAll) nfo.incREs = [];
269 if (nfo.excAll) nfo.excREs = [];
270 if (dis) nfo.disabled = dis;
271 if (nfo.unwrapped === null) nfo.unwrapped = false;
276 ////////////////////////////////////////////////////////////////////////////////
277 function cleanupLibCache () {
278 let deadScripts = false;
279 for (let k in libCache) {
280 if (typeof(k) != "string") continue;
281 let nfo = libCache[k];
282 if (nfo.upcount != uniqueCount) {
283 // dead script
284 if (!deadScripts) deadScripts = [];
285 deadScripts.push(k);
288 // remove dead scripts
289 if (deadScripts) {
290 if (guerillaOptions.debugCache) conlog(""+deadScripts.length+" dead libs found");
291 for (let f = deadScripts.length-1; f >= 0; --f) {
292 if (guerillaOptions.debugCache) conlog("removing dead lib '"+deadScripts[f]+"' ("+libCache[deadScripts[f]].upcount+" : "+uniqueCount+")");
293 delete libCache[deadScripts[f]];
299 function buildNameList () {
300 let deadScripts = false;
301 scNameList = new Array();
302 // scan scCache, collect dead scripts and fill name list
303 for (let k in scCache) {
304 if (typeof(k) != "string") continue;
305 let nfo = scCache[k];
306 if (nfo.upcount != uniqueCount) {
307 // dead script
308 if (!deadScripts) deadScripts = [];
309 deadScripts.push(k);
310 } else {
311 scNameList.push(k);
314 // remove dead scripts
315 if (deadScripts) {
316 if (guerillaOptions.debugCache) conlog(""+deadScripts.length+" dead scripts found");
317 for (let f = deadScripts.length-1; f >= 0; --f) {
318 if (guerillaOptions.debugCache) conlog("removing dead script '"+deadScripts[f]+"'");
319 delete scCache[deadScripts[f]];
322 // sort name list
323 scNameList.sort();
324 if (guerillaOptions.debugCache) {
325 let s = ""+scNameList.length+" alive scripts found";
326 for (let f = 0; f < scNameList.length; ++f) s += "\n '"+scNameList[f]+"'";
327 conlog(s);
332 function fixupLibraries () {
333 let deadScripts = false;
334 // scan scCache, fix libraries, collect dead scripts
335 for (let k in scCache) {
336 if (typeof(k) != "string") continue;
337 let nfo = scCache[k];
338 if (nfo.libs.length == 0) continue; // empty
339 if (typeof(nfo.libs[0]) != "string") {
340 // already processed, restore
341 let lll = [];
342 for (let idx = 0; idx < nfo.libs.length; ++idx) {
343 if (guerillaOptions.debugCache) conlog("script '"+k+"': unfixing library '"+nfo.libs[idx].name+"'");
344 lll.push(nfo.libs[idx].name);
346 nfo.libs = lll;
348 // check libraries
349 let liblist = [];
350 for (let idx = 0; idx < nfo.libs.length; ++idx) {
351 let lib = libCache[nfo.libs[idx]];
352 if (!isValidFileName(nfo.libs[idx])) {
353 logError("script '"+k+"': library '"+nfo.libs[idx]+"' has invalid name!");
354 if (!deadScripts) deadScripts = [];
355 deadScripts.push(k);
356 break;
358 if (lib) {
359 if (guerillaOptions.debugCache) conlog("script '"+k+"': found library '"+nfo.libs[idx]+"'");
360 liblist.push(lib);
361 } else {
362 logError("script '"+k+"': library '"+nfo.libs[idx]+"' not found!");
363 if (!deadScripts) deadScripts = [];
364 deadScripts.push(k);
365 break;
368 nfo.libs = liblist;
370 // remove dead scripts
371 if (deadScripts) {
372 if (guerillaOptions.debugCache) conlog(""+deadScripts.length+" invalid scripts found");
373 for (let f = deadScripts.length-1; f >= 0; --f) {
374 if (guerillaOptions.debugCache) conlog("removing invalid script '"+deadScripts[f]+"'");
375 delete scCache[deadScripts[f]];
377 buildNameList(); // i'm soooo lazy
382 ////////////////////////////////////////////////////////////////////////////////
383 function checkReqs (nfo, baseDir) {
384 let reqs = nfo.reqs;
385 if (!reqs || reqs.length == 0) return true; // nothing to do
386 let jsdir = baseDir.clone();
387 // strip extension from name
388 jsdir.append(nfo.baseName);
389 if (!jsdir.exists()) return false; // alas
390 if (guerillaOptions.debugCache) conlog("checking "+reqs.length+" reqs for '"+nfo.path+"'");
391 for (let idx = 0; idx < reqs.length; ++idx) {
392 let req = reqs[idx];
393 if (typeof(req) != "string") {
394 // check if access time was changed
395 let fl = jsdir.clone();
396 fl.append(req.name);
397 if (!fl.exists()) return false;
398 if (req.lmtime == fl.lastModifiedTime) {
399 // nothing was changed
400 if (guerillaOptions.debugCache) conlog(" req #"+idx+" ("+req.path+") is ok");
401 continue;
403 if (guerillaOptions.debugCache) conlog(" req #"+idx+" ("+req.path+") has different time");
404 // convert to name
405 req = req.name;
407 if (!isValidFileName(req)) return false; // alas
408 // now, we have to reload file
409 try {
410 let name = req;
411 req = {};
412 let fl = jsdir.clone();
413 fl.append(name);
414 if (!fl.exists()) return false;
415 req.text = fileReadText(fl);
416 req.lmtime = fl.lastModifiedTime;
417 req.name = name;
418 req.baseName = buildBaseName(req.name);
419 req.path = fl.path;
420 } catch (e) {
421 return false;
423 if (guerillaOptions.debugCache) conlog(" req #"+idx+" ("+req.path+") is loaded");
424 // set object
425 reqs[idx] = req;
427 return true;
431 function scanDirectory (cache, dir, isujs) {
432 const extRE = /(\.[^.]+)$/;
433 if (guerillaOptions.debugCache) conlog("updating cache: '"+dir.path+"'");
434 let en = dir.directoryEntries;
435 while (en.hasMoreElements()) {
436 let fl = en.getNext().QueryInterface(Ci.nsILocalFile);
437 if (!fl.exists() || fl.isDirectory()) continue;
438 let name = fl.leafName;
439 let mt = name.match(extRE);
440 if (!mt || mt[1] != ".js") continue; // invalid extension
441 let nfo = cache[name];
442 if (!nfo) {
443 if (guerillaOptions.debugCache) conlog("new js: '"+name+"'");
444 nfo = {};
445 nfo.name = name;
446 nfo.baseName = buildBaseName(nfo.name);
447 nfo.path = fl.path;
448 cache[nfo.name] = nfo;
449 } else if (nfo.lmtime != fl.lastModifiedTime) {
450 if (guerillaOptions.debugCache) conlog("updated js: '"+name+"'");
451 } else {
452 if (checkReqs(nfo, dir)) nfo.upcount = uniqueCount;
453 continue;
455 try {
456 nfo.text = fileReadText(fl);
457 nfo.lmtime = fl.lastModifiedTime;
458 parseUserJSOptions(nfo, isujs);
459 if (checkReqs(nfo, dir)) nfo.upcount = uniqueCount;
460 } catch (e) {
461 logException("JS PARSER", e);
467 ////////////////////////////////////////////////////////////////////////////////
468 let scacheAPI = {};
471 ////////////////////////////////////////////////////////////////////////////////
472 scacheAPI.reset = function () {
473 if (guerillaOptions.debugCache) conlog("resetting script cache");
474 //++uniqueCount; // = 0;
475 jsdirLMT = libdirLMT = 0;
476 scCache = {};
477 libCache = {};
478 scNameList = new Array();
482 ////////////////////////////////////////////////////////////////////////////////
483 //TODO: check requisites? for now user have to touch main script to reload changed requisites
484 scacheAPI.validate = function () {
485 let uniCountIncreased = false;
486 // libraries
487 let libdir = getUserLibDir();
488 if (libdir.exists()) {
489 let lmtime = libdir.lastModifiedTime;
490 if (libdirLMT != lmtime) {
491 if (guerillaOptions.debugCache) conlog("updating library cache");
492 libdirLMT = lmtime;
493 ++uniqueCount;
494 uniCountIncreased = true;
495 scanDirectory(libCache, libdir, false);
498 // scripts
499 let jsdir = getUserJSDir();
500 if (jsdir.exists()) {
501 let lmtime = jsdir.lastModifiedTime;
502 if (uniCountIncreased || jsdirLMT != lmtime) {
503 if (guerillaOptions.debugCache) conlog("updating script cache");
504 jsdirLMT = lmtime;
505 if (!uniCountIncreased) {
506 ++uniqueCount;
507 uniCountIncreased = true;
508 // fixup libraries counters
509 if (guerillaOptions.debugCache) conlog("doing library counter fixup ("+uniqueCount+")");
510 for (let k in libCache) if (typeof(k) == "string") libCache[k].upcount = uniqueCount;
512 scanDirectory(scCache, jsdir, true);
514 } else {
515 // the thing that should not be
516 scacheAPI.reset();
518 // now rebuild other things
519 if (uniCountIncreased) {
520 cleanupLibCache();
521 buildNameList();
522 fixupLibraries();
527 ////////////////////////////////////////////////////////////////////////////////
528 scacheAPI.isGoodScheme = function (scstr) {
529 let cpos = scstr.indexOf(":");
530 if (cpos < 0) return false;
531 scstr = scstr.substr(0, cpos);
532 switch (scstr) {
533 case "http":
534 case "https":
535 //case "ftp": // no, really, why?
536 return true;
537 default:
538 return false;
543 ////////////////////////////////////////////////////////////////////////////////
544 // string uri
545 // returns array of nfos or false
546 scacheAPI.scriptsForUri = function (uri, inFrame, docStart) {
547 let res = null;
549 let scheme = ioSvc.extractScheme(uri);
550 switch (scheme) {
551 case "http":
552 case "https":
553 case "ftp":
554 break;
555 default:
556 return false;
559 inFrame = !!inFrame;
560 docStart = !!docStart;
561 scacheAPI.validate();
562 for (let idx = 0; idx < scNameList.length; ++idx) {
563 let nfo = scCache[scNameList[idx]];
564 if (nfo.excAll || nfo.disabled) continue;
565 if (inFrame && nfo.noframes) continue;
566 if (docStart != nfo.atDocStart) continue;
567 // check includes
568 if (!nfo.incAll) {
569 let ok = false;
570 let rl = nfo.incREs;
571 for (let f = 0; f < rl.length; ++f) if (uri.match(rl[f])) { ok = true; break; }
572 if (!ok) continue;
574 // url matches, check excludes
576 let ok = false;
577 let rl = nfo.excREs;
578 for (let f = 0; f < rl.length; ++f) if (uri.match(rl[f])) { ok = true; break; }
579 if (ok) continue;
581 // ok, this script passed checks
582 if (!res) res = [];
583 res.push(nfo);
585 return res;
589 ////////////////////////////////////////////////////////////////////////////////
590 // get current `uniqueCount`
591 scacheAPI.getUC = function () { return uniqueCount; }
594 ////////////////////////////////////////////////////////////////////////////////
595 // get script list; returns array of xnfo
596 // xnfo:
597 // string path
598 // string name
599 // string baseName
600 // bool islib;
601 // xnfo[] libs
602 // xnfo[] reqs
603 // todo: flags?
604 scacheAPI.getScripts = function () {
605 var nfo2xnfo;
606 var nfoarr2xnfo;
608 nfo2xnfo = function (nfo, islib) {
609 let x = {
610 path: nfo.path,
611 name: nfo.name,
612 baseName: nfo.baseName,
614 if (islib) x.islib = true;
615 x.libs = nfoarr2xnfo(nfo.libs, true);
616 if (!islib) x.reqs = nfoarr2xnfo(nfo.reqs);
617 return x;
620 nfoarr2xnfo = function (nfoa, islib) {
621 let res = [];
622 if (nfoa && nfoa.length) {
623 for (let idx = 0; idx < nfoa.length; ++idx) res.push(nfo2xnfo(nfoa[idx], islib));
625 return res;
628 let res = [];
629 for (let idx = 0; idx < scNameList.length; ++idx) {
630 let nfo = scCache[scNameList[idx]];
631 res.push(nfo2xnfo(nfo));
633 // add libraries
634 let lnm = [];
635 for (let k in libCache) { lnm.push(libCache[k].name); }
636 lnm.sort();
637 for (let idx = 0; idx < lnm.length; ++idx) {
638 let nfo = libCache[lnm[idx]];
639 res.push(nfo2xnfo(nfo, true));
641 return res;
645 ////////////////////////////////////////////////////////////////////////////////
646 scacheAPI.fileReadText = fileReadText;
647 exports = scacheAPI;