Home Reference Source

src/code/validate.js

/**
 * @desc Exports a router for `/code/validate/` used to validate, compile, and
 * create code records in database.
 * @since 0.1.0
 */
'use strict';

// Requirements
/**
 * @ignore
 */
const CeallogFunction = require('../models/CeallogFunction'),
	compileRequest = require('../compiler').compileRequest,
	dbResultCallback = require('../mongoose-util').dbResultCallback,
	HttpError = require('../classes/HttpError'),
	settings = require('../settings'),
	uriBlacklist = require('../uri-blacklist'),
	ValidateError = require('../classes/ValidateError');

// Constants
/**
 * @desc An object acting like a map where key represents the type of message
 * and the value is a string with the message itself.
 * @since 0.1.0
 */
const messages = {
	NAME_NOT_ALLOWED: '`name` value cannot be used. Please choose another.',
	NO_BODY: 'Required `body` field missing.',
	NO_LABEL: 'Required `label` field missing.',
	NO_NAME: 'Required `name` field missing.',
	INVALID_NAME: '`name` invalid: must be an allowed URL segment.',
	UNEXPECTED_DB_RESULT: 'Unexpected database result.'
};

// Functions
/**
 * @desc Checks `req.body` to make sure it contains `body`, `name`, and `label`
 * fields. If not, an error is sent via `res`. If so, the next function is
 * called.
 * @param {Object} req Express request object
 * @param {Object} res Express response object
 * @param {function} next Function to be called by Express next.
 * @since 0.1.0
 */
const handleBody = (req, res, next) => {
	let body = req.body;

	/* istanbul ignore else */
	if (body) {
		// If a body exists, check there is a `body`, `name`, and `label`. If
		// any values are missing, throw an error and respond with a 400.
		// Otherwise, assign `body` to `req.code` for use by the compiler.
		try {
			if (!body.body) {
				throw new ValidateError(messages.NO_BODY, 'NO_BODY');
			} else if (!body.name) {
				throw new ValidateError(messages.NO_NAME, 'NO_NAME');
			} else if (!body.label) {
				throw new ValidateError(messages.NO_LABEL, 'NO_LABEL');
			} else {
				req.code = body.body;
			}
		} catch (e) {
			return new HttpError(e, e.errorType, 400).sendError(res);
		}

		return next();
	} else {
		// If a body is missing somehow, throw a generic error, log it, swallow
		// it, and respond with a 500.
		try {
			throw new Error('Missing request body.');
		} catch (e) {
			return new HttpError(e).sendError(res);
		} finally {
			return;
		}
	}
};

/**
 * @desc Proceeds to next function if no compiler error occured. If an error did
 * occur, it is normalised as an `HttpError`, including location information if
 * available, then sent with a 400 status code.
 * @param {Object} req Express request object
 * @param {Object} res Express response object
 * @param {function} next Function to be called by Express next.
 * @since 0.1.0
 */
const handleCompilerResult = (req, res, next) => {
	let err;

	try {
		if (req.compiler.error) {
			throw new HttpError(req.compiler.error, 'COMPILATION_ERROR', 400);
		}
	} catch (e) {
		err = e;
	} finally {
		if (err) {
			err.sendError(res);
			return;
		}
		next();
	}
};

/**
 * @desc Saves code resource to mongodb.  Sends 201 response if successful,
 * error response otherwise.
 * @param {Object} req Express request object
 * @param {Object} res Express response object
 * @param {function} next Function to be called by Express next.
 * @since 0.1.0
 */
const save = (req, res, next) => {
	let resource = {
		body: req.code,
		compiled: typeof req.compiler.compiled == 'function',
		label: req.body.label,
		name: req.body.name
	};

	let ceallogFunction = new CeallogFunction(resource);

	ceallogFunction.save((err, results) => {
		req.validator = {};

		let dbResult = dbResultCallback(req.validator)(err, results);

		/* istanbul ignore if */
		if (err) {
			try {
				throw new ValidateError(dbResult.message, dbResult.type);
			} catch (e) {
				return new HttpError(e, dbResult.type, dbResult.statusCode).sendError(
					res
				);
			}
		} else {
			let response = {
				compiled: true,
				id: results._id,
				created_date: results.created_date,
				label: results.label,
				message: dbResult.message,
				name: results.name,
				published: false,
				service: `/${settings.cealloga.api_path}/${
					settings.cealloga.test_path
				}/${results._id}`
			};
			
			req.cache.add(req.compiler.compiled, response);

			res.statusCode = 201;
			res.json(response);
		}
	});

	return;
};

/**
 * @desc Checks whether `req.body.name` is in the URI blacklist and proceeds if
 * not. Sends a 400 response otherwise.
 * @param {Object} req Express request object
 * @param {Object} res Express response object
 * @param {function} next Function to be called by Express next.
 * @since 0.1.0
 */
const validateURIBlacklist = (req, res, next) => {
	let name = req.body.name;
	
	if (!uriBlacklist.has(name)) {
		return next();
	} else {
		try {
			throw new ValidateError(messages.NAME_NOT_ALLOWED, 'NAME_NOT_ALLOWED');
		} catch(e) {
			new HttpError(e, e.errorType, 400).sendError(res);
		}
		return;
	}
};

/**
 * @desc Determines whether `req.body.name` is a valid URI component.
 * This function is a limitted implementation of the rules for RFC-2396: URI 
 * Generic Syntax (https://www.ietf.org/rfc/rfc2396.txt) as applied to relative
 * segment URI components (see section 5).
 * @param {Object} req Express request object
 * @param {Object} res Express response object
 * @param {function} next Function to be called by Express next.
 * @since 0.1.0
 */
const validateURIComponent = (req, res, next) => {
	let name = req.body.name;
	let reserved = /[;\/\?\:@&=\+\$,]/g;
	let reservedMatch = reserved.exec(name);
	let relSegment = /[A-Za-z0-9-_\.\!~\*'\(\)%]*/g;
	let relSegmentMatch = relSegment.exec(name);
	let isValidSegment = relSegmentMatch && relSegmentMatch.index == 0 
		&& relSegment.lastIndex == name.length;
	
	if (!reservedMatch && isValidSegment) {
		return next();
	} else {
		try {
			throw new ValidateError(messages.INVALID_NAME, 'INVALID_NAME');
		} catch(e) {
			new HttpError(e, e.errorType, 400).sendError(res);
		}
		return;
	}

};

module.exports = [
	handleBody,
	validateURIComponent,
	validateURIBlacklist,
	compileRequest,
	handleCompilerResult,
	save
];