diff --git a/example/rp/i/card.png b/example/rp/i/card.png new file mode 100644 index 0000000000000000000000000000000000000000..eac93fc8481e85d226d934eaa30a2b3b4737837d Binary files /dev/null and b/example/rp/i/card.png differ diff --git a/example/rp/index.html b/example/rp/index.html index 8487ef70dbd66ea2a9d69641f997ec76f6015052..1ff44aa9ed0e37be846258e0d91e30166d335665 100644 --- a/example/rp/index.html +++ b/example/rp/index.html @@ -69,6 +69,14 @@ pre { <input type="text" id="requiredEmail" width="80"> <label for="requiredEmail">Require a specific email</label><br /> </li> + </li><li> + <input type="text" id="name" width="80"> + <label for="name">Site Name (optional)</label><br /> + </li> + </li><li> + <input type="text" id="logoURL" width="80"> + <label for="logoURL">Site Logo Path (optional)</label><br /> + </li> </ul> <button class="assertion">Get an assertion</button> <button class="logout">logout</button> @@ -178,12 +186,17 @@ $(document).ready(function() { return; } + var logoURL = $.trim($('#logoURL').val()); + var name = $.trim($('#name').val()); + $(".specify button.assertion").attr('disabled', 'true'); navigator.id.request({ privacyURL: $('#privacy').attr('checked') ? "/privacy.html" : undefined, tosURL: $('#tos').attr('checked') ? "/TOS.html" : undefined, requiredEmail: requiredEmail, + name: name, + logoURL: logoURL, oncancel: function() { loggit("oncancel"); $(".specify button.assertion").removeAttr('disabled'); diff --git a/lib/static_resources.js b/lib/static_resources.js index ad17eac8848b58d1dd9a982e80f1ceabc1f2c652..bcf8a92f3cab73b52642165e8778379dc1795e81 100644 --- a/lib/static_resources.js +++ b/lib/static_resources.js @@ -104,7 +104,7 @@ var dialog_js = und.flatten([ '/dialog/controllers/generate_assertion.js', '/dialog/controllers/is_this_your_computer.js', '/dialog/controllers/set_password.js', - + '/dialog/controllers/rp_info.js', '/dialog/start.js' ]]); diff --git a/resources/static/dialog/controllers/actions.js b/resources/static/dialog/controllers/actions.js index e01cb576cc4b51a08526164834a982a55081b62b..80435e5220f6bafe3e5856ef3aeb358bc715a098 100644 --- a/resources/static/dialog/controllers/actions.js +++ b/resources/static/dialog/controllers/actions.js @@ -58,6 +58,10 @@ BrowserID.Modules.Actions = (function() { if(data.ready) _.defer(data.ready); }, + doRPInfo: function(info) { + startService("rp_info", info); + }, + doCancel: function() { if(onsuccess) onsuccess(null); }, diff --git a/resources/static/dialog/controllers/dialog.js b/resources/static/dialog/controllers/dialog.js index 8e5b8f1cd22e84a05175a9b7b1393c7c69e4a45f..740994a93ce35b50aa2b4736b6ef07cb9ebbef59 100644 --- a/resources/static/dialog/controllers/dialog.js +++ b/resources/static/dialog/controllers/dialog.js @@ -75,7 +75,6 @@ BrowserID.Modules.Dialog = (function() { function setOrigin(origin) { user.setOrigin(origin); - dom.setInner("#sitename", user.getHostname()); } function onWindowUnload() { @@ -93,6 +92,12 @@ BrowserID.Modules.Dialog = (function() { return encodeURI(u.validate().normalize().toString()); } + function fixupAbsolutePath(origin_url, path) { + if (/^\//.test(path)) return fixupURL(origin_url, path); + + throw "must be an absolute path: (" + path + ")"; + } + var Dialog = bid.Modules.PageModule.extend({ start: function(options) { var self=this; @@ -168,6 +173,17 @@ BrowserID.Modules.Dialog = (function() { params.privacyURL = fixupURL(origin_url, paramsFromRP.privacyPolicy); } + if (paramsFromRP.logoURL) { + // Until we have our head around the dangers of data uris and images + // that come from other domains, only allow absolute paths from the + // origin. + params.logoURL = fixupAbsolutePath(origin_url, paramsFromRP.logoURL); + } + + if (paramsFromRP.name) { + params.name = _.escape(paramsFromRP.name); + } + if (hash.indexOf("#CREATE_EMAIL=") === 0) { var email = hash.replace(/#CREATE_EMAIL=/, ""); if (!bid.verifyEmail(email)) diff --git a/resources/static/dialog/controllers/rp_info.js b/resources/static/dialog/controllers/rp_info.js new file mode 100644 index 0000000000000000000000000000000000000000..92384bfcac6d200781dbf47fd5ff887a698aee98 --- /dev/null +++ b/resources/static/dialog/controllers/rp_info.js @@ -0,0 +1,47 @@ +/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */ +/*global _: true, BrowserID: true, PageController: true */ +/* 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/. */ + + +/** + * Purpose: + * Display to the user RP related data such as hostname, name, and logo. + */ +BrowserID.Modules.RPInfo = (function() { + "use strict"; + + var bid = BrowserID, + renderer = bid.Renderer, + sc; + + var Module = bid.Modules.PageModule.extend({ + start: function(options) { + options = options || {}; + + /** + * Very important security info - it is assumed all parameters are + * already properly escaped before being passed here. This is done + * in dialog.js. Check it. + * + * hostname is set internally based on the RP URL, + * so it will not be escaped. It is set initially in user.js at the very + * bottom for the main site, and then in dialog.js->get for the dialog. + */ + renderer.render("#rp_info", "rp_info", { + hostname: options.hostname, + name: options.name, + logoURL: options.logoURL + }); + + sc.start.call(this, options); + } + }); + + sc = Module.sc; + + return Module; + +}()); + diff --git a/resources/static/dialog/css/m.css b/resources/static/dialog/css/m.css index 6991dbe0296b5f6f2abce6bf2ee662a95e95aa98..efc23a635bce1793a4b77e06a86d84493c3c4109 100644 --- a/resources/static/dialog/css/m.css +++ b/resources/static/dialog/css/m.css @@ -43,7 +43,7 @@ } #signIn { - top: 45px; /* 45px is a magic number - the height of the favicon area */ + top: auto; /* this will be set in JS to be at the bottom of the header */ right: 0; width: auto; padding: 0; @@ -57,13 +57,32 @@ * being partially cut off by the site URL bar */ position: static; + padding: 10px; + border-bottom: 1px solid rgba(0,0,0,0.05); + background-image: url('/i/bg.png'); + text-align: center; + left: 0; } - #favicon { - padding: 10px; + #favicon img { + max-width: 32px; + max-height: 32px; + display: inline; + margin: 0; + vertical-align: middle; + } + + #favicon h2, #favicon h3 { + margin: 5px 0 0 0; } - #signIn .table { + #favicon .vertical { + height: auto; + line-height: 20px; + } + + + #signIn .table, #signIn .container { width: 100%; } @@ -96,6 +115,10 @@ line-height: 20px; } + #formWrap { + background-color: transparent; + } + .form #formWrap, .waiting #wait, .delay #delay, #error #error { display: block; } diff --git a/resources/static/dialog/css/popup.css b/resources/static/dialog/css/popup.css index c55543e572188e2ce35d2a14b4f0102f31cd295b..b1d96da234a3a74fc4fc6aff7c7832a555c3ef8a 100644 --- a/resources/static/dialog/css/popup.css +++ b/resources/static/dialog/css/popup.css @@ -270,7 +270,8 @@ section > .contents { left: 400px; top: 0; bottom: 0; - right: 0; + right: 20px; /* The same as the left padding of the left hand side */ + overflow: hidden; z-index: 10; } @@ -282,14 +283,23 @@ section > .contents { #favicon img { display: block; - margin: 0 auto 10px; + margin: 0 auto; + max-height: 128px; + max-width: 128px; +} + +#favicon h2, #favicon h3 { + white-space: nowrap; + text-overflow: ellipsis; + height: 1.2em; /* the 1.2em is to keep y, g, j, etc from having their bottoms chopped off */ + overflow: hidden; + margin: 10px 0 0 0; } #favicon .vertical { display: table-cell; text-align: center; - overflow: hidden; - text-overflow: ellipsis; + max-width: 0; } div#required_email { diff --git a/resources/static/dialog/resources/screen_size_hacks.js b/resources/static/dialog/resources/screen_size_hacks.js index 503d84852d9bfd982016a7968c396cf97053045e..196060f86907d89028e9c00994fb7801faf58969 100644 --- a/resources/static/dialog/resources/screen_size_hacks.js +++ b/resources/static/dialog/resources/screen_size_hacks.js @@ -8,13 +8,15 @@ */ function onResize() { var selectEmailEl = $("#selectEmail"), - contentEl = $("#content"); + contentEl = $("#content"), + signInEl = $("#signIn"); selectEmailEl.css("position", "static"); if($(window).width() >= 640) { // First, remove the mobile hacks selectEmailEl.css("width", ""); contentEl.css("min-height", ""); + signInEl.css("top", ""); // This is a hack for desktop mode which centers the form vertically in // the middle of its container. We have to do this hack because we use @@ -105,6 +107,11 @@ contentEl.css("min-height", contentHeight + "px"); $("section,#signIn").css("position", ""); + + favIconHeight = $("#favicon").outerHeight(); + + // Force the top of the main content area to be below the favicon area. + signInEl.css("top", (headerHeight + favIconHeight) + "px"); } selectEmailEl.css("position", ""); diff --git a/resources/static/dialog/resources/state.js b/resources/static/dialog/resources/state.js index 8a016d6f574e2af46e97906e78d08d5b9b65887c..c56eee0ddf6d95326c190dde76913f89c403073d 100644 --- a/resources/static/dialog/resources/state.js +++ b/resources/static/dialog/resources/state.js @@ -46,6 +46,8 @@ BrowserID.State = (function() { self.tosURL = info.tosURL; requiredEmail = info.requiredEmail; + startAction(false, "doRPInfo", info); + if (info.email && info.type === "primary") { primaryVerificationInfo = info; redirectToState("primary_user", info); diff --git a/resources/static/dialog/start.js b/resources/static/dialog/start.js index 543136b023e673488b9e0d90c1fa9d1b34dc519c..70c574bad3f2e645532c48aee54fdec314b886fc 100644 --- a/resources/static/dialog/start.js +++ b/resources/static/dialog/start.js @@ -42,6 +42,7 @@ moduleManager.register("xhr_delay", modules.XHRDelay); moduleManager.register("xhr_disable_form", modules.XHRDisableForm); moduleManager.register("set_password", modules.SetPassword); + moduleManager.register("rp_info", modules.RPInfo); moduleManager.start("xhr_delay"); moduleManager.start("xhr_disable_form"); diff --git a/resources/static/dialog/views/rp_info.ejs b/resources/static/dialog/views/rp_info.ejs new file mode 100644 index 0000000000000000000000000000000000000000..dd3be3f921fbefe2ae0cced7971b6de4edcf2980 --- /dev/null +++ b/resources/static/dialog/views/rp_info.ejs @@ -0,0 +1,21 @@ +<% /* 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/. */ %> + +<% if(logoURL) { %> + <img id="rp_logo" src="<%= logoURL %>" /> +<% } %> + + +<% if(name) { %> + <h2 id="rp_name"><%= name %></h2> +<% } %> + +<% if(hostname) { %> + <% if(name) { %> + <h3 id="rp_hostname"><%= hostname %></h3> + <% } else { %> + <h2 id="rp_hostname"><%= hostname %></h2> + <% } %> +<% } %> + diff --git a/resources/static/test/cases/controllers/dialog.js b/resources/static/test/cases/controllers/dialog.js index 4190cd915513306d18faa797a3a542aec718676c..6efbdefb2d58e2d83a979fddb57a203508c5155e 100644 --- a/resources/static/test/cases/controllers/dialog.js +++ b/resources/static/test/cases/controllers/dialog.js @@ -469,5 +469,123 @@ }); }); + asyncTest("get with relative logoURL - not allowed", function() { + createController({ + ready: function() { + mediator.subscribe("start", function(msg, info) { + ok(false, "start should not have been called"); + }); + + var retval = controller.get(HTTP_TEST_DOMAIN, { + logoURL: "logo.png", + }); + + equal(retval, "must be an absolute path: (logo.png)", "expected error"); + testErrorVisible(); + start(); + } + }); + }); + + asyncTest("get with javascript: logoURL - not allowed", function() { + createController({ + ready: function() { + mediator.subscribe("start", function(msg, info) { + ok(false, "start should not have been called"); + }); + + var retval = controller.get(HTTP_TEST_DOMAIN, { + logoURL: "javascript:alert('xss')", + }); + + equal(retval, "must be an absolute path: (javascript:alert('xss'))", "expected error"); + testErrorVisible(); + start(); + } + }); + }); + + asyncTest("get with data-uri: logoURL - not allowed", function() { + createController({ + ready: function() { + mediator.subscribe("start", function(msg, info) { + ok(false, "start should not have been called"); + }); + + var retval = controller.get(HTTP_TEST_DOMAIN, { + logoURL: "data:image/png,FAKEDATA", + }); + + equal(retval, "must be an absolute path: (data:image/png,FAKEDATA)", "expected error"); + testErrorVisible(); + start(); + } + }); + }); + + asyncTest("get with http: logoURL - not allowed", function() { + createController({ + ready: function() { + mediator.subscribe("start", function(msg, info) { + ok(false, "start should not have been called"); + }); + + var retval = controller.get(HTTP_TEST_DOMAIN, { + logoURL: HTTP_TEST_DOMAIN + "://logo.png", + }); + + equal(retval, "must be an absolute path: (" + HTTP_TEST_DOMAIN + "://logo.png)", "expected error"); + testErrorVisible(); + start(); + } + }); + }); + + asyncTest("get with https: logoURL - not allowed", function() { + createController({ + ready: function() { + mediator.subscribe("start", function(msg, info) { + ok(false, "start should not have been called"); + }); + + var retval = controller.get(HTTP_TEST_DOMAIN, { + logoURL: HTTPS_TEST_DOMAIN + "://logo.png", + }); + + equal(retval, "must be an absolute path: (" + HTTPS_TEST_DOMAIN + "://logo.png)", "expected error"); + testErrorVisible(); + start(); + } + }); + }); + + asyncTest("get with absolute path - allowed URL but it must be properly escaped", function() { + createController({ + ready: function() { + var startInfo; + mediator.subscribe("start", function(msg, info) { + startInfo = info; + }); + + var logoURL = '/i/card.png" onerror="alert(\'xss\')" <script>alert(\'more xss\')</script>'; + var retval = controller.get(HTTP_TEST_DOMAIN, { + logoURL: logoURL + }); + + start(); + + testHelpers.testObjectValuesEqual(startInfo, { + logoURL: encodeURI(HTTP_TEST_DOMAIN + logoURL) + }); + equal(typeof retval, "undefined", "no error expected"); + testErrorNotVisible(); + start(); + } + }); + + }); + + + }()); diff --git a/resources/static/test/cases/controllers/rp_info.js b/resources/static/test/cases/controllers/rp_info.js new file mode 100644 index 0000000000000000000000000000000000000000..07a57d1c48248844c52ad4528953518c140f47e5 --- /dev/null +++ b/resources/static/test/cases/controllers/rp_info.js @@ -0,0 +1,88 @@ +/*jshint browsers:true, forin: true, laxbreak: true */ +/*global test: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID:true */ +/* 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/. */ +(function() { + "use strict"; + + var controller, + bid = BrowserID, + user = bid.User, + testHelpers = bid.TestHelpers, + register = bid.TestHelpers.register, + WindowMock = bid.Mocks.WindowMock, + RP_HOSTNAME = "hostname.org", + RP_NAME = "RP Name", + RP_HTTPS_LOGO = "https://en.gravatar.com/userimage/6966791/c4feac761b8544cce13e0406f36230aa.jpg"; + + module("controllers/rp_info", { + setup: testHelpers.setup, + + teardown: function() { + if (controller) { + try { + controller.destroy(); + controller = null; + } catch(e) { + // could already be destroyed from the close + } + } + window.scriptRun = null; + delete window.scriptRun; + testHelpers.teardown(); + } + }); + + + function createController(options) { + options = _.extend({ hostname: RP_HOSTNAME }, options); + + controller = bid.Modules.RPInfo.create(); + controller.start(options || {}); + } + + test("neither name nor logo specified - use site's rp_hostname as name", function() { + createController(); + equal($("#rp_hostname").html(), RP_HOSTNAME, "rp_hostname filled in"); + ok(!$("#rp_name").html(), "rp_name empty"); + ok(!$("#rp_logo").attr("src"), "rp logo not shown"); + }); + + test("name only specified - show specified name and rp_hostname", function() { + createController({ + name: RP_NAME, + }); + + equal($("#rp_hostname").html(), RP_HOSTNAME, "rp_hostname filled in"); + equal($("#rp_name").html(), RP_NAME, "rp_name filled in"); + ok(!$("#rp_logo").attr("src"), "rp logo not shown"); + }); + + test("logoURLs are allowed", function() { + var docMock = new WindowMock().document; + docMock.location.protocol = "http:"; + + createController({ + document: docMock, + logoURL: RP_HTTPS_LOGO + }); + + equal($("#rp_logo").attr("src"), RP_HTTPS_LOGO, "rp logo shown"); + equal($("#rp_hostname").html(), RP_HOSTNAME, "rp_hostname filled in"); + ok(!$("#rp_name").html(), "rp_name empty"); + }); + + test("both name and logo specified - show name, logo and rp_hostname", function() { + createController({ + name: RP_NAME, + logoURL: RP_HTTPS_LOGO + }); + + equal($("#rp_hostname").html(), RP_HOSTNAME, "rp_hostname filled in"); + equal($("#rp_name").html(), RP_NAME, "rp_name filled in"); + equal($("#rp_logo").attr("src"), RP_HTTPS_LOGO, "rp logo shown"); + }); + +}()); + diff --git a/resources/views/dialog.ejs b/resources/views/dialog.ejs index 1752efdd3c5de51970154617b621af299d07881a..9bf2250be9fd1a0834b44c4843125e54fe8f2c7e 100644 --- a/resources/views/dialog.ejs +++ b/resources/views/dialog.ejs @@ -5,8 +5,7 @@ <form novalidate> <div id="favicon"> <div class="table"> - <div class="vertical"> - <strong id="sitename"></strong> + <div class="vertical" id="rp_info"> </div> </div> </div> diff --git a/resources/views/test.ejs b/resources/views/test.ejs index dd25eca2f3ca107c19d0fe0776a669ed16ec0e06..8f6ea43fe4403fe499028fe55ee072f1b92ab9a5 100644 --- a/resources/views/test.ejs +++ b/resources/views/test.ejs @@ -25,6 +25,9 @@ <a href="#" onclick="$('#contents').hide(); return false;">Close</a> <h3>Test Contents, this will be updated and can be safely ignored</h3> + <div id="rp_info"> + </div> + <div id="page_head"> </div> @@ -131,6 +134,7 @@ <script src="/dialog/controllers/primary_user_provisioned.js"></script> <script src="/dialog/controllers/is_this_your_computer.js"></script> <script src="/dialog/controllers/set_password.js"></script> + <script src="/dialog/controllers/rp_info.js"></script> <script src="/pages/page_helpers.js"></script> <script src="/pages/verify_secondary_address.js"></script> @@ -191,6 +195,7 @@ <script src="cases/controllers/primary_user_provisioned.js"></script> <script src="cases/controllers/is_this_your_computer.js"></script> <script src="cases/controllers/set_password.js"></script> + <script src="cases/controllers/rp_info.js"></script> <!-- must go last or all other tests will fail. --> <script src="cases/controllers/dialog.js"></script>