Local packaging for firefox add-on.
[gssweb.git] / browsers / firefox / packaging / bootstrap.js
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5 // @see http://mxr.mozilla.org/mozilla-central/source/js/src/xpconnect/loader/mozJSComponentLoader.cpp
6
7 'use strict';
8
9 // IMPORTANT: Avoid adding any initialization tasks here, if you need to do
10 // something before add-on is loaded consider addon/runner module instead!
11
12 const { classes: Cc, Constructor: CC, interfaces: Ci, utils: Cu,
13         results: Cr, manager: Cm } = Components;
14 const ioService = Cc['@mozilla.org/network/io-service;1'].
15                   getService(Ci.nsIIOService);
16 const resourceHandler = ioService.getProtocolHandler('resource').
17                         QueryInterface(Ci.nsIResProtocolHandler);
18 const systemPrincipal = CC('@mozilla.org/systemprincipal;1', 'nsIPrincipal')();
19 const scriptLoader = Cc['@mozilla.org/moz/jssubscript-loader;1'].
20                      getService(Ci.mozIJSSubScriptLoader);
21 const prefService = Cc['@mozilla.org/preferences-service;1'].
22                     getService(Ci.nsIPrefService).
23                     QueryInterface(Ci.nsIPrefBranch);
24 const appInfo = Cc["@mozilla.org/xre/app-info;1"].
25                 getService(Ci.nsIXULAppInfo);
26 const vc = Cc["@mozilla.org/xpcom/version-comparator;1"].
27            getService(Ci.nsIVersionComparator);
28
29
30 const REASON = [ 'unknown', 'startup', 'shutdown', 'enable', 'disable',
31                  'install', 'uninstall', 'upgrade', 'downgrade' ];
32
33 const bind = Function.call.bind(Function.bind);
34
35 let loader = null;
36 let unload = null;
37 let cuddlefishSandbox = null;
38 let nukeTimer = null;
39
40 // Utility function that synchronously reads local resource from the given
41 // `uri` and returns content string.
42 function readURI(uri) {
43   let ioservice = Cc['@mozilla.org/network/io-service;1'].
44     getService(Ci.nsIIOService);
45   let channel = ioservice.newChannel(uri, 'UTF-8', null);
46   let stream = channel.open();
47
48   let cstream = Cc['@mozilla.org/intl/converter-input-stream;1'].
49     createInstance(Ci.nsIConverterInputStream);
50   cstream.init(stream, 'UTF-8', 0, 0);
51
52   let str = {};
53   let data = '';
54   let read = 0;
55   do {
56     read = cstream.readString(0xffffffff, str);
57     data += str.value;
58   } while (read != 0);
59
60   cstream.close();
61
62   return data;
63 }
64
65 // We don't do anything on install & uninstall yet, but in a future
66 // we should allow add-ons to cleanup after uninstall.
67 function install(data, reason) {}
68 function uninstall(data, reason) {}
69
70 function startup(data, reasonCode) {
71   try {
72     let reason = REASON[reasonCode];
73     // URI for the root of the XPI file.
74     // 'jar:' URI if the addon is packed, 'file:' URI otherwise.
75     // (Used by l10n module in order to fetch `locale` folder)
76     let rootURI = data.resourceURI.spec;
77
78     // TODO: Maybe we should perform read harness-options.json asynchronously,
79     // since we can't do anything until 'sessionstore-windows-restored' anyway.
80     let options = JSON.parse(readURI(rootURI + './harness-options.json'));
81
82     let id = options.jetpackID;
83     let name = options.name;
84
85     // Clean the metadata
86     options.metadata[name]['permissions'] = options.metadata[name]['permissions'] || {};
87
88     // freeze the permissionss
89     Object.freeze(options.metadata[name]['permissions']);
90     // freeze the metadata
91     Object.freeze(options.metadata[name]);
92
93     // Register a new resource 'domain' for this addon which is mapping to
94     // XPI's `resources` folder.
95     // Generate the domain name by using jetpack ID, which is the extension ID
96     // by stripping common characters that doesn't work as a domain name:
97     let uuidRe =
98       /^\{([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\}$/;
99
100     let domain = id.
101       toLowerCase().
102       replace(/@/g, '-at-').
103       replace(/\./g, '-dot-').
104       replace(uuidRe, '$1');
105
106     let prefixURI = 'resource://' + domain + '/';
107     let resourcesURI = ioService.newURI(rootURI + '/resources/', null, null);
108     resourceHandler.setSubstitution(domain, resourcesURI);
109
110     // Create path to URLs mapping supported by loader.
111     let paths = {
112       // Relative modules resolve to add-on package lib
113       './': prefixURI + name + '/lib/',
114       './tests/': prefixURI + name + '/tests/',
115       '': 'resource://gre/modules/commonjs/'
116     };
117
118     // Maps addon lib and tests ressource folders for each package
119     paths = Object.keys(options.metadata).reduce(function(result, name) {
120       result[name + '/'] = prefixURI + name + '/lib/'
121       result[name + '/tests/'] = prefixURI + name + '/tests/'
122       return result;
123     }, paths);
124
125     // We need to map tests folder when we run sdk tests whose package name
126     // is stripped
127     if (name == 'addon-sdk')
128       paths['tests/'] = prefixURI + name + '/tests/';
129
130     let useBundledSDK = options['force-use-bundled-sdk'];
131     if (!useBundledSDK) {
132       try {
133         useBundledSDK = prefService.getBoolPref("extensions.addon-sdk.useBundledSDK");
134       }
135       catch (e) {
136         // Pref doesn't exist, allow using Firefox shipped SDK
137       }
138     }
139
140     // Starting with Firefox 21.0a1, we start using modules shipped into firefox
141     // Still allow using modules from the xpi if the manifest tell us to do so.
142     // And only try to look for sdk modules in xpi if the xpi actually ship them
143     if (options['is-sdk-bundled'] &&
144         (vc.compare(appInfo.version, '21.0a1') < 0 || useBundledSDK)) {
145       // Maps sdk module folders to their resource folder
146       paths[''] = prefixURI + 'addon-sdk/lib/';
147       // test.js is usually found in root commonjs or SDK_ROOT/lib/ folder,
148       // so that it isn't shipped in the xpi. Keep a copy of it in sdk/ folder
149       // until we no longer support SDK modules in XPI:
150       paths['test'] = prefixURI + 'addon-sdk/lib/sdk/test.js';
151     }
152
153     // Retrieve list of module folder overloads based on preferences in order to
154     // eventually used a local modules instead of files shipped into Firefox.
155     let branch = prefService.getBranch('extensions.modules.' + id + '.path');
156     paths = branch.getChildList('', {}).reduce(function (result, name) {
157       // Allows overloading of any sub folder by replacing . by / in pref name
158       let path = name.substr(1).split('.').join('/');
159       // Only accept overloading folder by ensuring always ending with `/`
160       if (path) path += '/';
161       let fileURI = branch.getCharPref(name);
162
163       // On mobile, file URI has to end with a `/` otherwise, setSubstitution
164       // takes the parent folder instead.
165       if (fileURI[fileURI.length-1] !== '/')
166         fileURI += '/';
167
168       // Maps the given file:// URI to a resource:// in order to avoid various
169       // failure that happens with file:// URI and be close to production env
170       let resourcesURI = ioService.newURI(fileURI, null, null);
171       let resName = 'extensions.modules.' + domain + '.commonjs.path' + name;
172       resourceHandler.setSubstitution(resName, resourcesURI);
173
174       result[path] = 'resource://' + resName + '/';
175       return result;
176     }, paths);
177
178     // Make version 2 of the manifest
179     let manifest = options.manifest;
180
181     // Import `cuddlefish.js` module using a Sandbox and bootstrap loader.
182     let cuddlefishPath = 'loader/cuddlefish.js';
183     let cuddlefishURI = 'resource://gre/modules/commonjs/sdk/' + cuddlefishPath;
184     if (paths['sdk/']) { // sdk folder has been overloaded
185                          // (from pref, or cuddlefish is still in the xpi)
186       cuddlefishURI = paths['sdk/'] + cuddlefishPath;
187     }
188     else if (paths['']) { // root modules folder has been overloaded
189       cuddlefishURI = paths[''] + 'sdk/' + cuddlefishPath;
190     }
191
192     cuddlefishSandbox = loadSandbox(cuddlefishURI);
193     let cuddlefish = cuddlefishSandbox.exports;
194
195     // Normalize `options.mainPath` so that it looks like one that will come
196     // in a new version of linker.
197     let main = options.mainPath;
198
199     unload = cuddlefish.unload;
200     loader = cuddlefish.Loader({
201       paths: paths,
202       // modules manifest.
203       manifest: manifest,
204
205       // Add-on ID used by different APIs as a unique identifier.
206       id: id,
207       // Add-on name.
208       name: name,
209       // Add-on version.
210       version: options.metadata[name].version,
211       // Add-on package descriptor.
212       metadata: options.metadata[name],
213       // Add-on load reason.
214       loadReason: reason,
215
216       prefixURI: prefixURI,
217       // Add-on URI.
218       rootURI: rootURI,
219       // options used by system module.
220       // File to write 'OK' or 'FAIL' (exit code emulation).
221       resultFile: options.resultFile,
222       // Arguments passed as --static-args
223       staticArgs: options.staticArgs,
224
225       // Arguments related to test runner.
226       modules: {
227         '@test/options': {
228           allTestModules: options.allTestModules,
229           iterations: options.iterations,
230           filter: options.filter,
231           profileMemory: options.profileMemory,
232           stopOnError: options.stopOnError,
233           verbose: options.verbose,
234           parseable: options.parseable,
235           checkMemory: options.check_memory,
236         }
237       }
238     });
239
240     let module = cuddlefish.Module('sdk/loader/cuddlefish', cuddlefishURI);
241     let require = cuddlefish.Require(loader, module);
242
243     require('sdk/addon/runner').startup(reason, {
244       loader: loader,
245       main: main,
246       prefsURI: rootURI + 'defaults/preferences/prefs.js'
247     });
248   } catch (error) {
249     dump('Bootstrap error: ' +
250          (error.message ? error.message : String(error)) + '\n' +
251          (error.stack || error.fileName + ': ' + error.lineNumber) + '\n');
252     throw error;
253   }
254 };
255
256 function loadSandbox(uri) {
257   let proto = {
258     sandboxPrototype: {
259       loadSandbox: loadSandbox,
260       ChromeWorker: ChromeWorker
261     }
262   };
263   let sandbox = Cu.Sandbox(systemPrincipal, proto);
264   // Create a fake commonjs environnement just to enable loading loader.js
265   // correctly
266   sandbox.exports = {};
267   sandbox.module = { uri: uri, exports: sandbox.exports };
268   sandbox.require = function (id) {
269     if (id !== "chrome")
270       throw new Error("Bootstrap sandbox `require` method isn't implemented.");
271
272     return Object.freeze({ Cc: Cc, Ci: Ci, Cu: Cu, Cr: Cr, Cm: Cm,
273       CC: bind(CC, Components), components: Components,
274       ChromeWorker: ChromeWorker });
275   };
276   scriptLoader.loadSubScript(uri, sandbox, 'UTF-8');
277   return sandbox;
278 }
279
280 function unloadSandbox(sandbox) {
281   if ("nukeSandbox" in Cu)
282     Cu.nukeSandbox(sandbox);
283 }
284
285 function setTimeout(callback, delay) {
286   let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
287   timer.initWithCallback({ notify: callback }, delay,
288                          Ci.nsITimer.TYPE_ONE_SHOT);
289   return timer;
290 }
291
292 function shutdown(data, reasonCode) {
293   let reason = REASON[reasonCode];
294   if (loader) {
295     unload(loader, reason);
296     unload = null;
297
298     // Don't waste time cleaning up if the application is shutting down
299     if (reason != "shutdown") {
300       // Avoid leaking all modules when something goes wrong with one particular
301       // module. Do not clean it up immediatly in order to allow executing some
302       // actions on addon disabling.
303       // We need to keep a reference to the timer, otherwise it is collected
304       // and won't ever fire.
305       nukeTimer = setTimeout(nukeModules, 1000);
306     }
307   }
308 };
309
310 function nukeModules() {
311   nukeTimer = null;
312   // module objects store `exports` which comes from sandboxes
313   // We should avoid keeping link to these object to avoid leaking sandboxes
314   for (let key in loader.modules) {
315     delete loader.modules[key];
316   }
317   // Direct links to sandboxes should be removed too
318   for (let key in loader.sandboxes) {
319     let sandbox = loader.sandboxes[key];
320     delete loader.sandboxes[key];
321     // Bug 775067: From FF17 we can kill all CCW from a given sandbox
322     unloadSandbox(sandbox);
323   }
324   loader = null;
325
326   // both `toolkit/loader` and `system/xul-app` are loaded as JSM's via
327   // `cuddlefish.js`, and needs to be unloaded to avoid memory leaks, when
328   // the addon is unload.
329
330   unloadSandbox(cuddlefishSandbox.loaderSandbox);
331   unloadSandbox(cuddlefishSandbox.xulappSandbox);
332
333   // Bug 764840: We need to unload cuddlefish otherwise it will stay alive
334   // and keep a reference to this compartment.
335   unloadSandbox(cuddlefishSandbox);
336   cuddlefishSandbox = null;
337 }