Bumping manifests a=b2g-bump
[gecko.git] / dom / apps / UserCustomizations.jsm
blob96e37fe85df8b6c8f2d283fce8ac64004e774274
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 file,
3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
7 const Cu = Components.utils;
8 const Cc = Components.classes;
9 const Ci = Components.interfaces;
11 this.EXPORTED_SYMBOLS = ["UserCustomizations"];
13 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
14 Cu.import("resource://gre/modules/Services.jsm");
15 Cu.import("resource://gre/modules/AppsUtils.jsm");
17 XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
18                                    "@mozilla.org/parentprocessmessagemanager;1",
19                                    "nsIMessageBroadcaster");
21 XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
22                                    "@mozilla.org/childprocessmessagemanager;1",
23                                    "nsIMessageSender");
25 XPCOMUtils.defineLazyServiceGetter(this, "console",
26                                    "@mozilla.org/consoleservice;1",
27                                    "nsIConsoleService");
28 /**
29   * Customization scripts and CSS stylesheets can be specified in an
30   * application manifest with the following syntax:
31   * "customizations": [
32   *  {
33   *    "filter": "http://youtube.com",
34   *    "css": ["file1.css", "file2.css"],
35   *    "scripts": ["script1.js", "script2.js"]
36   *  }
37   * ]
38   */
40 let debug = Services.prefs.getBoolPref("dom.mozApps.debug")
41   ? (aMsg) => {
42       dump("-*-*- UserCustomizations (" +
43            (UserCustomizations._inParent ? "parent" : "child") +
44            "): " + aMsg + "\n");
45     }
46   : (aMsg) => {};
48 function log(aStr) {
49   console.logStringMessage(aStr);
52 this.UserCustomizations = {
53   _items: [],
54   _loaded : {},   // Keep track per manifestURL of css and scripts loaded.
55   _windows: null, // Set of currently opened windows.
56   _enabled: false,
58   _addItem: function(aItem) {
59     debug("_addItem: " + uneval(aItem));
60     this._items.push(aItem);
61     if (this._inParent) {
62       ppmm.broadcastAsyncMessage("UserCustomizations:Add", [aItem]);
63     }
64   },
66   _removeItem: function(aHash) {
67     debug("_removeItem: " + aHash);
68     let index = -1;
69     this._items.forEach((script, pos) => {
70       if (script.hash == aHash ) {
71         index = pos;
72       }
73     });
75     if (index != -1) {
76       this._items.splice(index, 1);
77     }
79     if (this._inParent) {
80       ppmm.broadcastAsyncMessage("UserCustomizations:Remove", aHash);
81     }
82   },
84   register: function(aManifest, aApp) {
85     debug("Starting customization registration for " + aApp.manifestURL);
87     if (!this._enabled || !aApp.enabled || aApp.role != "addon") {
88       debug("Rejecting registration (global enabled=" + this._enabled +
89             ") (app role=" + aApp.role +
90             ", enabled=" + aApp.enabled + ")");
91       debug(uneval(aApp));
92       return;
93     }
95     let customizations = aManifest.customizations;
96     if (customizations === undefined || !Array.isArray(customizations)) {
97       return;
98     }
100     let base = Services.io.newURI(aApp.origin, null, null);
102     customizations.forEach(item => {
103       // The filter property is mandatory.
104       if (!item.filter || (typeof item.filter !== "string")) {
105         log("Mandatory filter property not found in this customization item: " +
106             uneval(item) + " in " + aApp.manifestURL);
107         return;
108       }
110       // Create a new object with resolved urls and a hash that we reuse to
111       // remove items.
112       let custom = {
113         filter: item.filter,
114         status: aApp.appStatus,
115         manifestURL: aApp.manifestURL,
116         css: [],
117         scripts: []
118       };
119       custom.hash = AppsUtils.computeObjectHash(item);
121       if (item.css && Array.isArray(item.css)) {
122         item.css.forEach((css) => {
123           custom.css.push(base.resolve(css));
124         });
125       }
127       if (item.scripts && Array.isArray(item.scripts)) {
128         item.scripts.forEach((script) => {
129           custom.scripts.push(base.resolve(script));
130         });
131       }
133       this._addItem(custom);
134     });
135     this._updateAllWindows();
136   },
138   _updateAllWindows: function() {
139     debug("UpdateWindows");
140     if (this._inParent) {
141       ppmm.broadcastAsyncMessage("UserCustomizations:UpdateWindows", {});
142     }
143     // Inject in all currently opened windows.
144     this._windows.forEach(this._injectInWindow.bind(this));
145   },
147   unregister: function(aManifest, aApp) {
148     if (!this._enabled) {
149       return;
150     }
152     debug("Starting customization unregistration for " + aApp.manifestURL);
153     let customizations = aManifest.customizations;
154     if (customizations === undefined || !Array.isArray(customizations)) {
155       return;
156     }
158     customizations.forEach(item => {
159       this._removeItem(AppsUtils.computeObjectHash(item));
160     });
161     this._unloadForManifestURL(aApp.manifestURL);
162   },
164   _unloadForManifestURL: function(aManifestURL) {
165     debug("_unloadForManifestURL " + aManifestURL);
167     if (this._inParent) {
168       ppmm.broadcastAsyncMessage("UserCustomizations:Unload", aManifestURL);
169     }
171     if (!this._loaded[aManifestURL]) {
172       return;
173     }
175     if (this._loaded[aManifestURL].scripts &&
176         this._loaded[aManifestURL].scripts.length > 0) {
177       // We can't rollback script changes, so don't even try to unload in this
178       // situation.
179       return;
180     }
182     this._loaded[aManifestURL].css.forEach(aItem => {
183       try {
184         debug("unloading " + aItem.uri.spec);
185         let utils = aItem.window.QueryInterface(Ci.nsIInterfaceRequestor)
186                                 .getInterface(Ci.nsIDOMWindowUtils);
187         utils.removeSheet(aItem.uri, Ci.nsIDOMWindowUtils.AUTHOR_SHEET);
188       } catch(e) {
189         log("Error unloading stylesheet " + aItem.uri.spec + " : " + e);
190       }
191     });
193     this._loaded[aManifestURL] = null;
194   },
196   _injectItem: function(aWindow, aItem, aInjected) {
197     debug("Injecting item " + uneval(aItem) + " in " + aWindow.location.href);
198     let utils = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
199                        .getInterface(Ci.nsIDOMWindowUtils);
201     let manifestURL = aItem.manifestURL;
203     // Load the stylesheets only in this window.
204     aItem.css.forEach(aCss => {
205       if (aInjected.indexOf(aCss) !== -1) {
206         debug("Skipping duplicated css: " + aCss);
207         return;
208       }
210       let uri = Services.io.newURI(aCss, null, null);
211       try {
212         utils.loadSheet(uri, Ci.nsIDOMWindowUtils.AUTHOR_SHEET);
213         if (!this._loaded[manifestURL]) {
214           this._loaded[manifestURL] = { css: [], scripts: [] };
215         }
216         this._loaded[manifestURL].css.push({ window: aWindow, uri: uri });
217         aInjected.push(aCss);
218       } catch(e) {
219         log("Error loading stylesheet " + aCss + " : " + e);
220       }
221     });
223     let sandbox;
224     if (aItem.scripts.length > 0) {
225       sandbox = Cu.Sandbox([aWindow],
226                            { wantComponents: false,
227                              sandboxPrototype: aWindow });
228     }
230     // Load the scripts using a sandbox.
231     aItem.scripts.forEach(aScript => {
232       debug("Sandboxing " + aScript);
233       if (aInjected.indexOf(aScript) !== -1) {
234         debug("Skipping duplicated script: " + aScript);
235         return;
236       }
238       try {
239         Services.scriptloader.loadSubScript(aScript, sandbox, "UTF-8");
240         if (!this._loaded[manifestURL]) {
241           this._loaded[manifestURL] = { css: [], scripts: [] };
242         }
243         this._loaded[manifestURL].scripts.push({ sandbox: sandbox, uri: aScript });
244         aInjected.push(aScript);
245       } catch(e) {
246         log("Error sandboxing " + aScript + " : " + e);
247       }
248     });
250     // Makes sure we get rid of the sandbox.
251     if (sandbox) {
252       aWindow.addEventListener("unload", () => {
253         Cu.nukeSandbox(sandbox);
254         sandbox = null;
255       });
256     }
257   },
259   _injectInWindow: function(aWindow) {
260     debug("_injectInWindow");
262     if (!aWindow || !aWindow.document) {
263       return;
264     }
266     let principal = aWindow.document.nodePrincipal;
267     debug("principal status: " + principal.appStatus);
269     let href = aWindow.location.href;
271     // The list of resources loaded in this window, used to filter out
272     // duplicates.
273     let injected = [];
275     this._items.forEach((aItem) => {
276       // We only allow customizations to apply to apps with an equal or lower
277       // privilege level.
278       if (principal.appStatus > aItem.status) {
279         return;
280       }
282       let regexp = new RegExp(aItem.filter, "g");
283       if (regexp.test(href)) {
284         this._injectItem(aWindow, aItem, injected);
285         debug("Currently injected: " + injected.toString());
286       }
287     });
288   },
290   observe: function(aSubject, aTopic, aData) {
291     if (aTopic === "content-document-global-created") {
292       let window = aSubject.QueryInterface(Ci.nsIDOMWindow);
293       let href = window.location.href;
294       if (!href || href == "about:blank") {
295         return;
296       }
298       let id = window.QueryInterface(Ci.nsIInterfaceRequestor)
299                      .getInterface(Ci.nsIDOMWindowUtils)
300                      .currentInnerWindowID;
301       this._windows.set(id, window);
303       debug("document created: " + href);
304       this._injectInWindow(window);
305     } else if (aTopic === "inner-window-destroyed") {
306       let winId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data;
307       this._windows.delete(winId);
308     }
309   },
311   init: function() {
312     this._enabled = false;
313     try {
314       this._enabled = Services.prefs.getBoolPref("dom.apps.customization.enabled");
315     } catch(e) {}
317     if (!this._enabled) {
318       return;
319     }
321     this._windows = new Map(); // Can't be a WeakMap because we need to enumerate.
322     this._inParent = Cc["@mozilla.org/xre/runtime;1"]
323                        .getService(Ci.nsIXULRuntime)
324                        .processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
326     debug("init");
328     Services.obs.addObserver(this, "content-document-global-created",
329                              /* ownsWeak */ false);
330     Services.obs.addObserver(this, "inner-window-destroyed",
331                              /* ownsWeak */ false);
333     if (this._inParent) {
334       ppmm.addMessageListener("UserCustomizations:List", this);
335     } else {
336       cpmm.addMessageListener("UserCustomizations:Add", this);
337       cpmm.addMessageListener("UserCustomizations:Remove", this);
338       cpmm.addMessageListener("UserCustomizations:Unload", this);
339       cpmm.addMessageListener("UserCustomizations:UpdateWindows", this);
340       cpmm.sendAsyncMessage("UserCustomizations:List", {});
341     }
342   },
344   receiveMessage: function(aMessage) {
345     let name = aMessage.name;
346     let data = aMessage.data;
348     switch(name) {
349       case "UserCustomizations:List":
350         aMessage.target.sendAsyncMessage("UserCustomizations:Add", this._items);
351         break;
352       case "UserCustomizations:Add":
353         data.forEach(this._addItem, this);
354         break;
355       case "UserCustomizations:Remove":
356         this._removeItem(data);
357         break;
358       case "UserCustomizations:Unload":
359         this._unloadForManifestURL(data);
360         break;
361       case "UserCustomizations:UpdateWindows":
362         this._updateAllWindows();
363         break;
364     }
365   }
368 UserCustomizations.init();