diff --git a/browserid/compress.sh b/browserid/compress.sh index 3a493518a47c68c4aae8037fcf3931ac12a50e2f..761edbf14317e3b72540919e39c44b002c1b54e2 100755 --- a/browserid/compress.sh +++ b/browserid/compress.sh @@ -37,7 +37,7 @@ cat popup.css m.css > production.css $JAVA -jar $YUI_LOCATION production.css -o production.min.css cd ../../relay -cat ../dialog/resources/jschannel.js relay.js > production.js +cat ../dialog/resources/jschannel.js ../dialog/resources/browserid.js relay.js > production.js $UGLIFY < production.js > production.min.js mv production.min.js production.js diff --git a/browserid/static/dialog/resources/channel.js b/browserid/static/dialog/resources/channel.js index 0d52976bb22f8dd74c7599d74dff39609716189f..eb235bd41bab1dd453d4de17eb167e2325956637 100644 --- a/browserid/static/dialog/resources/channel.js +++ b/browserid/static/dialog/resources/channel.js @@ -52,93 +52,90 @@ (function() { - function getRelayID() { - return window.location.href.slice(window.location.href.indexOf('#') + 1); - } + var win = window, + nav = navigator, + onCompleteCallback; function getRelayName() { - return "browserid_relay_" + getRelayID(); + var relayID = win.location.href.slice(win.location.href.indexOf('#') + 1); + return "browserid_relay_" + relayID; } function getRelayWindow() { - var frameWindow = window.opener.frames[getRelayName()]; + var frameWindow = win.opener.frames[getRelayName()]; return frameWindow; } - function registerWithRelayFrame(callback) { - var frameWindow = getRelayWindow(); - if (frameWindow) { - frameWindow['register_dialog'](callback); - } - } + function setupNativeChannel(controller) { + nav.id.channel.registerController(controller); + }; - function getRPRelay() { - var frameWindow = getRelayWindow(); - return frameWindow && frameWindow['browserid_relay']; - } + function setupIFrameChannel(controller) { + // 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. + function onsuccess(rv) { + onCompleteCallback(rv, null); + } - function errorOut(trans, code) { - function getVerboseMessage(code) { - var msgs = { - "canceled": "user canceled selection", - "notImplemented": "the user tried to invoke behavior that's not yet implemented", - "serverError": "a technical problem was encountered while trying to communicate with BrowserID servers." - }; - var msg = msgs[code]; - if (!msg) { - alert("need verbose message for " + code); - msg = "unknown error"; - } - return msg; + function onerror(error) { + onCompleteCallback(null, error); } - trans.error(code, getVerboseMessage(code)); - window.self.close(); - } + // 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(function(origin, onComplete) { + onCompleteCallback = onComplete; + controller.getVerifiedEmail(origin, onsuccess, onerror); + }); + } - window.setupChannel = function(controller) { - if (navigator.id && navigator.id.channel) + win.location.hash = ''; + }; + + function open(controller) { + if (nav.id && nav.id.channel) setupNativeChannel(controller); else setupIFrameChannel(controller); }; - var setupNativeChannel = function(controller) { - navigator.id.channel.registerController(controller); - }; - var setupIFrameChannel = function(controller) { - // 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. + function init(options) { + onCompleteCallback = undefined; - // get the relay here at the time the channel is setup before any navigation has - // occured. if we wait the window hash might change as a side effect to user - // navigation, which would cause us to not find our parent window. - // issue #295 - var relay = getRPRelay(); - - function onsuccess(rv) { - // Get the relay here so that we ensure that the calling window is still - // open and we aren't causing a problem. - if (relay) { - relay(rv, null); - } + if(options.navigator) { + nav = navigator; } - function onerror(error) { - if (relay) { - relay(null, error); - } + if(options.window) { + win = options.window; } + } - // The relay frame will give us the origin. - registerWithRelayFrame(function(origin) { - controller.getVerifiedEmail(origin, onsuccess, onerror); - }); - window.location.hash = ''; - }; + if(window.BrowserID) { + BrowserID.Channel = { + /** + * Used to intialize the channel, mostly for unit testing to override + * window and navigator. + * @method init + */ + init: init, + /** + * Open the channel. + * @method open + */ + open: open + }; + } + /** + * This is here as a legacy API for addons/etc that are depending on + * window.setupChannel; + */ + window.setupChannel = open; }()); diff --git a/browserid/static/dialog/test/qunit/qunit.js b/browserid/static/dialog/test/qunit/qunit.js index 909773ac04e93ca3c91bfee896ebbc7ba143f4f8..7ceaabc35ed72539a42d52a9a1bab5412d3525d3 100644 --- a/browserid/static/dialog/test/qunit/qunit.js +++ b/browserid/static/dialog/test/qunit/qunit.js @@ -16,7 +16,9 @@ steal("/dialog/resources/browserid.js", 'pickemail.ejs') .then("browserid_unit_test") .then("include_unit_test") + .then("relay/relay_unit_test") .then("pages/add_email_address_test") + .then("resources/channel_unit_test") .then("resources/browser-support_unit_test") .then("resources/validation_unit_test") .then("resources/storage_unit_test") diff --git a/browserid/static/dialog/test/qunit/relay/relay_unit_test.js b/browserid/static/dialog/test/qunit/relay/relay_unit_test.js new file mode 100644 index 0000000000000000000000000000000000000000..ae7e6dc6109e84fd1d7e6957d0e208a14218212f --- /dev/null +++ b/browserid/static/dialog/test/qunit/relay/relay_unit_test.js @@ -0,0 +1,183 @@ +/*jshint browsers:true, forin: true, laxbreak: true */ +/*global steal: true, test: true, start: true, stop: 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 ***** */ +steal.plugins("jquery").then("/dialog/resources/jschannel", "/relay/relay", function() { + "use strict"; + + var winMock = {}, + relay = BrowserID.Relay; + + var channelMock = { + build: function(options) { + this.options = options; + + channelMock.bindMessage = channelMock.cb = channelMock.status = + channelMock.errorCode = channelMock.verboseError = undefined; + + return { + bind: function(message, cb) { + channelMock.bindMessage = message; + channelMock.cb = cb; + } + } + }, + + // Mock in the receiving of the RPC call from the RP. + receiveGetVerifiedEmail: function() { + // cb is the mock callback that is passed to Channel.bind + channelMock.cb({ + origin: "Origin", + delayReturn: function() {}, + complete: function(status) { + channelMock.status = status; + }, + error: function(code, verboseMessage) { + channelMock.errorCode = code; + channelMock.verboseError = verboseMessage; + } + }); + } + + }; + + + module("relay/relay", { + setup: function() { + relay.init({ + window: winMock, + channel: channelMock + }); + }, + teardown: function() { + relay.init({ + window: window.parent, + channel: Channel + }); + } + }); + + test("Can open the relay, happy case", function() { + relay.open(); + + /** + * Check to make sure channel build is correct + */ + equal(channelMock.options.window, winMock, "opening to the correct window"); + equal(channelMock.options.origin, "*", "accept messages from anybody"); + equal(channelMock.options.scope, "mozid", "mozid namespace"); + + /** + * Check to make sure the correct message is bound + */ + equal(channelMock.bindMessage, "getVerifiedEmail", "bound to getVerifiedEmail"); + }); + + test("channel.getVerifiedEmail before registerDialog", function() { + relay.open(); + + channelMock.receiveGetVerifiedEmail(); + + relay.registerClient(function(origin, completeCB) { + equal(origin, "Origin", "Origin set correctly"); + equal(typeof completeCB, "function", "A completion callback is specified"); + + start(); + }); + + stop(); + }); + + test("registerDialog before channel.getVerifiedEmail", function() { + relay.open(); + + relay.registerClient(function(origin, completeCB) { + equal(origin, "Origin", "Origin set correctly"); + equal(typeof completeCB, "function", "A completion callback is specified"); + + start(); + }); + + channelMock.receiveGetVerifiedEmail(); + + stop(); + }); + + test("calling the completeCB with assertion", function() { + relay.open(); + + channelMock.receiveGetVerifiedEmail(); + + relay.registerClient(function(origin, completeCB) { + completeCB("assertion", null); + equal(channelMock.status, "assertion", "channel gets the correct assertion"); + start(); + }); + + stop(); + }); + + + test("calling the completeCB with null assertion", function() { + relay.open(); + + channelMock.receiveGetVerifiedEmail(); + + relay.registerClient(function(origin, completeCB) { + completeCB(null, null); + strictEqual(channelMock.status, null, "channel gets the null assertion"); + start(); + }); + + stop(); + }); + + test("calling the completeCB with error", function() { + relay.open(); + + channelMock.receiveGetVerifiedEmail(); + + relay.registerClient(function(origin, completeCB) { + completeCB(null, "canceled"); + + equal(channelMock.errorCode, "canceled", "error callback called with error code"); + ok(channelMock.verboseError, "verbose error code set"); + + start(); + }); + + stop(); + }); +}); diff --git a/browserid/static/dialog/test/qunit/resources/channel_unit_test.js b/browserid/static/dialog/test/qunit/resources/channel_unit_test.js new file mode 100644 index 0000000000000000000000000000000000000000..da72df2344699d499d668ad582b1640bd98c6c5a --- /dev/null +++ b/browserid/static/dialog/test/qunit/resources/channel_unit_test.js @@ -0,0 +1,137 @@ +/*jshint browsers:true, forin: true, laxbreak: true */ +/*global steal: true, test: true, start: true, stop: 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 ***** */ +steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/channel", function() { + var channel = BrowserID.Channel; + + var navMock = { + id: {}, + }; + + // Mock in the window object as well as the frame relay + var winMock = { + location: { + href: "browserid.org/sign_in#1234" + }, + opener: { + frames: { + browserid_relay_1234: { + BrowserID: { + Relay: { + registerClient: function(callback) { + // mock of the registerClient function in the BrowserID.Channel. + callback("origin", function onComplete(success, error) { + winMock.success = success; + winMock.error = error; + }); + } + } + } + } + } + } + }; + + module("resources/channel", { + setup: function() { + }, + + teardown: function() { + channel.init({ + window: window, + navigator: navigator + }); + } + }); + + test("window.setupChannel exists for legacy uses", function() { + ok(typeof window.setupChannel, "function", "window.setupChannel exists for legacy uses"); + }); + + test("IFRAME channel with assertion", function() { + channel.init({ + window: winMock, + navigator: navMock + }); + + + channel.open({ + getVerifiedEmail: function(origin, onsuccess, onerror) { + onsuccess("assertion"); + equal(winMock.success, "assertion", "assertion made it to the relay"); + start(); + } + }); + + stop(); + }); + + test("IFRAME channel with null assertion", function() { + channel.init({ + window: winMock, + navigator: navMock + }); + + channel.open({ + getVerifiedEmail: function(origin, onsuccess, onerror) { + onsuccess(null); + strictEqual(winMock.success, null, "null assertion made it to the relay"); + start(); + } + }); + + stop(); + }); + + test("IFRAME channel with error", function() { + channel.init({ + window: winMock, + navigator: navMock + }); + + channel.open({ + getVerifiedEmail: function(origin, onsuccess, onerror) { + onerror("error"); + strictEqual(winMock.error, "error", "error made it to the relay"); + start(); + } + }); + + stop(); + }); + +}); + diff --git a/browserid/static/relay/relay.js b/browserid/static/relay/relay.js index 46212cc2479faef04dc09da5e978b792bbc7c67a..d9c86f4c13d0b4122eb43610ae4662769ce1c06e 100644 --- a/browserid/static/relay/relay.js +++ b/browserid/static/relay/relay.js @@ -43,41 +43,114 @@ log: function() {} }; - var ipServer = "https://browserid.org", - transaction, - origin, - chan = Channel.build( { - window: window.parent, + BrowserID.Relay = (function() { + var transaction, + origin, + channel = Channel, + win = window, + registerCB; + + + function init(options) { + origin = transaction = registerCB = undefined; + + if(options.window) { + win = options.window; + } + + if(options.channel) { + channel = options.channel; + } + } + + function open() { + var rpc = channel.build({ + window: win, origin: "*", scope: "mozid" - } ); + }); + rpc.bind("getVerifiedEmail", function(trans, s) { + trans.delayReturn(true); + origin = trans.origin; + transaction = trans; - window.register_dialog = function(callback) { - // register the dialog, tell the dialog what the origin is. - // Get the origin from the channel binding. - callback(origin); - }; + // If the client has run early and already registered its registration + // callback, call it now. + if (registerCB) { + registerCB(origin, completionCB); + } + }); + } - window.browserid_relay = function(status, error) { - if(error) { - errorOut(transaction, error); + function registerClient(callback) { + // If the origin is ready, call the callback immediately. + if (origin) { + callback(origin, completionCB); } else { - try { - transaction.complete(status); - } catch(e) { - // The relay function is called a second time after the - // initial success, when the window is closing. - } + registerCB = callback; } - }; + } + + function errorOut(code) { + function getVerboseMessage(code) { + var msgs = { + "canceled": "user canceled selection", + "notImplemented": "the user tried to invoke behavior that's not yet implemented", + "serverError": "a technical problem was encountered while trying to communicate with BrowserID servers." + }; + var msg = msgs[code]; + if (!msg) { + alert("need verbose message for " + code); + msg = "unknown error"; + } + return msg; + } + transaction.error(code, getVerboseMessage(code)); + } + + /** + * The client calls this to relay a message back to the RP whenever it is + * complete. This function is passed to the client when the client does + * its registerClient. + */ + function completionCB(status, error) { + if(error) { + errorOut(error); + } + else { + try { + transaction.complete(status); + } catch(e) { + // The relay function is called a second time after the + // initial success, when the window is closing. + } + } + } + + + return { + /** + * Initialize the relay. + * @method init + * @param {object} [options] - options used to override window, channel + * for unit testing. + */ + init: init, - chan.bind("getVerifiedEmail", function(trans, s) { - origin = trans.origin; - trans.delayReturn(true); + /** + * Open the relay with the parent window. + * @method open + */ + open: open, - transaction = trans; - }); + /** + * Register a client to use the relay + * @method registerClient + */ + registerClient: registerClient + }; + }()); }()); diff --git a/browserid/views/relay.ejs b/browserid/views/relay.ejs index b6f4cc4f87f190231ac5fffe8912ea1a9c778823..1fc38b014dd59ab903ef989de9604ece89eb63c2 100644 --- a/browserid/views/relay.ejs +++ b/browserid/views/relay.ejs @@ -13,8 +13,20 @@ <% } else { %> <script type='text/javascript' src='https://browserid.org/dialog/resources/jschannel.js'></script> + <script type='text/javascript' + src='https://browserid.org/dialog/resources/browserid.js'></script> <script type='text/javascript' src='https://browserid.org/relay/relay.js'></script> <% } %> + + <script type="text/javascript"> + var relay = BrowserID.Relay; + relay.init({ + channel: Channel, + window: window.parent + }); + + relay.open(); + </script> </body> </html>