mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-01 13:54:35 +03:00
e044136503
refs #2758 - add a set of default options to utils - update validation function to only pass through permitted options - pass permitted options into validate where necessary - setup basic validation for each known option, and generic validation for the remainder - change slug to treat 'name' as data, rather than an option
199 lines
7.1 KiB
JavaScript
199 lines
7.1 KiB
JavaScript
// # API Utils
|
|
// Shared helpers for working with the API
|
|
var Promise = require('bluebird'),
|
|
_ = require('lodash'),
|
|
path = require('path'),
|
|
errors = require('../errors'),
|
|
validation = require('../data/validation'),
|
|
utils;
|
|
|
|
utils = {
|
|
// ## Default Options
|
|
// Various default options for different types of endpoints
|
|
|
|
// ### Auto Default Options
|
|
// Handled / Added automatically by the validate function
|
|
// globalDefaultOptions - valid for every api endpoint
|
|
globalDefaultOptions: ['context', 'include'],
|
|
// dataDefaultOptions - valid for all endpoints which take object as well as options
|
|
dataDefaultOptions: ['data'],
|
|
|
|
// ### Manual Default Options
|
|
// These must be provided by the endpoint
|
|
// browseDefaultOptions - valid for all browse api endpoints
|
|
browseDefaultOptions: ['page', 'limit'],
|
|
// idDefaultOptions - valid whenever an id is valid
|
|
idDefaultOptions: ['id'],
|
|
|
|
/**
|
|
* ## Validate
|
|
* Prepare to validate the object and options passed to an endpoint
|
|
* @param {String} docName
|
|
* @param {Object} extras
|
|
* @returns {Function} doValidate
|
|
*/
|
|
validate: function validate(docName, extras) {
|
|
/**
|
|
* ### Do Validate
|
|
* Validate the object and options passed to an endpoint
|
|
* @argument object
|
|
* @argument options
|
|
*/
|
|
return function doValidate() {
|
|
var object, options, permittedOptions;
|
|
if (arguments.length === 2) {
|
|
object = arguments[0];
|
|
options = _.clone(arguments[1]) || {};
|
|
} else if (arguments.length === 1) {
|
|
options = _.clone(arguments[0]) || {};
|
|
} else {
|
|
options = {};
|
|
}
|
|
|
|
// Setup permitted options, starting with the global defaults
|
|
permittedOptions = utils.globalDefaultOptions;
|
|
|
|
// Add extra permitted options if any are passed in
|
|
if (extras && extras.opts) {
|
|
permittedOptions = permittedOptions.concat(extras.opts);
|
|
}
|
|
|
|
// This request will have a data key added during validation
|
|
if ((extras && extras.attrs) || object) {
|
|
permittedOptions = permittedOptions.concat(utils.dataDefaultOptions);
|
|
}
|
|
|
|
// If an 'attrs' object is passed, we use this to pick from options and convert them to data
|
|
if (extras && extras.attrs) {
|
|
options.data = _.pick(options, extras.attrs);
|
|
options = _.omit(options, extras.attrs);
|
|
}
|
|
|
|
/**
|
|
* ### Check Options
|
|
* Ensure that the options provided match exactly with what is permitted
|
|
* - incorrect option keys are sanitized
|
|
* - incorrect option values are validated
|
|
* @param {object} options
|
|
* @returns {Promise<options>}
|
|
*/
|
|
function checkOptions(options) {
|
|
// @TODO: should we throw an error if there are incorrect options provided?
|
|
options = _.pick(options, permittedOptions);
|
|
|
|
var validationErrors = utils.validateOptions(options);
|
|
|
|
if (_.isEmpty(validationErrors)) {
|
|
return Promise.resolve(options);
|
|
}
|
|
|
|
return errors.logAndRejectError(validationErrors);
|
|
}
|
|
|
|
// If we got an object, check that too
|
|
if (object) {
|
|
return utils.checkObject(object, docName, options.id).then(function (data) {
|
|
options.data = data;
|
|
|
|
return checkOptions(options);
|
|
});
|
|
}
|
|
|
|
// Otherwise just check options and return
|
|
return checkOptions(options);
|
|
};
|
|
},
|
|
|
|
validateOptions: function validateOptions(options) {
|
|
var globalValidations = {
|
|
id: {matches: /^\d+|me$/},
|
|
uuid: {isUUID: true},
|
|
page: {matches: /^\d+$/},
|
|
limit: {matches: /^\d+|all$/},
|
|
name: {}
|
|
},
|
|
// these values are sanitised/validated separately
|
|
noValidation = ['data', 'context', 'include'],
|
|
errors = [];
|
|
|
|
_.each(options, function (value, key) {
|
|
// data is validated elsewhere
|
|
if (noValidation.indexOf(key) === -1) {
|
|
if (globalValidations[key]) {
|
|
errors = errors.concat(validation.validate(value, key, globalValidations[key]));
|
|
} else {
|
|
// all other keys should be an alphanumeric string + -, like slug, tag, author, status, etc
|
|
errors = errors.concat(validation.validate(value, key, {matches: /^[a-z0-9\-]+$/}));
|
|
}
|
|
}
|
|
});
|
|
|
|
return errors;
|
|
},
|
|
|
|
prepareInclude: function prepareInclude(include, allowedIncludes) {
|
|
include = include || '';
|
|
include = _.intersection(include.split(','), allowedIncludes);
|
|
|
|
return include;
|
|
},
|
|
/**
|
|
* @param {Array} allowedIncludes
|
|
* @returns {Function} doConversion
|
|
*/
|
|
convertOptions: function convertOptions(allowedIncludes) {
|
|
/**
|
|
* Convert our options from API-style to Model-style
|
|
* @param {Object} options
|
|
* @returns {Object} options
|
|
*/
|
|
return function doConversion(options) {
|
|
if (options.include) {
|
|
options.include = utils.prepareInclude(options.include, allowedIncludes);
|
|
}
|
|
return options;
|
|
};
|
|
},
|
|
/**
|
|
* ### Check Object
|
|
* Check an object passed to the API is in the correct format
|
|
*
|
|
* @param {Object} object
|
|
* @param {String} docName
|
|
* @returns {Promise(Object)} resolves to the original object if it checks out
|
|
*/
|
|
checkObject: function (object, docName, editId) {
|
|
if (_.isEmpty(object) || _.isEmpty(object[docName]) || _.isEmpty(object[docName][0])) {
|
|
return errors.logAndRejectError(new errors.BadRequestError('No root key (\'' + docName + '\') provided.'));
|
|
}
|
|
|
|
// convert author property to author_id to match the name in the database
|
|
if (docName === 'posts') {
|
|
if (object.posts[0].hasOwnProperty('author')) {
|
|
object.posts[0].author_id = object.posts[0].author;
|
|
delete object.posts[0].author;
|
|
}
|
|
}
|
|
|
|
if (editId && object[docName][0].id && parseInt(editId, 10) !== parseInt(object[docName][0].id, 10)) {
|
|
return errors.logAndRejectError(new errors.BadRequestError('Invalid id provided.'));
|
|
}
|
|
|
|
return Promise.resolve(object);
|
|
},
|
|
checkFileExists: function (options, filename) {
|
|
return !!(options[filename] && options[filename].type && options[filename].path);
|
|
},
|
|
checkFileIsValid: function (file, types, extensions) {
|
|
var type = file.type,
|
|
ext = path.extname(file.name).toLowerCase();
|
|
|
|
if (_.contains(types, type) && _.contains(extensions, ext)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
};
|
|
|
|
module.exports = utils;
|