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/. */
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",
25 XPCOMUtils.defineLazyServiceGetter(this, "console",
26 "@mozilla.org/consoleservice;1",
29 * Customization scripts and CSS stylesheets can be specified in an
30 * application manifest with the following syntax:
33 * "filter": "http://youtube.com",
34 * "css": ["file1.css", "file2.css"],
35 * "scripts": ["script1.js", "script2.js"]
40 let debug = Services.prefs.getBoolPref("dom.mozApps.debug")
42 dump("-*-*- UserCustomizations (" +
43 (UserCustomizations._inParent ? "parent" : "child") +
49 console.logStringMessage(aStr);
52 this.UserCustomizations = {
54 _loaded : {}, // Keep track per manifestURL of css and scripts loaded.
55 _windows: null, // Set of currently opened windows.
58 _addItem: function(aItem) {
59 debug("_addItem: " + uneval(aItem));
60 this._items.push(aItem);
62 ppmm.broadcastAsyncMessage("UserCustomizations:Add", [aItem]);
66 _removeItem: function(aHash) {
67 debug("_removeItem: " + aHash);
69 this._items.forEach((script, pos) => {
70 if (script.hash == aHash ) {
76 this._items.splice(index, 1);
80 ppmm.broadcastAsyncMessage("UserCustomizations:Remove", aHash);
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 + ")");
95 let customizations = aManifest.customizations;
96 if (customizations === undefined || !Array.isArray(customizations)) {
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);
110 // Create a new object with resolved urls and a hash that we reuse to
114 status: aApp.appStatus,
115 manifestURL: aApp.manifestURL,
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));
127 if (item.scripts && Array.isArray(item.scripts)) {
128 item.scripts.forEach((script) => {
129 custom.scripts.push(base.resolve(script));
133 this._addItem(custom);
135 this._updateAllWindows();
138 _updateAllWindows: function() {
139 debug("UpdateWindows");
140 if (this._inParent) {
141 ppmm.broadcastAsyncMessage("UserCustomizations:UpdateWindows", {});
143 // Inject in all currently opened windows.
144 this._windows.forEach(this._injectInWindow.bind(this));
147 unregister: function(aManifest, aApp) {
148 if (!this._enabled) {
152 debug("Starting customization unregistration for " + aApp.manifestURL);
153 let customizations = aManifest.customizations;
154 if (customizations === undefined || !Array.isArray(customizations)) {
158 customizations.forEach(item => {
159 this._removeItem(AppsUtils.computeObjectHash(item));
161 this._unloadForManifestURL(aApp.manifestURL);
164 _unloadForManifestURL: function(aManifestURL) {
165 debug("_unloadForManifestURL " + aManifestURL);
167 if (this._inParent) {
168 ppmm.broadcastAsyncMessage("UserCustomizations:Unload", aManifestURL);
171 if (!this._loaded[aManifestURL]) {
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
182 this._loaded[aManifestURL].css.forEach(aItem => {
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);
189 log("Error unloading stylesheet " + aItem.uri.spec + " : " + e);
193 this._loaded[aManifestURL] = null;
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);
210 let uri = Services.io.newURI(aCss, null, null);
212 utils.loadSheet(uri, Ci.nsIDOMWindowUtils.AUTHOR_SHEET);
213 if (!this._loaded[manifestURL]) {
214 this._loaded[manifestURL] = { css: [], scripts: [] };
216 this._loaded[manifestURL].css.push({ window: aWindow, uri: uri });
217 aInjected.push(aCss);
219 log("Error loading stylesheet " + aCss + " : " + e);
224 if (aItem.scripts.length > 0) {
225 sandbox = Cu.Sandbox([aWindow],
226 { wantComponents: false,
227 sandboxPrototype: aWindow });
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);
239 Services.scriptloader.loadSubScript(aScript, sandbox, "UTF-8");
240 if (!this._loaded[manifestURL]) {
241 this._loaded[manifestURL] = { css: [], scripts: [] };
243 this._loaded[manifestURL].scripts.push({ sandbox: sandbox, uri: aScript });
244 aInjected.push(aScript);
246 log("Error sandboxing " + aScript + " : " + e);
250 // Makes sure we get rid of the sandbox.
252 aWindow.addEventListener("unload", () => {
253 Cu.nukeSandbox(sandbox);
259 _injectInWindow: function(aWindow) {
260 debug("_injectInWindow");
262 if (!aWindow || !aWindow.document) {
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
275 this._items.forEach((aItem) => {
276 // We only allow customizations to apply to apps with an equal or lower
278 if (principal.appStatus > aItem.status) {
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());
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") {
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);
312 this._enabled = false;
314 this._enabled = Services.prefs.getBoolPref("dom.apps.customization.enabled");
317 if (!this._enabled) {
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;
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);
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", {});
344 receiveMessage: function(aMessage) {
345 let name = aMessage.name;
346 let data = aMessage.data;
349 case "UserCustomizations:List":
350 aMessage.target.sendAsyncMessage("UserCustomizations:Add", this._items);
352 case "UserCustomizations:Add":
353 data.forEach(this._addItem, this);
355 case "UserCustomizations:Remove":
356 this._removeItem(data);
358 case "UserCustomizations:Unload":
359 this._unloadForManifestURL(data);
361 case "UserCustomizations:UpdateWindows":
362 this._updateAllWindows();
368 UserCustomizations.init();