--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -454,28 +454,37 @@ window[chromehidden~="toolbar"] toolbar:
.notification-anchor-icon {
-moz-user-focus: normal;
}
.notification-anchor-icon:not([showing]) {
display: none;
}
+#notification-popup .text-link.custom-link {
+ -moz-binding: url("chrome://global/content/bindings/text.xml#text-label");
+ text-decoration: none;
+}
+
#invalid-form-popup > description {
max-width: 280px;
}
#geolocation-notification {
-moz-binding: url("chrome://browser/content/urlbarBindings.xml#geolocation-notification");
}
#addon-progress-notification {
-moz-binding: url("chrome://browser/content/urlbarBindings.xml#addon-progress-notification");
}
+#identity-request-notification {
+ -moz-binding: url("chrome://browser/content/urlbarBindings.xml#identity-request-notification");
+}
+
/* override hidden="true" for the status bar compatibility shim
in case it was persisted for the real status bar */
#status-bar {
display: -moz-box;
}
/* Remove the resizer from the statusbar compatibility shim */
#status-bar[hideresizer] > .statusbar-resizerpanel {
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -559,16 +559,17 @@
oninput="gBrowser.userTypedValue = this.value;"
ontextentered="this.handleCommand(param);"
ontextreverted="return this.handleRevert();"
pageproxystate="invalid"
onfocus="document.getElementById('identity-box').style.MozUserFocus= 'normal'"
onblur="setTimeout(function() document.getElementById('identity-box').style.MozUserFocus = '', 0);">
<box id="notification-popup-box" hidden="true" align="center">
<image id="default-notification-icon" class="notification-anchor-icon" role="button"/>
+ <image id="identity-notification-icon" class="notification-anchor-icon" role="button"/>
<image id="geo-notification-icon" class="notification-anchor-icon" role="button"/>
<image id="addons-notification-icon" class="notification-anchor-icon" role="button"/>
<image id="indexedDB-notification-icon" class="notification-anchor-icon" role="button"/>
<image id="password-notification-icon" class="notification-anchor-icon" role="button"/>
<image id="webapps-notification-icon" class="notification-anchor-icon" role="button"/>
<image id="plugins-notification-icon" class="notification-anchor-icon" role="button"/>
</box>
<!-- Use onclick instead of normal popup= syntax since the popup
--- a/browser/base/content/test/browser_popupNotification.js
+++ b/browser/base/content/test/browser_popupNotification.js
@@ -73,17 +73,17 @@ function runNextTest() {
}
let onHidden = onHiddenArray.shift();
info("[Test #" + gTestIndex + "] popup hidden (" + onHiddenArray.length + " hides remaining)");
executeSoon(function () {
onHidden.call(nextTest, this);
if (!onHiddenArray.length)
goNext();
- });
+ }.bind(this));
}, onHiddenArray.length);
info("[Test #" + gTestIndex + "] added listeners; panel state: " + PopupNotifications.isPanelOpen);
}
info("[Test #" + gTestIndex + "] running test");
nextTest.run();
}
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -1170,16 +1170,271 @@
<method name="onDownloadEnded">
<body><![CDATA[
this.updateProgress();
]]></body>
</method>
</implementation>
</binding>
+ <binding id="identity-request-notification" extends="chrome://global/content/bindings/notification.xml#popup-notification">
+ <content align="start">
+
+ <xul:image class="popup-notification-icon"
+ xbl:inherits="popupid,src=icon"/>
+
+ <xul:vbox flex="1">
+ <xul:vbox anonid="identity-deck">
+ <xul:vbox flex="1" pack="center"> <!-- 1: add an email -->
+ <html:input type="email" anonid="email" required="required" size="30"/>
+ <xul:description anonid="newidentitydesc"/>
+ <xul:spacer flex="1"/>
+ <xul:label class="text-link custom-link small-margin" anonid="chooseemail" hidden="true"/>
+ </xul:vbox>
+ <xul:vbox flex="1" hidden="true"> <!-- 2: choose an email -->
+ <xul:description anonid="chooseidentitydesc"/>
+ <xul:radiogroup anonid="identities">
+ </xul:radiogroup>
+ <xul:label class="text-link custom-link" anonid="newemail"/>
+ </xul:vbox>
+ </xul:vbox>
+ <xul:hbox class="popup-notification-button-container"
+ pack="end" align="center">
+ <xul:label anonid="tos" class="text-link" hidden="true"/>
+ <xul:label anonid="privacypolicy" class="text-link" hidden="true"/>
+ <xul:spacer flex="1"/>
+ <xul:image anonid="throbber" src="chrome://browser/skin/tabbrowser/loading.png"
+ style="visibility:hidden" width="16" height="16"/>
+ <xul:button anonid="button"
+ type="menu-button"
+ class="popup-notification-menubutton"
+ xbl:inherits="oncommand=buttoncommand,label=buttonlabel,accesskey=buttonaccesskey">
+ <xul:menupopup anonid="menupopup"
+ xbl:inherits="oncommand=menucommand">
+ <children/>
+ <xul:menuitem class="menuitem-iconic popup-notification-closeitem"
+ label="&closeNotificationItem.label;"
+ xbl:inherits="oncommand=closeitemcommand"/>
+ </xul:menupopup>
+ </xul:button>
+ </xul:hbox>
+ </xul:vbox>
+ <xul:vbox pack="start">
+ <xul:toolbarbutton anonid="closebutton"
+ class="messageCloseButton popup-notification-closebutton tabbable"
+ xbl:inherits="oncommand=closebuttoncommand"
+ tooltiptext="&closeNotification.tooltip;"/>
+ </xul:vbox>
+ </content>
+ <implementation>
+ <constructor><![CDATA[
+ // this.notification.options.identity is used to pass identity-specific info to the binding
+ let origin = this.identity.origin
+
+ // Populate text
+ this.emailField.placeholder = gNavigatorBundle.
+ getString("identity.newIdentity.email.placeholder");
+ this.newIdentityDesc.textContent = gNavigatorBundle.getFormattedString(
+ "identity.newIdentity.description", [origin]);
+ this.chooseIdentityDesc.textContent = gNavigatorBundle.getFormattedString(
+ "identity.chooseIdentity.description", [origin]);
+
+ // Show optional terms of service and privacy policy links
+ this._populateLink(this.identity.termsOfService, "tos", "identity.termsOfService");
+ this._populateLink(this.identity.privacyPolicy, "privacypolicy", "identity.privacyPolicy");
+
+ // Populate the list of identities to choose from. The origin is used to provide
+ // better suggestions.
+ let identities = this.SignInToWebsiteUX.getIdentitiesForSite(origin);
+
+ this._populateIdentityList(identities);
+
+ if (typeof this.step == "undefined") {
+ // First opening of this notification
+ // Show the add email pane (0) if there are no existing identities otherwise show the list
+ this.step = "result" in identities && identities.result.length ? 1 : 0;
+ } else {
+ // Already opened so restore previous state
+ if (this.identity.typedEmail) {
+ this.emailField.value = this.identity.typedEmail;
+ }
+ if (this.identity.selected) {
+ // If the user already chose an identity then update the UI to reflect that
+ this.onIdentitySelected();
+ }
+ // Update the view for the step
+ this.step = this.step;
+ }
+
+ // Fire notification with the chosen identity when main button is clicked
+ this.button.addEventListener("command", this._onButtonCommand.bind(this), true);
+
+ // Do the same if enter is pressed in the email field
+ this.emailField.addEventListener("keypress", function emailFieldKeypress(aEvent) {
+ if (aEvent.keyCode != aEvent.DOM_VK_RETURN)
+ return;
+ this._onButtonCommand(aEvent);
+ }.bind(this));
+
+ this.addEmailLink.value = gNavigatorBundle.getString("identity.newIdentity.label");
+ this.addEmailLink.accessKey = gNavigatorBundle.getString("identity.newIdentity.accessKey");
+ this.addEmailLink.addEventListener("click", function addEmailClick(evt) {
+ this.step = 0;
+ }.bind(this));
+
+ this.chooseEmailLink.value = gNavigatorBundle.getString("identity.chooseIdentity.label");
+ this.chooseEmailLink.hidden = !("result" in identities && identities.result.length);
+ this.chooseEmailLink.addEventListener("click", function chooseEmailClick(evt) {
+ this.step = 1;
+ }.bind(this));
+
+ this.emailField.addEventListener("blur", function onEmailBlur() {
+ this.identity.typedEmail = this.emailField.value;
+ }.bind(this));
+ ]]></constructor>
+
+ <field name="SignInToWebsiteUX" readonly="true">
+ let sitw = {};
+ Components.utils.import("resource:///modules/SignInToWebsite.jsm", sitw);
+ sitw.SignInToWebsiteUX;
+ </field>
+
+ <field name="newIdentityDesc" readonly="true">
+ document.getAnonymousElementByAttribute(this, "anonid", "newidentitydesc");
+ </field>
+
+ <field name="chooseIdentityDesc" readonly="true">
+ document.getAnonymousElementByAttribute(this, "anonid", "chooseidentitydesc");
+ </field>
+
+ <field name="identityList" readonly="true">
+ document.getAnonymousElementByAttribute(this, "anonid", "identities");
+ </field>
+
+ <field name="emailField" readonly="true">
+ document.getAnonymousElementByAttribute(this, "anonid", "email");
+ </field>
+
+ <field name="addEmailLink" readonly="true">
+ document.getAnonymousElementByAttribute(this, "anonid", "newemail");
+ </field>
+
+ <field name="chooseEmailLink" readonly="true">
+ document.getAnonymousElementByAttribute(this, "anonid", "chooseemail");
+ </field>
+
+ <field name="throbber" readonly="true">
+ document.getAnonymousElementByAttribute(this, "anonid", "throbber");
+ </field>
+
+ <field name="identity" readonly="true">
+ this.notification.options.identity;
+ </field>
+
+ <!-- persist the state on the identity object so we can re-create the
+ notification state upon re-opening -->
+ <property name="step">
+ <getter>
+ return this.identity.step;
+ </getter>
+ <setter><![CDATA[
+ let deck = document.getAnonymousElementByAttribute(this, "anonid", "identity-deck");
+ for (let i = 0; i < deck.children.length; i++) {
+ deck.children[i].hidden = (val != i);
+ }
+ this.identity.step = val;
+ switch (val) {
+ case 0:
+ this.emailField.focus();
+ break;
+ }]]>
+ </setter>
+ </property>
+
+ <method name="onIdentitySelected">
+ <body><![CDATA[
+ this.throbber.style.visibility = "visible";
+ this.button.disabled = true;
+ this.emailField.value = this.identity.selected
+ this.emailField.disabled = true;
+ this.identityList.disabled = true;
+ ]]></body>
+ </method>
+
+ <method name="_populateLink">
+ <parameter name="aURL"/>
+ <parameter name="aLinkId"/>
+ <parameter name="aStringId"/>
+ <body><![CDATA[
+ if (aURL) {
+ // Show optional link to aURL
+ let link = document.getAnonymousElementByAttribute(this, "anonid", aLinkId);
+ link.value = gNavigatorBundle.getString(aStringId);
+ link.href = aURL;
+ link.hidden = false;
+ }
+ ]]></body>
+ </method>
+
+ <method name="_populateIdentityList">
+ <parameter name="aIdentities"/>
+ <body><![CDATA[
+ let foundLastUsed = false;
+ let lastUsed = this.identity.selected || aIdentities.lastUsed;
+ for (let id in aIdentities.result) {
+ let label = aIdentities.result[id];
+ let opt = this.identityList.appendItem(label);
+ if (label == lastUsed) {
+ this.identityList.selectedItem = opt;
+ foundLastUsed = true;
+ }
+ }
+ if (!foundLastUsed) {
+ this.identityList.selectedIndex = -1;
+ }
+ ]]></body>
+ </method>
+
+ <method name="_onButtonCommand">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ if (aEvent.target != aEvent.currentTarget)
+ return;
+ let chosenId;
+ switch (this.step) {
+ case 0:
+ aEvent.stopPropagation();
+ if (!this.emailField.validity.valid) {
+ this.emailField.focus();
+ return;
+ }
+ chosenId = this.emailField.value;
+ break;
+ case 1:
+ aEvent.stopPropagation();
+ let selectedItem = this.identityList.selectedItem
+ chosenId = selectedItem ? selectedItem.label : null;
+ if (!chosenId)
+ return;
+ break;
+ default:
+ throw new Error("Unknown case");
+ return;
+ }
+ // Actually select the identity
+ this.SignInToWebsiteUX.selectIdentity(this.identity.rpId, chosenId);
+ this.identity.selected = chosenId;
+ this.onIdentitySelected();
+ ]]></body>
+ </method>
+
+ </implementation>
+ </binding>
+
+
<binding id="splitmenu">
<content>
<xul:hbox anonid="menuitem" flex="1"
class="splitmenu-menuitem"
xbl:inherits="iconic,label,disabled,onclick=oncommand,_moz-menuactive=active"/>
<xul:menu anonid="menu" class="splitmenu-menu"
xbl:inherits="disabled,_moz-menuactive=active"
oncommand="event.stopPropagation();">
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -7,30 +7,31 @@ const Ci = Components.interfaces;
const Cc = Components.classes;
const Cr = Components.results;
const Cu = Components.utils;
const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource:///modules/SignInToWebsite.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "BookmarkHTMLUtils",
"resource://gre/modules/BookmarkHTMLUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "webappsUI",
+XPCOMUtils.defineLazyModuleGetter(this, "webappsUI",
"resource:///modules/webappsUI.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PageThumbs",
"resource:///modules/PageThumbs.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PdfJs",
"resource://pdf.js/PdfJs.jsm");
@@ -303,16 +304,17 @@ BrowserGlue.prototype = {
this._idleService.removeIdleObserver(this, BOOKMARKS_BACKUP_IDLE_TIME);
if (this._isPlacesInitObserver)
os.removeObserver(this, "places-init-complete");
if (this._isPlacesLockedObserver)
os.removeObserver(this, "places-database-locked");
if (this._isPlacesShutdownObserver)
os.removeObserver(this, "places-shutdown");
webappsUI.uninit();
+ SignInToWebsiteUX.uninit();
},
_onAppDefaults: function BG__onAppDefaults() {
// apply distribution customizations (prefs)
// other customizations are applied in _onProfileStartup()
this._distributionCustomizer.applyPrefDefaults();
},
@@ -332,16 +334,18 @@ BrowserGlue.prototype = {
// handle any UI migration
this._migrateUI();
// Initialize webapps UI
webappsUI.init();
PageThumbs.init();
+ SignInToWebsiteUX.init();
+
PdfJs.init();
Services.obs.notifyObservers(null, "browser-ui-startup-complete", "");
},
// the first browser window has finished initializing
_onFirstWindowLoaded: function BG__onFirstWindowLoaded() {
#ifdef XP_WIN
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -378,8 +378,26 @@ social.enable.label=%S integration
social.enable.accesskey=n
# LOCALIZATION NOTE (social.remove.label): %S = brandShortName
social.remove.label=Remove from %S
social.remove.accesskey=R
# LOCALIZATION NOTE (social.enabled.message): %1$S is the name of the social provider, %2$S is brandShortName (e.g. Firefox)
social.activated.message=%1$S integration with %2$S has been activated.
+
+# Identity notifications popups
+identity.termsOfService = Terms of Service
+identity.privacyPolicy = Privacy Policy
+identity.chooseIdentity.description = Sign in to %S
+identity.chooseIdentity.label = Use an existing email
+identity.newIdentity.label = Use a different email
+identity.newIdentity.accessKey = e
+identity.newIdentity.email.placeholder = Email
+# LOCALIZATION NOTE (identity.newIdentity.description, identity.chooseIdentity.description): %S is the website origin (ie. https://www.mozilla.org) shown in popup notifications.
+identity.newIdentity.description = Enter your email address to sign in to %S
+identity.next.label = Next
+identity.next.accessKey = n
+# LOCALIZATION NOTE: shown in the popup notification when a user successfully logs into a website
+# LOCALIZATION NOTE (identity.loggedIn.description): %S is the website origin (ie. https://www.mozilla.org)
+identity.loggedIn.description = Signed in as: %S
+identity.loggedIn.signOut.label = Sign Out
+identity.loggedIn.signOut.accessKey = O
--- a/browser/modules/Makefile.in
+++ b/browser/modules/Makefile.in
@@ -13,21 +13,22 @@ include $(topsrcdir)/config/config.mk
TEST_DIRS += test
EXTRA_JS_MODULES = \
openLocationLastURL.jsm \
NetworkPrioritizer.jsm \
NewTabUtils.jsm \
offlineAppCache.jsm \
+ SignInToWebsite.jsm \
TelemetryTimestamps.jsm \
Social.jsm \
webappsUI.jsm \
$(NULL)
-ifeq ($(MOZ_WIDGET_TOOLKIT),windows)
+ifeq ($(MOZ_WIDGET_TOOLKIT),windows)
EXTRA_JS_MODULES += \
WindowsPreviewPerTab.jsm \
WindowsJumpLists.jsm \
$(NULL)
endif
include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/browser/modules/SignInToWebsite.jsm
@@ -0,0 +1,235 @@
+/* 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 EXPORTED_SYMBOLS = ["SignInToWebsiteUX"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+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.apply(Logger, ["SignInToWebsiteUX"].concat(aMessageArgs));
+}
+
+let SignInToWebsiteUX = {
+
+ init: function SignInToWebsiteUX_init() {
+ Services.obs.addObserver(this, "identity-request", false);
+ Services.obs.addObserver(this, "identity-auth", false);
+ Services.obs.addObserver(this, "identity-auth-complete", false);
+ Services.obs.addObserver(this, "identity-login-state-changed", false);
+ },
+
+ uninit: function SignInToWebsiteUX_uninit() {
+ Services.obs.removeObserver(this, "identity-request");
+ Services.obs.removeObserver(this, "identity-auth");
+ Services.obs.removeObserver(this, "identity-auth-complete");
+ Services.obs.removeObserver(this, "identity-login-state-changed");
+ },
+
+ observe: function SignInToWebsiteUX_observe(aSubject, aTopic, aData) {
+ log("observe: received", aTopic, "with", aData, "for", aSubject);
+ let options = null;
+ if (aSubject) {
+ options = aSubject.wrappedJSObject;
+ }
+ switch(aTopic) {
+ case "identity-request":
+ this.requestLogin(options);
+ break;
+ case "identity-auth":
+ this._openAuthenticationUI(aData, options);
+ break;
+ case "identity-auth-complete":
+ this._closeAuthenticationUI(aData);
+ break;
+ case "identity-login-state-changed":
+ let emailAddress = aData;
+ if (emailAddress) {
+ this._removeRequestUI(options);
+ this._showLoggedInUI(emailAddress, options);
+ } else {
+ this._removeLoggedInUI(options);
+ }
+ break;
+ default:
+ Logger.reportError("SignInToWebsiteUX", "Unknown observer notification:", aTopic);
+ break;
+ }
+ },
+
+ /**
+ * The website is requesting login so the user must choose an identity to use.
+ */
+ requestLogin: function SignInToWebsiteUX_requestLogin(aOptions) {
+ let windowID = aOptions.rpId;
+ log("requestLogin", aOptions);
+ let [chromeWin, browserEl] = this._getUIForWindowID(windowID);
+
+ // message is not shown in the UI but is required
+ let message = aOptions.origin;
+ let mainAction = {
+ label: chromeWin.gNavigatorBundle.getString("identity.next.label"),
+ accessKey: chromeWin.gNavigatorBundle.getString("identity.next.accessKey"),
+ callback: function() {}, // required
+ };
+ let options = {
+ identity: {
+ origin: aOptions.origin,
+ },
+ };
+ let secondaryActions = [];
+
+ // add some extra properties to the notification to store some identity-related state
+ for (let opt in aOptions) {
+ options.identity[opt] = aOptions[opt];
+ }
+ log("requestLogin: rpId: ", options.identity.rpId);
+
+ chromeWin.PopupNotifications.show(browserEl, "identity-request", message,
+ "identity-notification-icon", mainAction,
+ [], options);
+ },
+
+ /**
+ * Get the list of possible identities to login to the given origin.
+ */
+ getIdentitiesForSite: function SignInToWebsiteUX_getIdentitiesForSite(aOrigin) {
+ return IdentityService.RP.getIdentitiesForSite(aOrigin);
+ },
+
+ /**
+ * User chose a new or existing identity from the doorhanger after a request() call
+ */
+ selectIdentity: function SignInToWebsiteUX_selectIdentity(aRpId, aIdentity) {
+ log("selectIdentity: rpId: ", aRpId, " identity: ", aIdentity);
+ IdentityService.selectIdentity(aRpId, aIdentity);
+ },
+
+ // Private
+
+ /**
+ * Return the chrome window and <browser> for the given outer window ID.
+ */
+ _getUIForWindowID: function(aWindowID) {
+ let someWindow = Services.wm.getMostRecentWindow("navigator:browser");
+ if (!someWindow) {
+ Logger.reportError("SignInToWebsiteUX", "no window");
+ return [null, null];
+ }
+
+ let windowUtils = someWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ let content = windowUtils.getOuterWindowWithId(aWindowID);
+
+ if (content) {
+ let browser = content.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell).chromeEventHandler;
+ let chromeWin = browser.ownerDocument.defaultView;
+ return [chromeWin, browser];
+ }
+ Logger.reportError("SignInToWebsiteUX", "no content");
+
+ return [null, null];
+ },
+
+ /**
+ * Open UI with a content frame displaying aAuthURI so that the user can authenticate with their
+ * IDP. Then tell Identity.jsm the identifier for the window so that it knows that the DOM API
+ * calls are for this authentication flow.
+ */
+ _openAuthenticationUI: function _openAuthenticationUI(aAuthURI, aContext) {
+ // Open a tab/window with aAuthURI with an identifier (aID) attached so that the DOM APIs know this is an auth. window.
+ let chromeWin = Services.wm.getMostRecentWindow('navigator:browser');
+ let features = "chrome=false,width=640,height=480,centerscreen,location=yes,resizable=yes,scrollbars=yes,status=yes";
+ log("aAuthURI: ", aAuthURI);
+ let authWin = Services.ww.openWindow(chromeWin, "about:blank", "", features, null);
+ let windowID = authWin.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
+ log("authWin outer id: ", windowID);
+
+ let provId = aContext.provId;
+ // Tell the ID service about the id before loading the url
+ IdentityService.IDP.setAuthenticationFlow(windowID, provId);
+
+ authWin.location = aAuthURI;
+ },
+
+ _closeAuthenticationUI: function _closeAuthenticationUI(aAuthId) {
+ log("_closeAuthenticationUI:", aAuthId);
+ let [chromeWin, browserEl] = this._getUIForWindowID(aAuthId);
+ if (chromeWin)
+ chromeWin.close();
+ else
+ Logger.reportError("SignInToWebsite", "Could not close window with ID", aAuthId);
+ },
+
+ /**
+ * Show a doorhanger indicating the currently logged-in user.
+ */
+ _showLoggedInUI: function _showLoggedInUI(aIdentity, aContext) {
+ let windowID = aContext.rpId;
+ log("_showLoggedInUI for ", windowID);
+ let [chromeWin, browserEl] = this._getUIForWindowID(windowID);
+
+ let message = chromeWin.gNavigatorBundle.getFormattedString("identity.loggedIn.description",
+ [aIdentity]);
+ let mainAction = {
+ label: chromeWin.gNavigatorBundle.getString("identity.loggedIn.signOut.label"),
+ accessKey: chromeWin.gNavigatorBundle.getString("identity.loggedIn.signOut.accessKey"),
+ callback: function() {
+ log("sign out callback fired");
+ IdentityService.RP.logout(windowID);
+ },
+ };
+ let secondaryActions = [];
+ let options = {
+ dismissed: true,
+ };
+ let loggedInNot = chromeWin.PopupNotifications.show(browserEl, "identity-logged-in", message,
+ "identity-notification-icon", mainAction,
+ secondaryActions, options);
+ loggedInNot.rpId = windowID;
+ },
+
+ /**
+ * Remove the doorhanger indicating the currently logged-in user.
+ */
+ _removeLoggedInUI: function _removeLoggedInUI(aContext) {
+ let windowID = aContext.rpId;
+ log("_removeLoggedInUI for ", windowID);
+ if (!windowID)
+ throw "_removeLoggedInUI: Invalid RP ID";
+ let [chromeWin, browserEl] = this._getUIForWindowID(windowID);
+
+ let loggedInNot = chromeWin.PopupNotifications.getNotification("identity-logged-in", browserEl);
+ if (loggedInNot)
+ chromeWin.PopupNotifications.remove(loggedInNot);
+ },
+
+ /**
+ * Remove the doorhanger indicating the currently logged-in user.
+ */
+ _removeRequestUI: function _removeRequestUI(aContext) {
+ let windowID = aContext.rpId;
+ log("_removeRequestUI for ", windowID);
+ let [chromeWin, browserEl] = this._getUIForWindowID(windowID);
+
+ let requestNot = chromeWin.PopupNotifications.getNotification("identity-request", browserEl);
+ if (requestNot)
+ chromeWin.PopupNotifications.remove(requestNot);
+ },
+
+};
--- a/browser/modules/test/Makefile.in
+++ b/browser/modules/test/Makefile.in
@@ -9,18 +9,19 @@ VPATH = @srcdir@
relativesrcdir = @relativesrcdir@
include $(DEPTH)/config/autoconf.mk
include $(topsrcdir)/config/rules.mk
_BROWSER_FILES = \
browser_NetworkPrioritizer.js \
browser_TelemetryTimestamps.js \
+ browser_SignInToWebsite.js \
$(NULL)
-ifeq ($(MOZ_WIDGET_TOOLKIT),windows)
+ifeq ($(MOZ_WIDGET_TOOLKIT),windows)
_BROWSER_FILES += \
browser_taskbar_preview.js \
$(NULL)
endif
libs:: $(_BROWSER_FILES)
$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/browser_SignInToWebsite.js
@@ -0,0 +1,549 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * TO TEST:
+ * - test state saved on doorhanger dismissal
+ * - links to switch steps
+ * - TOS and PP link clicks
+ * - identityList is populated correctly
+ */
+
+Services.prefs.setBoolPref("toolkit.identity.debug", true);
+
+XPCOMUtils.defineLazyModuleGetter(this, "IdentityService",
+ "resource://gre/modules/identity/Identity.jsm");
+
+const TEST_ORIGIN = "https://example.com";
+const TEST_EMAIL = "user@example.com";
+
+let gTestIndex = 0;
+let outerWinId = gBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
+
+function NotificationBase(aNotId) {
+ this.id = aNotId;
+}
+NotificationBase.prototype = {
+ message: TEST_ORIGIN,
+ mainAction: {
+ label: "",
+ callback: function() {
+ this.mainActionClicked = true;
+ }.bind(this),
+ },
+ secondaryActions: [],
+ options: {
+ "identity": {
+ origin: TEST_ORIGIN,
+ rpId: outerWinId,
+ },
+ },
+};
+
+let tests = [
+ {
+ name: "test_request_required_typed",
+
+ run: function() {
+ setupRPFlow();
+ this.notifyOptions = {
+ rpId: outerWinId,
+ origin: TEST_ORIGIN,
+ };
+ this.notifyObj = new NotificationBase("identity-request");
+ Services.obs.notifyObservers({wrappedJSObject: this.notifyOptions},
+ "identity-request", null);
+ },
+
+ onShown: function(popup) {
+ checkPopup(popup, this.notifyObj);
+ let notification = popup.childNodes[0];
+
+ // Check identity popup state
+ let state = notification.identity;
+ ok(!state.typedEmail, "Nothing should be typed yet");
+ ok(!state.selected, "Identity should not be selected yet");
+ ok(!state.termsOfService, "No TOS specified");
+ ok(!state.privacyPolicy, "No PP specified");
+ is(state.step, 0, "Step should be persisted with default value");
+ is(state.rpId, outerWinId, "Check rpId");
+ is(state.origin, TEST_ORIGIN, "Check origin");
+
+ is(notification.step, 0, "Should be on the new email step");
+ is(notification.chooseEmailLink.hidden, true, "Identity list is empty so link to list view should be hidden");
+ is(notification.addEmailLink.parentElement.hidden, true, "We are already on the email input step so choose email pane should be hidden");
+ is(notification.emailField.value, "", "Email field should default to empty on a new notification");
+ let notifDoc = notification.ownerDocument;
+ ok(notifDoc.getAnonymousElementByAttribute(notification, "anonid", "tos").hidden,
+ "TOS link should be hidden");
+ ok(notifDoc.getAnonymousElementByAttribute(notification, "anonid", "privacypolicy").hidden,
+ "PP link should be hidden");
+
+ // Try to continue with a missing email address
+ triggerMainCommand(popup);
+ is(notification.throbber.style.visibility, "hidden", "is throbber visible");
+ ok(!notification.button.disabled, "Button should not be disabled");
+ is(window.gIdentitySelected, null, "Check no identity selected");
+
+ // Fill in an invalid email address and try again
+ notification.emailField.value = "foo";
+ triggerMainCommand(popup);
+ is(notification.throbber.style.visibility, "hidden", "is throbber visible");
+ ok(!notification.button.disabled, "Button should not be disabled");
+ is(window.gIdentitySelected, null, "Check no identity selected");
+
+ // Fill in an email address and try again
+ notification.emailField.value = TEST_EMAIL;
+ triggerMainCommand(popup);
+ is(window.gIdentitySelected.rpId, outerWinId, "Check identity selected rpId");
+ is(window.gIdentitySelected.identity, TEST_EMAIL, "Check identity selected email");
+ is(notification.identity.selected, TEST_EMAIL, "Check persisted email");
+ is(notification.throbber.style.visibility, "visible", "is throbber visible");
+ ok(notification.button.disabled, "Button should be disabled");
+ ok(notification.emailField.disabled, "Email field should be disabled");
+ ok(notification.identityList.disabled, "Identity list should be disabled");
+
+ PopupNotifications.getNotification("identity-request").remove();
+ },
+
+ onHidden: function(popup) { },
+ },
+ {
+ name: "test_request_optional",
+
+ run: function() {
+ this.notifyOptions = {
+ rpId: outerWinId,
+ origin: TEST_ORIGIN,
+ privacyPolicy: TEST_ORIGIN + "/pp.txt",
+ termsOfService: TEST_ORIGIN + "/tos.tzt",
+ };
+ this.notifyObj = new NotificationBase("identity-request");
+ Services.obs.notifyObservers({ wrappedJSObject: this.notifyOptions },
+ "identity-request", null);
+ },
+
+ onShown: function(popup) {
+ checkPopup(popup, this.notifyObj);
+ let notification = popup.childNodes[0];
+
+ // Check identity popup state
+ let state = notification.identity;
+ ok(!state.typedEmail, "Nothing should be typed yet");
+ ok(!state.selected, "Identity should not be selected yet");
+ is(state.termsOfService, this.notifyOptions.termsOfService, "Check TOS URL");
+ is(state.privacyPolicy, this.notifyOptions.privacyPolicy, "Check PP URL");
+ is(state.step, 0, "Step should be persisted with default value");
+ is(state.rpId, outerWinId, "Check rpId");
+ is(state.origin, TEST_ORIGIN, "Check origin");
+
+ is(notification.step, 0, "Should be on the new email step");
+ is(notification.chooseEmailLink.hidden, true, "Identity list is empty so link to list view should be hidden");
+ is(notification.addEmailLink.parentElement.hidden, true, "We are already on the email input step so choose email pane should be hidden");
+ is(notification.emailField.value, "", "Email field should default to empty on a new notification");
+ let notifDoc = notification.ownerDocument;
+ let tosLink = notifDoc.getAnonymousElementByAttribute(notification, "anonid", "tos");
+ ok(!tosLink.hidden, "TOS link should be visible");
+ is(tosLink.href, this.notifyOptions.termsOfService, "Check TOS link URL");
+ let ppLink = notifDoc.getAnonymousElementByAttribute(notification, "anonid", "privacypolicy");
+ ok(!ppLink.hidden, "PP link should be visible");
+ is(ppLink.href, this.notifyOptions.privacyPolicy, "Check PP link URL");
+
+ // Try to continue with a missing email address
+ triggerMainCommand(popup);
+ is(notification.throbber.style.visibility, "hidden", "is throbber visible");
+ ok(!notification.button.disabled, "Button should not be disabled");
+ is(window.gIdentitySelected, null, "Check no identity selected");
+
+ // Fill in an invalid email address and try again
+ notification.emailField.value = "foo";
+ triggerMainCommand(popup);
+ is(notification.throbber.style.visibility, "hidden", "is throbber visible");
+ ok(!notification.button.disabled, "Button should not be disabled");
+ is(window.gIdentitySelected, null, "Check no identity selected");
+
+ // Fill in an email address and try again
+ notification.emailField.value = TEST_EMAIL;
+ triggerMainCommand(popup);
+ is(window.gIdentitySelected.rpId, outerWinId, "Check identity selected rpId");
+ is(window.gIdentitySelected.identity, TEST_EMAIL, "Check identity selected email");
+ is(notification.identity.selected, TEST_EMAIL, "Check persisted email");
+ is(notification.throbber.style.visibility, "visible", "is throbber visible");
+ ok(notification.button.disabled, "Button should be disabled");
+ ok(notification.emailField.disabled, "Email field should be disabled");
+ ok(notification.identityList.disabled, "Identity list should be disabled");
+
+ PopupNotifications.getNotification("identity-request").remove();
+ },
+
+ onHidden: function(popup) {},
+ },
+ {
+ name: "test_login_state_changed",
+ run: function () {
+ this.notifyOptions = {
+ rpId: outerWinId,
+ };
+ this.notifyObj = new NotificationBase("identity-logged-in");
+ this.notifyObj.message = "Signed in as: user@example.com";
+ this.notifyObj.mainAction.label = "Sign Out";
+ this.notifyObj.mainAction.accessKey = "O";
+ Services.obs.notifyObservers({ wrappedJSObject: this.notifyOptions },
+ "identity-login-state-changed", TEST_EMAIL);
+ executeSoon(function() {
+ PopupNotifications.getNotification("identity-logged-in").anchorElement.click();
+ });
+ },
+
+ onShown: function(popup) {
+ checkPopup(popup, this.notifyObj);
+
+ // Fire the notification that the user is no longer logged-in to close the UI.
+ Services.obs.notifyObservers({ wrappedJSObject: this.notifyOptions },
+ "identity-login-state-changed", null);
+ },
+
+ onHidden: function(popup) {},
+ },
+ {
+ name: "test_login_state_changed_logout",
+ run: function () {
+ this.notifyOptions = {
+ rpId: outerWinId,
+ };
+ this.notifyObj = new NotificationBase("identity-logged-in");
+ this.notifyObj.message = "Signed in as: user@example.com";
+ this.notifyObj.mainAction.label = "Sign Out";
+ this.notifyObj.mainAction.accessKey = "O";
+ Services.obs.notifyObservers({ wrappedJSObject: this.notifyOptions },
+ "identity-login-state-changed", TEST_EMAIL);
+ executeSoon(function() {
+ PopupNotifications.getNotification("identity-logged-in").anchorElement.click();
+ });
+ },
+
+ onShown: function(popup) {
+ checkPopup(popup, this.notifyObj);
+
+ // This time trigger the Sign Out button and make sure the UI goes away.
+ triggerMainCommand(popup);
+ },
+
+ onHidden: function(popup) {},
+ },
+];
+
+function test_auth() {
+ let notifyOptions = {
+ provId: outerWinId,
+ origin: TEST_ORIGIN,
+ };
+
+ Services.obs.addObserver(function() {
+ // prepare to send auth-complete and close the window
+ let winCloseObs = new WindowObserver(function(closedWin) {
+ info("closed window");
+ finish();
+ }, "domwindowclosed");
+ Services.ww.registerNotification(winCloseObs);
+ Services.obs.notifyObservers(null, "identity-auth-complete", IdentityService.IDP.authenticationFlowSet.authId);
+
+ }, "test-identity-auth-window", false);
+
+ let winObs = new WindowObserver(function(authWin) {
+ ok(authWin, "Authentication window opened");
+ ok(authWin.contentWindow.location);
+ });
+
+ Services.ww.registerNotification(winObs);
+
+ Services.obs.notifyObservers({ wrappedJSObject: notifyOptions },
+ "identity-auth", TEST_ORIGIN + "/auth");
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ registerCleanupFunction(cleanUp);
+
+ let sitw = {};
+ Components.utils.import("resource:///modules/SignInToWebsite.jsm", sitw);
+
+ ok(sitw.SignInToWebsiteUX, "SignInToWebsiteUX object exists");
+
+ // Replace implementation of ID Service functions for testing
+ window.selectIdentity = sitw.SignInToWebsiteUX.selectIdentity;
+ sitw.SignInToWebsiteUX.selectIdentity = function(aRpId, aIdentity) {
+ info("Identity selected: " + aIdentity);
+ window.gIdentitySelected = {rpId: aRpId, identity: aIdentity};
+ };
+
+ window.setAuthenticationFlow = IdentityService.IDP.setAuthenticationFlow;
+ IdentityService.IDP.setAuthenticationFlow = function(aAuthId, aProvId) {
+ info("setAuthenticationFlow: " + aAuthId + " : " + aProvId);
+ this.authenticationFlowSet = { authId: aAuthId, provId: aProvId };
+ Services.obs.notifyObservers(null, "test-identity-auth-window", aAuthId);
+ };
+
+ runNextTest();
+}
+
+// Cleanup between tests
+function resetState() {
+ delete window.gIdentitySelected;
+ delete IdentityService.IDP.authenticationFlowSet;
+ IdentityService.reset();
+}
+
+// Cleanup after all tests
+function cleanUp() {
+ info("cleanup");
+ resetState();
+
+ for (let topic in gActiveObservers)
+ Services.obs.removeObserver(gActiveObservers[topic], topic);
+ for (let eventName in gActiveListeners)
+ PopupNotifications.panel.removeEventListener(eventName, gActiveListeners[eventName], false);
+ delete IdentityService.RP._rpFlows[outerWinId];
+
+ // Put the JSM functions back to how they were
+ IdentityService.IDP.setAuthenticationFlow = window.setAuthenticationFlow;
+ delete window.setAuthenticationFlow;
+
+ let sitw = {};
+ Components.utils.import("resource:///modules/SignInToWebsite.jsm", sitw);
+ sitw.SignInToWebsiteUX.selectIdentity = window.selectIdentity;
+ delete window.selectIdentity;
+
+ Services.prefs.clearUserPref("toolkit.identity.debug");
+}
+
+let gActiveListeners = {};
+let gActiveObservers = {};
+let gShownState = {};
+
+function runNextTest() {
+ let nextTest = tests[gTestIndex];
+
+ function goNext() {
+ resetState();
+ if (++gTestIndex == tests.length)
+ executeSoon(test_auth);
+ else
+ executeSoon(runNextTest);
+ }
+
+ function addObserver(topic) {
+ function observer() {
+ Services.obs.removeObserver(observer, "PopupNotifications-" + topic);
+ delete gActiveObservers["PopupNotifications-" + topic];
+
+ info("[Test #" + gTestIndex + "] observer for " + topic + " called");
+ nextTest[topic]();
+ goNext();
+ }
+ Services.obs.addObserver(observer, "PopupNotifications-" + topic, false);
+ gActiveObservers["PopupNotifications-" + topic] = observer;
+ }
+
+ if (nextTest.backgroundShow) {
+ addObserver("backgroundShow");
+ } else if (nextTest.updateNotShowing) {
+ addObserver("updateNotShowing");
+ } else {
+ doOnPopupEvent("popupshowing", function () {
+ info("[Test #" + gTestIndex + "] popup showing");
+ });
+ doOnPopupEvent("popupshown", function () {
+ gShownState[gTestIndex] = true;
+ info("[Test #" + gTestIndex + "] popup shown");
+ nextTest.onShown(this);
+ });
+
+ // We allow multiple onHidden functions to be defined in an array. They're
+ // called in the order they appear.
+ let onHiddenArray = nextTest.onHidden instanceof Array ?
+ nextTest.onHidden :
+ [nextTest.onHidden];
+ doOnPopupEvent("popuphidden", function () {
+ if (!gShownState[gTestIndex]) {
+ // TODO: needed?
+ info("Popup from test " + gTestIndex + " was hidden before its popupshown fired");
+ }
+
+ let onHidden = onHiddenArray.shift();
+ info("[Test #" + gTestIndex + "] popup hidden (" + onHiddenArray.length + " hides remaining)");
+ executeSoon(function () {
+ onHidden.call(nextTest, this);
+ if (!onHiddenArray.length)
+ goNext();
+ }.bind(this));
+ }, onHiddenArray.length);
+ info("[Test #" + gTestIndex + "] added listeners; panel state: " + PopupNotifications.isPanelOpen);
+ }
+
+ info("[Test #" + gTestIndex + "] running test");
+ nextTest.run();
+}
+
+function doOnPopupEvent(eventName, callback, numExpected) {
+ gActiveListeners[eventName] = function (event) {
+ if (event.target != PopupNotifications.panel)
+ return;
+ if (typeof(numExpected) === "number")
+ numExpected--;
+ if (!numExpected) {
+ PopupNotifications.panel.removeEventListener(eventName, gActiveListeners[eventName], false);
+ delete gActiveListeners[eventName];
+ }
+
+ callback.call(PopupNotifications.panel);
+ };
+ PopupNotifications.panel.addEventListener(eventName, gActiveListeners[eventName], false);
+}
+
+function checkPopup(popup, notificationObj) {
+ info("[Test #" + gTestIndex + "] checking popup");
+
+ let notifications = popup.childNodes;
+ is(notifications.length, 1, "only one notification displayed");
+ let notification = notifications[0];
+ let icon = document.getAnonymousElementByAttribute(notification, "class", "popup-notification-icon");
+ is(notification.getAttribute("label"), notificationObj.message, "message matches");
+ is(notification.id, notificationObj.id + "-notification", "id matches");
+ if (notificationObj.id != "identity-request" && notificationObj.mainAction) {
+ is(notification.getAttribute("buttonlabel"), notificationObj.mainAction.label, "main action label matches");
+ is(notification.getAttribute("buttonaccesskey"), notificationObj.mainAction.accessKey, "main action accesskey matches");
+ }
+ let actualSecondaryActions = notification.childNodes;
+ let secondaryActions = notificationObj.secondaryActions || [];
+ let actualSecondaryActionsCount = actualSecondaryActions.length;
+ if (secondaryActions.length) {
+ let lastChild = actualSecondaryActions.item(actualSecondaryActions.length - 1);
+ is(lastChild.tagName, "menuseparator", "menuseparator exists");
+ actualSecondaryActionsCount--;
+ }
+ is(actualSecondaryActionsCount, secondaryActions.length, actualSecondaryActions.length + " secondary actions");
+ secondaryActions.forEach(function (a, i) {
+ is(actualSecondaryActions[i].getAttribute("label"), a.label, "label for secondary action " + i + " matches");
+ is(actualSecondaryActions[i].getAttribute("accesskey"), a.accessKey, "accessKey for secondary action " + i + " matches");
+ });
+}
+
+function triggerMainCommand(popup) {
+ info("[Test #" + gTestIndex + "] triggering main command");
+ let notifications = popup.childNodes;
+ ok(notifications.length > 0, "at least one notification displayed");
+ let notification = notifications[0];
+
+ // 20, 10 so that the inner button is hit
+ EventUtils.synthesizeMouse(notification.button, 20, 10, {});
+}
+
+function triggerSecondaryCommand(popup, index) {
+ info("[Test #" + gTestIndex + "] triggering secondary command");
+ let notifications = popup.childNodes;
+ ok(notifications.length > 0, "at least one notification displayed");
+ let notification = notifications[0];
+
+ notification.button.focus();
+
+ popup.addEventListener("popupshown", function () {
+ popup.removeEventListener("popupshown", arguments.callee, false);
+
+ // Press down until the desired command is selected
+ for (let i = 0; i <= index; i++)
+ EventUtils.synthesizeKey("VK_DOWN", {});
+
+ // Activate
+ EventUtils.synthesizeKey("VK_ENTER", {});
+ }, false);
+
+ // One down event to open the popup
+ EventUtils.synthesizeKey("VK_DOWN", { altKey: (navigator.platform.indexOf("Mac") == -1) });
+}
+
+function dismissNotification(popup) {
+ info("[Test #" + gTestIndex + "] dismissing notification");
+ executeSoon(function () {
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ });
+}
+
+function partial(fn) {
+ let args = Array.prototype.slice.call(arguments, 1);
+ return function() {
+ return fn.apply(this, args.concat(Array.prototype.slice.call(arguments)));
+ };
+}
+
+// create a mock "doc" object, which the Identity Service
+// uses as a pointer back into the doc object
+function mock_doc(aIdentity, aOrigin, aDoFunc) {
+ let mockedDoc = {};
+ mockedDoc.id = outerWinId;
+ mockedDoc.loggedInEmail = aIdentity;
+ mockedDoc.origin = aOrigin;
+ mockedDoc['do'] = aDoFunc;
+ mockedDoc.doReady = partial(aDoFunc, 'ready');
+ mockedDoc.doLogin = partial(aDoFunc, 'login');
+ mockedDoc.doLogout = partial(aDoFunc, 'logout');
+ mockedDoc.doError = partial(aDoFunc, 'error');
+ mockedDoc.doCancel = partial(aDoFunc, 'cancel');
+ mockedDoc.doCoffee = partial(aDoFunc, 'coffee');
+
+ return mockedDoc;
+}
+
+// takes a list of functions and returns a function that
+// when called the first time, calls the first func,
+// then the next time the second, etc.
+function call_sequentially() {
+ let numCalls = 0;
+ let funcs = arguments;
+
+ return function() {
+ if (!funcs[numCalls]) {
+ let argString = Array.prototype.slice.call(arguments).join(",");
+ ok(false, "Too many calls: " + argString);
+ return;
+ }
+ funcs[numCalls].apply(funcs[numCalls], arguments);
+ numCalls += 1;
+ };
+}
+
+function setupRPFlow(aIdentity) {
+ IdentityService.RP.watch(mock_doc(aIdentity, TEST_ORIGIN, call_sequentially(
+ function(action, params) {
+ is(action, "ready", "1st callback");
+ is(params, null);
+ },
+ function(action, params) {
+ is(action, "logout", "2nd callback");
+ is(params, null);
+ },
+ function(action, params) {
+ is(action, "ready", "3rd callback");
+ is(params, null);
+ }
+ )));
+}
+
+function WindowObserver(aCallback, aObserveTopic = "domwindowopened") {
+ this.observe = function(aSubject, aTopic, aData) {
+ if (aTopic != aObserveTopic) {
+ return;
+ }
+ info(aObserveTopic);
+ Services.ww.unregisterNotification(this);
+
+ SimpleTest.executeSoon(function() {
+ let domWin = aSubject.QueryInterface(Ci.nsIDOMWindow);
+ aCallback(domWin);
+ });
+ };
+}
--- a/browser/themes/gnomestripe/browser.css
+++ b/browser/themes/gnomestripe/browser.css
@@ -1235,16 +1235,20 @@ toolbar[iconsize="small"] #feed-button {
.notification-anchor-icon:-moz-focusring {
outline: 1px dotted -moz-DialogText;
}
#default-notification-icon {
list-style-image: url(chrome://global/skin/icons/information-16.png);
}
+#identity-notification-icon {
+ list-style-image: url(chrome://mozapps/skin/profile/profileicon.png);
+}
+
#geo-notification-icon {
list-style-image: url(chrome://browser/skin/Geolocation-16.png);
}
#addons-notification-icon {
list-style-image: url(chrome://mozapps/skin/extensions/extensionGeneric-16.png);
}
--- a/browser/themes/pinstripe/browser.css
+++ b/browser/themes/pinstripe/browser.css
@@ -2364,22 +2364,29 @@ toolbarbutton.chevron > .toolbarbutton-m
box-shadow: 0 0 2px 1px -moz-mac-focusring inset,
0 0 3px 2px -moz-mac-focusring;
}
#default-notification-icon {
list-style-image: url(chrome://global/skin/icons/information-16.png);
}
+#identity-notification-icon {
+ list-style-image: url(chrome://mozapps/skin/profile/profileicon.png);
+}
+
#geo-notification-icon {
list-style-image: url(chrome://browser/skin/Geolocation-16.png);
}
+#notification-popup .text-link {
+ color: #fff;
+}
+
.geolocation-text-link {
- color: #fff;
-moz-margin-start: 0; /* override default label margin to match description margin */
}
.telemetry-text-link {
color: #fff;
}
#addons-notification-icon {
--- a/browser/themes/winstripe/browser.css
+++ b/browser/themes/winstripe/browser.css
@@ -2370,16 +2370,20 @@ toolbarbutton.bookmark-item[dragover="tr
outline: 1px dotted -moz-DialogText;
outline-offset: -3px;
}
#default-notification-icon {
list-style-image: url(chrome://global/skin/icons/information-16.png);
}
+#identity-notification-icon {
+ list-style-image: url(chrome://mozapps/skin/profile/profileicon.png);
+}
+
#geo-notification-icon {
list-style-image: url(chrome://browser/skin/Geolocation-16.png);
}
#addons-notification-icon {
list-style-image: url(chrome://mozapps/skin/extensions/extensionGeneric-16.png);
}
--- a/toolkit/identity/RelyingParty.jsm
+++ b/toolkit/identity/RelyingParty.jsm
@@ -205,23 +205,23 @@ IdentityRelyingParty.prototype = {
* @param aRPId
* (integer) the id of the doc object obtained in .watch()
*
* @param aOptions
* (Object) options including privacyPolicy, termsOfService
*/
request: function request(aRPId, aOptions) {
log("request: rpId:", aRPId);
+ let rp = this._rpFlows[aRPId];
// Notify UX to display identity picker.
// Pass the doc id to UX so it can pass it back to us later.
- let options = {rpId: aRPId};
+ let options = {rpId: aRPId, origin: rp.origin};
// Append URLs after resolving
- let rp = this._rpFlows[aRPId];
let baseURI = Services.io.newURI(rp.origin, null, null);
for (let optionName of ["privacyPolicy", "termsOfService"]) {
if (aOptions[optionName]) {
options[optionName] = baseURI.resolve(aOptions[optionName]);
}
}
Services.obs.notifyObservers({wrappedJSObject: options}, "identity-request", null);
--- a/toolkit/identity/tests/chrome/test_sandbox.xul
+++ b/toolkit/identity/tests/chrome/test_sandbox.xul
@@ -212,17 +212,17 @@ function WindowObserver(aCallback) {
this.observe = function(aSubject, aTopic, aData) {
if (aTopic != "domwindowopened") {
return;
}
Services.ww.unregisterNotification(this);
let domWin = aSubject.QueryInterface(Ci.nsIDOMWindow);
ok(!domWin, "No window should be opened");
- SimpleTest.executesoon(function() {
+ SimpleTest.executeSoon(function() {
info("Closing opened window");
domWin.close();
aCallback();
});
}
}
// Can the sandbox call window.alert() or popup other UI?
--- a/toolkit/identity/tests/mochitest/head_identity.js
+++ b/toolkit/identity/tests/mochitest/head_identity.js
@@ -24,16 +24,27 @@ const TEST_IDPPARAMS = {
};
const Services = Cu.import("resource://gre/modules/Services.jsm").Services;
// Set the debug pref before loading other modules
SpecialPowers.setBoolPref("toolkit.identity.debug", true);
SpecialPowers.setBoolPref("dom.identity.enabled", true);
+// Shutdown the UX if it exists so that it won't interfere with tests by also responding to
+// observer notifications.
+try {
+ const SignInToWebsiteUX = Cu.import("resource:///modules/SignInToWebsite.jsm").SignInToWebsiteUX;
+ if (SignInToWebsiteUX) {
+ SignInToWebsiteUX.uninit();
+ }
+} catch (ex) {
+ // The module doesn't exist
+}
+
const jwcrypto = Cu.import("resource://gre/modules/identity/jwcrypto.jsm").jwcrypto;
const IdentityStore = Cu.import("resource://gre/modules/identity/IdentityStore.jsm").IdentityStore;
const RelyingParty = Cu.import("resource://gre/modules/identity/RelyingParty.jsm").RelyingParty;
const XPCOMUtils = Cu.import("resource://gre/modules/XPCOMUtils.jsm").XPCOMUtils;
const IDService = Cu.import("resource://gre/modules/identity/Identity.jsm").IdentityService;
const IdentityProvider = Cu.import("resource://gre/modules/identity/IdentityProvider.jsm").IdentityProvider;
const identity = navigator.id || navigator.mozId;
@@ -170,16 +181,25 @@ function setup_provisioning(identity, af
function resetState() {
IDService.reset();
}
function cleanup() {
resetState();
SpecialPowers.clearUserPref("toolkit.identity.debug");
SpecialPowers.clearUserPref("dom.identity.enabled");
+ // Re-init the UX that we uninit
+ try {
+ const SignInToWebsiteUX = Cu.import("resource:///modules/SignInToWebsite.jsm").SignInToWebsiteUX;
+ if (SignInToWebsiteUX) {
+ SignInToWebsiteUX.init();
+ }
+ } catch (ex) {
+ // The module doesn't exist
+ }
}
var TESTS = [];
function run_next_test() {
if (!identity) {
todo(false, "DOM API is not available. Skipping tests.");
cleanup();
--- a/toolkit/identity/tests/mochitest/test_relying_party.html
+++ b/toolkit/identity/tests/mochitest/test_relying_party.html
@@ -24,17 +24,17 @@ Test of Relying Party (RP) using the DOM
"use strict";
SimpleTest.waitForExplicitFinish();
const DOMIdentity = Cu.import("resource://gre/modules/DOMIdentity.jsm")
.DOMIdentity;
let outerWinId = window.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
+ .getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
// Reset the DOM state then run the next test
function run_next_rp_test() {
let rpContext = RelyingParty._rpFlows[outerWinId];
if (rpContext) {
makeObserver("identity-DOM-state-reset", function() {
SimpleTest.executeSoon(run_next_test);
});