mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-27 10:42:45 +03:00
Add validation from schema.js
closes #1401 - added data/validation/index.js - added generic validation for length - added generic validation for nullable - added validations object to schema.js for custom validation - removed pyramid of doom from api/db.js
This commit is contained in:
parent
f273e17e08
commit
ac7f4f05c4
@ -6,7 +6,7 @@ var dataExport = require('../data/export'),
|
||||
when = require('when'),
|
||||
nodefn = require('when/node/function'),
|
||||
_ = require('lodash'),
|
||||
schema = require('../data/schema').tables,
|
||||
validation = require('../data/validation'),
|
||||
config = require('../config'),
|
||||
api = {},
|
||||
|
||||
@ -69,8 +69,7 @@ db = {
|
||||
return nodefn.call(fs.readFile, options.importfile.path);
|
||||
}).then(function (fileContents) {
|
||||
var importData,
|
||||
error = '',
|
||||
tableKeys = _.keys(schema);
|
||||
error = '';
|
||||
|
||||
// Parse the json data
|
||||
try {
|
||||
@ -83,28 +82,13 @@ db = {
|
||||
return when.reject(new Error("Import data does not specify version"));
|
||||
}
|
||||
|
||||
_.each(tableKeys, function (constkey) {
|
||||
_.each(importData.data[constkey], function (elem) {
|
||||
var prop;
|
||||
for (prop in elem) {
|
||||
if (elem.hasOwnProperty(prop)) {
|
||||
if (schema[constkey].hasOwnProperty(prop)) {
|
||||
if (!_.isNull(elem[prop])) {
|
||||
if (elem[prop].length > schema[constkey][prop].maxlength) {
|
||||
error += error !== "" ? "<br>" : "";
|
||||
error += "Property '" + prop + "' exceeds maximum length of " + schema[constkey][prop].maxlength + " (element:" + constkey + " / id:" + elem.id + ")";
|
||||
}
|
||||
} else {
|
||||
if (!schema[constkey][prop].nullable) {
|
||||
error += error !== "" ? "<br>" : "";
|
||||
error += "Property '" + prop + "' is not nullable (element:" + constkey + " / id:" + elem.id + ")";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error += error !== "" ? "<br>" : "";
|
||||
error += "Property '" + prop + "' is not allowed (element:" + constkey + " / id:" + elem.id + ")";
|
||||
}
|
||||
}
|
||||
_.each(_.keys(importData.data), function (tableName) {
|
||||
_.each(importData.data[tableName], function (importValues) {
|
||||
try {
|
||||
validation.validateSchema(tableName, importValues);
|
||||
} catch (err) {
|
||||
error += error !== "" ? "<br>" : "";
|
||||
error += err.message;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -1,14 +1,14 @@
|
||||
var db = {
|
||||
posts: {
|
||||
id: {type: 'increments', nullable: false, primary: true},
|
||||
uuid: {type: 'string', maxlength: 36, nullable: false},
|
||||
uuid: {type: 'string', maxlength: 36, nullable: false, validations: {'isUUID': true}},
|
||||
title: {type: 'string', maxlength: 150, nullable: false},
|
||||
slug: {type: 'string', maxlength: 150, nullable: false, unique: true},
|
||||
markdown: {type: 'text', maxlength: 16777215, fieldtype: 'medium', nullable: true},
|
||||
html: {type: 'text', maxlength: 16777215, fieldtype: 'medium', nullable: true},
|
||||
image: {type: 'text', maxlength: 2000, nullable: true},
|
||||
featured: {type: 'bool', nullable: false, defaultTo: false},
|
||||
page: {type: 'bool', nullable: false, defaultTo: false},
|
||||
page: {type: 'bool', nullable: false, defaultTo: false, validations: {'isIn': ['true', 'false']}},
|
||||
status: {type: 'string', maxlength: 150, nullable: false, defaultTo: 'draft'},
|
||||
language: {type: 'string', maxlength: 6, nullable: false, defaultTo: 'en_US'},
|
||||
meta_title: {type: 'string', maxlength: 150, nullable: true},
|
||||
@ -23,15 +23,15 @@ var db = {
|
||||
},
|
||||
users: {
|
||||
id: {type: 'increments', nullable: false, primary: true},
|
||||
uuid: {type: 'string', maxlength: 36, nullable: false},
|
||||
uuid: {type: 'string', maxlength: 36, nullable: false, validations: {'isUUID': true}},
|
||||
name: {type: 'string', maxlength: 150, nullable: false, unique: true},
|
||||
slug: {type: 'string', maxlength: 150, nullable: false},
|
||||
password: {type: 'string', maxlength: 60, nullable: false},
|
||||
email: {type: 'string', maxlength: 254, nullable: false, unique: true},
|
||||
email: {type: 'string', maxlength: 254, nullable: false, unique: true, validations: {'isEmail': true}},
|
||||
image: {type: 'text', maxlength: 2000, nullable: true},
|
||||
cover: {type: 'text', maxlength: 2000, nullable: true},
|
||||
bio: {type: 'string', maxlength: 200, nullable: true},
|
||||
website: {type: 'text', maxlength: 2000, nullable: true},
|
||||
website: {type: 'text', maxlength: 2000, nullable: true, validations: {'isUrl': true}},
|
||||
location: {type: 'text', maxlength: 65535, nullable: true},
|
||||
accessibility: {type: 'text', maxlength: 65535, nullable: true},
|
||||
status: {type: 'string', maxlength: 150, nullable: false, defaultTo: 'active'},
|
||||
@ -46,7 +46,7 @@ var db = {
|
||||
},
|
||||
roles: {
|
||||
id: {type: 'increments', nullable: false, primary: true},
|
||||
uuid: {type: 'string', maxlength: 36, nullable: false},
|
||||
uuid: {type: 'string', maxlength: 36, nullable: false, validations: {'isUUID': true}},
|
||||
name: {type: 'string', maxlength: 150, nullable: false},
|
||||
description: {type: 'string', maxlength: 200, nullable: true},
|
||||
created_at: {type: 'dateTime', nullable: false},
|
||||
@ -61,7 +61,7 @@ var db = {
|
||||
},
|
||||
permissions: {
|
||||
id: {type: 'increments', nullable: false, primary: true},
|
||||
uuid: {type: 'string', maxlength: 36, nullable: false},
|
||||
uuid: {type: 'string', maxlength: 36, nullable: false, validations: {'isUUID': true}},
|
||||
name: {type: 'string', maxlength: 150, nullable: false},
|
||||
object_type: {type: 'string', maxlength: 150, nullable: false},
|
||||
action_type: {type: 'string', maxlength: 150, nullable: false},
|
||||
@ -88,10 +88,10 @@ var db = {
|
||||
},
|
||||
settings: {
|
||||
id: {type: 'increments', nullable: false, primary: true},
|
||||
uuid: {type: 'string', maxlength: 36, nullable: false},
|
||||
uuid: {type: 'string', maxlength: 36, nullable: false, validations: {'isUUID': true}},
|
||||
key: {type: 'string', maxlength: 150, nullable: false, unique: true},
|
||||
value: {type: 'text', maxlength: 65535, nullable: true},
|
||||
type: {type: 'string', maxlength: 150, nullable: false, defaultTo: 'core'},
|
||||
type: {type: 'string', maxlength: 150, nullable: false, defaultTo: 'core', validations: {'isIn': ['core', 'blog', 'theme', 'app', 'plugin']}},
|
||||
created_at: {type: 'dateTime', nullable: false},
|
||||
created_by: {type: 'integer', nullable: false},
|
||||
updated_at: {type: 'dateTime', nullable: true},
|
||||
@ -99,7 +99,7 @@ var db = {
|
||||
},
|
||||
tags: {
|
||||
id: {type: 'increments', nullable: false, primary: true},
|
||||
uuid: {type: 'string', maxlength: 36, nullable: false},
|
||||
uuid: {type: 'string', maxlength: 36, nullable: false, validations: {'isUUID': true}},
|
||||
name: {type: 'string', maxlength: 150, nullable: false},
|
||||
slug: {type: 'string', maxlength: 150, nullable: false, unique: true},
|
||||
description: {type: 'string', maxlength: 200, nullable: true},
|
||||
|
89
core/server/data/validation/index.js
Normal file
89
core/server/data/validation/index.js
Normal file
@ -0,0 +1,89 @@
|
||||
var schema = require('../schema').tables,
|
||||
_ = require('lodash'),
|
||||
validator = require('validator'),
|
||||
when = require('when'),
|
||||
|
||||
validateSchema,
|
||||
validateSettings,
|
||||
validate;
|
||||
|
||||
// Validation validation against schema attributes
|
||||
// values are checked against the validation objects
|
||||
// form schema.js
|
||||
validateSchema = function (tableName, model) {
|
||||
var columns = _.keys(schema[tableName]);
|
||||
|
||||
_.each(columns, function (columnKey) {
|
||||
// check nullable
|
||||
if (model.hasOwnProperty(columnKey) && schema[tableName][columnKey].hasOwnProperty('nullable')
|
||||
&& schema[tableName][columnKey].nullable !== true) {
|
||||
validator.check(model[columnKey], 'Value in [' + tableName + '.' + columnKey +
|
||||
'] cannot be blank.').notNull();
|
||||
validator.check(model[columnKey], 'Value in [' + tableName + '.' + columnKey +
|
||||
'] cannot be blank.').notEmpty();
|
||||
}
|
||||
// TODO: check if mandatory values should be enforced
|
||||
if (model[columnKey]) {
|
||||
// check length
|
||||
if (schema[tableName][columnKey].hasOwnProperty('maxlength')) {
|
||||
validator.check(model[columnKey], 'Value in [' + tableName + '.' + columnKey +
|
||||
'] exceeds maximum length of %2 characters.').len(0, schema[tableName][columnKey].maxlength);
|
||||
}
|
||||
|
||||
//check validations objects
|
||||
if (schema[tableName][columnKey].hasOwnProperty('validations')) {
|
||||
validate(model[columnKey], columnKey, schema[tableName][columnKey].validations);
|
||||
}
|
||||
|
||||
//check type
|
||||
if (schema[tableName][columnKey].hasOwnProperty('type')) {
|
||||
if (schema[tableName][columnKey].type === 'integer') {
|
||||
validator.check(model[columnKey], 'Value in [' + tableName + '.' + columnKey +
|
||||
'] is no valid integer.' + model[columnKey]).isInt();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Validation for settings
|
||||
// settings are checked against the validation objects
|
||||
// form default-settings.json
|
||||
validateSettings = function (defaultSettings, model) {
|
||||
var values = model.toJSON(),
|
||||
matchingDefault = defaultSettings[values.key];
|
||||
|
||||
if (matchingDefault && matchingDefault.validations) {
|
||||
validate(values.value, values.key, matchingDefault.validations);
|
||||
}
|
||||
};
|
||||
|
||||
// Validate using the validation module.
|
||||
// Each validation's key is a name and its value is an array of options
|
||||
// Use true (boolean) if options aren't applicable
|
||||
//
|
||||
// eg:
|
||||
// validations: { isUrl: true, len: [20, 40] }
|
||||
//
|
||||
// will validate that a values's length is a URL between 20 and 40 chars,
|
||||
// available validators: https://github.com/chriso/node-validator#list-of-validation-methods
|
||||
validate = function (value, key, validations) {
|
||||
_.each(validations, function (validationOptions, validationName) {
|
||||
var validation = validator.check(value, 'Validation [' + validationName + '] of field [' + key + '] failed.');
|
||||
|
||||
if (validationOptions === true) {
|
||||
validationOptions = null;
|
||||
}
|
||||
if (typeof validationOptions !== 'array') {
|
||||
validationOptions = [validationOptions];
|
||||
}
|
||||
|
||||
// equivalent of validation.isSomething(option1, option2)
|
||||
validation[validationName].apply(validation, validationOptions);
|
||||
}, this);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
validateSchema: validateSchema,
|
||||
validateSettings: validateSettings
|
||||
};
|
@ -4,10 +4,10 @@ var Bookshelf = require('bookshelf'),
|
||||
_ = require('lodash'),
|
||||
uuid = require('node-uuid'),
|
||||
config = require('../config'),
|
||||
Validator = require('validator').Validator,
|
||||
unidecode = require('unidecode'),
|
||||
sanitize = require('validator').sanitize,
|
||||
schema = require('../data/schema'),
|
||||
validation = require('../data/validation'),
|
||||
|
||||
ghostBookshelf;
|
||||
|
||||
@ -15,7 +15,6 @@ var Bookshelf = require('bookshelf'),
|
||||
ghostBookshelf = Bookshelf.ghost = Bookshelf.initialize(config().database);
|
||||
ghostBookshelf.client = config().database.client;
|
||||
|
||||
ghostBookshelf.validator = new Validator();
|
||||
|
||||
// The Base Model which other Ghost objects will inherit from,
|
||||
// including some convenience functions as static properties on the model.
|
||||
@ -45,7 +44,7 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
|
||||
},
|
||||
|
||||
validate: function () {
|
||||
return true;
|
||||
validation.validateSchema(this.tableName, this.toJSON());
|
||||
},
|
||||
|
||||
creating: function () {
|
||||
|
@ -1,6 +1,7 @@
|
||||
var ghostBookshelf = require('./base'),
|
||||
User = require('./user').User,
|
||||
Role = require('./role').Role,
|
||||
validation = require('../data/validation'),
|
||||
|
||||
Permission,
|
||||
Permissions;
|
||||
@ -9,11 +10,6 @@ Permission = ghostBookshelf.Model.extend({
|
||||
|
||||
tableName: 'permissions',
|
||||
|
||||
validate: function () {
|
||||
// TODO: validate object_type, action_type and object_id
|
||||
ghostBookshelf.validator.check(this.get('name'), "Permission name cannot be blank").notEmpty();
|
||||
},
|
||||
|
||||
roles: function () {
|
||||
return this.belongsToMany(Role);
|
||||
},
|
||||
|
@ -9,6 +9,7 @@ var _ = require('lodash'),
|
||||
Tag = require('./tag').Tag,
|
||||
Tags = require('./tag').Tags,
|
||||
ghostBookshelf = require('./base'),
|
||||
validation = require('../data/validation'),
|
||||
|
||||
Post,
|
||||
Posts,
|
||||
@ -37,12 +38,7 @@ Post = ghostBookshelf.Model.extend({
|
||||
},
|
||||
|
||||
validate: function () {
|
||||
ghostBookshelf.validator.check(this.get('title'), "Post title cannot be blank").notEmpty();
|
||||
ghostBookshelf.validator.check(this.get('title'), 'Post title maximum length is 150 characters.').len(0, 150);
|
||||
ghostBookshelf.validator.check(this.get('slug'), "Post title cannot be blank").notEmpty();
|
||||
ghostBookshelf.validator.check(this.get('slug'), 'Post title maximum length is 150 characters.').len(0, 150);
|
||||
|
||||
return true;
|
||||
validation.validateSchema(this.tableName, this.toJSON());
|
||||
},
|
||||
|
||||
saving: function (newPage, attr, options) {
|
||||
|
@ -9,11 +9,6 @@ Role = ghostBookshelf.Model.extend({
|
||||
|
||||
tableName: 'roles',
|
||||
|
||||
validate: function () {
|
||||
ghostBookshelf.validator.check(this.get('name'), "Role name cannot be blank").notEmpty();
|
||||
ghostBookshelf.validator.check(this.get('description'), "Role description cannot be blank").notEmpty();
|
||||
},
|
||||
|
||||
users: function () {
|
||||
return this.belongsToMany(User);
|
||||
},
|
||||
|
@ -1,10 +1,10 @@
|
||||
var Settings,
|
||||
ghostBookshelf = require('./base'),
|
||||
validator = ghostBookshelf.validator,
|
||||
uuid = require('node-uuid'),
|
||||
_ = require('lodash'),
|
||||
errors = require('../errorHandling'),
|
||||
when = require('when'),
|
||||
validation = require('../data/validation'),
|
||||
|
||||
defaultSettings;
|
||||
|
||||
@ -41,37 +41,9 @@ Settings = ghostBookshelf.Model.extend({
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
// Validate default settings using the validator module.
|
||||
// Each validation's key is a name and its value is an array of options
|
||||
// Use true (boolean) if options aren't applicable
|
||||
//
|
||||
// eg:
|
||||
// validations: { isUrl: true, len: [20, 40] }
|
||||
//
|
||||
// will validate that a setting's length is a URL between 20 and 40 chars,
|
||||
// available validators: https://github.com/chriso/node-validator#list-of-validation-methods
|
||||
validate: function () {
|
||||
validator.check(this.get('key'), "Setting key cannot be blank").notEmpty();
|
||||
validator.check(this.get('type'), "Setting type cannot be blank").notEmpty();
|
||||
|
||||
var matchingDefault = defaultSettings[this.get('key')];
|
||||
|
||||
if (matchingDefault && matchingDefault.validations) {
|
||||
_.each(matchingDefault.validations, function (validationOptions, validationName) {
|
||||
var validation = validator.check(this.get('value'));
|
||||
|
||||
if (validationOptions === true) {
|
||||
validationOptions = null;
|
||||
}
|
||||
if (typeof validationOptions !== 'array') {
|
||||
validationOptions = [validationOptions];
|
||||
}
|
||||
|
||||
// equivalent of validation.isSomething(option1, option2)
|
||||
validation[validationName].apply(validation, validationOptions);
|
||||
}, this);
|
||||
}
|
||||
validation.validateSchema(this.tableName, this.toJSON());
|
||||
validation.validateSettings(defaultSettings, this);
|
||||
},
|
||||
|
||||
|
||||
|
@ -8,11 +8,6 @@ Tag = ghostBookshelf.Model.extend({
|
||||
|
||||
tableName: 'tags',
|
||||
|
||||
validate: function () {
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
saving: function () {
|
||||
var self = this;
|
||||
ghostBookshelf.Model.prototype.saving.apply(this, arguments);
|
||||
|
@ -10,6 +10,7 @@ var _ = require('lodash'),
|
||||
Permission = require('./permission').Permission,
|
||||
http = require('http'),
|
||||
crypto = require('crypto'),
|
||||
validator = require('validator'),
|
||||
|
||||
tokenSecurity = {},
|
||||
User,
|
||||
@ -17,11 +18,10 @@ var _ = require('lodash'),
|
||||
|
||||
function validatePasswordLength(password) {
|
||||
try {
|
||||
ghostBookshelf.validator.check(password, "Your password must be at least 8 characters long.").len(8);
|
||||
validator.check(password, "Your password must be at least 8 characters long.").len(8);
|
||||
} catch (error) {
|
||||
return when.reject(error);
|
||||
}
|
||||
|
||||
return when.resolve();
|
||||
}
|
||||
|
||||
@ -37,16 +37,6 @@ User = ghostBookshelf.Model.extend({
|
||||
|
||||
tableName: 'users',
|
||||
|
||||
validate: function () {
|
||||
ghostBookshelf.validator.check(this.get('email'), "Please enter a valid email address. That one looks a bit dodgy.").isEmail();
|
||||
ghostBookshelf.validator.check(this.get('bio'), "We're not writing a novel here! I'm afraid your bio has to stay under 200 characters.").len(0, 200);
|
||||
if (this.get('website') && this.get('website').length > 0) {
|
||||
ghostBookshelf.validator.check(this.get('website'), "Looks like your website is not actually a website. Try again?").isUrl();
|
||||
}
|
||||
ghostBookshelf.validator.check(this.get('location'), 'This seems a little too long! Please try and keep your location under 150 characters.').len(0, 150);
|
||||
return true;
|
||||
},
|
||||
|
||||
saving: function () {
|
||||
var self = this;
|
||||
// disabling sanitization until we can implement a better version
|
||||
|
@ -246,7 +246,7 @@ describe("Import", function () {
|
||||
}).then(function () {
|
||||
(1).should.eql(0, 'Data import should not resolve promise.');
|
||||
}, function (error) {
|
||||
error.should.eql('Error importing data: Post title maximum length is 150 characters.');
|
||||
error.should.eql('Error importing data: Value in [posts.title] exceeds maximum length of 150 characters.');
|
||||
|
||||
when.all([
|
||||
knex("users").select(),
|
||||
@ -292,7 +292,7 @@ describe("Import", function () {
|
||||
}).then(function () {
|
||||
(1).should.eql(0, 'Data import should not resolve promise.');
|
||||
}, function (error) {
|
||||
error.should.eql('Error importing data: Setting key cannot be blank');
|
||||
error.should.eql('Error importing data: Value in [settings.key] cannot be blank.');
|
||||
|
||||
when.all([
|
||||
knex("users").select(),
|
||||
@ -433,7 +433,7 @@ describe("Import", function () {
|
||||
}).then(function () {
|
||||
(1).should.eql(0, 'Data import should not resolve promise.');
|
||||
}, function (error) {
|
||||
error.should.eql('Error importing data: Post title maximum length is 150 characters.');
|
||||
error.should.eql('Error importing data: Value in [posts.title] exceeds maximum length of 150 characters.');
|
||||
|
||||
when.all([
|
||||
knex("users").select(),
|
||||
@ -479,7 +479,7 @@ describe("Import", function () {
|
||||
}).then(function () {
|
||||
(1).should.eql(0, 'Data import should not resolve promise.');
|
||||
}, function (error) {
|
||||
error.should.eql('Error importing data: Setting key cannot be blank');
|
||||
error.should.eql('Error importing data: Value in [settings.key] cannot be blank.');
|
||||
|
||||
when.all([
|
||||
knex("users").select(),
|
||||
|
Loading…
Reference in New Issue
Block a user