diff --git a/resources/static/dialog/resources/channel.js b/resources/static/dialog/resources/channel.js index 2353280621ae29b22eba36facf94db4c1db2f6d5..99dd6d836630eb04dde0d573a6b696f4ba87672e 100644 --- a/resources/static/dialog/resources/channel.js +++ b/resources/static/dialog/resources/channel.js @@ -55,6 +55,7 @@ var win = window, nav = navigator, onCompleteCallback, + _relayName, _relayWindow = null; function getRelayName() { @@ -67,8 +68,9 @@ } function getRelayWindow() { - if (!_relayWindow) - _relayWindow = win.opener.frames[getRelayName()]; + if (!_relayWindow) { + _relayWindow = win.opener.frames[_relayName]; + } return _relayWindow; } @@ -80,28 +82,34 @@ // TODO - Add a check for whether the dialog was opened by another window // (has window.opener) as well as whether the relay function exists. // If these conditions are not met, then print an appropriate message. - - var REGISTERED_METHODS = { - 'get': function(origin, params, onsuccess, onerror) { - // check for old controller methods - // FIXME KILL THIS SOON - if (controller.get) { - return controller.get(origin, params, onsuccess, onerror); - } else { - return controller.getVerifiedEmail(origin, onsuccess, onerror); + _relayName = getRelayName(); + if(_relayName !== "NATIVE") { + var REGISTERED_METHODS = { + 'get': function(origin, params, onsuccess, onerror) { + // check for old controller methods + // FIXME KILL THIS SOON + if (controller.get) { + return controller.get(origin, params, onsuccess, onerror); + } else { + return controller.getVerifiedEmail(origin, onsuccess, onerror); + } } + }; + + // The relay frame will give us the origin and a function to call when + // dialog processing is complete. + var frameWindow = getRelayWindow(); + + if (frameWindow) { + frameWindow.BrowserID.Relay.registerClient(REGISTERED_METHODS); + } + else { + // Only run if we are searching for the IFRAME. If the relay name is + // NATIVE, that means that either the channel has already been set up + // before this or it will be set up after this. Since the relay name is + // native, do not throw an exception. + throw "relay frame not found"; } - }; - - // The relay frame will give us the origin and a function to call when - // dialog processing is complete. - var frameWindow = getRelayWindow(); - - if (frameWindow) { - frameWindow.BrowserID.Relay.registerClient(REGISTERED_METHODS); - } - else { - throw "relay frame not found"; } } @@ -114,14 +122,14 @@ function close() { var frameWindow = getRelayWindow(); - + if (frameWindow) { frameWindow.BrowserID.Relay.unregisterClient(); _relayWindow = null; } else { throw "relay frame not found"; - } + } } function init(options) { @@ -140,7 +148,7 @@ if(window.BrowserID) { BrowserID.Channel = { /** - * Used to intialize the channel, mostly for unit testing to override + * Used to intialize the channel, mostly for unit testing to override * window and navigator. * @method init */ @@ -148,7 +156,7 @@ /** * Open the channel. - * @method open + * @method open * @param {object} options - contains: * * options.getVerifiedEmail {function} - function to /get */ @@ -162,7 +170,7 @@ } /** - * This is here as a legacy API for addons/etc that are depending on + * This is here as a legacy API for addons/etc that are depending on * window.setupChannel; */ window.setupChannel = open; diff --git a/resources/static/dialog/resources/internal_api.js b/resources/static/dialog/resources/internal_api.js new file mode 100644 index 0000000000000000000000000000000000000000..29bdd85d297ac251f83a7dd289b8b0ec85057f79 --- /dev/null +++ b/resources/static/dialog/resources/internal_api.js @@ -0,0 +1,140 @@ +/*jshint browsers:true, forin: true, laxbreak: true */ +/*global BrowserID: true*/ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla BrowserID. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +(function() { + "use strict"; + + var nav = navigator, + bid = BrowserID, + internal = bid.internal = bid.internal || {}, + user = bid.User, + storage = bid.Storage, + moduleManager = bid.module; + + /** + * Set the persistent flag to true for an origin. + * @method setPersistent + * @param {string} origin + * @param {string} email + * @param {callback} [callback] - callback to call when complete. Called + * with true if successful, null if user is not authenticated or failure. + */ + internal.setPersistent = function(origin, callback) { + function complete(status) { + callback && callback(status); + } + + user.checkAuthentication(function onComplete(authenticated) { + if(authenticated) { + storage.site.set(origin, "remember", true); + } + + complete(authenticated || null); + }, complete.bind(null, null)); + }; + + /** + * Get an assertion. Mimics the behavior of navigator.id.get. + * options.silent defaults to true. To use the dialog to get an assertion, + * set options.silent to false. To specify a required email, set + * options.requiredEmail. By specifying both silent:true and requiredEmail: + * <email>, an assertion will be attempted to be retreived for the given + * email. + * @method get + * @param {string} origin + * @param {object} options. See options block for navigator.id.get. + * options.silent defaults to true. + * @param {function} callback - called when complete. Called with assertion + * if success, null if the user cancels. Other conditions causing null + * return value: silent is true and user is not authenticated. silent is + * true, requiredEmail is specified but user does not control email. + */ + internal.get = function(origin, options, callback) { + function complete(assertion) { + // If no assertion, give no reason why there was a failure. + callback && callback(assertion || null); + } + + var silent = options.silent !== false; + if(silent) { + // first, check the required email field, if that is not specified, go + // check if an email is associated with this site. If that is not + // available, there is not enough information to continue. + var requiredEmail = options.requiredEmail || storage.site.get(origin, "email"); + if(requiredEmail) { + getSilent(origin, requiredEmail, callback); + } + else { + complete(); + } + } + else { + // Use the standard dialog facilities to get the assertion, pass the + // options block directly to the dialog. + var controller = moduleManager.getModule("dialog"); + if(controller) { + controller.get(origin, options, complete, complete); + } + else { + complete(); + } + } + }; + + /* + * Get an assertion without user interaction - internal use + */ + function getSilent(origin, email, callback) { + function complete(assertion) { + callback && callback(assertion); + } + + user.checkAuthenticationAndSync(function onSuccess() {}, function onComplete(authenticated) { + // User must be authenticated to get an assertion. + if(authenticated) { + user.setOrigin(origin); + user.getAssertion(email, function(assertion) { + complete(assertion || null); + }, complete.bind(null, null)); + } + else { + complete(null); + } + }, complete.bind(null, null)); + } + +}()); diff --git a/resources/static/shared/user.js b/resources/static/shared/user.js index fa362732edbda0cb5493509ae39dd5ecea55a78e..c093b48de9e41c4f2bc194a0af9b4d21806cdd01 100644 --- a/resources/static/shared/user.js +++ b/resources/static/shared/user.js @@ -642,7 +642,8 @@ BrowserID.User = (function() { // to avoid issues with clock drift on user's machine. // (issue #329) var storedID = storage.getEmail(email), - assertion; + assertion, + self=this; function createAssertion(idInfo) { network.serverTime(function(serverTime) { @@ -658,6 +659,7 @@ BrowserID.User = (function() { // yield! setTimeout(function() { assertion = vep.bundleCertsAndAssertion([idInfo.cert], tok.sign(sk)); + storage.site.set(self.getOrigin(), "email", email); if (onSuccess) { onSuccess(assertion); } diff --git a/resources/static/test/qunit.html b/resources/static/test/qunit.html index b05ae11393b474a044a436b03e3c83b798513a02..c145491f268649d29747f443ebe408542cce6a13 100644 --- a/resources/static/test/qunit.html +++ b/resources/static/test/qunit.html @@ -102,6 +102,7 @@ <script type="text/javascript" src="/shared/validation.js"></script> <script type="text/javascript" src="/shared/helpers.js"></script> + <script type="text/javascript" src="/dialog/resources/internal_api.js"></script> <script type="text/javascript" src="/dialog/resources/channel.js"></script> <script type="text/javascript" src="/dialog/resources/helpers.js"></script> <script type="text/javascript" src="/dialog/resources/state_machine.js"></script> @@ -148,6 +149,7 @@ <script type="text/javascript" src="qunit/shared/network_unit_test.js"></script> <script type="text/javascript" src="qunit/shared/user_unit_test.js"></script> + <script type="text/javascript" src="qunit/resources/internal_api_unit_test.js"></script> <script type="text/javascript" src="qunit/resources/helpers_unit_test.js"></script> <script type="text/javascript" src="qunit/resources/state_machine_unit_test.js"></script> <script type="text/javascript" src="qunit/resources/channel_unit_test.js"></script> diff --git a/resources/static/test/qunit/resources/channel_unit_test.js b/resources/static/test/qunit/resources/channel_unit_test.js index 65bf19189c47f8b82de9457adebdc0f63734be83..878f2acbe0de4fe59db27a0b5052980c717e1536 100644 --- a/resources/static/test/qunit/resources/channel_unit_test.js +++ b/resources/static/test/qunit/resources/channel_unit_test.js @@ -132,6 +132,16 @@ }); }); + test("IFRAME channel with #NATIVE channel specified", function() { + winMock.location.hash = "#NATIVE"; + + channel.open({ + getVerifiedEmail: function(origin, onsuccess, onerror) { + ok(false, "getVerifiedEmail should not be called with a native channel"); + } + }); + }); + asyncTest("IFRAME channel with error on open", function() { delete winMock.opener.frames['1234']; diff --git a/resources/static/test/qunit/resources/internal_api_unit_test.js b/resources/static/test/qunit/resources/internal_api_unit_test.js new file mode 100644 index 0000000000000000000000000000000000000000..e4d01bedc41e9c9259b0f83c0b123fbb890aaf6c --- /dev/null +++ b/resources/static/test/qunit/resources/internal_api_unit_test.js @@ -0,0 +1,220 @@ +/*jshint browsers:true, forin: true, laxbreak: true */ +/*global test: true, start: true, module: true, ok: true, equal: true, BrowserID:true */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla BrowserID. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ +(function() { + "use strict"; + + var bid = BrowserID, + internal = bid.internal, + network = bid.Network, + user = bid.User, + xhr = bid.Mocks.xhr, + origin = "https://browserid.org", + storage = bid.Storage, + moduleManager = bid.module; + + module("resources/internal_api", { + setup: function() { + network.setXHR(xhr); + xhr.useResult("valid"); + xhr.setContextInfo("authenticated", false); + storage.clear(); + }, + + teardown: function() { + network.setXHR($); + } + }); + + test("make sure internal api namespace is there", function() { + ok(bid.internal, "BrowserID.internal exists"); + }); + + asyncTest("BrowserID.internal.setPersistent with the user not authenticated", function() { + internal.setPersistent(origin, function(status) { + strictEqual(status, null, "user is not authenticated should not succeed in setting persistent"); + + strictEqual(typeof storage.site.get(origin, "remember"), "undefined", "remember status not set"); + strictEqual(typeof storage.site.get(origin, "email"), "undefined", "email not set"); + start(); + }); + }); + + asyncTest("BrowserID.internal.setPersistent with authenticated user", function() { + user.authenticate("testuser@testuser.com", "password", function() { + internal.setPersistent(origin, function(status) { + equal(status, true, "setPersistent status reported as true"); + + equal(storage.site.get(origin, "remember"), true, "remember status set to true"); + start(); + }); + }); + }); + + asyncTest("BrowserID.internal.get with silent: true, non-authenticated user - returns null assertion", function() { + internal.get(origin, { + requiredEmail: "testuser@testuser.com" + }, function(assertion) { + strictEqual(assertion, null, "user not logged in, assertion impossible to get"); + start(); + }); + }); + + asyncTest("BrowserID.internal.get with silent: true, authenticated user, no requiredEmail, and no email address associated with site - not enough info to generate an assertion", function() { + user.authenticate("testuser@testuser.com", "password", function() { + internal.get(origin, {}, function(assertion) { + strictEqual(assertion, null, "not enough info to generate an assertion, assertion should not be generated"); + start(); + }); + }); + }); + + asyncTest("BrowserID.internal.get with silent: true, authenticated user, no requiredEmail, email address associated with site, XHR failure - return null assertion.", function() { + user.authenticate("testuser@testuser.com", "password", function() { + user.syncEmails(function() { + storage.site.set(origin, "email", "testuser@testuser.com"); + + xhr.useResult("invalid"); + + internal.get(origin, {}, function(assertion) { + strictEqual(assertion, null, "XHR failure while getting assertion"); + start(); + }); + }); + }); + }); + + asyncTest("BrowserID.internal.get with silent: true, authenticated user, no requiredEmail, email address associated with site - use info stored for site to get assertion", function() { + user.authenticate("testuser@testuser.com", "password", function() { + user.syncEmails(function() { + storage.site.set(origin, "email", "testuser@testuser.com"); + + internal.get(origin, {}, function(assertion) { + ok(assertion, "assertion generated using stored email address for site."); + start(); + }); + }); + }); + }); + + asyncTest("BrowserID.internal.get with silent: true, authenticated user, requiredEmail set to uncontrolled email address - return null assertion", function() { + user.authenticate("testuser@testuser.com", "password", function() { + // email addresses will not be synced just because we authenticated. + // Depending on get to do the sync. + internal.get(origin, { + requiredEmail: "invalid@testuser.com" + }, function(assertion) { + strictEqual(assertion, null, "uncontrolled email address returns null assertion"); + start(); + }); + }); + }); + + asyncTest("BrowserID.internal.get with silent: true, authenticated user, requiredEmail and XHR error - return null assertion", function() { + user.authenticate("testuser@testuser.com", "password", function() { + xhr.useResult("invalid"); + internal.get(origin, { + requiredEmail: "invalid@testuser.com" + }, function(assertion) { + strictEqual(assertion, null, "unregistered email address returns null assertion"); + start(); + }); + }); + }); + + asyncTest("BrowserID.internal.get with silent: true, authenticated user, requiredEmail, and registered email address - return an assertion", function() { + user.authenticate("testuser@testuser.com", "password", function() { + internal.get(origin, { + requiredEmail: "testuser@testuser.com" + }, function(assertion) { + ok(assertion, "assertion has been returned"); + start(); + }); + }); + }); + + asyncTest("BrowserID.internal.get with dialog - return an assertion", function() { + moduleManager.reset(); + + moduleManager.register("dialog", { + get: function(getOrigin, options, onsuccess, onerror) { + equal(getOrigin, origin, "correct origin passed"); + onsuccess("assertion"); + } + }); + + internal.get(origin, { + silent: false + }, function onComplete(assertion) { + equal(assertion, "assertion", "Kosher assertion"); + start(); + }); + }); + + asyncTest("BrowserID.internal.get with dialog with failure - return null assertion", function() { + moduleManager.reset(); + + moduleManager.register("dialog", { + get: function(getOrigin, options, onsuccess, onerror) { + onerror(); + } + }); + + internal.get(origin, { + silent: false + }, function onComplete(assertion) { + equal(assertion, null, "on failure, assertion is null"); + start(); + }); + }); + + asyncTest("BrowserID.internal.get with dialog with user cancellation - return null assertion", function() { + moduleManager.reset(); + + moduleManager.register("dialog", { + get: function(getOrigin, options, onsuccess, onerror) { + onsuccess(null); + } + }); + + internal.get(origin, { + silent: false + }, function onComplete(assertion) { + equal(assertion, null, "on cancel, assertion is null"); + start(); + }); + }); +}()); diff --git a/resources/static/test/qunit/shared/user_unit_test.js b/resources/static/test/qunit/shared/user_unit_test.js index b2790de56a0a5981215e84b463107f3707530a86..e7c7e5b63dfc7c696973a1fd843d9f7aa2c3bcf3 100644 --- a/resources/static/test/qunit/shared/user_unit_test.js +++ b/resources/static/test/qunit/shared/user_unit_test.js @@ -96,6 +96,7 @@ var jwcert = require("./jwcert"); xhr.useResult("valid"); lib.clearStoredEmailKeypairs(); lib.setOrigin(testOrigin); + storage.site.remove(testOrigin, "email"); }, teardown: function() { network.setXHR($); @@ -787,6 +788,7 @@ var jwcert = require("./jwcert"); lib.syncEmailKeypair("testuser@testuser.com", function() { lib.getAssertion("testuser@testuser.com", function onSuccess(assertion) { testAssertion(assertion, start); + equal(storage.site.get(testOrigin, "email"), "testuser@testuser.com", "email address was persisted"); }, failure("getAssertion failure")); }, failure("syncEmailKeypair failure")); }); @@ -798,6 +800,7 @@ var jwcert = require("./jwcert"); storage.addEmail("testuser@testuser.com", {}); lib.getAssertion("testuser@testuser.com", function onSuccess(assertion) { testAssertion(assertion, start); + equal(storage.site.get(testOrigin, "email"), "testuser@testuser.com", "email address was persisted"); }, failure("getAssertion failure")); }); @@ -806,6 +809,7 @@ var jwcert = require("./jwcert"); lib.syncEmailKeypair("testuser@testuser.com", function() { lib.getAssertion("testuser2@testuser.com", function onSuccess(assertion) { equal("undefined", typeof assertion, "email was unknown, we do not have an assertion"); + equal(storage.site.get(testOrigin, "email"), undefined, "email address was not set"); start(); }); }, failure("getAssertion failure")); @@ -840,7 +844,7 @@ var jwcert = require("./jwcert"); }, failure("syncEmails failure")); }, failure("authenticate failure")); - + }); asyncTest("logoutUser with XHR failure", function(onSuccess) { @@ -860,7 +864,7 @@ var jwcert = require("./jwcert"); }, failure("syncEmails failure")); }, failure("authenticate failure")); - + }); asyncTest("cancelUser", function(onSuccess) { @@ -870,7 +874,7 @@ var jwcert = require("./jwcert"); start(); }); - + }); asyncTest("cancelUser with XHR failure", function(onSuccess) { @@ -883,7 +887,7 @@ var jwcert = require("./jwcert"); start(); }); - + }); asyncTest("getPersistentSigninAssertion with invalid login", function() { @@ -903,7 +907,7 @@ var jwcert = require("./jwcert"); }); }); - + }); asyncTest("getPersistentSigninAssertion with valid login with remember set to true but no email", function() { @@ -919,7 +923,7 @@ var jwcert = require("./jwcert"); start(); }); - + }); asyncTest("getPersistentSigninAssertion with valid login with email and remember set to false", function() { @@ -940,7 +944,7 @@ var jwcert = require("./jwcert"); }); }); - + }); asyncTest("getPersistentSigninAssertion with valid login, email, and remember set to true", function() { @@ -961,7 +965,7 @@ var jwcert = require("./jwcert"); }); }); - + }); asyncTest("getPersistentSigninAssertion with XHR failure", function() { @@ -984,7 +988,7 @@ var jwcert = require("./jwcert"); }); }); - + }); asyncTest("clearPersistentSignin with invalid login", function() { @@ -998,7 +1002,7 @@ var jwcert = require("./jwcert"); start(); }); - + }); asyncTest("clearPersistentSignin with valid login with remember set to true", function() { @@ -1014,6 +1018,6 @@ var jwcert = require("./jwcert"); start(); }); - + }); }()); diff --git a/scripts/compress.sh b/scripts/compress.sh index 68fbaa0d1e2c97f1d669ef3cf382606d7eb1a2c1..77842a832ba6a0e5ddfa3ce1b8328ebf22bb8bcb 100755 --- a/scripts/compress.sh +++ b/scripts/compress.sh @@ -42,7 +42,7 @@ cp shared/templates.js shared/templates.js.orig cp dialog/views/templates.js shared/templates.js # produce the dialog js -cat dialog/resources/channel.js lib/jquery-1.6.2.min.js lib/jschannel.js lib/underscore-min.js lib/vepbundle.js lib/ejs.js shared/browserid.js lib/hub.js lib/dom-jquery.js lib/module.js shared/javascript-extensions.js shared/mediator.js shared/class.js shared/storage.js shared/templates.js shared/renderer.js shared/error-display.js shared/screens.js shared/tooltip.js shared/validation.js shared/network.js shared/user.js shared/error-messages.js shared/browser-support.js shared/wait-messages.js shared/helpers.js dialog/resources/helpers.js dialog/resources/state_machine.js dialog/controllers/page.js dialog/controllers/dialog.js dialog/controllers/authenticate.js dialog/controllers/forgotpassword.js dialog/controllers/checkregistration.js dialog/controllers/pickemail.js dialog/controllers/addemail.js dialog/controllers/required_email.js dialog/start.js > dialog/production.js +cat dialog/resources/channel.js lib/jquery-1.6.2.min.js lib/jschannel.js lib/underscore-min.js lib/vepbundle.js lib/ejs.js shared/browserid.js lib/hub.js lib/dom-jquery.js lib/module.js shared/javascript-extensions.js shared/mediator.js shared/class.js shared/storage.js shared/templates.js shared/renderer.js shared/error-display.js shared/screens.js shared/tooltip.js shared/validation.js shared/network.js shared/user.js shared/error-messages.js shared/browser-support.js shared/wait-messages.js shared/helpers.js dialog/resources/internal_api.js dialog/resources/helpers.js dialog/resources/state_machine.js dialog/controllers/page.js dialog/controllers/dialog.js dialog/controllers/authenticate.js dialog/controllers/forgotpassword.js dialog/controllers/checkregistration.js dialog/controllers/pickemail.js dialog/controllers/addemail.js dialog/controllers/required_email.js dialog/start.js > dialog/production.js # produce the non interactive frame js cat lib/jquery-1.6.2.min.js lib/jschannel.js lib/underscore-min.js lib/vepbundle.js shared/javascript-extensions.js shared/browserid.js shared/storage.js shared/network.js shared/user.js communication_iframe/start.js > communication_iframe/production.js