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
25 * The above copyright notice and this permission notice shall be included in all
26 * copies or substantial portions of the Software.
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);
40 exports.newIFile = newIFile;
43 ////////////////////////////////////////////////////////////////////////////////
44 function buildRelFile (fl, path) {
48 for (let n of path.split("/")) {
49 if (!n || n == ".") continue;
51 if (--depthCount < 0) return null;
53 if (!res) return null;
61 exports.buildRelFile = buildRelFile;
64 ////////////////////////////////////////////////////////////////////////////////
65 let nameExtRE = /^(.+)(\.[^.]+)$/;
68 ////////////////////////////////////////////////////////////////////////////////
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"
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}
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);
100 conlog(" include: *");
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, "]"); }
106 for (let [name, fl] in Iterator(nfo.resources)) {
107 if (!wasOut) conlog(" resources:");
109 conlog(" [", name, "]=[", fl.path, "]");
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
134 for (let ef of arr) if (ef.equals(fl)) { found = true; break; }
135 if (!found) arr.push(fl);
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;
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
154 nfo.runat = "document-end"; // default
155 nfo.libs = new Array();
156 nfo.incs = new Array();
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);
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");
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 *");
182 let re = uri2re(kv.value);
183 if (re) nfo.excREs.push(re);
186 if (kv.name === "include") {
188 if (!kv.value) continue;
189 if (kv.value === "*") {
192 if (nfo.incREs.length != 0) nfo.incREs = new Array();
193 if (nfo.incMatches.length != 0) nfo.incMatches = new Array();
195 let re = uri2re(kv.value);
196 if (re) nfo.incREs.push(re);
201 if (kv.name === "match") {
203 if (!kv.value) continue;
205 let mt = new MatchPattern(kv.value);
207 //if (addonOptions.debugCache) conlog("@match <all_urls>");
210 if (nfo.incREs.length != 0) nfo.incREs = new Array();
211 if (nfo.incMatches.length != 0) nfo.incMatches = new Array();
213 //if (addonOptions.debugCache) conlog("@match: ", mt.pattern);
215 nfo.incMatches.push(mt);
221 if (kv.name === "run-at") {
223 case "document-start":
225 nfo.runat = kv.value;
230 if (kv.name === "unwrap" || kv.name === "unwrapped") {
231 nfo.unwrapped = true;
234 if (kv.name === "wrap" || kv.name === "wrapped") {
235 nfo.unwrapped = false;
238 if (kv.name === "noframe" || kv.name === "noframes") {
239 nfo.noframes = false;
242 if (kv.name === "frame" || kv.name === "frames") {
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");
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);
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;
268 if (!procJSIncLib(dir, kv.value, nfo.incs)) {
269 if (addonOptions.debugCache) conlog("bad @requires");
275 if (kv.name === "grant") {
276 // "@grant none" means the same as "@unwrap"
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;
287 if (!incAll && !wasMatch && nfo.incREs.length == 0) {
288 // no includes at all
289 if (addonOptions.debugCache) conlog("no @include");
296 if (nfo.wantlog === null) nfo.wantlog = (!nfo.isPackage && !nfo.isUserJS);
298 // if we met "@grant none", this means "unwrap"
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;
305 // nothing was specified
306 if (nfo.unwrapped === null) nfo.unwrapped = false;
308 // public `conlog` function for unwrapped scripts
309 if (nfo.unwrapped) nfo.wantlog = true;
312 logException("processScriptMeta", e);
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
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;
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, "'");
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, "'");
351 userscripts.push(nfo);
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;
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, "'");
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);
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
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};
410 pkgscripts.push(nfo);
412 //conlog("=== pkgscripts ==="); for (let nfo of pkgscripts) dumpNfo(nfo);
416 ////////////////////////////////////////////////////////////////////////////////
417 exports.reset = function () {
418 if (addonOptions.debugCache) conlog("resetting script cache");
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);
450 case "ftp": // no, really, why?
451 case "data": // inject into frames and other smart things created with "data:" URL
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;
466 if (nfo.excREs.length) {
468 for (let re of nfo.excREs) if (re.test(url)) { hit = true; break; }
469 if (hit) return false;
472 if (nfo.incREs.length === 0) return true; // "@include *"
473 if (nfo.incREs.length) {
475 for (let re of nfo.incREs) if (re.test(url)) { hit = true; break; }
476 if (hit) return true;
479 if (nfo.incMatches.length) {
481 for (let mt of nfo.incMatches) if (mt.doMatch(url)) { hit = true; break; }
482 if (hit) return true;
489 // returns array of nfos or false
492 exports.scriptsForUrl = function (url, inFrame, runat) {
493 let res = new Array();
497 for (let nfo of userscripts) {
498 if (!isGoodForUrl(nfo, url, inFrame, runat)) continue;
502 for (let nfo of pkgscripts) {
503 if (!isGoodForUrl(nfo, url, inFrame, runat)) continue;
510 ////////////////////////////////////////////////////////////////////////////////
511 // get script list; returns array of xnfo
517 let list = new Array();
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;
535 xnfo.name = (islib ? "libs/" : "")+fl.leafName;
537 //conlog("getScriptsForEdit: '", xnfo.name, "' [", xnfo.path, "]");
542 global.getScriptsForEdit = function () {
543 let jdir = getUserJSDir();
544 let ldir = getUserLibDir();
546 if (jdLMT !== jdir.lastModifiedTime || ldLMT !== ldir.lastModifiedTime) {
548 jdLMT = jdir.lastModifiedTime;
549 ldLMT = ldir.lastModifiedTime;
551 scanDir(jdir, false);
555 return {lmod: lastModCount, list:list};
558 exports.getScriptsForEdit = getScriptsForEdit;