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 //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();
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
+"'");
46 if (res
.length
>= 3 && res
.substr(0, 3) == "\u00EF\u00BB\u00BF") res
= res
.substr(3);
51 ////////////////////////////////////////////////////////////////////////////////
52 function uri2re (uri
) {
53 uri
= uri
.replace(/^\s+/, "").replace(/\s+$/, "");
54 if (uri
== "") return null;
55 if (uri
== "*") return null;
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
63 for (let f
= 0; f
< uri
.length
; ++f
) {
65 case "*": re
+= ".*?"; break; // any, non-greedy
66 case ".": case "?": case "^": case "$": case "+":
67 case "{": case "}": case "[": case "]": case "|":
68 case "(": case ")": case "\\":
71 case " ": break; // ignore spaces
72 default: re
+= uri
[f
]; break;
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;
90 function normFileName (fname
) {
91 fname
= fname
.replace(/^\s+/, "").replace(/\s+$/, "");
93 for (let fn
of fname
.split("/")) {
94 if (fn
.length
== 0 || fn
== ".") continue;
96 if (res
.length
> 0) res
= res
.slice(0, res
.length
-1);
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];
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] != "//");
141 function processMeta (nfo
, meta
) {
146 let lines
= meta
.split("\n");
147 for (let lno
= 0; lno
< lines
.length
; ++lno
) {
148 let line
= lines
[lno
];
150 if (line
.length
> 1 && line
.charCodeAt(0) == 47 && line
.charCodeAt(1) == 47) line
= line
.substr(2);
151 let kv
= line
.match(metakvRE
);
153 kv
= line
.match(metakeyRE
);
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;
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;
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
);
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;
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
);
189 if (key
== "run-at") {
190 if (value
== "document-start") nfo
.atDocStart
= true;
193 // "@grant none" means the same as "@unwrap"
194 if (key
== "grant") {
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;
203 if (key
!= "include" && key
!= "exclude") continue;
205 if (key
== "include") nfo
.incAll
= true; else nfo
.excAll
= true;
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 ////////////////////////////////////////////////////////////////////////////////
218 let jsdirLMT
= 0, libdirLMT
= 0;
219 let scCache
= {}, libCache
= {};
220 let scNameList
= new Array(); // alphabetically sorted
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 "*"
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)
238 * bool wasGrantAnything
244 ////////////////////////////////////////////////////////////////////////////////
245 function parseUserJSOptions (nfo
, isujs
) {
248 nfo
.incAll
= nfo
.excAll
= false;
249 nfo
.unwrapped
= null;
250 nfo
.noframes
= false;
251 nfo
.atDocStart
= false;
254 nfo
.disabled
= false;
255 nfo
.wasGrantAnything
= false;
256 nfo
.wasGrantNone
= false;
257 nfo
.wantLog
= isujs
&& !isGMJS(nfo
);
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
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
) {
284 if (!deadScripts
) deadScripts
= [];
288 // remove dead scripts
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
) {
308 if (!deadScripts
) deadScripts
= [];
314 // remove dead scripts
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
]];
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
]+"'";
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
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
);
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
= [];
359 if (guerillaOptions
.debugCache
) conlog("script '"+k
+"': found library '"+nfo
.libs
[idx
]+"'");
362 logError("script '"+k
+"': library '"+nfo
.libs
[idx
]+"' not found!");
363 if (!deadScripts
) deadScripts
= [];
370 // remove dead scripts
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
) {
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
) {
393 if (typeof(req
) != "string") {
394 // check if access time was changed
395 let fl
= jsdir
.clone();
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");
403 if (guerillaOptions
.debugCache
) conlog(" req #"+idx
+" ("+req
.path
+") has different time");
407 if (!isValidFileName(req
)) return false; // alas
408 // now, we have to reload file
412 let fl
= jsdir
.clone();
414 if (!fl
.exists()) return false;
415 req
.text
= fileReadText(fl
);
416 req
.lmtime
= fl
.lastModifiedTime
;
418 req
.baseName
= buildBaseName(req
.name
);
423 if (guerillaOptions
.debugCache
) conlog(" req #"+idx
+" ("+req
.path
+") is loaded");
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
];
443 if (guerillaOptions
.debugCache
) conlog("new js: '"+name
+"'");
446 nfo
.baseName
= buildBaseName(nfo
.name
);
448 cache
[nfo
.name
] = nfo
;
449 } else if (nfo
.lmtime
!= fl
.lastModifiedTime
) {
450 if (guerillaOptions
.debugCache
) conlog("updated js: '"+name
+"'");
452 if (checkReqs(nfo
, dir
)) nfo
.upcount
= uniqueCount
;
456 nfo
.text
= fileReadText(fl
);
457 nfo
.lmtime
= fl
.lastModifiedTime
;
458 parseUserJSOptions(nfo
, isujs
);
459 if (checkReqs(nfo
, dir
)) nfo
.upcount
= uniqueCount
;
461 logException("JS PARSER", e
);
467 ////////////////////////////////////////////////////////////////////////////////
471 ////////////////////////////////////////////////////////////////////////////////
472 scacheAPI
.reset = function () {
473 if (guerillaOptions
.debugCache
) conlog("resetting script cache");
474 //++uniqueCount; // = 0;
475 jsdirLMT
= libdirLMT
= 0;
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;
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");
494 uniCountIncreased
= true;
495 scanDirectory(libCache
, libdir
, false);
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");
505 if (!uniCountIncreased
) {
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);
515 // the thing that should not be
518 // now rebuild other things
519 if (uniCountIncreased
) {
527 ////////////////////////////////////////////////////////////////////////////////
528 scacheAPI
.isGoodScheme = function (scstr
) {
529 let cpos
= scstr
.indexOf(":");
530 if (cpos
< 0) return false;
531 scstr
= scstr
.substr(0, cpos
);
535 //case "ftp": // no, really, why?
543 ////////////////////////////////////////////////////////////////////////////////
545 // returns array of nfos or false
546 scacheAPI
.scriptsForUri = function (uri
, inFrame
, docStart
) {
549 let scheme = ioSvc.extractScheme(uri);
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;
571 for (let f
= 0; f
< rl
.length
; ++f
) if (uri
.match(rl
[f
])) { ok
= true; break; }
574 // url matches, check excludes
578 for (let f
= 0; f
< rl
.length
; ++f
) if (uri
.match(rl
[f
])) { ok
= true; break; }
581 // ok, this script passed checks
589 ////////////////////////////////////////////////////////////////////////////////
590 // get current `uniqueCount`
591 scacheAPI
.getUC = function () { return uniqueCount
; }
594 ////////////////////////////////////////////////////////////////////////////////
595 // get script list; returns array of xnfo
604 scacheAPI
.getScripts = function () {
608 nfo2xnfo = function (nfo
, islib
) {
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
);
620 nfoarr2xnfo = function (nfoa
, islib
) {
622 if (nfoa
&& nfoa
.length
) {
623 for (let idx
= 0; idx
< nfoa
.length
; ++idx
) res
.push(nfo2xnfo(nfoa
[idx
], islib
));
629 for (let idx
= 0; idx
< scNameList
.length
; ++idx
) {
630 let nfo
= scCache
[scNameList
[idx
]];
631 res
.push(nfo2xnfo(nfo
));
635 for (let k
in libCache
) { lnm
.push(libCache
[k
].name
); }
637 for (let idx
= 0; idx
< lnm
.length
; ++idx
) {
638 let nfo
= libCache
[lnm
[idx
]];
639 res
.push(nfo2xnfo(nfo
, true));
645 ////////////////////////////////////////////////////////////////////////////////
646 scacheAPI
.fileReadText
= fileReadText
;