cosmetix
[guerillascript.git] / main / modules / scriptcache.js
blobd53a00604f86c1613faf60b7dcd70e22afbedadc
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 {parseMeta} = require("utils/metaparser");
32 ////////////////////////////////////////////////////////////////////////////////
33 const nameExtRE = /^(.+)(\.[^.]+)$/;
36 ////////////////////////////////////////////////////////////////////////////////
37 /* nfo:
38 * nsIFile file: script file object
39 * string name: script name, without ".js" ("abcd")
40 * number lmtime: lastmod time
41 * RegExp[] incREs: regular expressions for "@include"; empty array means "*"
42 * RegExp[] excREs: regular expressions for "@exclude"
43 * bool unwrapped
44 * bool noframes
45 * bool wantlog: want `conlog()` and `GM_log()` APIs?
46 * string runat: run at "document-start", "document-end"
47 * nsIFile[] libs: list of required libraries
48 * nsIFile[] incs: list of required includes
49 * {string name} resources: resources dict, keyed by resource name, value is {nsIFile file, string contenttype}
50 * bool isPackage
51 * bool isUserJS
53 function dumpNfo (nfo) {
54 conlog(" ========================================");
55 conlog(" file: [", nfo.file.path, "]");
56 conlog(" name: [", nfo.name, "]");
57 conlog(" lmtime: [", nfo.lmtime, "]");
58 conlog(" isPackage: [", nfo.isPackage, "]");
59 conlog(" isUserJS: [", nfo.isUserJS, "]");
60 conlog(" unwrapped: [", nfo.unwrapped, "]");
61 conlog(" noframes: [", nfo.noframes, "]");
62 conlog(" wantlog: [", nfo.wantlog, "]");
63 conlog(" runat: [", nfo.runat, "]");
64 if (nfo.incREs.length) {
65 conlog(" include:"); for (let re of nfo.incREs) conlog(" ", re);
66 } else {
67 conlog(" include: *");
69 if (nfo.excREs.length) { conlog(" exclude:"); for (let re of nfo.excREs) conlog(" ", re); }
70 if (nfo.libs.length) { conlog(" libs:"); for (let fl of nfo.libs) conlog(" [", fl.path, "]"); }
71 if (nfo.incs.length) { conlog(" incs:"); for (let fl of nfo.incs) conlog(" [", fl.path, "]"); }
72 let wasOut = false;;
73 for (let [name, fl] in Iterator(nfo.resources)) {
74 if (!wasOut) conlog(" resources:");
75 wasOut = true;
76 conlog(" [", name, "]=[", fl.path, "]");
79 exports.dumpScriptNfo = dumpNfo;
82 ////////////////////////////////////////////////////////////////////////////////
83 let ujsdirLMT = false; // directory modify times; will become number
84 let userscripts = new Array(); // sorted by name
85 let pkgscripts = new Array(); // sorted by package name
88 ////////////////////////////////////////////////////////////////////////////////
89 // return `false` if this script should be excluded
90 function processScriptMeta (nfo, reqtformCB) {
91 function addIncLib (dir, fname, arr) {
92 if (addonOptions.debugCache) conlog("lib: [", dir.path, "] : [", fname, "]");
93 let ne = fname.match(nameExtRE);
94 if (!ne || ne[2] != ".js") return true; // invalid file name, skip it
95 let fl = buildRelFile(dir, fname);
96 if (!dir.contains(fl, true)) return false; // access restricted
97 if (!fl.exists()) return false;
98 if (!fl.isFile() || !fl.isReadable()) return false;
99 // check for duplicates
100 let found = false;
101 for (let ef of arr) if (ef.equals(fl)) { found = true; break; }
102 if (!found) arr.push(fl);
103 return true;
106 // return `false` if invalid or non-existing file accessed
107 function procJSIncLib (dir, value, arr) {
108 if (addonOptions.debugCache) conlog("libs: [", value, "] from [", dir.path, "]");
109 for (let fname of value.split(/\s+/)) if (fname && !addIncLib(dir, fname, arr)) return false;
110 return true;
113 try {
114 nfo.isUserJS = nfo.isPackage || (/\.user\.js$/.test(nfo.file.leafName)); // is ".user.js" script?
115 nfo.incREs = new Array();
116 nfo.excREs = new Array();
117 nfo.unwrapped = null;
118 nfo.noframes = (nfo.isUserJS ? false : true); // default
119 nfo.wantlog = null;
120 nfo.runat = "document-end"; // default
121 nfo.libs = new Array();
122 nfo.incs = new Array();
123 nfo.resources = {};
124 let text = fileReadText(nfo.file);
125 if (!text) return false;
126 let meta = parseMeta(text);
127 if (meta.length == 0) return false;
128 //conlog("got meta for ", nfo.file.path);
129 // parse meta fields
130 let incAll = false;
131 let wasGrantNone = false;
132 let wasGrantLog = false;
133 let wasGrantAnything = false;
134 for (let kv of meta) {
135 if (kv.name === "disabled") {
136 // this script is disabled
137 if (addonOptions.debugCache) conlog("@disabled");
138 return false;
140 if (kv.name === "exclude") {
141 if (!kv.value) continue;
142 if (kv.value === "*") {
143 // excluded for all uris
144 if (addonOptions.debugCache) conlog("@exclude *");
145 return false;
147 let re = uri2re(kv.value);
148 if (re) nfo.excREs.push(re);
149 continue;
151 if (kv.name === "include") {
152 if (!incAll) {
153 if (!kv.value) continue;
154 if (kv.value === "*") {
155 incAll = true;
156 if (nfo.incREs.length != 0) nfo.incREs = new Array();
157 } else {
158 let re = uri2re(kv.value);
159 if (re) nfo.incREs.push(re);
162 continue;
164 if (kv.name === "run-at") {
165 switch (kv.value) {
166 case "document-start":
167 case "document-end":
168 nfo.runat = kv.value;
169 break;
171 continue;
173 if (kv.name === "unwrap" || kv.name === "unwrapped") {
174 nfo.unwrapped = true;
175 continue;
177 if (kv.name === "wrap" || kv.name === "wrapped") {
178 nfo.unwrapped = false;
179 continue;
181 if (kv.name === "noframe" || kv.name === "noframes") {
182 nfo.noframes = false;
183 continue;
185 if (kv.name === "frame" || kv.name === "frames") {
186 nfo.noframes = true;
187 continue;
189 if (kv.name === "library" || kv.name === "libraries") {
190 if (nfo.isPackage) continue;
191 if (!procJSIncLib(getUserLibDir(), kv.value, nfo.libs)) {
192 if (addonOptions.debugCache) conlog("bad @libraries");
193 return false;
195 continue;
197 if (kv.name === "require" || kv.name === "requires") {
198 if (nfo.isPackage) continue;
199 let dir = nfo.file.parent; // remove script name
200 dir.append(nfo.name);
201 if (nfo.isPackage) {
202 // special processing for scripts from packages
203 if (kv.name !== "require" || !kv.value) continue;
204 if (typeof(reqtformCB) !== "function") continue;
205 let v = reqtformCB(nfo, kv.value);
206 if (typeof(v) === "undefined") continue;
207 if (v === false) return false; // alas
208 if (!v || typeof(v) !== "string") continue;
209 if (!addIncLib(dir, v, nfo.incs)) return false;
210 } else {
211 if (!procJSIncLib(dir, kv.value, nfo.incs)) {
212 if (addonOptions.debugCache) conlog("bad @requires");
213 return false;
216 continue;
218 if (kv.name === "grant") {
219 // "@grant none" means the same as "@unwrap"
220 switch (kv.value) {
221 case "none": wasGrantNone = true; break;
222 case "GM_log": wasGrantLog = true; break;
223 case "unsafeWindow": break;
224 default: if (kv.value) wasGrantAnything = true; break;
226 continue;
229 // met any includes?
230 if (!incAll && nfo.incREs.length == 0) {
231 // no includes at all
232 if (addonOptions.debugCache) conlog("no @include");
233 return false;
235 // fix access rights
236 if (wasGrantLog) {
237 nfo.wantlog = true;
238 } else {
239 if (nfo.wantlog === null) nfo.wantlog = (!nfo.isPackage && !nfo.isUserJS);
241 // if we met "@grant none", this means "unwrap"
242 if (wasGrantNone) {
243 if (nfo.unwrapped === null) nfo.unwrapped = true;
244 } else if (wasGrantAnything) {
245 // other "@grant"s means "wrapped"
246 if (nfo.unwrapped === null) nfo.unwrapped = false;
247 } else {
248 // nothing was specified
249 if (nfo.unwrapped === null) nfo.unwrapped = false;
251 // public `conlog` function for unwrapped scripts
252 if (nfo.unwrapped) nfo.wantlog = true;
253 return true;
254 } catch (e) {
255 logException("processScriptMeta", e);
257 return false;
259 exports.processScriptMeta = processScriptMeta;
262 ////////////////////////////////////////////////////////////////////////////////
263 // bool forced: do force scan (i.e. ignore jsdirLMT value)
264 function scanUJSDirectory (forced) {
265 //if (addonOptions.debugCache) conlog("updating cache: '"+dir.path+"'");
266 let dir = getUserJSDir();
267 if (!dir.exists()) { userscripts = new Array(); return; }
268 if (!dir.isDirectory()) { userscripts = new Array(); return; }
269 let lmtime = dir.lastModifiedTime;
270 if (!forced && lmtime === ujsdirLMT) return; // do nothing, as nothing was changed
271 // rescan
272 ujsdirLMT = lmtime;
273 userscripts = new Array();
274 let en = dir.directoryEntries;
275 while (en.hasMoreElements()) {
276 let fl = en.getNext().QueryInterface(Ci.nsILocalFile);
277 if (!fl.exists()) continue;
278 if (!fl.isFile() || !fl.isReadable()) continue;
279 // check file name
280 let name = fl.leafName;
281 if (name[0] == "_") continue; // disabled
282 let ne = name.match(nameExtRE);
283 if (!ne || ne[2] != ".js") continue; // invalid extension
284 // create script nfo object
285 if (addonOptions.debugCache) conlog("trying ujs '", name, "'");
286 let nfo = {};
287 nfo.isPackage = false;
288 nfo.file = fl.clone();
289 nfo.name = ne[1]; // base name (w/o extension)
290 nfo.lmtime = fl.lastModifiedTime;
291 if (!processScriptMeta(nfo)) continue; // skip this script
292 if (addonOptions.debugCache) conlog("adding ujs '", name, "'");
293 //dumpNfo(nfo);
294 userscripts.push(nfo);
296 // sort scripts
297 userscripts.sort(function (a, b) {
298 let ap = a.file.leafName;
299 let bp = b.file.leafName;
300 if (ap < bp) return -1;
301 if (ap > bp) return 1;
302 return 0;
307 ////////////////////////////////////////////////////////////////////////////////
308 function scanPackages () {
309 pkgscripts = new Array(); // sorted by package name
310 for (let pi of pkgDB.getActivePackagesForCache()) {
311 let fl = newIFile(pi.path);
312 if (!fl.exists()) continue;
313 if (!fl.isFile() || !fl.isReadable()) continue;
314 let name = fl.leafName;
315 //if (name[0] == "_") continue; // disabled
316 let ne = name.match(nameExtRE);
317 if (!ne || ne[2] != ".js") continue; // invalid extension
318 // create script nfo object
319 if (addonOptions.debugCache) conlog("trying pkg '", fl.path, "'");
320 let nfo = {};
321 nfo.isPackage = true;
322 nfo.file = fl.clone();
323 nfo.name = ne[1]; // base name (w/o extension)
324 nfo.lmtime = fl.lastModifiedTime;
325 if (!processScriptMeta(nfo)) continue; // skip this script
326 if (addonOptions.debugCache) conlog("added pkg '", fl.path, "'");
327 //conlog("path: ", pi.path);
328 // add includes
329 nfo.incs = new Array();
330 for (let ifn of pi.reqs) {
331 //conlog(" req: [", ifn, "]");
332 let ifl = newIFile(ifn);
333 if (!ifl.exists()) continue;
334 if (!ifl.isFile() || !ifl.isReadable()) continue;
335 let mt = ifl.leafName.match(nameExtRE);
336 if (!mt || mt[2] != ".js") continue; // invalid extension
337 nfo.incs.push(ifl);
339 // add resources
340 nfo.resources = {};
341 for (let [name, rsrc] in Iterator(pi.rsrc)) {
342 if (!name) continue; // just in case
343 //conlog(" res: [", name, "] is [", rsrc.path, "] : [", rsrc.contenttype, "]");
344 let rfl = newIFile(rsrc.path);
345 if (!rfl.exists()) continue;
346 if (!rfl.isFile() || !rfl.isReadable()) continue;
347 nfo.resources[name] = {file:rfl, contenttype:rsrc.contenttype};
349 //dumpNfo(nfo);
350 pkgscripts.push(nfo);
352 //conlog("=== pkgscripts ==="); for (let nfo of pkgscripts) dumpNfo(nfo);
356 ////////////////////////////////////////////////////////////////////////////////
357 exports.reset = function () {
358 if (addonOptions.debugCache) conlog("resetting script cache");
359 ujsdirLMT = false;
360 userscripts = new Array(); // sorted by name
361 pkgscripts = new Array(); // sorted by package name
365 ////////////////////////////////////////////////////////////////////////////////
366 exports.dumpCache = function () {
367 conlog("=== userscripts ==="); for (let nfo of userscripts) dumpNfo(nfo);
368 conlog("=== pkgscripts ==="); for (let nfo of pkgscripts) dumpNfo(nfo);
372 ////////////////////////////////////////////////////////////////////////////////
373 // bool forced: do force scan (i.e. ignore jsdirLMT value)
374 function refreshCache (forced) {
375 if (ujsdirLMT === false) forced = true;
376 scanUJSDirectory(forced);
377 if (forced) scanPackages();
379 exports.refreshCache = refreshCache;
382 ////////////////////////////////////////////////////////////////////////////////
383 exports.isGoodScheme = function (scstr) {
384 let cpos = scstr.indexOf(":");
385 if (cpos < 0) return false;
386 scstr = scstr.substr(0, cpos);
387 switch (scstr) {
388 case "http":
389 case "https":
390 //case "ftp": // no, really, why?
391 return true;
392 default:
393 return false;
398 ////////////////////////////////////////////////////////////////////////////////
399 function isGoodForUrl (nfo, url, inFrame, runat) {
400 if (!nfo) return false;
401 if (inFrame && nfo.noframes) return false;
402 if (runat !== nfo.runat) return false;
403 // check includes
404 if (nfo.incREs.length) {
405 let ok = false;
406 for (let re of nfo.incREs) if (re.test(url)) { ok = true; break; }
407 if (!ok) return false;
409 // url matches, check excludes
410 if (nfo.excREs.length) {
411 let ok = false;
412 for (let re of nfo.excREs) if (re.test(url)) { ok = true; break; }
413 if (ok) return false;
415 return true;
419 // string uri
420 // returns array of nfos or false
421 // url: string
422 // runat: string
423 exports.scriptsForUrl = function (url, inFrame, runat) {
424 let res = new Array();
425 inFrame = !!inFrame;
426 refreshCache();
427 // userscripts
428 for (let nfo of userscripts) {
429 if (!isGoodForUrl(nfo, url, inFrame, runat)) continue;
430 res.push(nfo);
432 // package scripts
433 for (let nfo of pkgscripts) {
434 if (!isGoodForUrl(nfo, url, inFrame, runat)) continue;
435 res.push(nfo);
437 return res;
441 ////////////////////////////////////////////////////////////////////////////////
442 // get script list; returns array of xnfo
443 // xnfo:
444 // string path
445 // string name
446 // bool islib;
447 (function (global) {
448 let list = new Array();
450 let jdLMT = false;
451 let ldLMT = false;
452 let lastModCount = 0;
454 function scanDir (dir, islib) {
455 if (!dir.exists()) return;
456 if (!dir.isDirectory()) return;
457 let en = dir.directoryEntries;
458 while (en.hasMoreElements()) {
459 let fl = en.getNext().QueryInterface(Ci.nsILocalFile);
460 if (!fl.exists()) continue;
461 if (!fl.isFile() || !fl.isReadable()) continue;
462 let ne = fl.leafName.match(nameExtRE);
463 if (!ne || ne[2] != ".js") continue;
464 let xnfo = {};
465 xnfo.path = fl.path;
466 xnfo.name = (islib ? "libs/" : "")+fl.leafName;
467 xnfo.islib = islib;
468 //conlog("getScriptsForEdit: '", xnfo.name, "' [", xnfo.path, "]");
469 list.push(xnfo);
473 global.getScriptsForEdit = function () {
474 let jdir = getUserJSDir();
475 let ldir = getUserLibDir();
477 if (jdLMT !== jdir.lastModifiedTime || ldLMT !== ldir.lastModifiedTime) {
478 ++lastModCount;
479 jdLMT = jdir.lastModifiedTime;
480 ldLMT = ldir.lastModifiedTime;
481 list = new Array();
482 scanDir(jdir, false);
483 scanDir(ldir, true);
486 return {lmod: lastModCount, list:list};
488 })(this);
489 exports.getScriptsForEdit = getScriptsForEdit;