From e50c2b1ebc6235905838827cc0f0319984c980a2 Mon Sep 17 00:00:00 2001 From: Shane Tomlinson <stomlinson@mozilla.com> Date: Wed, 26 Oct 2011 09:37:34 -0600 Subject: [PATCH] improved messaging for unsupported browsers - closes #273, closes #484 --- browserid/app.js | 7 +- .../dialog/controllers/dialog_controller.js | 14 +- .../dialog/controllers/page_controller.js | 16 ++- browserid/static/dialog/css/m.css | 15 +- browserid/static/dialog/css/popup.css | 37 ++++- browserid/static/dialog/qunit.html | 27 ++-- .../dialog/resources/browser-support.js | 118 ++++++++++++++++ .../controllers/page_controller_unit_test.js | 34 ++++- .../dialog/test/qunit/include_unit_test.js | 52 +++++++ browserid/static/dialog/test/qunit/qunit.js | 8 +- .../resources/browser-support_unit_test.js | 102 ++++++++++++++ browserid/static/i/firefox_logo.png | Bin 0 -> 26648 bytes browserid/static/include.js | 131 ++++++++++++------ browserid/tests/page-requests-test.js | 116 ++++++++++++++++ browserid/views/dialog.ejs | 113 ++++----------- browserid/views/dialog_layout.ejs | 58 ++++++++ browserid/views/unsupported_dialog.ejs | 28 ++++ 17 files changed, 708 insertions(+), 168 deletions(-) create mode 100644 browserid/static/dialog/resources/browser-support.js create mode 100644 browserid/static/dialog/test/qunit/include_unit_test.js create mode 100644 browserid/static/dialog/test/qunit/resources/browser-support_unit_test.js create mode 100644 browserid/static/i/firefox_logo.png create mode 100755 browserid/tests/page-requests-test.js create mode 100644 browserid/views/dialog_layout.ejs create mode 100644 browserid/views/unsupported_dialog.ejs diff --git a/browserid/app.js b/browserid/app.js index f637f884a..6f661ee7c 100644 --- a/browserid/app.js +++ b/browserid/app.js @@ -80,11 +80,16 @@ function router(app) { metrics.userEntry(req); res.render('dialog.ejs', { title: 'A Better Way to Sign In', - layout: false, + layout: 'dialog_layout.ejs', + useJavascript: true, production: configuration.get('use_minified_resources') }); }); + app.get("/unsupported_dialog", function(req,res) { + res.render('unsupported_dialog.ejs', {layout: 'dialog_layout.ejs', useJavascript: false}); + }); + // simple redirects (internal for now) app.get('/register_iframe', internal_redirector('/dialog/register_iframe.html',true)); diff --git a/browserid/static/dialog/controllers/dialog_controller.js b/browserid/static/dialog/controllers/dialog_controller.js index 771c31770..49560e08d 100644 --- a/browserid/static/dialog/controllers/dialog_controller.js +++ b/browserid/static/dialog/controllers/dialog_controller.js @@ -50,18 +50,17 @@ PageController.extend("Dialog", {}, { init: function(el) { var self=this; - //this.element.show(); // keep track of where we are and what we do on success and error self.onsuccess = null; self.onerror = null; setupChannel(self); self.stateMachine(); + }, getVerifiedEmail: function(origin_url, onsuccess, onerror) { var self=this; - self.onsuccess = onsuccess; self.onerror = onerror; @@ -71,8 +70,6 @@ } user.setOrigin(origin_url); - - // get the cleaned origin. $("#sitename").text(user.getHostname()); self.doCheckAuth(); @@ -88,7 +85,6 @@ var self=this, hub = OpenAjax.hub, el = this.element; - hub.subscribe("offline", function(msg, info) { self.doOffline(); @@ -119,7 +115,7 @@ }); hub.subscribe("assertion_generated", function(msg, info) { - if(info.assertion !== null) { + if (info.assertion !== null) { self.doAssertionGenerated(info.assertion); } else { @@ -158,12 +154,12 @@ }, doOffline: function() { - this.renderError(errors.offline); + this.renderError("wait.ejs", errors.offline); offline = true; }, doXHRError: function(info) { - if (!offline) this.renderError(errors.offline); + if (!offline) this.renderError("wait.ejs", errors.offline); }, doConfirmUser: function(email) { @@ -178,7 +174,7 @@ doCancel: function() { var self=this; - if(self.onsuccess) { + if (self.onsuccess) { self.onsuccess(null); } }, diff --git a/browserid/static/dialog/controllers/page_controller.js b/browserid/static/dialog/controllers/page_controller.js index ba80f9bbf..01ddbb26a 100644 --- a/browserid/static/dialog/controllers/page_controller.js +++ b/browserid/static/dialog/controllers/page_controller.js @@ -48,6 +48,8 @@ var me=this, bodyTemplate = options.bodyTemplate, bodyVars = options.bodyVars, + errorTemplate = options.errorTemplate, + errorVars = options.errorVars, waitTemplate = options.waitTemplate, waitVars = options.waitVars; @@ -60,6 +62,10 @@ me.renderWait(waitTemplate, waitVars); } + if(errorTemplate) { + me.renderError(errorTemplate, errorVars); + } + // XXX move all of these, bleck. $("form").bind("submit", me.onSubmit.bind(me)); $("#cancel").click(me.onCancel.bind(me)); @@ -99,10 +105,10 @@ $("#wait").stop().hide().fadeIn(ANIMATION_TIME); }, - renderError: function(error_vars) { - this.renderTemplates("#error", "wait.ejs", error_vars); - $("body").removeClass("waiting").removeClass("form").addClass("error").css('opacity', 1); - $("#error").stop().hide().fadeIn(ANIMATION_TIME); + renderError: function(body, body_vars) { + this.renderTemplates("#error", body, body_vars); + $("body").removeClass("waiting").removeClass("form").addClass("error"); + $("#error").stop().css('opacity', 1).hide().fadeIn(ANIMATION_TIME); }, onSubmit: function(event) { @@ -144,7 +150,7 @@ */ getErrorDialog: function(info) { var self=this; - return self.renderError.bind(self, info); + return self.renderError.bind(self, "wait.ejs", info); }, onCancel: function(event) { diff --git a/browserid/static/dialog/css/m.css b/browserid/static/dialog/css/m.css index 49f4a5613..9ddb605ea 100644 --- a/browserid/static/dialog/css/m.css +++ b/browserid/static/dialog/css/m.css @@ -111,6 +111,19 @@ height: 250px; } -} + #error .vertical { + width: auto; + } + #error .vertical > div { + display: block; + height: auto; + padding: 10px; + } + + #error #borderbox { + border-left: none; + padding: 0; + } + diff --git a/browserid/static/dialog/css/popup.css b/browserid/static/dialog/css/popup.css index b1b54243f..e5f4503f2 100644 --- a/browserid/static/dialog/css/popup.css +++ b/browserid/static/dialog/css/popup.css @@ -118,16 +118,17 @@ section > .contents { #wait, #error { text-align: center; - background-image: url("/i/bg.png"); } #wait { z-index: 1; + background-image: url("/i/bg.png"); } #error { display: none; z-index: 2; + background-color: #fff; } #wait strong, #error strong { @@ -135,10 +136,40 @@ section > .contents { font-weight: bold; } -#error { - z-index: 2; + +#error .vertical { + width: 630px; + margin: 0 auto; + display: block; +} + + +#error .vertical > div { + display: table-cell; + vertical-align: middle; + padding: 0 10px; + height: 250px; +} + +#error #alternative a { + color: #549FDC; + text-decoration: underline; +} + +#error #borderbox { + border-left: 1px solid #777; + padding: 20px 0; } +#error #borderbox img { + border: none; +} + +#error #alternative .lighter { + color: #777; +} + + #formWrap { background-color: #fff; background-image: none; diff --git a/browserid/static/dialog/qunit.html b/browserid/static/dialog/qunit.html index 716ec5323..7e8189f82 100644 --- a/browserid/static/dialog/qunit.html +++ b/browserid/static/dialog/qunit.html @@ -12,28 +12,31 @@ <div id="qunit-testrunner-toolbar"></div> <h2 id="qunit-userAgent"></h2> <div id="test-content"> - <div id="page_controller"> + </div> + <ol id="qunit-tests"></ol> + <div id="qunit-test-area"></div> - <div id="formWrap"> - <div class="contents"></div> - </div> + <h3>Content below here is test content that can be ignored</h3> - <div id="wait"> - <div class="contents"></div> - </div> + <div id="controller_head"> - <div id="error"> - <div class="contents"></div> - </div> + <div id="formWrap"> + <div class="contents"></div> + </div> + <div id="wait"> + <div class="contents"></div> </div> + + <div id="error"> + <div class="contents"></div> + </div> + <span id="email"></span> <span id="cannotconfirm" class="error">Cannot confirm</span> <span id="cannotcommunicate" class="error">Cannot communicate</span> <span id="siteinfo" class="error"><span class="website"></span></span> <span class=".hint">Hint</span> </div> - <ol id="qunit-tests"></ol> - <div id="qunit-test-area"></div> </body> </html> diff --git a/browserid/static/dialog/resources/browser-support.js b/browserid/static/dialog/resources/browser-support.js new file mode 100644 index 000000000..6c2e34818 --- /dev/null +++ b/browserid/static/dialog/resources/browser-support.js @@ -0,0 +1,118 @@ +/*globals 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 ***** */ +BrowserID.BrowserSupport = (function() { + var bid = BrowserID, + win = window, + nav = navigator, + reason; + + // For unit testing + function setTestEnv(newNav, newWindow) { + nav = newNav; + win = newWindow; + } + + function getInternetExplorerVersion() { + var rv = -1; // Return value assumes failure. + if (nav.appName == 'Microsoft Internet Explorer') { + var ua = nav.userAgent; + var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})"); + if (re.exec(ua) != null) + rv = parseFloat(RegExp.$1); + } + + return rv; + } + + function checkIE() { + var ieVersion = getInternetExplorerVersion(), + ieNosupport = ieVersion > -1 && ieVersion < 9; + + if(ieNosupport) { + return "IE_VERSION"; + } + } + + function explicitNosupport() { + return checkIE(); + } + + function checkLocalStorage() { + var localStorage = 'localStorage' in win && win['localStorage'] !== null; + if(!localStorage) { + return "LOCALSTORAGE"; + } + } + + function checkPostMessage() { + if(!win.postMessage) { + return "POSTMESSAGE"; + } + } + + function isSupported() { + reason = checkLocalStorage() || checkPostMessage() || explicitNosupport(); + + return !reason; + } + + function getNoSupportReason() { + return reason; + } + + return { + /** + * Set the test environment. + * @method setTestEnv + */ + setTestEnv: setTestEnv, + /** + * Check whether the current browser is supported + * @method isSupported + * @returns {boolean} + */ + isSupported: isSupported, + /** + * Called after isSupported, if isSupported returns false. Gets the reason + * why browser is not supported. + * @method getNoSupportReason + * @returns {string} + */ + getNoSupportReason: getNoSupportReason + }; + +}()); + diff --git a/browserid/static/dialog/test/qunit/controllers/page_controller_unit_test.js b/browserid/static/dialog/test/qunit/controllers/page_controller_unit_test.js index fca829d5c..e2df4dbb6 100644 --- a/browserid/static/dialog/test/qunit/controllers/page_controller_unit_test.js +++ b/browserid/static/dialog/test/qunit/controllers/page_controller_unit_test.js @@ -41,16 +41,21 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", function() { bodyTemplate = "testBodyTemplate.ejs", waitTemplate = "wait.ejs"; + function reset() { + el = $("#controller_head"); + el.find("#formWrap .contents").html(""); + el.find("#wait .contents").html(""); + el.find("#error .contents").html(""); + } + module("PageController", { setup: function() { - el = $("#page_controller"); + reset(); }, teardown: function() { - el.find("#formWrap .contents").html(""); - el.find("#wait .contents").html(""); - el.find("#error .contents").html(""); controller.destroy(); + reset(); } }); @@ -76,10 +81,11 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", function() { var html = el.find("#formWrap .contents").html(); ok(html.length, "with template specified, form text is loaded"); +/* var input = el.find("input").eq(0); ok(input.is(":focus"), "make sure the first input is focused"); - +*/ html = el.find("#wait .contents").html(); equal(html, "", "with body template specified, wait text is not loaded"); }); @@ -100,6 +106,22 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", function() { ok(html.length, "with wait template specified, wait text is loaded"); }); + test("page controller with error template renders in #error .contents", function() { + controller = el.page({ + errorTemplate: waitTemplate, + errorVars: { + title: "Test title", + message: "Test message" + } + }).controller(); + + var html = el.find("#formWrap .contents").html(); + equal(html, "", "with error template specified, form is ignored"); + + html = el.find("#error .contents").html(); + ok(html.length, "with error template specified, error text is loaded"); + }); + test("renderError renders an error message", function() { controller = el.page({ waitTemplate: waitTemplate, @@ -109,7 +131,7 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", function() { } }).controller(); - controller.renderError({ + controller.renderError("wait.ejs", { title: "error title", message: "error message" }); diff --git a/browserid/static/dialog/test/qunit/include_unit_test.js b/browserid/static/dialog/test/qunit/include_unit_test.js new file mode 100644 index 000000000..815ecf821 --- /dev/null +++ b/browserid/static/dialog/test/qunit/include_unit_test.js @@ -0,0 +1,52 @@ +/*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("/include.js", function() { + "use strict"; + + module("include.js"); + + test("navigator.id is available", function() { + equal(typeof navigator.id, "object", "navigator.id namespace is available"); + }); + + test("navigator.id.getVerifiedEmail is available", function() { + equal(typeof navigator.id.getVerifiedEmail, "function", "navigator.id.getVerifiedEmail is available"); + }); + + +}); + diff --git a/browserid/static/dialog/test/qunit/qunit.js b/browserid/static/dialog/test/qunit/qunit.js index 1733ac554..450add4ea 100644 --- a/browserid/static/dialog/test/qunit/qunit.js +++ b/browserid/static/dialog/test/qunit/qunit.js @@ -1,4 +1,5 @@ steal("/dialog/resources/browserid.js", + "/dialog/resources/browser-support.js", "/dialog/resources/storage.js", "/dialog/resources/tooltip.js", "/dialog/resources/validation.js", @@ -12,10 +13,15 @@ steal("/dialog/resources/browserid.js", "funcunit/qunit") .views('testBodyTemplate.ejs') .views('wait.ejs') + .then("/dialog/controllers/page_controller.js") .then("browserid_unit_test") + .then("include_unit_test") .then("pages/add_email_address_test") - .then("controllers/page_controller_unit_test") + .then("resources/browser-support_unit_test") .then("resources/validation_unit_test") .then("resources/storage_unit_test") .then("resources/network_unit_test") .then("resources/user_unit_test") + .then("controllers/page_controller_unit_test") + .then("controllers/page_controller_unit_test") + diff --git a/browserid/static/dialog/test/qunit/resources/browser-support_unit_test.js b/browserid/static/dialog/test/qunit/resources/browser-support_unit_test.js new file mode 100644 index 000000000..a26857f01 --- /dev/null +++ b/browserid/static/dialog/test/qunit/resources/browser-support_unit_test.js @@ -0,0 +1,102 @@ +/*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(function() { + "use strict"; + + var bid = BrowserID, + support = bid.BrowserSupport, + stubWindow, + stubNavigator; + + module("browser-support", { + setup: function() { + // Hard coded goodness for testing purposes + stubNavigator = { + appName: "Netscape", + userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:7.0.1) Gecko/20100101 Firefox/7.0.1" + }; + + stubWindow = { + localStorage: {}, + postMessage: function() {} + }; + + support.setTestEnv(stubNavigator, stubWindow); + }, + + teardown: function() { + } + }); + + test("browser without localStorage", function() { + delete stubWindow.localStorage; + + equal(support.isSupported(), false, "window.localStorage is required"); + equal(support.getNoSupportReason(), "LOCALSTORAGE", "correct reason"); + }); + + + test("browser without postMessage", function() { + delete stubWindow.postMessage; + + equal(support.isSupported(), false, "window.postMessage is required"); + equal(support.getNoSupportReason(), "POSTMESSAGE", "correct reason"); + }); + + test("Fake being IE8 - unsupported intentionally", function() { + stubNavigator.appName = "Microsoft Internet Explorer"; + stubNavigator.userAgent = "MSIE 8.0"; + + equal(support.isSupported(), false, "IE8 is not supported"); + equal(support.getNoSupportReason(), "IE_VERSION", "correct reason"); + }); + + test("Fake being IE9 - supported", function() { + stubNavigator.appName = "Microsoft Internet Explorer"; + stubNavigator.userAgent = "MSIE 9.0"; + + equal(support.isSupported(), true, "IE9 is supported"); + equal(typeof support.getNoSupportReason(), "undefined", "no reason, we are all good"); + }); + + test("Firefox 7.01 with postMessage, localStorage", function() { + equal(support.isSupported(), true, "Firefox 7.01 is supported"); + equal(typeof support.getNoSupportReason(), "undefined", "no reason, we are all good"); + }); +}); + + diff --git a/browserid/static/i/firefox_logo.png b/browserid/static/i/firefox_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c55a338081cc1e0f1d2e78fc50226695c0b37488 GIT binary patch literal 26648 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 z%0D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 literal 0 HcmV?d00001 diff --git a/browserid/static/include.js b/browserid/static/include.js index 9f16d5d47..cebeafc44 100644 --- a/browserid/static/include.js +++ b/browserid/static/include.js @@ -557,55 +557,88 @@ }; })(); - function getInternetExplorerVersion() { - var rv = -1; // Return value assumes failure. - if (navigator.appName == 'Microsoft Internet Explorer') { - var ua = navigator.userAgent; - var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})"); - if (re.exec(ua) != null) - rv = parseFloat(RegExp.$1); + var BrowserSupport = (function() { + var win = window, + nav = navigator, + reason; + + // For unit testing + function setTestEnv(newNav, newWindow) { + nav = newNav; + win = newWindow; } - return rv; - } - - function checkIE() { - var ieVersion = getInternetExplorerVersion(), - ieNosupport = ieVersion > -1 && ieVersion < 9, - message; + function getInternetExplorerVersion() { + var rv = -1; // Return value assumes failure. + if (nav.appName == 'Microsoft Internet Explorer') { + var ua = nav.userAgent; + var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})"); + if (re.exec(ua) != null) + rv = parseFloat(RegExp.$1); + } - if(ieNosupport) { - message = "Unfortunately, your version of Internet Explorer is not yet supported.\n" + - 'If you are using Internet Explorer 9, turn off "Compatibility View".'; + return rv; } - return message; - } + function checkIE() { + var ieVersion = getInternetExplorerVersion(), + ieNosupport = ieVersion > -1 && ieVersion < 9; - function explicitNosupport() { - var message = checkIE(); + if(ieNosupport) { + return "IE_VERSION"; + } + } - if (message) { - message += "\nWe are working hard to bring BrowserID support to your browser!"; - alert(message); + function explicitNosupport() { + return checkIE(); } - return message; - } + function checkLocalStorage() { + var localStorage = 'localStorage' in win && win['localStorage'] !== null; + if(!localStorage) { + return "LOCALSTORAGE"; + } + } - function checkRequirements() { - var localStorage = 'localStorage' in window && window['localStorage'] !== null; - var postMessage = !!window.postMessage; - var json = true; + function checkPostMessage() { + if(!win.postMessage) { + return "POSTMESSAGE"; + } + } - var explicitNo = explicitNosupport() + function isSupported() { + reason = checkLocalStorage() || checkPostMessage() || explicitNosupport(); - if(!explicitNo && !(localStorage && postMessage && json)) { - alert("Unfortunately, your browser does not meet the minimum HTML5 support required for BrowserID."); + return !reason; } - return localStorage && postMessage && json && !(explicitNo); - } + function getNoSupportReason() { + return reason; + } + + return { + /** + * Set the test environment. + * @method setTestEnv + */ + setTestEnv: setTestEnv, + /** + * Check whether the current browser is supported + * @method isSupported + * @returns {boolean} + */ + isSupported: isSupported, + /** + * Called after isSupported, if isSupported returns false. Gets the reason + * why browser is not supported. + * @method getNoSupportReason + * @returns {string} + */ + getNoSupportReason: getNoSupportReason + }; + + }()); + // this is for calls that are non-interactive function _open_hidden_iframe(doc) { @@ -636,16 +669,20 @@ return iframe; } - function _open_window() { + function _open_window(url) { + url = url || "about:blank"; // we open the window initially blank, and only after our relay frame has // been constructed do we update the location. This is done because we // must launch the window inside a click handler, but we should wait to // start loading it until our relay iframe is instantiated and ready. // see issue #287 & #286 - return window.open( - "about:blank", + var dialog = window.open( + url, "_mozid_signin", isFennec ? undefined : "menubar=0,location=0,resizable=0,scrollbars=0,status=0,dialog=1,width=700,height=375"); + + dialog.focus(); + return dialog; } function _attach_event(element, name, listener) { @@ -684,16 +721,17 @@ // keep track of these so that we can re-use/re-focus an already open window. navigator.id.getVerifiedEmail = function(callback) { - if(!checkRequirements()) { - return; - } - if (w) { // if there is already a window open, just focus the old window. w.focus(); return; } + if (!BrowserSupport.isSupported()) { + w = _open_window(ipServer + "/unsupported_dialog"); + return; + } + var frameid = _get_relayframe_id(); var iframe = _open_relayframe("browserid_relay_" + frameid); w = _open_window(); @@ -712,8 +750,7 @@ // has a problem re-attaching new iframes with the same name. Code inside // of frames with the same name sometimes does not get run. // See https://bugzilla.mozilla.org/show_bug.cgi?id=350023 - w.location = ipServer + "/sign_in#" + frameid; - w.focus(); + w = _open_window(ipServer + "/sign_in#" + frameid); } }); @@ -721,8 +758,10 @@ chan.destroy(); chan = null; - w.close(); - w = null; + if (w) { + w.close(); + w = null; + } iframe.parentNode.removeChild(iframe); iframe = null; diff --git a/browserid/tests/page-requests-test.js b/browserid/tests/page-requests-test.js new file mode 100755 index 000000000..156a0611d --- /dev/null +++ b/browserid/tests/page-requests-test.js @@ -0,0 +1,116 @@ +#!/usr/bin/env node + +/* ***** 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 ***** */ + +require('./lib/test_env.js'); + +const assert = require('assert'), +http = require('http'), +vows = require('vows'), +start_stop = require('./lib/start-stop.js'), +wsapi = require('./lib/wsapi.js'); + +var suite = vows.describe('page requests'); + +// start up a pristine server +start_stop.addStartupBatches(suite); + +// This set of tests check to make sure all of the expected pages are served +// up with the correct status codes. We use Lloyd's wsapi client as our REST +// interface. + + + +// Taken from the vows page. +function assertStatus(code) { + return function (res, err) { + assert.equal(res.code, code); + }; +} + +function respondsWith(status) { + var context = { + topic: function () { + // Get the current context's name, such as "POST /" + // and split it at the space. + var req = this.context.name.split(/ +/), // ["POST", "/"] + method = req[0].toLowerCase(), // "post" + path = req[1]; // "/" + + // Perform the contextual client request, + // with the above method and path. + wsapi[method](path).call(this); + } + }; + + // Create and assign the vow to the context. + // The description is generated from the expected status code + // and the status name, from node's http module. + context['should respond with a ' + status + ' ' + + http.STATUS_CODES[status]] = assertStatus(status); + + return context; +} + +suite.addBatch({ + 'GET /': respondsWith(200), + 'GET /signup': respondsWith(200), + 'GET /forgot': respondsWith(200), + 'GET /signin': respondsWith(200), + 'GET /about': respondsWith(200), + 'GET /tos': respondsWith(200), + 'GET /privacy': respondsWith(200), + 'GET /verify_email_address': respondsWith(200), + 'GET /add_email_address': respondsWith(200), + 'GET /pk': respondsWith(200), + 'GET /vepbundle': respondsWith(200), + 'GET /signin': respondsWith(200), + 'GET /unsupported_dialog': respondsWith(200), + 'GET /developers': respondsWith(200), + 'GET /manage': respondsWith(302), + 'GET /users': respondsWith(302), + 'GET /users/': respondsWith(302), + 'GET /primaries': respondsWith(302), + 'GET /primaries/': respondsWith(302), + 'GET /developers': respondsWith(302) +}); + +// shut the server down and cleanup +start_stop.addShutdownBatches(suite); + +// run or export the suite. +if (process.argv[1] === __filename) suite.run(); +else suite.export(module); diff --git a/browserid/views/dialog.ejs b/browserid/views/dialog.ejs index bf766dfda..d2b634a26 100644 --- a/browserid/views/dialog.ejs +++ b/browserid/views/dialog.ejs @@ -1,90 +1,35 @@ -<!doctype html> -<html> -<head> - <meta charset="utf-8"> - <meta name="viewport" content="initial-scale=1.0; maximum-scale=1.0; width=device-width;"> - <!--[if lt IE 9]> - <script type="text/javascript" src="/js/html5shim.js"></script> - <![endif]--> - <% if (production) { %> - <link href="/dialog/css/production.min.css" rel="stylesheet" type="text/css"> - <% } else { %> - <link href="/dialog/css/popup.css" rel="stylesheet" type="text/css"> - <link href="/dialog/css/m.css" rel="stylesheet" type="text/css"> - <% } %> - <link href="https://fonts.googleapis.com/css?family=Droid+Serif:400,400italic,700,700italic" rel="stylesheet" type="text/css"> - <title>Browser ID</title> -</head> - <body class="waiting"> - <div id="wrapper"> - <header id="header" class="cf"> - <ul> - <li><a class="home" target="_blank" href="/"></a></li> - </ul> - </header> - - <div id="content"> - <section id="formWrap"> - <form novalidate> - <div id="favicon"> - <div class="vertical"> - <strong id="sitename"></strong> - </div> - </div> - - <div id="signIn"> - <div class="arrow"></div> - <div class="table"> - <div class="vertical contents"> - </div> - </div> - </div> - </form> - </section> - - - <section id="wait"> - <div class="table"> - <div class="vertical contents"> - <h2>Communicating with server</h2> - <p>Just a moment while we talk with the server.</p> - </div> - </div> - </section> - - - <section id="error"> - <div class="table"> - <div class="vertical contents"> - </div> - </div> - </section> - </div> - - <footer> - <ul class="cf"> - <li>By <a href="http://mozillalabs.com">Mozilla Labs</a></li> - - <li>—</li> - <li><a href="#">Privacy</a></li> - <li><a href="#">TOS</a></li> - </ul> + <section id="formWrap"> + <form novalidate> + <div id="favicon"> + <div class="vertical"> + <strong id="sitename"></strong> + </div> + </div> - <div class="learn"> - BrowserID is the fast and secure way to sign in — <a target="_blank" href="/about">learn more</a> + <div id="signIn"> + <div class="arrow"></div> + <div class="table"> + <div class="vertical contents"> </div> + </div> + </div> + </form> + </section> + - <a class="help" href="https://support.mozilla.com/en-US/kb/what-browserid-and-how-does-it-work" target="_blank">need help?</a> - </footer> + <section id="wait"> + <div class="table"> + <div class="vertical contents"> + <h2>Communicating with server</h2> + <p>Just a moment while we talk with the server.</p> + </div> + </div> + </section> - </div> - <script type="text/html" id="templateTooltip"> - <div class="tooltip"> - {{ contents }} + <section id="error"> + <div class="table"> + <div class="vertical contents"> + </div> </div> - </script> - <script type="text/javascript" src="/vepbundle"></script> - <script type="text/javascript" src="steal/steal<%= production ? '.production' : '' %>.js?dialog"></script> - </body> -</html> + </section> diff --git a/browserid/views/dialog_layout.ejs b/browserid/views/dialog_layout.ejs new file mode 100644 index 000000000..13dd4d98f --- /dev/null +++ b/browserid/views/dialog_layout.ejs @@ -0,0 +1,58 @@ +<!doctype html> +<html> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="initial-scale=1.0; maximum-scale=1.0; width=device-width;"> + <!--[if lt IE 9]> + <script type="text/javascript" src="/js/html5shim.js"></script> + <![endif]--> + <% if (production) { %> + <link href="/dialog/css/production.min.css" rel="stylesheet" type="text/css"> + <% } else { %> + <link href="/dialog/css/popup.css" rel="stylesheet" type="text/css"> + <link href="/dialog/css/m.css" rel="stylesheet" type="text/css"> + <% } %> + <link href="https://fonts.googleapis.com/css?family=Droid+Serif:400,400italic,700,700italic" rel="stylesheet" type="text/css"> + <title>Browser ID</title> +</head> + <body class="waiting"> + <div id="wrapper"> + <header id="header" class="cf"> + <ul> + <li><a class="home" target="_blank" href="/"></a></li> + </ul> + </header> + + <div id="content"> + <%- body %> + </div> + + <footer> + <ul class="cf"> + <li>By <a href="http://mozillalabs.com">Mozilla Labs</a></li> + + <li>—</li> + <li><a href="#">Privacy</a></li> + <li><a href="#">TOS</a></li> + </ul> + + <div class="learn"> + BrowserID is the fast and secure way to sign in — <a target="_blank" href="/about">learn more</a> + </div> + + <a class="help" href="https://support.mozilla.com/en-US/kb/what-browserid-and-how-does-it-work" target="_blank">need help?</a> + </footer> + + </div> + + <% if (useJavascript !== false) { %> + <script type="text/html" id="templateTooltip"> + <div class="tooltip"> + {{ contents }} + </div> + </script> + <script type="text/javascript" src="/vepbundle"></script> + <script type="text/javascript" src="steal/steal<%= production ? '.production' : '' %>.js?dialog"></script> + <% } %> + </body> +</html> diff --git a/browserid/views/unsupported_dialog.ejs b/browserid/views/unsupported_dialog.ejs new file mode 100644 index 000000000..af410a76e --- /dev/null +++ b/browserid/views/unsupported_dialog.ejs @@ -0,0 +1,28 @@ + <section id="error" style="display: block"> + <div class="table"> + <div class="vertical contents"> + <div id="reason"> + We're sorry, but currently your browser isn't supported. + </div> + + <div id="alternative"> + + <div id="borderbox"> + <a href="http://getfirefox.com" target="_blank"> + <img src="/i/firefox_logo.png" width="250" height="88" alt="Firefox logo" /> + </a> + + <p> + BrowserID requires <a href="http://getfirefox.com" target="_blank" title="Get Firefox">Firefox</a> + </p> + + <p class="lighter"> + or other <a href="" target="_blank">modern browser.</a> + </p> + </div> + + </div> + + </div> + </div> + </section> -- GitLab