ghost/api-framework/lib/validators/input/all.js
const debug = require('@tryghost/debug')('validators:input:all');
const _ = require('lodash');
const tpl = require('@tryghost/tpl');
const {BadRequestError, ValidationError} = require('@tryghost/errors');
const validator = require('@tryghost/validator');
const messages = {
validationFailed: 'Validation ({validationName}) failed for {key}',
noRootKeyProvided: 'No root key (\'{docName}\') provided.',
invalidIdProvided: 'Invalid id provided.'
};
const GLOBAL_VALIDATORS = {
id: {matches: /^[a-f\d]{24}$|^1$|me/i},
page: {matches: /^\d+$/},
limit: {matches: /^\d+|all$/},
from: {isDate: true},
to: {isDate: true},
columns: {matches: /^[\w, ]+$/},
order: {matches: /^[a-z0-9_,. ]+$/i},
uuid: {isUUID: true},
slug: {isSlug: true},
name: {},
email: {isEmail: true},
filter: false,
context: false,
forUpdate: false,
transacting: false,
include: false,
formats: false
};
const validate = (config, attrs) => {
let errors = [];
_.each(config, (value, key) => {
if (value.required && !attrs[key]) {
errors.push(new ValidationError({
message: tpl(messages.validationFailed, {
validationName: 'FieldIsRequired',
key: key
})
}));
}
});
_.each(attrs, (value, key) => {
debug(key, value);
if (GLOBAL_VALIDATORS[key]) {
debug('global validation');
errors = errors.concat(validator.validate(value, key, GLOBAL_VALIDATORS[key]));
}
if (config?.[key]) {
const allowedValues = Array.isArray(config[key]) ? config[key] : config[key].values;
if (allowedValues) {
debug('ctrl validation');
// CASE: we allow e.g. `formats=`
if (!value || !value.length) {
return;
}
const valuesAsArray = Array.isArray(value) ? value : value.trim().toLowerCase().split(',');
const unallowedValues = _.filter(valuesAsArray, (valueToFilter) => {
return !allowedValues.includes(valueToFilter);
});
if (unallowedValues.length) {
// CASE: we do not error for invalid includes, just silently remove
if (key === 'include') {
attrs.include = valuesAsArray.filter(x => allowedValues.includes(x));
return;
}
errors.push(new ValidationError({
message: tpl(messages.validationFailed, {
validationName: 'AllowedValues',
key: key
})
}));
}
}
}
});
return errors;
};
module.exports = {
/**
* @param {object} apiConfig
* @param {import('@tryghost/api-framework').Frame} frame
*/
all(apiConfig, frame) {
debug('validate all');
let validationErrors = validate(apiConfig.options, frame.options);
if (!_.isEmpty(validationErrors)) {
return Promise.reject(validationErrors[0]);
}
return Promise.resolve();
},
/**
* @param {object} apiConfig
* @param {import('@tryghost/api-framework').Frame} frame
*/
browse(apiConfig, frame) {
debug('validate browse');
let validationErrors = [];
if (frame.data) {
validationErrors = validate(apiConfig.data, frame.data);
}
if (!_.isEmpty(validationErrors)) {
return Promise.reject(validationErrors[0]);
}
},
read() {
debug('validate read');
return this.browse(...arguments);
},
/**
* @param {object} apiConfig
* @param {import('@tryghost/api-framework').Frame} frame
*/
add(apiConfig, frame) {
debug('validate add');
// NOTE: this block should be removed completely once JSON Schema validations
// are introduced for all of the endpoints
if (!['posts', 'tags'].includes(apiConfig.docName)) {
if (_.isEmpty(frame.data) || _.isEmpty(frame.data[apiConfig.docName]) || _.isEmpty(frame.data[apiConfig.docName][0])) {
return Promise.reject(new BadRequestError({
message: tpl(messages.noRootKeyProvided, {docName: apiConfig.docName})
}));
}
}
const jsonpath = require('jsonpath');
if (apiConfig.data) {
const missedDataProperties = [];
const nilDataProperties = [];
_.each(apiConfig.data, (value, key) => {
if (jsonpath.query(frame.data[apiConfig.docName][0], key).length === 0) {
missedDataProperties.push(key);
} else if (_.isNil(frame.data[apiConfig.docName][0][key])) {
nilDataProperties.push(key);
}
});
if (missedDataProperties.length) {
return Promise.reject(new ValidationError({
message: tpl(messages.validationFailed, {
validationName: 'FieldIsRequired',
key: JSON.stringify(missedDataProperties)
})
}));
}
if (nilDataProperties.length) {
return Promise.reject(new ValidationError({
message: tpl(messages.validationFailed, {
validationName: 'FieldIsInvalid',
key: JSON.stringify(nilDataProperties)
})
}));
}
}
},
/**
* @param {object} apiConfig
* @param {import('@tryghost/api-framework').Frame} frame
*/
edit(apiConfig, frame) {
debug('validate edit');
const result = this.add(...arguments);
if (result instanceof Promise) {
return result;
}
// NOTE: this block should be removed completely once JSON Schema validations
// are introduced for all of the endpoints. `id` property is currently
// stripped from the request body and only the one provided in `options`
// is used in later logic
if (!['posts', 'tags'].includes(apiConfig.docName)) {
if (frame.options.id && frame.data[apiConfig.docName][0].id
&& frame.options.id !== frame.data[apiConfig.docName][0].id) {
return Promise.reject(new BadRequestError({
message: tpl(messages.invalidIdProvided)
}));
}
}
},
changePassword() {
debug('validate changePassword');
return this.add(...arguments);
},
resetPassword() {
debug('validate resetPassword');
return this.add(...arguments);
},
setup() {
debug('validate setup');
return this.add(...arguments);
},
publish() {
debug('validate schedule');
return this.browse(...arguments);
}
};