more pm28 fixes
[guerillascript.git] / main / startup.js
blobd7b6c6c9b6c7154a5cc7eb4003813255dc8d158f
1 /* coded by Ketmar // Invisible Vector (psyc://ketmar.no-ip.org/~Ketmar)
2  * Understanding is not required. Only obedience.
3  *
4  * This program is free software. It comes without any warranty, to
5  * the extent permitted by applicable law. You can redistribute it
6  * and/or modify it under the terms of the Do What The Fuck You Want
7  * To Public License, Version 2, as published by Sam Hocevar. See
8  * http://www.wtfpl.net/txt/copying/ for more details.
9  */
10 ////////////////////////////////////////////////////////////////////////////////
11 this.EXPORTED_SYMBOLS = [];
13 let {utils:Cu, classes:Cc, interfaces:Ci, results:Cr} = Components;
16 ////////////////////////////////////////////////////////////////////////////////
17 // addonName: used in `alert()`
18 function initAddonInternal (global, addonName, urlMain, urlJS) {
19   const {utils:Cu, classes:Cc, interfaces:Ci, results:Cr} = Components;
21   const promptSvc = Cc["@mozilla.org/embedcomp/prompt-service;1"].getService(Ci.nsIPromptService);
22   const appInfo = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo);
23   switch (appInfo.ID) {
24     case "{8de7fcbb-c55c-4fbe-bfc5-fc555c87dbc4}": break;
25     case "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}": promptSvc.alert(null, addonName, "Ff is not supported"); return null;
26     case "{3550f703-e582-4d05-9a08-453d09bdfdc6}": promptSvc.alert(null, addonName, "Thunderbird is not supported"); return null;
27     case "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}": promptSvc.alert(null, addonName, "Seamonkey is not supported"); return null;
28     default: promptSvc.alert(null, addonName, "Unknown application "+appInfo.ID+" is not supported"); return null;
29   }
31   // bootstrapped addons has no `window`
32   //const principal = (typeof(window) !== "undefined" ? window : Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal));
33   let gsbox = new Cu.Sandbox(Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal), {
34     sandboxName: addonName+" Internal Sandbox",
35     wantComponents: false,
36     wantXrays: false,
37     wantXHRConstructor: true,
38   });
39   Cu.import("resource://gre/modules/Services.jsm", gsbox);
41   // bootstrapped addons has no `window`
42   if (typeof(window) !== "undefined") {
43     gsbox.window = window;
44     gsbox.document = window.document;
45   }
47   for (let name of ["CSS","indexedDB","XMLHttpRequest","TextEncoder","TextDecoder","URL","URLSearchParams","atob","btoa","Blob","File"]) {
48     if (name in global) {
49       //Components.utils.reportError("exporting '"+name+"'");
50       gsbox[name] = global[name];
51     }
52   }
54   // add some common API
55   Object.defineProperty(gsbox, "jscodeUrl", {get: function () urlJS});
56   Object.defineProperty(gsbox, "contentUrl", {get: function () urlMain});
58   // `tieto` API: ties method to object, so it always has correct `this`
59   // you may specify additional arguments that will be always passed unchanged
60   function tieto (obj, method) {
61     let mt;
62     if (typeof(obj) === "undefined") throw new Error("object or `null` expected");
63     if (obj !== null && typeof(obj) !== "object") throw new Error("object or `null` expected");
64     switch (typeof(method)) {
65       case "string":
66         if (!obj) throw new Error("can't take method by name from `null` object");
67         if (!(method in obj)) throw new Error("method '"+method+"' doesn't exist in object '"+obj+"'");
68         mt = obj[method];
69         if (typeof(mt) !== "function") throw new Error("method '"+method+"' isn't a function in object '"+obj+"'");
70         break;
71       case "function":
72         if (!obj) obj = {};
73         mt = method;
74         break;
75       default:
76         throw new Error("`method` should be function or method name");
77     }
78     // hack: replace first array item; avoiding extra arrays
79     let rest = Array.prototype.splice.call(arguments, 1, arguments.length);
80     rest[0] = obj;
81     // now use `bind`
82     return mt.bind.apply(mt, rest);
83   }
85   const sb_tieto = tieto(gsbox, tieto);
86   Object.defineProperty(gsbox, "tieto", {get: function () sb_tieto});
88   const sb_Components = tieto(gsbox, function (c) c, Components);
89   const sb_Cu = tieto(gsbox, function (cu) cu, Cu);
90   const sb_Cc = tieto(gsbox, function (cc) cc, Cc);
91   const sb_Ci = tieto(gsbox, function (ci) ci, Ci);
92   const sb_Cr = tieto(gsbox, function (cr) cr, Cr);
94   Object.defineProperty(gsbox, "Components", {get: function () sb_Components()});
95   Object.defineProperty(gsbox, "Cu", {get: function () sb_Cu()});
96   Object.defineProperty(gsbox, "Cc", {get: function () sb_Cc()});
97   Object.defineProperty(gsbox, "Ci", {get: function () sb_Ci()});
98   Object.defineProperty(gsbox, "Cr", {get: function () sb_Cr()});
100   // console log functions
101   function logErr (str) Cu.reportError(str);
102   const sb_logError = tieto(gsbox, function (logErrMsg) {
103     if (arguments.length > 1) {
104       let s = "";
105       for (let idx = 1; idx < arguments.length; ++idx) s += ""+arguments[idx];
106       logErrMsg(s);
107     }
108   }, logErr);
109   Object.defineProperty(gsbox, "logError", {get: function () sb_logError});
111   const sb_logException = tieto(gsbox, function (logErrMsg, msg, e) {
112     if (typeof(e) === "undefined" && typeof(msg) === "object") {
113       e = msg;
114       msg = "SOME";
115     }
116     if (typeof(e) !== "undefined" /*&& e instanceof global.Error*/ && e) {
117       logErrMsg(""+msg+" ERROR: "+e.name+": "+e.message+" : "+e.lineNumber);
118       if (e.stack) logErrMsg(e.stack);
119       logErrMsg(e);
120     } else {
121       logErrMsg(""+msg);
122       if (typeof(e) !== "undefined" && e) logErrMsg("e="+e);
123     }
124   }, logErr);
125   Object.defineProperty(gsbox, "logException", {get: function () sb_logException});
127   // https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsIConsoleService#logStringMessage() - wstring / wide string
128   const conService = Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService);
129   function logStr (str) conService.logStringMessage(str);
131   const sb_conlog = tieto(gsbox, function (logStrMsg) {
132     if (arguments.length > 1) {
133       let le = ("addonOptions" in this ? this["addonOptions"].logEnabled : true);
134       if (!le) return;
135       let s = "";
136       for (let idx = 1; idx < arguments.length; ++idx) s += ""+arguments[idx];
137       logStrMsg(s);
138     }
139   }, logStr);
140   Object.defineProperty(gsbox, "conlog", {get: function () sb_conlog});
142   const sb_debuglog = tieto(gsbox, function (logStrMsg) {
143     if (arguments.length > 1) {
144       if ("addonOptions" in this) {
145         let ao = this["addonOptions"];
146         //if (("logEnabled" in ao) && !ao.logEnabled) return;
147         if (!("debugMode" in ao) || !ao.debugMode) return;
148       }
149       let s = "";
150       for (let idx = 1; idx < arguments.length; ++idx) s += ""+arguments[idx];
151       logStrMsg(s);
152     }
153   }, logStr);
154   Object.defineProperty(gsbox, "debuglog", {get: function () sb_debuglog});
156   const sb_print = tieto(gsbox, function (logStrMsg) {
157     if (arguments.length > 1) {
158       let s = "";
159       for (let idx = 1; idx < arguments.length; ++idx) {
160         let t = ""+arguments[idx];
161         if (t) s += " "+t;
162       }
163       logStrMsg(s.substr(1));
164     }
165   }, logStr);
166   Object.defineProperty(gsbox, "print", {get: function () sb_print});
168   // script loaders
169   const newURI = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService).newURI;
170   //const newFileURI = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService).newFileURI;
171   const loadSubScript = Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader).loadSubScript;
173   const specsSchemes = [
174     { re: /^mainjs:(.+)$/, pfx: urlJS},
175     { re: /^main:(.+)$/, pfx: urlMain}
176   ];
177   function toFullUrl (scriptSpec, name) {
178     if (/^(?:chrome|resource|file):/.test(name)) return name;
179     for (let sp of specsSchemes) {
180       let mt = name.match(sp.re);
181       if (mt) return sp.pfx+mt[1];
182     }
183     return scriptSpec+name;
184   }
186   // include("incname")
187   const sb_include = tieto(gsbox, function (scriptSpec, newURI, loadSubScript, reportError, name) {
188     if (name.length == 0) throw new Error("invalid include name: '"+name+"'");
189     if (!(/.\.js$/.test(name))) name += ".js";
190     let fullUrl = toFullUrl(scriptSpec, name);
191     try {
192       let uri = newURI(fullUrl, null, null);
193       loadSubScript(uri.spec, gsbox, "UTF-8");
194     } catch (e) {
195       if (typeof(e) === "object") {
196         reportError("INCLUDE ERROR'"+name+"': "+e.name+": "+e.message+" : "+e.lineNumber+"\n"+e.stack);
197         reportError(e);
198       } else {
199         reportError("INCLUDE ERROR '"+name+"': "+e);
200       }
201       throw e;
202     }
203   }, urlJS+"includes/", newURI, loadSubScript, Cu.reportError);
204   Object.defineProperty(gsbox, "include", {get: function () sb_include});
207   function loadTextContentsFromUrl (url, encoding) {
208     if (typeof(encoding) !== "string") encoding = "UTF-8"; else encoding = encoding||"UTF-8";
210     function toUnicode (text) {
211       if (typeof(text) === "string") {
212         if (text.length == 0) return "";
213       } else if (text instanceof ArrayBuffer) {
214         if (text.byteLength == 0) return "";
215         text = new Uint8Array(text);
216         return new TextDecoder(encoding).decode(text);
217       } else if ((text instanceof Uint8Array) || (text instanceof Int8Array)) {
218         if (text.length == 0) return "";
219         return new TextDecoder(encoding).decode(text);
220       } else {
221         return "";
222       }
223       let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Ci.nsIScriptableUnicodeConverter);
224       converter.charset = encoding;
225       let res = converter.ConvertToUnicode(text);
226       if (res.length >= 3 && res.substr(0, 3) == "\u00EF\u00BB\u00BF") res = res.substr(3); // fuck BOM
227       return res;
228     }
230     // download from URL
231     let req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance();
232     req.mozBackgroundRequest = true;
233     req.open("GET", url, false);
234     req.timeout = 30*1000;
235     const cirq = Ci.nsIRequest;
236     req.channel.loadFlags |= cirq.INHIBIT_CACHING|cirq.INHIBIT_PERSISTENT_CACHING|cirq.LOAD_BYPASS_CACHE|cirq.LOAD_ANONYMOUS;
237     req.responseType = "arraybuffer";
238     //if (sendCookies) addCookies(req, urlsend);
239     let error = false;
240     req.onerror = function () { error = true; }; // just in case
241     req.ontimeout = function () { error = true; }; // just in case
242     req.send(null);
243     if (!error) {
244       error = (req.status != 0 && Math.floor(req.status/100) != 2);
245       //(/^file:/.test(file.spec) ? (req.status != 0) : (Math.floor(req.status/100) != 2));
246     }
247     if (error) throw new Error("can't load URI contents: status="+req.status+"; error="+error);
248     return toUnicode(req.response, encoding);
249   }
252   let modules = {}; // list of loaded modules
254   // require("modname")
255   const sb_require = tieto(gsbox, function (scriptSpec, newURI, loadSubScript, reportError, import_, modules, name) {
256     if (name.length == 0) throw new Error("invalid include name: '"+name+"'");
257     if (!(/.\.js$/.test(name))) name += ".js";
258     if (name in modules) return modules[name];
259     let fullUrl = toFullUrl(scriptSpec, name);
260     //else sb_debuglog("requiring '", name, "' : [", fullUrl, "]");
261     //let scope = {};
262     let scope = Object.create(gsbox);
263     scope.exports = {};
264     scope.exportsGlobal = {};
266     try {
267       let uri = newURI(fullUrl, null, null);
269       let box = new Cu.Sandbox(Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal), {
270         wantComponents: false,
271         wantXrays: false,
272         wantXHRConstructor: true,
273         sandboxPrototype: scope,
274       });
276       // uses(name[, name]...);
277       let sc_uses = tieto(box, function (scope, import_) {
278         if (arguments.length > 3) {
279           for (let idx = 2; idx < arguments.length; ++idx) import_(arguments[idx], scope);
280           return null;
281         } else if (arguments.length == 3) {
282           return import_(arguments[2], scope);
283         } else {
284           return null;
285         }
286       }, scope, Cu.import);
287       Object.defineProperty(box, "uses", {get: function () sc_uses});
289       // include("incname")
290       let sc_include = tieto(box, function (scriptSpec, newURI, loadSubScript, reportError, box, name) {
291         if (name.length == 0) throw new Error("invalid include name: '"+name+"'");
292         if (!(/.\.js$/.test(name))) name += ".js";
293         let fullUrl = toFullUrl(scriptSpec, name);
294         try {
295           let uri = newURI(fullUrl, null, null);
296           loadSubScript(uri.spec, box, "UTF-8");
297         } catch (e) {
298           if (typeof(e) === "object") {
299             reportError("INCLUDE ERROR'"+name+"': "+e.name+": "+e.message+" : "+e.lineNumber+"\n"+e.stack);
300             reportError(e);
301           } else {
302             reportError("INCLUDE ERROR '"+name+"': "+e);
303           }
304           throw e;
305         }
306       }, urlJS+"includes/", newURI, loadSubScript, Cu.reportError, box);
307       Object.defineProperty(box, "include", {get: function () sc_include});
309       let text = loadTextContentsFromUrl(uri.spec);
310       Cu.evalInSandbox(text, box, "ECMAv5", name, 1);
311     } catch (e) {
312       if (typeof(e) === "object") {
313         reportError("REQUIRE ERROR'"+name+"': "+e.name+": "+e.message+" : "+e.lineNumber+"\n"+e.stack);
314         reportError(e);
315       } else {
316         reportError("REQUIRE ERROR '"+name+"': "+e);
317       }
318       throw e;
319     }
321     modules[name] = scope.exports;
322     if (typeof(scope.exportsGlobal) === "object") {
323       for (let k in scope.exportsGlobal) {
324         if (k !== "__exposedProps__" && Object.prototype.hasOwnProperty.call(scope.exportsGlobal, k)) {
325           try { gsbox[k] = scope.exportsGlobal[k]; } catch (e) {}
326         }
327       }
328     }
329     return scope.exports;
330   }, urlJS+"modules/", newURI, loadSubScript, Cu.reportError, Cu.import, modules);
331   Object.defineProperty(gsbox, "require", {get: function () sb_require});
334   // alert("msg")
335   let sb_alert = tieto(gsbox, function (promptSvc, addonName, msg) {
336     promptSvc.alert(null, addonName, ""+msg);
337   }, promptSvc, addonName);
338   Object.defineProperty(gsbox, "alert", {get: function () sb_alert});
341   let startupHooks = [], shutdownHooks = [];
342   let winloadHooks = [], winunloadHooks = [];
344   let mainLoaded = false;
346   function runHooks (hooktype, list, rev, firsttime) {
347     let rest = Array.prototype.splice.call(arguments, 4, arguments.length);
348     let callmain = false;
349     if (firsttime) {
350       //Cu.reportError("runHooks ("+hooktype+"): first time");
351       if (!mainLoaded) {
352         //Cu.reportError("runHooks ("+hooktype+"): main not loaded");
353         // now load main file
354         mainLoaded = true;
355         let loadmain = ("__dontLoadMainJS" in global ? !global.__dontLoadMainJS : true);
356         if (loadmain) {
357           //Cu.reportError("runHooks ("+hooktype+"): loading main...");
358           //Cu.reportError("runHooks ("+hooktype+"): len="+list.length);
359           gsbox.include("mainjs:prefs/prefs.js"); // define default extension preferences
360           gsbox.require("mainjs:prefsengine.js");
361           callmain = true;
362           //Cu.reportError("runHooks ("+hooktype+"): len="+list.length);
363           gsbox.include("mainjs:main.js");
364         }
365       }
366     }
367     if (hooktype === "shutdown" && typeof(gsbox.main_preshutdown) === "function") {
368       try {
369         gsbox.main_preshutdown();
370       } catch (e) {
371         Cu.reportError("PRESHUTDOWN ERROR: "+e.name+": "+e.message+" : "+e.lineNumber);
372         if (e.stack) Cu.reportError(e.stack);
373         Cu.reportError(e);
374       }
375     }
376     let idx = (rev ? list.length : -1);
377     for (;;) {
378       if (rev) {
379         if (--idx < 0) break;
380       } else {
381         if (++idx >= list.length) break;
382       }
383       let h = list[idx];
384       try {
385         //conService.logStringMessage("running hook: '"+h.name+"'");
386         let args = Array.prototype.slice.call(rest);
387         //Array.prototype.push.apply(args, arguments);
388         h.cback.apply(gsbox, args);
389       } catch (e) {
390         callmain = false;
391         Cu.reportError("HOOK '"+hooktype+"' ("+h.name+") ERROR: "+e.name+": "+e.message+" : "+e.lineNumber+(e.stack ? "\n"+e.stack : ""));
392         Cu.reportError(e);
393         if (e.stack) Cu.reportError(e.stack);
394       }
395     }
396     if (callmain && typeof(gsbox.main_postinit) === "function") gsbox.main_postinit();
397   }
399   function addHook (hooks, name, cback) {
400     if (typeof(cback) === "undefined") {
401       // register*Hook(hookfn)
402       cback = name;
403       name = null;
404     }
405     if (typeof(name) === "undefined") throw new Error("name or `null` expected");
406     if (!name) name = "";
407     if (typeof(name) !== "string") throw new Error("name or `null` expected");
408     if (typeof(cback) !== "function") throw new Error("function expected");
409     for (let obj in hooks) if (obj.cback === cback) return; // nothing to do
410     hooks.push({name:name, cback:cback});
411   }
413   const sb_rsth = tieto(gsbox, addHook, startupHooks);
414   const sb_rsuh = tieto(gsbox, addHook, shutdownHooks);
416   // registerStartupHook(name, hookfn)
417   // registerShutdownHook(name, hookfn)
418   Object.defineProperty(gsbox, "registerStartupHook", {get: function () sb_rsth});
419   Object.defineProperty(gsbox, "registerShutdownHook", {get: function () sb_rsuh});
421   const sb_rwlh = tieto(gsbox, addHook, winloadHooks);
422   const sb_rwuh = tieto(gsbox, addHook, winunloadHooks);
424   // registerWindowLoadHook(name, hookfn)
425   // registerWindowUnloadHook(name, hookfn)
426   Object.defineProperty(gsbox, "registerWindowLoadHook", {get: function () sb_rwlh});
427   Object.defineProperty(gsbox, "registerWindowUnloadHook", {get: function () sb_rwuh});
429   return {
430     sbox: gsbox,
431     tieto: tieto,
432     defProp: tieto(this, function (mainobj, name, ops) {
433       //Cu.reportError("GS: defining property '"+name+"'");
434       mainobj.defineProperty(gsbox, name, ops);
435     }, Object),
436     //onInstall: function () {},
437     //onUnistall: function () {},
438     onStartup: tieto(this, runHooks, "startup", startupHooks, false, true),
439     onShutdown: tieto(this, runHooks, "shutdown", shutdownHooks, true, false),
440     onWindowLoad: tieto(this, runHooks, "window opened", winloadHooks, true, false),
441     onWindowUnload: tieto(this, runHooks, "window closed", winunloadHooks, true, false),
442   };
446 ////////////////////////////////////////////////////////////////////////////////
447 const addonName = "Guerilla Script"; // addon name, used in `alert()`
448 const addonContentUrl = "chrome://guerilla-script/content/"; // content URL, must end with "/"
449 const addonJSUrl = "chrome://guerilla-script-jscode/content/"; // addon js code URL, must end with "/"
451 let addonobj = null;
454 ////////////////////////////////////////////////////////////////////////////////
455 function initAddon (global) {
456   try {
457     addonobj = initAddonInternal(global, addonName, addonContentUrl, addonJSUrl);
458     if (!addonobj) throw new Error("can't init addon '"+addonName+"'"); // alas, something is wrong
459     addonobj.defProp("gsdoxUrl", {get: function () "chrome://guerilla-script-dox/content/"});
460     addonobj.onStartup();
461   } catch (e) {
462     if (addonobj) try { addonobj.onShutdown(); } catch (xx) {}
463     addonobj = false;
464     Cu.reportError(e.stack);
465     Cu.reportError(e);
466     throw e;
467   }
471 ////////////////////////////////////////////////////////////////////////////////
472 let windowCount = 0;
474 function newWindow (global, win) {
475   if (addonobj === null) initAddon(global);
476   if (addonobj) {
477     win.addEventListener("load", function () {
478       ++windowCount;
479       // setup unload hook
480       global.window.addEventListener("unload", function () {
481         --windowCount;
482         addonobj.onWindowUnload(win);
483         if (windowCount == 0) {
484           // no more registered windows --> shutdown
485           addonobj.onShutdown();
486         }
487       }, false);
488       addonobj.onWindowLoad(win);
489     }, false);
490   }
494 ////////////////////////////////////////////////////////////////////////////////
495 function getAddonObj () addonobj;