diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in index ffe8c060df8..3a5598d8226 100644 --- a/browser/installer/package-manifest.in +++ b/browser/installer/package-manifest.in @@ -474,6 +474,9 @@ @BINPATH@/components/Webapps.manifest @BINPATH@/components/AppsService.js @BINPATH@/components/AppsService.manifest +@BINPATH@/components/nsDOMIdentity.js +@BINPATH@/components/nsIDService.js +@BINPATH@/components/Identity.manifest @BINPATH@/components/ContactManager.js @BINPATH@/components/ContactManager.manifest diff --git a/dom/Makefile.in b/dom/Makefile.in index e8955b4ef4e..6a17c46f560 100644 --- a/dom/Makefile.in +++ b/dom/Makefile.in @@ -67,6 +67,7 @@ DIRS += \ indexedDB \ system \ ipc \ + identity \ workers \ $(NULL) diff --git a/dom/identity/DOMIdentity.jsm b/dom/identity/DOMIdentity.jsm new file mode 100644 index 00000000000..99d1da5ae25 --- /dev/null +++ b/dom/identity/DOMIdentity.jsm @@ -0,0 +1,266 @@ +/* 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 {classes: Cc, interfaces: Ci, utils: Cu} = Components; + +// This is the parent process corresponding to nsDOMIdentity. +let EXPORTED_SYMBOLS = ["DOMIdentity"]; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "IdentityService", + "resource://gre/modules/identity/Identity.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, + "Logger", + "resource://gre/modules/identity/LogUtils.jsm"); + +function log(...aMessageArgs) { + Logger.log(["DOMIdentity"].concat(aMessageArgs)); +} + +function IDDOMMessage(aID) { + this.id = aID; +} + +function IDPProvisioningContext(aID, aOrigin, aTargetMM) { + this._id = aID; + this._origin = aOrigin; + this._mm = aTargetMM; +} + +IDPProvisioningContext.prototype = { + get id() this._id, + get origin() this._origin, + + doBeginProvisioningCallback: function IDPPC_doBeginProvCB(aID, aCertDuration) { + let message = new IDDOMMessage(this.id); + message.identity = aID; + message.certDuration = aCertDuration; + this._mm.sendAsyncMessage("Identity:IDP:CallBeginProvisioningCallback", + message); + }, + + doGenKeyPairCallback: function IDPPC_doGenKeyPairCallback(aPublicKey) { + log("doGenKeyPairCallback"); + let message = new IDDOMMessage(this.id); + message.publicKey = aPublicKey; + this._mm.sendAsyncMessage("Identity:IDP:CallGenKeyPairCallback", message); + }, + + doError: function(msg) { + log("Provisioning ERROR: " + msg); + }, +}; + +function IDPAuthenticationContext(aID, aOrigin, aTargetMM) { + this._id = aID; + this._origin = aOrigin; + this._mm = aTargetMM; +} + +IDPAuthenticationContext.prototype = { + get id() this._id, + get origin() this._origin, + + doBeginAuthenticationCallback: function IDPAC_doBeginAuthCB(aIdentity) { + let message = new IDDOMMessage(this.id); + message.identity = aIdentity; + this._mm.sendAsyncMessage("Identity:IDP:CallBeginAuthenticationCallback", + message); + }, + + doError: function IDPAC_doError(msg) { + log("Authentication ERROR: " + msg); + }, +}; + +function RPWatchContext(aID, aOrigin, aLoggedInEmail, aTargetMM) { + this._id = aID; + this._origin = aOrigin; + this._loggedInEmail = aLoggedInEmail; + this._mm = aTargetMM; +} + +RPWatchContext.prototype = { + get id() this._id, + get origin() this._origin, + get loggedInEmail() this._loggedInEmail, + + doLogin: function RPWatchContext_onlogin(aAssertion) { + log("doLogin: " + this.id); + let message = new IDDOMMessage(this.id); + message.assertion = aAssertion; + this._mm.sendAsyncMessage("Identity:RP:Watch:OnLogin", message); + }, + + doLogout: function RPWatchContext_onlogout() { + log("doLogout :" + this.id); + let message = new IDDOMMessage(this.id); + this._mm.sendAsyncMessage("Identity:RP:Watch:OnLogout", message); + }, + + doReady: function RPWatchContext_onready() { + log("doReady: " + this.id); + let message = new IDDOMMessage(this.id); + this._mm.sendAsyncMessage("Identity:RP:Watch:OnReady", message); + }, + + doError: function RPWatchContext_onerror(aMessage) { + log("doError: " + aMessage); + } +}; + +let DOMIdentity = { + // nsIFrameMessageListener + receiveMessage: function DOMIdentity_receiveMessage(aMessage) { + let msg = aMessage.json; + + // Target is the frame message manager that called us and is + // used to send replies back to the proper window. + let targetMM = aMessage.target + .QueryInterface(Ci.nsIFrameLoaderOwner) + .frameLoader.messageManager; + + switch (aMessage.name) { + // RP + case "Identity:RP:Watch": + this._watch(msg, targetMM); + break; + case "Identity:RP:Request": + this._request(msg); + break; + case "Identity:RP:Logout": + this._logout(msg); + break; + // IDP + case "Identity:IDP:BeginProvisioning": + this._beginProvisioning(msg, targetMM); + break; + case "Identity:IDP:GenKeyPair": + this._genKeyPair(msg); + break; + case "Identity:IDP:RegisterCertificate": + this._registerCertificate(msg); + break; + case "Identity:IDP:ProvisioningFailure": + this._provisioningFailure(msg); + break; + case "Identity:IDP:BeginAuthentication": + this._beginAuthentication(msg, targetMM); + break; + case "Identity:IDP:CompleteAuthentication": + this._completeAuthentication(msg); + break; + case "Identity:IDP:AuthenticationFailure": + this._authenticationFailure(msg); + break; + } + }, + + // nsIObserver + observe: function DOMIdentity_observe(aSubject, aTopic, aData) { + switch (aTopic) { + case "domwindowopened": + case "domwindowclosed": + let win = aSubject.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + this._configureMessages(win, aTopic == "domwindowopened"); + break; + + case "xpcom-shutdown": + Services.ww.unregisterNotification(this); + Services.obs.removeObserver(this, "xpcom-shutdown"); + break; + } + }, + + messages: ["Identity:RP:Watch", "Identity:RP:Request", "Identity:RP:Logout", + "Identity:IDP:BeginProvisioning", "Identity:IDP:ProvisioningFailure", + "Identity:IDP:RegisterCertificate", "Identity:IDP:GenKeyPair", + "Identity:IDP:BeginAuthentication", + "Identity:IDP:CompleteAuthentication", + "Identity:IDP:AuthenticationFailure"], + + // Private. + _init: function DOMIdentity__init() { + Services.ww.registerNotification(this); + Services.obs.addObserver(this, "xpcom-shutdown", false); + }, + + _configureMessages: function DOMIdentity__configureMessages(aWindow, aRegister) { + if (!aWindow.messageManager) + return; + + let func = aWindow.messageManager[aRegister ? "addMessageListener" + : "removeMessageListener"]; + + for (let message of this.messages) { + func(message, this); + } + }, + + _resetFrameState: function(aContext) { + log("_resetFrameState: ", aContext.id); + if (!aContext._mm) { + throw new Error("ERROR: Trying to reset an invalid context"); + } + let message = new IDDOMMessage(aContext.id); + aContext._mm.sendAsyncMessage("Identity:ResetState", message); + }, + + _watch: function DOMIdentity__watch(message, targetMM) { + log("DOMIdentity__watch: " + message.id); + // Pass an object with the watch members to Identity.jsm so it can call the + // callbacks. + let context = new RPWatchContext(message.id, message.origin, + message.loggedInEmail, targetMM); + IdentityService.RP.watch(context); + }, + + _request: function DOMIdentity__request(message) { + IdentityService.RP.request(message.id, message); + }, + + _logout: function DOMIdentity__logout(message) { + IdentityService.RP.logout(message.id, message.origin); + }, + + _beginProvisioning: function DOMIdentity__beginProvisioning(message, targetMM) { + let context = new IDPProvisioningContext(message.id, message.origin, + targetMM); + IdentityService.IDP.beginProvisioning(context); + }, + + _genKeyPair: function DOMIdentity__genKeyPair(message) { + IdentityService.IDP.genKeyPair(message.id); + }, + + _registerCertificate: function DOMIdentity__registerCertificate(message) { + IdentityService.IDP.registerCertificate(message.id, message.cert); + }, + + _provisioningFailure: function DOMIdentity__provisioningFailure(message) { + IdentityService.IDP.raiseProvisioningFailure(message.id, message.reason); + }, + + _beginAuthentication: function DOMIdentity__beginAuthentication(message, targetMM) { + let context = new IDPAuthenticationContext(message.id, message.origin, + targetMM); + IdentityService.IDP.beginAuthentication(context); + }, + + _completeAuthentication: function DOMIdentity__completeAuthentication(message) { + IdentityService.IDP.completeAuthentication(message.id); + }, + + _authenticationFailure: function DOMIdentity__authenticationFailure(message) { + IdentityService.IDP.cancelAuthentication(message.id); + }, +}; + +// Object is initialized by nsIDService.js diff --git a/dom/identity/Identity.manifest b/dom/identity/Identity.manifest new file mode 100644 index 00000000000..de72b30c372 --- /dev/null +++ b/dom/identity/Identity.manifest @@ -0,0 +1,9 @@ +# nsDOMIdentity.js +component {8bcac6a3-56a4-43a4-a44c-cdf42763002f} nsDOMIdentity.js +contract @mozilla.org/dom/identity;1 {8bcac6a3-56a4-43a4-a44c-cdf42763002f} +category JavaScript-navigator-property id @mozilla.org/dom/identity;1 + +# nsIDService.js (initialization on startup) +component {baa581e5-8e72-406c-8c9f-dcd4b23a6f82} nsIDService.js +contract @mozilla.org/dom/identity/service;1 {baa581e5-8e72-406c-8c9f-dcd4b23a6f82} +category app-startup IDService @mozilla.org/dom/identity/service;1 diff --git a/dom/identity/Makefile.in b/dom/identity/Makefile.in new file mode 100644 index 00000000000..4b8233e6eb3 --- /dev/null +++ b/dom/identity/Makefile.in @@ -0,0 +1,28 @@ +# 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/. + +DEPTH = ../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +relativesrcdir = dom/identity + +include $(DEPTH)/config/autoconf.mk + +EXTRA_COMPONENTS = \ + nsDOMIdentity.js \ + nsIDService.js \ + Identity.manifest \ + $(NULL) + +EXTRA_JS_MODULES = \ + DOMIdentity.jsm \ + $(NULL) + +ifdef ENABLE_TESTS +DIRS += tests +endif + +include $(topsrcdir)/config/rules.mk diff --git a/dom/identity/nsDOMIdentity.js b/dom/identity/nsDOMIdentity.js new file mode 100644 index 00000000000..37fc2d5f6c2 --- /dev/null +++ b/dom/identity/nsDOMIdentity.js @@ -0,0 +1,530 @@ +/* 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 {classes: Cc, interfaces: Ci, utils: Cu} = Components; + +const PREF_DEBUG = "toolkit.identity.debug"; +const PREF_ENABLED = "dom.identity.enabled"; + +// Maximum length of a string that will go through IPC +const MAX_STRING_LENGTH = 2048; +// Maximum number of times navigator.id.request can be called for a document +const MAX_RP_CALLS = 100; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +// This is the child process corresponding to nsIDOMIdentity. + + +function nsDOMIdentity(aIdentityInternal) { + this._identityInternal = aIdentityInternal; +} +nsDOMIdentity.prototype = { + __exposedProps__: { + // Relying Party (RP) + watch: 'r', + request: 'r', + logout: 'r', + + // Provisioning + beginProvisioning: 'r', + genKeyPair: 'r', + registerCertificate: 'r', + raiseProvisioningFailure: 'r', + + // Authentication + beginAuthentication: 'r', + completeAuthentication: 'r', + raiseAuthenticationFailure: 'r', + }, + + // nsIDOMIdentity + /** + * Relying Party (RP) APIs + */ + + watch: function nsDOMIdentity_watch(aOptions) { + this._log("watch"); + if (this._rpWatcher) { + throw new Error("navigator.id.watch was already called"); + } + + if (!aOptions || typeof(aOptions) !== "object") { + throw new Error("options argument to watch is required"); + } + + // Check for required callbacks + let requiredCallbacks = ["onlogin", "onlogout"]; + for (let cbName of requiredCallbacks) { + if ((!(cbName in aOptions)) + || typeof(aOptions[cbName]) !== "function") { + throw new Error(cbName + " callback is required."); + } + } + + // Optional callback "onready" + if (aOptions["onready"] + && typeof(aOptions['onready']) !== "function") { + throw new Error("onready must be a function"); + } + + let message = this.DOMIdentityMessage(); + + // loggedInEmail + message.loggedInEmail = null; + let emailType = typeof(aOptions["loggedInEmail"]); + if (aOptions["loggedInEmail"] && aOptions["loggedInEmail"] !== "undefined") { + if (emailType !== "string") { + throw new Error("loggedInEmail must be a String or null"); + } + + // TODO: Bug 767610 - check email format. + // See nsHTMLInputElement::IsValidEmailAddress + if (aOptions["loggedInEmail"].indexOf("@") == -1 + || aOptions["loggedInEmail"].length > MAX_STRING_LENGTH) { + throw new Error("loggedInEmail is not valid"); + } + // Set loggedInEmail in this block that "undefined" doesn't get through. + message.loggedInEmail = aOptions.loggedInEmail; + } + this._log("loggedInEmail: " + message.loggedInEmail); + + this._rpWatcher = aOptions; + this._identityInternal._mm.sendAsyncMessage("Identity:RP:Watch", message); + }, + + request: function nsDOMIdentity_request(aOptions) { + // TODO: Bug 769569 - "must be invoked from within a click handler" + + // Has the caller called watch() before this? + if (!this._rpWatcher) { + throw new Error("navigator.id.request called before navigator.id.watch"); + } + if (this._rpCalls > MAX_RP_CALLS) { + throw new Error("navigator.id.request called too many times"); + } + + let message = this.DOMIdentityMessage(); + + if (aOptions) { + // Optional string properties + let optionalStringProps = ["privacyPolicy", "termsOfService"]; + for (let propName of optionalStringProps) { + if (!aOptions[propName] || aOptions[propName] === "undefined") + continue; + if (typeof(aOptions[propName]) !== "string") { + throw new Error(propName + " must be a string representing a URL."); + } + if (aOptions[propName].length > MAX_STRING_LENGTH) { + throw new Error(propName + " is invalid."); + } + message[propName] = aOptions[propName]; + } + + if (aOptions["oncancel"] + && typeof(aOptions["oncancel"]) !== "function") { + throw new Error("oncancel is not a function"); + } else { + // Store optional cancel callback for later. + this._onCancelRequestCallback = aOptions.oncancel; + } + } + + this._rpCalls++; + this._identityInternal._mm.sendAsyncMessage("Identity:RP:Request", message); + }, + + logout: function nsDOMIdentity_logout() { + if (!this._rpWatcher) { + throw new Error("navigator.id.logout called before navigator.id.watch"); + } + if (this._rpCalls > MAX_RP_CALLS) { + throw new Error("navigator.id.logout called too many times"); + } + + this._rpCalls++; + let message = this.DOMIdentityMessage(); + this._identityInternal._mm.sendAsyncMessage("Identity:RP:Logout", message); + }, + + /** + * Identity Provider (IDP) Provisioning APIs + */ + + beginProvisioning: function nsDOMIdentity_beginProvisioning(aCallback) { + this._log("beginProvisioning"); + if (this._beginProvisioningCallback) { + throw new Error("navigator.id.beginProvisioning already called."); + } + if (!aCallback || typeof(aCallback) !== "function") { + throw new Error("beginProvisioning callback is required."); + } + + this._beginProvisioningCallback = aCallback; + this._identityInternal._mm.sendAsyncMessage("Identity:IDP:BeginProvisioning", + this.DOMIdentityMessage()); + }, + + genKeyPair: function nsDOMIdentity_genKeyPair(aCallback) { + this._log("genKeyPair"); + if (!this._beginProvisioningCallback) { + throw new Error("navigator.id.genKeyPair called outside of provisioning"); + } + if (this._genKeyPairCallback) { + throw new Error("navigator.id.genKeyPair already called."); + } + if (!aCallback || typeof(aCallback) !== "function") { + throw new Error("genKeyPair callback is required."); + } + + this._genKeyPairCallback = aCallback; + this._identityInternal._mm.sendAsyncMessage("Identity:IDP:GenKeyPair", + this.DOMIdentityMessage()); + }, + + registerCertificate: function nsDOMIdentity_registerCertificate(aCertificate) { + this._log("registerCertificate"); + if (!this._genKeyPairCallback) { + throw new Error("navigator.id.registerCertificate called outside of provisioning"); + } + if (this._provisioningEnded) { + throw new Error("Provisioning already ended"); + } + this._provisioningEnded = true; + + let message = this.DOMIdentityMessage(); + message.cert = aCertificate; + this._identityInternal._mm.sendAsyncMessage("Identity:IDP:RegisterCertificate", message); + }, + + raiseProvisioningFailure: function nsDOMIdentity_raiseProvisioningFailure(aReason) { + this._log("raiseProvisioningFailure '" + aReason + "'"); + if (this._provisioningEnded) { + throw new Error("Provisioning already ended"); + } + if (!aReason || typeof(aReason) != "string") { + throw new Error("raiseProvisioningFailure reason is required"); + } + this._provisioningEnded = true; + + let message = this.DOMIdentityMessage(); + message.reason = aReason; + this._identityInternal._mm.sendAsyncMessage("Identity:IDP:ProvisioningFailure", message); + }, + + /** + * Identity Provider (IDP) Authentication APIs + */ + + beginAuthentication: function nsDOMIdentity_beginAuthentication(aCallback) { + this._log("beginAuthentication"); + if (this._beginAuthenticationCallback) { + throw new Error("navigator.id.beginAuthentication already called."); + } + if (typeof(aCallback) !== "function") { + throw new Error("beginAuthentication callback is required."); + } + if (!aCallback || typeof(aCallback) !== "function") { + throw new Error("beginAuthentication callback is required."); + } + + this._beginAuthenticationCallback = aCallback; + this._identityInternal._mm.sendAsyncMessage("Identity:IDP:BeginAuthentication", + this.DOMIdentityMessage()); + }, + + completeAuthentication: function nsDOMIdentity_completeAuthentication() { + if (this._authenticationEnded) { + throw new Error("Authentication already ended"); + } + if (!this._beginAuthenticationCallback) { + throw new Error("navigator.id.completeAuthentication called outside of authentication"); + } + this._authenticationEnded = true; + + this._identityInternal._mm.sendAsyncMessage("Identity:IDP:CompleteAuthentication", + this.DOMIdentityMessage()); + }, + + raiseAuthenticationFailure: function nsDOMIdentity_raiseAuthenticationFailure(aReason) { + if (this._authenticationEnded) { + throw new Error("Authentication already ended"); + } + if (!aReason || typeof(aReason) != "string") { + throw new Error("raiseProvisioningFailure reason is required"); + } + + let message = this.DOMIdentityMessage(); + message.reason = aReason; + this._identityInternal._mm.sendAsyncMessage("Identity:IDP:AuthenticationFailure", message); + }, + + // Private. + _init: function nsDOMIdentity__init(aWindow) { + + this._initializeState(); + + // Store window and origin URI. + this._window = aWindow; + this._origin = aWindow.document.nodePrincipal.origin; + + // Setup identifiers for current window. + let util = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + this._id = util.outerWindowID; + }, + + /** + * Called during init and shutdown. + */ + _initializeState: function nsDOMIdentity__initializeState() { + // Some state to prevent abuse + // Limit the number of calls to .request + this._rpCalls = 0; + this._provisioningEnded = false; + this._authenticationEnded = false; + + this._rpWatcher = null; + this._onCancelRequestCallback = null; + this._beginProvisioningCallback = null; + this._genKeyPairCallback = null; + this._beginAuthenticationCallback = null; + }, + + _receiveMessage: function nsDOMIdentity_receiveMessage(aMessage) { + let msg = aMessage.json; + this._log("receiveMessage: " + aMessage.name); + + switch (aMessage.name) { + case "Identity:ResetState": + if (!this._identityInternal._debug) { + return; + } + this._initializeState(); + Services.obs.notifyObservers(null, "identity-DOM-state-reset", this._id); + break; + case "Identity:RP:Watch:OnLogin": + // Do we have a watcher? + if (!this._rpWatcher) { + return; + } + + if (this._rpWatcher.onlogin) { + this._rpWatcher.onlogin(msg.assertion); + } + break; + case "Identity:RP:Watch:OnLogout": + // Do we have a watcher? + if (!this._rpWatcher) { + return; + } + + if (this._rpWatcher.onlogout) { + this._rpWatcher.onlogout(); + } + break; + case "Identity:RP:Watch:OnReady": + // Do we have a watcher? + if (!this._rpWatcher) { + return; + } + + if (this._rpWatcher.onready) { + this._rpWatcher.onready(); + } + break; + case "Identity:RP:Request:OnCancel": + // Do we have a watcher? + if (!this._rpWatcher) { + return; + } + + if (this._onCancelRequestCallback) { + this._onCancelRequestCallback(); + } + break; + case "Identity:IDP:CallBeginProvisioningCallback": + this._callBeginProvisioningCallback(msg); + break; + case "Identity:IDP:CallGenKeyPairCallback": + this._callGenKeyPairCallback(msg); + break; + case "Identity:IDP:CallBeginAuthenticationCallback": + this._callBeginAuthenticationCallback(msg); + break; + } + }, + + _log: function nsDOMIdentity__log(msg) { + this._identityInternal._log(msg); + }, + + _callGenKeyPairCallback: function nsDOMIdentity__callGenKeyPairCallback(message) { + // create a pubkey object that works + let chrome_pubkey = JSON.parse(message.publicKey); + + // bunch of stuff to create a proper object in window context + function genPropDesc(value) { + return { + enumerable: true, configurable: true, writable: true, value: value + }; + } + + let propList = {}; + for (let k in chrome_pubkey) { + propList[k] = genPropDesc(chrome_pubkey[k]); + } + + let pubkey = Cu.createObjectIn(this._window); + Object.defineProperties(pubkey, propList); + Cu.makeObjectPropsNormal(pubkey); + + // do the callback + this._genKeyPairCallback(pubkey); + }, + + _callBeginProvisioningCallback: + function nsDOMIdentity__callBeginProvisioningCallback(message) { + let identity = message.identity; + let certValidityDuration = message.certDuration; + this._beginProvisioningCallback(identity, + certValidityDuration); + }, + + _callBeginAuthenticationCallback: + function nsDOMIdentity__callBeginAuthenticationCallback(message) { + let identity = message.identity; + this._beginAuthenticationCallback(identity); + }, + + /** + * Helper to create messages to send using a message manager + */ + DOMIdentityMessage: function DOMIdentityMessage() { + return { + id: this._id, + origin: this._origin, + }; + }, + +}; + +/** + * Internal functions that shouldn't be exposed to content. + */ +function nsDOMIdentityInternal() { +} +nsDOMIdentityInternal.prototype = { + + // nsIFrameMessageListener + receiveMessage: function nsDOMIdentityInternal_receiveMessage(aMessage) { + let msg = aMessage.json; + // Is this message intended for this window? + if (msg.id != this._id) { + return; + } + this._identity._receiveMessage(aMessage); + }, + + // nsIObserver + observe: function nsDOMIdentityInternal_observe(aSubject, aTopic, aData) { + let wId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data; + if (wId != this._innerWindowID) { + return; + } + + Services.obs.removeObserver(this, "inner-window-destroyed"); + this._identity._initializeState(); + this._identity = null; + + // TODO: Also send message to DOMIdentity notifiying window is no longer valid + // ie. in the case that the user closes the auth. window and we need to know. + + try { + for (let msgName of this._messages) { + this._mm.removeMessageListener(msgName, this); + } + } catch (ex) { + // Avoid errors when removing more than once. + } + + this._mm = null; + }, + + // nsIDOMGlobalPropertyInitializer + init: function nsDOMIdentityInternal_init(aWindow) { + if (Services.prefs.getPrefType(PREF_ENABLED) != Ci.nsIPrefBranch.PREF_BOOL + || !Services.prefs.getBoolPref(PREF_ENABLED)) { + return null; + } + + this._debug = + Services.prefs.getPrefType(PREF_DEBUG) == Ci.nsIPrefBranch.PREF_BOOL + && Services.prefs.getBoolPref(PREF_DEBUG); + + this._identity = new nsDOMIdentity(this); + + this._identity._init(aWindow); + + let util = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + this._id = util.outerWindowID; + this._innerWindowID = util.currentInnerWindowID; + + this._log("init was called from " + aWindow.document.location); + + this._mm = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIContentFrameMessageManager); + + // Setup listeners for messages from parent process. + this._messages = [ + "Identity:ResetState", + "Identity:RP:Watch:OnLogin", + "Identity:RP:Watch:OnLogout", + "Identity:RP:Watch:OnReady", + "Identity:RP:Request:OnCancel", + "Identity:IDP:CallBeginProvisioningCallback", + "Identity:IDP:CallGenKeyPairCallback", + "Identity:IDP:CallBeginAuthenticationCallback", + ]; + this._messages.forEach((function(msgName) { + this._mm.addMessageListener(msgName, this); + }).bind(this)); + + // Setup observers so we can remove message listeners. + Services.obs.addObserver(this, "inner-window-destroyed", false); + + return this._identity; + }, + + // Private. + _log: function nsDOMIdentityInternal__log(msg) { + if (!this._debug) { + return; + } + dump("nsDOMIdentity (" + this._id + "): " + msg + "\n"); + }, + + // Component setup. + classID: Components.ID("{8bcac6a3-56a4-43a4-a44c-cdf42763002f}"), + + QueryInterface: XPCOMUtils.generateQI( + [Ci.nsIDOMGlobalPropertyInitializer, Ci.nsIFrameMessageListener] + ), + + classInfo: XPCOMUtils.generateCI({ + classID: Components.ID("{8bcac6a3-56a4-43a4-a44c-cdf42763002f}"), + contractID: "@mozilla.org/dom/identity;1", + interfaces: [], + classDescription: "Identity DOM Implementation" + }) + +}; + +const NSGetFactory = XPCOMUtils.generateNSGetFactory([nsDOMIdentityInternal]); diff --git a/dom/identity/nsIDService.js b/dom/identity/nsIDService.js new file mode 100644 index 00000000000..d6d4bd648f6 --- /dev/null +++ b/dom/identity/nsIDService.js @@ -0,0 +1,34 @@ +/* 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 {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +function IDService() { + this.wrappedJSObject = this; +} +IDService.prototype = { + classID: Components.ID("{baa581e5-8e72-406c-8c9f-dcd4b23a6f82}"), + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsISupportsWeakReference]), + + observe: function observe(subject, topic, data) { + switch (topic) { + case "app-startup": + Services.obs.addObserver(this, "final-ui-startup", true); + break; + case "final-ui-startup": + // Startup DOMIdentity.jsm + Cu.import("resource://gre/modules/DOMIdentity.jsm"); + DOMIdentity._init(); + break; + } + } +}; + +const NSGetFactory = XPCOMUtils.generateNSGetFactory([IDService]); diff --git a/dom/identity/tests/Makefile.in b/dom/identity/tests/Makefile.in new file mode 100644 index 00000000000..58e64b396a6 --- /dev/null +++ b/dom/identity/tests/Makefile.in @@ -0,0 +1,28 @@ +# 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/. + +DEPTH = ../../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +relativesrcdir = dom/identity/tests + +include $(DEPTH)/config/autoconf.mk + +DIRS = \ + $(NULL) + +include $(topsrcdir)/config/rules.mk + +_TEST_FILES = \ + head_identity.js \ + test_identity_idp_auth_basics.html \ + test_identity_idp_prov_basics.html \ + test_identity_rp_basics.html \ + $(NULL) + +libs:: $(_TEST_FILES) + $(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/tests/$(relativesrcdir) + diff --git a/dom/identity/tests/head_identity.js b/dom/identity/tests/head_identity.js new file mode 100644 index 00000000000..8852e9a7487 --- /dev/null +++ b/dom/identity/tests/head_identity.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const Ci = Components.interfaces; +const Cu = SpecialPowers.wrap(Components).utils; + +SpecialPowers.setBoolPref("toolkit.identity.debug", true); +SpecialPowers.setBoolPref("dom.identity.enabled", true); + +const Services = Cu.import("resource://gre/modules/Services.jsm").Services; +const DOMIdentity = Cu.import("resource://gre/modules/DOMIdentity.jsm") + .DOMIdentity; + +let util = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); +let outerWinId = util.outerWindowID; + +const identity = navigator.id || navigator.mozId; + +let index = 0; + +// mimicking callback funtionality for ease of testing +// this observer auto-removes itself after the observe function +// is called, so this is meant to observe only ONE event. +function makeObserver(aObserveTopic, aObserveFunc) { + function observe(aSubject, aTopic, aData) { + if (aTopic == aObserveTopic) { + aObserveFunc(aSubject, aTopic, aData); + Services.obs.removeObserver(this, aObserveTopic); + } + } + + Services.obs.addObserver(observe, aObserveTopic, false); +} + +function expectException(aFunc, msg, aErrorType="Error") { + info("Expecting an exception: " + msg); + msg = msg || ""; + let caughtEx = null; + try { + aFunc(); + } catch (ex) { + let exProto = Object.getPrototypeOf(ex); + // Don't count NS_* exceptions since they shouldn't be exposed to content + if (exProto.toString() == aErrorType + && ex.toString().indexOf("NS_ERROR_FAILURE") == -1) { + caughtEx = ex; + } else { + ok(false, ex); + return; + } + } + isnot(caughtEx, null, "Check for thrown exception."); +} + +function next() { + if (!identity) { + todo(false, "DOM API is not available. Skipping tests."); + finish_tests(); + return; + } + if (index >= steps.length) { + ok(false, "Shouldn't get here!"); + return; + } + try { + let fn = steps[index]; + info("Begin test " + index + " '" + steps[index].name + "'!"); + fn(); + } catch(ex) { + ok(false, "Caught exception", ex); + } + index += 1; +} + +function finish_tests() { + info("all done"); + SpecialPowers.clearUserPref("toolkit.identity.debug"); + SpecialPowers.clearUserPref("dom.identity.enabled"); + SimpleTest.finish(); +} diff --git a/dom/identity/tests/test_identity_idp_auth_basics.html b/dom/identity/tests/test_identity_idp_auth_basics.html new file mode 100644 index 00000000000..517c9b1bbb8 --- /dev/null +++ b/dom/identity/tests/test_identity_idp_auth_basics.html @@ -0,0 +1,87 @@ + + + +
+ ++ ++ + diff --git a/dom/identity/tests/test_identity_idp_prov_basics.html b/dom/identity/tests/test_identity_idp_prov_basics.html new file mode 100644 index 00000000000..a5a02547a84 --- /dev/null +++ b/dom/identity/tests/test_identity_idp_prov_basics.html @@ -0,0 +1,160 @@ + + + + + +
+ ++ + diff --git a/dom/identity/tests/test_identity_rp_basics.html b/dom/identity/tests/test_identity_rp_basics.html new file mode 100644 index 00000000000..2d101af88ba --- /dev/null +++ b/dom/identity/tests/test_identity_rp_basics.html @@ -0,0 +1,172 @@ + + + + + +
+ ++ + diff --git a/toolkit/identity/IdentityProvider.jsm b/toolkit/identity/IdentityProvider.jsm index 83fee7f5b8b..684911e6d0c 100644 --- a/toolkit/identity/IdentityProvider.jsm +++ b/toolkit/identity/IdentityProvider.jsm @@ -42,6 +42,7 @@ function IdentityProviderService() { IdentityProviderService.prototype = { QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]), + _sandboxConfigured: false, observe: function observe(aSubject, aTopic, aData) { switch (aTopic) { @@ -83,6 +84,14 @@ IdentityProviderService.prototype = { shutdown: function RP_shutdown() { this.reset(); + + if (this._sandboxConfigured) { + // Tear down message manager listening on the hidden window + Cu.import("resource://gre/modules/DOMIdentity.jsm"); + DOMIdentity._configureMessages(Services.appShell.hiddenDOMWindow, false); + this._sandboxConfigured = false; + } + Services.obs.removeObserver(this, "quit-application-granted"); }, @@ -432,6 +441,14 @@ IdentityProviderService.prototype = { */ _createProvisioningSandbox: function _createProvisioningSandbox(aURL, aCallback) { log("_createProvisioningSandbox:", aURL); + + if (!this._sandboxConfigured) { + // Configure message manager listening on the hidden window + Cu.import("resource://gre/modules/DOMIdentity.jsm"); + DOMIdentity._configureMessages(Services.appShell.hiddenDOMWindow, true); + this._sandboxConfigured = true; + } + new Sandbox(aURL, aCallback); },