we shouldn't init guerilla subsystems before prefs setup
[guerillascript.git] / modules / scriptcache.js
blob94ec57556c6097db3112adb0ec4af94adbe521a9
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
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, "]");
81 ////////////////////////////////////////////////////////////////////////////////
82 let ujsdirLMT = false; // directory modify times; will become number
83 let userscripts = new Array(); // sorted by name
84 let pkgscripts = new Array(); // sorted by package name
87 ////////////////////////////////////////////////////////////////////////////////
88 // return `false` if this script should be excluded
89 function processScriptMeta (nfo, reqtformCB) {
90 function addIncLib (dir, fname, arr) {
91 //conlog("lib: [", dir.path, "] : [", fname, "]");
92 let ne = fname.match(nameExtRE);
93 if (!ne || ne[2] != ".js") return true; // invalid file name, skip it
94 let fl = buildRelFile(dir, fname);
95 if (!dir.contains(fl, true)) return false; // access restricted
96 if (!fl.exists()) return false;
97 if (!fl.isFile() || !fl.isReadable()) return false;
98 // check for duplicates
99 let found = false;
100 for (let ef of arr) if (ef.equals(fl)) { found = true; break; }
101 if (!found) arr.push(fl);
102 return true;
105 // return `false` if invalid or non-existing file accessed
106 function procJSIncLib (dir, value, arr) {
107 //conlog("libs: [", value, "]");
108 for (let fname of value.split(/\s+/)) if (fname && !addIncLib(dir, fname, arr)) return false;
109 return true;
112 try {
113 nfo.isUserJS = nfo.isPackage || (/\.user\.js$/.test(nfo.file.leafName)); // is ".user.js" script?
114 nfo.incREs = new Array();
115 nfo.excREs = new Array();
116 nfo.unwrapped = null;
117 nfo.noframes = (nfo.isUserJS ? false : true); // default
118 nfo.wantlog = null;
119 nfo.runat = "document-end"; // default
120 nfo.libs = new Array();
121 nfo.incs = new Array();
122 nfo.resources = {};
123 let text = fileReadText(nfo.file);
124 if (!text) return false;
125 let meta = parseMeta(text);
126 if (meta.length == 0) return false;
127 //conlog("got meta for ", nfo.file.path);
128 // parse meta fields
129 let incAll = false;
130 let wasGrantNone = false;
131 let wasGrantLog = false;
132 let wasGrantAnything = false;
133 for (let kv of meta) {
134 if (kv.name === "disabled") return false; // this script is disabled
135 if (kv.name === "exclude") {
136 if (!kv.value) continue;
137 if (kv.value === "*") return false; // excluded for all uris
138 let re = uri2re(kv.value);
139 if (re) nfo.excREs.push(re);
140 continue;
142 if (kv.name === "include") {
143 if (!incAll) {
144 if (!kv.value) continue;
145 if (kv.value === "*") {
146 incAll = true;
147 if (nfo.incREs.length != 0) nfo.incREs = new Array();
148 } else {
149 let re = uri2re(kv.value);
150 if (re) nfo.incREs.push(re);
153 continue;
155 if (kv.name === "run-at") {
156 switch (kv.value) {
157 case "document-start":
158 case "document-end":
159 nfo.runat = kv.value;
160 break;
162 continue;
164 if (kv.name === "unwrap" || kv.name === "unwrapped") {
165 nfo.unwrapped = true;
166 continue;
168 if (kv.name === "wrap" || kv.name === "wrapped") {
169 nfo.unwrapped = false;
170 continue;
172 if (kv.name === "noframe" || kv.name === "noframes") {
173 nfo.noframes = false;
174 continue;
176 if (kv.name === "frame" || kv.name === "frames") {
177 nfo.noframes = true;
178 continue;
180 if (kv.name === "library" || kv.name === "libraries") {
181 if (nfo.isPackage) continue;
182 if (!procJSIncLib(getUserLibDir(), kv.value, nfo.libs)) return false;
183 continue;
185 if (kv.name === "require" || kv.name === "requires") {
186 if (nfo.isPackage) continue;
187 let dir = nfo.file.parent; // remove script name
188 dir.append(nfo.name);
189 if (nfo.isPackage) {
190 // special processing for scripts from packages
191 if (kv.name !== "require" || !kv.value) continue;
192 if (typeof(reqtformCB) !== "function") continue;
193 let v = reqtformCB(nfo, kv.value);
194 if (typeof(v) === "undefined") continue;
195 if (v === false) return false; // alas
196 if (!v || typeof(v) !== "string") continue;
197 if (!addIncLib(dir, v, nfo.incs)) return false;
198 } else {
199 if (!procJSIncLib(dir, kv.value, nfo.incs)) return false;
201 continue;
203 if (kv.name === "grant") {
204 // "@grant none" means the same as "@unwrap"
205 switch (kv.value) {
206 case "none": wasGrantNone = true; break;
207 case "GM_log": wasGrantLog = true; break;
208 case "unsafeWindow": break;
209 default: if (kv.value) wasGrantAnything = true; break;
211 continue;
214 // met any includes?
215 if (!incAll && nfo.incREs.length == 0) return false; // no includes at all
216 // fix access rights
217 if (wasGrantLog) {
218 nfo.wantlog = true;
219 } else {
220 if (nfo.wantlog === null) nfo.wantlog = (!nfo.isPackage && !nfo.isUserJS);
222 // if we met "@grant none", this means "unwrap"
223 if (wasGrantNone) {
224 if (nfo.unwrapped === null) nfo.unwrapped = true;
225 } else if (wasGrantAnything) {
226 // other "@grant"s means "wrapped"
227 if (nfo.unwrapped === null) nfo.unwrapped = false;
228 } else {
229 // nothing was specified
230 if (nfo.unwrapped === null) nfo.unwrapped = false;
232 // public `conlog` function for unwrapped scripts
233 if (nfo.unwrapped) nfo.wantlog = true;
234 return true;
235 } catch (e) {
236 logException("processScriptMeta", e);
238 return false;
240 exports.processScriptMeta = processScriptMeta;
243 ////////////////////////////////////////////////////////////////////////////////
244 // bool forced: do force scan (i.e. ignore jsdirLMT value)
245 function scanUJSDirectory (forced) {
246 //if (guerillaOptions.debugCache) conlog("updating cache: '"+dir.path+"'");
247 let dir = getUserJSDir();
248 if (!dir.exists()) { userscripts = new Array(); return; }
249 if (!dir.isDirectory()) { userscripts = new Array(); return; }
250 let lmtime = dir.lastModifiedTime;
251 if (!forced && lmtime === ujsdirLMT) return; // do nothing, as nothing was changed
252 // rescan
253 ujsdirLMT = lmtime;
254 userscripts = new Array();
255 let en = dir.directoryEntries;
256 while (en.hasMoreElements()) {
257 let fl = en.getNext().QueryInterface(Ci.nsILocalFile);
258 if (!fl.exists()) continue;
259 if (!fl.isFile() || !fl.isReadable()) continue;
260 // check file name
261 let name = fl.leafName;
262 if (name[0] == "_") continue; // disabled
263 let ne = name.match(nameExtRE);
264 if (!ne || ne[2] != ".js") continue; // invalid extension
265 // create script nfo object
266 let nfo = {};
267 nfo.isPackage = false;
268 nfo.file = fl.clone();
269 nfo.name = ne[1]; // base name (w/o extension)
270 nfo.lmtime = fl.lastModifiedTime;
271 if (!processScriptMeta(nfo)) continue; // skip this script
272 //dumpNfo(nfo);
273 userscripts.push(nfo);
275 // sort scripts
276 userscripts.sort(function (a, b) {
277 let ap = a.file.leafName;
278 let bp = b.file.leafName;
279 if (ap < bp) return -1;
280 if (ap > bp) return 1;
281 return 0;
286 ////////////////////////////////////////////////////////////////////////////////
287 function scanPackages () {
288 pkgscripts = new Array(); // sorted by package name
289 for (let pi of pkgDB.getActivePackagesForCache()) {
290 let fl = newIFile(pi.path);
291 if (!fl.exists()) continue;
292 if (!fl.isFile() || !fl.isReadable()) continue;
293 let name = fl.leafName;
294 //if (name[0] == "_") continue; // disabled
295 let ne = name.match(nameExtRE);
296 if (!ne || ne[2] != ".js") continue; // invalid extension
297 // create script nfo object
298 let nfo = {};
299 nfo.isPackage = true;
300 nfo.file = fl.clone();
301 nfo.name = ne[1]; // base name (w/o extension)
302 nfo.lmtime = fl.lastModifiedTime;
303 if (!processScriptMeta(nfo)) continue; // skip this script
304 //conlog("path: ", pi.path);
305 // add includes
306 nfo.incs = new Array();
307 for (let ifn of pi.reqs) {
308 let ifl = newIFile(ifn);
309 if (!ifl.exists()) continue;
310 if (!ifl.isFile() || !ifl.isReadable()) continue;
311 let mt = name.match(ifl.leafName);
312 if (!mt || mt[2] != ".js") continue; // invalid extension
313 nfo.incs.push(ifl);
315 // add resources
316 nfo.resources = {};
317 for (let [name, path] in Iterator(pi.rsrc)) {
318 if (!name) continue; // just in case
319 let rfl = newIFile(path);
320 if (!rfl.exists()) continue;
321 if (!rfl.isFile() || !rfl.isReadable()) continue;
322 nfo.resources[name] = rfl;
324 //dumpNfo(nfo);
325 pkgscripts.push(nfo);
330 ////////////////////////////////////////////////////////////////////////////////
331 exports.reset = function () {
332 if (guerillaOptions.debugCache) conlog("resetting script cache");
333 ujsdirLMT = false;
334 userscripts = new Array(); // sorted by name
335 pkgscripts = new Array(); // sorted by package name
339 ////////////////////////////////////////////////////////////////////////////////
340 // bool forced: do force scan (i.e. ignore jsdirLMT value)
341 function refreshCache (forced) {
342 if (ujsdirLMT === false) forced = true;
343 scanUJSDirectory(forced);
344 if (forced) scanPackages();
346 exports.refreshCache = refreshCache;
349 ////////////////////////////////////////////////////////////////////////////////
350 exports.dumpCache = function () {
351 conlog("=== userscripts ==="); for (let nfo of userscripts) dumpNfo(nfo);
352 conlog("=== pkgscripts ==="); for (let nfo of pkgscripts) dumpNfo(nfo);
356 ////////////////////////////////////////////////////////////////////////////////
357 exports.isGoodScheme = function (scstr) {
358 let cpos = scstr.indexOf(":");
359 if (cpos < 0) return false;
360 scstr = scstr.substr(0, cpos);
361 switch (scstr) {
362 case "http":
363 case "https":
364 //case "ftp": // no, really, why?
365 return true;
366 default:
367 return false;
372 ////////////////////////////////////////////////////////////////////////////////
373 function isGoodForUrl (nfo, url, inFrame, runat) {
374 if (!nfo) return false;
375 if (inFrame && nfo.noframes) return false;
376 if (runat !== nfo.runat) return false;
377 // check includes
378 if (nfo.incREs.length) {
379 let ok = false;
380 for (let re of nfo.incREs) if (re.test(url)) { ok = true; break; }
381 if (!ok) return false;
383 // url matches, check excludes
384 if (nfo.excREs.length) {
385 let ok = false;
386 for (let re of nfo.excREs) if (re.test(url)) { ok = true; break; }
387 if (ok) return false;
389 return true;
393 // string uri
394 // returns array of nfos or false
395 // url: string
396 // runat: string
397 exports.scriptsForUrl = function (url, inFrame, runat) {
398 let res = new Array();
399 inFrame = !!inFrame;
400 refreshCache();
401 // userscripts
402 for (let nfo of userscripts) {
403 if (!isGoodForUrl(nfo, url, inFrame, runat)) continue;
404 res.push(nfo);
406 // package scripts
407 for (let nfo of pkgscripts) {
408 if (!isGoodForUrl(nfo, url, inFrame, runat)) continue;
409 res.push(nfo);
411 return res;
415 ////////////////////////////////////////////////////////////////////////////////
416 // get script list; returns array of xnfo
417 // xnfo:
418 // string path
419 // string name
420 // bool islib;
421 (function (global) {
422 let list = new Array();
424 let jdLMT = false;
425 let ldLMT = false;
426 let lastModCount = 0;
428 function scanDir (dir, islib) {
429 if (!dir.exists()) return;
430 if (!dir.isDirectory()) return;
431 let en = dir.directoryEntries;
432 while (en.hasMoreElements()) {
433 let fl = en.getNext().QueryInterface(Ci.nsILocalFile);
434 if (!fl.exists()) continue;
435 if (!fl.isFile() || !fl.isReadable()) continue;
436 let ne = fl.leafName.match(nameExtRE);
437 if (!ne || ne[2] != ".js") continue;
438 let xnfo = {};
439 xnfo.path = fl.path;
440 xnfo.name = (islib ? "libs/" : "")+fl.leafName;
441 xnfo.islib = islib;
442 //conlog("getScriptsForEdit: '", xnfo.name, "' [", xnfo.path, "]");
443 list.push(xnfo);
447 global.getScriptsForEdit = function () {
448 let jdir = getUserJSDir();
449 let ldir = getUserLibDir();
451 if (jdLMT !== jdir.lastModifiedTime || ldLMT !== ldir.lastModifiedTime) {
452 ++lastModCount;
453 jdLMT = jdir.lastModifiedTime;
454 ldLMT = ldir.lastModifiedTime;
455 list = new Array();
456 scanDir(jdir, false);
457 scanDir(ldir, true);
460 return {lmod: lastModCount, list:list};
462 })(this);
463 exports.getScriptsForEdit = getScriptsForEdit;