bugfix for `GM_getValue()`, as suggested by https://github.com/qsniyg/
[guerillascript.git] / main / modules / scriptcache.js
blob043885cf0b2dcd9add3cc3fc9b685b4050cc8a86
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
5  *
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:
12  *
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.
16  *
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.
24  *
25  * The above copyright notice and this permission notice shall be included in all
26  * copies or substantial portions of the Software.
27  */
28 ////////////////////////////////////////////////////////////////////////////////
29 require("utils/utils");
30 let {parseMeta} = require("utils/metaparser");
31 let {MatchPattern, uri2re} = require("utils/matchpattern");
34 ////////////////////////////////////////////////////////////////////////////////
35 function newIFile (path) {
36   let fl = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
37   fl.initWithPath(path);
38   return fl;
40 exports.newIFile = newIFile;
43 ////////////////////////////////////////////////////////////////////////////////
44 function buildRelFile (fl, path) {
45   if (!fl) return null;
46   let res = fl.clone();
47   let depthCount = 0;
48   for (let n of path.split("/")) {
49     if (!n || n == ".") continue;
50     if (n == "..") {
51       if (--depthCount < 0) return null;
52       res = res.parent;
53       if (!res) return null;
54     } else {
55       ++depthCount;
56       res.append(n);
57     }
58   }
59   return res;
61 exports.buildRelFile = buildRelFile;
64 ////////////////////////////////////////////////////////////////////////////////
65 let nameExtRE = /^(.+)(\.[^.]+)$/;
68 ////////////////////////////////////////////////////////////////////////////////
69 /* nfo:
70  *  nsIFile file: script file object
71  *  string name: script name, without ".js" ("abcd")
72  *  number lmtime: lastmod time
73  *  RegExp[] incREs: regular expressions for "@include"; empty array means "*"
74  *  RegExp[] incMatches: regular expressions for "@match"; empty array means "no @match"
75  *  RegExp[] excREs: regular expressions for "@exclude"
76  *  bool unwrapped
77  *  bool noframes
78  *  bool wantlog: want `conlog()` and `GM_log()` APIs?
79  *  string runat: run at "document-start", "document-end"
80  *  nsIFile[] libs: list of required libraries
81  *  nsIFile[] incs: list of required includes
82  *  {string name} resources: resources dict, keyed by resource name, value is {nsIFile file, string contenttype}
83  *  bool isPackage
84  *  bool isUserJS
85  */
86 function dumpNfo (nfo) {
87   conlog(" ========================================");
88   conlog(" file: [", nfo.file.path, "]");
89   conlog(" name: [", nfo.name, "]");
90   conlog(" lmtime: [", nfo.lmtime, "]");
91   conlog(" isPackage: [", nfo.isPackage, "]");
92   conlog(" isUserJS: [", nfo.isUserJS, "]");
93   conlog(" unwrapped: [", nfo.unwrapped, "]");
94   conlog(" noframes: [", nfo.noframes, "]");
95   conlog(" wantlog: [", nfo.wantlog, "]");
96   conlog(" runat: [", nfo.runat, "]");
97   if (nfo.incREs.length) {
98     conlog(" include:"); for (let re of nfo.incREs) conlog("   ", re);
99   } else {
100     conlog(" include: *");
101   }
102   if (nfo.excREs.length) { conlog(" exclude:"); for (let re of nfo.excREs) conlog("   ", re); }
103   if (nfo.libs.length) { conlog(" libs:"); for (let fl of nfo.libs) conlog("   [", fl.path, "]"); }
104   if (nfo.incs.length) { conlog(" incs:"); for (let fl of nfo.incs) conlog("   [", fl.path, "]"); }
105   let wasOut = false;;
106   for (let [name, fl] in Iterator(nfo.resources)) {
107     if (!wasOut) conlog(" resources:");
108     wasOut = true;
109     conlog("   [", name, "]=[", fl.path, "]");
110   }
112 exports.dumpScriptNfo = dumpNfo;
115 ////////////////////////////////////////////////////////////////////////////////
116 let ujsdirLMT = false; // directory modify times; will become number
117 let userscripts = new Array(); // sorted by name
118 let pkgscripts = new Array(); // sorted by package name
121 ////////////////////////////////////////////////////////////////////////////////
122 // return `false` if this script should be excluded
123 function processScriptMeta (nfo, reqtformCB) {
124   function addIncLib (dir, fname, arr) {
125     if (addonOptions.debugCache) conlog("lib: [", dir.path, "] : [", fname, "]");
126     let ne = fname.match(nameExtRE);
127     if (!ne || ne[2] != ".js") return true; // invalid file name, skip it
128     let fl = buildRelFile(dir, fname);
129     if (!dir.contains(fl, true)) return false; // access restricted
130     if (!fl.exists()) return false;
131     if (!fl.isFile() || !fl.isReadable()) return false;
132     // check for duplicates
133     let found = false;
134     for (let ef of arr) if (ef.equals(fl)) { found = true; break; }
135     if (!found) arr.push(fl);
136     return true;
137   }
139   // return `false` if invalid or non-existing file accessed
140   function procJSIncLib (dir, value, arr) {
141     if (addonOptions.debugCache) conlog("libs: [", value, "] from [", dir.path, "]");
142     for (let fname of value.split(/\s+/)) if (fname && !addIncLib(dir, fname, arr)) return false;
143     return true;
144   }
146   try {
147     nfo.isUserJS = nfo.isPackage || (/\.user\.js$/.test(nfo.file.leafName)); // is ".user.js" script?
148     nfo.incREs = new Array();
149     nfo.excREs = new Array();
150     nfo.incMatches = new Array();
151     nfo.unwrapped = null;
152     nfo.noframes = (nfo.isUserJS ? false : true); // default
153     nfo.wantlog = null;
154     nfo.runat = "document-end"; // default
155     nfo.libs = new Array();
156     nfo.incs = new Array();
157     nfo.resources = {};
158     let text = fileReadText(nfo.file);
159     if (!text) return false;
160     let meta = parseMeta(text);
161     if (meta.length == 0) return false;
162     //conlog("got meta for ", nfo.file.path);
163     // parse meta fields
164     let incAll = false;
165     let wasMatch = false;
166     let wasGrantNone = false;
167     let wasGrantLog = false;
168     let wasGrantAnything = false;
169     for (let kv of meta) {
170       if (kv.name === "disabled") {
171         // this script is disabled
172         if (addonOptions.debugCache) conlog("@disabled");
173         return false;
174       }
175       if (kv.name === "exclude") {
176         if (!kv.value) continue;
177         if (kv.value === "*") {
178           // excluded for all uris
179           if (addonOptions.debugCache) conlog("@exclude *");
180           return false;
181         }
182         let re = uri2re(kv.value);
183         if (re) nfo.excREs.push(re);
184         continue;
185       }
186       if (kv.name === "include") {
187         if (!incAll) {
188           if (!kv.value) continue;
189           if (kv.value === "*") {
190             incAll = true;
191             wasMatch = false;
192             if (nfo.incREs.length != 0) nfo.incREs = new Array();
193             if (nfo.incMatches.length != 0) nfo.incMatches = new Array();
194           } else {
195             let re = uri2re(kv.value);
196             if (re) nfo.incREs.push(re);
197           }
198         }
199         continue;
200       }
201       if (kv.name === "match") {
202         if (!incAll) {
203           if (!kv.value) continue;
204           try {
205             let mt = new MatchPattern(kv.value);
206             if (mt.all) {
207               //if (addonOptions.debugCache) conlog("@match <all_urls>");
208               incAll = true;
209               wasMatch = false;
210               if (nfo.incREs.length != 0) nfo.incREs = new Array();
211               if (nfo.incMatches.length != 0) nfo.incMatches = new Array();
212             } else {
213               //if (addonOptions.debugCache) conlog("@match: ", mt.pattern);
214               wasMatch = true;
215               nfo.incMatches.push(mt);
216             }
217           } catch (e) {}
218         }
219         continue;
220       }
221       if (kv.name === "run-at") {
222         switch (kv.value) {
223           case "document-start":
224           case "document-end":
225             nfo.runat = kv.value;
226             break;
227         }
228         continue;
229       }
230       if (kv.name === "unwrap" || kv.name === "unwrapped") {
231         nfo.unwrapped = true;
232         continue;
233       }
234       if (kv.name === "wrap" || kv.name === "wrapped") {
235         nfo.unwrapped = false;
236         continue;
237       }
238       if (kv.name === "noframe" || kv.name === "noframes") {
239         nfo.noframes = false;
240         continue;
241       }
242       if (kv.name === "frame" || kv.name === "frames") {
243         nfo.noframes = true;
244         continue;
245       }
246       if (kv.name === "library" || kv.name === "libraries") {
247         if (nfo.isPackage) continue;
248         if (!procJSIncLib(getUserLibDir(), kv.value, nfo.libs)) {
249           if (addonOptions.debugCache) conlog("bad @libraries");
250           return false;
251         }
252         continue;
253       }
254       if (kv.name === "require" || kv.name === "requires") {
255         if (nfo.isPackage) continue;
256         let dir = nfo.file.parent; // remove script name
257         dir.append(nfo.name);
258         if (nfo.isPackage) {
259           // special processing for scripts from packages
260           if (kv.name !== "require" || !kv.value) continue;
261           if (typeof(reqtformCB) !== "function") continue;
262           let v = reqtformCB(nfo, kv.value);
263           if (typeof(v) === "undefined") continue;
264           if (v === false) return false; // alas
265           if (!v || typeof(v) !== "string") continue;
266           if (!addIncLib(dir, v, nfo.incs)) return false;
267         } else {
268           if (!procJSIncLib(dir, kv.value, nfo.incs)) {
269             if (addonOptions.debugCache) conlog("bad @requires");
270             return false;
271           }
272         }
273         continue;
274       }
275       if (kv.name === "grant") {
276         // "@grant none" means the same as "@unwrap"
277         switch (kv.value) {
278           case "none": wasGrantNone = true; break;
279           case "GM_log": wasGrantLog = true; break;
280           case "unsafeWindow": break;
281           default: if (kv.value) wasGrantAnything = true; break;
282         }
283         continue;
284       }
285     }
286     // met any includes?
287     if (!incAll && !wasMatch && nfo.incREs.length == 0) {
288       // no includes at all
289       if (addonOptions.debugCache) conlog("no @include");
290       return false;
291     }
292     // fix access rights
293     if (wasGrantLog) {
294       nfo.wantlog = true;
295     } else {
296       if (nfo.wantlog === null) nfo.wantlog = (!nfo.isPackage && !nfo.isUserJS);
297     }
298     // if we met "@grant none", this means "unwrap"
299     if (wasGrantNone) {
300       if (nfo.unwrapped === null) nfo.unwrapped = true;
301     } else if (wasGrantAnything) {
302       // other "@grant"s means "wrapped"
303       if (nfo.unwrapped === null) nfo.unwrapped = false;
304     } else {
305       // nothing was specified
306       if (nfo.unwrapped === null) nfo.unwrapped = false;
307     }
308     // public `conlog` function for unwrapped scripts
309     if (nfo.unwrapped) nfo.wantlog = true;
310     return true;
311   } catch (e) {
312     logException("processScriptMeta", e);
313   }
314   return false;
316 exports.processScriptMeta = processScriptMeta;
319 ////////////////////////////////////////////////////////////////////////////////
320 // bool forced: do force scan (i.e. ignore jsdirLMT value)
321 function scanUJSDirectory (forced) {
322   //if (addonOptions.debugCache) conlog("updating cache: '"+dir.path+"'");
323   let dir = getUserJSDir();
324   if (!dir.exists()) { userscripts = new Array(); return; }
325   if (!dir.isDirectory()) { userscripts = new Array(); return; }
326   let lmtime = dir.lastModifiedTime;
327   if (!forced && lmtime === ujsdirLMT) return; // do nothing, as nothing was changed
328   // rescan
329   ujsdirLMT = lmtime;
330   userscripts = new Array();
331   let en = dir.directoryEntries;
332   while (en.hasMoreElements()) {
333     let fl = en.getNext().QueryInterface(Ci.nsILocalFile);
334     if (!fl.exists()) continue;
335     if (!fl.isFile() || !fl.isReadable()) continue;
336     // check file name
337     let name = fl.leafName;
338     if (name[0] == "_") continue; // disabled
339     let ne = name.match(nameExtRE);
340     if (!ne || ne[2] != ".js") continue; // invalid extension
341     // create script nfo object
342     if (addonOptions.debugCache) conlog("trying ujs '", name, "'");
343     let nfo = {};
344     nfo.isPackage = false;
345     nfo.file = fl.clone();
346     nfo.name = ne[1]; // base name (w/o extension)
347     nfo.lmtime = fl.lastModifiedTime;
348     if (!processScriptMeta(nfo)) continue; // skip this script
349     if (addonOptions.debugCache) conlog("adding ujs '", name, "'");
350     //dumpNfo(nfo);
351     userscripts.push(nfo);
352   }
353   // sort scripts
354   userscripts.sort(function (a, b) {
355     let ap = a.file.leafName;
356     let bp = b.file.leafName;
357     if (ap < bp) return -1;
358     if (ap > bp) return 1;
359     return 0;
360   });
364 ////////////////////////////////////////////////////////////////////////////////
365 function scanPackages () {
366   pkgscripts = new Array(); // sorted by package name
367   for (let pi of pkgDB.getActivePackagesForCache()) {
368     if (addonOptions.debugCache) conlog("*pkg: pi.path=[", pi.path, "]");
369     let fl = newIFile(pi.path);
370     if (!fl.exists()) continue;
371     if (!fl.isFile() || !fl.isReadable()) continue;
372     let name = fl.leafName;
373     //if (name[0] == "_") continue; // disabled
374     let ne = name.match(nameExtRE);
375     if (!ne || ne[2] != ".js") continue; // invalid extension
376     // create script nfo object
377     if (addonOptions.debugCache) conlog("trying pkg '", fl.path, "'");
378     let nfo = {};
379     nfo.isPackage = true;
380     nfo.file = fl.clone();
381     nfo.name = ne[1]; // base name (w/o extension)
382     nfo.lmtime = fl.lastModifiedTime;
383     if (!processScriptMeta(nfo)) continue; // skip this script
384     if (addonOptions.debugCache) conlog("added pkg '", fl.path, "'");
385     //conlog("path: ", pi.path);
386     // add includes
387     nfo.incs = new Array();
388     for (let ifn of pi.reqs) {
389       //conlog("  req: [", ifn, "]");
390       if (addonOptions.debugCache) conlog("  ifn=[", ifn, "]");
391       let ifl = newIFile(ifn);
392       if (!ifl.exists()) continue;
393       if (!ifl.isFile() || !ifl.isReadable()) continue;
394       let mt = ifl.leafName.match(nameExtRE);
395       if (!mt || mt[2] != ".js") continue; // invalid extension
396       nfo.incs.push(ifl);
397     }
398     // add resources
399     nfo.resources = {};
400     for (let [name, rsrc] in Iterator(pi.rsrc)) {
401       if (!name) continue; // just in case
402       //conlog("  res: [", name, "] is [", rsrc.path, "] : [", rsrc.contenttype, "]");
403       if (addonOptions.debugCache) conlog("  rsrc.path=[", rsrc.path, "]");
404       let rfl = newIFile(rsrc.path);
405       if (!rfl.exists()) continue;
406       if (!rfl.isFile() || !rfl.isReadable()) continue;
407       nfo.resources[name] = {file:rfl, contenttype:rsrc.contenttype};
408     }
409     //dumpNfo(nfo);
410     pkgscripts.push(nfo);
411   }
412   //conlog("=== pkgscripts ==="); for (let nfo of pkgscripts) dumpNfo(nfo);
416 ////////////////////////////////////////////////////////////////////////////////
417 exports.reset = function () {
418   if (addonOptions.debugCache) conlog("resetting script cache");
419   ujsdirLMT = false;
420   userscripts = new Array(); // sorted by name
421   pkgscripts = new Array(); // sorted by package name
425 ////////////////////////////////////////////////////////////////////////////////
426 exports.dumpCache = function () {
427   conlog("=== userscripts ==="); for (let nfo of userscripts) dumpNfo(nfo);
428   conlog("=== pkgscripts ==="); for (let nfo of pkgscripts) dumpNfo(nfo);
432 ////////////////////////////////////////////////////////////////////////////////
433 // bool forced: do force scan (i.e. ignore jsdirLMT value)
434 function refreshCache (forced) {
435   if (ujsdirLMT === false) forced = true;
436   scanUJSDirectory(forced);
437   if (forced) scanPackages();
439 exports.refreshCache = refreshCache;
442 ////////////////////////////////////////////////////////////////////////////////
443 exports.isGoodScheme = function (scstr) {
444   let cpos = scstr.indexOf(":");
445   if (cpos < 0) return false;
446   scstr = scstr.substr(0, cpos);
447   switch (scstr) {
448     case "http":
449     case "https":
450     case "ftp": // no, really, why?
451     case "data": // inject into frames and other smart things created with "data:" URL
452     case "file":
453       return true;
454     default:
455       return false;
456   }
460 ////////////////////////////////////////////////////////////////////////////////
461 function isGoodForUrl (nfo, url, inFrame, runat) {
462   if (!nfo) return false;
463   if (inFrame && nfo.noframes) return false;
464   if (runat !== nfo.runat) return false;
465   // check excludes
466   if (nfo.excREs.length) {
467     let hit = false;
468     for (let re of nfo.excREs) if (re.test(url)) { hit = true; break; }
469     if (hit) return false;
470   }
471   // check includes
472   if (nfo.incREs.length === 0) return true; // "@include *"
473   if (nfo.incREs.length) {
474     let hit = false;
475     for (let re of nfo.incREs) if (re.test(url)) { hit = true; break; }
476     if (hit) return true;
477   }
478   // check matches
479   if (nfo.incMatches.length) {
480     let hit = false;
481     for (let mt of nfo.incMatches) if (mt.doMatch(url)) { hit = true; break; }
482     if (hit) return true;
483   }
484   return false;
488 // string uri
489 // returns array of nfos or false
490 // url: string
491 // runat: string
492 exports.scriptsForUrl = function (url, inFrame, runat) {
493   let res = new Array();
494   inFrame = !!inFrame;
495   refreshCache();
496   // userscripts
497   for (let nfo of userscripts) {
498     if (!isGoodForUrl(nfo, url, inFrame, runat)) continue;
499     res.push(nfo);
500   }
501   // package scripts
502   for (let nfo of pkgscripts) {
503     if (!isGoodForUrl(nfo, url, inFrame, runat)) continue;
504     res.push(nfo);
505   }
506   return res;
510 ////////////////////////////////////////////////////////////////////////////////
511 // get script list; returns array of xnfo
512 // xnfo:
513 //  string path
514 //  string name
515 //  bool islib;
516 (function (global) {
517   let list = new Array();
519   let jdLMT = false;
520   let ldLMT = false;
521   let lastModCount = 0;
523   function scanDir (dir, islib) {
524     if (!dir.exists()) return;
525     if (!dir.isDirectory()) return;
526     let en = dir.directoryEntries;
527     while (en.hasMoreElements()) {
528       let fl = en.getNext().QueryInterface(Ci.nsILocalFile);
529       if (!fl.exists()) continue;
530       if (!fl.isFile() || !fl.isReadable()) continue;
531       let ne = fl.leafName.match(nameExtRE);
532       if (!ne || ne[2] != ".js") continue;
533       let xnfo = {};
534       xnfo.path = fl.path;
535       xnfo.name = (islib ? "libs/" : "")+fl.leafName;
536       xnfo.islib = islib;
537       //conlog("getScriptsForEdit: '", xnfo.name, "' [", xnfo.path, "]");
538       list.push(xnfo);
539     }
540   }
542   global.getScriptsForEdit = function () {
543     let jdir = getUserJSDir();
544     let ldir = getUserLibDir();
546     if (jdLMT !== jdir.lastModifiedTime || ldLMT !== ldir.lastModifiedTime) {
547       ++lastModCount;
548       jdLMT = jdir.lastModifiedTime;
549       ldLMT = ldir.lastModifiedTime;
550       list = new Array();
551       scanDir(jdir, false);
552       scanDir(ldir, true);
553     }
555     return {lmod: lastModCount, list:list};
556   };
557 })(this);
558 exports.getScriptsForEdit = getScriptsForEdit;