diff --git a/core/server/api/db.js b/core/server/api/db.js index 5db32833ca..bde548c53e 100644 --- a/core/server/api/db.js +++ b/core/server/api/db.js @@ -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'), errors = require('../../server/errorHandling'), api = {}, @@ -47,8 +47,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 { @@ -62,28 +61,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 !== "" ? "
" : ""; - error += "Property '" + prop + "' exceeds maximum length of " + schema[constkey][prop].maxlength + " (element:" + constkey + " / id:" + elem.id + ")"; - } - } else { - if (!schema[constkey][prop].nullable) { - error += error !== "" ? "
" : ""; - error += "Property '" + prop + "' is not nullable (element:" + constkey + " / id:" + elem.id + ")"; - } - } - } else { - error += error !== "" ? "
" : ""; - 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 !== "" ? "
" : ""; + error += err.message; } }); }); diff --git a/core/server/data/schema.js b/core/server/data/schema.js index 4d31dbda87..1734d0d1f9 100644 --- a/core/server/data/schema.js +++ b/core/server/data/schema.js @@ -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}, diff --git a/core/server/data/validation/index.js b/core/server/data/validation/index.js new file mode 100644 index 0000000000..25dd00f7ff --- /dev/null +++ b/core/server/data/validation/index.js @@ -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 +}; \ No newline at end of file diff --git a/core/server/models/base.js b/core/server/models/base.js index 47d47e9b3f..4eab1d6a2b 100644 --- a/core/server/models/base.js +++ b/core/server/models/base.js @@ -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 () { diff --git a/core/server/models/permission.js b/core/server/models/permission.js index 77064be098..f84f5be610 100644 --- a/core/server/models/permission.js +++ b/core/server/models/permission.js @@ -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); }, diff --git a/core/server/models/post.js b/core/server/models/post.js index 34b2ad48b3..2fb6c86c2e 100644 --- a/core/server/models/post.js +++ b/core/server/models/post.js @@ -10,6 +10,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) { diff --git a/core/server/models/role.js b/core/server/models/role.js index 66729ef11a..f9d134e441 100644 --- a/core/server/models/role.js +++ b/core/server/models/role.js @@ -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); }, diff --git a/core/server/models/settings.js b/core/server/models/settings.js index 49a38bdfcf..74a5557925 100644 --- a/core/server/models/settings.js +++ b/core/server/models/settings.js @@ -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); }, diff --git a/core/server/models/tag.js b/core/server/models/tag.js index a251dc0e3b..e1daff07fa 100644 --- a/core/server/models/tag.js +++ b/core/server/models/tag.js @@ -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); diff --git a/core/server/models/user.js b/core/server/models/user.js index 165738f57e..0405ae6f50 100644 --- a/core/server/models/user.js +++ b/core/server/models/user.js @@ -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 diff --git a/core/test/unit/import_spec.js b/core/test/unit/import_spec.js index f5b4c90f82..804e95e284 100644 --- a/core/test/unit/import_spec.js +++ b/core/test/unit/import_spec.js @@ -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(),