diff --git a/.gitignore b/.gitignore index ff44ccd0c0be5f6e7288f1282a2f0a853f06dc72..5c48f0e0b97b6d82bf93880858cd51d2db92a22b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ /npm-debug.log /resources/static/build /resources/static/production +.DS_Store \ No newline at end of file diff --git a/ChangeLog b/ChangeLog index e30219c1821e21c9a0f791e3a317adf012ec93a2..8e07fa26f09ef9b8313c0e2d0816dc00689b0c30 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,23 @@ -train-2012.06.22: (in progress) - * +train-2012.07.06: (in progress) + +train-2012.06.22: + * browserid.org now redirects to login.persona.org, all URLs are updated: #1743 + * Websites can now provide their name and logo (requires SSL) to be displayed in the dialog: #1098, #1761 + * A user is now sent back to the site they were visiting after verification (requires .watch() API): #385 + * Fix .watch() API under IE8: #1637 + * For dev and ephemeral deployments, move to awsbox, and new URLs: #1394, #1046, #1741 + * Fix the scroll bar appearing on the main site's index page if it is not needed: #1693 + * Clear the password if the user types a password then changes the email address: #1540 + * New watch API now requires invocation with proper context (navigator.id.foo, not var foo = navigator.id.foo) + * Router fixes: #1713 + * Serve fonts locally, don't pull resources from google: #1695 + * Optimize images: #1747 + * Fix flashes when verifying an email address: #1734 + * Unit test added which runs jshint: #1731 + * Fix submit occurring when selecting an email address in Firefox from the auto-complete list: #1780 + * For KPI data, round timestamp to nearest 10 minutes, making correlation improbable: #1732 + * Code cleanup: #1701, #1703, #1000, #1248, #1759, #1733, #1792 + * Breaking API change: Persona now returns pubkey from generateKeypair to IdPs as a string train-2012.06.08: * rebrand from 'browserid' to 'persona': (including regressions #1711 #1706 #1716 #1719) diff --git a/bin/router b/bin/router index 56db15569691be00b3498ca77ddffe1171575f22..dc409f51267387c05c08a9f0f6c540854cf6c25c 100755 --- a/bin/router +++ b/bin/router @@ -49,7 +49,10 @@ if (!config.get('browserid_url')) { // #1 - Setup health check / heartbeat middleware. // This is in front of logging on purpose. see issue #537 -heartbeat.setup(app); +var browserid_url = urlparse(config.get('browserid_url')).validate().normalize().originOnly(); +heartbeat.setup(app, { + dependencies: [browserid_url] +}); // #2 - logging! all requests other than __heartbeat__ are logged app.use(express.logger({ @@ -119,7 +122,6 @@ wsapi.setup({ }, app); // Forward all leftover requests to browserid -var browserid_url = urlparse(config.get('browserid_url')).validate().normalize().originOnly(); app.use(function(req, res, next) { forward( browserid_url+req.url, req, res, diff --git a/config/aws.json b/config/aws.json index 418d8fc46340525c56893f7452c79cb9d41b1c94..95eb55fdfae619519adb14a3a8040cd717dcc507 100644 --- a/config/aws.json +++ b/config/aws.json @@ -21,5 +21,7 @@ }, "proxy": { "bind_to": { "port": 10006 } }, "router": { "bind_to": { "port": 8080 } }, - "kpi_backend_db_url" : "https://kpiggybank.hacksign.in/wsapi/interaction_data" + "kpi_backend_db_url" : "https://kpiggybank.hacksign.in/wsapi/interaction_data", + // whether to show the development menu. + "enable_development_menu": true } diff --git a/config/local.json b/config/local.json index 6d346f58d91652f91b4eff0e375a3941ec55d7a2..0aec32890f91b63109148e09f7bf85cd7877df87 100644 --- a/config/local.json +++ b/config/local.json @@ -12,5 +12,8 @@ "express_log_format": "dev_bid", "email_to_console": true, "env": "local", - "kpi_backend_sample_rate": 1.0 + "kpi_backend_sample_rate": 1.0, + + // whether to show the development menu. + "enable_development_menu": true } diff --git a/config/production.json b/config/production.json index cf9b1968d99e7548030a83e60c4978f78352c1d2..13cbcaf2ed977a1d1dc597c93a2d988335666677 100644 --- a/config/production.json +++ b/config/production.json @@ -50,7 +50,10 @@ "dbwriter_url": "http://127.0.0.1:62900", "browserid": { "bind_to": { "port": 62700 } }, "browserid_url": "http://127.0.0.1:62700", - "router": { "bind_to": { "port": 63300 } } + "router": { "bind_to": { "port": 63300 } }, + + // set to true to enable the development menu. + "enable_development_menu": false // http_proxy should be overridded per env //"http_proxy": { diff --git a/example/primary/provision.html b/example/primary/provision.html index 94395c345c921b38b0949f727cd28b1f9f03cda7..c8b7cd583c4c38008d9df3181fa2ee3539bbce92 100644 --- a/example/primary/provision.html +++ b/example/primary/provision.html @@ -33,7 +33,7 @@ $.ajax({ url: '/api/cert_key', data: JSON.stringify({ - pubkey: pubkey, + pubkey: JSON.parse(pubkey), duration: cert_duration }), type: 'POST', diff --git a/lib/browserid/views.js b/lib/browserid/views.js index 15fcadb077e7c06f9d9525bb52398d988239b053..53fcf01427448db8ed61dd22d00e6de3f6f796f5 100644 --- a/lib/browserid/views.js +++ b/lib/browserid/views.js @@ -38,6 +38,9 @@ function renderCachableView(req, res, template, options) { res.setHeader('Date', new Date().toUTCString()); res.setHeader('Vary', 'Accept-Encoding,Accept-Language'); res.setHeader('Content-Type', 'text/html; charset=utf8'); + + options.enable_development_menu = config.get('enable_development_menu'); + res.render(template, options); } @@ -136,7 +139,12 @@ exports.setup = function(app) { app.get("/forgot", function(req, res) { // !cachable! email embedded in DOM - res.render('forgot.ejs', {title: 'Forgot Password', fullpage: false, email: req.query.email}); + res.render('forgot.ejs', { + title: 'Forgot Password', + fullpage: false, + email: req.query.email, + enable_development_menu: config.get('enable_development_menu') + }); }); app.get("/signin", function(req, res) { @@ -157,7 +165,12 @@ exports.setup = function(app) { app.get("/verify_email_address", function(req, res) { // !cachable! token is embedded in DOM - res.render('verify_email_address.ejs', {title: 'Complete Registration', fullpage: true, token: req.query.token}); + res.render('verify_email_address.ejs', { + title: 'Complete Registration', + fullpage: true, + token: req.query.token, + enable_development_menu: config.get('enable_development_menu') + }); }); app.get("/add_email_address", function(req,res) { @@ -168,7 +181,7 @@ exports.setup = function(app) { if ([ 'https://login.persona.org', 'https://login.anosrep.org' ].indexOf(config.get('public_url')) === -1) { // serve test.ejs to /test or /test/ or /test/index.html app.get(/^\/test\/(?:index.html)?$/, function (req, res) { - res.render('test.ejs', {title: 'BrowserID QUnit Test', layout: false}); + res.render('test.ejs', {title: 'Mozilla Persona QUnit Test', layout: false}); }); } else { // this is stage or production, explicitly disable all resources under /test diff --git a/lib/configuration.js b/lib/configuration.js index 2858160e4e2b9acc26a0ab3eacf640a4ba606adc..0c5b68dce08d4963c117b79ced4bf9663e2d4c09 100644 --- a/lib/configuration.js +++ b/lib/configuration.js @@ -219,6 +219,10 @@ var conf = module.exports = convict({ declaration_of_support_timeout_ms: { doc: "The amount of time we wait for a server to respond with a declaration of support, before concluding that they are not a primary. Only relevant when our local proxy is in use, not in production or staging", format: 'integer = 15000' + }, + enable_development_menu: { + doc: "Whether or not the development menu can be accessed", + format: 'boolean = false' } }); diff --git a/lib/heartbeat.js b/lib/heartbeat.js index 666e77767ac78f4fb8fdb74355a42a4bf870dadc..e5e309fd4c990366e7cb198bb549744e179ea550 100644 --- a/lib/heartbeat.js +++ b/lib/heartbeat.js @@ -4,20 +4,74 @@ const urlparse = require('urlparse'), -logger = require('./logging.js').logger; +logger = require('./logging.js').logger, +url = require('url'); // the path that heartbeats live at exports.path = '/__heartbeat__'; +const checkTimeout = 5000; + // a helper function to set up a heartbeat check -exports.setup = function(app, cb) { +exports.setup = function(app, options, cb) { + var dependencies = []; + + if (typeof options == 'function') { + cb = options; + } else if (options && options.dependencies) { + dependencies = options.dependencies; + } + var count = dependencies.length; + app.use(function(req, res, next) { - if (req.method === 'GET' && req.path === exports.path) { - function ok(yeah) { - res.writeHead(yeah ? 200 : 500); - res.write(yeah ? 'ok' : 'not ok'); - res.end(); + if (req.method !== 'GET' || req.path !== exports.path) { + return next(); + } + + var checked = 0; + var query = url.parse(req.url, true).query; + var deep = typeof query.deep != 'undefined'; + var notOk = []; + + // callback for checking a dependency + function checkCB (num) { + return function (err, isOk) { + checked++; + if (err) { + notOk.push(dependencies[num] + ': '+ err); + } + + // if all dependencies have been checked + if (checked == count) { + if (notOk.length === 0) { + try { + if (cb) cb(ok); + else ok(true); + } catch(e) { + logger.error("Exception caught in heartbeat handler: " + e.toString()); + ok(false, e); + } + } else { + logger.warn("heartbeat failed due to dependencies - " + notOk.join(', ')); + ok(false, '\n' + notOk.join('\n') + '\n'); + } + } + }; + } + + function ok(yeah, msg) { + res.writeHead(yeah ? 200 : 500); + res.write(yeah ? 'ok' : 'bad'); + if (msg) res.write(msg); + res.end(); + } + + // check all dependencies if deep + if (deep && count) { + for (var i = 0; i < count; i++) { + check(dependencies[i] + exports.path, checkCB(i)); } + } else { try { if (cb) cb(ok); else ok(true); @@ -25,29 +79,39 @@ exports.setup = function(app, cb) { logger.error("Exception caught in heartbeat handler: " + e.toString()); ok(false); } - } else { - return next(); } }); }; + // a function to check the heartbeat of a remote server -exports.check = function(url, cb) { +var check = exports.check = function(url, cb) { if (typeof url === 'string') url = urlparse(url).normalize().validate(); else if (typeof url !== 'object') throw "url string or object required as argumnet to heartbeat.check"; if (!url.port) url.port = (url.scheme === 'http') ? 80 : 443; var shortname = url.host + ':' + url.port; - require(url.scheme).get({ + var timeoutHandle = setTimeout(function() { + req.abort(); + }, checkTimeout); + + var req = require(url.scheme).get({ host: url.host, port: url.port, path: exports.path }, function (res) { - if (res.statusCode === 200) cb(true); - else logger.error("non-200 response from " + shortname + ". fatal! (" + res.statusCode + ")"); - }, function (e) { - logger.error("can't communicate with " + shortname + ". fatal: " + e); - cb(false); + clearTimeout(timeoutHandle); + if (res.statusCode === 200) cb(null, true); + else { + logger.warn("heartbeat failure: non-200 response from " + shortname + ". fatal! (" + + res.statusCode + ")"); + cb("response code " + res.statusCode); + } + }); + req.on('error', function (e) { + clearTimeout(timeoutHandle); + logger.warn("heartbeat failure: can't communicate with " + shortname + ". fatal: " + e); + cb(e ? e : "unknown error"); }); }; diff --git a/lib/static_resources.js b/lib/static_resources.js index 9c0b1e951dd482c52c874ce01f7b83971ac7e54f..b994c3842d6b8abe2f939c53caf349150c4e521c 100644 --- a/lib/static_resources.js +++ b/lib/static_resources.js @@ -68,7 +68,8 @@ var browserid_js = und.flatten([ '/pages/forgot.js', '/pages/manage_account.js', '/pages/signin.js', - '/pages/signup.js' + '/pages/signup.js', + '/pages/about.js' ] ]); diff --git a/package.json b/package.json index 7801ed885964e53f924355ddae3bb3d909007649..eecc9a09648dccf0957d9ccd54744eefca6de0ed 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,8 @@ "devDependencies": { "vows": "0.5.13", "awsbox": "0.2.12", - "irc": "0.3.3" + "irc": "0.3.3", + "jshint": "0.7.1" }, "scripts": { "postinstall": "./scripts/generate_ephemeral_keys.sh", diff --git a/resources/static/communication_iframe/start.js b/resources/static/communication_iframe/start.js index 57ce36dcaca52ba1d78fc3db41208e89371444ec..fa26648aef2caed72eeb18c9a81b2259cee266c5 100644 --- a/resources/static/communication_iframe/start.js +++ b/resources/static/communication_iframe/start.js @@ -9,6 +9,11 @@ user = bid.User, storage = bid.Storage; + // Initialize all localstorage values to default values. Neccesary for + // proper sync of IE8 localStorage across multiple simultaneous + // browser sessions. + storage.setDefaultValues(); + network.init(); var chan = Channel.build({ diff --git a/resources/static/css/common.css b/resources/static/css/common.css index c12bdae416fdfb37a4ca365559162111d5418ab9..11f1f79ee52db1129100d736c9765cecb79de34d 100644 --- a/resources/static/css/common.css +++ b/resources/static/css/common.css @@ -128,6 +128,10 @@ input[type=password]:disabled { * issue #1311 */ -webkit-text-fill-color: #4f4f4f; opacity: 1; + /* Remove the box-shadow and border-color that come with a focused input + * field */ + box-shadow: none; + border-color: #b2b2b2; } input[type=radio], @@ -359,7 +363,12 @@ footer .help { #wait, #delay, #error { background-color: #dadee1; - background-image: url("/i/grain.png"), -moz-linear-gradient(top, #dadee1, #c7ccd0); + background-image: url("/i/grain.png"), -webkit-gradient(linear, left top, left bottom, from(#dadee1), to(#c7ccd0)); + background-image: url("/i/grain.png"), -webkit-linear-gradient(top, #dadee1, #c7ccd0); + background-image: url("/i/grain.png"), -moz-linear-gradient(top, #dadee1, #c7ccd0); + background-image: url("/i/grain.png"), -ms-linear-gradient(top, #dadee1, #c7ccd0); + background-image: url("/i/grain.png"), -o-linear-gradient(top, #dadee1, #c7ccd0); + background-image: url("/i/grain.png"), linear-gradient(top, #dadee1, #c7ccd0); } #wait, #delay { diff --git a/resources/static/css/m.css b/resources/static/css/m.css index 8325d9e0bf567610fb120ad1941dc9c7cc4640fc..50943635fe29a7aa2366788155d2265970643603 100644 --- a/resources/static/css/m.css +++ b/resources/static/css/m.css @@ -45,7 +45,7 @@ /* * 620 catches most mobile devices in landscape mode. The purpose of this is * to make sure the right hand nav menu does not drop partially below the - * persona logo. + * persona logo. This also adjusts the boxes on the "How It Works" page. */ @media screen and (max-width: 620px) { header ul { @@ -54,6 +54,51 @@ display: block; text-align: center; } + + .blurb.half { + width: 100%; + float: none; + min-height: 0 !important; + } + .blurb.half.first { + margin-right: 0; + } + + .blurb { + display: -webkit-box; + display: box; + -webkit-box-orient: vertical; + box-orient: vertical; + } + .blurb .info { + -webkit-box-ordinal-group: 2; + -moz-box-ordinal-group: 2; + -ms-box-ordinal-group: 2; + box-ordinal-group: 2; + } + .blurb h1{ + font-size: 20px; + } + .blurb.flexible .graphic { + margin: 0 0 30px; + } + .blurb .first { + padding-right: 0; + } + .blurb .info, .blurb .graphic { + float: none; + width: 100%; + } + + h2.title { + font-size: 32px; + padding-bottom: 15px; + } + + .privacy{ + margin: 60px 0 30px; + padding-bottom: 30px; + } } /* @@ -78,11 +123,6 @@ padding: 10px; } - #about { - padding: 10px; - border-radius: 5px; - } - .headline-main { font-size: 37px; text-align: center; @@ -159,39 +199,6 @@ list-style-position: inside; } - #about .video, - #about .video img { - width: 300px; - height: auto; - } - - .row { - padding: 20px 20px 0; - margin: 0; - } - - .row div { - width: auto; - height: auto; - vertical-align: inherit; - display: block; - padding: 20px 0; - } - - .row img { - float: none; - width: 260px; - height: auto; - } - - .row p { - float: none; - display: block; - width: 260px; - text-indent: -33px; - padding-left: 33px; - } - #signUpFormWrap { margin: 122px 10px 122px; } diff --git a/resources/static/css/style.css b/resources/static/css/style.css index 35bfa524ab66efd353488ddd596061022bb3fc77..6ac4fb9fd4c9c7f5c8eac819133263d8ed2883eb 100644 --- a/resources/static/css/style.css +++ b/resources/static/css/style.css @@ -106,15 +106,6 @@ body { padding: 50px 0; } -#about { - color: #444; - text-shadow: 1px 1px 0 rgba(255,255,255,0.5); - padding: 50px 75px; - background-color: #fff; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25); - border-radius: 5px; -} - h1 { margin-bottom: 35px; } @@ -123,65 +114,6 @@ h1 { font-weight: 300; } -.row { - margin: 25px 0 0 0; - padding: 0 0 25px 0; - position: relative; - border-bottom: 1px solid #eee; -} - - - - -.row:last-child { - padding-bottom: 0; - border-bottom: none; -} - -.row div { - height: 140px; - width: 500px; - padding: 0 0 0 20px; - display: table-cell; - vertical-align: middle; -} - -.row p { - width: 380px; - text-shadow: 1px 1px 0 rgba(255,255,255,0.5); - float: left; -} - -.row img { - float: left; -} - -.row button, .row .button { - float: right; - display: inline-block; -} - -div.steps { - width: 24px; - height: 24px; - vertical-align: bottom; - margin-right: 10px; - background-image: url('/i/count.png'); - float: left; -} - -.one .steps { - background-position: left 0; -} - -.two .steps { - background-position: left -24px; -} - -.three .steps { - background-position: left -48px; -} - #legal { padding: 75px 125px; @@ -515,30 +447,10 @@ button.delete:active { padding: 0; } -#signUpForm .red { - color: red; -} - #signUpForm .submit { height: 28px; } -#signUpForm .error { - margin-top: 20px; - color: red; - background-color: rgba(255,0,0,0.25); - -ms-filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#3fff0000,endColorstr=#3fff0000); - filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#3fff0000,endColorstr=#3fff0000); - zoom: 1; - padding: 5px; - line-height: 16px; - - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - -o-border-radius: 3px; - border-radius: 3px; -} - #signUpForm > .siteinfo { margin-bottom: 10px; } @@ -679,3 +591,141 @@ footer ul li:first-child a:hover { display: block; } +/* How It Works + ***************/ + + h2.title { + font-size: 48px; + font-weight: normal; + color: #fff; + text-shadow: 0 1px rgba(0, 0, 0, 0.5); + text-align: center; + letter-spacing: -2px; + padding-bottom: 30px; + margin: 0; +} + +.blurb, a.developers { + -webkit-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.13); + -moz-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.13); + -ms-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.13); + box-shadow: 0 1px 0 rgba(255, 255, 255, 0.13); + background: #63727d; + background: rgba(13, 28, 41, 0.1); + font-size: 14px; + color: #fff; +} + +.blurb { + zoom: 1; + margin-top: 30px; + padding: 30px; + text-align: left; + line-height: 1.5; +} +.blurb:before, .blurb:after { + content: ""; + display: table; +} +.blurb:after { + clear: both; +} + +.blurb h1, .blurb p, .blurb a, a.developers{ + text-shadow: 0 1px rgba(0, 0, 0, 0.5); + font-weight: normal; +} + +.blurb img{ + max-width: 100%; + vertical-align: bottom; +} + +.blurb a { + color: #fff; + border-bottom: 1px dotted #fff; + font-weight: normal; +} +.blurb a:hover { + color: #53b7fb; +} +.blurb.half { + width: 48%; + float: left; +} +.blurb.half.first { + margin-right: 4%; +} +.blurb .info, .blurb .graphic { + width: 50%; + float: left; +} +.blurb .first { + padding-right: 30px; +} +.blurb .graphic { + text-align: center; +} +.blurb h1 { + font-size: 32px; + font-weight: normal; + letter-spacing: -1px; + line-height: 1.1; + margin-bottom: 20px; +} +.blurb p { + margin-bottom: 1em; +} +.blurb p:last-of-type { + margin-bottom: 0; +} + +.privacy { + -webkit-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.1); + -moz-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.1); + -ms-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.1); + box-shadow: 0 1px 0 rgba(255, 255, 255, 0.1); + zoom: 1; + border-bottom: 1px solid rgba(0, 0, 0, 0.2); + padding-bottom: 60px; + margin: 100px 0 60px; +} +.privacy:before, .privacy:after { + content: ""; + display: table; +} +.privacy:after { + clear: both; +} + +a.developers { + -webkit-transition: all 300ms ease; + -moz-transition: all 300ms ease; + -ms-transition: all 300ms ease; + transition: all 300ms ease; + display: block; + padding: 13px 15px; + line-height: 1.4; + text-align: center; +} +a.developers:hover { + background: #3b4e5c; + background: rgba(13, 28, 41, 0.2); +} +a.developers img { + margin: 0 5px -7px 0; +} +a.developers span { + color: #53b7fb; + font-weight: bold; + margin-right: 10px; + display: inline-block; +} + +article.flexible { + padding-bottom: 0; +} +article.flexible .info { + margin-bottom: 30px; +} + diff --git a/resources/static/dialog/controllers/actions.js b/resources/static/dialog/controllers/actions.js index 80435e5220f6bafe3e5856ef3aeb358bc715a098..3a95c89155d98f12c71b9f8eb11231f0a1f82bfe 100644 --- a/resources/static/dialog/controllers/actions.js +++ b/resources/static/dialog/controllers/actions.js @@ -28,7 +28,6 @@ BrowserID.Modules.Actions = (function() { } mediator.publish("service", { name: name }); - bid.resize(); return module; } diff --git a/resources/static/dialog/controllers/add_email.js b/resources/static/dialog/controllers/add_email.js index c9a8c638ca85ddc7f7566fb1c80e8dd90fa85d89..da642e0d2ced87d3cdf769b0d2389e0b2ad46e9a 100644 --- a/resources/static/dialog/controllers/add_email.js +++ b/resources/static/dialog/controllers/add_email.js @@ -7,17 +7,38 @@ BrowserID.Modules.AddEmail = (function() { "use strict"; var bid = BrowserID, + dom = bid.DOM, helpers = bid.Helpers, dialogHelpers = helpers.Dialog, errors = bid.Errors, complete = helpers.complete, - tooltip = bid.Tooltip; + tooltip = bid.Tooltip, + hints = ["addressInfo"], + ANIMATION_TIME = 250; + + function hideHint(selector) { + $("." + selector).hide(); + } + + function showHint(selector, callback) { + _.each(hints, function(className) { + if (className !== selector) { + hideHint(className); + } + }); + + $("." + selector).fadeIn(ANIMATION_TIME, function() { + dom.fireEvent(window, "resize"); + complete(callback); + }); + } function addEmail(callback) { var email = helpers.getAndValidateEmail("#newEmail"), self=this; if (email) { + showHint("addressInfo"); dialogHelpers.addEmail.call(self, email, callback); } else { @@ -39,6 +60,7 @@ BrowserID.Modules.AddEmail = (function() { }); self.renderDialog("add_email", templateData); + hideHint("addressInfo"); self.click("#cancel", cancelAddEmail); Module.sc.start.call(self, options); diff --git a/resources/static/dialog/controllers/authenticate.js b/resources/static/dialog/controllers/authenticate.js index ac413aa99a82cb3fd6573580dee2a04d4e83e99c..c316e55a5e5822825c2ad2bb69f1b32a6bf106fa 100644 --- a/resources/static/dialog/controllers/authenticate.js +++ b/resources/static/dialog/controllers/authenticate.js @@ -43,6 +43,7 @@ BrowserID.Modules.Authenticate = (function() { if (!email) return; + dom.setAttr('#email', 'disabled', 'disabled'); if(info && info.type) { onAddressInfo(info); } @@ -54,6 +55,7 @@ BrowserID.Modules.Authenticate = (function() { function onAddressInfo(info) { addressInfo = info; + dom.removeAttr('#email', 'disabled'); if(info.type === "primary") { self.close("primary_user", info, info); diff --git a/resources/static/dialog/controllers/dialog.js b/resources/static/dialog/controllers/dialog.js index eff93813905bb9617cb979b6286d5b9a89c01b6f..1e14ca80d2f1354555169b977c9986431cbbe8a8 100644 --- a/resources/static/dialog/controllers/dialog.js +++ b/resources/static/dialog/controllers/dialog.js @@ -177,6 +177,11 @@ BrowserID.Modules.Dialog = (function() { // that come from other domains, only allow absolute paths from the // origin. params.siteLogo = fixupAbsolutePath(origin_url, paramsFromRP.siteLogo); + // To avoid mixed content errors, only allow siteLogos to be served + // from https RPs + if (URLParse(origin_url).scheme !== "https") { + throw "only https sites can specify a siteLogo"; + } } if (paramsFromRP.siteName) { diff --git a/resources/static/dialog/css/m.css b/resources/static/dialog/css/m.css index 2a757809b174111c8050bdb28fcc7a5a5cd8d9cf..ff2ee544b1ecc1f17c11781afb4dfa69e4e2313b 100644 --- a/resources/static/dialog/css/m.css +++ b/resources/static/dialog/css/m.css @@ -159,14 +159,6 @@ line-height: 40px; } - #error { - position: static; - } - - #error .vertical, #delay .vertical, #wait .vertical { - height: 250px; - } - #error .vertical { width: auto; } diff --git a/resources/static/dialog/resources/screen_size_hacks.js b/resources/static/dialog/resources/screen_size_hacks.js index 33685cb756f36123246e781af5600ff6b586e4c8..c304ca0e4cd3cac9d242d47d07a78f3993e78e30 100644 --- a/resources/static/dialog/resources/screen_size_hacks.js +++ b/resources/static/dialog/resources/screen_size_hacks.js @@ -106,6 +106,8 @@ contentHeight = Math.max(100, contentHeight, formHeight); contentEl.css("min-height", contentHeight + "px"); + // Remove the explicit static position we added to let this go back to + // the position specified in CSS. $("section,#signIn").css("position", ""); favIconHeight = $("#favicon").outerHeight(); diff --git a/resources/static/dialog/views/add_email.ejs b/resources/static/dialog/views/add_email.ejs index 5af78c55bba4147120393579d08484e2be5ebbb2..9e183be4c20c66d881def28e9617382b19731f1a 100644 --- a/resources/static/dialog/views/add_email.ejs +++ b/resources/static/dialog/views/add_email.ejs @@ -27,6 +27,10 @@ <%= gettext('That address is already added to your account!') %> </div> </li> + + <li id="hint_section" class="addressInfo"> + <%= gettext("Please hold on while we get information about your email provider.") %> + </li> </ul> <div class="submit cf"> diff --git a/resources/static/dialog/views/error.ejs b/resources/static/dialog/views/error.ejs index 0b35cd680dc17fcb47e25adc6284ae7cce111b46..2a0ee0066700746b6c73e31818f4353ee1156976 100644 --- a/resources/static/dialog/views/error.ejs +++ b/resources/static/dialog/views/error.ejs @@ -14,6 +14,10 @@ <%= gettext("Persona requires cookies") %> </h2> <%= format(gettext("Please close this window, <a %s>enable cookies</a> and try again"), [" target='_blank' href='http://support.mozilla.org/en-US/kb/Websites%20say%20cookies%20are%20blocked'"]) %> + <% } else if(typeof title === "string") { %> + <h2> + <span class="emphasis"><%= title %></span> + </h2> <% } else { %> <h2 id="defaultError"> <%= gettext("We are very sorry.") %><span class="emphasis"> <%= gettext("There has been an error!") %></span> diff --git a/resources/static/fonts/LICENSE.txt b/resources/static/fonts/LICENSE.txt new file mode 100644 index 0000000000000000000000000000000000000000..0b365b09f4a75a94f6e9baa26b7283ce7c873d0d --- /dev/null +++ b/resources/static/fonts/LICENSE.txt @@ -0,0 +1,203 @@ +Fonts obtained from Google's Web Font service at: http://www.google.com/webfonts + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/resources/static/i/developers-link.png b/resources/static/i/developers-link.png new file mode 100644 index 0000000000000000000000000000000000000000..e730af8df49fcf06234eb9361ec5b894870fd922 Binary files /dev/null and b/resources/static/i/developers-link.png differ diff --git a/resources/static/i/flexible-graphic.png b/resources/static/i/flexible-graphic.png new file mode 100644 index 0000000000000000000000000000000000000000..c94d2cc1e3e86106a155785b26f6b2c151a19a11 Binary files /dev/null and b/resources/static/i/flexible-graphic.png differ diff --git a/resources/static/i/one-password-graphic.png b/resources/static/i/one-password-graphic.png new file mode 100644 index 0000000000000000000000000000000000000000..380579a0ed0599b8fc206d1331817d175202b5ab Binary files /dev/null and b/resources/static/i/one-password-graphic.png differ diff --git a/resources/static/pages/about.js b/resources/static/pages/about.js new file mode 100644 index 0000000000000000000000000000000000000000..9741062539480bfb5b450d9eac79235427d75c5e --- /dev/null +++ b/resources/static/pages/about.js @@ -0,0 +1,45 @@ +/*globals BrowserID:true, $: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/. */ + +BrowserID.about = (function() { + "use strict"; + + var bid = BrowserID; + + function resize() { + // Get tallest blurb + var tallestBlurb = 0 + + $('.half').each(function(index) { + var $this = $(this); + + if (index == 0) { + tallestBlurb = $this.height(); + } else { + + if ($this.height() < tallestBlurb) { + $this.css('min-height', tallestBlurb); + } else { + $('.half.first').css('min-height', $this.height()); + } + + } + }); + } + + var Module = bid.Modules.PageModule.extend({ + start: function(options) { + var self=this; + + Module.sc.start.call(self, options); + resize(); + + // The half heights can change every time there is a window resize. + self.bind(window, "resize", resize); + } + }); + + return Module; +}()); diff --git a/resources/static/pages/signin.js b/resources/static/pages/signin.js index 8a487f82fff0b6a0cbb9914ac7fb311e698a7b87..edc58501195871df8b021adfdcd325f87ba6357d 100644 --- a/resources/static/pages/signin.js +++ b/resources/static/pages/signin.js @@ -48,7 +48,9 @@ BrowserID.signIn = (function() { email = helpers.getAndValidateEmail("#email"); if(email) { + dom.setAttr('#email', 'disabled', 'disabled'); user.addressInfo(email, function(info) { + dom.removeAttr('#email', 'disabled'); addressInfo = info; if(info.type === "secondary") { diff --git a/resources/static/pages/signup.js b/resources/static/pages/signup.js index 3fef51badbfe646ad8f2d9ec523afff178dfd896..d7d4049ba49380eddc94045312655fd0685d1c5f 100644 --- a/resources/static/pages/signup.js +++ b/resources/static/pages/signup.js @@ -99,15 +99,17 @@ BrowserID.signUp = (function() { self = this; if (email) { - + dom.setAttr('#email', 'disabled', 'disabled'); user.isEmailRegistered(email, function(isRegistered) { if(isRegistered) { + dom.removeAttr('#email', 'disabled'); $('#registeredEmail').html(email); showNotice(".alreadyRegistered"); oncomplete && oncomplete(false); } else { user.addressInfo(email, function(info) { + dom.removeAttr('#email', 'disabled'); if(info.type === "primary") { createPrimaryUser.call(self, info, oncomplete); } diff --git a/resources/static/pages/start.js b/resources/static/pages/start.js index f6bb1b258897ed2588cd7c148d8445065db60d09..62f92b774bed66ebba614798c1d1303912b51670 100644 --- a/resources/static/pages/start.js +++ b/resources/static/pages/start.js @@ -90,7 +90,7 @@ $(function() { // footer remains at the bottom of the screen. var paddingTop = 0, paddingBottom = 0; - if(paddingAddedToMinHeight()) { + if (paddingAddedToMinHeight()) { paddingTop = parseInt($("#content").css("padding-top") || 0, 10); paddingBottom = parseInt($("#content").css("padding-bottom") || 0, 10); } @@ -107,7 +107,7 @@ $(function() { moduleManager.register("development", Development); moduleManager.start("development"); - if(shouldCheckCookies(path)) { + if (shouldCheckCookies(path)) { // do a cookie check on every page except the main page. moduleManager.register("cookie_check", CookieCheck); moduleManager.start("cookie_check", { ready: start }); @@ -120,7 +120,7 @@ $(function() { function start(status) { // If cookies are disabled, do not run any of the page specific code and // instead just show the error message. - if(!status) return; + if (!status) return; if (!path || path === "/") { @@ -144,13 +144,17 @@ $(function() { verifyFunction: "verifyEmail" }); } - else if(path === "/verify_email_address") { + else if (path === "/verify_email_address") { var module = bid.verifySecondaryAddress.create(); module.start({ token: token, verifyFunction: "verifyUser" }); } + else if (path === "/about") { + var module = bid.about.create(); + module.start({}); + } else { // Instead of throwing a hard error here, adding a message to the console // to let developers know something is up. diff --git a/resources/static/pages/verify_secondary_address.js b/resources/static/pages/verify_secondary_address.js index df4561ec37b6707e3401afafd4f7c07225ba283a..5b2f7f20051e8bd9c3ce71c181ac75c2cc6c4c69 100644 --- a/resources/static/pages/verify_secondary_address.js +++ b/resources/static/pages/verify_secondary_address.js @@ -19,7 +19,6 @@ BrowserID.verifySecondaryAddress = (function() { tooltip = bid.Tooltip, token, sc, - needsPassword, mustAuth, verifyFunction, doc = document, @@ -29,11 +28,6 @@ BrowserID.verifySecondaryAddress = (function() { redirectTo, redirectTimeout; // set in config if available, use REDIRECT_SECONDS otw. - function showError(el, oncomplete) { - dom.hide(".hint,#signUpForm"); - $(el).fadeIn(ANIMATION_TIME, oncomplete); - } - function showRegistrationInfo(info) { dom.setInner("#email", info.email); @@ -55,38 +49,37 @@ BrowserID.verifySecondaryAddress = (function() { function submit(oncomplete) { var pass = dom.getInner("#password") || undefined, - vpass = dom.getInner("#vpassword") || undefined, - inputValid = (!needsPassword || - validation.passwordAndValidationPassword(pass, vpass)) - && (!mustAuth || - validation.password(pass)); + inputValid = !mustAuth || validation.password(pass); if (inputValid) { user[verifyFunction](token, pass, function(info) { dom.addClass("body", "complete"); - var verified = info.valid, - selector = verified ? "#congrats" : "#cannotcomplete"; - - pageHelpers.replaceFormWithNotice(selector, function() { - if (redirectTo && verified) { - - // set the loggedIn status for the site. This allows us to get - // a silent assertion without relying on the dialog to set the - // loggedIn status for the domain. This is useful when the user - // closes the dialog OR if redirection happens before the dialog - // has had a chance to finish its business. - storage.setLoggedIn(URLParse(redirectTo).originOnly(), email); - - setTimeout(function() { - doc.location.href = redirectTo; + var verified = info.valid; + + if (verified) { + pageHelpers.replaceFormWithNotice("#congrats", function() { + if (redirectTo) { + // set the loggedIn status for the site. This allows us to get + // a silent assertion without relying on the dialog to set the + // loggedIn status for the domain. This is useful when the user + // closes the dialog OR if redirection happens before the dialog + // has had a chance to finish its business. + storage.setLoggedIn(URLParse(redirectTo).originOnly(), email); + + setTimeout(function() { + doc.location.href = redirectTo; + complete(oncomplete, verified); + }, redirectTimeout); + } + else { complete(oncomplete, verified); - }, redirectTimeout); - } - else { - complete(oncomplete, verified); - } - }); + } + }); + } + else { + pageHelpers.showFailure(errors.cannotComplete, info, oncomplete); + } }, function(info) { if (info.network && info.network.status === 401) { tooltip.showTooltip("#cannot_authenticate"); @@ -103,36 +96,30 @@ BrowserID.verifySecondaryAddress = (function() { } function startVerification(oncomplete) { + var self=this; user.tokenInfo(token, function(info) { if (info) { redirectTo = info.returnTo; email = info.email; showRegistrationInfo(info); - needsPassword = info.needs_password; mustAuth = info.must_auth; - - if (needsPassword) { - // This is a fix for legacy users who started the user creation - // process without setting their password in the dialog. If the user - // needs a password, they must set it now. Once all legacy users are - // verified or their links invalidated, this flow can be removed. - dom.addClass("body", "enter_password"); - dom.addClass("body", "enter_verify_password"); - complete(oncomplete, true); - } - else if (mustAuth) { - // These are users who have set their passwords inside of the dialog. + if (mustAuth) { + // These are users who are authenticating in a different browser or + // session than the initiator. dom.addClass("body", "enter_password"); complete(oncomplete, true); } else { - // These are users who do not have to set their passwords at all. + // Easy case where user is in same browser and same session, just + // verify and be done with it all! submit(oncomplete); } } else { - showError("#cannotconfirm"); + // renderError is used directly instead of pageHelpers.showFailure + // because showFailure hides the title in the extended info. + self.renderError("error", errors.cannotConfirm); complete(oncomplete, false); } }, pageHelpers.getFailure(errors.getTokenInfo, oncomplete)); @@ -140,7 +127,8 @@ BrowserID.verifySecondaryAddress = (function() { var Module = bid.Modules.PageModule.extend({ start: function(options) { - this.checkRequired(options, "token", "verifyFunction"); + var self=this; + self.checkRequired(options, "token", "verifyFunction"); token = options.token; verifyFunction = options.verifyFunction; @@ -151,9 +139,9 @@ BrowserID.verifySecondaryAddress = (function() { redirectTimeout = REDIRECT_SECONDS * 1000; } - startVerification(options.ready); + startVerification.call(self, options.ready); - sc.start.call(this, options); + sc.start.call(self, options); }, submit: submit diff --git a/resources/static/shared/error-display.js b/resources/static/shared/error-display.js index c607eac8f9c37b1a0ecaf07af2b7e50a47699338..7777364d2242006aca12f7c98c08d099466c586e 100644 --- a/resources/static/shared/error-display.js +++ b/resources/static/shared/error-display.js @@ -14,7 +14,12 @@ BrowserID.ErrorDisplay = (function() { /** * XXX What a big steaming pile, use CSS animations for this! */ - $("#moreInfo").slideDown(); + $("#moreInfo").slideDown(function() { + // The expanded info may be partially obscured on mobile devices in + // landscape mode. Force the screen size hacks to account for the new + // expanded size. + dom.fireEvent(window, "resize"); + }); $("#openMoreInfo").css({visibility: "hidden"}); } diff --git a/resources/static/shared/error-messages.js b/resources/static/shared/error-messages.js index 2ce74cfde884cc3adb5a4103cd12e30794f5be89..f761aabf63ff14e628987e485123a9a311698476 100644 --- a/resources/static/shared/error-messages.js +++ b/resources/static/shared/error-messages.js @@ -35,6 +35,14 @@ BrowserID.Errors = (function(){ title: "Cancelling User Account" }, + cannotConfirm: { + title: gettext("There was a problem with your signup link. Has this address already been registered?") + }, + + cannotComplete: { + title: gettext("Error encountered trying to complete registration.") + }, + checkAuthentication: { title: "Checking Authentication" }, @@ -48,7 +56,7 @@ BrowserID.Errors = (function(){ }, cookiesDisabled: { - title: gettext("BrowserID requires cookies"), + title: gettext("Persona requires cookies"), message: format(gettext("Please close this window, <a %s>enable cookies</a> and try again"), [" target='_blank' href='" + enableCookiesURL + "'"]) }, diff --git a/resources/static/shared/modules/interaction_data.js b/resources/static/shared/modules/interaction_data.js index f9de4013f329732d62db7fe36bfe7c5423408999..e07e04de42e27fea588e31ad5ddd805040ba694b 100644 --- a/resources/static/shared/modules/interaction_data.js +++ b/resources/static/shared/modules/interaction_data.js @@ -23,6 +23,8 @@ // listen for events via the mediator? BrowserID.Modules.InteractionData = (function() { + "use strict"; + var bid = BrowserID, model = bid.Models.InteractionData, network = bid.Network, @@ -98,10 +100,16 @@ BrowserID.Modules.InteractionData = (function() { return; } + // server_time is sent in milliseconds. The promise to users and data + // safety is the timestamp would be at a 10 minute resolution. Round to the + // nearest 10 minute mark. + var TEN_MINS_IN_MS = 10 * 60 * 1000, + roundedServerTime = Math.round(result.server_time / TEN_MINS_IN_MS) * TEN_MINS_IN_MS; + var currentData = { event_stream: self.initialEventStream, sample_rate: sampleRate, - timestamp: result.server_time, + timestamp: roundedServerTime, local_timestamp: self.startTime.toString(), lang: dom.getAttr('html', 'lang') || null, }; diff --git a/resources/static/shared/modules/page_module.js b/resources/static/shared/modules/page_module.js index 54b570841d4f0b3bb250039939ec26e0f173c1f2..36aa78c7c29a72aae98d2d6552b08100467c54b2 100644 --- a/resources/static/shared/modules/page_module.js +++ b/resources/static/shared/modules/page_module.js @@ -15,16 +15,6 @@ BrowserID.Modules.PageModule = (function() { cancelEvent = helpers.cancelEvent, mediator = bid.Mediator; - function onKeypress(event) { - if (event.which === 13) { - // IE8 does not trigger the submit event when hitting enter. Submit the - // form if the key press was an enter and prevent the default action so - // the form is not submitted twice. - event.preventDefault(); - this.submit(); - } - } - function onSubmit() { if (!dom.hasClass("body", "submit_disabled") && this.validate()) { this.submit(); @@ -69,7 +59,6 @@ BrowserID.Modules.PageModule = (function() { self.options = options || {}; self.bind("form", "submit", cancelEvent(onSubmit)); - self.bind("input", "keypress", onKeypress); }, stop: function() { diff --git a/resources/static/shared/provisioning.js b/resources/static/shared/provisioning.js index 177ad14a68e0b076456d601968817301cfdccdd8..fa91170c2ea3941fa2c59b5b1b53ff748125f2d7 100644 --- a/resources/static/shared/provisioning.js +++ b/resources/static/shared/provisioning.js @@ -72,7 +72,7 @@ BrowserID.Provisioning = (function() { trans.delayReturn(true); jwcrypto.generateKeypair({algorithm: "DS", keysize: BrowserID.KEY_LENGTH}, function(err, kp) { keypair = kp; - trans.complete(keypair.publicKey.toSimpleObject()); + trans.complete(keypair.publicKey.serialize()); }); }); diff --git a/resources/static/shared/screens.js b/resources/static/shared/screens.js index 0ce5cd13392214a46ad7c22c839945c284b854cd..e45daaea63ce87b41b4759c3497852602b78fbe3 100644 --- a/resources/static/shared/screens.js +++ b/resources/static/shared/screens.js @@ -15,11 +15,13 @@ BrowserID.Screens = (function() { show: function(template, vars) { renderer.render(target + " .contents", template, vars); dom.addClass(BODY, className); + dom.fireEvent(window, "resize"); this.visible = true; }, hide: function() { dom.removeClass(BODY, className); + dom.fireEvent(window, "resize"); this.visible = false; } } diff --git a/resources/static/shared/storage.js b/resources/static/shared/storage.js index c4f7b0f741b4b9dbc450396f3c90147f882edd39..b195e760191606371425e2f2f218eb2a6ad0dcc6 100644 --- a/resources/static/shared/storage.js +++ b/resources/static/shared/storage.js @@ -37,23 +37,40 @@ BrowserID.Storage = (function() { if (window.console && console.error) console.error(msg); } - function prepareDeps() { - if (!jwcrypto) { - jwcrypto = require("./jwcrypto"); - } - } - function storeEmails(emails) { storage.emails = JSON.stringify(emails); } function clear() { storage.removeItem("emails"); - storage.removeItem("tempKeypair"); storage.removeItem("siteInfo"); storage.removeItem("managePage"); } + // initialize all localStorage values to default if they are unset. + // this function is only neccesary on IE8 where there are localStorage + // synchronization issues between different browsing contexts, however + // it's intended to avoid IE8 specific bugs from being introduced. + // see issue #1637 + function setDefaultValues() { + _.each({ + emailToUserID: {}, + emails: {}, + interaction_data: {}, + loggedIn: {}, + main_site: {}, + managePage: {}, + returnTo: null, + siteInfo: {}, + stagedOnBehalfOf: null, + usersComputer: {} + }, function(defaultVal, key) { + if (!storage[key]) { + storage[key] = JSON.stringify(defaultVal); + } + }); + } + function getEmails() { try { var emails = JSON.parse(storage.emails || "{}"); @@ -567,6 +584,13 @@ BrowserID.Storage = (function() { */ clear: clear, setReturnTo: setReturnTo, - getReturnTo: getReturnTo + getReturnTo: getReturnTo, + /** + * Set all used storage values to default if they are unset. This function + * is required for proper localStorage sync between different browsing contexts, + * see issue #1637 for full details. + * @method setDefaultValues + */ + setDefaultValues: setDefaultValues }; }()); diff --git a/resources/static/test/cases/controllers/dialog.js b/resources/static/test/cases/controllers/dialog.js index 088e182caa3aa417a81206efc52305fa10c0ae32..1f0f7ffd1ac497d3a62b4922394a24d062f20e94 100644 --- a/resources/static/test/cases/controllers/dialog.js +++ b/resources/static/test/cases/controllers/dialog.js @@ -501,7 +501,26 @@ }); }); - asyncTest("get with absolute path - allowed URL but it must be properly escaped", function() { + asyncTest("get with absolute path and http RP - not allowed", function() { + createController({ + ready: function() { + mediator.subscribe("start", function(msg, info) { + ok(false, "start should not have been called"); + }); + + var siteLogo = '/i/card.png'; + var retval = controller.get(HTTP_TEST_DOMAIN, { + siteLogo: siteLogo + }); + + equal(retval, "only https sites can specify a siteLogo", "expected error"); + testErrorVisible(); + start(); + } + }); + }); + + asyncTest("get with absolute path and https RP - allowed URL but is properly escaped", function() { createController({ ready: function() { var startInfo; @@ -510,12 +529,12 @@ }); var siteLogo = '/i/card.png" onerror="alert(\'xss\')" <script>alert(\'more xss\')</script>'; - var retval = controller.get(HTTP_TEST_DOMAIN, { + var retval = controller.get(HTTPS_TEST_DOMAIN, { siteLogo: siteLogo }); testHelpers.testObjectValuesEqual(startInfo, { - siteLogo: encodeURI(HTTP_TEST_DOMAIN + siteLogo) + siteLogo: encodeURI(HTTPS_TEST_DOMAIN + siteLogo) }); equal(typeof retval, "undefined", "no error expected"); testErrorNotVisible(); diff --git a/resources/static/test/cases/pages/about.js b/resources/static/test/cases/pages/about.js new file mode 100644 index 0000000000000000000000000000000000000000..298642ce39fc66e35c73f04e9a6be107b781ebdd --- /dev/null +++ b/resources/static/test/cases/pages/about.js @@ -0,0 +1,33 @@ +/*jshint browsers:true, forin: true, laxbreak: true */ +/*global test: true, start: 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 bid = BrowserID, + testHelpers = bid.TestHelpers, + controller; + + module("pages/about", { + setup: function() { + testHelpers.setup(); + bid.Renderer.render("#page_head", "site/about", {}); + }, + teardown: function() { + testHelpers.teardown(); + } + }); + + function createController(options, callback) { + controller = BrowserID.about.create(); + controller.start(options); + } + + test("start - no errors", function() { + createController({}); + ok(controller, "controller created"); + }); + +}()); diff --git a/resources/static/test/cases/pages/verify_secondary_address.js b/resources/static/test/cases/pages/verify_secondary_address.js index 0d74afe90af7dc39c853bc3f8860a11adfbac517..ac4ddaa8b832c09fe5a03cd19c8a49a29fdcd37e 100644 --- a/resources/static/test/cases/pages/verify_secondary_address.js +++ b/resources/static/test/cases/pages/verify_secondary_address.js @@ -13,6 +13,7 @@ dom = bid.DOM, testHelpers = bid.TestHelpers, testHasClass = testHelpers.testHasClass, + testVisible = testHelpers.testVisible, validToken = true, controller, config = { @@ -56,7 +57,7 @@ } function testCannotConfirm() { - ok($("#cannotconfirm").is(":visible"), "cannot confirm box is visible"); + testHelpers.testErrorVisible(); } test("start with missing token", function() { @@ -70,22 +71,21 @@ equal(error, "missing config option: token", "correct error thrown"); }); - asyncTest("no password: start with good token and site", function() { + asyncTest("valid token, no password necessary - verify user and show site info", function() { var returnTo = "https://test.domain/path"; storage.setReturnTo(returnTo); createController(config, function() { - testEmail(); - ok($(".siteinfo").is(":visible"), "siteinfo is visible when we say what it is"); - equal($(".website:nth(0)").text(), returnTo, "website is updated"); + testVisible("#congrats"); testHasClass("body", "complete"); + equal($(".website").text(), returnTo, "website is updated"); equal(doc.location.href, returnTo, "redirection occurred to correct URL"); equal(storage.getLoggedIn("https://test.domain"), "testuser@testuser.com", "logged in status set"); start(); }); }); - asyncTest("no password: start with good token and nosite", function() { + asyncTest("valid token, no password necessary, no saved site info - verify user but do not show site info", function() { createController(config, function() { testEmail(); equal($(".siteinfo").is(":visible"), false, "siteinfo is not visible without having it"); @@ -94,7 +94,7 @@ }); }); - asyncTest("no password: start with bad token", function() { + asyncTest("invalid token - show cannot confirm error", function() { xhr.useResult("invalid"); createController(config, function() { @@ -103,7 +103,7 @@ }); }); - asyncTest("no password: start with emailForVerficationToken XHR failure", function() { + asyncTest("valid token with xhr error - show error screen", function() { xhr.useResult("ajaxError"); createController(config, function() { testHelpers.testErrorVisible(); @@ -156,88 +156,4 @@ }); }); - asyncTest("must set password, successful login", function() { - xhr.useResult("needsPassword"); - createController(config, function() { - xhr.useResult("valid"); - - $("#password").val("password"); - $("#vpassword").val("password"); - - testHasClass("body", "enter_password"); - testHasClass("body", "enter_verify_password"); - - controller.submit(function(status) { - equal(status, true, "correct status"); - testHasClass("body", "complete"); - start(); - }); - }); - }); - - asyncTest("must set password, too short a password", function() { - xhr.useResult("needsPassword"); - createController(config, function() { - xhr.useResult("valid"); - - $("#password").val("pass"); - $("#vpassword").val("pass"); - - controller.submit(function(status) { - equal(status, false, "correct status"); - testHelpers.testTooltipVisible(); - start(); - }); - }); - }); - - asyncTest("must set password, too long a password", function() { - xhr.useResult("needsPassword"); - createController(config, function() { - xhr.useResult("valid"); - - var pass = testHelpers.generateString(81); - $("#password").val(pass); - $("#vpassword").val(pass); - - controller.submit(function(status) { - equal(status, false, "correct status"); - testHelpers.testTooltipVisible(); - start(); - }); - }); - }); - - asyncTest("must set password, missing verification password", function() { - xhr.useResult("needsPassword"); - createController(config, function() { - xhr.useResult("valid"); - - $("#password").val("password"); - $("#vpassword").val(""); - - controller.submit(function(status) { - equal(status, false, "correct status"); - testHelpers.testTooltipVisible(); - start(); - }); - }); - }); - - asyncTest("must set password, mismatched passwords", function() { - xhr.useResult("needsPassword"); - createController(config, function() { - xhr.useResult("valid"); - - $("#password").val("password"); - $("#vpassword").val("password1"); - - controller.submit(function(status) { - equal(status, false, "correct status"); - testHelpers.testTooltipVisible(); - start(); - }); - }); - }); - }()); diff --git a/resources/static/test/cases/shared/modules/interaction_data.js b/resources/static/test/cases/shared/modules/interaction_data.js index f2b6b675d0ae32888178c1e63639f8f270c3e7dd..35940a19c9bd04e043467b3a9ea3d1df6a5c8e2b 100644 --- a/resources/static/test/cases/shared/modules/interaction_data.js +++ b/resources/static/test/cases/shared/modules/interaction_data.js @@ -235,4 +235,15 @@ }); }); + asyncTest("timestamp rounded to 10 minute intervals", function() { + var TEN_MINS_IN_MS = 10 * 60 * 1000; + createController(); + network.withContext(function() { + var timestamp = controller.getCurrent().timestamp; + ok(timestamp, "a timestamp has been passed: " + timestamp); + equal(timestamp % TEN_MINS_IN_MS, 0, "timestamp has been rounded to a 10 minute interval"); + start(); + }); + }); + }()); diff --git a/resources/static/test/cases/shared/modules/page_module.js b/resources/static/test/cases/shared/modules/page_module.js index 0544d20f0e97f9dd9e4bce4a2e6fdc2e6350ba32..ebd90fdba6b1f573a564ed409aca4196a781926a 100644 --- a/resources/static/test/cases/shared/modules/page_module.js +++ b/resources/static/test/cases/shared/modules/page_module.js @@ -200,33 +200,5 @@ equal(submitCalled, true, "submit permitted to complete"); }); - test("form is submitted once 'enter keypress' event", function() { - createController(); - controller.renderDialog("test_template_with_input", { - title: "Test title", - message: "Test message" - }); - - controller.start(); - - - var submitCalled = 0; - controller.submit = function() { - submitCalled++; - }; - - // synthesize the entire series of key* events so we replicate the behavior - // of keyboard interaction. - var e = jQuery.Event("keydown", { keyCode: 13, which: 13 }); - $("#templateInput").trigger(e); - - var e = jQuery.Event("keyup", { keyCode: 13, which: 13 }); - $("#templateInput").trigger(e); - - var e = jQuery.Event("keypress", { keyCode: 13, which: 13 }); - $("#templateInput").trigger(e); - - equal(submitCalled, 1, "submit called a single time"); - }); }()); diff --git a/resources/static/test/testHelpers/helpers.js b/resources/static/test/testHelpers/helpers.js index 0a7130ef5fc565d31ee50761f104cad7d28043d2..ed90066d39a1fdf4438117acbea6bcf7ce7f2823 100644 --- a/resources/static/test/testHelpers/helpers.js +++ b/resources/static/test/testHelpers/helpers.js @@ -221,8 +221,7 @@ BrowserID.TestHelpers = (function() { }, testHasClass: function(selector, className, msg) { - ok($(selector).hasClass(className), - selector + " has className " + className + " - " + msg); + ok($(selector).hasClass(className), msg || selector + " has className: " + className); }, testUndefined: function(toTest, msg) { @@ -231,6 +230,10 @@ BrowserID.TestHelpers = (function() { testNotUndefined: function(toTest, msg) { notEqual(typeof toTest, "undefined", msg || "object is defined"); + }, + + testVisible: function(selector, msg) { + ok($(selector).is(":visible"), msg || selector + " should be visible"); } }; diff --git a/resources/views/about.ejs b/resources/views/about.ejs index a7eb8dea4e292cea91e2080126587a176305d933..dfdeba036ae1e32c4470c9530e9b96b6a31d2792 100644 --- a/resources/views/about.ejs +++ b/resources/views/about.ejs @@ -3,38 +3,46 @@ - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> <div id="content" class="display_always"> - <div id="about"> - - <h1 class="center"> - <strong>Persona</strong> is an <strong>easier</strong> way to sign in - </h1> - - - <div class="row one cf"> - <img src="/i/tutorial_1.png"> - <div> - <div class="steps"></div> On your favorite website that supports Persona, Click the ‘Sign In’ button - </div> - </div> - - <div class="row two cf"> - <img src="/i/tutorial_2.png"> - <div> - <div class="steps"></div> Select your preferred Email - </div> - - </div> - - <div class="row three cf"> - <img src="/i/tutorial_3.png"> - <div> - <div class="steps"></div> No passwords necessary, you're done! - </div> - </div> - - <div class="row cf center"> - If you need some help with Persona, head over to our <a href="https://support.mozilla.com/en-US/kb/what-browserid-and-how-does-it-work" target="_blank">support center</a>. - </div> - </div> + <div class="about"> + <section class="simple-signon"> + <h2 class="title">Simplified sign-on.</h2> + <article class="blurb"> + <div class="info first"> + <h1>Persona replaces multiple passwords</h1> + <p>Sites such as <a href="http://crossword.thetimes.co.uk/">The Times Crossword</a>, <a href="http://openphoto.net/">OpenPhoto</a> and <a href="https://www.voo.st/">Voost</a> use Persona instead of usernames to sign you in.</p><p>This means you only need one password to sign in to many sites.</p> + </div> + + <div class="graphic"> + <img src="i/one-password-graphic.png" alt="One password to rule them all."> + </div> + </article> + + <article class="blurb flexible"> + <div class="graphic first"> + <img src="i/flexible-graphic.png" alt="Use multiple email addresses"> + </div> + + <div class="info"> + <h1>Persona is flexible</h1> + <p>Within Persona, your identity is your email address. You can use as many email addresses as you want, but you still only need one password.</p> + </div> + </article> + </section> + + <section class="privacy"> + <h2 class="title">Real privacy.</h2> + + <article class="blurb half first" style="min-height: 195px; "> + <h1>Persona is proudly non-profit for you</h1> + <p>Persona is developed by Mozilla, a not-for-profit company trusted throughout the Web community. Our goal is to create technologies that balance an open Web platform with people’s privacy.</p> + </article> + <article class="blurb half"> + <h1>Persona preserves your privacy</h1> + <p>Persona does not track your activity around the Web. It creates a wall between signing you in and what you do once you’re there. The history of what sites you visit is stored only on your own computer.</p> + </article> + </section> + + <a href="https://developer.mozilla.org/en/BrowserID/Quick_Setup" class="developers"><img src="i/developers-link.png" alt="Persona for developers"><span>Implement Persona on your site </span>Developer guides and API documentation</a> + </div><!-- #dashboard --> </div> diff --git a/resources/views/add_email_address.ejs b/resources/views/add_email_address.ejs index 9a4128aeee67fac5702ae45de569cd952d41fe4a..f56a6caf70bff61116465caf50941f13d20b2bec 100644 --- a/resources/views/add_email_address.ejs +++ b/resources/views/add_email_address.ejs @@ -2,15 +2,12 @@ - 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/. --> -<div id="hAlign" class="display_always"> +<div id="hAlign"> <div id="vAlign"> - <ul class="notifications"> - <li class="notification error" id="cannotconfirm"><%= gettext('Error encountered while attempting to confirm your address. Have you previously verified this address?') %></li> - <li class="notification error" id="cannotcomplete"><%= gettext('Error encountered trying to complete registration.') %></li> - </ul> - - <form id="signUpForm" class="cf"> - <p class="hint siteinfo"><%= gettext('Finish signing into:') %> <strong><span class="website"></span></strong></p> + <form id="signUpForm" class="cf password_entry"> + <p class="hint siteinfo"> + <%= gettext('Finish signing into:') %> <strong class="website"></strong> + </p> <h1><%= gettext('Email Verification') %></h1> @@ -19,7 +16,8 @@ <label for="email"><%= gettext('Email Address') %></label> <input class="youraddress" id="email" placeholder="<%= gettext('Your Email') %>" type="email" value="" disabled="disabled" maxlength="254" /> </li> - <li class="password_entry"> + + <li> <label for="password"><%= gettext('Password') %></label> <input id="password" placeholder="<%= gettext('Your Password') %>" type="password" autofocus maxlength=80 /> @@ -27,35 +25,16 @@ <%= gettext('Password is required.') %> </div> - <div class="tooltip" id="password_length" for="password"> - <%= gettext('Password must be between 8 and 80 characters long.') %> - </div> - <div id="cannot_authenticate" class="tooltip" for="password"> <%= gettext('The account cannot be verified with this username and password.') %> </div> </li> - - <li class="password_entry" id="verify_password"> - <label for="vpassword"><%= gettext('Verify Password') %></label> - <input id="vpassword" placeholder="<%= gettext('Repeat Password') %>" type="password" maxlength="80"> - - <div id="vpassword_required" class="tooltip" for="vpassword"> - <%= gettext('Verification password is required.') %> - </div> - - <div class="tooltip" id="passwords_no_match" for="vpassword"> - <%= gettext ('Passwords do not match.') %> - </div> - - </li> </ul> <div class="submit cf password_entry"> <button><%= gettext('finish') %></button> </div> - </form> <div id="congrats"> diff --git a/resources/views/dialog_layout.ejs b/resources/views/dialog_layout.ejs index 6ca9a9ade30ed647c0d717a819ed21a21009c58c..4b12722611e3681ead1acbc0eb7e202eff51bad5 100644 --- a/resources/views/dialog_layout.ejs +++ b/resources/views/dialog_layout.ejs @@ -20,7 +20,9 @@ </head> <body class="waiting"> <header id="header"> - <a href="#" id="showDevelopment"> </a> + <% if (useJavascript !== false && enable_development_menu) { %> + <a href="#" id="showDevelopment"> </a> + <% } %> <h1><a class="home" target="_blank" href="/">Mozilla Persona Home</a></h1> </header> diff --git a/resources/views/layout.ejs b/resources/views/layout.ejs index 91e873f19e3636e98ae79402aad503636d46afcd..d01aa64f819fb1fa741e229cd64231fe48afc183 100644 --- a/resources/views/layout.ejs +++ b/resources/views/layout.ejs @@ -18,7 +18,9 @@ <title><%= format(gettext("Mozilla Persona: %s"), [title]) %></title> </head> <body class="loading"> -<a href="#" id="showDevelopment"> </a> +<% if (enable_development_menu) { %> + <a href="#" id="showDevelopment"> </a> +<% } %> <div id="errorBackground"></div> <div id="wrapper"> diff --git a/resources/views/test.ejs b/resources/views/test.ejs index 7f05abc2301be6417ad5444f2932d358047a7d19..da1332fe11b80a0fe7d2547c457495081ea4b377 100644 --- a/resources/views/test.ejs +++ b/resources/views/test.ejs @@ -143,6 +143,7 @@ <script src="/pages/manage_account.js"></script> <script src="/pages/signin.js"></script> <script src="/pages/signup.js"></script> + <script src="/pages/about.js"></script> <script src="testHelpers/helpers.js"></script> @@ -179,6 +180,7 @@ <script src="cases/pages/signin.js"></script> <script src="cases/pages/signup.js"></script> <script src="cases/pages/manage_account.js"></script> + <script src="cases/pages/about.js"></script> <script src="cases/resources/internal_api.js"></script> <script src="cases/resources/helpers.js"></script> diff --git a/resources/views/verify_email_address.ejs b/resources/views/verify_email_address.ejs index d033b9417e2b39158d875138bb6ec24aca1a0286..118c70df793807bfb6586e04eb955624061bbe5a 100644 --- a/resources/views/verify_email_address.ejs +++ b/resources/views/verify_email_address.ejs @@ -2,15 +2,13 @@ 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/. */ %> -<div id="hAlign" class="display_always"> +<div id="hAlign"> <div id="vAlign"> - <ul class="notifications"> - <li class="notification error" id="cannotconfirm"><%= gettext('There was a problem with your signup link. Has this address already been registered?') %></li> - <li class="notification error" id="cannotcomplete"><%= gettext('Error encountered trying to complete registration.') %></li> - </ul> + <form id="signUpForm" class="cf password_entry"> + <p class="hint siteinfo"> + <%= gettext('Finish signing into:') %> <strong class="website"></strong> + </p> - <form id="signUpForm" class="cf"> - <p class="hint siteinfo"><%= gettext('Finish signing into:') %> <strong><span class="website"></span></strong></p> <h1><%= gettext('Last step!') %></h1> <ul class="inputs"> @@ -19,7 +17,7 @@ <input class="youraddress" id="email" placeholder="<%= gettext('Your Email') %>" type="email" value="" disabled="disabled" maxlength="254" /> </li> - <li class="password_entry"> + <li> <label for="password"><%= gettext('Password') %></label> <input id="password" placeholder="<%= gettext('Your Password') %>" type="password" autofocus maxlength=80 /> @@ -27,28 +25,10 @@ <%= gettext('Password is required.') %> </div> - <div class="tooltip" id="password_length" for="password"> - <%= gettext('Password must be between 8 and 80 characters long.') %> - </div> - <div id="cannot_authenticate" class="tooltip" for="password"> <%= gettext('The account cannot be verified with this username and password.') %> </div> </li> - - <li class="password_entry" id="verify_password"> - <label for="vpassword"><%= gettext('Verify Password') %></label> - <input id="vpassword" placeholder="<%= gettext('Repeat Password') %>" type="password" maxlength="80"> - - <div id="vpassword_required" class="tooltip" for="vpassword"> - <%= gettext('Verification password is required.') %> - </div> - - <div class="tooltip" id="passwords_no_match" for="vpassword"> - <%= gettext ('Passwords do not match.') %> - </div> - - </li> </ul> <div class="submit cf password_entry"> diff --git a/scripts/browserid.spec b/scripts/browserid.spec index 5429472cb4112474e621c0da403e096cd0e73664..6bb044ea89e314de69c2ad4b6b31dcbdb7bfdcd2 100644 --- a/scripts/browserid.spec +++ b/scripts/browserid.spec @@ -1,7 +1,7 @@ %define _rootdir /opt/browserid Name: browserid-server -Version: 0.2012.06.22 +Version: 0.2012.07.06 Release: 1%{?dist}_%{svnrev} Summary: BrowserID server Packager: Pete Fritchman <petef@mozilla.com> diff --git a/scripts/run_locally.js b/scripts/run_locally.js index 2a0b9bca58e6d832abee7774b5fa4f1696f58f9a..346fa7dda7dff3152af234e9f9fef5261e0d01fd 100755 --- a/scripts/run_locally.js +++ b/scripts/run_locally.js @@ -112,11 +112,19 @@ function runDaemon(daemon, cb) { }); }; +// start all daemons except the router in parallel var daemonNames = Object.keys(daemonsToRun); -function runNextDaemon() { - if (daemonNames.length) runDaemon(daemonNames.shift(), runNextDaemon); -} -runNextDaemon(); +daemonNames.splice(daemonNames.indexOf('router'), 1); + +var numDaemonsRun = 0; +daemonNames.forEach(function(dn) { + runDaemon(dn, function() { + if (++numDaemonsRun === daemonNames.length) { + // after all daemons are up and running, start the router + runDaemon('router', function() { }); + } + }); +}); process.on('SIGINT', function () { console.log('\nSIGINT recieved! trying to shut down gracefully...'); diff --git a/tests/conformance-test.js b/tests/conformance-test.js old mode 100644 new mode 100755 diff --git a/tests/data/lib.jshintrc b/tests/data/lib.jshintrc new file mode 100644 index 0000000000000000000000000000000000000000..fa8c0257f97f0e09d30c121ae4930ac6bad0394d --- /dev/null +++ b/tests/data/lib.jshintrc @@ -0,0 +1,8 @@ +{ + "undef": true, + "node": true, + "es5": true, + "esnext": true, + "strict": false, + "sub": true +} diff --git a/tests/heartbeat-test.js b/tests/heartbeat-test.js new file mode 100755 index 0000000000000000000000000000000000000000..815f97120003b062da83fe9d898acc0fc16ff471 --- /dev/null +++ b/tests/heartbeat-test.js @@ -0,0 +1,122 @@ +#!/usr/bin/env node + +/* 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/. */ + +require('./lib/test_env.js'); + +const assert = +require('assert'), +vows = require('vows'), +start_stop = require('./lib/start-stop.js'), +wsapi = require('./lib/wsapi.js'), +db = require('../lib/db.js'), +config = require('../lib/configuration.js'), +bcrypt = require('bcrypt'), +http = require('http'); + +var suite = vows.describe('heartbeat'); + +// disable vows (often flakey?) async error behavior +suite.options.error = false; + +start_stop.addStartupBatches(suite); + +// test deep and shallow heartbeats work for all processes +[ 10004, 10002, 10003, 10004, 10007 ].forEach(function(port) { + [ true, false ].forEach(function(shallow) { + var testName = "shallow heartbeat check for 127.0.0.1:" + port; + suite.addBatch({ + testName: { + topic: function() { + var self = this; + + var req = http.get({ + host: '127.0.0.1', + port: port, + path: '/__heartbeat__' + ( shallow ? "" : "?deep=true") + }, function(res) { + self.callback(null, res.statusCode); + req.abort(); + }).on('error', function(e) { + self.callback(e, null); + req.abort(); + }); + }, + "works": function(err, code) { + assert.strictEqual(err, null); + assert.equal(code, 200); + } + } + }); + }); +}); + +// now let's SIGSTOP the browserid process and verify that the router's +// deep heartbeat fails within 11s +suite.addBatch({ + "stopping the browserid process": { + topic: function() { + process.kill(parseInt(process.env['BROWSERID_PID'], 10), 'SIGSTOP'); + this.callback(); + }, + "then doing a deep __heartbeat__ on router": { + topic: function() { + var self = this; + var start = new Date(); + var req = http.get({ + host: '127.0.0.1', + port: 10002, + path: '/__heartbeat__?deep=true' + }, function(res) { + self.callback(null, res.statusCode, start); + req.abort(); + }).on('error', function(e) { + self.callback(e, null); + req.abort(); + }); + }, + "fails": function(e, code, start) { + assert.ok(!e); + assert.strictEqual(500, code); + }, + "takes about 5s": function(e, code, start) { + assert.ok(!e); + var elapsedMS = new Date() - start; + assert.ok(3000 < elapsedMS < 7000); + }, + "but upon SIGCONT": { + topic: function(e, code) { + process.kill(parseInt(process.env['BROWSERID_PID'], 10), 'SIGCONT'); + this.callback(); + }, + "a deep heartbeat": { + topic: function() { + var self = this; + var req = http.get( + { host: '127.0.0.1', port: 10002, path: '/__heartbeat__?deep=true'}, + function(res) { + self.callback(null, res.statusCode); + req.abort(); + }).on('error', function(e) { + self.callback(e, null); + req.abort(); + }); + }, + "works": function(err, code) { + assert.ok(!err); + assert.strictEqual(200, code); + } + } + } + } + } +}); + + +start_stop.addShutdownBatches(suite); + +// run or export the suite. +if (process.argv[1] === __filename) suite.run(); +else suite.export(module); diff --git a/tests/jshint-test.js b/tests/jshint-test.js new file mode 100755 index 0000000000000000000000000000000000000000..e0971e0887ef6a66ae046cb7ed413554eb4e98c0 --- /dev/null +++ b/tests/jshint-test.js @@ -0,0 +1,46 @@ +#!/usr/bin/env node + +/* 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/. */ + +require('./lib/test_env.js'); + +// add lib/ to the require path + +const +assert = require('assert'), +vows = require('vows'), +fs = require('fs'), +path = require('path'), +exec = require('child_process').exec; + +var suite = vows.describe('jshint'); +var jshintPath = '../node_modules/jshint/bin/hint'; + +// disable vows (often flakey?) async error behavior +suite.options.error = false; + +suite.addBatch({ + "run jshint on the lib directory": { + topic: function () { + var cmd = jshintPath + ' --config ./data/lib.jshintrc ../lib/ | grep "not defined"'; + var child = exec(cmd, {cwd: path.resolve(__dirname)}, this.callback); + }, + "jshint is found and runs" : function (error, stdout, stderr) { + // NOTE: until we clean up jshint errors and agree on what options, + // we only verify that the program was found and runs, but not that + // it is completely clean and error free in jshint's opinion. + assert.ok(!error || error.toString().indexOf('No such') === -1); + }, + "no globals are created or referenced" : function (error, stdout, stderr) { + var errors = stdout.split("\n").length - 1; + assert.strictEqual(errors, 0); + } + } +}); + + +// run or export the suite. +if (process.argv[1] === __filename) suite.run(); +else suite.export(module); diff --git a/tests/lib/start-stop.js b/tests/lib/start-stop.js index b73aa2ea8f4951a1c615788e750259ab93bd7ece..742fb032745d0607052ff7b19de83157dff95e8a 100644 --- a/tests/lib/start-stop.js +++ b/tests/lib/start-stop.js @@ -46,10 +46,13 @@ function setupProc(proc) { } } var tokenRegex = new RegExp('token=([A-Za-z0-9]+)$', 'm'); + var pidRegex = new RegExp('^spawned (\\w+) \\(.*\\) with pid ([0-9]+)$'); if (!sentReady && /^router.*127\.0\.0\.1:10002$/.test(x)) { exports.browserid.emit('ready'); sentReady = true; + } else if (!sentReady && (m = pidRegex.exec(x))) { + process.env[m[1].toUpperCase() + "_PID"] = m[2]; } else if (m = tokenRegex.exec(x)) { if (!(/forwarding request:/.test(x))) { tokenStack.push(m[1]); diff --git a/tests/simple-stage-user-utf8-password.js b/tests/simple-stage-user-utf8-password.js old mode 100644 new mode 100755