diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000000000000000000000000000000000..7983595abb83961fe7e6694b753357d9cd9c929c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,20 @@ +language: node_js + +before_install: + - sudo apt-get install libgmp3-dev + - "mysql -e 'create database browserid;'" + +node_js: + - 0.6 + +notifications: + irc: "irc.mozilla.org#identity" + +env: + - MYSQL_USER=root + +mysql: + adapter: mysql2 + username: root + encoding: utf8 + database: browserid diff --git a/ChangeLog b/ChangeLog index d9626bc253858a3e334ff28c076a554015a4fd3a..245d20964c8af2516c948e2778d5894fd6f94842 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,6 +1,24 @@ -train-2011.02.08 (in progress): +train-2012.02.29 (in progress): -train-2011.02.02: +train-2012.02.16: + * improve failure mode when cookies are disabled (especially on iOS): #1056 + * serve static css/js resources from perma URLs to improve load times - #620 + * improve UI flows concerning cancelation during primary sign in: #983 #1036 + * localization improvements: #1040, #1045, #1048, #1062, #1081, #1113 + * cosmetic dialog fixes: #1062, #1058, #892, #1117 + * fix bug preventing email addresses with under-bars in hostnames: #1074 + * Mobile specific cosmetic improvements: #1072 + * don't localize developer targeted error strings: #1051 + * remove obsolete code: #1082 + * sort email addresses alphabetically in dialog picker: #130 + * improve error messages: #835, #1056 + * improve log messages: #1069 + * wsapi semantic improvements: #1083, #835 + * logging in with a primary email address no longer forces you to re-enter your password when subsequently using a secondary address: #1049 + * Fix IE specific issue where cookies with same name on domain and subdomain would collide: #296 + * long emails look better: #1100 + +train-2012.02.02: * i18n support, now BrowserID speaks your language: #926, #936, #977, #1013, #1031 * improved error screens on slow server responses: #913, #915 * better cache headers on all html resources (which Vary by Accept-Languages): #226, #620, #920, #938 @@ -21,8 +39,10 @@ train-2011.02.02: * (hotfix 2012.02.07) Fix the missing email address in the "check your email" screen for the forgot password flow. #1058 * (hotfix 2012.02.07) Modify build process to pick up locales from a .json file * (hotfix 2012.02.07) fix production-locales.sh script to defer to the environment for configuration + * (hotfix 2012.02.13) fix for IE users not seeing error screens sometimes: #1087 + * (hotfix 2012.02.22) add banner announcing brand change -train-2011.01.18: +train-2012.01.18: * support for 3rd party primary identity providers: #761, #904, #865 * loadgen improvements * Re-license under MPL2: #859 (& #827) @@ -45,7 +65,7 @@ train-2011.01.18: * (hotfix 2012.01.31) fix silent assertions: #972 * (hotfix 2012.02.01) fix verification of email on a browser other than the initiator: #973, #1026 (and maybe others) -train-2011.01.05: +train-2012.01.05: * client entropy pool mixes in randomness from server for better browser RNG: #298, #800 * new assertion format that avoids double (base64) encoding - 33% smaller: #507 * Turn license URL in ToS into a clickable link: #382 diff --git a/README.md b/README.md index 16c9a2836fb4fed60c9e9af913b25acad527124f..b683b966a0ae82417b1918ba28438eabbbb334c4 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Unit tests can be run by invoking `npm test` at the top level, and you should run them often. Like before committing code. To fully test the code you should install mysql and have a well permissions `test` user (can create and drop databases). If you don't have mysql installed, -code testing is still possible (it just uses a little json database). +code testing is still possible (it just uses a little JSON database). ## Development model diff --git a/bin/browserid b/bin/browserid index cf199e123b0bbbc93ae1a04370d4e0cc9e504878..5802ba36d9abc4a58aca7e50b1b04f04ba02047c 100755 --- a/bin/browserid +++ b/bin/browserid @@ -13,6 +13,8 @@ urlparse = require('urlparse'), express = require('express'); const +assets = require('../lib/static_resources').all, +cachify = require('connect-cachify'), i18n = require('../lib/i18n.js'), wsapi = require('../lib/wsapi.js'), httputils = require('../lib/httputils.js'), @@ -131,6 +133,16 @@ app.use(function(req, resp, next) { return next(); }); +var static_root = path.join(__dirname, "..", "resources", "static"); + +app.use(cachify.setup(assets(config.get('supported_languages')), + { + prefix: 'v', + production: config.get('use_minified_resources'), + root: static_root, + })); + + // #7 - perform response substitution to support local/dev/beta environments // (specifically, this replaces URLs in responses, e.g. https://browserid.org // with https://diresworb.org) @@ -168,14 +180,7 @@ app.use(function(req, res, next) { next(); }); -app.use(express.static(path.join(__dirname, "..", "resources", "static"))); - -// custom 404 page -app.use(function(req, res,next) { - res.statusCode = 404; - res.write("Cannot find this resource"); - res.end(); -}); +app.use(express.static(static_root)); // open the databse db.open(config.get('database'), function (error) { diff --git a/bin/verifier b/bin/verifier index 50210785b86e0f0db18e9fa0b9e7829c4f6042bb..d19cfc2cd096e4fa9b39c9245d2bb48376bd87ac 100755 --- a/bin/verifier +++ b/bin/verifier @@ -129,13 +129,6 @@ app.post('/verify', function(req, resp, next) { // shutdown when /code_update is invoked shutdown.installUpdateHandler(app); -// custom 404 -app.use(function(req, res,next) { - res.statusCode = 404; - res.write("Cannot find this resource"); - res.end(); -}); - // shutdown nicely on signals shutdown.handleTerminationSignals(app, function() { cc.exit(); diff --git a/config/l10n-all.json b/config/l10n-all.json index 920a42d54c46543059940cdb4f520111cc631bf2..3f78aa2cc1e09ae9e2d64cc4894cf9d533ba661a 100644 --- a/config/l10n-all.json +++ b/config/l10n-all.json @@ -1,9 +1,10 @@ { - "supported_languages": [ - "af", "ca", "cs", "da", "de", "el", "en-US", "eo", "es", "es-MX", "et", "eu", - "fi", "fr", "fy", "ga", "gd", "gl", "hr", "it", "ja", "lij", "lt", - "ml", "nl", "pl", "pt", "pt-BR", "rm", "ro", "ru", "sk", "sl", "son", - "sq", "sr", "tr", "zh-CN", "zh-TW", +"supported_languages": [ + "af", "ca", "cs", "da", "de", "el", "en-US", "eo", "es", "es-MX", + "et", "eu", "fi", "fr", "fy", "ga", "gd", "gl", "he", "hr", "hu", + "it", "ja", "lij", "lt", "ml", "nl", "pa", "pl", "pt", "pt-BR", + "rm", "ro", "ru", "sk", "sl", "son", "sq", "sr", "sv", "tr", "uk", + "zh-CN", "zh-TW", "it-CH", "db-LB" ] } \ No newline at end of file diff --git a/docs/AWS_DEPLOYMENT.md b/docs/AWS_DEPLOYMENT.md index 4071567e366499de26596de5dcd71543e448055e..8bcced1d0be07d65c85c2e7bf9068a693b25d26c 100644 --- a/docs/AWS_DEPLOYMENT.md +++ b/docs/AWS_DEPLOYMENT.md @@ -19,8 +19,11 @@ Once you have these things, you'll need to relay them to deployment scripts via your environment. you might put something like this in your .bashrc: + # This is your Access Key ID from your AWS Security Credentials export AWS_ID=<your id> + # This is your Secret Access Key from your AWS Security Credentials export AWS_SECRET=<your secret> + # This is a magic credential you get from lloyd export BROWSERID_DEPLOY_DNS_KEY=98...33 ## test! diff --git a/example/rp/TOS.html b/example/rp/TOS.html new file mode 100644 index 0000000000000000000000000000000000000000..c61b94818cad466262fff27ad5492dcd150c1bd3 --- /dev/null +++ b/example/rp/TOS.html @@ -0,0 +1,5 @@ +<html> +<body> +This is my ToS... I pour out. +</body> +</html> diff --git a/example/rp/index.html b/example/rp/index.html index 7c9492c92dcfe92a20afad8f53219d9ee3695012..d14fa1ad693c3b72b16dd057c7b6a1da0e5eaeb2 100644 --- a/example/rp/index.html +++ b/example/rp/index.html @@ -35,6 +35,9 @@ pre { word-wrap: break-word; } +.specify ul { padding-left: 0px; } +.specify li { list-style: none; } + @media screen and (max-width: 640px) { .intro, .output, .step { width: 90%; @@ -54,13 +57,26 @@ pre { </div> <div class="specify"> - What flavor of assertion would you like? <br/> - <p> - <input type="checkbox" id="silent"> Silent <br/> - <input type="checkbox" id="allowPersistent"> Allow persistent sign-in <br/> - <input type="text" id="requiredEmail" width="80"> Require a specific email <br/> + <p>What flavor of assertion would you like?</p> + <ul> + <li> + <input type="checkbox" id="silent"> + <label for="silent">Silent</label> + </li><li> + <input type="checkbox" id="allowPersistent"> + <label for="allowPersistent">Allow persistent sign-in</label> + </li><li> + <input type="checkbox" id="privacy"> + <label for="privacy">Supply a privacy policy</label> + </li><li> + <input type="checkbox" id="tos"> + <label for="tos">Supply a ToS</label> + </li><li> + <input type="text" id="requiredEmail" width="80"> + <label for="requiredEmail">Require a specific email</label><br /> + </li> + </ul> <button>Get an assertion</button> - </p> </div> <div class="verifierResp"> @@ -113,6 +129,8 @@ $(document).ready(function() { }, { silent: $('#silent').attr('checked'), allowPersistent: $('#allowPersistent').attr('checked'), + privacyURL: $('#privacy').attr('checked') ? "/privacy.html" : undefined, + tosURL: $('#tos').attr('checked') ? "/TOS.html" : undefined, requiredEmail: requiredEmail }); }); diff --git a/example/rp/privacy.html b/example/rp/privacy.html new file mode 100644 index 0000000000000000000000000000000000000000..7fe9a3994759da46f77e48b49402438efcfb7cc0 --- /dev/null +++ b/example/rp/privacy.html @@ -0,0 +1,5 @@ +<html> +<body> +This is my privacy policy. When you tip me over... +</body> +</html> diff --git a/lib/browserid/fake_verification.js b/lib/browserid/fake_verification.js index 9d1df8bb6ce20a8a76ac5f841d3fecd4db05cfb1..03f33f1c15af0fbeb0f68cd43ce6f0f3e782bed1 100644 --- a/lib/browserid/fake_verification.js +++ b/lib/browserid/fake_verification.js @@ -12,7 +12,8 @@ const configuration = require('../configuration.js'), url = require('url'), db = require('../db.js'); -logger = require('../logging.js').logger; +logger = require('../logging.js').logger, +wsapi = require('../wsapi'); logger.warn("HEAR YE: Fake verfication enabled, aceess via /wsapi/fake_verification?email=foo@bar.com"); logger.warn("THIS IS NEVER OK IN A PRODUCTION ENVIRONMENT"); @@ -20,7 +21,8 @@ logger.warn("THIS IS NEVER OK IN A PRODUCTION ENVIRONMENT"); exports.addVerificationWSAPI = function(app) { app.get('/wsapi/fake_verification', function(req, res) { var email = url.parse(req.url, true).query['email']; - db.verificationSecretForEmail(email, function(secret) { + db.verificationSecretForEmail(email, function(err, secret) { + if (err) return wsapi.databaseDown(resp, err); if (secret) res.write(secret); else res.writeHead(400, {"Content-Type": "text/plain"}); res.end(); diff --git a/lib/browserid/views.js b/lib/browserid/views.js index d87269bd1bb467ce5ee0f20174bf28cd17569286..0ca111100a1211e636c1e17cda6a818486223e0b 100644 --- a/lib/browserid/views.js +++ b/lib/browserid/views.js @@ -9,7 +9,9 @@ logger = require('../logging.js').logger, fs = require('fs'), connect = require('connect'), config = require('../configuration.js'), -util = require('util'); +und = require('underscore'), +util = require('util'), +httputils = require('../httputils.js'); // all templated content, redirects, and renames are handled here. // anything that is not an api, and not static @@ -90,6 +92,10 @@ exports.setup = function(app) { renderCachableView(req, res, 'unsupported_dialog.ejs', {layout: 'dialog_layout.ejs', useJavascript: false}); }); + app.get("/cookies_disabled", function(req,res) { + renderCachableView(req, res, 'cookies_disabled.ejs', {layout: 'dialog_layout.ejs', useJavascript: false}); + }); + // Used for a relay page for communication. app.get("/relay", function(req, res, next) { // Allow the relay to be run within a frame @@ -149,18 +155,19 @@ exports.setup = function(app) { renderCachableView(req, res, 'add_email_address.ejs', {title: 'Verify Email Address', fullpage: false}); }); - /** - * - * XXX benadida or lloyd, I tried to use straight up regexp to do this, but. - * is there a better way to do this? - */ - function QUnit(req, res) { - res.render('test.ejs', {title: 'BrowserID QUnit Test', layout: false}); + // serve up testing templates. but NOT in staging or production. see GH-1044 + if ([ 'https://browserid.org', 'https://diresworb.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}); + }); + } else { + // this is stage or production, explicitly disable all resources under /test + app.get(/^\/test/, function(req, res) { + httputils.notFound("Cannot " + req.method + " " + req.url); + }); } - app.get("/test", QUnit); - app.get("/test/index.html", QUnit); - // REDIRECTS REDIRECTS = { "/manage": "/", diff --git a/lib/configuration.js b/lib/configuration.js index 78eead9e9a37dfb31eff37c96b9fdc452f775e46..1b443b5754a3c9a52f9afb56814c7a37d673c622 100644 --- a/lib/configuration.js +++ b/lib/configuration.js @@ -76,7 +76,10 @@ var conf = module.exports = convict({ }, database: { driver: 'string ["json", "mysql"] = "json"', - user: 'string?', + user: { + format: 'string?', + env: 'MYSQL_USER' + }, create_schema: 'boolean = true', may_write: 'boolean = true', name: { @@ -84,7 +87,16 @@ var conf = module.exports = convict({ env: 'DATABASE_NAME' }, password: 'string?', - host: 'string?' + host: 'string?', + max_query_time_ms: { + format: 'integer = 5000', + doc: "The maximum amount of time we'll allow a query to run before considering the database to be sick", + env: 'MAX_QUERY_TIME_MS' + }, + max_reconnect_attempts: { + format: 'integer = 1', + doc: "The maximum number of times we'll attempt to reconnect to the database before failing all outstanding queries" + } }, smtp: { host: 'string?', diff --git a/lib/db.js b/lib/db.js index c0670cad8e46ccab2ea7c8f92e6d76e2dd9ab5cf..6765c28fbc71ad132d2c3e5dd69352954c2e7b50 100644 --- a/lib/db.js +++ b/lib/db.js @@ -39,7 +39,7 @@ exports.open = function(cfg, cb) { ready = true; waiting.forEach(function(f) { f() }); waiting = []; - if (cb) cb(); + if (cb) cb(null); } }); }; diff --git a/lib/db/json.js b/lib/db/json.js index 266ae22b3d29e14993defe99528e447d3d6af7e8..b9a4c64088258d22a75452e88521af511239dc28 100644 --- a/lib/db/json.js +++ b/lib/db/json.js @@ -81,41 +81,40 @@ exports.open = function(cfg, cb) { logger.debug("opening JSON database: " + dbPath); sync(); - - setTimeout(cb, 0); + process.nextTick(function() { cb(null); }); }; exports.closeAndRemove = function(cb) { // if the file cannot be removed, it's not an error, just means it was never // written or deleted by a different process try { fs.unlinkSync(dbPath); } catch(e) { } - setTimeout(function() { cb(undefined); }, 0); + process.nextTick(function() { cb(null); }); }; exports.close = function(cb) { // don't flush database here to disk, the database is flushed synchronously when // written - If we were to flush here we could overwrite changes made by // another process - see issue #557 - setTimeout(function() { cb(undefined) }, 0); + process.nextTick(function() { cb(null) }); }; exports.emailKnown = function(email, cb) { sync(); var m = jsel.match(".emails ." + ESC(email), db.users); - setTimeout(function() { cb(m.length > 0) }, 0); + process.nextTick(function() { cb(null, m.length > 0) }); }; exports.emailType = function(email, cb) { sync(); var m = jsel.match(".emails ." + ESC(email), db.users); - process.nextTick(function() { cb(m.length ? m[0].type : undefined); }); + process.nextTick(function() { cb(null, m.length ? m[0].type : undefined); }); }; exports.isStaged = function(email, cb) { if (cb) { setTimeout(function() { sync(); - cb(db.stagedEmails.hasOwnProperty(email)); + cb(null, db.stagedEmails.hasOwnProperty(email)); }, 0); } }; @@ -127,7 +126,7 @@ exports.lastStaged = function(email, cb) { if (db.stagedEmails.hasOwnProperty(email)) { d = new Date(db.staged[db.stagedEmails[email]].when); } - setTimeout(function() { cb(d); }, 0); + setTimeout(function() { cb(null, d); }, 0); } }; @@ -135,7 +134,7 @@ exports.emailsBelongToSameAccount = function(lhs, rhs, cb) { sync(); var m = jsel.match(".emails:has(."+ESC(lhs)+"):has(."+ESC(rhs)+")", db.users); process.nextTick(function() { - cb(m && m.length == 1); + cb(null, m && m.length == 1); }); }; @@ -145,7 +144,7 @@ exports.emailToUID = function(email, cb) { if (m.length === 0) m = undefined; else m = m[0]; process.nextTick(function() { - cb(m); + cb(null, m); }); }; @@ -153,7 +152,7 @@ exports.userOwnsEmail = function(uid, email, cb) { sync(); var m = jsel.match(":root > object:has(:root > .id:expr(x=" + ESC(uid) + ")):has(.emails > ." + ESC(email) + ")", db.users); process.nextTick(function() { - cb(m && m.length == 1); + cb(null, m && m.length == 1); }); }; @@ -172,7 +171,7 @@ function addEmailToAccount(userID, email, type, cb) { emails[0][email] = { type: type }; flush(); } - cb(); + cb(null); }); } @@ -187,7 +186,7 @@ exports.stageUser = function(email, cb) { }; db.stagedEmails[email] = secret; flush(); - setTimeout(function() { cb(secret); }, 0); + process.nextTick(function() { cb(null, secret); }); }); }; @@ -204,7 +203,7 @@ exports.stageEmail = function(existing_user, new_email, cb) { db.stagedEmails[new_email] = secret; flush(); - setTimeout(function() { cb(secret); }, 0); + process.nextTick(function() { cb(null, secret); }); }); }; @@ -219,14 +218,14 @@ exports.createUserWithPrimaryEmail = function(email, cb) { }); flush(); process.nextTick(function() { - cb(undefined, uid); + cb(null, uid); }); }; exports.haveVerificationSecret = function(secret, cb) { process.nextTick(function() { sync(); - cb(!!(db.staged[secret])); + cb(null, !!(db.staged[secret])); }); }; @@ -235,8 +234,8 @@ exports.emailForVerificationSecret = function(secret, cb) { process.nextTick(function() { sync(); if (!db.staged[secret]) return cb("no such secret"); - exports.checkAuth(db.staged[secret].existing_user, function (hash) { - cb(undefined, { + exports.checkAuth(db.staged[secret].existing_user, function (err, hash) { + cb(err, { email: db.staged[secret].email, needs_password: !hash }); @@ -247,7 +246,7 @@ exports.emailForVerificationSecret = function(secret, cb) { exports.verificationSecretForEmail = function(email, cb) { setTimeout(function() { sync(); - cb(db.stagedEmails[email]); + cb(null, db.stagedEmails[email]); }, 0); }; @@ -261,7 +260,7 @@ exports.gotVerificationSecret = function(secret, hash, cb) { delete db.stagedEmails[o.email]; flush(); if (o.type === 'add_account') { - exports.emailKnown(o.email, function(known) { + exports.emailKnown(o.email, function(err, known) { function createAccount() { var emailVal = {}; emailVal[o.email] = { type: 'secondary' }; @@ -272,7 +271,7 @@ exports.gotVerificationSecret = function(secret, hash, cb) { emails: emailVal }); flush(); - cb(undefined, o.email, uid); + cb(null, o.email, uid); } // if this email address is known and a user has completed a re-verification of this email @@ -291,7 +290,7 @@ exports.gotVerificationSecret = function(secret, hash, cb) { } }); } else if (o.type === 'add_email') { - exports.emailKnown(o.email, function(known) { + exports.emailKnown(o.email, function(err, known) { function addIt() { addEmailToAccount(o.existing_user, o.email, 'secondary', function(e) { cb(e, o.email, o.existing_user); @@ -313,7 +312,7 @@ exports.gotVerificationSecret = function(secret, hash, cb) { exports.addPrimaryEmailToAccount = function(userID, emailToAdd, cb) { sync(); - exports.emailKnown(emailToAdd, function(known) { + exports.emailKnown(emailToAdd, function(err, known) { function addIt() { addEmailToAccount(userID, emailToAdd, 'primary', cb); } @@ -336,7 +335,7 @@ exports.checkAuth = function(userID, cb) { if (m.length === 0) m = undefined; else m = m[0]; } - process.nextTick(function() { cb(m) }); + process.nextTick(function() { cb(null, m) }); }; exports.userKnown = function(userID, cb) { @@ -344,7 +343,7 @@ exports.userKnown = function(userID, cb) { var m = jsel.match(":root > object:has(:root > .id:expr(x=" + ESC(userID) + "))", db.users); if (m.length === 0) m = undefined; else m = m[0]; - process.nextTick(function() { cb(m) }); + process.nextTick(function() { cb(null, m) }); }; exports.updatePassword = function(userID, hash, cb) { @@ -378,7 +377,7 @@ exports.removeEmail = function(authenticated_user, email, cb) { delete emails[email]; flush(); } - setTimeout(function() { cb(); }, 0); + setTimeout(function() { cb(null); }, 0); }; function removeEmailNoCheck(email, cb) { @@ -389,7 +388,7 @@ function removeEmailNoCheck(email, cb) { delete emails[email]; flush(); } - process.nextTick(function() { cb(); }); + process.nextTick(function() { cb(null); }); }; exports.cancelAccount = function(authenticated_uid, cb) { @@ -405,7 +404,7 @@ exports.cancelAccount = function(authenticated_uid, cb) { flush(); } - process.nextTick(function() { cb(); }); + process.nextTick(function() { cb(null); }); }; exports.addTestUser = function(email, hash, cb) { @@ -419,10 +418,10 @@ exports.addTestUser = function(email, hash, cb) { emails: emailVal }); flush(); - cb(); + cb(null); }); }; exports.ping = function(cb) { - setTimeout(function() { cb(); }, 0); + process.nextTick(function() { cb(null); }); }; diff --git a/lib/db/mysql.js b/lib/db/mysql.js index 8b8839635b94a59297da75df9651d1457412d4f3..2931097bcde0d45dd58fdeef5265e1c1524a2a6f 100644 --- a/lib/db/mysql.js +++ b/lib/db/mysql.js @@ -29,13 +29,34 @@ */ const -mysql = require('mysql'), +mysql = require('./mysql_wrapper.js'), secrets = require('../secrets.js'), logger = require('../logging.js').logger, -statsd = require('../statsd'); +conf = require('../configuration.js'); var client = undefined; +// for testing! when 'STALL_MYSQL_WHEN_PRESENT' is defined in the environment, +// it causes the driver to simulate stalling whent said file is present +if (conf.get('env') === 'test_mysql' && process.env['STALL_MYSQL_WHEN_PRESENT']) { + logger.debug('database driver will be stalled when file is present: ' + + process.env['STALL_MYSQL_WHEN_PRESENT']); + const fs = require('fs'); + fs.watchFile( + process.env['STALL_MYSQL_WHEN_PRESENT'], + { persistent: false, interval: 1 }, + function (curr, prev) { + // stall the database driver when specified file is present + fs.stat(process.env['STALL_MYSQL_WHEN_PRESENT'], function(err, stats) { + if (client) { + var stall = !(err && err.code === 'ENOENT'); + logger.debug("database driver is " + (stall ? "stalled" : "unblocked")); + client.stall(stall); + } + }); + }); +} + // If you change these schemas, please notify <services-ops@mozilla.com> const schemas = [ "CREATE TABLE IF NOT EXISTS user (" + @@ -68,7 +89,7 @@ function logUnexpectedError(detail) { var where; try { dne; } catch (e) { where = e.stack.split('\n')[2].trim(); }; // now log it! - logger.error("unexpected database failure: " + detail + " -- " + where); + logger.warn("unexpected database failure: " + detail + " -- " + where); } // open & create the mysql database @@ -98,27 +119,6 @@ exports.open = function(cfg, cb) { options.database = database; client = mysql.createClient(options); - // replace .query with a function that times queries and - // logs to statsd - var realQuery = client.query; - client.query = function() { - var startTime = new Date(); - var client_cb; - var new_cb = function() { - var reqTime = new Date - startTime; - statsd.timing('query_time', reqTime); - if (client_cb) client_cb.apply(null, arguments); - }; - var args = Array.prototype.slice.call(arguments); - if (typeof args[args.length - 1] === 'function') { - client_cb = args[args.length - 1]; - args[args.length - 1] = new_cb; - } else { - args.push(new_cb); - } - realQuery.apply(client, args); - }; - client.ping(function(err) { logger.debug("connection to database " + (err ? ("fails: " + err) : "established")); cb(err); @@ -176,7 +176,7 @@ exports.close = function(cb) { client.end(function(err) { client = undefined; if (err) logUnexpectedError(err); - if (cb) cb(err); + if (cb) cb(err === undefined ? null : err); }); }; @@ -198,8 +198,7 @@ exports.emailKnown = function(email, cb) { client.query( "SELECT COUNT(*) as N FROM email WHERE address = ?", [ email ], function(err, rows) { - if (err) logUnexpectedError(err); - cb(rows && rows.length > 0 && rows[0].N > 0); + cb(err, rows && rows.length > 0 && rows[0].N > 0); } ); }; @@ -208,8 +207,7 @@ exports.userKnown = function(uid, cb) { client.query( "SELECT COUNT(*) as N FROM user WHERE id = ?", [ uid ], function(err, rows) { - if (err) logUnexpectedError(err); - cb(rows && rows.length > 0 && rows[0].N > 0); + cb(err, rows && rows.length > 0 && rows[0].N > 0); } ); }; @@ -218,8 +216,7 @@ exports.emailType = function(email, cb) { client.query( "SELECT type FROM email WHERE address = ?", [ email ], function(err, rows) { - if (err) logUnexpectedError(err); - cb((rows && rows.length > 0) ? rows[0].type : undefined); + cb(err, (rows && rows.length > 0) ? rows[0].type : undefined); } ); } @@ -228,8 +225,7 @@ exports.isStaged = function(email, cb) { client.query( "SELECT COUNT(*) as N FROM staged WHERE email = ?", [ email ], function(err, rows) { - if (err) logUnexpectedError(err); - cb(rows && rows.length > 0 && rows[0].N > 0); + cb(err, rows && rows.length > 0 && rows[0].N > 0); } ); } @@ -238,9 +234,9 @@ exports.lastStaged = function(email, cb) { client.query( "SELECT UNIX_TIMESTAMP(ts) as ts FROM staged WHERE email = ?", [ email ], function(err, rows) { - if (err) logUnexpectedError(err); - if (!rows || rows.length === 0) cb(); - else cb(new Date(rows[0].ts * 1000)); + if (err) cb(err); + else if (!rows || rows.length === 0) cb(null); + else cb(null, new Date(rows[0].ts * 1000)); } ); }; @@ -252,10 +248,7 @@ exports.stageUser = function(email, cb) { 'ON DUPLICATE KEY UPDATE secret=?, existing_user=NULL, new_acct=TRUE, ts=NOW()', [ secret, email, secret], function(err) { - if (err) { - logUnexpectedError(err); - cb(undefined, err); - } else cb(secret); + cb(err, err ? undefined : secret); }); }); }; @@ -265,8 +258,7 @@ exports.haveVerificationSecret = function(secret, cb) { client.query( "SELECT count(*) as n FROM staged WHERE secret = ?", [ secret ], function(err, rows) { - if (err) cb(false); - else cb(rows.length === 1 && rows[0].n === 1); + cb(err, rows && rows.length === 1 && rows[0].n === 1); }); }; @@ -274,14 +266,15 @@ exports.emailForVerificationSecret = function(secret, cb) { client.query( "SELECT * FROM staged WHERE secret = ?", [ secret ], function(err, rows) { - if (err) logUnexpectedError(err); + if (err) return cb("database unavailable"); + // if the record was not found, fail out if (!rows || rows.length != 1) return cb("no such secret"); var o = rows[0]; // if the record was found and this is for a new_acct, return the email - if (o.new_acct) return cb(undefined, { email: o.email, needs_password: false }); + if (o.new_acct) return cb(null, { email: o.email, needs_password: false }); // we need a userid. the old schema had an 'existing' field which was an email // address. the new schema has an 'existing_user' field which is a userid. @@ -289,8 +282,8 @@ exports.emailForVerificationSecret = function(secret, cb) { // and can be removed in feb 2012 some time. maybe for valentines day? if (typeof o.existing_user === 'number') doCheckAuth(o.existing_user); else if (typeof o.existing === 'string') { - exports.emailToUID(o.existing, function(uid) { - if (uid === undefined) return cb('acct associated with staged email doesn\'t exist'); + exports.emailToUID(o.existing, function(err, uid) { + if (err || uid === undefined) return cb('acct associated with staged email doesn\'t exist'); doCheckAuth(uid); }); } @@ -301,8 +294,8 @@ exports.emailForVerificationSecret = function(secret, cb) { // are associated with the acct at the moment, then there will not be a // password set and the user will need to set one with the addition of // this addresss) - exports.checkAuth(uid, function(hash) { - cb(undefined, { + exports.checkAuth(uid, function(err, hash) { + cb(err, { email: o.email, needs_password: !hash }); @@ -315,8 +308,7 @@ exports.verificationSecretForEmail = function(email, cb) { client.query( "SELECT secret FROM staged WHERE email = ?", [ email ], function(err, rows) { - if (err) logUnexpectedError(err); - cb((rows && rows.length > 0) ? rows[0].secret : undefined); + cb(err, (rows && rows.length > 0) ? rows[0].secret : undefined); }); }; @@ -329,14 +321,14 @@ function addEmailToUser(userID, email, type, cb) { "DELETE FROM email WHERE address = ?", [ email ], function(err, info) { - if (err) { logUnexpectedError(err); cb(err); return; } + if (err) return cb(err); else { client.query( "INSERT INTO email(user, address, type) VALUES(?, ?, ?)", [ userID, email, type ], function(err, info) { if (err) logUnexpectedError(err); - cb(err ? err : undefined, email, userID); + cb(err, email, userID); }); } }); @@ -363,7 +355,7 @@ exports.gotVerificationSecret = function(secret, hash, cb) { "INSERT INTO user(passwd) VALUES(?)", [ hash ], function(err, info) { - if (err) { logUnexpectedError(err); cb(err); return; } + if (err) return cb(err); addEmailToUser(info.insertId, o.email, 'secondary', cb); }); } else { @@ -374,7 +366,7 @@ exports.gotVerificationSecret = function(secret, hash, cb) { if (typeof o.existing_user === 'number') doAddEmail(o.existing_user); else if (typeof o.existing === 'string') { exports.emailToUID(o.existing, function(uid) { - if (uid === undefined) return cb('acct associated with staged email doesn\'t exist'); + if (err || uid === undefined) return cb('acct associated with staged email doesn\'t exist'); doAddEmail(uid); }); } @@ -400,14 +392,13 @@ exports.createUserWithPrimaryEmail = function(email, cb) { client.query( "INSERT INTO user() VALUES()", function(err, info) { - if (err) { logUnexpectedError(err); cb(err); return; } + if (err) return cb(err); var uid = info.insertId; client.query( "INSERT INTO email(user, address, type) VALUES(?, ?, ?)", [ uid, email, 'primary' ], function(err, info) { - if (err) logUnexpectedError(err); - cb(err ? err : undefined, uid); + cb(err, uid); }); }); }; @@ -417,8 +408,7 @@ exports.emailsBelongToSameAccount = function(lhs, rhs, cb) { 'SELECT COUNT(*) AS n FROM email WHERE address = ? AND user = ( SELECT user FROM email WHERE address = ? );', [ lhs, rhs ], function (err, rows) { - if (err) cb(false); - else cb(rows.length === 1 && rows[0].n === 1); + cb(err, rows && rows.length === 1 && rows[0].n === 1); }); } @@ -427,8 +417,7 @@ exports.userOwnsEmail = function(uid, email, cb) { 'SELECT COUNT(*) AS n FROM email WHERE address = ? AND user = ?', [ email, uid ], function (err, rows) { - if (err) cb(false); - else cb(rows.length === 1 && rows[0].n === 1); + cb(err, rows && rows.length === 1 && rows[0].n === 1); }); } @@ -439,11 +428,7 @@ exports.stageEmail = function(existing_user, new_email, cb) { 'ON DUPLICATE KEY UPDATE secret=?, existing_user=?, new_acct=FALSE, ts=NOW()', [ secret, existing_user, new_email, secret, existing_user], function(err) { - if (err) { - logUnexpectedError(err); - cb(undefined, err); - } - else cb(secret); + cb(err, err ? undefined : secret); }); }); }; @@ -453,8 +438,7 @@ exports.emailToUID = function(email, cb) { 'SELECT user FROM email WHERE address = ?', [ email ], function (err, rows) { - if (err) logUnexpectedError(err); - cb((rows && rows.length == 1) ? rows[0].user : undefined); + cb(err, (rows && rows.length == 1) ? rows[0].user : undefined); }); }; @@ -463,8 +447,7 @@ exports.checkAuth = function(uid, cb) { 'SELECT passwd FROM user WHERE id = ?', [ uid ], function (err, rows) { - if (err) logUnexpectedError(err); - cb((rows && rows.length == 1) ? rows[0].passwd : undefined); + cb(err, (rows && rows.length == 1) ? rows[0].passwd : undefined); }); } @@ -473,8 +456,10 @@ exports.updatePassword = function(uid, hash, cb) { 'UPDATE user SET passwd = ? WHERE id = ?', [ hash, uid ], function (err, rows) { - if (err) logUnexpectedError(err); - cb((err || rows.affectedRows !== 1) ? ("no record with email " + email) : undefined); + if (!err && (!rows || rows.affectedRows !== 1)) { + err = "no record with email " + email; + } + cb(err); }); } @@ -504,7 +489,9 @@ exports.listEmails = function(uid, cb) { }; exports.removeEmail = function(authenticated_user, email, cb) { - exports.userOwnsEmail(authenticated_user, email, function(ok) { + exports.userOwnsEmail(authenticated_user, email, function(err, ok) { + if (err) return cb(err); + if (!ok) { logger.warn(authenticated_user + ' attempted to delete an email that doesn\'t belong to her: ' + email); cb("authenticated user doesn't have permission to remove specified email " + email); @@ -515,18 +502,16 @@ exports.removeEmail = function(authenticated_user, email, cb) { 'DELETE FROM email WHERE address = ?', [ email ], function(err, info) { - if (err) logUnexpectedError(err); - // smash null into undefined - cb(err ? err : undefined); + cb(err); }); }); }; exports.cancelAccount = function(uid, cb) { - function reportErr(err) { if (err) logUnexpectedError(err); } - client.query("DELETE LOW_PRIORITY FROM email WHERE user = ?", [ uid ], reportErr); - client.query("DELETE LOW_PRIORITY FROM user WHERE id = ?", [ uid ], reportErr); - process.nextTick(cb); + client.query("DELETE LOW_PRIORITY FROM email WHERE user = ?", [ uid ], function(err) { + if (err) return cb(err); + client.query("DELETE LOW_PRIORITY FROM user WHERE id = ?", [ uid ], cb); + }); }; exports.addTestUser = function(email, hash, cb) { @@ -534,17 +519,14 @@ exports.addTestUser = function(email, hash, cb) { "INSERT INTO user(passwd) VALUES(?)", [ hash ], function(err, info) { - if (err) { - logUnexpectedError(err); - cb(err); - return; - } + if (err) return cb(err); + client.query( "INSERT INTO email(user, address) VALUES(?, ?)", [ info.insertId, email ], function(err, info) { if (err) logUnexpectedError(err); - cb(err ? err : undefined, email); + cb(err, err ? null : email); }); }); }; diff --git a/lib/db/mysql_wrapper.js b/lib/db/mysql_wrapper.js new file mode 100644 index 0000000000000000000000000000000000000000..91c4634827d590ed31bc007ad03decbd4fcbec8b --- /dev/null +++ b/lib/db/mysql_wrapper.js @@ -0,0 +1,138 @@ +/* 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/. */ + +/* This abstraction wraps the mysql driver and provides application level + * queueing, as well as query timing and reconnect upon an apparently "stalled" + * driver + */ + +const +mysql = require('mysql'), +statsd = require('../statsd'), +logger = require('../logging.js').logger, +config = require('../configuration.js'); + +exports.createClient = function(options) { + // the application level query queue + var queryQueue = []; + // The slowQueryTimer is !null when a query is running, and holds + // the result from setTimeout. This variable is both a means to + // check if a query is running (only one runs at a time), and as + // the timeout handle. + var slowQueryTimer = null; + // how many consecutive failures have we seen when running queries? + var consecutiveFailures = 0; + // a testing feature. By calling `client.stall` you can + // cause responses to be dropped which will trigger slow query detection + var stalled = false; + + var client = { + stall: function(stalledState) { + stalled = stalledState; + }, + realClient: null, + _resetConnection: function() { + if (this.realClient) this.realClient.destroy(); + this.realClient = mysql.createClient(options); + this.realClient.on('error', function(e) { + logger.warn("database connection down: " + e.toString()); + }); + }, + ping: function(cb) { + this.realClient.ping(cb); + }, + _runNextQuery: function() { + var self = this; + + if (slowQueryTimer !== null || !queryQueue.length) return; + + var work = queryQueue.shift(); + + function invokeCallback(cb, err, rez) { + if (cb) { + process.nextTick(function() { + try { + cb(err, rez); + } catch(e) { + logger.error('database query callback failed: ' + e.toString()); + } + }); + } + } + + slowQueryTimer = setTimeout(function() { + if (++consecutiveFailures > config.get('database.max_reconnect_attempts')) { + // if we can't run the query multiple times in a row, we'll fail all outstanding + // queries, and reinitialize the connection, so that the process stays up and + // retries mysql connection the next time a request which requires db interaction + // comes in. + queryQueue.unshift(work); + logger.warn("cannot reconnect to mysql! " + queryQueue.length + " outstanding queries #fail."); + queryQueue.forEach(function(work) { + invokeCallback(work.cb, "database connection unavailable"); + }); + queryQueue = []; + self._resetConnection(); + slowQueryTimer = null; + } else { + logger.warn("Query taking more than " + config.get('database.max_query_time_ms') + "ms! reconnecting to mysql"); + // we'll fail the long running query, because we cannot + // meaningfully know whether or not it completed in the case where + // the driver is unresponsive. + invokeCallback(work.cb, "database connection unavailable"); + self._resetConnection(); + slowQueryTimer = null; + self._runNextQuery(); + } + }, config.get('database.max_query_time_ms')); + + this.realClient.query(work.query, work.args, function(err, r) { + // if we want to simulate a "stalled" mysql connection, we simply + // ignore the results from a query. + if (stalled) return; + + clearTimeout(slowQueryTimer); + slowQueryTimer = null; + consecutiveFailures = 0; + + // report query time for all queries via statsd + var reqTime = new Date - work.startTime; + statsd.timing('query_time', reqTime); + + // report failed queries via statsd + if (err) statsd.increment('failed_query'); + + invokeCallback(work.cb, err, r); + self._runNextQuery(); + }); + }, + query: function() { + var client_cb; + var args = Array.prototype.slice.call(arguments); + var query = args.shift(); + if (args.length && typeof args[args.length - 1] === 'function') { + client_cb = args.pop(); + } + args = args.length ? args[0] : []; + queryQueue.push({ + query: query, + args: args, + cb: client_cb, + // record the time .query was called by the application for + // true end to end query timing in statsd + startTime: new Date() + }); + this._runNextQuery(); + }, + end: function(cb) { + this.realClient.end(cb); + }, + useDatabase: function(db, cb) { + this.realClient.useDatabase(db, cb); + } + }; + client._resetConnection(); + client.database = client.realClient.database; + return client; +}; diff --git a/lib/httputils.js b/lib/httputils.js index 2491e5ee34a978b30c91aa53c4b69d772f89887d..81e68334d52bcf82484bef7e0d086f8ab471e17f 100644 --- a/lib/httputils.js +++ b/lib/httputils.js @@ -5,53 +5,37 @@ // various little utilities to make crafting boilerplate responses // simple -exports.fourOhFour = function(resp, reason) -{ - resp.writeHead(404, {"Content-Type": "text/plain"}); - resp.write("Not Found"); - if (reason) { - resp.write(": " + reason); +function sendResponse(resp, content, reason, code) { + if (content) { + if (reason) content += ": " + reason; + } else if (reason) { + content = reason; + } else { + content = ""; } - resp.end(); + resp.send(content, {"Content-Type": "text/plain"}, code); +} + +exports.notFound = function(resp, reason) { + sendResponse(resp, "Not Found", reason, 404); }; -exports.serverError = function(resp, reason) -{ - resp.writeHead(500, {"Content-Type": "text/plain"}); - if (reason) resp.write(reason); - resp.end(); +exports.serverError = function(resp, reason) { + sendResponse(resp, "Server Error", reason, 500); }; -exports.badRequest = function(resp, reason) -{ - resp.writeHead(400, {"Content-Type": "text/plain"}); - resp.write("Bad Request"); - if (reason) { - resp.write(": " + reason); - } - resp.end(); +exports.serviceUnavailable = function(resp, reason) { + sendResponse(resp, "Service Unavailable", reason, 503); }; -exports.forbidden = function(resp, reason) -{ - resp.writeHead(403, {"Content-Type": "text/plain"}); - resp.write("Forbidden"); - if (reason) { - resp.write(": " + reason); - } - resp.end(); +exports.badRequest = function(resp, reason) { + sendResponse(resp, "Bad Request", reason, 400); }; -exports.jsonResponse = function(resp, obj) -{ - resp.writeHead(200, {"Content-Type": "application/json"}); - if (obj !== undefined) resp.write(JSON.stringify(obj)); - resp.end(); +exports.forbidden = function(resp, reason) { + sendResponse(resp, "Forbidden", reason, 403); }; -exports.xmlResponse = function(resp, doc) -{ - resp.writeHead(200, {"Content-Type": "text/xml"}); - if (doc !== undefined) resp.write(doc); - resp.end(); +exports.throttled = function(resp, reason) { + sendResponse(resp, "Too Many Requests", reason, 429); }; diff --git a/lib/static_resources.js b/lib/static_resources.js new file mode 100644 index 0000000000000000000000000000000000000000..859d3d88ca5f9497ee03fbdcdafcd4e3c41705c0 --- /dev/null +++ b/lib/static_resources.js @@ -0,0 +1,150 @@ +var i18n = require('./i18n'), + und = require('underscore'); + +/** + * Module for managing all the known static assets in browserid. + * In filenames/paths below, you may use ``:locale`` as a url + * variable to be expanded later. + * + * These settings affect usage of cachify and eventually our + * asset build steps. + * + * Be careful editing common_js, as it will affect all + * minified scripts that depend on that variable. IE re-ordering + * the list or removing a script. + */ + +// Common to browserid.js dialog.js +var common_js = [ + '/lib/jquery-1.7.1.min.js', + '/lib/winchan.js', + '/lib/underscore-min.js', + '/lib/vepbundle.js', + '/lib/ejs.js', + '/shared/javascript-extensions.js', + '/i18n/:locale/client.json', + '/shared/gettext.js', + '/shared/browserid.js', + '/lib/hub.js', + '/lib/dom-jquery.js', + '/lib/module.js', + '/lib/jschannel.js', + '/shared/templates.js', + '/shared/renderer.js', + '/shared/class.js', + '/shared/mediator.js', + '/shared/tooltip.js', + '/shared/validation.js', + '/shared/helpers.js', + '/shared/screens.js', + '/shared/browser-support.js', + '/shared/wait-messages.js', + '/shared/error-messages.js', + '/shared/error-display.js', + '/shared/storage.js', + '/shared/xhr.js', + '/shared/network.js', + '/shared/provisioning.js', + '/shared/user.js', + '/shared/modules/page_module.js', + '/shared/modules/xhr_delay.js', + '/shared/modules/xhr_disable_form.js', + '/shared/modules/cookie_check.js' +]; + +var browserid_min_js = '/production/:locale/browserid.js'; +var browserid_js = und.flatten([ + common_js, + [ + '/pages/page_helpers.js', + '/pages/index.js', + '/pages/start.js', + '/pages/add_email_address.js', + '/pages/verify_email_address.js', + '/pages/forgot.js', + '/pages/manage_account.js', + '/pages/signin.js', + '/pages/signup.js' + ] +]); + +var dialog_min_js = '/production/:locale/dialog.js'; +var dialog_js = und.flatten([ + common_js, + [ + '/lib/urlparse.js', + + '/shared/command.js', + '/shared/history.js', + '/shared/state_machine.js', + + '/dialog/resources/internal_api.js', + '/dialog/resources/helpers.js', + '/dialog/resources/state.js', + + '/dialog/controllers/actions.js', + '/dialog/controllers/dialog.js', + '/dialog/controllers/authenticate.js', + '/dialog/controllers/forgot_password.js', + '/dialog/controllers/check_registration.js', + '/dialog/controllers/pick_email.js', + '/dialog/controllers/add_email.js', + '/dialog/controllers/required_email.js', + '/dialog/controllers/verify_primary_user.js', + '/dialog/controllers/provision_primary_user.js', + '/dialog/controllers/primary_user_provisioned.js', + '/dialog/controllers/email_chosen.js', + + '/dialog/start.js' + ]]); + +exports.resources = resources = { + '/production/dialog.css': [ + '/css/common.css', + '/dialog/css/popup.css', + '/dialog/css/m.css' + ], + '/production/browserid.css': [ + '/css/common.css', + '/css/style.css', + '/css/m.css' + ] +}; +resources[dialog_min_js] = dialog_js; +resources[browserid_min_js] = browserid_js; + +var replace = function(path, locale) { return path.replace(':locale', locale); }; + +/** + * Returns all filenames of static resources + * in a connect-cachify compatible format. + * + * @langs - array of languages we support + * @return { minified_file: [dependent, files] } + * + * Languages will be converted to locales. Filenames and list of files + * will be expanded to match all the permutations. + */ +exports.all = function(langs) { + var res = {}; + for (var f in resources) { + langs.forEach(function (lang) { + var l = i18n.localeFrom(lang); + res[replace(f, l)] = getResources(f, l); + }); + } + return res; +}; + +/** + * Get all resource urls for a specified resource based on the locale + */ +exports.getResources = getResources = function(path, locale) { + var res = []; + if (resources[path]) { + resources[path].forEach(function(r) { + res.push(replace(r, locale)); + }); + } + return res; +}; diff --git a/lib/wsapi.js b/lib/wsapi.js index 8df4ff9aca5077991229e8474787774bb9ac602e..e42d6f828259d12a496d568d35a4088762aab268 100644 --- a/lib/wsapi.js +++ b/lib/wsapi.js @@ -38,7 +38,18 @@ var abide = i18n.abide({ }); const COOKIE_SECRET = secrets.hydrateSecret('browserid_cookie', config.get('var_path')); -const COOKIE_KEY = 'browserid_state'; +var COOKIE_KEY = 'browserid_state'; + +// to support testing of browserid, we'll add a hash fragment to the cookie name for +// sites other than browserid.org. This is to address a bug in IE, see issue #296 +if (config.get('public_url').indexOf('https://browserid.org') !== 0) { + const crypto = require('crypto'); + var hash = crypto.createHash('md5'); + hash.update(config.get('public_url')); + COOKIE_KEY += "_" + hash.digest('hex').slice(0, 6); +} + +logger.info('session cookie name is: ' + COOKIE_KEY); function clearAuthenticatedUser(session) { session.reset(['csrf']); @@ -69,8 +80,15 @@ function authenticateSession(session, uid, level) { if (['assertion', 'password'].indexOf(level) === -1) throw "invalid authentication level: " + level; - session.userid = uid; - session.auth_level = level; + // if the user is *already* authenticated as this uid with an equal or better + // level of auth, let's not lower them. Issue #1049 + if (session.userid === uid && session.auth_level === 'password' && + session.auth_level !== level) { + logger.info("not resetting cookies to 'assertion' authenticate a user who is already password authenticated"); + } else { + session.userid = uid; + session.auth_level = level; + } } function checkPassword(pass) { @@ -89,6 +107,11 @@ function langContext(req) { }; } +function databaseDown(res, err) { + logger.warn('database is down, cannot process request: ' + err); + httputils.serviceUnavailable(res, "database unavailable"); +} + // common functions exported, for use by different api calls exports.clearAuthenticatedUser = clearAuthenticatedUser; exports.isAuthed = isAuthed; @@ -97,6 +120,7 @@ exports.authenticateSession = authenticateSession; exports.checkPassword = checkPassword; exports.fowardWritesTo = undefined; exports.langContext = langContext; +exports.databaseDown = databaseDown; exports.setup = function(options, app) { const WSAPI_PREFIX = '/wsapi/'; @@ -182,7 +206,7 @@ exports.setup = function(options, app) { if (req.session === undefined || typeof req.session.csrf !== 'string') { // there must be a session logger.warn("POST calls to /wsapi require a cookie to be sent, this user may have cookies disabled"); - return httputils.badRequest(resp, "no cookie"); + return httputils.forbidden(resp, "no cookie"); } // and the token must match what is sent in the post body diff --git a/lib/wsapi/account_cancel.js b/lib/wsapi/account_cancel.js index c91428f8745102980299e4b0bfa1d4b31b59099c..a0e3644ab3183aeda642259f4e40a967886aa449 100644 --- a/lib/wsapi/account_cancel.js +++ b/lib/wsapi/account_cancel.js @@ -4,7 +4,7 @@ const db = require('../db.js'), -httputils = require('../httputils'), +wsapi = require('../wsapi'), logger = require('../logging.js').logger; exports.method = 'post'; @@ -15,8 +15,7 @@ exports.i18n = false; exports.process = function(req, res) { db.cancelAccount(req.session.userid, function(error) { if (error) { - logger.error("error canceling account : " + error.toString()); - httputils.badRequest(res, error.toString()); + wsapi.databaseDown(res, error); } else { res.json({ success: true }); }}); diff --git a/lib/wsapi/add_email_with_assertion.js b/lib/wsapi/add_email_with_assertion.js index e0ab5a626ce38087d88974cacd306318992a36d3..e8649ceb64911cfc659eefc2ae44c5da639d9980 100644 --- a/lib/wsapi/add_email_with_assertion.js +++ b/lib/wsapi/add_email_with_assertion.js @@ -38,10 +38,7 @@ exports.process = function(req, res) { if (err) { logger.warn('cannot add primary email "' + email + '" to acct with uid "' + req.session.userid + '": ' + err); - return res.json({ - success: false, - reason: "database error" - }); + return wsapi.databaseDown(res, err); } // success! diff --git a/lib/wsapi/address_info.js b/lib/wsapi/address_info.js index 0cd29bd0595afd5121533bf6adbb89faaec2582d..bfccae4b9af457f43520e46dcfc2aa81de5d67fa 100644 --- a/lib/wsapi/address_info.js +++ b/lib/wsapi/address_info.js @@ -4,7 +4,8 @@ const db = require('../db.js'), -primary = require('../primary.js'); +primary = require('../primary.js'), +wsapi = require('../wsapi.js'); // return information about an email address. // type: is this an address with 'primary' or 'secondary' support? @@ -27,28 +28,25 @@ exports.process = function(req, resp) { var email = url.parse(req.url, true).query['email']; var m = emailRegex.exec(email); if (!m) { - resp.sendHeader(400); - resp.json({ "error": "invalid email address" }); - return; + return httputils.badRequest(resp, "invalid email address"); } primary.checkSupport(m[1], function(err, rv) { if (err) { logger.warn('error checking "' + m[1] + '" for primary support: ' + err); - resp.sendHeader(500); - resp.json({ "error": "can't check email address" }); - return; + return httputils.serverError(resp, "can't check email address"); } if (rv) { rv.type = 'primary'; resp.json(rv); } else { - db.emailKnown(email, function(known) { - resp.json({ - type: 'secondary', - known: known - }); + db.emailKnown(email, function(err, known) { + if (err) { + return wsapi.databaseDown(resp, err); + } else { + resp.json({ type: 'secondary', known: known }); + } }); } }); diff --git a/lib/wsapi/auth_with_assertion.js b/lib/wsapi/auth_with_assertion.js index 3cd5075e9fd7351103cd97d43dada13efc7e5405..8781151358379e93e4f3e0bbe09c182c62b75667 100644 --- a/lib/wsapi/auth_with_assertion.js +++ b/lib/wsapi/auth_with_assertion.js @@ -33,10 +33,13 @@ exports.process = function(req, res) { } // 2. if valid, does the user exist? - db.emailType(email, function(type) { + db.emailType(email, function(err, type) { + if (err) return wsapi.databaseDown(res, err); + // if this is a known primary email, authenticate the user and we're done! if (type === 'primary') { - return db.emailToUID(email, function(uid) { + return db.emailToUID(email, function(err, uid) { + if (err) return wsapi.databaseDown(res, err); if (!uid) return res.json({ success: false, reason: "internal error" }); wsapi.authenticateSession(req.session, uid, 'assertion'); return res.json({ success: true }); diff --git a/lib/wsapi/authenticate_user.js b/lib/wsapi/authenticate_user.js index 22cc195e9360ec46c3455c48c54ad4ec7d1ba405..b1715a1b4c21fce281502e366ab4c1b47b8877fd 100644 --- a/lib/wsapi/authenticate_user.js +++ b/lib/wsapi/authenticate_user.js @@ -27,12 +27,16 @@ exports.process = function(req, res) { return res.json(r); } - db.emailToUID(req.body.email, function(uid) { + db.emailToUID(req.body.email, function(err, uid) { + if (err) return wsapi.databaseDown(res, err); + if (typeof uid !== 'number') { return fail('no such user'); } - db.checkAuth(uid, function(hash) { + db.checkAuth(uid, function(err, hash) { + if (err) return wsapi.databaseDown(res, err); + if (typeof hash !== 'string') { return fail('no password set for user'); } diff --git a/lib/wsapi/cert_key.js b/lib/wsapi/cert_key.js index 84b1b7582dd865174d381eae326723a3661c410a..9b642eb341646f360b8e4288dd166ed15b6d6713 100644 --- a/lib/wsapi/cert_key.js +++ b/lib/wsapi/cert_key.js @@ -8,7 +8,8 @@ httputils = require('../httputils'), logger = require('../logging.js').logger, forward = require('../http_forward.js'), config = require('../configuration.js'), -urlparse = require('urlparse'); +urlparse = require('urlparse'), +wsapi = require('../wsapi.js'); exports.method = 'post'; exports.writes_db = false; @@ -17,7 +18,9 @@ exports.args = ['email','pubkey']; exports.i18n = false; exports.process = function(req, res) { - db.userOwnsEmail(req.session.userid, req.body.email, function(owned) { + db.userOwnsEmail(req.session.userid, req.body.email, function(err, owned) { + if (err) return wsapi.databaseDown(res, err); + // not same account? big fat error if (!owned) return httputils.badRequest(res, "that email does not belong to you"); @@ -26,9 +29,8 @@ exports.process = function(req, res) { keysigner.path = '/wsapi/cert_key'; forward(keysigner, req, res, function(err) { if (err) { - logger.error("error forwarding request: " + err); - res.sendHeader(500); - res.json({ "error": "can't contact keysigner" }); + logger.error("error forwarding request to keysigner: " + err); + httputils.serverError(res, "can't contact keysigner"); return; } }); diff --git a/lib/wsapi/complete_email_addition.js b/lib/wsapi/complete_email_addition.js index 7c61b60a6c85700c1a623ce8b1dab6ab9b2f5aac..1359b49c379efab649d942f988f37f8c466c7dfa 100644 --- a/lib/wsapi/complete_email_addition.js +++ b/lib/wsapi/complete_email_addition.js @@ -21,6 +21,10 @@ exports.process = function(req, res) { // is currently NULL - this would occur in the case where this is the // first secondary address to be added to an account db.emailForVerificationSecret(req.body.token, function(err, r) { + if (err === 'database unavailable') { + return wsapi.databaseDown(res, err); + } + if (!err && r.needs_password && !req.body.pass) { err = "user must choose a password"; } @@ -40,7 +44,7 @@ exports.process = function(req, res) { db.gotVerificationSecret(req.body.token, req.body.pass, function(e, email, uid) { if (e) { logger.warn("couldn't complete email verification: " + e); - res.json({ success: false }); + wsapi.databaseDown(res, e); } else { // now do we need to set the password? if (r.needs_password && req.body.pass) { @@ -52,12 +56,13 @@ exports.process = function(req, res) { db.updatePassword(uid, hash, function(err) { if (err) { logger.warn("couldn't update password during email verification: " + err); + wsapi.databaseDown(res, err); } else { - // XXX: what if our software 503s? User doens't get a password set and + // XXX: what if our software 503s? User doesn't get a password set and // cannot change it. wsapi.authenticateSession(req.session, uid, 'password'); + res.json({ success: !err }); } - res.json({ success: !err }); }); }); } else { diff --git a/lib/wsapi/complete_user_creation.js b/lib/wsapi/complete_user_creation.js index 5249c90bb96e74b4f48098c8b826768a2444cccd..882351b630f784c34528302de0682ff9870859fc 100644 --- a/lib/wsapi/complete_user_creation.js +++ b/lib/wsapi/complete_user_creation.js @@ -26,17 +26,17 @@ exports.process = function(req, res) { // We should check to see if the verification secret is valid *before* // bcrypting the password (which is expensive), to prevent a possible // DoS attack. - db.haveVerificationSecret(req.body.token, function(known) { + db.haveVerificationSecret(req.body.token, function(err, known) { + if (err) return wsapi.databaseDown(res, err); + if (!known) return res.json({ success: false} ); // now bcrypt the password wsapi.bcryptPassword(req.body.pass, function (err, hash) { if (err) { - console.log(err); if (err.indexOf('exceeded') != -1) { logger.warn("max load hit, failing on auth request with 503: " + err); - res.status(503); - return res.json({ success: false, reason: "server is too busy" }); + return httputils.serviceUnavailable("server is too busy"); } logger.error("can't bcrypt: " + err); return res.json({ success: false }); @@ -45,7 +45,7 @@ exports.process = function(req, res) { db.gotVerificationSecret(req.body.token, hash, function(err, email, uid) { if (err) { logger.warn("couldn't complete email verification: " + err); - res.json({ success: false }); + wsapi.databaseDown(res, err); } else { // FIXME: not sure if we want to do this (ba) // at this point the user has set a password associated with an email address diff --git a/lib/wsapi/create_account_with_assertion.js b/lib/wsapi/create_account_with_assertion.js index 58cf266465c7d636e5eeb55f21202f883a5264bd..13f96d395fb6b56c7f4fa85adfcbc38472ea48cf 100644 --- a/lib/wsapi/create_account_with_assertion.js +++ b/lib/wsapi/create_account_with_assertion.js @@ -27,11 +27,7 @@ exports.process = function(req, res) { } db.createUserWithPrimaryEmail(email, function(err, uid) { - if (err) { - // yikes. couldn't write database? - logger.error('error creating user with primary email address for "'+email+'": ' + err); - return httputils.serverError(res); - } + if (err) return wsapi.databaseDown(res); res.json({ success: true, userid: uid }); }); }); diff --git a/lib/wsapi/email_addition_status.js b/lib/wsapi/email_addition_status.js index 833820e39aaf7db6ed4d32f02cb85894ba8639e8..5a7a3017d536a095cb2072137e94a6c6fa436021 100644 --- a/lib/wsapi/email_addition_status.js +++ b/lib/wsapi/email_addition_status.js @@ -3,7 +3,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const -db = require('../db.js'); +db = require('../db.js'), +wsapi = require('../wsapi.js'); /* First half of account creation. Stages a user account for creation. * this involves creating a secret url that must be delivered to the @@ -25,15 +26,19 @@ exports.process = function(req, res) { db.userOwnsEmail( req.session.userid, email, - function(registered) { - if (registered) { + function(err, registered) { + if (err) { + wsapi.databaseDown(res, err); + } else if (registered) { delete req.session.pendingAddition; res.json({ status: 'complete' }); } else if (!req.session.pendingAddition) { res.json({ status: 'failed' }); } else { - db.haveVerificationSecret(req.session.pendingAddition, function (known) { - if (known) { + db.haveVerificationSecret(req.session.pendingAddition, function (err, known) { + if (err) { + return wsapi.databaseDown(res, err); + } else if (known) { return res.json({ status: 'pending' }); } else { delete req.session.pendingAddition; diff --git a/lib/wsapi/email_for_token.js b/lib/wsapi/email_for_token.js index 4856cdcf0873a6fa3405134e83324faf419242a4..bfb122a747e8a514d1aca045fb3f14d7567fc6de 100644 --- a/lib/wsapi/email_for_token.js +++ b/lib/wsapi/email_for_token.js @@ -3,7 +3,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const -db = require('../db.js'); +db = require('../db.js'), +httputils = require('../httputils.js'); /* First half of account creation. Stages a user account for creation. * this involves creating a secret url that must be delivered to the @@ -20,10 +21,14 @@ exports.i18n = false; exports.process = function(req, res) { db.emailForVerificationSecret(req.query.token, function(err, r) { if (err) { - res.json({ - success: false, - reason: err - }); + if (err === 'database unavailable') { + httputils.serviceUnavailable(res, err); + } else { + res.json({ + success: false, + reason: err + }); + } } else { res.json({ success: true, diff --git a/lib/wsapi/have_email.js b/lib/wsapi/have_email.js index 05b88930b230be51ff30f1f2e18064f1d1ed8400..ec832546bc261fa1970197a5eda4c0e7312afcd0 100644 --- a/lib/wsapi/have_email.js +++ b/lib/wsapi/have_email.js @@ -3,7 +3,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const -db = require('../db.js'); +db = require('../db.js'), +wsapi = require('../wsapi.js'); // return if an email is known to browserid @@ -15,7 +16,8 @@ exports.i18n = false; exports.process = function(req, resp) { var email = url.parse(req.url, true).query['email']; - db.emailKnown(email, function(known) { + db.emailKnown(email, function(err, known) { + if (err) return wsapi.databaseDown(resp, err); resp.json({ email_known: known }); }); }; diff --git a/lib/wsapi/list_emails.js b/lib/wsapi/list_emails.js index fbe4fd64b5fa4bdc9cab607ceccd2cba638c4ca4..6da607007c3b967b53ccc4b6d77834c705db4387 100644 --- a/lib/wsapi/list_emails.js +++ b/lib/wsapi/list_emails.js @@ -4,7 +4,8 @@ const db = require('../db.js'), -logger = require('../logging.js').logger; +logger = require('../logging.js').logger, +wsapi = require('../wsapi.js'); // returns a list of emails owned by the user: // @@ -21,7 +22,7 @@ exports.i18n = false; exports.process = function(req, resp) { logger.debug('listing emails for user ' + req.session.userid); db.listEmails(req.session.userid, function(err, emails) { - if (err) httputils.serverError(resp, err); + if (err) wsapi.databaseDown(resp, err); else resp.json(emails); }); }; diff --git a/lib/wsapi/remove_email.js b/lib/wsapi/remove_email.js index fe7dc3c93265dfb2210628604ebe18b7b60a3b84..145adf03deab484899eb96fffacfb55b1d3b0d90 100644 --- a/lib/wsapi/remove_email.js +++ b/lib/wsapi/remove_email.js @@ -4,6 +4,7 @@ const db = require('../db.js'), +wsapi = require('../wsapi'), httputils = require('../httputils'), logger = require('../logging.js').logger; @@ -18,8 +19,12 @@ exports.process = function(req, res) { db.removeEmail(req.session.userid, email, function(error) { if (error) { - logger.error("error removing email " + email); - httputils.badRequest(res, error.toString()); + logger.warn("error removing email " + email); + if (error === 'database connection unavailable') { + wsapi.databaseDown(res, error); + } else { + httputils.badRequest(res, error.toString()); + } } else { res.json({ success: true }); }}); diff --git a/lib/wsapi/session_context.js b/lib/wsapi/session_context.js index 08a82cf01e0234808ab7923327bc6f6612087e8a..8b7f9e13d7a058f28192ae354cfe55b2a9e5b09b 100644 --- a/lib/wsapi/session_context.js +++ b/lib/wsapi/session_context.js @@ -59,8 +59,10 @@ exports.process = function(req, res) { logger.debug("user is not authenticated"); sendResponse(); } else { - db.userKnown(req.session.userid, function (known) { - if (!known) { + db.userKnown(req.session.userid, function (err, known) { + if (err) { + return wsapi.databaseDown(res, err); + } else if (!known) { logger.debug("user is authenticated with an account that doesn't exist in the database"); wsapi.clearAuthenticatedUser(req.session); } else { diff --git a/lib/wsapi/stage_email.js b/lib/wsapi/stage_email.js index c5b562304f4ca262e5eff7b2e66e025bb435b783..8acda357269408aacc0fb5741172dd9015c4709c 100644 --- a/lib/wsapi/stage_email.js +++ b/lib/wsapi/stage_email.js @@ -22,16 +22,20 @@ exports.args = ['email','site']; exports.i18n = true; exports.process = function(req, res) { - db.lastStaged(req.body.email, function (last) { + db.lastStaged(req.body.email, function (err, last) { + if (err) return wsapi.databaseDown(res, err); + if (last && (new Date() - last) < config.get('min_time_between_emails_ms')) { logger.warn('throttling request to stage email address ' + req.body.email + ', only ' + ((new Date() - last) / 1000.0) + "s elapsed"); - return httputils.forbidden(res, "throttling. try again later."); + return httputils.throttled(res, "Too many emails sent to that address, try again later."); } try { // on failure stageEmail may throw - db.stageEmail(req.session.userid, req.body.email, function(secret) { + db.stageEmail(req.session.userid, req.body.email, function(err, secret) { + if (err) return wsapi.databaseDown(res, err); + var langContext = wsapi.langContext(req); // store the email being added in session data diff --git a/lib/wsapi/stage_user.js b/lib/wsapi/stage_user.js index dc5f0aef04cb392a3e02de3f31de20f450bf2242..14bb947e148a270e4f5b25c7714a1d24f94114fd 100644 --- a/lib/wsapi/stage_user.js +++ b/lib/wsapi/stage_user.js @@ -27,17 +27,21 @@ exports.process = function(req, resp) { // staging a user logs you out. wsapi.clearAuthenticatedUser(req.session); - db.lastStaged(req.body.email, function (last) { + db.lastStaged(req.body.email, function (err, last) { + if (err) return wsapi.databaseDown(resp, err); + if (last && (new Date() - last) < config.get('min_time_between_emails_ms')) { logger.warn('throttling request to stage email address ' + req.body.email + ', only ' + ((new Date() - last) / 1000.0) + "s elapsed"); - return httputils.forbidden(resp, "throttling. try again later."); + return httputils.throttled(resp, "Too many emails sent to that address, try again later."); } try { // upon success, stage_user returns a secret (that'll get baked into a url // and given to the user), on failure it throws - db.stageUser(req.body.email, function(secret) { + db.stageUser(req.body.email, function(err, secret) { + if (err) return wsapi.databaseDown(resp, err); + // store the email being registered in the session data if (!req.session) req.session = {}; diff --git a/lib/wsapi/update_password.js b/lib/wsapi/update_password.js index e98a285b055ef56b747c8a7edb6743bffb41a495..d7a395c3a49a7cf4d8330bf30b703443ce0fe3de 100644 --- a/lib/wsapi/update_password.js +++ b/lib/wsapi/update_password.js @@ -24,7 +24,9 @@ exports.process = function(req, res) { }); } - db.checkAuth(req.session.userid, function(hash) { + db.checkAuth(req.session.userid, function(err, hash) { + if (err) return wsapi.databaseDown(res, err); + if (typeof hash !== 'string' || typeof req.body.oldpass !== 'string') { return res.json({ success: false }); @@ -62,9 +64,10 @@ exports.process = function(req, res) { var success = true; if (err) { logger.error("error updating bcrypted password for email " + req.body.email, err); - success = false; + wsapi.databaseDown(res, err); + } else { + res.json({ success: success }); } - return res.json({ success: success }); }); }); }); diff --git a/lib/wsapi/user_creation_status.js b/lib/wsapi/user_creation_status.js index e866430862a653f1a4bbd24c425d4db0d434079f..e6812c9c26bd722aef6f17f074b04bffaff1402c 100644 --- a/lib/wsapi/user_creation_status.js +++ b/lib/wsapi/user_creation_status.js @@ -17,8 +17,9 @@ exports.process = function(req, res) { // if the user is authenticated as the user in question, we're done if (wsapi.isAuthed(req, 'assertion')) { - db.userOwnsEmail(req.session.userid, email, function(owned) { - if (owned) res.json({ status: 'complete' }); + db.userOwnsEmail(req.session.userid, email, function(err, owned) { + if (err) wsapi.databaseDown(res, err); + else if (owned) res.json({ status: 'complete' }); else notAuthed(); }); } else { @@ -34,7 +35,9 @@ exports.process = function(req, res) { // if the secret is still in the database, it hasn't yet been verified and // verification is still pending - db.haveVerificationSecret(req.session.pendingCreation, function (known) { + db.haveVerificationSecret(req.session.pendingCreation, function (err, known) { + if (err) return wsapi.databaseDown(res, err); + if (known) return res.json({ status: 'pending' }); // if the secret isn't known, and we're not authenticated, then the user must authenticate // (maybe they verified the URL on a different browser, or maybe they canceled the account diff --git a/package.json b/package.json index 864d5d385db0721159015f52f142a8fee0baa0fc..a5fc6a2919e3bd6bb7b41adc813a623b16c337fa 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "convict": "0.0.6", "cjson": "0.0.6", "client-sessions": "0.0.3", + "connect-cachify": "0.0.8", "connect-cookie-session": "0.0.2", "connect-logger-statsd": "0.0.1", "ejs": "0.4.3", @@ -22,11 +23,12 @@ "node-statsd": "https://github.com/downloads/lloyd/node-statsd/3a73de.tgz", "nodemailer": "0.1.18", "optimist": "0.2.8", - "postprocess": "0.2.1", + "postprocess": "0.2.4", "semver": "1.0.12", "temp": "0.2.0", "uglify-js": "1.0.6", "uglifycss": "0.0.4", + "underscore": "1.3.1", "urlparse": "0.0.1", "winston": "0.5.6" }, diff --git a/resources/static/css/common.css b/resources/static/css/common.css index d0c57bd5a9c6d22f92006f3579a2c0e1353750a6..558d082f2b8ab8fb4acf974711df9f78466ed97a 100644 --- a/resources/static/css/common.css +++ b/resources/static/css/common.css @@ -21,7 +21,7 @@ body { font-size: 13px; line-height: 21px; background-image: url('/i/bg.png'); - overflow-y: scroll; + overflow-y: auto; } /* for floats */ @@ -146,7 +146,6 @@ button, font-family: 'Droid Serif', Georgia, serif; color: #fff; text-shadow: -1px -1px 0 #37A6FF; - text-transform: lowercase; cursor: pointer; -webkit-box-shadow: 0 0 0 1px #76C2FF inset; diff --git a/resources/static/css/m.css b/resources/static/css/m.css index c8004bb1d30b84a130f6c05e4d63c6dd2e37ac25..66274ef26d56b47b296906d4d9c91ba91a02b8c0 100644 --- a/resources/static/css/m.css +++ b/resources/static/css/m.css @@ -99,7 +99,7 @@ padding: 0 10px; font-size: 16px; line-height: 21px; - margin: 122px 0 122px; + margin: 0 0 90px; /* Add a bottom margin so the footer is never overlapped. */ } #signUp p { @@ -231,4 +231,8 @@ float: right; } + #newsbanner { + margin: 115px 0 28px 0; /* put a margin-top on so that it does not go under the header */ + + } } diff --git a/resources/static/css/style.css b/resources/static/css/style.css index 0dfb71a30495e00c7a74da99a02c8c5b1a1df328..7e42ec91215f19a5f1aafd04aed6b5032fa7e6f2 100644 --- a/resources/static/css/style.css +++ b/resources/static/css/style.css @@ -808,3 +808,18 @@ footer { bottom: 0; } +#newsbanner { + margin-top: 60px; /* put a margin-top on so that it does not go under the header */ + background-color: #faca33; + line-height: 32px; + border-radius: 4px; + margin-bottom: 20px; + text-align: center; + color: #626160; + text-shadow: 1px 1px 0 rgba(255,255,255,0.5); + -webkit-transition: all 500ms; + -moz-transition: all 500ms; + -ms-transition: all 500ms; + -o-transition: all 500ms; + transition: all 500ms; +} diff --git a/resources/static/dialog/controllers/actions.js b/resources/static/dialog/controllers/actions.js index bdc78df16ac55c6df3abe6f23d312c187ca83e25..320877daf875844026c6909919e5660447cc13c7 100644 --- a/resources/static/dialog/controllers/actions.js +++ b/resources/static/dialog/controllers/actions.js @@ -67,10 +67,6 @@ BrowserID.Modules.Actions = (function() { this.renderError(template, info); }, - doOffline: function() { - this.renderError("offline", {}); - }, - doCancel: function() { if(onsuccess) onsuccess(null); }, diff --git a/resources/static/dialog/controllers/add_email.js b/resources/static/dialog/controllers/add_email.js index 1ef8d01fb3622c74f955da684fddbd8db3cd0d9b..8711e69d1c9506c5478ae89a6e62fd312c8a31f5 100644 --- a/resources/static/dialog/controllers/add_email.js +++ b/resources/static/dialog/controllers/add_email.js @@ -9,7 +9,6 @@ BrowserID.Modules.AddEmail = (function() { var bid = BrowserID, helpers = bid.Helpers, dialogHelpers = helpers.Dialog, - cancelEvent = dialogHelpers.cancelEvent, errors = bid.Errors, complete = helpers.complete, tooltip = bid.Tooltip; @@ -37,7 +36,7 @@ BrowserID.Modules.AddEmail = (function() { self.renderDialog("add_email", options); - self.bind("#cancel", "click", cancelEvent(cancelAddEmail)); + self.click("#cancel", cancelAddEmail); Module.sc.start.call(self, options); }, submit: addEmail diff --git a/resources/static/dialog/controllers/authenticate.js b/resources/static/dialog/controllers/authenticate.js index c8f376d3de4ed5d928d04489c386b3885376adac..be2b793250f2de1a54e424bdbb3a2e2ac30d0ac3 100644 --- a/resources/static/dialog/controllers/authenticate.js +++ b/resources/static/dialog/controllers/authenticate.js @@ -14,7 +14,6 @@ BrowserID.Modules.Authenticate = (function() { tooltip = bid.Tooltip, helpers = bid.Helpers, dialogHelpers = helpers.Dialog, - cancelEvent = helpers.cancelEvent, complete = helpers.complete, dom = bid.DOM, lastEmail = "", @@ -62,6 +61,7 @@ BrowserID.Modules.Authenticate = (function() { } else { createSecondaryUserState.call(self); } + $('p.tospp').css('width', (240 - $('#signIn button:visible').outerWidth()) + 'px'); } } @@ -151,16 +151,19 @@ BrowserID.Modules.Authenticate = (function() { var self=this; self.renderDialog("authenticate", { sitename: user.getHostname(), - email: lastEmail + email: lastEmail, + privacy_url: options.privacyURL, + tos_url: options.tosURL }); $(".newuser,.forgot,.returning,.start").hide(); self.bind("#email", "keyup", emailKeyUp); - self.bind("#forgotPassword", "click", cancelEvent(forgotPassword)); + self.click("#forgotPassword", forgotPassword); Module.sc.start.call(self, options); initialState.call(self, options); + $('p.tospp').css('width', (240 - $('#signIn button:visible').outerWidth()) + 'px'); } // BEGIN TESTING API diff --git a/resources/static/dialog/controllers/check_registration.js b/resources/static/dialog/controllers/check_registration.js index 05ed707771575c346337b447980bb4b441c2a25b..4cf14f76f9b573111f7b15cf6a3f594583461f75 100644 --- a/resources/static/dialog/controllers/check_registration.js +++ b/resources/static/dialog/controllers/check_registration.js @@ -22,8 +22,8 @@ BrowserID.Modules.CheckRegistration = (function() { self.verifier = options.verifier; self.verificationMessage = options.verificationMessage; - self.bind("#back", "click", self.back); - self.bind("#cancel", "click", self.cancel); + self.click("#back", self.back); + self.click("#cancel", self.cancel); Module.sc.start.call(self, options); }, diff --git a/resources/static/dialog/controllers/dialog.js b/resources/static/dialog/controllers/dialog.js index 7192887df535bad47060c338a62ee6acefc9c327..9d5c2693bbeeacea1e703d51b0403967feff70b3 100644 --- a/resources/static/dialog/controllers/dialog.js +++ b/resources/static/dialog/controllers/dialog.js @@ -16,15 +16,6 @@ BrowserID.Modules.Dialog = (function() { channel, sc; - function checkOnline() { - if (false && 'onLine' in navigator && !navigator.onLine) { - this.publish("offline"); - return false; - } - - return true; - } - function startActions(onsuccess, onerror) { var actions = BrowserID.Modules.Actions.create(); actions.start({ @@ -90,6 +81,14 @@ BrowserID.Modules.Dialog = (function() { this.publish("window_unload"); } + function fixupURL(origin, url) { + var u; + if (/^http/.test(url)) u = URLParse(url); + else if (/^\//.test(url)) u = URLParse(origin + url); + else throw "relative urls not allowed: (" + url + ")"; + return u.validate().normalize().toString(); + } + var Dialog = bid.Modules.PageModule.extend({ start: function(options) { var self=this; @@ -121,35 +120,41 @@ BrowserID.Modules.Dialog = (function() { var actions = startActions.call(self, success, error); startStateMachine.call(self, actions); - if(checkOnline.call(self)) { - params = params || {}; - - params.hostname = user.getHostname(); - - // XXX Perhaps put this into the state machine. - self.bind(win, "unload", onWindowUnload); - - if(hash.indexOf("#CREATE_EMAIL=") === 0) { - var email = hash.replace(/#CREATE_EMAIL=/, ""); - params.type = "primary"; - params.email = email; - params.add = false; - } - else if(hash.indexOf("#ADD_EMAIL=") === 0) { - var email = hash.replace(/#ADD_EMAIL=/, ""); - params.type = "primary"; - params.email = email; - params.add = true; + params = params || {}; + params.hostname = user.getHostname(); + + // verify params + if (params.tosURL && params.privacyURL) { + try { + params.tosURL = fixupURL(origin_url, params.tosURL); + params.privacyURL = fixupURL(origin_url, params.privacyURL); + } catch(e) { + return self.renderError("error", { + action: { + title: "error in " + origin_url, + message: "improper usage of API: " + e + } + }); } + } - /* - if(hash.indexOf("REQUIRED=true") > -1) { - params.requiredEmail = params.email; - } - */ + // XXX Perhaps put this into the state machine. + self.bind(win, "unload", onWindowUnload); - self.publish("start", params); + if(hash.indexOf("#CREATE_EMAIL=") === 0) { + var email = hash.replace(/#CREATE_EMAIL=/, ""); + params.type = "primary"; + params.email = email; + params.add = false; + } + else if(hash.indexOf("#ADD_EMAIL=") === 0) { + var email = hash.replace(/#ADD_EMAIL=/, ""); + params.type = "primary"; + params.email = email; + params.add = true; } + + self.publish("start", params); } // BEGIN TESTING API diff --git a/resources/static/dialog/controllers/forgot_password.js b/resources/static/dialog/controllers/forgot_password.js index 69ae248a5ada141e4146cd1160189944def76f81..268f72417b625a66a07ade614a8a1b8d26ceb8e2 100644 --- a/resources/static/dialog/controllers/forgot_password.js +++ b/resources/static/dialog/controllers/forgot_password.js @@ -10,7 +10,6 @@ BrowserID.Modules.ForgotPassword = (function() { bid = BrowserID, helpers = bid.Helpers, dialogHelpers = helpers.Dialog, - cancelEvent = dialogHelpers.cancelEvent, dom = bid.DOM; function resetPassword() { @@ -31,7 +30,7 @@ BrowserID.Modules.ForgotPassword = (function() { requiredEmail: options.requiredEmail }); - self.bind("#cancel", "click", cancelEvent(cancelResetPassword)); + self.click("#cancel", cancelResetPassword); Module.sc.start.call(self, options); }, diff --git a/resources/static/dialog/controllers/pick_email.js b/resources/static/dialog/controllers/pick_email.js index ce39bd9fde150e60c7595c6f1056a8f3c75fd969..f268c105f475a89abaeacda3c9aad7b0dd3c2676 100644 --- a/resources/static/dialog/controllers/pick_email.js +++ b/resources/static/dialog/controllers/pick_email.js @@ -11,7 +11,6 @@ BrowserID.Modules.PickEmail = (function() { errors = bid.Errors, storage = bid.Storage, helpers = bid.Helpers, - cancelEvent = helpers.cancelEvent, dialogHelpers = helpers.Dialog, dom = bid.DOM, sc; @@ -65,6 +64,25 @@ BrowserID.Modules.PickEmail = (function() { return identities; } + function proxyEventToInput(event) { + // iOS will not select a radio/checkbox button if the user clicks on the + // corresponding label. Because of this, if the user clicks on the label, + // an even is manually fired on the the radio button. This only applies + // if the user clicks on the actual label, not on any input elements + // contained within the label. This restriction is necessary or else we + // would be in a never ending loop that would continually toggle the state + // of any check boxes. + if(dom.is(event.target, "label")) { + // Must prevent standard acting browsers from taking care of the click or + // else it acts like two consecutive clicks. For radio buttons this will + // just toggle state. + event.preventDefault(); + + var target = dom.getAttr(event.target, "for"); + dom.fireEvent("#" + target, event.type); + } + } + var Module = bid.Modules.PageModule.extend({ start: function(options) { var origin = user.getOrigin(), @@ -79,10 +97,12 @@ BrowserID.Modules.PickEmail = (function() { identities: getSortedIdentities(), siteemail: storage.site.get(origin, "email"), allow_persistent: options.allow_persistent || false, - remember: storage.site.get(origin, "remember") || false + remember: storage.site.get(origin, "remember") || false, + privacy_url: options.privacyURL, + tos_url: options.tosURL }); dom.getElements("body").css("opacity", "1"); - + $('p.tospp').css('width', (240 - $('#signIn button:visible').outerWidth()) + 'px'); if (dom.getElements("#selectEmail input[type=radio]:visible").length === 0) { // If there is only one email address, the radio button is never shown, // instead focus the sign in button so that the user can click enter. @@ -90,7 +110,11 @@ BrowserID.Modules.PickEmail = (function() { dom.focus("#signInButton"); } - self.bind("#useNewEmail", "click", cancelEvent(addEmail)); + self.click("#useNewEmail", addEmail); + // The click function does not pass the event to the function. The event + // is needed for the label handler so that the correct radio button is + // selected. + self.bind("#selectEmail label", "click", proxyEventToInput); sc.start.call(self, options); diff --git a/resources/static/dialog/controllers/required_email.js b/resources/static/dialog/controllers/required_email.js index 205c1df7650415dfcbfe2e91e1cad0a6a8aa4dd6..788a1f2dcd4e0cd8f9822fd926f606d32d2d9708 100644 --- a/resources/static/dialog/controllers/required_email.js +++ b/resources/static/dialog/controllers/required_email.js @@ -14,7 +14,6 @@ BrowserID.Modules.RequiredEmail = (function() { dialogHelpers = helpers.Dialog, dom = bid.DOM, assertion, - cancelEvent = dialogHelpers.cancelEvent, email, auth_level, primaryInfo, @@ -117,13 +116,16 @@ BrowserID.Modules.RequiredEmail = (function() { // a user could not be looking at stale data and/or authenticate as // somebody else. var emailInfo = user.getStoredEmailKeypair(email); + //alert(auth_level + ' ' + JSON.stringify(emailInfo) + JSON.stringify(options)); if(emailInfo && emailInfo.type === "secondary") { // secondary user, show the password field if they are not // authenticated to the "password" level. showTemplate({ signin: true, password: auth_level !== "password", - secondary_auth: secondaryAuth + secondary_auth: secondaryAuth, + privacy_url: options.privacyURL, + tos_url: options.tosURL }); ready(); } @@ -160,7 +162,9 @@ BrowserID.Modules.RequiredEmail = (function() { // user is authenticated, but does not control address // OR // address is unknown, make the user verify. - showTemplate({ verify: true }); + showTemplate({ verify: true, + privacy_url: options.privacyURL, + tos_url: options.tosURL }); } else { // We've made it all this way. It is a user who is not logged in @@ -179,14 +183,19 @@ BrowserID.Modules.RequiredEmail = (function() { signin: false, password: false, secondary_auth: false, - primary: false + primary: false, + privacy_url: undefined, + tos_url: undefined }, options); + self.renderDialog("required_email", options); - self.bind("#sign_in", "click", cancelEvent(signIn)); - self.bind("#verify_address", "click", cancelEvent(verifyAddress)); - self.bind("#forgotPassword", "click", cancelEvent(forgotPassword)); - self.bind("#cancel", "click", cancelEvent(cancel)); + self.click("#sign_in", signIn); + self.click("#verify_address", verifyAddress); + self.click("#forgotPassword", forgotPassword); + self.click("#cancel", cancel); + + $('p.tospp').css('width', (240 - $('#signIn button:visible').outerWidth()) + 'px'); } RequiredEmail.sc.start.call(self, options); diff --git a/resources/static/dialog/controllers/verify_primary_user.js b/resources/static/dialog/controllers/verify_primary_user.js index 5721e82c03224f117b5e0c687e6cdd6203f38403..3be01935aae8f5984e94d4da7a275c37b7ee53f2 100644 --- a/resources/static/dialog/controllers/verify_primary_user.js +++ b/resources/static/dialog/controllers/verify_primary_user.js @@ -13,8 +13,7 @@ BrowserID.Modules.VerifyPrimaryUser = (function() { email, auth_url, helpers = bid.Helpers, - complete = helpers.complete, - cancelEvent = helpers.Dialog.cancelEvent; + complete = helpers.complete; function verify(callback) { this.publish("primary_user_authenticating"); @@ -51,7 +50,7 @@ BrowserID.Modules.VerifyPrimaryUser = (function() { data.requiredEmail = data.requiredEmail || false; self.renderDialog("verify_primary_user", data); - self.bind("#cancel", "click", cancelEvent(cancel)); + self.click("#cancel", cancel); sc.start.call(self, data); }, diff --git a/resources/static/dialog/css/m.css b/resources/static/dialog/css/m.css index fafc2636fb6a81a21d12c0d6b9fb6beb7cee8f9d..909148297701d7c36b41dc8f9572f310e95f84fe 100644 --- a/resources/static/dialog/css/m.css +++ b/resources/static/dialog/css/m.css @@ -51,13 +51,11 @@ } #signIn { - max-width: none; padding: 10px; } - #signIn .table { + #signIn .container { width: 100%; - margin: 0; } #signIn form { @@ -101,13 +99,18 @@ } #signIn .vertical { - padding-bottom: 0; + padding: 10px; } #signIn .vertical ul li { margin-top: 20px; } + #selectEmail > .inputs > li > label { + margin: 0; + padding: 15px 1px; + } + #signIn .submit { position: static; line-height: 40px; @@ -150,20 +153,13 @@ height: 250px; } - #error .vertical, - #error.unsupported .vertical { + #error .vertical { width: auto; } - #error .vertical > div, - #error.unsupported .vertical > div { + #error .vertical > div { display: block; height: auto; padding: 10px; } - #error #borderbox { - border-left: none; - padding: 0; - } - diff --git a/resources/static/dialog/css/popup.css b/resources/static/dialog/css/popup.css index 600016f1f56b390339468600baea1e6c90e377b9..db0e7d8d72efd340242483bae44f9d59369448fb 100644 --- a/resources/static/dialog/css/popup.css +++ b/resources/static/dialog/css/popup.css @@ -18,22 +18,26 @@ h2 { } +.vertical { + height: 250px; +} + .table { display: table; width: 100%; } -.vertical { - height: 250px; +.table .vertical { display: table-cell; vertical-align: middle; - width: 100%; } #content { position: relative; height: 250px; overflow: hidden; + /* Fix for IE6 not displaying the unsupported dialog correctly */ + _width: 100%; } section { @@ -83,14 +87,16 @@ section > .contents { background-image: url("/i/bg.png"); } + .waiting #wait { z-index: 1; -ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=100)"; opacity: 1; } -.error #error { +.error #error, #error.unsupported, #error.cookies_disabled { z-index: 3; + -ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=100)"; opacity: 1; } @@ -102,33 +108,36 @@ section > .contents { -#error.unsupported .vertical { - width: 630px; - margin: 0 auto; - display: block; +#error.unsupported { + padding: 20px 0; } - -#error.unsupported .vertical > div { - display: table-cell; - vertical-align: middle; - padding: 0 10px; - height: 250px; +.unsupported { + /* + * These are fixes for IE6 - IE6 does not support the combination #id.class + * selector, so we have to use just the class, and then prepend the css + * attributes with _ so only IE6 renders them. + */ + _padding: 20px 0; + _width: 100%; + _height: 100%; } -#error #borderbox { - border-left: 1px solid #777; - padding: 20px 0; +#error.unsupported h2 { + margin: 0 0 20px; } -#error #borderbox img { +#error img { border: none; } -#error #alternative .lighter { +#error .lighter { color: #777; } +#wait .vertical, #error .vertical, #delay .vertical { + padding: 0 20px; +} #formWrap { background-color: #fff; @@ -143,9 +152,12 @@ section > .contents { top: 0; } -#signIn .table { +#signIn .container { + /** + * Set the width of the container for when the arrow animation happens + * otherwise the buttons slide right with the arrow + */ width: 325px; - margin-right: 40px; } .arrow { @@ -193,12 +205,12 @@ div#required_email { } #signIn .vertical { - padding: 0 20px; + padding: 20px 52px 20px 20px; + position: relative; } #signIn .vertical ul { list-style-type: none; - position: relative; } #signIn .vertical ul li { @@ -212,9 +224,13 @@ div#required_email { #signIn .submit { line-height: 28px; position: absolute; - bottom: 0; + bottom: 20px; left: 0; - right: 0; + right: 52px; +} + +#signIn .submit { + margin-left: 20px; } #signIn .submit > p { @@ -255,6 +271,17 @@ label.selectable { .inputs > li > label { color: #333; + overflow: hidden; + text-overflow: ellipsis; +} + +#signIn #selectEmail > .inputs > li { + margin: 0; +} + +#selectEmail > .inputs > li > label { + padding: 5px 1px; + white-space: nowrap; } .inputs > li > label.preselected { @@ -272,6 +299,21 @@ label.selectable { text-shadow: 1px 1px 0 rgba(255,255,255,0.5); } +#signIn .submit > p.tospp { + /* width comes from controller/<page>.js p.tospp.css('width') update */ + bottom: 0px; + color: #333; + font-size: 11px; + line-height: 1.2; + position: absolute; + text-align: justify; + left; 0px; +} + +.tospp a { + color: #549FDC; +} + footer .learn a { color: #549FDC; } @@ -319,20 +361,19 @@ footer { .inputs { margin: 1em 0 .5em; - padding: 0 1em; line-height: 18px; max-height: 130px; overflow-y: auto; } -.pickemail .inputs { - position: relative; +/* Some languages have long text for the "sign in" and "use a different email" + * buttons. If the user >= 6 emails in these languages, the buttons overlap. + * This shrinks the email address box by one address to prevent this overlap. + */ +#selectEmail .inputs { + max-height: 115px; } -.form_section { - height: 176px; - position: relative; -} .add { font-size: 80%; @@ -343,19 +384,14 @@ footer { } label[for=remember] { - display: inline; - margin-left: 13px; + display: inline-block; + margin-bottom: 10px; } #thisIsNotMe { - margin-right: 10px; float: right; } -#useNewEmail { - margin-left: 0.8em; -} - a.emphasize { background-color: #F0EFED; color: #4E4E4E; @@ -366,7 +402,7 @@ a.emphasize { } .submit > button { - margin: 0 5px 0 0; + margin: 0 0 0 5px; } #newEmail { @@ -386,4 +422,3 @@ a.emphasize { #checkemail { text-align: center; } - diff --git a/resources/static/dialog/resources/state.js b/resources/static/dialog/resources/state.js index 22a3098e78db35eba06155414b2251cf0a8f3f13..2b6a0fceb26b214bfb96f1109ca9151ad43dce48 100644 --- a/resources/static/dialog/resources/state.js +++ b/resources/static/dialog/resources/state.js @@ -32,15 +32,13 @@ BrowserID.State = (function() { }, cancelState = self.popState.bind(self); - subscribe("offline", function(msg, info) { - startState("doOffline"); - }); - subscribe("start", function(msg, info) { info = info || {}; self.hostname = info.hostname; self.allowPersistent = !!info.allowPersistent; + self.privacyURL = info.privacyURL; + self.tosURL = info.tosURL; requiredEmail = info.requiredEmail; if ((typeof(requiredEmail) !== "undefined") && (!bid.verifyEmail(requiredEmail))) { @@ -72,7 +70,9 @@ BrowserID.State = (function() { if (requiredEmail) { startState("doAuthenticateWithRequiredEmail", { - email: requiredEmail + email: requiredEmail, + privacyURL: self.privacyURL, + tosURL: self.tosURL }); } else if (authenticated) { @@ -83,6 +83,9 @@ BrowserID.State = (function() { }); subscribe("authenticate", function(msg, info) { + info = info || {}; + info.privacyURL = self.privacyURL; + info.tosURL = self.tosURL; startState("doAuthenticate", info); }); @@ -131,7 +134,7 @@ BrowserID.State = (function() { else if(info.add) { // Add the pick_email in case the user cancels the add_email screen. // The user needs something to go "back" to. - publish("pick_email", info); + publish("pick_email"); publish("add_email", info); } else { @@ -157,11 +160,15 @@ BrowserID.State = (function() { subscribe("pick_email", function() { startState("doPickEmail", { origin: self.hostname, - allow_persistent: self.allowPersistent + allow_persistent: self.allowPersistent, + privacyURL: self.privacyURL, + tosURL: self.tosURL }); }); subscribe("email_chosen", function(msg, info) { + info = info || {}; + var email = info.email, idInfo = storage.getEmail(email); @@ -190,7 +197,9 @@ BrowserID.State = (function() { // screen. startState("doAuthenticateWithRequiredEmail", { email: email, - secondary_auth: true + secondary_auth: true, + privacyURL: self.privacyURL, + tosURL: self.tosURL }); } else { @@ -214,7 +223,7 @@ BrowserID.State = (function() { }); subscribe("authenticated", function(msg, info) { - publish("pick_email"); + publish("email_chosen", info); }); subscribe("forgot_password", function(msg, info) { @@ -234,7 +243,7 @@ BrowserID.State = (function() { startState("doAssertionGenerated", info.assertion); } else { - startState("doPickEmail"); + publish("pick_email"); } }); diff --git a/resources/static/dialog/views/authenticate.ejs b/resources/static/dialog/views/authenticate.ejs index 7e08dedb9834d0eb6ae564abd7870b173404efe7..c23560dba67c1e4da420b898ed806de99596cc9b 100644 --- a/resources/static/dialog/views/authenticate.ejs +++ b/resources/static/dialog/views/authenticate.ejs @@ -32,7 +32,7 @@ <li id="create_text_section" class="newuser"> <p><strong><%= gettext('Welcome to BrowserID!') %></strong></p> - <p><%= gettext('This email looks new, so let's get you set up.') %></p> + <p><%= gettext("This email looks new, so let's get you set up.") %></p> </li> <li class="returning"> @@ -55,9 +55,19 @@ </ul> <div class="submit cf"> + <% if (privacy_url && tos_url) { %> + <p class="tospp"> + <%= format( + gettext('By clicking %s, you confirm that you accept this site\'s <a %s>Terms of Use</a> and <a %s>Privacy Policy</a>.'), + [ gettext('next'), + format(' href="%s" target="_new"', [tos_url]), + format(' href="%s" target="_new"', [privacy_url]) + ]) %> + </p> + <% } %> <button class="start" tabindex="3"><%= gettext('next') %></button> <button class="newuser" tabindex="3"><%= gettext('verify email') %></button> - <button class="returning" tabindex="3"><%= gettext('select email') %></button> + <button class="returning" tabindex="3"><%= gettext('sign in') %></button> </div> </div> diff --git a/resources/static/dialog/views/error.ejs b/resources/static/dialog/views/error.ejs index b21fa0afbe96c3ac1ee37e6ab2caaefeca04c8bd..4a61ab7092777177336739789d7c521f6950e660 100644 --- a/resources/static/dialog/views/error.ejs +++ b/resources/static/dialog/views/error.ejs @@ -7,24 +7,33 @@ <h2 id="error_503"> <%= gettext("We are very sorry, the server is under extreme load!") %> </h2> + <% } else if (typeof network !== "undefined" && network.status == 403) { %> + <h2 id="error_403"> + <%= gettext("BrowserID 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 { %> <h2 id="defaultError"> <%= gettext("We are very sorry, there has been an error!") %> </h2> <% } %> - <p> - <% if (typeof dialog !== "undefined" && dialog !== false) { %> - <%= gettext("To retry, you will have to reload the page and try again.") %> - <% } else { %> - <%= gettext("To retry, you will have to close this window and try again.") %> + <% if (!(typeof network !== "undefined" && network.status == 403)) { %> + <p> + <% if (typeof dialog !== "undefined" && dialog !== false) { %> + <%= gettext("Please reload the page and try again.") %> + <% } else { %> + <%= gettext("Please close this window and try again.") %> + <% } %> + </p> <% } %> - </p> <% if(typeof action !== "undefined" || typeof network !== "undefined") { %> - <a href="#" id="openMoreInfo"> - <%= gettext("See more info") %> - </a> + <p> + <a href="#" id="openMoreInfo"> + <%= gettext("See more info") %> + </a> + </p> <ul id="moreInfo"> <% if (typeof action !== "undefined") { %> @@ -45,7 +54,7 @@ <strong id="network">Network Info:</strong> <%= network.type %>: <%= network.url %> <p> - <strong>Response Code - </strong> <%= network.textStatus %> + <strong>Response Code - </strong> <%= network.status %> </p> <% if (network.responseText) { %> diff --git a/resources/static/dialog/views/forgot_password.ejs b/resources/static/dialog/views/forgot_password.ejs index 68b0d5749adbcdcc0c616e1d4bba5bd848fcb9fb..7c7a21f0450def891bd846a36fabde65e2ca174d 100644 --- a/resources/static/dialog/views/forgot_password.ejs +++ b/resources/static/dialog/views/forgot_password.ejs @@ -24,7 +24,7 @@ </ul> <div class="submit cf"> - <button tabindex="1"><%= gettext('Reset Password') %></button> - <a href="#" id="cancel" tabindex="2"><%= gettext('Cancel') %></a> + <button tabindex="1"><%= gettext('reset password') %></button> + <a href="#" id="cancel" tabindex="2"><%= gettext('cancel') %></a> </div> </div> diff --git a/resources/static/dialog/views/offline.ejs b/resources/static/dialog/views/offline.ejs deleted file mode 100644 index 942e98958271b64dd78554a3fe6d2295dde58362..0000000000000000000000000000000000000000 --- a/resources/static/dialog/views/offline.ejs +++ /dev/null @@ -1,12 +0,0 @@ -<!-- 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/. --> - - - <h2 id="offline"><%= gettext('You are offline!') %></h2> - - <p> - <%= gettext('We are sorry, but we cannot communicate with BrowserID while you are offline.') %> - </p> - - diff --git a/resources/static/dialog/views/pick_email.ejs b/resources/static/dialog/views/pick_email.ejs index 95a10c155209a67457c058121d7914bdaadb669c..bd14335643b521591ac68e93b966f797338fab01 100644 --- a/resources/static/dialog/views/pick_email.ejs +++ b/resources/static/dialog/views/pick_email.ejs @@ -9,11 +9,10 @@ <ul class="inputs"> <% _.each(identities, function(item) { var emailAddress = item.address; var cleanedEmail = emailAddress.replace("@","_").replace(".", "_"); %> <li> - - <label for="<%= cleanedEmail %>" class="serif<% if (emailAddress === siteemail) { %> preselected<% } %> selectable"> + <label for="<%= cleanedEmail %>" class="serif<% if (emailAddress === siteemail) { %> preselected<% } %> selectable" title="<%= emailAddress %>"> <input type="radio" name="email" id="<%= cleanedEmail %>" value="<%= emailAddress %>" - <% if (emailAddress === siteemail) { %> checked="checked" <% } %> - /> + <% if (emailAddress === siteemail) { %> checked="checked" <% } %> + /> <%= emailAddress %> </label> </li> @@ -22,18 +21,31 @@ <a id="useNewEmail" class="emphasize" href="#"><%= gettext('Use a different email') %></a> <div class="submit add cf"> + <% if (allow_persistent || (privacy_url && tos_url)) { %> + <p class="tospp"> + <% } %> - <% if (allow_persistent) { %> +<% if (allow_persistent) { %> <label for="remember" class="selectable"> <input type="checkbox" id="remember" name="remember" <% if (remember) { %> checked="checked" <% } %> /> <%= gettext('Always sign in using this email') %> </label> <% } %> + <% if (privacy_url && tos_url) { %> +<%= format( + gettext('By clicking %s, you confirm that you accept this site\'s <a %s>Terms of Use</a> and <a %s>Privacy Policy</a>.'), + [ gettext('sign in'), + format(' href="%s" target="_new"', [tos_url]), + format(' href="%s" target="_new"', [privacy_url]) + ]) %> + <% } %> + + <% if (allow_persistent || (privacy_url && tos_url)) { %> + </p> + <% } %> <button id="signInButton"><%= gettext('sign in') %></button> - - <p> - </p> + <br style="clear: both" /> </div> </div> diff --git a/resources/static/dialog/views/required_email.ejs b/resources/static/dialog/views/required_email.ejs index 2f8cf5472ac565e6c724d6695ad9235a32bef697..f80c47e95f42644797e682b0022a9c069cc5c52f 100644 --- a/resources/static/dialog/views/required_email.ejs +++ b/resources/static/dialog/views/required_email.ejs @@ -52,6 +52,16 @@ </ul> <div class="submit cf"> + <% if (privacy_url && tos_url) { %> + <p class="tospp"> + <%= format( + gettext('By clicking %s, you confirm that you accept this site\'s <a %s>Terms of Use</a> and <a %s>Privacy Policy</a>.'), + [ gettext('sign in'), + format(' href="%s" target="_new"', [tos_url]), + format(' href="%s" target="_new"', [privacy_url]) + ]) %> + </p> + <% } %> <% if (signin) { %> <button id="sign_in" tabindex="3"><%= gettext("sign in") %></button> <% } else if (verify) { %> diff --git a/resources/static/dialog/views/verify_primary_user.ejs b/resources/static/dialog/views/verify_primary_user.ejs index ea43a338118f5409606a1296c3e32299830d74dc..8fde3e6cd650e30ca85419dd64f2734173ac9421 100644 --- a/resources/static/dialog/views/verify_primary_user.ejs +++ b/resources/static/dialog/views/verify_primary_user.ejs @@ -24,7 +24,7 @@ </ul> <div class="submit cf"> - <button id="VerifyWithPrimary"><%= gettext("Verify") %></button> + <button id="verifyWithPrimary"><%= gettext("verify") %></button> </div> </div> @@ -38,8 +38,8 @@ </p> <div class="submit cf"> - <button id="verifyWithPrimary"><%= gettext("Verify") %></button> - <a href="#" id="cancel"><%= gettext("Cancel") %></a> + <button id="verifyWithPrimary"><%= gettext("verify") %></button> + <a href="#" id="cancel"><%= gettext("cancel") %></a> </div> </div> diff --git a/resources/static/include_js/include.js b/resources/static/include_js/include.js index e835b8bd2047671cac878f414e966d300b462b18..89fef47ee479e6352bff774465fe3fb99b370beb 100644 --- a/resources/static/include_js/include.js +++ b/resources/static/include_js/include.js @@ -354,7 +354,7 @@ } }, timeout); } - + var onMessage = function(origin, method, m) { // if an observer was specified at allocation time, invoke it if (typeof cfg.gotMessageObserver === 'function') { @@ -656,7 +656,10 @@ // checking Mobile Firefox (Fennec) function isFennec() { try { - return (navigator.userAgent.indexOf('Fennec/') != -1); + // We must check for both XUL and Java versions of Fennec. Both have + // distinct UA strings. + return (userAgent.indexOf('Fennec/') != -1) || // XUL + (userAgent.indexOf('Firefox/') != -1 && userAgent.indexOf('Android') != -1); // Java } catch(e) {}; return false; } @@ -831,7 +834,7 @@ ieNosupport = ieVersion > -1 && ieVersion < 8; if(ieNosupport) { - return "IE_VERSION"; + return "BAD_IE_VERSION"; } } @@ -840,30 +843,55 @@ } function checkLocalStorage() { - var localStorage = 'localStorage' in win && win['localStorage'] !== null; - if(!localStorage) { - return "LOCALSTORAGE"; + // Firefox/Fennec/Chrome blow up when trying to access or + // write to localStorage. We must do two explicit checks, first + // whether the browser has localStorage. Second, we must check + // whether the localStorage can be written to. Firefox (at v11) + // throws an exception when querying win['localStorage'] + // when cookies are disabled. Chrome (v17) excepts when trying to + // write to localStorage when cookies are disabled. If an + // exception is thrown, then localStorage is disabled. If no + // exception is thrown, hasLocalStorage will be true if the + // browser supports localStorage and it can be written to. + try { + var hasLocalStorage = 'localStorage' in win + // Firefox will except here if cookies are disabled. + && win['localStorage'] !== null; + + if(hasLocalStorage) { + // browser has localStorage, check if it can be written to. If + // cookies are disabled, some browsers (Chrome) will except here. + win['localStorage'].setItem("test", "true"); + win['localStorage'].removeItem("test"); + } + else { + // Browser does not have local storage. + return "LOCALSTORAGE_NOT_SUPPORTED"; + } + } catch(e) { + return "LOCALSTORAGE_DISABLED"; } } function checkPostMessage() { if(!win.postMessage) { - return "POSTMESSAGE"; + return "POSTMESSAGE_NOT_SUPPORTED"; } } function checkJSON() { if(!(window.JSON && window.JSON.stringify && window.JSON.parse)) { - return "JSON"; + return "JSON_NOT_SUPPORTED"; } } function isSupported() { - reason = checkLocalStorage() || checkPostMessage() || checkJSON() || explicitNosupport(); + reason = explicitNosupport() || checkLocalStorage() || checkPostMessage() || checkJSON(); return !reason; } + function getNoSupportReason() { return reason; } @@ -911,10 +939,15 @@ if (!navigator.id.getVerifiedEmail || navigator.id._getVerifiedEmailIsShimmed) { var ipServer = "https://browserid.org"; - var isFennec = navigator.userAgent.indexOf('Fennec/') != -1; + var userAgent = navigator.userAgent; + // We must check for both XUL and Java versions of Fennec. Both have + // distinct UA strings. + var isFennec = (userAgent.indexOf('Fennec/') != -1) || // XUL + (userAgent.indexOf('Firefox/') != -1 && userAgent.indexOf('Android') != -1); // Java + var windowOpenOpts = (isFennec ? undefined : - "menubar=0,location=1,resizable=0,scrollbars=0,status=0,dialog=1,width=700,height=375"); + "menubar=0,location=1,resizable=1,scrollbars=1,status=0,dialog=1,width=700,height=375"); var w; @@ -942,8 +975,15 @@ } if (!BrowserSupport.isSupported()) { + var reason = BrowserSupport.getNoSupportReason(), + url = "unsupported_dialog"; + + if(reason === "LOCALSTORAGE_DISABLED") { + url = "cookies_disabled"; + } + w = window.open( - ipServer + "/unsupported_dialog", + ipServer + "/" + url, null, windowOpenOpts); return; diff --git a/resources/static/lib/dom-jquery.js b/resources/static/lib/dom-jquery.js index 889d165bf42ce1080d6326e6559431ba148a0d18..860c033277fa2579044a22216426b7ac88bbd8eb 100644 --- a/resources/static/lib/dom-jquery.js +++ b/resources/static/lib/dom-jquery.js @@ -296,6 +296,20 @@ BrowserID.DOM = ( function() { */ focus: function( elementToFocus ) { jQuery( elementToFocus ).focus(); + }, + + /** + * Check the current matched set of elements against + * a selector or element and return true if at least + * one of these elements matches the given arguments. + * @method is + * @param {selector || element} elementToCheck + * @param {string} type + * @returns {boolean} true if elementToCheck matches the specified + * type, false otw. + */ + is: function( elementToCheck, type ) { + return jQuery( elementToCheck ).is( type ); } diff --git a/resources/static/lib/ejs.js b/resources/static/lib/ejs.js index 49d95252400213f7c1f2105344541485ebcb50aa..31a9df53fb1b958f4059a2d33c1fa300d61e4cab 100644 --- a/resources/static/lib/ejs.js +++ b/resources/static/lib/ejs.js @@ -1,5 +1,5 @@ (function(){ - + var rsplit = function(string, regex) { var result = regex.exec(string),retArr = new Array(), first_idx, last_idx, first_bit; @@ -11,10 +11,10 @@ var rsplit = function(string, regex) { first_bit = string.substring(0,first_idx); retArr.push(string.substring(0,first_idx)); string = string.slice(first_idx); - } + } retArr.push(result[0]); string = string.slice(result[0].length); - result = regex.exec(string); + result = regex.exec(string); } if (! string == '') { @@ -32,7 +32,7 @@ extend = function(d, s){ } -EJS = function( options ){ +window.EJS = function( options ){ options = typeof options == "string" ? {view: options} : options this.set_options(options); if(options.precompiled){ @@ -76,7 +76,7 @@ EJS = function( options ){ template.compile(options, this.name); - + EJS.update(this.name, this); this.template = template; }; @@ -145,7 +145,7 @@ EJS.endExt = function(path, match){ /* @Static*/ EJS.Scanner = function(source, left, right) { - + extend(this, {left_delimiter: left +'%', right_delimiter: '%'+right, @@ -155,7 +155,7 @@ EJS.Scanner = function(source, left, right) { left_comment: left+'%#'}) this.SplitRegexp = left=='[' ? /(\[%%)|(%%\])|(\[%=)|(\[%#)|(\[%)|(%\]\n)|(%\])|(\n)/ : new RegExp('('+this.double_left+')|(%%'+this.double_right+')|('+this.left_equal+')|('+this.left_comment+')|('+this.left_delimiter+')|('+this.right_delimiter+'\n)|('+this.right_delimiter+')|(\n)') ; - + this.source = source; this.stag = null; this.lines = 0; @@ -166,15 +166,15 @@ EJS.Scanner.to_text = function(input){ return ''; if(input instanceof Date) return input.toDateString(); - if(input.toString) + if(input.toString) return input.toString(); return ''; }; EJS.Scanner.prototype = { scan: function(block) { - scanline = this.scanline; - regex = this.SplitRegexp; + var scanline = this.scanline, + regex = this.SplitRegexp; if (! this.source == '') { var source_split = rsplit(this.source, /\n/); @@ -212,7 +212,7 @@ EJS.Buffer = function(pre_cmd, post_cmd) { } }; EJS.Buffer.prototype = { - + push: function(cmd) { this.line.push(cmd); }, @@ -230,17 +230,17 @@ EJS.Buffer.prototype = { this.push(pre_cmd[i]); } this.script = this.script + this.line.join('; '); - line = null; + this.line = null; } } - + }; EJS.Compiler = function(source, left) { this.pre_cmd = ['var ___ViewO = [];']; this.post_cmd = new Array(); - this.source = ' '; + this.source = ' '; if (source != null) { if (typeof source == 'string') @@ -250,7 +250,7 @@ EJS.Compiler = function(source, left) { this.source = source; }else if (source.innerHTML){ this.source = source.innerHTML; - } + } if (typeof this.source != 'string'){ this.source = ""; } @@ -276,7 +276,7 @@ EJS.Compiler.prototype = { this.out = ''; var put_cmd = "___ViewO.push("; var insert_cmd = put_cmd; - var buff = new EJS.Buffer(this.pre_cmd, this.post_cmd); + var buff = new EJS.Buffer(this.pre_cmd, this.post_cmd); var content = ''; var clean = function(content) { @@ -352,7 +352,7 @@ EJS.Compiler.prototype = { buff.close(); this.out = buff.script + ";"; var to_be_evaled = '/*'+name+'*/this.process = function(_CONTEXT,_VIEW) { try { with(_VIEW) { with (_CONTEXT) {'+this.out+" return ___ViewO.join('');}}}catch(e){e.lineNumber=null;throw e;}};"; - + try{ eval(to_be_evaled); }catch(e){ @@ -397,13 +397,13 @@ EJS.Compiler.prototype = { </td> </tr> </tbody></table> - * + * */ EJS.config = function(options){ EJS.cache = options.cache != null ? options.cache : EJS.cache; EJS.type = options.type != null ? options.type : EJS.type; EJS.ext = options.ext != null ? options.ext : EJS.ext; - + var templates_directory = EJS.templates_directory || {}; //nice and private container EJS.templates_directory = templates_directory; EJS.get = function(path, cache){ @@ -411,12 +411,12 @@ EJS.config = function(options){ if(templates_directory[path]) return templates_directory[path]; return null; }; - - EJS.update = function(path, template) { + + EJS.update = function(path, template) { if(path == null) return; templates_directory[path] = template ; }; - + EJS.INVALID_PATH = -1; }; EJS.config( {cache: true, type: '<', ext: '.ejs' } ); @@ -425,7 +425,7 @@ EJS.config( {cache: true, type: '<', ext: '.ejs' } ); /** * @constructor - * By adding functions to EJS.Helpers.prototype, those functions will be available in the + * By adding functions to EJS.Helpers.prototype, those functions will be available in the * views. * @init Creates a view helper. This function is called internally. You should never call it. * @param {Object} data The data passed to the view. Helpers have access to it through this._data @@ -452,7 +452,7 @@ EJS.Helpers.prototype = { * For a given value, tries to create a human representation. * @param {Object} input the value being converted. * @param {Object} null_text what text should be present if input == null or undefined, defaults to '' - * @return {String} + * @return {String} */ to_text: function(input, null_text) { if(input == null || input === undefined) return null_text || ''; @@ -471,21 +471,21 @@ EJS.Helpers.prototype = { catch(e) { continue;} } } - + EJS.request = function(path){ var request = new EJS.newRequest() request.open("GET", path, false); - + try{request.send(null);} catch(e){return null;} - + if ( request.status == 404 || request.status == 2 ||(request.status == 0 && request.responseText == '') ) return null; - + return request.responseText } EJS.ajax_request = function(params){ params.method = ( params.method ? params.method : 'GET') - + var request = new EJS.newRequest(); request.onreadystatechange = function(){ if(request.readyState == 4){ @@ -502,4 +502,4 @@ EJS.Helpers.prototype = { } -})(); \ No newline at end of file +})(); diff --git a/resources/static/lib/urlparse.js b/resources/static/lib/urlparse.js new file mode 100644 index 0000000000000000000000000000000000000000..a4fe85475202dd07f525798048c45241f2d04ca0 --- /dev/null +++ b/resources/static/lib/urlparse.js @@ -0,0 +1,191 @@ +/** + * urlparse.js + * + * Includes parseUri (c) Steven Levithan <steven@levithan.com> Under the MIT License + * + * Features: + * + parse a url into components + * + url validiation + * + semantically lossless normalization + * + url prefix matching + * + * window.URLParse(string) - + * parse a url using the 'parseUri' algorithm, returning an object containing various + * uri components. returns an object with the following properties (all optional): + * + * PROPERTIES: + * anchor - stuff after the # + * authority - everything after the :// and before the path. Including user auth, host, and port + * directory - path with trailing filename and everything after removed + * file - path without directory + * host - host + * password - password part when user:pass@ is prepended to host + * path - full path, sans query or anchor + * port - port, when present in url + * query - ?XXX + * relative - + * scheme - url scheme (http, file, https, etc.) + * source - full string passed to URLParse() + * user - user part when user:pass@ is prepended to host + * userInfo - + * + * FUNCTIONS: + * (string) toString() - generate a string representation of the url + * + * (this) validate() - validate the url, possbly throwing a string exception + * if determined to not be a valid URL. Returns this, thus may be chained. + * + * (this) normalize() - perform in-place modification of the url to place it in a normal + * (and verbose) form. Returns this, thus may be chained. + * + * (bool) contains(str) - returns whether the object upon which contains() is called is a + * "url prefix" for the passed in string, after normalization. + * + * (this) originOnly() - removes everything that would occur after port, including + * path, query, and anchor. + * + */ + +(function() { + /* const */ var INV_URL = "invalid url: "; + var parseURL = function(s) { + var toString = function() { + var str = this.scheme + "://"; + if (this.user) str += this.user; + if (this.password) str += ":" + this.password; + if (this.user || this.password) str += "@"; + if (this.host) str += this.host; + if (this.port) str += ":" + this.port; + if (this.path) str += this.path; + if (this.query) str += "?" + this.query; + if (this.anchor) str += "#" + this.anchor; + return str; + }; + + var originOnly = function() { + this.path = this.query = this.anchor = undefined; + return this; + }; + + var validate = function() { + if (!this.scheme) throw INV_URL +"missing scheme"; + if (this.scheme !== 'http' && this.scheme !== 'https') + throw INV_URL + "unsupported scheme: " + this.scheme; + if (!this.host) throw INV_URL + "missing host"; + if (this.port) { + var p = parseInt(this.port); + if (!this.port.match(/^\d+$/)) throw INV_URL + "non-numeric numbers in port"; + if (p <= 0 || p >= 65536) throw INV_URL + "port out of range (" +this.port+")"; + } + if (this.path && this.path.indexOf('/') != 0) throw INV_URL + "path must start with '/'"; + + return this; + }; + + var normalize = function() { + // lowercase scheme + if (this.scheme) this.scheme = this.scheme.toLowerCase(); + + // for directory references, append trailing slash + if (!this.path) this.path = "/"; + + // remove port numbers same as default + if (this.port === "80" && 'http' === this.scheme) delete this.port; + if (this.port === "443" && 'https' === this.scheme) delete this.port; + + // remove dot segments from path, algorithm + // http://tools.ietf.org/html/rfc3986#section-5.2.4 + this.path = (function (p) { + var out = []; + while (p) { + if (p.indexOf('../') === 0) p = p.substr(3); + else if (p.indexOf('./') === 0) p = p.substr(2); + else if (p.indexOf('/./') === 0) p = p.substr(2); + else if (p === '/.') p = '/'; + else if (p.indexOf('/../') === 0 || p === '/..') { + if (out.length > 0) out.pop(); + p = '/' + p.substr(4); + } else if (p === '.' || p === '..') p = ''; + else { + var m = p.match(/^\/?([^\/]*)/); + // remove path match from input + p = p.substr(m[0].length); + // add path to output + out.push(m[1]); + } + } + return '/' + out.join('/'); + })(this.path); + + // XXX: upcase chars in % escaping? + + // now we need to update all members + var n = parseURL(this.toString()), + i = 14, + o = parseUri.options; + + while (i--) { + var k = o.key[i]; + if (n[k] && typeof(n[k]) === 'string') this[k] = n[k]; + else if (this[k] && typeof(this[k]) === 'string') delete this[k]; + } + + return this; + }; + + var contains = function(str) { + try { + this.validate(); + var prefix = parseURL(this.toString()).normalize().toString(); + var url = parseURL(str).validate().normalize().toString(); + return (url.indexOf(prefix) === 0); + } catch(e) { + console.log(e); + // if any exceptions are raised, then the comparison fails + return false; + } + }; + + // parseUri 1.2.2 + // (c) Steven Levithan <stevenlevithan.com> + // MIT License + var parseUri = function(str) { + var o = parseUri.options, + m = o.parser.exec(str), + uri = {}, + i = 14; + + while (i--) if (m[i]) uri[o.key[i]] = m[i]; + + if (uri[o.key[12]]) { + uri[o.q.name] = {}; + uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) { + if ($1) uri[o.q.name][$1] = $2; + }); + } + // member functions + uri.toString = toString; + uri.validate = validate; + uri.normalize = normalize; + uri.contains = contains; + uri.originOnly = originOnly; + return uri; + }; + + parseUri.options = { + key: ["source","scheme","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"], + q: { + name: "queryKey", + parser: /(?:^|&)([^&=]*)=?([^&]*)/g + }, + parser: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/ + }; + // end parseUri + + // parse URI using the parseUri code and return the resultant object + return parseUri(s); + }; + + if (typeof exports === 'undefined') window.URLParse = parseURL; + else module.exports = parseURL; +})(); diff --git a/resources/static/pages/page_helpers.js b/resources/static/pages/page_helpers.js index b15cf2eb8002c985150dd4f92af6e76628756511..375584079fd592cc22019a2d72ca6ec8602f5dac 100644 --- a/resources/static/pages/page_helpers.js +++ b/resources/static/pages/page_helpers.js @@ -63,6 +63,7 @@ BrowserID.PageHelpers = (function() { function showFailure(error, info, callback) { info = $.extend(info || {}, { action: error, dialog: false }); bid.Screens.error.show("error", info); + errorDisplay.start(); callback && callback(false); } diff --git a/resources/static/pages/signin.js b/resources/static/pages/signin.js index 27633981de20533c67c13fd6bf90c66aecd8a159..813cce5cdbcebade2f83524a24d140e8e7284193 100644 --- a/resources/static/pages/signin.js +++ b/resources/static/pages/signin.js @@ -13,7 +13,6 @@ BrowserID.signIn = (function() { helpers = bid.Helpers, errors = bid.Errors, pageHelpers = bid.PageHelpers, - cancelEvent = pageHelpers.cancelEvent, doc = document, winchan = window.WinChan, verifyEmail, @@ -144,7 +143,7 @@ BrowserID.signIn = (function() { pageHelpers.setupEmail(); - self.bind("#authWithPrimary", "click", cancelEvent(authWithPrimary)); + self.click("#authWithPrimary", authWithPrimary); self.bind("#email", "change", onEmailChange); self.bind("#email", "keyup", onEmailChange); diff --git a/resources/static/shared/error-messages.js b/resources/static/shared/error-messages.js index bb0b665454977f9e0d490aa99520f5828d6c7cab..3616066baf88c9d62d56639f12b4078ed88ce2c7 100644 --- a/resources/static/shared/error-messages.js +++ b/resources/static/shared/error-messages.js @@ -46,8 +46,8 @@ BrowserID.Errors = (function(){ }, cookiesDisabled: { - title: gettext("We are sorry, BrowserID requires cookies"), - message: gettext("BrowserID requires your browser's cookies to be enabled to operate. Please enable your browser's cookies and try again") + title: gettext("BrowserID requires cookies"), + message: 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'"]) }, cookiesEnabled: { @@ -78,11 +78,6 @@ BrowserID.Errors = (function(){ title: "Logout Failed" }, - offline: { - title: gettext("You are offline!"), - message: gettext("Unfortunately, BrowserID cannot communicate while offline!") - }, - primaryAuthentication: { title: "Authenticating with Identity Provider", message: "We had trouble communicating with your email provider, please try again!" diff --git a/resources/static/shared/modules/page_module.js b/resources/static/shared/modules/page_module.js index f968a016994e6569f498a67df93dde1c83643aaf..b47ecf8ac7a2a496634f1b6c4ed87e437fb3e3d0 100644 --- a/resources/static/shared/modules/page_module.js +++ b/resources/static/shared/modules/page_module.js @@ -52,7 +52,7 @@ BrowserID.Modules.PageModule = (function() { start: function(options) { var self=this; self.bind("form", "submit", cancelEvent(onSubmit)); - self.bind("#thisIsNotMe", "click", cancelEvent(self.close.bind(self, "notme"))); + self.click("#thisIsNotMe", self.close.bind(self, "notme")); }, stop: function() { @@ -86,6 +86,17 @@ BrowserID.Modules.PageModule = (function() { }); }, + /** + * Shortcut to bind a click handler + * @method click + * @param {string} + * @param {function} callback + * @param {object} [context] - optional context, if not given, use this. + */ + click: function(target, callback, context) { + this.bind(target, "click", cancelEvent(callback), context); + }, + unbindAll: function() { var self=this, evt; diff --git a/resources/static/shared/network.js b/resources/static/shared/network.js index a16b38df9b737888eb61b435ac5e0df12f4a7931..be58ccc312ca02b373365023ce7121e82b64da58 100644 --- a/resources/static/shared/network.js +++ b/resources/static/shared/network.js @@ -13,7 +13,6 @@ BrowserID.Network = (function() { domain_key_creation_time, auth_status, code_version, - cookies_enabled, time_until_delay, mediator = bid.Mediator, xhr = bid.XHR, @@ -29,7 +28,6 @@ BrowserID.Network = (function() { domain_key_creation_time = result.domain_key_creation_time; auth_status = result.auth_level; code_version = result.code_version; - cookies_enabled = result.cookies_enabled || true; // seed the PRNG // FIXME: properly abstract this out, probably by exposing a jwcrypto @@ -186,8 +184,8 @@ BrowserID.Network = (function() { complete(onComplete, status.success); }, error: function(info) { - // 403 is throttling. - if (info.network.status === 403) { + // 429 is throttling. + if (info.network.status === 429) { complete(onComplete, false); } else complete(onFailure, info); @@ -392,8 +390,8 @@ BrowserID.Network = (function() { complete(onComplete, response.success); }, error: function(info) { - // 403 is throttling. - if (info.network.status === 403) { + // 429 is throttling. + if (info.network.status === 429) { complete(onComplete, false); } else complete(onFailure, info); @@ -570,8 +568,19 @@ BrowserID.Network = (function() { * @method cookiesEnabled */ cookiesEnabled: function(onComplete, onFailure) { + // Make sure we get context first or else we will needlessly send + // a cookie to the server. withContext(function() { - complete(onComplete, cookies_enabled); + try { + // set a test cookie with a duration of 1 second. + // NOTE - The Android 3.3 default browser will still pass this. + // http://stackoverflow.com/questions/8509387/android-browser-not-respecting-cookies-disabled/9264996#9264996 + document.cookie = "test=true; max-age=1"; + var enabled = document.cookie.indexOf("test") > -1; + complete(onComplete, enabled); + } catch(e) { + complete(onComplete, false); + } }, onFailure); } }; diff --git a/resources/static/test/cases/controllers/actions.js b/resources/static/test/cases/controllers/actions.js index 20593ecac0c0a8920a89d67386f861beaa5cafa0..c670633e8a71193cb9443778085c5702d2f4aac9 100644 --- a/resources/static/test/cases/controllers/actions.js +++ b/resources/static/test/cases/controllers/actions.js @@ -54,17 +54,6 @@ }); }); - asyncTest("doOffline - print offline error screen", function() { - createController({ - ready: function() { - controller.doOffline(); - ok($("#error .contents").text().length, "contents have been written"); - ok($("#error #offline").text().length, "offline error message has been written"); - start(); - } - }); - }); - asyncTest("doProvisionPrimaryUser - start the provision_primary_user service", function() { createController({ ready: function() { diff --git a/resources/static/test/cases/controllers/authenticate.js.bak b/resources/static/test/cases/controllers/authenticate.js.bak deleted file mode 100644 index a77ba9c3d829b753c1bcce1feff8aa6e5801f5e5..0000000000000000000000000000000000000000 --- a/resources/static/test/cases/controllers/authenticate.js.bak +++ /dev/null @@ -1,257 +0,0 @@ -/*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, - el = $("body"), - bid = BrowserID, - storage = bid.Storage, - network = bid.Network, - xhr = bid.Mocks.xhr, - emailRegistered = false, - userCreated = true, - mediator = bid.Mediator, - registrations = [], - testHelpers = bid.TestHelpers, - register = testHelpers.register, - provisioning = bid.Mocks.Provisioning; - - function reset() { - emailRegistered = false; - userCreated = true; - } - - function createController(options) { - options = options || {}; - controller = bid.Modules.Authenticate.create(); - controller.start(options); - } - - module("controllers/authenticate", { - setup: function() { - reset(); - testHelpers.setup(); - }, - - teardown: function() { - if (controller) { - try { - controller.destroy(); - } catch(e) { - // may already be destroyed from close inside of the controller. - } - } - reset(); - testHelpers.teardown(); - } - }); - - asyncTest("providing primary email address - only show email address", function() { - $("#email").val(""); - createController({ - email: "registered@testuser.com", - type: "primary", - ready: function() { - equal($("#email").val(), "registered@testuser.com", "email prefilled"); - equal(false, "need a test"); - start(); - } - }); - }); - - asyncTest("providing known secondary - show password", function() { - $("#email").val(""); - createController({ - email: "registered@testuser.com", - type: "secondary", - known: true, - ready: function() { - equal($("#email").val(), "registered@testuser.com", "email prefilled"); - equal(false, "need a test"); - start(); - } - }); - }); - - asyncTest("providing unknown secondary address - show email address, nothing more", function() { - $("#email").val(""); - createController({ - email: "unregistered@testuser.com", - type: "secondary", - known: false, - ready: function() { - equal($("#email").val(), "unregistered@testuser.com", "email prefilled"); - equal(false, "need a test"); - start(); - } - }); - }); - - function testUserUnregistered() { - register("create_user", function() { - ok(true, "email was valid, user not registered"); - start(); - }); - - controller.checkEmail(); - } - - function testUserUnregistered() { - var createUserCalled = false; - register("create_user", function() { - createUserCalled = true; - }); - - controller.checkEmail(function() { - equal(createUserCalled, true, "create_user was triggered"); - start(); - }); - } - - asyncTest("checkEmail with unknown secondary email, expect 'create_user' message", function() { - createController(); - $("#email").val("unregistered@testuser.com"); - xhr.useResult("unknown_secondary"); - - testUserUnregistered(); - }); - - asyncTest("checkEmail with email with leading/trailing whitespace, user not registered, expect 'create_user' message", function() { - createController(); - $("#email").val(" unregistered@testuser.com "); - xhr.useResult("unknown_secondary"); - - testUserUnregistered(); - }); - - asyncTest("checkEmail with normal email, user registered, expect 'enter_password' message", function() { - createController(); - $("#email").val("registered@testuser.com"); - xhr.useResult("known_secondary"); - - register("enter_password", function() { - ok(true, "email was valid, user registered"); - start(); - }); - - controller.checkEmail(); - }); - - asyncTest("checkEmail with email that has IdP support, expect 'primary_user' message", function() { - createController(); - $("#email").val("unregistered@testuser.com"); - xhr.useResult("primary"); - - register("primary_user", function(msg, info) { - equal(info.email, "unregistered@testuser.com", "email correctly passed"); - equal(info.auth, "https://auth_url", "IdP authentication URL passed"); - equal(info.prov, "https://prov_url", "IdP provisioning URL passed"); - start(); - }); - - controller.checkEmail(); - }); - - function testAuthenticated() { - register("authenticated", function() { - ok(true, "user authenticated as expected"); - start(); - }); - controller.authenticate(); - } - - asyncTest("normal authentication is kosher", function() { - createController(); - $("#email").val("registered@testuser.com"); - $("#password").val("password"); - - testAuthenticated(); - }); - - asyncTest("leading/trailing whitespace on the username is stripped for authentication", function() { - createController(); - $("#email").val(" registered@testuser.com "); - $("#password").val("password"); - - testAuthenticated(); - }); - - asyncTest("forgotPassword triggers forgot_password message", function() { - createController(); - $("#email").val("registered@testuser.com"); - - register("forgot_password", function(msg, info) { - equal(info.email, "registered@testuser.com", "forgot_password with correct email triggered"); - start(); - }); - - controller.forgotPassword(); - }); - - asyncTest("createUser with valid email", function() { - createController(); - $("#email").val("unregistered@testuser.com"); - xhr.useResult("unknown_secondary"); - - register("user_staged", function(msg, info) { - equal(info.email, "unregistered@testuser.com", "user_staged with correct email triggered"); - start(); - }); - - controller.createUser(); - }); - - asyncTest("createUser with invalid email", function() { - createController(); - $("#email").val("unregistered"); - - var handlerCalled = false; - register("user_staged", function(msg, info) { - handlerCalled = true; - }); - - controller.createUser(function() { - equal(handlerCalled, false, "bad jiji, user_staged should not have been called with invalid email"); - start(); - }); - }); - - asyncTest("createUser with valid email but throttling", function() { - createController(); - $("#email").val("unregistered@testuser.com"); - - var handlerCalled = false; - register("user_staged", function(msg, info) { - handlerCalled = true; - }); - - xhr.useResult("throttle"); - controller.createUser(function() { - equal(handlerCalled, false, "bad jiji, user_staged should not have been called with throttling"); - equal(bid.Tooltip.shown, true, "tooltip is shown"); - start(); - }); - }); - - asyncTest("createUser with valid email, XHR error", function() { - createController(); - $("#email").val("unregistered@testuser.com"); - - var handlerCalled = false; - register("user_staged", function(msg, info) { - handlerCalled = true; - }); - - xhr.useResult("ajaxError"); - controller.createUser(function() { - equal(handlerCalled, false, "bad jiji, user_staged should not have been called with XHR error"); - start(); - }); - }); - -}()); - diff --git a/resources/static/test/cases/controllers/pick_email.js b/resources/static/test/cases/controllers/pick_email.js index b11e116dcc8b22bbba20bfc37e3eb3d3ffaa8b1c..8a1309a3762046461dfcca4eae65132bcd713947 100644 --- a/resources/static/test/cases/controllers/pick_email.js +++ b/resources/static/test/cases/controllers/pick_email.js @@ -64,7 +64,7 @@ var radioButton = $("input[type=radio]").eq(0); ok(radioButton.is(":checked"), "the email address we specified is checked"); - var label = radioButton.parent(); + var label = $("label[for=" + radioButton.attr("id") + "]"); ok(label.hasClass("preselected"), "the label has the preselected class"); }); @@ -155,5 +155,47 @@ controller.addEmail(); }); + test("click on an email label and radio button - select corresponding radio button", function() { + storage.addEmail("testuser@testuser.com", {}); + storage.addEmail("testuser2@testuser.com", {}); + + createController(false); + + equal($("#testuser_testuser_com").is(":checked"), false, "radio button is not selected before click."); + + // selects testuser@testuser.com + $("label[for=testuser_testuser_com]").trigger("click"); + equal($("#testuser_testuser_com").is(":checked"), true, "radio button is correctly selected"); + + // selects testuser2@testuser.com + $("#testuser2_testuser_com").trigger("click"); + equal($("#testuser2_testuser_com").is(":checked"), true, "radio button is correctly selected"); + }); + + test("click on the 'Always sign in...' label and checkbox - correct toggling", function() { + createController(true); + + var label = $("label[for=remember]"), + checkbox = $("#remember").removeAttr("checked"); + + equal(checkbox.is(":checked"), false, "checkbox is not yet checked"); + + // toggle checkbox to on clicking on label + label.trigger("click"); + equal(checkbox.is(":checked"), true, "checkbox is correctly checked"); + + // toggle checkbox to off clicking on label + label.trigger("click"); + equal(checkbox.is(":checked"), false, "checkbox is correctly unchecked"); + + // toggle checkbox to on clicking on checkbox + checkbox.trigger("click"); + equal(checkbox.is(":checked"), true, "checkbox is correctly checked"); + + // toggle checkbox to off clicking on checkbox + checkbox.trigger("click"); + equal(checkbox.is(":checked"), false, "checkbox is correctly unchecked"); + }); + }()); diff --git a/resources/static/test/cases/pages/add_email_address_test.js b/resources/static/test/cases/pages/add_email_address_test.js index 8cbcce9abde9424fe67fea11d404cfae30fe3c27..79fe35d5e22cb38c4888550cd30c91643f728d3d 100644 --- a/resources/static/test/cases/pages/add_email_address_test.js +++ b/resources/static/test/cases/pages/add_email_address_test.js @@ -25,7 +25,6 @@ }, teardown: function() { testHelpers.teardown(); - $("#page_head").empty(); } }); @@ -134,10 +133,7 @@ }); asyncTest("password: too long of a password", function() { - var tooLong = ""; - for(var i = 0; i < 81; i++) { - tooLong += (i % 10); - } + var tooLong = testHelpers.generateString(81); $("#password").val(tooLong); $("#vpassword").val(tooLong); diff --git a/resources/static/test/cases/pages/manage_account.js b/resources/static/test/cases/pages/manage_account.js index f92596004f3fead7c1b5ad9be2d269a3dc9c97b0..5e1ddb2cca1febf5e0cf97fb6230366a44b176ab 100644 --- a/resources/static/test/cases/pages/manage_account.js +++ b/resources/static/test/cases/pages/manage_account.js @@ -201,14 +201,10 @@ asyncTest("changePassword with too long of a password - tooltip", function() { bid.manageAccount(mocks, function() { $("#old_password").val("oldpassword"); - var tooLong = ""; - for(var i = 0; i < 81; i++) { - tooLong += (i % 10); - } - $("#new_password").val(tooLong); + $("#new_password").val(testHelpers.generateString(81)); bid.manageAccount.changePassword(function(status) { - equal(status, false, "on too short of a password, status is false"); + equal(status, false, "on too long of a password, status is false"); testHelpers.testTooltipVisible(); start(); }); diff --git a/resources/static/test/cases/pages/page_helpers.js b/resources/static/test/cases/pages/page_helpers.js index 77293232e367abaa16bd8f355f2f97a2453a4b90..5df6b052f6f8d79a3539b7d453da436bdc361824 100644 --- a/resources/static/test/cases/pages/page_helpers.js +++ b/resources/static/test/cases/pages/page_helpers.js @@ -147,10 +147,22 @@ }); }); - asyncTest("showFailure shows a failure screen", function() { - pageHelpers.showFailure({}, errors.offline, function() { + asyncTest("showFailure - show a failure screen, extended info can be opened", function() { + pageHelpers.showFailure("error", { network: 400, status: "error"}, function() { testHelpers.testErrorVisible(); - start(); + + // We have to make sure the error screen itself is visible and that the + // extra info is hidden so when we click on the extra info it opens. + $("#error").show(); + $("#moreInfo").hide(); + $("#openMoreInfo").trigger("click"); + + // Add a bit of delay to wait for the animation + setTimeout(function() { + equal($("#moreInfo").is(":visible"), true, "extra info is visible after click"); + start(); + }, 100); + }); }); diff --git a/resources/static/test/cases/pages/verify_email_address_test.js b/resources/static/test/cases/pages/verify_email_address_test.js index a4c3ef06d7407dd377bcb79b1310da0c59424357..c11ae2574bd2b46e071d12037a981eed2ca1907c 100644 --- a/resources/static/test/cases/pages/verify_email_address_test.js +++ b/resources/static/test/cases/pages/verify_email_address_test.js @@ -11,6 +11,7 @@ storage = bid.Storage, xhr = bid.Mocks.xhr, testHelpers = bid.TestHelpers, + testTooltipVisible = testHelpers.testTooltipVisible, validToken = true; module("pages/verify_email_address", { @@ -82,6 +83,35 @@ bid.verifyEmailAddress.submit(function() { equal($("#congrats").is(":visible"), false, "congrats is not visible, missing password"); + testTooltipVisible(); + start(); + }); + }); + }); + + asyncTest("submit with good token, too short of a password", function() { + bid.verifyEmailAddress("token", function() { + var pass = testHelpers.generateString(6); + $("#password").val(pass); + $("#vpassword").val(pass); + + bid.verifyEmailAddress.submit(function() { + equal($("#congrats").is(":visible"), false, "congrats is not visible, too short of a password"); + testTooltipVisible(); + start(); + }); + }); + }); + + asyncTest("submit with good token, too long of a password", function() { + bid.verifyEmailAddress("token", function() { + var pass = testHelpers.generateString(81); + $("#password").val(pass); + $("#vpassword").val(pass); + + bid.verifyEmailAddress.submit(function() { + equal($("#congrats").is(":visible"), false, "congrats is not visible, too long of a password"); + testTooltipVisible(); start(); }); }); @@ -96,6 +126,7 @@ bid.verifyEmailAddress.submit(function() { equal($("#congrats").is(":visible"), false, "congrats is not visible, missing verification password"); + testTooltipVisible(); start(); }); @@ -109,6 +140,7 @@ bid.verifyEmailAddress.submit(function() { equal($("#congrats").is(":visible"), false, "congrats is not visible, different passwords"); + testTooltipVisible(); start(); }); diff --git a/resources/static/test/cases/resources/state.js b/resources/static/test/cases/resources/state.js index a9e95a48b27d6f9bcdddb780e85731a8b2c1059c..c951c5ec98f1b11a55af5b52b98c58a17522a534 100644 --- a/resources/static/test/cases/resources/state.js +++ b/resources/static/test/cases/resources/state.js @@ -67,12 +67,6 @@ equal(error, "start: controller must be specified", "creating a state machine without a controller fails"); }); - test("offline does offline", function() { - mediator.publish("offline"); - - equal(actions.called.doOffline, true, "controller is offline"); - }); - test("user_staged - call doConfirmUser", function() { mediator.publish("user_staged", { email: "testuser@testuser.com" @@ -170,10 +164,11 @@ ok(actions.called.doEmailChosen, "doEmailChosen called"); }); - test("authenticated", function() { - mediator.publish("authenticated"); + test("authenticated - call doEmailChosen", function() { + storage.addEmail("testuser@testuser.com", {}); + mediator.publish("authenticated", { email: "testuser@testuser.com" }); - ok(actions.called.doPickEmail, "doPickEmail has been called"); + ok(actions.called.doEmailChosen, "doEmailChosen has been called"); }); test("forgot_password", function() { @@ -359,4 +354,12 @@ equal(error, "invalid email", "expected exception thrown"); }); + test("null assertion generated - preserve original options in doPickEmail", function() { + mediator.publish("start", { allowPersistent: true }); + mediator.publish("assertion_generated", { assertion: null }); + + equal(actions.called.doPickEmail, true, "doPickEmail callled"); + equal(actions.info.doPickEmail.allow_persistent, true, "allow_persistent preserved"); + }); + }()); diff --git a/resources/static/test/cases/shared/helpers.js b/resources/static/test/cases/shared/helpers.js index 755aeabc77d5eb4737080caa8bc620fd270665b5..8384bc151cd41b57ef6c1433fa8afcb6df541018 100644 --- a/resources/static/test/cases/shared/helpers.js +++ b/resources/static/test/cases/shared/helpers.js @@ -7,15 +7,17 @@ "use strict"; var bid = BrowserID, - helpers = bid.Helpers; + helpers = bid.Helpers, + testHelpers = bid.TestHelpers; module("shared/helpers", { setup: function() { + testHelpers.setup(); bid.Renderer.render("#page_head", "site/add_email_address", {}); }, teardown: function() { - $("#page_head").empty(); + testHelpers.teardown(); } }); diff --git a/resources/static/test/cases/shared/modules/cookie_check.js b/resources/static/test/cases/shared/modules/cookie_check.js index 86e49d7ca00cd65ead97619e4c02fb60b8a42671..b6358ba50fe1571fec66ee894aa40d8cacd4a6d3 100644 --- a/resources/static/test/cases/shared/modules/cookie_check.js +++ b/resources/static/test/cases/shared/modules/cookie_check.js @@ -50,19 +50,5 @@ }); }); - /* - // XXX - disabling this test until we have the full solution for how we are going - // to interact with the backend. - asyncTest("createController with cookies disabled - ready returns with false status, error shown", function() { - transport.setContextInfo("cookies_enabled", false); - createController({ - ready: function(status) { - testHelpers.testErrorVisible(); - equal(status, false, "cookies are disabled, false status"); - start(); - } - }); - }); - */ }()); diff --git a/resources/static/test/cases/shared/modules/page_module.js b/resources/static/test/cases/shared/modules/page_module.js index 7db90097c54912a67da1d48541f5960803317827..7baac2497bae62c255f945e57fc8a260e7201ee8 100644 --- a/resources/static/test/cases/shared/modules/page_module.js +++ b/resources/static/test/cases/shared/modules/page_module.js @@ -109,6 +109,18 @@ $("body").trigger("click"); }); + asyncTest("click - bind a click handler, handler does not get event", function() { + createController(); + + controller.click("body", function(event) { + equal(typeof event, "undefined", "event is undefined"); + strictEqual(this, controller, "context is correct"); + start(); + }); + + $("body").trigger("click"); + }); + asyncTest("unbindAll removes all listeners", function() { createController(); var listenerCalled = false; diff --git a/resources/static/test/cases/shared/network.js b/resources/static/test/cases/shared/network.js index ba08d8f894ccc074c6421e2a42416aa9b79560c7..64e6b809ff5e587981d73fc1ceb7ed77c21d8057 100644 --- a/resources/static/test/cases/shared/network.js +++ b/resources/static/test/cases/shared/network.js @@ -541,18 +541,11 @@ failureCheck(network.changePassword, "oldpassword", "newpassword"); }); - asyncTest("cookiesEnabled with cookies enabled", function() { - transport.setContextInfo("cookies_enabled", true); - + asyncTest("cookiesEnabled with cookies enabled - return true status", function() { network.cookiesEnabled(function(status) { equal(status, true, "cookies are enabled, correct status"); start(); }, testHelpers.unexpectedXHRFailure); }); - asyncTest("cookiesEnabled with XHR failure", function() { - transport.useResult("contextAjaxError"); - failureCheck(network.cookiesEnabled); - }); - }()); diff --git a/resources/static/test/cases/shared/renderer.js b/resources/static/test/cases/shared/renderer.js index 2ac4b75c701fa3cb1723cdecc3ab63753ce44326..ab6f605d9a8a00ce841d8bb9ea618bef7ff18b3c 100644 --- a/resources/static/test/cases/shared/renderer.js +++ b/resources/static/test/cases/shared/renderer.js @@ -7,40 +7,32 @@ "use strict"; var bid = BrowserID, - renderer = bid.Renderer; + renderer = bid.Renderer, + testHelpers = bid.TestHelpers; module("shared/renderer", { setup: function() { - + testHelpers.setup(); }, teardown: function() { - + testHelpers.teardown(); } }); test("render template loaded using XHR", function() { - $("#formWrap .contents").empty(); - $("#templateInput").remove(); - renderer.render("#formWrap .contents", "test_template_with_input"); ok($("#templateInput").length, "template written when loaded using XHR"); }); test("render template from memory", function() { - $("#formWrap .contents").empty(); - $("#templateInput").remove(); - renderer.render("#formWrap .contents", "inMemoryTemplate"); ok($("#templateInput").length, "template written when loaded from memory"); }); test("append template to element", function() { - $("#formWrap .contents").empty(); - $("#templateInput").remove(); - renderer.append("#formWrap", "inMemoryTemplate"); ok($("#formWrap > #templateInput").length && $("#formWrap > .contents"), "template appended to element instead of overwriting it"); diff --git a/resources/static/test/cases/shared/screens.js b/resources/static/test/cases/shared/screens.js index f34c1ae9aace7241df52afd5ca818fe6556e55f7..98f75978889c33ddbae761a25fb628dfa71c67b9 100644 --- a/resources/static/test/cases/shared/screens.js +++ b/resources/static/test/cases/shared/screens.js @@ -8,23 +8,21 @@ var bid = BrowserID, screens = bid.Screens, + testHelpers = bid.TestHelpers, el; module("shared/screens", { setup: function() { - + testHelpers.setup(); }, teardown: function() { - if (el) { - el.empty(); - } + testHelpers.teardown(); } }); test("form", function() { el = $("#formWrap .contents"); - el.empty(); screens.form.show("test_template_with_input"); ok($("#templateInput").length, "the template has been written"); @@ -38,7 +36,6 @@ test("wait", function() { var el = $("#wait .contents"); - el.empty(); screens.wait.show("test_template_with_input"); ok($("#templateInput").length, "the template has been written"); @@ -52,7 +49,6 @@ test("error", function() { var el = $("#error .contents"); - el.empty(); screens.error.show("test_template_with_input"); ok($("#templateInput").length, "the template has been written"); @@ -66,7 +62,6 @@ test("XHR 503 (server unavailable) error", function() { var el = $("#error .contents"); - el.empty(); screens.error.show("error", { network: { @@ -76,4 +71,16 @@ ok($("#error_503").length, "503 header is shown"); }); + + test("XHR 403 (Forbidden) error - show the 403, cookies required error", function() { + var el = $("#error .contents"); + + screens.error.show("error", { + network: { + status: 403 + } + }); + + ok($("#error_403").length, "403 header is shown"); + }); }()); diff --git a/resources/static/test/mocks/xhr.js b/resources/static/test/mocks/xhr.js index e74cccc1a24659f5dd930d61bbb5cee7bb8c0db2..6b82fb2362359f5607173d8f1a2679a60c50160c 100644 --- a/resources/static/test/mocks/xhr.js +++ b/resources/static/test/mocks/xhr.js @@ -13,8 +13,7 @@ BrowserID.Mocks.xhr = (function() { authenticated: false, auth_level: undefined, code_version: "ABC123", - random_seed: "H+ZgKuhjVckv/H4i0Qvj/JGJEGDVOXSIS5RCOjY9/Bo=", - cookies_enabled: true + random_seed: "H+ZgKuhjVckv/H4i0Qvj/JGJEGDVOXSIS5RCOjY9/Bo=" }; // this cert is meaningless, but it has the right format @@ -52,7 +51,7 @@ BrowserID.Mocks.xhr = (function() { "post /wsapi/stage_user unknown_secondary": { success: true }, "post /wsapi/stage_user valid": { success: true }, "post /wsapi/stage_user invalid": { success: false }, - "post /wsapi/stage_user throttle": 403, + "post /wsapi/stage_user throttle": 429, "post /wsapi/stage_user ajaxError": undefined, "get /wsapi/user_creation_status?email=registered%40testuser.com pending": { status: "pending" }, "get /wsapi/user_creation_status?email=registered%40testuser.com complete": { status: "complete" }, @@ -79,7 +78,7 @@ BrowserID.Mocks.xhr = (function() { "post /wsapi/stage_email unknown_secondary": { success: true }, "post /wsapi/stage_email known_secondary": { success: true }, "post /wsapi/stage_email invalid": { success: false }, - "post /wsapi/stage_email throttle": 403, + "post /wsapi/stage_email throttle": 429, "post /wsapi/stage_email ajaxError": undefined, "post /wsapi/cert_key ajaxError": undefined, "get /wsapi/email_addition_status?email=registered%40testuser.com pending": { status: "pending" }, diff --git a/resources/static/test/testHelpers/helpers.js b/resources/static/test/testHelpers/helpers.js index 507c885c6341c2534a1429bbe7808a00b3a21ec6..493ea1db0756779b69735a7f18c104b51c1d3e26 100644 --- a/resources/static/test/testHelpers/helpers.js +++ b/resources/static/test/testHelpers/helpers.js @@ -68,15 +68,13 @@ BrowserID.TestHelpers = (function() { $("body").stop().show(); $("body")[0].className = ""; - var el = $("#controller_head"); - el.find("#formWrap .contents").html(""); - el.find("#wait .contents").html(""); $(".error").removeClass("error"); - $("#error").stop().html("<div class='contents'></div>").hide(); + $("#error").hide(); $(".notification").stop().hide(); $("form").show(); screens.wait.hide(); screens.error.hide(); + screens.delay.hide(); tooltip.reset(); provisioning.setStatus(provisioning.NOT_AUTHENTICATED); user.reset(); @@ -96,12 +94,9 @@ BrowserID.TestHelpers = (function() { }); network.init(); storage.clear(); - $(".error").removeClass("error"); - $("#error").stop().html("<div class='contents'></div>").hide(); - $(".notification").stop().hide(); - $("form").show(); screens.wait.hide(); screens.error.hide(); + screens.delay.hide(); tooltip.reset(); provisioning.setStatus(provisioning.NOT_AUTHENTICATED); user.reset(); @@ -185,6 +180,17 @@ BrowserID.TestHelpers = (function() { } cb && cb.apply(null, args); + }, + + /** + * Generate a long string + */ + generateString: function(length) { + var str = ""; + for(var i = 0; i < length; i++) { + str += (i % 10); + } + return str; } }; diff --git a/resources/views/add_email_address.ejs b/resources/views/add_email_address.ejs index 13b6a5af6ddb10247c8a323cf3dc16a22ee03da9..979cea68aa1953636faecadd5c2370d81a366722 100644 --- a/resources/views/add_email_address.ejs +++ b/resources/views/add_email_address.ejs @@ -10,7 +10,7 @@ </ul> <form id="signUpForm" class="cf"> - <p class="hint siteinfo"><%= gettext('Finish signing into: ') %><strong><span class="website"></span></strong></p> + <p class="hint siteinfo"><%= gettext('Finish signing into:') %> <strong><span class="website"></span></strong></p> <h1 class="serif"><%= gettext('Email Verification') %></h1> @@ -46,7 +46,7 @@ </ul> <div class="submit cf password_entry"> - <button><%= gettext('Finish') %></button> + <button><%= gettext('finish') %></button> </div> diff --git a/resources/views/cookies_disabled.ejs b/resources/views/cookies_disabled.ejs new file mode 100644 index 0000000000000000000000000000000000000000..01fb4baea956430a667abf0d4883a46a9ad6c01c --- /dev/null +++ b/resources/views/cookies_disabled.ejs @@ -0,0 +1,17 @@ +<!-- 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/. --> + + <section id="error" class="cookies_disabled"> + <div class="table"> + <div class="vertical contents"> + <h2 id="reason"> + <%= gettext("BrowserID requires cookies") %> + </h2> + + <p> + <%- 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'"]) %> + </p> + </div> + </div> + </section> diff --git a/resources/views/dialog.ejs b/resources/views/dialog.ejs index 7551a7b1c26192ddf3546c7178c662653b177bd0..775879d65d29259d3a78248d8e8128cdea63c151 100644 --- a/resources/views/dialog.ejs +++ b/resources/views/dialog.ejs @@ -11,9 +11,8 @@ <div id="signIn"> <div class="arrow"></div> - <div class="table"> - <div class="vertical contents"> - </div> + <div class="container"> + <div class="vertical contents"></div> </div> </div> </form> @@ -21,6 +20,7 @@ <section id="wait"> + <!-- because each section is an absolutely positioned element, we have to use the inner table container element to be able to vertically/horizontally center correctly. Without the table element, the layout gets all messed up. --> <div class="table"> <div class="vertical contents"> <h2><%= gettext('Communicating with server') %></h2> diff --git a/resources/views/dialog_layout.ejs b/resources/views/dialog_layout.ejs index edefc45f38b623d3fb1c086d43f80eb72ade47c9..24f728902afed069c42fcc80ae65b87912906a3b 100644 --- a/resources/views/dialog_layout.ejs +++ b/resources/views/dialog_layout.ejs @@ -12,13 +12,7 @@ <!--[if lt IE 9]> <script src="/lib/html5shim.js"></script> <![endif]--> - <% if (production) { %> - <link href="/production/dialog.css" rel="stylesheet" type="text/css"> - <% } else { %> - <link href="/css/common.css" rel="stylesheet" type="text/css"> - <link href="/dialog/css/popup.css" rel="stylesheet" type="text/css"> - <link href="/dialog/css/m.css" rel="stylesheet" type="text/css"> - <% } %> + <%- cachify_css('/production/dialog.css') %> <link href="https://fonts.googleapis.com/css?family=Droid+Serif:400,400italic,700,700italic" rel="stylesheet" type="text/css"> <title><%= gettext('BrowserID') %></title> </head> @@ -44,7 +38,7 @@ </ul--> <div class="learn"> -<%- gettext('BrowserID is the fast and secure way to sign in — <a target="_blank" href="/about">learn more</a>') %> +<%- format(gettext('BrowserID is the fast and secure way to sign in — <a %s>learn more</a>'), [" href='/about' target='_blank'"]) %> </div> </footer> @@ -52,67 +46,7 @@ </div> <% if (useJavascript !== false) { %> - <% if (production) { %> - <script src="/production/<%= locale %>/dialog.js"></script> - <% } else { %> - <script src="/lib/jquery-1.7.1.min.js"></script> - <script src="/lib/winchan.js"></script> - <script src="/lib/underscore-min.js"></script> - <script src="/lib/vepbundle.js"></script> - <script src="/lib/ejs.js"></script> - <script src="/shared/javascript-extensions.js"></script> - <script src="/i18n/<%= locale %>/client.json"></script> - <script src="/shared/gettext.js"></script> - <script src="/shared/browserid.js"></script> - <script src="/lib/hub.js"></script> - <script src="/lib/dom-jquery.js"></script> - <script src="/lib/module.js"></script> - <script src="/lib/jschannel.js"></script> - - <script src="/shared/templates.js"></script> - <script src="/shared/renderer.js"></script> - <script src="/shared/class.js"></script> - <script src="/shared/mediator.js"></script> - <script src="/shared/tooltip.js"></script> - <script src="/shared/validation.js"></script> - <script src="/shared/helpers.js"></script> - <script src="/shared/screens.js"></script> - <script src="/shared/browser-support.js"></script> - <script src="/shared/wait-messages.js"></script> - <script src="/shared/error-messages.js"></script> - <script src="/shared/error-display.js"></script> - <script src="/shared/storage.js"></script> - <script src="/shared/xhr.js"></script> - <script src="/shared/network.js"></script> - <script src="/shared/provisioning.js"></script> - <script src="/shared/user.js"></script> - <script src="/shared/command.js"></script> - <script src="/shared/history.js"></script> - <script src="/shared/state_machine.js"></script> - - <script src="/shared/modules/page_module.js"></script> - <script src="/shared/modules/xhr_delay.js"></script> - <script src="/shared/modules/xhr_disable_form.js"></script> - <script src="/shared/modules/cookie_check.js"></script> - - <script src="/dialog/resources/internal_api.js"></script> - <script src="/dialog/resources/helpers.js"></script> - <script src="/dialog/resources/state.js"></script> - - <script src="/dialog/controllers/actions.js"></script> - <script src="/dialog/controllers/dialog.js"></script> - <script src="/dialog/controllers/authenticate.js"></script> - <script src="/dialog/controllers/forgot_password.js"></script> - <script src="/dialog/controllers/check_registration.js"></script> - <script src="/dialog/controllers/pick_email.js"></script> - <script src="/dialog/controllers/add_email.js"></script> - <script src="/dialog/controllers/required_email.js"></script> - <script src="/dialog/controllers/verify_primary_user.js"></script> - <script src="/dialog/controllers/provision_primary_user.js"></script> - <script src="/dialog/controllers/primary_user_provisioned.js"></script> - <script src="/dialog/controllers/email_chosen.js"></script> - <script src="/dialog/start.js"></script> - <% } %> + <%- cachify_js(util.format('/production/%s/dialog.js', locale)) %> <% } %> </body> </html> diff --git a/resources/views/index.ejs b/resources/views/index.ejs index b07203f655059f705ee9ea99c8da847928294d35..57f1c8d7b96254292d735fb74bf6bab32639dd4a 100644 --- a/resources/views/index.ejs +++ b/resources/views/index.ejs @@ -52,6 +52,10 @@ </div> <div id="vAlign" class="display_nonauth"> + <div id="newsbanner"> + BrowserID is graduating: we're launching <b>Mozilla Persona</b>. Find out more on <a href="http://identity.mozilla.com/">the identity blog</a>. + </div> + <div id="signUp"> <div id="card"><img src="/i/slit.png"></div> <div id="hint"></div> diff --git a/resources/views/layout.ejs b/resources/views/layout.ejs index 118292f3363b6f28469566b4867885fc17f204b2..c72eb8247356adf821333daaf9b6c26df15d3deb 100644 --- a/resources/views/layout.ejs +++ b/resources/views/layout.ejs @@ -11,62 +11,8 @@ <script src="/lib/html5shim.js"></script> <![endif]--> <link href='https://fonts.googleapis.com/css?family=Droid+Serif:400,400italic,700,700italic' rel='stylesheet' type='text/css'> - <% if (production) { %> - <link rel="stylesheet" type="text/css" href="/production/browserid.css"> - <script src="/production/<%= locale %>/browserid.js"></script> - <% } else { %> - <link rel="stylesheet" href="/css/common.css" type="text/css" media="screen"> - <link rel="stylesheet" href="/css/style.css" type="text/css" media="screen"> - <link rel="stylesheet" href="/css/m.css" type="text/css" media="screen"> - - <script src="/lib/vepbundle.js"></script> - <script src="/lib/jquery-1.7.1.min.js"></script> - <script src="/lib/underscore-min.js"></script> - <script src="/lib/ejs.js"></script> - <script src="/shared/javascript-extensions.js"></script> - <script src="/i18n/<%= locale %>/client.json"></script> - <script src="/shared/gettext.js"></script> - <script src="/shared/browserid.js"></script> - <script src="/lib/dom-jquery.js"></script> - <script src="/lib/module.js"></script> - <script src="/lib/jschannel.js"></script> - <script src="/lib/winchan.js"></script> - <script src="/lib/hub.js"></script> - - <script src="/shared/templates.js"></script> - <script src="/shared/renderer.js"></script> - <script src="/shared/class.js"></script> - <script src="/shared/mediator.js"></script> - <script src="/shared/tooltip.js"></script> - <script src="/shared/validation.js"></script> - <script src="/shared/helpers.js"></script> - <script src="/shared/screens.js"></script> - <script src="/shared/browser-support.js"></script> - <script src="/shared/wait-messages.js"></script> - <script src="/shared/error-messages.js"></script> - <script src="/shared/error-display.js"></script> - <script src="/shared/mediator.js"></script> - <script src="/shared/storage.js"></script> - <script src="/shared/xhr.js"></script> - <script src="/shared/network.js"></script> - <script src="/shared/provisioning.js"></script> - <script src="/shared/user.js"></script> - - <script src="/shared/modules/page_module.js"></script> - <script src="/shared/modules/xhr_delay.js"></script> - <script src="/shared/modules/xhr_disable_form.js"></script> - <script src="/shared/modules/cookie_check.js"></script> - - <script src="/pages/page_helpers.js"></script> - <script src="/pages/index.js"></script> - <script src="/pages/start.js"></script> - <script src="/pages/add_email_address.js"></script> - <script src="/pages/verify_email_address.js"></script> - <script src="/pages/forgot.js"></script> - <script src="/pages/manage_account.js"></script> - <script src="/pages/signin.js"></script> - <script src="/pages/signup.js"></script> - <% } %> + <%- cachify_css('/production/browserid.css') %> + <%- cachify_js(util.format('/production/%s/browserid.js', locale)) %> <title><%= format(gettext("BrowserID: %s"), [title]) %></title> </head> <body> @@ -97,8 +43,8 @@ <footer id="footer"> <ul class="cf"> - <li><%- format(gettext('By the <a href="%s" target="_blank">Identity Team</a> @ <a href="%s" target="_blank">Mozilla Labs</a>'), - ['http://identity.mozilla.com', 'http://mozillalabs.com']) %></li> + <li><%- format(gettext('By the <a %s>Identity Team</a> @ <a %s>Mozilla Labs</a>'), + [" href='http://identity.mozilla.com' target='_blank'", " href='http://mozillalabs.com' target='_blank'"]) %></li> <li>—</li> <li><a href="/privacy"><%= gettext('Privacy') %></a></li> <li><a href="/tos"><%= gettext('TOS') %></a></li> diff --git a/resources/views/test.ejs b/resources/views/test.ejs index 073bc76d5ba52cbdb380a5c162154872b6a0ec83..036d577407b9b3fa9cb039104b09bfd1b30bdc67 100644 --- a/resources/views/test.ejs +++ b/resources/views/test.ejs @@ -21,7 +21,7 @@ <ol id="qunit-tests"></ol> <div id="qunit-test-area"></div> - <div style="position: absolute; top: -1000px; left: 100px; right: 100px; height: 300px;"> + <div id="qunit-fixture" style="position: absolute; top: -1000px; left: 100px; right: 100px; height: 300px;"> <a href="#" onclick="$('#contents').hide(); return false;">Close</a> <h3>Test Contents, this will be updated and can be safely ignored</h3> diff --git a/resources/views/unsupported_dialog.ejs b/resources/views/unsupported_dialog.ejs index 2fc05e2206682df54ff3fc433364666be24091d2..fc39f468a3d4ce646b45065f0a7276fcfccfcd4a 100644 --- a/resources/views/unsupported_dialog.ejs +++ b/resources/views/unsupported_dialog.ejs @@ -3,30 +3,22 @@ - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> <section id="error" style="display: block" class="unsupported"> - <div class="table"> - <div class="vertical contents"> - <div id="reason"> - We're sorry, but currently your browser isn't supported. - </div> + <h2> + We are sorry, but currently your browser is not supported. + </h2> - <div id="alternative"> - <div id="borderbox"> - <a href="http://getfirefox.com" target="_blank"> - <img src="/i/firefox_logo.png" width="250" height="88" alt="Firefox logo" /> - </a> + <a href="http://getfirefox.com" target="_blank"> + <img src="/i/firefox_logo.png" width="250" height="88" alt="Firefox logo" /> + </a> - <p> - BrowserID works with <a href="http://getfirefox.com" target="_blank" title="Get Firefox">Firefox</a> - </p> + <p> + BrowserID works with <a href="http://getfirefox.com" target="_blank" title="Get Firefox">Firefox</a> + </p> - <p class="lighter"> - and other <a href="http://whatbrowser.org" target="_blank">modern browsers.</a> - </p> - </div> + <p class="lighter"> + and other <a href="http://whatbrowser.org" target="_blank">modern browsers.</a> + </p> - </div> - </div> - </div> </section> diff --git a/resources/views/verify_email_address.ejs b/resources/views/verify_email_address.ejs index 4df7506ed2be239db5aeeff28049931b1820ede6..e86a1f5b6a41afa004449cae41925c9636ca6104 100644 --- a/resources/views/verify_email_address.ejs +++ b/resources/views/verify_email_address.ejs @@ -6,12 +6,12 @@ <div id="signUpFormWrap"> <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="cannotcommunicate"><%= gettext('Error comunicating with server.') %></li> + <li class="notification error" id="cannotcommunicate"><%= gettext('Error communicating with server.') %></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> + <p class="hint siteinfo"><%= gettext('Finish signing into:') %> <strong><span class="website"></span></strong></p> <h1 class="serif"><%= gettext('Last step!') %></h1> <ul class="inputs"> @@ -46,7 +46,7 @@ </ul> <div class="submit cf"> - <button><%= gettext('Finish') %></button> + <button><%= gettext('finish') %></button> </div> </form> diff --git a/scripts/browserid.spec b/scripts/browserid.spec index e2af1e255723775418a627d91f7aedf4c2db8b86..6ed39bbfd82a3d7c4c310e7b9d8de75e05742771 100644 --- a/scripts/browserid.spec +++ b/scripts/browserid.spec @@ -1,7 +1,7 @@ %define _rootdir /opt/browserid Name: browserid-server -Version: 0.2012.02.08 +Version: 0.2012.02.29 Release: 1%{?dist}_%{svnrev} Summary: BrowserID server Packager: Pete Fritchman <petef@mozilla.com> @@ -12,7 +12,7 @@ Source0: %{name}.tar.gz BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root AutoReqProv: no Requires: openssl nodejs -BuildRequires: gcc-c++ git jre make npm openssl-devel expat-devel perl perl-JSON perl-Locale-PO +BuildRequires: gcc-c++ git jre make npm openssl-devel expat-devel %description browserid server & web home for browserid.org @@ -37,6 +37,8 @@ mkdir -p %{buildroot}%{_rootdir} for f in bin lib locale node_modules resources scripts *.json; do cp -rp $f %{buildroot}%{_rootdir}/ done +mkdir -p %{buildroot}%{_rootdir}/config +cp -p config/l10n-all.json %{buildroot}%{_rootdir}/config %clean rm -rf %{buildroot} diff --git a/scripts/check_po.sh b/scripts/check_po.sh new file mode 100755 index 0000000000000000000000000000000000000000..19278dc05951e59ff5d5e28d0b44d6ce928f3baa --- /dev/null +++ b/scripts/check_po.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# syntax: +# check-po.sh + +for lang in `find locale -type f -name "*.po"`; do + dir=`dirname $lang` + stem=`basename $lang .po` + printf "${lang}: " + msgfmt --statistics ${dir}/${stem}.po +done +rm messages.mo diff --git a/scripts/compress-locales.sh b/scripts/compress-locales.sh index 6264656b5a54c0f49201a78717ecdf968ca4d9e4..d3a7c0ec72cf28f30df6fcb3e92cab69f87c6289 100755 --- a/scripts/compress-locales.sh +++ b/scripts/compress-locales.sh @@ -52,7 +52,7 @@ for locale in $locales; do mkdir -p $BUILD_PATH/../i18n/$locale # Touch as the trigger locale doesn't really exist touch $BUILD_PATH/../i18n/${locale}/client.json - cat lib/jquery-1.7.1.min.js lib/winchan.js lib/underscore-min.js lib/vepbundle.js lib/ejs.js shared/javascript-extensions.js i18n/${locale}/client.json shared/gettext.js shared/browserid.js lib/hub.js lib/dom-jquery.js lib/module.js lib/jschannel.js $BUILD_PATH/templates.js shared/renderer.js shared/class.js shared/mediator.js shared/tooltip.js shared/validation.js shared/helpers.js shared/screens.js shared/browser-support.js shared/wait-messages.js shared/error-messages.js shared/error-display.js shared/storage.js shared/xhr.js shared/network.js shared/provisioning.js shared/user.js shared/command.js shared/history.js shared/state_machine.js shared/modules/page_module.js shared/modules/xhr_delay.js shared/modules/xhr_disable_form.js shared/modules/cookie_check.js dialog/resources/internal_api.js dialog/resources/helpers.js dialog/resources/state.js dialog/controllers/actions.js dialog/controllers/dialog.js dialog/controllers/authenticate.js dialog/controllers/forgot_password.js dialog/controllers/check_registration.js dialog/controllers/pick_email.js dialog/controllers/add_email.js dialog/controllers/required_email.js dialog/controllers/verify_primary_user.js dialog/controllers/provision_primary_user.js dialog/controllers/primary_user_provisioned.js dialog/controllers/email_chosen.js dialog/start.js > $BUILD_PATH/$locale/dialog.uncompressed.js + cat lib/jquery-1.7.1.min.js lib/winchan.js lib/underscore-min.js lib/vepbundle.js lib/ejs.js shared/javascript-extensions.js i18n/${locale}/client.json shared/gettext.js shared/browserid.js lib/hub.js lib/dom-jquery.js lib/module.js lib/jschannel.js $BUILD_PATH/templates.js shared/renderer.js shared/class.js shared/mediator.js shared/tooltip.js shared/validation.js shared/helpers.js shared/screens.js shared/browser-support.js shared/wait-messages.js shared/error-messages.js shared/error-display.js shared/storage.js shared/xhr.js shared/network.js shared/provisioning.js shared/user.js shared/command.js shared/history.js shared/state_machine.js shared/modules/page_module.js shared/modules/xhr_delay.js shared/modules/xhr_disable_form.js shared/modules/cookie_check.js lib/urlparse.js dialog/resources/internal_api.js dialog/resources/helpers.js dialog/resources/state.js dialog/controllers/actions.js dialog/controllers/dialog.js dialog/controllers/authenticate.js dialog/controllers/forgot_password.js dialog/controllers/check_registration.js dialog/controllers/pick_email.js dialog/controllers/add_email.js dialog/controllers/required_email.js dialog/controllers/verify_primary_user.js dialog/controllers/provision_primary_user.js dialog/controllers/primary_user_provisioned.js dialog/controllers/email_chosen.js dialog/start.js > $BUILD_PATH/$locale/dialog.uncompressed.js done echo '' diff --git a/scripts/deploy/vm.js b/scripts/deploy/vm.js index c79475d091386d0f72b1e3b9e39edf47edbc2ce6..dd6d9b77699d7eacb06fce571d3c99f5a3a38fe6 100644 --- a/scripts/deploy/vm.js +++ b/scripts/deploy/vm.js @@ -4,7 +4,7 @@ jsel = require('JSONSelect'), key = require('./key.js'), sec = require('./sec.js'); -const BROWSERID_TEMPLATE_IMAGE_ID = 'ami-2d34e444'; +const BROWSERID_TEMPLATE_IMAGE_ID = 'ami-5678aa3f'; function extractInstanceDeets(horribleBlob) { var instance = {}; diff --git a/scripts/deploy_server.js b/scripts/deploy_server.js index f8a6acc9d83da90ca9ece8eb047771d8bac9bc64..130bcd5ae06cb9276c2a0fa53343ddb0a9145036 100755 --- a/scripts/deploy_server.js +++ b/scripts/deploy_server.js @@ -263,10 +263,10 @@ deployer.on('error', function(r) { }); -// we check every 30 minutes no mattah what. (checks are cheap) +// we check every 3 minutes no mattah what. (checks are cheap, github webhooks are flakey) setInterval(function () { deployer.checkForUpdates(); -}, (1000 * 60 * 30)); +}, (1000 * 60 * 3)); // check for updates at startup deployer.on('ready', function() { diff --git a/scripts/extract_po.sh b/scripts/extract_po.sh index 5407ff2644e5a0fb2f62b17ff1f7baae304a1185..92ef55b724b1532428069106eeb59f2f8cc493cd 100755 --- a/scripts/extract_po.sh +++ b/scripts/extract_po.sh @@ -3,16 +3,17 @@ # syntax: # extract-po.sh +# No -j on first line, to clear out .pot file (Issue#1170) # messages.po is server side strings -xgettext -j --keyword=_ -L Perl --output-dir=locale/templates/LC_MESSAGES --from-code=utf-8 --output=messages.pot\ - `find lib -name '*.js' | grep -v 'i18n.js'` +xgettext --keyword=_ -L Perl --output-dir=locale/templates/LC_MESSAGES --from-code=utf-8 --output=messages.pot\ + `find lib -name '*.js' | grep -v 'i18n.js' | grep -v jwcrypto` xgettext -j -L PHP --keyword=_ --output-dir=locale/templates/LC_MESSAGES --output=messages.pot `find resources/views -name '*.ejs'` xgettext -j -L PHP --keyword=_ --output-dir=locale/templates/LC_MESSAGES --output=messages.pot `find lib/browserid -name '*.ejs'` # client.po # js -xgettext -j -L Perl --output-dir=locale/templates/LC_MESSAGES --from-code=utf-8 --output=client.pot\ +xgettext -L Perl --output-dir=locale/templates/LC_MESSAGES --from-code=utf-8 --output=client.pot\ `find resources/static -name '*.js' | grep -v /lib/ | grep -v /build/ | grep -v /production/ | grep -v 'gettext.js'` xgettext -j -L Perl --output-dir=locale/templates/LC_MESSAGES --output=client.pot `find resources/static/dialog/ -name '*.js' | grep -v include.js` # ejs diff --git a/scripts/test_db_connectivity.js b/scripts/test_db_connectivity.js index 3b1bad4283c7252c0c3f8b99466585097d85c43b..364d44a759b6846e1a3fd3fdfe383353c70ec4ef 100755 --- a/scripts/test_db_connectivity.js +++ b/scripts/test_db_connectivity.js @@ -21,8 +21,8 @@ var dbCfg = configuration.get('database'); // don't bother creating the schema delete dbCfg.create_schema; -db.open(dbCfg, function (r) { - if (r && r.message === "Unknown database 'browserid'") r = undefined; +db.open(dbCfg, function (err, r) { + if (err && err.message === "Unknown database 'browserid'") r = undefined; function end() { process.exit(r === undefined ? 0 : 1); } if (r === undefined) db.close(end); else end(); diff --git a/tests/cache-header-tests.js b/tests/cache-header-tests.js index 0b772e610625229309d55c6754199410e8229b35..690af835b47845bf58764b9a58668b2665b9f603 100755 --- a/tests/cache-header-tests.js +++ b/tests/cache-header-tests.js @@ -93,6 +93,7 @@ suite.addBatch({ '/sign_in': hasProperCacheHeaders('/sign_in'), '/communication_iframe': hasProperCacheHeaders('/communication_iframe'), '/unsupported_dialog': hasProperCacheHeaders('/unsupported_dialog'), + '/cookies_disabled': hasProperCacheHeaders('/cookies_disabled'), '/relay': hasProperCacheHeaders('/relay'), '/authenticate_with_primary': hasProperCacheHeaders('/authenticate_with_primary'), '/signup': hasProperCacheHeaders('/signup'), diff --git a/tests/cookie-session-security-test.js b/tests/cookie-session-security-test.js index 9ca0f4d109ba31bce04acffe5187d061b234fc60..dd3466090546e782684210debf352d2e15e024e4 100755 --- a/tests/cookie-session-security-test.js +++ b/tests/cookie-session-security-test.js @@ -52,7 +52,7 @@ suite.addBatch({ wsapi.clearCookies(); // mess up the cookie - var the_match = first_cookie.match(/browserid_state=([^;]*);/); + var the_match = first_cookie.match(/browserid_state(?:_[a-z0-9]+)?=([^;]*);/); assert.isNotNull(the_match); var new_cookie_val = the_match[1].substring(0, the_match[1].length - 1); wsapi.injectCookies({browserid_state: new_cookie_val}); diff --git a/tests/db-test.js b/tests/db-test.js index e25def6e19a64badc16fb1248086f5382b79c62a..65abaffa2926f260fd0157a7ee27dbf9f36d5565 100755 --- a/tests/db-test.js +++ b/tests/db-test.js @@ -36,8 +36,8 @@ suite.addBatch({ topic: function() { db.open(dbCfg, this.callback); }, - "and its ready": function(r) { - assert.isUndefined(r); + "and its ready": function(err) { + assert.isNull(err); }, "doesn't prevent onReady": { topic: function() { db.onReady(this.callback); }, @@ -54,7 +54,8 @@ suite.addBatch({ topic: function() { db.isStaged('lloyd@nowhe.re', this.callback); }, - "isStaged returns false": function (r) { + "isStaged returns false": function (err, r) { + assert.isNull(err); assert.isFalse(r); } }, @@ -62,7 +63,8 @@ suite.addBatch({ topic: function() { db.emailKnown('lloyd@nowhe.re', this.callback); }, - "emailKnown returns false": function (r) { + "emailKnown returns false": function (err, r) { + assert.isNull(err); assert.isFalse(r); } } @@ -73,13 +75,14 @@ suite.addBatch({ topic: function() { db.stageUser('lloyd@nowhe.re', this.callback); }, - "staging returns a valid secret": function(r) { + "staging returns a valid secret": function(err, r) { + assert.isNull(err); secret = r; assert.isString(secret); assert.strictEqual(secret.length, 48); }, "fetch email for given secret": { - topic: function(secret) { + topic: function(err, secret) { db.emailForVerificationSecret(secret, this.callback); }, "matches expected email": function(err, r) { @@ -87,10 +90,11 @@ suite.addBatch({ } }, "fetch secret for email": { - topic: function(secret) { + topic: function(err, secret) { db.verificationSecretForEmail('lloyd@nowhe.re', this.callback); }, - "matches expected secret": function(storedSecret) { + "matches expected secret": function(err, storedSecret) { + assert.isNull(err); assert.strictEqual(storedSecret, secret); } } @@ -102,7 +106,8 @@ suite.addBatch({ topic: function() { db.isStaged('lloyd@nowhe.re', this.callback); }, - " as staged after it is": function (r) { + " as staged after it is": function (err, r) { + assert.isNull(err); assert.strictEqual(r, true); } }, @@ -110,7 +115,8 @@ suite.addBatch({ topic: function() { db.emailKnown('lloyd@nowhe.re', this.callback); }, - " as known when it is only staged": function (r) { + " as known when it is only staged": function (err, r) { + assert.isNull(err); assert.strictEqual(r, false); } } @@ -121,8 +127,8 @@ suite.addBatch({ topic: function() { db.gotVerificationSecret(secret, 'fakepasswordhash', this.callback); }, - "gotVerificationSecret completes without error": function (r) { - assert.strictEqual(r, undefined); + "gotVerificationSecret completes without error": function (err, r) { + assert.isNull(err); } } }); @@ -132,7 +138,8 @@ suite.addBatch({ topic: function() { db.isStaged('lloyd@nowhe.re', this.callback); }, - "as staged immediately after its verified": function (r) { + "as staged immediately after its verified": function (err, r) { + assert.isNull(err); assert.strictEqual(r, false); } }, @@ -140,7 +147,8 @@ suite.addBatch({ topic: function() { db.emailKnown('lloyd@nowhe.re', this.callback); }, - "when it is": function (r) { + "when it is": function (err, r) { + assert.isNull(err); assert.strictEqual(r, true); } } @@ -150,11 +158,12 @@ suite.addBatch({ "checkAuth returns": { topic: function() { var cb = this.callback; - db.emailToUID('lloyd@nowhe.re', function(uid) { + db.emailToUID('lloyd@nowhe.re', function(err, uid) { db.checkAuth(uid, cb); }); }, - "the correct password": function(r) { + "the correct password": function(err, r) { + assert.isNull(err); assert.strictEqual(r, "fakepasswordhash"); } } @@ -165,14 +174,16 @@ suite.addBatch({ topic: function() { db.emailToUID('lloyd@nowhe.re', this.callback); }, - "returns a valid userid": function(r) { + "returns a valid userid": function(err, r) { + assert.isNull(err); assert.isNumber(r); }, "returns a UID": { - topic: function(uid) { + topic: function(err, uid) { db.userOwnsEmail(uid, 'lloyd@nowhe.re', this.callback); }, - "that owns the original email": function(r) { + "that owns the original email": function(err, r) { + assert.isNull(err); assert.ok(r); } } @@ -180,38 +191,48 @@ suite.addBatch({ }); suite.addBatch({ - "getting a UID, then": { + "getting a UID": { topic: function() { db.emailToUID('lloyd@nowhe.re', this.callback); }, - "staging an email": { - topic: function(uid) { + "does not error": function(err, uid) { + assert.isNull(err); + }, + "then staging an email": { + topic: function(err, uid) { db.stageEmail(uid, 'lloyd@somewhe.re', this.callback); }, - "yields a valid secret": function(secret) { + "yields a valid secret": function(err, secret) { + assert.isNull(err); assert.isString(secret); assert.strictEqual(secret.length, 48); }, "then": { - topic: function(secret) { + topic: function(err, secret) { var cb = this.callback; - db.isStaged('lloyd@somewhe.re', function(r) { cb(secret, r); }); + db.isStaged('lloyd@somewhe.re', function(err, r) { cb(secret, r); }); }, "makes it visible via isStaged": function(sekret, r) { assert.isTrue(r); }, "lets you verify it": { topic: function(secret, r) { db.gotVerificationSecret(secret, undefined, this.callback); }, - "successfully": function(r) { - assert.isUndefined(r); + "successfully": function(err, r) { + assert.isNull(err); }, "and knownEmail": { topic: function() { db.emailKnown('lloyd@somewhe.re', this.callback); }, - "returns true": function(r) { assert.isTrue(r); } + "returns true": function(err, r) { + assert.isNull(err); + assert.isTrue(r); + } }, "and isStaged": { topic: function() { db.isStaged('lloyd@somewhe.re', this.callback); }, - "returns false": function(r) { assert.isFalse(r); } + "returns false": function(err, r) { + assert.isNull(err); + assert.isFalse(r); + } } } } @@ -226,7 +247,8 @@ suite.addBatch({ topic: function() { db.emailsBelongToSameAccount('lloyd@nowhe.re', 'lloyd@somewhe.re', this.callback); }, - "when they do": function(r) { + "when they do": function(err, r) { + assert.isNull(err); assert.isTrue(r); } }, @@ -234,7 +256,8 @@ suite.addBatch({ topic: function() { db.emailsBelongToSameAccount('lloyd@anywhe.re', 'lloyd@somewhe.re', this.callback); }, - "when they don't": function(r) { + "when they don't": function(err, r) { + assert.isNull(err); assert.isFalse(r); } } @@ -246,7 +269,8 @@ suite.addBatch({ topic: function() { db.emailType('lloyd@anywhe.re', this.callback); }, - "is null": function (r) { + "is null": function (err, r) { + assert.isNull(err); assert.isUndefined(r); } }, @@ -254,7 +278,8 @@ suite.addBatch({ topic: function() { db.emailType('lloyd@somewhe.re', this.callback); }, - "is 'secondary'": function (r) { + "is 'secondary'": function (err, r) { + assert.isNull(err); assert.strictEqual(r, 'secondary'); } }, @@ -262,7 +287,8 @@ suite.addBatch({ topic: function() { db.emailType('lloyd@nowhe.re', this.callback); }, - "is 'secondary'": function (r) { + "is 'secondary'": function (err, r) { + assert.isNull(err); assert.strictEqual(r, 'secondary'); } } @@ -272,18 +298,20 @@ suite.addBatch({ "removing an existing email": { topic: function() { var cb = this.callback; - db.emailToUID("lloyd@somewhe.re", function(uid) { + db.emailToUID("lloyd@somewhe.re", function(err, uid) { db.removeEmail(uid, "lloyd@nowhe.re", cb); }); }, - "returns no error": function(r) { + "returns no error": function(err, r) { + assert.isNull(err); assert.isUndefined(r); }, "causes emailKnown": { topic: function() { db.emailKnown('lloyd@nowhe.re', this.callback); }, - "to return false": function (r) { + "to return false": function (err, r) { + assert.isNull(err); assert.strictEqual(r, false); } } @@ -295,14 +323,15 @@ suite.addBatch({ topic: function() { db.createUserWithPrimaryEmail("lloyd@primary.domain", this.callback); }, - "returns no error": function(r) { - assert.isUndefined(r); + "returns no error": function(err, r) { + assert.isNull(err); }, "causes emailKnown": { topic: function() { db.emailKnown('lloyd@primary.domain', this.callback); }, - "to return true": function (r) { + "to return true": function (err, r) { + assert.isNull(err); assert.strictEqual(r, true); } }, @@ -310,7 +339,8 @@ suite.addBatch({ topic: function() { db.emailType('lloyd@primary.domain', this.callback); }, - "to return 'primary'": function (r) { + "to return 'primary'": function (err, r) { + assert.isNull(err); assert.strictEqual(r, 'primary'); } } @@ -321,18 +351,19 @@ suite.addBatch({ "adding a primary email to that account": { topic: function() { var cb = this.callback; - db.emailToUID('lloyd@primary.domain', function(uid) { + db.emailToUID('lloyd@primary.domain', function(err, uid) { db.addPrimaryEmailToAccount(uid, "lloyd2@primary.domain", cb); }); }, - "returns no error": function(r) { - assert.isUndefined(r); + "returns no error": function(err) { + assert.isNull(err); }, "causes emailKnown": { topic: function() { db.emailKnown('lloyd2@primary.domain', this.callback); }, - "to return true": function (r) { + "to return true": function (err, r) { + assert.isNull(err); assert.strictEqual(r, true); } }, @@ -340,7 +371,8 @@ suite.addBatch({ topic: function() { db.emailType('lloyd@primary.domain', this.callback); }, - "to return 'primary'": function (r) { + "to return 'primary'": function (err, r) { + assert.isNull(err); assert.strictEqual(r, 'primary'); } } @@ -348,18 +380,19 @@ suite.addBatch({ "adding a primary email to an account with only secondaries": { topic: function() { var cb = this.callback; - db.emailToUID('lloyd@somewhe.re', function(uid) { + db.emailToUID('lloyd@somewhe.re', function(err, uid) { db.addPrimaryEmailToAccount(uid, "lloyd3@primary.domain", cb); }); }, - "returns no error": function(r) { - assert.isUndefined(r); + "returns no error": function(err) { + assert.isNull(err); }, "causes emailKnown": { topic: function() { db.emailKnown('lloyd3@primary.domain', this.callback); }, - "to return true": function (r) { + "to return true": function (err, r) { + assert.isNull(err); assert.strictEqual(r, true); } }, @@ -367,7 +400,8 @@ suite.addBatch({ topic: function() { db.emailType('lloyd3@primary.domain', this.callback); }, - "to return 'primary'": function (r) { + "to return 'primary'": function (err, r) { + assert.isNull(err); assert.strictEqual(r, 'primary'); } } @@ -378,18 +412,19 @@ suite.addBatch({ "adding a registered primary email to an account": { topic: function() { var cb = this.callback; - db.emailToUID('lloyd@primary.domain', function(uid) { + db.emailToUID('lloyd@primary.domain', function(err, uid) { db.addPrimaryEmailToAccount(uid, "lloyd3@primary.domain", cb); }); }, - "returns no error": function(r) { - assert.isUndefined(r); + "returns no error": function(err) { + assert.isNull(err); }, "and emailKnown": { topic: function() { db.emailKnown('lloyd3@primary.domain', this.callback); }, - "still returns true": function (r) { + "still returns true": function (err, r) { + assert.isNull(err); assert.strictEqual(r, true); } }, @@ -397,7 +432,8 @@ suite.addBatch({ topic: function() { db.emailType('lloyd@primary.domain', this.callback); }, - "still returns 'primary'": function (r) { + "still returns 'primary'": function (err, r) { + assert.isNull(err); assert.strictEqual(r, 'primary'); } }, @@ -405,7 +441,8 @@ suite.addBatch({ topic: function() { db.emailsBelongToSameAccount('lloyd3@primary.domain', 'lloyd@somewhe.re', this.callback); }, - "from original account": function(r) { + "from original account": function(err, r) { + assert.isNull(err); assert.isFalse(r); } }, @@ -413,7 +450,8 @@ suite.addBatch({ topic: function() { db.emailsBelongToSameAccount('lloyd3@primary.domain', 'lloyd@primary.domain', this.callback); }, - "to new account": function(r) { + "to new account": function(err, r) { + assert.isNull(err); assert.isTrue(r); } } @@ -424,18 +462,19 @@ suite.addBatch({ "canceling an account": { topic: function() { var cb = this.callback; - db.emailToUID("lloyd@somewhe.re", function(uid) { + db.emailToUID("lloyd@somewhe.re", function(err, uid) { db.cancelAccount(uid, cb); }); }, - "returns no error": function(r) { - assert.isUndefined(r); + "returns no error": function(err) { + assert.isNull(err); }, "causes emailKnown": { topic: function() { db.emailKnown('lloyd@somewhe.re', this.callback); }, - "to return false": function (r) { + "to return false": function (err, r) { + assert.isNull(err); assert.strictEqual(r, false); } } @@ -448,21 +487,21 @@ suite.addBatch({ db.close(this.callback); }, "should work": function(err) { - assert.isUndefined(err); + assert.isNull(err); }, "re-opening the database": { topic: function() { db.open(dbCfg, this.callback); }, - "works": function(r) { - assert.isUndefined(r); + "works": function(err) { + assert.isNull(err); }, "and then purging": { topic: function() { db.closeAndRemove(this.callback); }, "works": function(r) { - assert.isUndefined(r); + assert.isNull(r); } } } diff --git a/tests/email-throttling-test.js b/tests/email-throttling-test.js index db6a7d1a38193463945238af2bcc1bd0626bf402..ba807a7829aa692ca705946e767ec9c461a7a7de 100755 --- a/tests/email-throttling-test.js +++ b/tests/email-throttling-test.js @@ -52,7 +52,7 @@ suite.addBatch({ site:'fakesite.com' }), "is throttled": function(err, r) { - assert.strictEqual(r.code, 403); + assert.strictEqual(r.code, 429); } } }); @@ -101,8 +101,8 @@ suite.addBatch({ email: 'second@fakeemail.com', site:'fakesite.com' }), - "is throttled with a 403": function(err, r) { - assert.strictEqual(r.code, 403); + "is throttled with a 429": function(err, r) { + assert.strictEqual(r.code, 429); } } }); diff --git a/tests/lib/start-stop.js b/tests/lib/start-stop.js index d7485e469285845f34916b3f1d7a34675a3f0849..895ae04f4ec1c039b32a590e8d0351084cf5acfd 100644 --- a/tests/lib/start-stop.js +++ b/tests/lib/start-stop.js @@ -103,7 +103,7 @@ exports.addStartupBatches = function(suite) { db.open(cfg, this.callback); }, "should work fine": function(r) { - assert.isUndefined(r); + assert.isNull(r); } } }); @@ -184,7 +184,7 @@ exports.addShutdownBatches = function(suite) { db.closeAndRemove(this.callback); }, "should work": function(err) { - assert.isUndefined(err); + assert.isNull(err); } } }); diff --git a/tests/no-cookie-test.js b/tests/no-cookie-test.js index 6a030d218fd27499e63d6746c5591b90419bc431..150d92cd448d862b228f2736abb5f86c9bec703d 100755 --- a/tests/no-cookie-test.js +++ b/tests/no-cookie-test.js @@ -88,10 +88,10 @@ suite.addBatch({ })); req.end(); }, - "returns a 400 with 'no cookie' as the body": function(err, r) { + "returns a 403 with 'no cookie' as the body": function(err, r) { assert.equal(err, null); - assert.equal(r.code, 400); - assert.equal(r.body, 'Bad Request: no cookie'); + assert.equal(r.code, 403); + assert.equal(r.body, 'Forbidden: no cookie'); } } }); diff --git a/tests/page-requests-test.js b/tests/page-requests-test.js index e26a5892ceec944eb4e8e85ae4bab80828697fed..0ee2537c058c543df04c5aabcc707c40fbda5a23 100755 --- a/tests/page-requests-test.js +++ b/tests/page-requests-test.js @@ -67,6 +67,7 @@ suite.addBatch({ 'GET /.well-known/browserid': respondsWith(200), 'GET /signin': respondsWith(200), 'GET /unsupported_dialog': respondsWith(200), + 'GET /cookies_disabled': respondsWith(200), 'GET /developers': respondsWith(200), 'GET /manage': respondsWith(302), 'GET /users': respondsWith(302), @@ -75,7 +76,7 @@ suite.addBatch({ 'GET /primaries/': respondsWith(302), 'GET /developers': respondsWith(302), 'GET /developers/': respondsWith(302), - 'GET /test': respondsWith(200), + 'GET /test': respondsWith(301), 'GET /test/': respondsWith(200), 'GET /include.js': respondsWith(200), 'GET /include.orig.js': respondsWith(200) diff --git a/tests/password-bcrypt-update-test.js b/tests/password-bcrypt-update-test.js index db6a994957ec49641568b68d3ccc43655e9f2f75..c403d6ef715d1250b6671128abd9a64b1b7af923 100755 --- a/tests/password-bcrypt-update-test.js +++ b/tests/password-bcrypt-update-test.js @@ -87,11 +87,12 @@ suite.addBatch({ "the password": { topic: function() { var cb = this.callback; - db.emailToUID(TEST_EMAIL, function(uid) { + db.emailToUID(TEST_EMAIL, function(err, uid) { db.checkAuth(uid, cb); }); }, - "is bcrypted with the expected number of rounds": function(r) { + "is bcrypted with the expected number of rounds": function(err, r) { + assert.isNull(err); assert.equal(typeof r, 'string'); assert.equal(config.get('bcrypt_work_factor'), bcrypt.get_rounds(r)); } @@ -134,11 +135,12 @@ suite.addBatch({ "if we recheck the auth hash": { topic: function() { var cb = this.callback; - db.emailToUID(TEST_EMAIL, function(uid) { + db.emailToUID(TEST_EMAIL, function(err, uid) { db.checkAuth(uid, cb); }); }, - "its bcrypted with 8 rounds": function(r) { + "its bcrypted with 8 rounds": function(err, r) { + assert.isNull(err); assert.equal(typeof r, 'string'); assert.equal(8, bcrypt.get_rounds(r)); } diff --git a/tests/stalled-mysql-test.js b/tests/stalled-mysql-test.js new file mode 100755 index 0000000000000000000000000000000000000000..a0753552763a36b9a1f434c1986e47b64cee4ca8 --- /dev/null +++ b/tests/stalled-mysql-test.js @@ -0,0 +1,379 @@ +#!/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'); + +if (process.env['NODE_ENV'] != 'test_mysql') process.exit(0); + +const assert = require('assert'), +vows = require('vows'), +start_stop = require('./lib/start-stop.js'), +wsapi = require('./lib/wsapi.js'), +temp = require('temp'), +fs = require('fs'), +jwk = require('jwcrypto/jwk.js'), +jwt = require('jwcrypto/jwt.js'), +vep = require('jwcrypto/vep.js'), +jwcert = require('jwcrypto/jwcert.js'), +path = require('path'); + +var suite = vows.describe('forgotten-email'); + +// disable vows (often flakey?) async error behavior +suite.options.error = false; + +// let's reduce the amount of time allowed for queries, so that +// we get a faster failure and tests run quicker +process.env['MAX_QUERY_TIME_MS'] = 250; + +// and let's instruct children to pretend as if the driver is +// stalled if a file exists +var stallFile = temp.path({suffix: '.stall'}); +process.env['STALL_MYSQL_WHEN_PRESENT'] = stallFile; + +start_stop.addStartupBatches(suite); + +// ever time a new token is sent out, let's update the global +// var 'token' +var token = undefined; + +function addStallDriverBatch(stall) { + suite.addBatch({ + "changing driver state": { + topic: function() { + if (stall) fs.writeFileSync(stallFile, ""); + else fs.unlinkSync(stallFile); + + // After changing the file which indicates to child + // processes whether the driver should simulate a stalled + // state or not, we need to wait for them to detect the + // change. because we use `fs.watchFile()` on a short poll, + // this should be nearly instantaneous. 300ms is a magic number + // which is hoped to allow plenty of time even on a loaded + // machine + setTimeout(this.callback, 300); + }, + "completes": function(err, r) { } + } + }); +} + +// first stall mysql +addStallDriverBatch(true); + +// call session context once to populate CSRF stuff in the +// wsapi client lib +suite.addBatch({ + "get context": { + topic: wsapi.get('/wsapi/session_context'), + "works" : function(err, r) { + assert.isNull(err); + } + } +}); + +// now try all apis that can be excercised without further setup +suite.addBatch({ + "address_info": { + topic: wsapi.get('/wsapi/address_info', { + email: 'test@example.domain' + }), + "works": function(err, r) { + // address info with a primary address doesn't need db access. + assert.strictEqual(r.code, 200); + } + }, + "address_info": { + topic: wsapi.get('/wsapi/address_info', { + email: 'test@non-existant.domain' + }), + "fails with 503": function(err, r) { + assert.strictEqual(r.code, 503); + } + }, + "have_email": { + topic: wsapi.get('/wsapi/have_email', { + email: 'test@example.com' + }), + "fails with 503": function(err, r) { + assert.strictEqual(r.code, 503); + } + }, + "authenticate_user": { + topic: wsapi.post('/wsapi/authenticate_user', { + email: 'test@example.com', + pass: 'oogabooga' + }), + "fails with 503": function(err, r) { + assert.strictEqual(r.code, 503); + } + }, + "complete_email_addition": { + topic: wsapi.post('/wsapi/complete_email_addition', { + token: 'bogus' + }), + "fails with 503": function(err, r) { + assert.strictEqual(r.code, 503); + } + }, + "complete_user_creation": { + topic: wsapi.post('/wsapi/complete_user_creation', { + token: 'bogus', + pass: 'fakefake' + }), + "fails with 503": function(err, r) { + assert.strictEqual(r.code, 503); + } + }, + "email_for_token": { + topic: wsapi.get('/wsapi/email_for_token', { + token: 'bogus' + }), + "fails with 503": function(err, r) { + assert.strictEqual(r.code, 503); + } + }, + "stage_user": { + topic: wsapi.post('/wsapi/stage_user', { + email: 'bogus@bogus.edu', + site: 'whatev.er' + }), + "fails with 503": function(err, r) { + assert.strictEqual(r.code, 503); + } + } +}); + +// now unstall the driver, we'll create an account and sign in in +// order to test the behavior of the remaining APIs when the database +// is stalled +addStallDriverBatch(false); + +var token = undefined; + +suite.addBatch({ + "account staging": { + topic: wsapi.post('/wsapi/stage_user', { + email: "stalltest@whatev.er", + site: 'fakesite.com' + }), + "works": function(err, r) { + assert.equal(r.code, 200); + } + } +}); + +suite.addBatch({ + "a token": { + topic: function() { + start_stop.waitForToken(this.callback); + }, + "is obtained": function (t) { + assert.strictEqual(typeof t, 'string'); + }, + "setting password": { + topic: function(token) { + wsapi.post('/wsapi/complete_user_creation', { + token: token, + pass: "somepass" + }).call(this); + }, + "works just fine": function(err, r) { + assert.equal(r.code, 200); + } + } + } +}); + +// re-stall mysql +addStallDriverBatch(true); + +// test remaining wsapis + +suite.addBatch({ + "account_cancel": { + topic: wsapi.post('/wsapi/account_cancel', { }), + "fails with 503": function(err, r) { + assert.strictEqual(r.code, 503); + } + }, + "cert_key": { + topic: wsapi.post('/wsapi/cert_key', { + email: "test@whatev.er", + pubkey: "bogus" + }), + "fails with 503": function(err, r) { + assert.strictEqual(r.code, 503); + } + }, + "email_addition_status": { + topic: wsapi.get('/wsapi/email_addition_status', { + email: "test@whatev.er" + }), + "fails with 503": function(err, r) { + assert.strictEqual(r.code, 503); + } + }, + "list_emails": { + topic: wsapi.get('/wsapi/list_emails', {}), + "fails with 503": function(err, r) { + assert.strictEqual(r.code, 503); + } + }, + "remove_email": { + topic: wsapi.post('/wsapi/remove_email', { + email: "test@whatev.er" + }), + "fails with 503": function(err, r) { + assert.strictEqual(r.code, 503); + } + }, + "session_context": { + topic: wsapi.get('/wsapi/session_context', { }), + "fails with 503": function(err, r) { + assert.strictEqual(r.code, 503); + } + }, + "stage_email": { + topic: wsapi.post('/wsapi/stage_email', { + email: "test2@whatev.er", + site: "foo.com" + }), + "fails with 503": function(err, r) { + assert.strictEqual(r.code, 503); + } + }, + "update_password": { + topic: wsapi.post('/wsapi/update_password', { + oldpass: "oldpassword", + newpass: "newpassword" + }), + "fails with 503": function(err, r) { + assert.strictEqual(r.code, 503); + } + }, + "user_creation_status": { + topic: wsapi.get('/wsapi/user_creation_status', { + email: "test3@whatev.er" + }), + "fails with 503": function(err, r) { + assert.strictEqual(r.code, 503); + } + } +}); + +// now let's test apis that require an assertion, and only after verifying +// that, hit the database +const TEST_DOMAIN = 'example.domain', + TEST_EMAIL = 'testuser@' + TEST_DOMAIN, + TEST_ORIGIN = 'http://127.0.0.1:10002', + TEST_FIRST_ACCT = 'testuser@fake.domain'; + +var g_keypair, g_cert, g_assertion; + +suite.addBatch({ + "generating a keypair": { + topic: function() { + return jwk.KeyPair.generate("DS", 256) + }, + "succeeds": function(r, err) { + assert.isObject(r); + assert.isObject(r.publicKey); + assert.isObject(r.secretKey); + g_keypair = r; + } + } +}); + +var g_privKey = jwk.SecretKey.fromSimpleObject( + JSON.parse(require('fs').readFileSync( + path.join(__dirname, '..', 'example', 'primary', 'sample.privatekey')))); + + +suite.addBatch({ + "generting a certificate": { + topic: function() { + var domain = process.env['SHIMMED_DOMAIN']; + + var expiration = new Date(); + expiration.setTime(new Date().valueOf() + 60 * 60 * 1000); + g_cert = new jwcert.JWCert(TEST_DOMAIN, expiration, new Date(), + g_keypair.publicKey, {email: TEST_EMAIL}).sign(g_privKey); + return g_cert; + }, + "works swimmingly": function(cert, err) { + assert.isString(cert); + assert.lengthOf(cert.split('.'), 3); + } + } +}); + +suite.addBatch({ + "generating an assertion": { + topic: function() { + var expirationDate = new Date(new Date().getTime() + (2 * 60 * 1000)); + var tok = new jwt.JWT(null, expirationDate, TEST_ORIGIN); + return vep.bundleCertsAndAssertion([g_cert], tok.sign(g_keypair.secretKey)); + }, + "succeeds": function(r, err) { + assert.isString(r); + g_assertion = r; + } + } +}); + +// finally! we have our assertion in g_assertion +suite.addBatch({ + "add_email_with_assertion": { + topic: function() { + wsapi.post('/wsapi/add_email_with_assertion', { + assertion: g_assertion + }).call(this); + }, + "fails with 503": function(err, r) { + assert.strictEqual(r.code, 503); + } + }, + "auth_with_assertion": { + topic: function() { + wsapi.post('/wsapi/auth_with_assertion', { + assertion: g_assertion + }).call(this); + }, + "fails with 503": function(err, r) { + assert.strictEqual(r.code, 503); + } + }, + "create_account_with_assertion": { + topic: function() { + wsapi.post('/wsapi/create_account_with_assertion', { + assertion: g_assertion + }).call(this); + }, + "fails with 503": function(err, r) { + assert.strictEqual(r.code, 503); + } + } +}); + +// logout doesn't need database, it should still succeed +suite.addBatch({ + "logout": { + topic: wsapi.post('/wsapi/logout', { }), + "succeeds": function(err, r) { + assert.strictEqual(r.code, 200); + } + } +}); + +// finally, unblock mysql so we can shut down +addStallDriverBatch(false); + +start_stop.addShutdownBatches(suite); + +// run or export the suite. +if (process.argv[1] === __filename) suite.run(); +else suite.export(module); diff --git a/tests/static-resource-test.js b/tests/static-resource-test.js new file mode 100755 index 0000000000000000000000000000000000000000..f7fa6603da31aec4b64ed4706383f3a8ed82d449 --- /dev/null +++ b/tests/static-resource-test.js @@ -0,0 +1,68 @@ +#!/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'), + resources = require('../lib/static_resources'); + +var suite = vows.describe('cache header tests'); +suite.options.error = false; + +var locales = ['ar', 'de', 'en_US', 'fr']; +suite.addBatch({ + "All resources expand": { + topic: function () { + this.callback(resources.all(locales)); + }, + "We get stuff": function (files) { + var res = resources.resources; + assert.ok(files['/production/dialog.css'].length >= 3); + // Get ride of non-localized asset bundles + ['/production/dialog.css', '/production/browserid.css'].forEach( + function (nonLocaleAsset) { + delete res[nonLocaleAsset]; + delete files[nonLocaleAsset]; + }); + + // Keys expand + // files ['/production/:locale/dialog.js'] + // becomes ['/production/ar/dialog.js', 'production/de/dialog.js', ...] + assert.equal(Object.keys(files).length, + Object.keys(res).length * locales.length); + + // Let's use the first bundle + var minFile = Object.keys(files)[0]; + var minRes = Object.keys(res)[0]; + + // Number of files underneath stay the same + assert.equal(files[minFile].length, + res[minRes].length); + // Non-localized files underneath stay the same + [0, 1, 2, 3, 4, 5, 7].forEach(function (nonLocalizedIndex) { + assert.equal(files[minFile][nonLocalizedIndex], + res[minRes][nonLocalizedIndex]); + }); + // Fragile - filename with :locale... + // When fixing this test case... console.log(res[Object.keys(res)[0]]); + var localeIndex = 6; + assert.notEqual(files[minFile][localeIndex], + res[minRes][localeIndex]); + var counter = 0; + for (var key in res) { + res[key].forEach(function (item) { + counter++; + }); + } + assert.ok(counter > 90); + } + } +}); + +// run or export the suite. +if (process.argv[1] === __filename) suite.run(); +else suite.export(module); diff --git a/tests/verifier-test.js b/tests/verifier-test.js index cdafb345bd39d7b56a236bc88490599fb44330fe..b2a455dc04807502630199c49333d5a9a42e7572 100755 --- a/tests/verifier-test.js +++ b/tests/verifier-test.js @@ -721,7 +721,7 @@ function make_other_issuer_tests(new_style) { var fakeDomainKeypair = jwk.KeyPair.generate("RS", 64); var newClientKeypair = jwk.KeyPair.generate("DS", 256); expiration = new Date(new Date().getTime() + (1000 * 60 * 60 * 6)); - var cert = new jwcert.JWCert("lloyd.io", expiration, new Date(), newClientKeypair.publicKey, + var cert = new jwcert.JWCert("no.such.domain", expiration, new Date(), newClientKeypair.publicKey, {email: TEST_EMAIL}).sign(fakeDomainKeypair.secretKey); var expirationDate = new Date(new Date().getTime() + (2 * 60 * 1000)); @@ -744,7 +744,7 @@ function make_other_issuer_tests(new_style) { "to return a clear error message": function (err, r) { var resp = JSON.parse(r.body); assert.strictEqual(resp.status, 'failure'); - assert.strictEqual(resp.reason, "can't get public key for lloyd.io"); + assert.strictEqual(resp.reason, "can't get public key for no.such.domain"); } } };