diff --git a/b2g/app/b2g.js b/b2g/app/b2g.js
index 84dd8af4012d..87884cbcca1b 100644
--- a/b2g/app/b2g.js
+++ b/b2g/app/b2g.js
@@ -1037,6 +1037,9 @@ pref("browser.autofocus", false);
// Enable wakelock
pref("dom.wakelock.enabled", true);
+// Enable webapps add-ons
+pref("dom.apps.customization.enabled", true);
+
// Enable touch caret by default
pref("touchcaret.enabled", true);
diff --git a/b2g/config/dolphin/sources.xml b/b2g/config/dolphin/sources.xml
index 4f6fe1ea9a98..cd63f63f5c6d 100644
--- a/b2g/config/dolphin/sources.xml
+++ b/b2g/config/dolphin/sources.xml
@@ -15,7 +15,7 @@
-
+
@@ -23,7 +23,7 @@
-
+
diff --git a/b2g/config/emulator-ics/sources.xml b/b2g/config/emulator-ics/sources.xml
index e03290bf0871..8013c3dd8f40 100644
--- a/b2g/config/emulator-ics/sources.xml
+++ b/b2g/config/emulator-ics/sources.xml
@@ -19,13 +19,13 @@
-
+
-
+
diff --git a/b2g/config/emulator-jb/sources.xml b/b2g/config/emulator-jb/sources.xml
index 8c5dee5ecc7e..a75f0cab82ad 100644
--- a/b2g/config/emulator-jb/sources.xml
+++ b/b2g/config/emulator-jb/sources.xml
@@ -17,10 +17,10 @@
-
+
-
+
diff --git a/b2g/config/emulator-kk/sources.xml b/b2g/config/emulator-kk/sources.xml
index d56cf5683028..0d10fe413a1c 100644
--- a/b2g/config/emulator-kk/sources.xml
+++ b/b2g/config/emulator-kk/sources.xml
@@ -15,7 +15,7 @@
-
+
@@ -23,7 +23,7 @@
-
+
diff --git a/b2g/config/emulator/sources.xml b/b2g/config/emulator/sources.xml
index e03290bf0871..8013c3dd8f40 100644
--- a/b2g/config/emulator/sources.xml
+++ b/b2g/config/emulator/sources.xml
@@ -19,13 +19,13 @@
-
+
-
+
diff --git a/b2g/config/flame-kk/sources.xml b/b2g/config/flame-kk/sources.xml
index 454dad93e7b3..2119e71a6b5f 100644
--- a/b2g/config/flame-kk/sources.xml
+++ b/b2g/config/flame-kk/sources.xml
@@ -15,7 +15,7 @@
-
+
@@ -23,7 +23,7 @@
-
+
diff --git a/b2g/config/flame/sources.xml b/b2g/config/flame/sources.xml
index 99cf0ab39c7b..f63bb9f6a129 100644
--- a/b2g/config/flame/sources.xml
+++ b/b2g/config/flame/sources.xml
@@ -17,10 +17,10 @@
-
+
-
+
diff --git a/b2g/config/gaia.json b/b2g/config/gaia.json
index 8e1da4a0d689..b98ecf3e93f1 100644
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -4,6 +4,6 @@
"remote": "",
"branch": ""
},
- "revision": "99bea97a4bd9d6af1d7462e1abe15c8aaf4d7981",
+ "revision": "8175c3383310bf79cbfd01d36273620dede2a111",
"repo_path": "integration/gaia-central"
}
diff --git a/b2g/config/hamachi/sources.xml b/b2g/config/hamachi/sources.xml
index ea6349e53193..5d649a7303c3 100644
--- a/b2g/config/hamachi/sources.xml
+++ b/b2g/config/hamachi/sources.xml
@@ -17,11 +17,11 @@
-
+
-
+
diff --git a/b2g/config/helix/sources.xml b/b2g/config/helix/sources.xml
index 0fd8ff4c8aef..8c8101bf45b9 100644
--- a/b2g/config/helix/sources.xml
+++ b/b2g/config/helix/sources.xml
@@ -15,7 +15,7 @@
-
+
diff --git a/b2g/config/nexus-4/sources.xml b/b2g/config/nexus-4/sources.xml
index f6bede2140f7..84bff6533fe3 100644
--- a/b2g/config/nexus-4/sources.xml
+++ b/b2g/config/nexus-4/sources.xml
@@ -17,10 +17,10 @@
-
+
-
+
diff --git a/b2g/config/wasabi/sources.xml b/b2g/config/wasabi/sources.xml
index 6f8c7a49dc3a..26b8cdde71cc 100644
--- a/b2g/config/wasabi/sources.xml
+++ b/b2g/config/wasabi/sources.xml
@@ -17,12 +17,12 @@
-
+
-
+
diff --git a/dom/apps/AppsService.js b/dom/apps/AppsService.js
index d5558fdb3691..63483a58be63 100644
--- a/dom/apps/AppsService.js
+++ b/dom/apps/AppsService.js
@@ -15,6 +15,11 @@ const Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
+try {
+ if (Services.prefs.getBoolPref("dom.apps.customization.enabled")) {
+ Cu.import("resource://gre/modules/UserCustomizations.jsm");
+ }
+} catch(e) {}
const APPS_SERVICE_CID = Components.ID("{05072afa-92fe-45bf-ae22-39b69c117058}");
diff --git a/dom/apps/AppsUtils.jsm b/dom/apps/AppsUtils.jsm
index da801e4b2d2f..1c11dea0d04c 100644
--- a/dom/apps/AppsUtils.jsm
+++ b/dom/apps/AppsUtils.jsm
@@ -479,9 +479,12 @@ this.AppsUtils = {
return true;
},
+ allowUnsignedAddons: false, // for testing purposes.
+
/**
- * Checks if the app role is allowed.
+ * Checks if the app role is allowed:
* Only certified apps can be themes.
+ * Only privileged or certified apps can be addons.
* @param aRole : the role assigned to this app.
* @param aStatus : the APP_STATUS_* for this app.
*/
@@ -489,6 +492,12 @@ this.AppsUtils = {
if (aRole == "theme" && aStatus !== Ci.nsIPrincipal.APP_STATUS_CERTIFIED) {
return false;
}
+ if (!this.allowUnsignedAddons &&
+ (aRole == "addon" &&
+ aStatus !== Ci.nsIPrincipal.APP_STATUS_CERTIFIED &&
+ aStatus !== Ci.nsIPrincipal.APP_STATUS_PRIVILEGED)) {
+ return false;
+ }
return true;
},
@@ -718,6 +727,11 @@ this.AppsUtils = {
// Convert the binary hash data to a hex string.
return [toHexString(hash.charCodeAt(i)) for (i in hash)].join("");
+ },
+
+ // Returns the hash for a JS object.
+ computeObjectHash: function(aObject) {
+ return this.computeHash(JSON.stringify(aObject));
}
}
diff --git a/dom/apps/UserCustomizations.jsm b/dom/apps/UserCustomizations.jsm
new file mode 100644
index 000000000000..96e37fe85df8
--- /dev/null
+++ b/dom/apps/UserCustomizations.jsm
@@ -0,0 +1,368 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const Cu = Components.utils;
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+this.EXPORTED_SYMBOLS = ["UserCustomizations"];
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/AppsUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
+ "@mozilla.org/parentprocessmessagemanager;1",
+ "nsIMessageBroadcaster");
+
+XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
+ "@mozilla.org/childprocessmessagemanager;1",
+ "nsIMessageSender");
+
+XPCOMUtils.defineLazyServiceGetter(this, "console",
+ "@mozilla.org/consoleservice;1",
+ "nsIConsoleService");
+/**
+ * Customization scripts and CSS stylesheets can be specified in an
+ * application manifest with the following syntax:
+ * "customizations": [
+ * {
+ * "filter": "http://youtube.com",
+ * "css": ["file1.css", "file2.css"],
+ * "scripts": ["script1.js", "script2.js"]
+ * }
+ * ]
+ */
+
+let debug = Services.prefs.getBoolPref("dom.mozApps.debug")
+ ? (aMsg) => {
+ dump("-*-*- UserCustomizations (" +
+ (UserCustomizations._inParent ? "parent" : "child") +
+ "): " + aMsg + "\n");
+ }
+ : (aMsg) => {};
+
+function log(aStr) {
+ console.logStringMessage(aStr);
+}
+
+this.UserCustomizations = {
+ _items: [],
+ _loaded : {}, // Keep track per manifestURL of css and scripts loaded.
+ _windows: null, // Set of currently opened windows.
+ _enabled: false,
+
+ _addItem: function(aItem) {
+ debug("_addItem: " + uneval(aItem));
+ this._items.push(aItem);
+ if (this._inParent) {
+ ppmm.broadcastAsyncMessage("UserCustomizations:Add", [aItem]);
+ }
+ },
+
+ _removeItem: function(aHash) {
+ debug("_removeItem: " + aHash);
+ let index = -1;
+ this._items.forEach((script, pos) => {
+ if (script.hash == aHash ) {
+ index = pos;
+ }
+ });
+
+ if (index != -1) {
+ this._items.splice(index, 1);
+ }
+
+ if (this._inParent) {
+ ppmm.broadcastAsyncMessage("UserCustomizations:Remove", aHash);
+ }
+ },
+
+ register: function(aManifest, aApp) {
+ debug("Starting customization registration for " + aApp.manifestURL);
+
+ if (!this._enabled || !aApp.enabled || aApp.role != "addon") {
+ debug("Rejecting registration (global enabled=" + this._enabled +
+ ") (app role=" + aApp.role +
+ ", enabled=" + aApp.enabled + ")");
+ debug(uneval(aApp));
+ return;
+ }
+
+ let customizations = aManifest.customizations;
+ if (customizations === undefined || !Array.isArray(customizations)) {
+ return;
+ }
+
+ let base = Services.io.newURI(aApp.origin, null, null);
+
+ customizations.forEach(item => {
+ // The filter property is mandatory.
+ if (!item.filter || (typeof item.filter !== "string")) {
+ log("Mandatory filter property not found in this customization item: " +
+ uneval(item) + " in " + aApp.manifestURL);
+ return;
+ }
+
+ // Create a new object with resolved urls and a hash that we reuse to
+ // remove items.
+ let custom = {
+ filter: item.filter,
+ status: aApp.appStatus,
+ manifestURL: aApp.manifestURL,
+ css: [],
+ scripts: []
+ };
+ custom.hash = AppsUtils.computeObjectHash(item);
+
+ if (item.css && Array.isArray(item.css)) {
+ item.css.forEach((css) => {
+ custom.css.push(base.resolve(css));
+ });
+ }
+
+ if (item.scripts && Array.isArray(item.scripts)) {
+ item.scripts.forEach((script) => {
+ custom.scripts.push(base.resolve(script));
+ });
+ }
+
+ this._addItem(custom);
+ });
+ this._updateAllWindows();
+ },
+
+ _updateAllWindows: function() {
+ debug("UpdateWindows");
+ if (this._inParent) {
+ ppmm.broadcastAsyncMessage("UserCustomizations:UpdateWindows", {});
+ }
+ // Inject in all currently opened windows.
+ this._windows.forEach(this._injectInWindow.bind(this));
+ },
+
+ unregister: function(aManifest, aApp) {
+ if (!this._enabled) {
+ return;
+ }
+
+ debug("Starting customization unregistration for " + aApp.manifestURL);
+ let customizations = aManifest.customizations;
+ if (customizations === undefined || !Array.isArray(customizations)) {
+ return;
+ }
+
+ customizations.forEach(item => {
+ this._removeItem(AppsUtils.computeObjectHash(item));
+ });
+ this._unloadForManifestURL(aApp.manifestURL);
+ },
+
+ _unloadForManifestURL: function(aManifestURL) {
+ debug("_unloadForManifestURL " + aManifestURL);
+
+ if (this._inParent) {
+ ppmm.broadcastAsyncMessage("UserCustomizations:Unload", aManifestURL);
+ }
+
+ if (!this._loaded[aManifestURL]) {
+ return;
+ }
+
+ if (this._loaded[aManifestURL].scripts &&
+ this._loaded[aManifestURL].scripts.length > 0) {
+ // We can't rollback script changes, so don't even try to unload in this
+ // situation.
+ return;
+ }
+
+ this._loaded[aManifestURL].css.forEach(aItem => {
+ try {
+ debug("unloading " + aItem.uri.spec);
+ let utils = aItem.window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ utils.removeSheet(aItem.uri, Ci.nsIDOMWindowUtils.AUTHOR_SHEET);
+ } catch(e) {
+ log("Error unloading stylesheet " + aItem.uri.spec + " : " + e);
+ }
+ });
+
+ this._loaded[aManifestURL] = null;
+ },
+
+ _injectItem: function(aWindow, aItem, aInjected) {
+ debug("Injecting item " + uneval(aItem) + " in " + aWindow.location.href);
+ let utils = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+
+ let manifestURL = aItem.manifestURL;
+
+ // Load the stylesheets only in this window.
+ aItem.css.forEach(aCss => {
+ if (aInjected.indexOf(aCss) !== -1) {
+ debug("Skipping duplicated css: " + aCss);
+ return;
+ }
+
+ let uri = Services.io.newURI(aCss, null, null);
+ try {
+ utils.loadSheet(uri, Ci.nsIDOMWindowUtils.AUTHOR_SHEET);
+ if (!this._loaded[manifestURL]) {
+ this._loaded[manifestURL] = { css: [], scripts: [] };
+ }
+ this._loaded[manifestURL].css.push({ window: aWindow, uri: uri });
+ aInjected.push(aCss);
+ } catch(e) {
+ log("Error loading stylesheet " + aCss + " : " + e);
+ }
+ });
+
+ let sandbox;
+ if (aItem.scripts.length > 0) {
+ sandbox = Cu.Sandbox([aWindow],
+ { wantComponents: false,
+ sandboxPrototype: aWindow });
+ }
+
+ // Load the scripts using a sandbox.
+ aItem.scripts.forEach(aScript => {
+ debug("Sandboxing " + aScript);
+ if (aInjected.indexOf(aScript) !== -1) {
+ debug("Skipping duplicated script: " + aScript);
+ return;
+ }
+
+ try {
+ Services.scriptloader.loadSubScript(aScript, sandbox, "UTF-8");
+ if (!this._loaded[manifestURL]) {
+ this._loaded[manifestURL] = { css: [], scripts: [] };
+ }
+ this._loaded[manifestURL].scripts.push({ sandbox: sandbox, uri: aScript });
+ aInjected.push(aScript);
+ } catch(e) {
+ log("Error sandboxing " + aScript + " : " + e);
+ }
+ });
+
+ // Makes sure we get rid of the sandbox.
+ if (sandbox) {
+ aWindow.addEventListener("unload", () => {
+ Cu.nukeSandbox(sandbox);
+ sandbox = null;
+ });
+ }
+ },
+
+ _injectInWindow: function(aWindow) {
+ debug("_injectInWindow");
+
+ if (!aWindow || !aWindow.document) {
+ return;
+ }
+
+ let principal = aWindow.document.nodePrincipal;
+ debug("principal status: " + principal.appStatus);
+
+ let href = aWindow.location.href;
+
+ // The list of resources loaded in this window, used to filter out
+ // duplicates.
+ let injected = [];
+
+ this._items.forEach((aItem) => {
+ // We only allow customizations to apply to apps with an equal or lower
+ // privilege level.
+ if (principal.appStatus > aItem.status) {
+ return;
+ }
+
+ let regexp = new RegExp(aItem.filter, "g");
+ if (regexp.test(href)) {
+ this._injectItem(aWindow, aItem, injected);
+ debug("Currently injected: " + injected.toString());
+ }
+ });
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic === "content-document-global-created") {
+ let window = aSubject.QueryInterface(Ci.nsIDOMWindow);
+ let href = window.location.href;
+ if (!href || href == "about:blank") {
+ return;
+ }
+
+ let id = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .currentInnerWindowID;
+ this._windows.set(id, window);
+
+ debug("document created: " + href);
+ this._injectInWindow(window);
+ } else if (aTopic === "inner-window-destroyed") {
+ let winId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data;
+ this._windows.delete(winId);
+ }
+ },
+
+ init: function() {
+ this._enabled = false;
+ try {
+ this._enabled = Services.prefs.getBoolPref("dom.apps.customization.enabled");
+ } catch(e) {}
+
+ if (!this._enabled) {
+ return;
+ }
+
+ this._windows = new Map(); // Can't be a WeakMap because we need to enumerate.
+ this._inParent = Cc["@mozilla.org/xre/runtime;1"]
+ .getService(Ci.nsIXULRuntime)
+ .processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+
+ debug("init");
+
+ Services.obs.addObserver(this, "content-document-global-created",
+ /* ownsWeak */ false);
+ Services.obs.addObserver(this, "inner-window-destroyed",
+ /* ownsWeak */ false);
+
+ if (this._inParent) {
+ ppmm.addMessageListener("UserCustomizations:List", this);
+ } else {
+ cpmm.addMessageListener("UserCustomizations:Add", this);
+ cpmm.addMessageListener("UserCustomizations:Remove", this);
+ cpmm.addMessageListener("UserCustomizations:Unload", this);
+ cpmm.addMessageListener("UserCustomizations:UpdateWindows", this);
+ cpmm.sendAsyncMessage("UserCustomizations:List", {});
+ }
+ },
+
+ receiveMessage: function(aMessage) {
+ let name = aMessage.name;
+ let data = aMessage.data;
+
+ switch(name) {
+ case "UserCustomizations:List":
+ aMessage.target.sendAsyncMessage("UserCustomizations:Add", this._items);
+ break;
+ case "UserCustomizations:Add":
+ data.forEach(this._addItem, this);
+ break;
+ case "UserCustomizations:Remove":
+ this._removeItem(data);
+ break;
+ case "UserCustomizations:Unload":
+ this._unloadForManifestURL(data);
+ break;
+ case "UserCustomizations:UpdateWindows":
+ this._updateAllWindows();
+ break;
+ }
+ }
+}
+
+UserCustomizations.init();
diff --git a/dom/apps/Webapps.jsm b/dom/apps/Webapps.jsm
index 0623ce4de056..40d49008895c 100755
--- a/dom/apps/Webapps.jsm
+++ b/dom/apps/Webapps.jsm
@@ -43,6 +43,23 @@ Cu.import("resource://gre/modules/osfile.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyGetter(this, "UserCustomizations", function() {
+ let enabled = false;
+ try {
+ enabled = Services.prefs.getBoolPref("dom.apps.customization.enabled");
+ } catch(e) {}
+
+ if (enabled) {
+ return Cu.import("resource://gre/modules/UserCustomizations.jsm", {})
+ .UserCustomizations;
+ } else {
+ return {
+ register: function() {},
+ unregister: function() {}
+ };
+ }
+});
+
XPCOMUtils.defineLazyModuleGetter(this, "TrustedRootCertificate",
"resource://gre/modules/StoreTrustAnchor.jsm");
@@ -406,6 +423,7 @@ this.DOMApplicationRegistry = {
app.redirects = this.sanitizeRedirects(aResult.redirects);
}
app.kind = this.appKind(app, aResult.manifest);
+ UserCustomizations.register(aResult.manifest, app);
});
// Nothing else to do but notifying we're ready.
@@ -1097,6 +1115,7 @@ this.DOMApplicationRegistry = {
this._registerSystemMessages(manifest, app);
this._registerInterAppConnections(manifest, app);
appsToRegister.push({ manifest: manifest, app: app });
+ UserCustomizations.register(manifest, app);
});
this._safeToClone.resolve();
this._registerActivitiesForApps(appsToRegister, aRunUpdate);
@@ -1991,7 +2010,8 @@ this.DOMApplicationRegistry = {
// Updates the redirect mapping, activities and system message handlers.
// aOldManifest can be null if we don't have any handler to unregister.
updateAppHandlers: function(aOldManifest, aNewManifest, aApp) {
- debug("updateAppHandlers: old=" + aOldManifest + " new=" + aNewManifest);
+ debug("updateAppHandlers: old=" + uneval(aOldManifest) +
+ " new=" + uneval(aNewManifest));
this.notifyAppsRegistryStart();
if (aApp.appStatus >= Ci.nsIPrincipal.APP_STATUS_PRIVILEGED) {
aApp.redirects = this.sanitizeRedirects(aNewManifest.redirects);
@@ -2001,6 +2021,8 @@ this.DOMApplicationRegistry = {
new ManifestHelper(aNewManifest, aApp.origin, aApp.manifestURL);
this._saveWidgetsFullPath(manifest, aApp);
+ aApp.role = manifest.role ? manifest.role : "";
+
if (supportSystemMessages()) {
if (aOldManifest) {
this._unregisterActivities(aOldManifest, aApp);
@@ -2012,6 +2034,12 @@ this.DOMApplicationRegistry = {
// Nothing else to do but notifying we're ready.
this.notifyAppsRegistryReady();
}
+
+ // Update user customizations.
+ if (aOldManifest) {
+ UserCustomizations.unregister(aOldManifest, aApp);
+ }
+ UserCustomizations.register(aNewManifest, aApp);
},
checkForUpdate: function(aData, aMm) {
@@ -4019,6 +4047,7 @@ this.DOMApplicationRegistry = {
if (supportSystemMessages()) {
this._unregisterActivities(aApp.manifest, aApp);
}
+ UserCustomizations.unregister(aApp.manifest, aApp);
let dir = this._getAppDir(id);
try {
@@ -4388,6 +4417,12 @@ this.DOMApplicationRegistry = {
});
this.broadcastMessage("Webapps:SetEnabled:Return", app);
});
+
+ // Update customization.
+ this.getManifestFor(app.manifestURL).then((aManifest) => {
+ app.enabled ? UserCustomizations.register(aManifest, app)
+ : UserCustomizations.unregister(aManifest, app);
+ });
},
getManifestFor: function(aManifestURL) {
diff --git a/dom/apps/moz.build b/dom/apps/moz.build
index f4b68c4cbd64..925e7b5a7304 100644
--- a/dom/apps/moz.build
+++ b/dom/apps/moz.build
@@ -38,6 +38,7 @@ EXTRA_JS_MODULES += [
'PermissionsInstaller.jsm',
'PermissionsTable.jsm',
'StoreTrustAnchor.jsm',
+ 'UserCustomizations.jsm',
]
EXTRA_PP_JS_MODULES += [
diff --git a/dom/apps/tests/addons/application.zip b/dom/apps/tests/addons/application.zip
new file mode 100644
index 000000000000..543587c8869b
Binary files /dev/null and b/dom/apps/tests/addons/application.zip differ
diff --git a/dom/apps/tests/addons/index.html b/dom/apps/tests/addons/index.html
new file mode 100644
index 000000000000..b8f4c54eb3a7
--- /dev/null
+++ b/dom/apps/tests/addons/index.html
@@ -0,0 +1,22 @@
+
+
+
+ This page will be modified by the add-on
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dom/apps/tests/addons/manifest.webapp b/dom/apps/tests/addons/manifest.webapp
new file mode 100644
index 000000000000..3d9e095e3aae
--- /dev/null
+++ b/dom/apps/tests/addons/manifest.webapp
@@ -0,0 +1,12 @@
+{
+ "name": "Addon app",
+ "description": "Let me inject script and css!",
+ "customizations" : [
+ {
+ "filter": "http://mochi.test:8888/tests/dom/apps/tests/addons",
+ "css": ["style.css", "style2.css", "invalid.css", "style.css"],
+ "scripts": ["script.js", "script2.js", "invalid.js", "script.js"]
+ }
+ ],
+ "role": "addon"
+}
diff --git a/dom/apps/tests/addons/script.js b/dom/apps/tests/addons/script.js
new file mode 100644
index 000000000000..aac734710fb3
--- /dev/null
+++ b/dom/apps/tests/addons/script.js
@@ -0,0 +1,4 @@
+document.addEventListener("DOMContentLoaded", function() {
+ var head = document.getElementById("header");
+ head.innerHTML = "Hello World!";
+}, false);
\ No newline at end of file
diff --git a/dom/apps/tests/addons/style.css b/dom/apps/tests/addons/style.css
new file mode 100644
index 000000000000..4e395e6f31cb
--- /dev/null
+++ b/dom/apps/tests/addons/style.css
@@ -0,0 +1,3 @@
+#header {
+ color: red;
+}
\ No newline at end of file
diff --git a/dom/apps/tests/addons/update.webapp b/dom/apps/tests/addons/update.webapp
new file mode 100644
index 000000000000..e7df00358291
--- /dev/null
+++ b/dom/apps/tests/addons/update.webapp
@@ -0,0 +1,5 @@
+{
+ "name": "Addon app",
+ "description": "Let me inject script and css!",
+ "package_path" : "application.zip"
+}
diff --git a/dom/apps/tests/addons/update.webapp^headers^ b/dom/apps/tests/addons/update.webapp^headers^
new file mode 100644
index 000000000000..3cea33fec8b6
--- /dev/null
+++ b/dom/apps/tests/addons/update.webapp^headers^
@@ -0,0 +1 @@
+Content-Type: application/manifest+json
\ No newline at end of file
diff --git a/dom/apps/tests/mochitest.ini b/dom/apps/tests/mochitest.ini
index 1c90136f1096..798a32bd9617 100644
--- a/dom/apps/tests/mochitest.ini
+++ b/dom/apps/tests/mochitest.ini
@@ -1,6 +1,10 @@
[DEFAULT]
skip-if = e10s
support-files =
+ addons/application.zip
+ addons/update.webapp
+ addons/update.webapp^headers^
+ addons/index.html
chromeAddCert.js
file_app.sjs
file_app.template.html
@@ -25,6 +29,8 @@ support-files =
marketplace/*
pkg_install_iframe.html
+[test_app_addons.html]
+skip-if = os == "android" || toolkit == "gonk" # embed-apps doesn't work in mochitest app
[test_app_enabled.html]
[test_app_update.html]
skip-if = os == "android" || toolkit == "gonk" # embed-apps doesn't work in mochitest app
diff --git a/dom/apps/tests/test_app_addons.html b/dom/apps/tests/test_app_addons.html
new file mode 100644
index 000000000000..689d49d7cecb
--- /dev/null
+++ b/dom/apps/tests/test_app_addons.html
@@ -0,0 +1,186 @@
+
+
+
+
+ Test for Bug 923897 - Test apps as addons
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dom/ipc/preload.js b/dom/ipc/preload.js
index 891635e56e46..194b212cee33 100644
--- a/dom/ipc/preload.js
+++ b/dom/ipc/preload.js
@@ -27,6 +27,11 @@ const BrowserElementIsPreloaded = true;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/SettingsDB.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+ try {
+ if (Services.prefs.getBoolPref("dom.apps.customization.enabled")) {
+ Cu.import("resource://gre/modules/UserCustomizations.jsm");
+ }
+ } catch(e) {}
Cc["@mozilla.org/appshell/appShellService;1"].getService(Ci["nsIAppShellService"]);
Cc["@mozilla.org/appshell/window-mediator;1"].getService(Ci["nsIWindowMediator"]);
diff --git a/dom/system/gonk/RadioInterfaceLayer.js b/dom/system/gonk/RadioInterfaceLayer.js
index 4371f869abab..c256f8983cb7 100644
--- a/dom/system/gonk/RadioInterfaceLayer.js
+++ b/dom/system/gonk/RadioInterfaceLayer.js
@@ -494,19 +494,13 @@ XPCOMUtils.defineLazyGetter(this, "gRadioEnabledController", function() {
_getNumCards: function() {
let numCards = 0;
for (let i = 0, N = _ril.numRadioInterfaces; i < N; ++i) {
- if (this._isCardPresentAtClient(i)) {
+ if (_ril.getRadioInterface(i).isCardPresent()) {
numCards++;
}
}
return numCards;
},
- _isCardPresentAtClient: function(clientId) {
- let cardState = _ril.getRadioInterface(clientId).rilContext.cardState;
- return cardState !== Ci.nsIIccProvider.CARD_STATE_UNDETECTED &&
- cardState !== Ci.nsIIccProvider.CARD_STATE_UNKNOWN;
- },
-
_isRadioAbleToEnableAtClient: function(clientId, numCards) {
if (!RILQUIRKS_RADIO_OFF_WO_CARD) {
return true;
@@ -516,7 +510,7 @@ XPCOMUtils.defineLazyGetter(this, "gRadioEnabledController", function() {
// 1. a SIM card is presented or
// 2. it is the default clientId and there is no any SIM card at any client.
- if (this._isCardPresentAtClient(clientId)) {
+ if (_ril.getRadioInterface(clientId).isCardPresent()) {
return true;
}
@@ -1511,12 +1505,15 @@ RadioInterfaceLayer.prototype = {
},
getClientIdForEmergencyCall: function() {
+ // Select the client with sim card first.
for (let cid = 0; cid < this.numRadioInterfaces; ++cid) {
- if (gRadioEnabledController._isRadioAbleToEnableAtClient(cid)) {
+ if (this.getRadioInterface(cid).isCardPresent()) {
return cid;
}
}
- return -1;
+
+ // Use the defualt client if no card presents.
+ return HW_DEFAULT_CLIENT_ID;
},
setMicrophoneMuted: function(muted) {
@@ -1824,6 +1821,12 @@ RadioInterface.prototype = {
return false;
},
+ isCardPresent: function() {
+ let cardState = this.rilContext.cardState;
+ return cardState !== Ci.nsIIccProvider.CARD_STATE_UNDETECTED &&
+ cardState !== Ci.nsIIccProvider.CARD_STATE_UNKNOWN;
+ },
+
/**
* Process a message from the content process.
*/
diff --git a/dom/telephony/Telephony.cpp b/dom/telephony/Telephony.cpp
index 47863ae828fe..8d05798c4b8c 100644
--- a/dom/telephony/Telephony.cpp
+++ b/dom/telephony/Telephony.cpp
@@ -62,27 +62,17 @@ public:
}
};
-class Telephony::EnumerationAck : public nsRunnable
-{
- nsRefPtr mTelephony;
-
-public:
- explicit EnumerationAck(Telephony* aTelephony)
- : mTelephony(aTelephony)
- {
- MOZ_ASSERT(mTelephony);
- }
-
- NS_IMETHOD Run()
- {
- mTelephony->NotifyEvent(NS_LITERAL_STRING("ready"));
- return NS_OK;
- }
-};
-
Telephony::Telephony(nsPIDOMWindow* aOwner)
- : DOMEventTargetHelper(aOwner), mEnumerated(false)
+ : DOMEventTargetHelper(aOwner)
{
+ nsCOMPtr global = do_QueryInterface(aOwner);
+ MOZ_ASSERT(global);
+
+ ErrorResult rv;
+ nsRefPtr promise = Promise::Create(global, rv);
+ MOZ_ASSERT(!rv.Failed());
+
+ mReadyPromise = promise;
}
Telephony::~Telephony()
@@ -327,6 +317,7 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(Telephony,
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCalls)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCallsList)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGroup)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mReadyPromise)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(Telephony,
@@ -335,6 +326,7 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(Telephony,
NS_IMPL_CYCLE_COLLECTION_UNLINK(mCalls)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mCallsList)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mGroup)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mReadyPromise)
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(Telephony)
@@ -462,14 +454,16 @@ Telephony::ConferenceGroup() const
return group.forget();
}
-// EventTarget
-
-void
-Telephony::EventListenerAdded(nsIAtom* aType)
+already_AddRefed
+Telephony::GetReady(ErrorResult& aRv) const
{
- if (aType == nsGkAtoms::onready) {
- EnqueueEnumerationAck();
+ if (!mReadyPromise) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
}
+
+ nsRefPtr promise = mReadyPromise;
+ return promise.forget();
}
// nsITelephonyListener
@@ -548,8 +542,6 @@ Telephony::ConferenceCallStateChanged(uint16_t aCallState)
NS_IMETHODIMP
Telephony::EnumerateCallStateComplete()
{
- MOZ_ASSERT(!mEnumerated);
-
// Set conference state.
if (mGroup->CallsArray().Length() >= 2) {
const nsTArray > &calls = mGroup->CallsArray();
@@ -565,10 +557,8 @@ Telephony::EnumerateCallStateComplete()
mGroup->ChangeState(callState);
}
- mEnumerated = true;
-
- if (NS_FAILED(NotifyEvent(NS_LITERAL_STRING("ready")))) {
- NS_WARNING("Failed to notify ready!");
+ if (mReadyPromise) {
+ mReadyPromise->MaybeResolve(JS::UndefinedHandleValue);
}
if (NS_FAILED(mService->RegisterListener(mListener))) {
@@ -693,19 +683,6 @@ Telephony::DispatchCallEvent(const nsAString& aType,
return DispatchTrustedEvent(event);
}
-void
-Telephony::EnqueueEnumerationAck()
-{
- if (!mEnumerated) {
- return;
- }
-
- nsCOMPtr task = new EnumerationAck(this);
- if (NS_FAILED(NS_DispatchToCurrentThread(task))) {
- NS_WARNING("Failed to dispatch to current thread!");
- }
-}
-
already_AddRefed
NS_CreateTelephonyService()
{
diff --git a/dom/telephony/Telephony.h b/dom/telephony/Telephony.h
index 98ac27bfd131..c800bd1426d7 100644
--- a/dom/telephony/Telephony.h
+++ b/dom/telephony/Telephony.h
@@ -40,9 +40,7 @@ class Telephony MOZ_FINAL : public DOMEventTargetHelper,
* also bug 775997 comment #51.
*/
class Listener;
- class EnumerationAck;
- friend class EnumerationAck;
friend class telephony::TelephonyDialCallback;
nsCOMPtr mService;
@@ -53,7 +51,7 @@ class Telephony MOZ_FINAL : public DOMEventTargetHelper,
nsRefPtr mGroup;
- bool mEnumerated;
+ nsRefPtr mReadyPromise;
public:
NS_DECL_ISUPPORTS_INHERITED
@@ -109,7 +107,9 @@ public:
already_AddRefed
ConferenceGroup() const;
- IMPL_EVENT_HANDLER(ready)
+ already_AddRefed
+ GetReady(ErrorResult& aRv) const;
+
IMPL_EVENT_HANDLER(incoming)
IMPL_EVENT_HANDLER(callschanged)
IMPL_EVENT_HANDLER(remoteheld)
@@ -146,8 +146,6 @@ public:
return mCalls;
}
- virtual void EventListenerAdded(nsIAtom* aType) MOZ_OVERRIDE;
-
private:
explicit Telephony(nsPIDOMWindow* aOwner);
~Telephony();
@@ -198,9 +196,6 @@ private:
nsresult
DispatchCallEvent(const nsAString& aType, TelephonyCall* aCall);
- void
- EnqueueEnumerationAck();
-
already_AddRefed
GetCall(uint32_t aServiceId, uint32_t aCallIndex);
diff --git a/dom/telephony/test/marionette/test_ready.js b/dom/telephony/test/marionette/test_ready.js
index f73347971b69..2e0cfb49d51a 100644
--- a/dom/telephony/test/marionette/test_ready.js
+++ b/dom/telephony/test/marionette/test_ready.js
@@ -13,10 +13,10 @@ function cleanUp() {
let telephony = window.navigator.mozTelephony;
ok(telephony);
-telephony.onready = function() {
- log("Receive 'ready' event");
+telephony.ready.then(function() {
+ log("Telephony got ready");
- // Test registering 'ready' event in another window.
+ // Test telephony.ready in another window.
let iframe = document.createElement("iframe");
iframe.addEventListener("load", function load() {
iframe.removeEventListener("load", load);
@@ -24,12 +24,12 @@ telephony.onready = function() {
let iframeTelephony = iframe.contentWindow.navigator.mozTelephony;
ok(iframeTelephony);
- iframeTelephony.onready = function() {
- log("Receive 'ready' event in iframe");
+ iframeTelephony.ready.then(function() {
+ log("Telephony in iframe got ready");
cleanUp();
- };
+ });
});
document.body.appendChild(iframe);
-};
+});
diff --git a/dom/webidl/Telephony.webidl b/dom/webidl/Telephony.webidl
index d0fc9eb4755c..a2c79cef072d 100644
--- a/dom/webidl/Telephony.webidl
+++ b/dom/webidl/Telephony.webidl
@@ -46,8 +46,9 @@ interface Telephony : EventTarget {
readonly attribute CallsList calls;
readonly attribute TelephonyCallGroup conferenceGroup;
- // The 'ready' event will be fired when the telephony object is ready.
- attribute EventHandler onready;
+ // Async notification that object initialization is done.
+ [Throws]
+ readonly attribute Promise ready;
attribute EventHandler onincoming;
attribute EventHandler oncallschanged;
diff --git a/hal/gonk/GonkHal.cpp b/hal/gonk/GonkHal.cpp
index f366d3d662c5..69b40766f289 100644
--- a/hal/gonk/GonkHal.cpp
+++ b/hal/gonk/GonkHal.cpp
@@ -27,7 +27,11 @@
#include
#include
#include
+#if ANDROID_VERSION >= 21
+#include
+#else
#include
+#endif
#include "mozilla/DebugOnly.h"
diff --git a/js/xpconnect/loader/mozJSSubScriptLoader.cpp b/js/xpconnect/loader/mozJSSubScriptLoader.cpp
index 48d1a3efdfde..ab9e4e0024c8 100644
--- a/js/xpconnect/loader/mozJSSubScriptLoader.cpp
+++ b/js/xpconnect/loader/mozJSSubScriptLoader.cpp
@@ -346,7 +346,7 @@ mozJSSubScriptLoader::DoLoadSubScriptWithOptions(const nsAString &url,
return ReportError(cx, LOAD_ERROR_NOSCHEME, uri);
}
- if (!scheme.EqualsLiteral("chrome")) {
+ if (!scheme.EqualsLiteral("chrome") && !scheme.EqualsLiteral("app")) {
// This might be a URI to a local file, though!
nsCOMPtr innerURI = NS_GetInnermostURI(uri);
nsCOMPtr fileURL = do_QueryInterface(innerURI);
diff --git a/testing/profiles/prefs_general.js b/testing/profiles/prefs_general.js
index b6495975310b..3e3080ced0bc 100644
--- a/testing/profiles/prefs_general.js
+++ b/testing/profiles/prefs_general.js
@@ -251,6 +251,8 @@ user_pref("browser.aboutHomeSnippets.updateUrl", "nonexistent://test");
// Enable debug logging in the mozApps implementation.
user_pref("dom.mozApps.debug", true);
+// Enable apps customizations
+user_pref("dom.apps.customization.enabled", true);
// Don't fetch or send directory tiles data from real servers
user_pref("browser.newtabpage.directory.source", 'data:application/json,{"testing":1}');
diff --git a/testing/specialpowers/content/SpecialPowersObserverAPI.js b/testing/specialpowers/content/SpecialPowersObserverAPI.js
index 3ea7fba54120..4cdb2402510e 100644
--- a/testing/specialpowers/content/SpecialPowersObserverAPI.js
+++ b/testing/specialpowers/content/SpecialPowersObserverAPI.js
@@ -353,6 +353,13 @@ SpecialPowersObserverAPI.prototype = {
let val = Webapps.DOMApplicationRegistry.allAppsLaunchable;
Webapps.DOMApplicationRegistry.allAppsLaunchable = aMessage.json.launchable;
return val;
+ case "allow-unsigned-addons":
+ {
+ let utils = {};
+ Components.utils.import("resource://gre/modules/AppsUtils.jsm", utils);
+ utils.AppsUtils.allowUnsignedAddons = true;
+ return;
+ }
default:
throw new SpecialPowersException("Invalid operation for SPWebAppsService");
}
diff --git a/testing/specialpowers/content/specialpowersAPI.js b/testing/specialpowers/content/specialpowersAPI.js
index 0687eb99a41a..8d6c6b611841 100644
--- a/testing/specialpowers/content/specialpowersAPI.js
+++ b/testing/specialpowers/content/specialpowersAPI.js
@@ -1099,6 +1099,13 @@ SpecialPowersAPI.prototype = {
});
},
+ // Allow tests to install addons without signing the package, for convenience.
+ allowUnsignedAddons: function() {
+ this._sendSyncMessage("SPWebAppService", {
+ op: "allow-unsigned-addons"
+ });
+ },
+
// Restore the launchable property to its default value.
flushAllAppsLaunchable: function() {
this._sendSyncMessage("SPWebAppService", {