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 + + + +

Lorem ipsum

+

Uncustomized content

+ + \ 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", {