Permalink
Browse files

Validation for human beings [closes #61]

Conflicts:
	controllers/baker.js
	lib/regex.js
  • Loading branch information...
brianloveswords committed Mar 19, 2012
2 parents fcaf2b2 + bbebcac commit 446a7d66e4baf3515604eab192e17c85915195ee
View
12 app.js
@@ -16,7 +16,6 @@ app.config = configuration;
// default view engine
app.set('view engine', 'hogan.js');
app.register('hogan.js', hoganadapter.init(hogan))
-app.register('.coffee', require('coffeekup').adapters.express)
// View helpers. `user` and `badges` are set so we can use them in `if`
// statements without getting undefined errors and without having to use typeof
@@ -29,17 +28,6 @@ app.helpers({
success: [],
badges: {},
reverse: router.reverse,
-
- hardcode: {
- textbox: function(attrs) {
- attrs.type = 'text';
- attrs.name = attrs.id;
- return div(function(){input(attrs)});
- },
- safe: function(val) {
- return text(val.replace(/</g, '&lt;').replace(/>/g, '&gt;'))
- }
- }
});
// Middleware. See `middleware.js` for more information on the custom
// middleware used.
View
@@ -79,6 +79,7 @@ exports.baker = function(req, res) {
recipient: email
}
awardBadge(opts, function (err, badge) {
+ console.dir(err);
if (err) res.setHeader('x-badge-awarded', 'false');
else res.setHeader('x-badge-awarded', badge.recipient);
return res.send(badgePNGdata);
View
@@ -1,4 +1,13 @@
-var fs = require('fs');
+var _ = require('underscore')
+ , request = require('request')
+ , fs = require('fs')
+ , logger = require('../lib/logging').logger
+ , reverse = require('../lib/router').reverse
+ , awardBadge = require('../lib/award')
+ , remote = require('../lib/remote')
+ , validator = require('validator')
+ , Badge = require('../models/badge.js')
+
var myFiles = [
"issuer-parts/issuer-script-intro.js"
@@ -12,26 +21,6 @@ myFiles = myFiles.map(function(filename) {
return __dirname + '/../static/js/' + filename;
});
-exports.generateScript = function(req, res) {
- concatenate(myFiles, function(err, data) {
- if (err) {
- res.send(500);
- throw err;
- } else {
- res.header('Content-Type', 'application/javascript');
- res.send(data);
- }
- });
-};
-
-exports.frame = function(req, res) {
- res.render('issuer-frame', {
- layout: null,
- csrfToken: req.session._csrf,
- email: req.session.emails && req.session.emails[0]
- });
-};
-
function concatenate(files, cb) {
var completed = 0;
var contents = [];
@@ -63,13 +52,25 @@ if (module.parent === null) {
});
}
-var request = require('request')
- , logger = require('../lib/logging').logger
- , reverse = require('../lib/router').reverse
- , awardBadge = require('../lib/award')
- , remote = require('../lib/remote')
- , validator = require('validator')
- , Badge = require('../models/badge.js')
+exports.generateScript = function(req, res) {
+ concatenate(myFiles, function(err, data) {
+ if (err) {
+ res.send(500);
+ throw err;
+ } else {
+ res.header('Content-Type', 'application/javascript');
+ res.send(data);
+ }
+ });
+};
+
+exports.frame = function(req, res) {
+ res.render('issuer-frame', {
+ layout: null,
+ csrfToken: req.session._csrf,
+ email: req.session.emails && req.session.emails[0]
+ });
+};
exports.issuerBadgeAddFromAssertion = function(req, res, next) {
@@ -189,3 +190,97 @@ exports.issuerBadgeAddFromAssertion = function(req, res, next) {
});
}); // end of the assertion grabbing badge adding.
};
+
+exports.validator = function (request, response) {
+ var assertion = request.query.assertion || (request.body && request.body.assertion)
+ var accept = request.headers['accept'];
+ var missingMsg = 'error: could not validate, could not find assertion in `data` field';
+ var status = 200;
+ var fields = {};
+
+ if (!assertion) {
+ status = 400;
+ fields = { general: missingMsg };
+ }
+ else {
+ try {
+ status = 200;
+ var o = JSON.parse(assertion);
+ if (!o.badge) o.badge = {}
+ if (!o.badge.issuer) o.badge.issuer = {}
+ var errors = Badge.validateBody( o );
+ if (errors) {
+ fields = errors.fields;
+ status = 400;
+ }
+ }
+
+ catch (err) {
+ status = 500;
+ if (err.name === "SyntaxError") {
+ fields = { general: "Could not parse this JSON blob. Make sure it is well formed and try again!" }
+ } else {
+ fields = { general: err.name + ": " + err.message }
+ }
+ }
+ }
+
+ function humanize (value, field) {
+ var url = 'Must either be a fully qualified URL (<code>http://example.com/path/evidence.html</code>) or begin with a forward slash (<code>/path/evidence.html</code>). <br> Non-qualified URLs will be prefixed with the origin specified in <code>badge.issuer.origin</code>';
+ var length = 'Cannot be longer than 128 characters'
+ var msgs = {
+ recipient: 'Must be an email address (<code>someone@example.com</code>) or a hash (<code>sha256$1234567890abcdef</code>)',
+ evidence: url,
+ "badge.version": 'Must be in the form of <code>x.y</code> or <code>x.y.z</code>',
+ "badge.name": length,
+ "badge.description": length,
+ "badge.image": url.replace(/evidence.html/g, 'image.png'),
+ "badge.criteria": url.replace(/evidence.html/g, 'criteria.html'),
+ "badge.issuer.name": length,
+ "badge.issuer.org": length,
+ "badge.issuer.contact": 'Must be an email address',
+ "badge.issuer.origin": 'Must be a fully qualified origin (<code>http://example.com</code>)'
+ }
+ if (value.match(/invalid/)) value = msgs[field] || value;
+ return {field: field, value: value};
+ }
+
+ var responder = {
+ 'text/plain': function () {
+ response.contentType('txt');
+ if (!_.isEmpty(fields)) {
+ var values = _.values(fields);
+ var bullets = _.map(values, function(s){return '* ' + s;}).join('\n');
+ return response.send(bullets, status);
+ }
+ else {
+ return response.send('everything looks good', 200);
+ }
+ },
+
+ 'application/json': function () {
+ response.contentType('json');
+ if (fields) {
+ return response.json({ status: 'error', errors: _.map(fields, humanize) }, status);
+ }
+ else {
+ return response.json({ status: 'okay' }, 200);
+ }
+ },
+
+ 'default': function () {
+ var fielderrors = _.map(fields, humanize);
+ return response.render('validator', {
+ status: 200,
+ errors: fielderrors,
+ csrfToken: request.session._csrf,
+ submitted: !!assertion,
+ success: assertion && _.isEmpty(fields),
+ assertion: assertion
+ });
+ }
+ };
+
+ if (!responder[accept]) accept = 'default';
+ return responder[accept]();
+};
View
@@ -1,6 +1,6 @@
exports.url = /(^(https?):\/\/[^\s\/$.?#].[^\s]*$)|(^\/\S+$)/;
exports.email = /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/;
-exports.origin = /^(https?):\/\/[^\s\/$.?#].[^\s\/]*$/;
+exports.origin = /^(https?):\/\/[^\s\/$.?#].[^\s\/]*\/?$/;
exports.version = /^v?\d+\.\d+(\.\d+)?$/;
exports.date = /(^\d{4}-\d{2}-\d{2}$)|(^\d{1,10}$)/;
exports.emailOrHash = /([a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)|((sha1|sha256|sha512|md5)\$[a-fA-F0-9]+)/;
View
@@ -142,7 +142,7 @@ Badge.validateBody = function (body) {
missing: function (fieldStr) {
var field = fieldFromDottedString(fieldStr, body);
if (!field) {
- err.fields[fieldStr] = 'missing email address for `' + fieldStr + '`';
+ err.fields[fieldStr] = 'missing required field: `' + fieldStr + '`';
}
},
regexp: function (fieldStr, type) {
@@ -183,7 +183,7 @@ Badge.validateBody = function (body) {
} else {
test.missing('badge.issuer.origin');
test.missing('badge.issuer.name');
- test.regexp('badge.issuer.origin', 'url');
+ test.regexp('badge.issuer.origin', 'origin');
test.regexp('badge.issuer.contact', 'email');
test.length('badge.issuer.org', 128);
test.length('badge.issuer.name', 128);
View
@@ -16,6 +16,7 @@
, "functools": "1.2.x"
, "hogan.js": "1.0.x"
, "metapng": "git+https://github.com/brianloveswords/metapng.js.git"
+ , "mime": "1.2.x"
, "mysql": "0.9.x"
, "request": "2.9.x"
, "soda": "0.2.5"
View
@@ -480,6 +480,23 @@ table.information {
line-height: 1.7em;
}
+#validator textarea {
+ width: 600px;
+ height: 300px;
+}
+
+#validator .submit {
+ margin: 1em 0;
+}
+
+#validator h2 {
+ margin: 0.25em 0;
+}
+
+#validator li strong {
+ color: #d00;
+}
+
body {
cursor: default;
}
@@ -6,7 +6,7 @@ var _ = require('underscore')
, mysql = require('../lib/mysql.js')
, map = require('functools').map
, utils = require('./utils')
- ,request = utils.conn.request
+ , request = utils.conn.request
, response = utils.conn.response
var user, badge, group;
View
@@ -19,8 +19,8 @@ var URLS = {
bad: ['-not-asdo', 'ftp://bad-scheme', '@.com:90/', 'just totally wrong']
};
var ORIGINS = {
- good: ['http://example.com', 'https://example.com:80', 'https://www.example.com', 'https://www.example.com:8080'],
- bad: URLS.bad
+ good: ['http://example.com', 'https://example.com:80', 'https://www.example.com', 'https://www.example.com:8080', 'http://example.com/'],
+ bad: ['-not-asdo', 'ftp://bad-scheme', '@.com:90/', 'just totally wrong', 'http://example.com/what', 'http://example.com:8080/false']
};
var DATES = {
good: [Date.now()/1000 | 0, '2012-01-01'],
View
@@ -0,0 +1,91 @@
+var vows = require('vows')
+var assert = require('assert')
+var should = require('should')
+var conmock = require('./conmock.js')
+
+
+
+vows.describe('Connection mocking').addBatch({
+ 'Mocker': {
+ '#send' : {
+ topic: function () {
+ function wut (request, response, next) { response.send('okay', 200) };
+ conmock(wut, {}, this.callback);
+ },
+ 'and knows what was sent' : function (err, mock) {
+ mock.fntype.should.equal('send');
+ mock.status.should.equal(200);
+ mock.body.should.equal('okay');
+ },
+ },
+ '#json' : {
+ topic: function () {
+ function wut (request, response, next) { response.json({message: 'yup'}, 200) };
+ conmock(wut, {}, this.callback);
+ },
+ 'and knows what was sent' : function (err, mock) {
+ mock.fntype.should.equal('json');
+ mock.status.should.equal(200);
+ mock.body.message.should.equal('yup');
+ },
+ },
+ '#render' : {
+ topic: function () {
+ function wut (request, response, next) {
+ response.statusCode = 'fffffffuuuuuuuuuuuu';
+ response.render('ohai', {some: "thing", status: 404})
+ };
+ conmock(wut, {}, this.callback);
+ },
+ 'and knows the options and the render path' : function (err, mock) {
+ mock.fntype.should.equal('render');
+ mock.path.should.equal('ohai');
+ mock.status.should.equal(404);
+ mock.options.some.should.equal('thing');
+ },
+ },
+ '#header' : {
+ topic: function () {
+ function wut (request, response, next) {
+ response.header('oh', 'hai');
+ response.send('okay')
+ };
+ conmock(wut, {}, this.callback);
+ },
+ 'and knows the options and the render path' : function (err, mock) {
+ mock.fntype.should.equal('send');
+ mock.status.should.equal(200);
+ mock.headers['oh'].should.equal('hai');
+ },
+ },
+ '#contentType' : {
+ 'given "json"': {
+ topic: function () {
+ function wut (request, response, next) { response.contentType('json'); response.send('okay') };
+ conmock(wut, {}, this.callback);
+ },
+ 'content-type should be "application/json"' : function (err, mock) {
+ mock.headers['Content-Type'].should.equal('application/json');
+ },
+ },
+ 'given "txt"': {
+ topic: function () {
+ function wut (request, response, next) { response.contentType('txt'); response.send('okay') };
+ conmock(wut, {}, this.callback);
+ },
+ 'content-type should be "text/plain"' : function (err, mock) {
+ mock.headers['Content-Type'].should.equal('text/plain');
+ },
+ },
+ 'given "html"': {
+ topic: function () {
+ function wut (request, response, next) { response.contentType('html'); response.send('okay') };
+ conmock(wut, {}, this.callback);
+ },
+ 'content-type should be "text/plain"' : function (err, mock) {
+ mock.headers['Content-Type'].should.equal('text/html');
+ },
+ }
+ },
+ }
+}).export(module);
Oops, something went wrong.

0 comments on commit 446a7d6

Please sign in to comment.