Ghost/core/server/data/validation/index.js
Kevin Ansfield 80fbfd7a85 Revert post.page->post.type handling
no issue
- the column addition/removal can be too slow for large sites
- will be added back in 3.0

---

Revert "Fixed canary api for page/type column"
This reverts commit a5a7e7e919.

Revert "Updated frontend canary url config for page/type"
This reverts commit 19100ec5e6.

Revert "Updated canary api to handle type column correctly (#11006)"
This reverts commit c3e8ba0523.

Revert "Ensured `page` filter works in routes.yaml"
This reverts commit 9037c19e50.

Revert "Replaced usage of mongo util with nql-map-key-values"
This reverts commit 8c5f1d0ef0.

Revert "Added shared nql-map-key-values module"
This reverts commit ef4fd4b8ef.

Revert "Ensured page prop is present on content api response"
This reverts commit cfa0a0862b.

Revert "Fixed failing regression tests"
This reverts commit 9c2bb3811f.

Revert "Updated xmlrpc and slack service to use type column"
This reverts commit 44a02c7d36.

Revert "Updated v0.1 posts api to work with type column"
This reverts commit 2c81d7c914.

Revert "Removed updates to v0.1 specific code"
This reverts commit 08d83c1f53.

Revert "Added missing context from ValidationError"
This reverts commit cd45ab4f54.

Revert "Renamed page->type in the page&posts serializers"
This reverts commit df99e724e3.

Revert "Added mongo helper to input serializers"
This reverts commit fb8eadb4a8.

Revert "Passed mongoTransformer through to NQL"
This reverts commit 0ae3f0fdfc.

Revert "Permitted mongoTransformer option for read methods"
This reverts commit a89376bf26.

Revert "Updated the count plugin to reference the type column"
This reverts commit a52f15d3d3.

Revert "Updated hashes for db integrity check"
This reverts commit bb6b337be3.

Revert "Remove page column and remaining references"
This reverts commit 9d7190d692.

Revert "Added type column to data generator"
This reverts commit e59806cb45.

Revert "Removed references to page column in rss tests"
This reverts commit 04d0f855de.

Revert "Removed page column references in validation tests"
This reverts commit f0afbc5cc0.

Revert "Updated the post model to use the `type` column"
This reverts commit 1189bc823a.

Revert "Updated url service to use type column"
This reverts commit 61612ba8fd.

Revert "Updated the v2 api to deal with type column"
This reverts commit 57afb2de2b.

Revert "Added type property to post model defaults"
This reverts commit dc3345b1c5.

Revert "Added type property to the default post fixtures"
This reverts commit 82d8c38033.

Revert "Added type column to posts table"
This reverts commit 9b85fc6a69.
2019-08-19 12:02:14 +01:00

365 lines
14 KiB
JavaScript

var schema = require('../schema').tables,
_ = require('lodash'),
validator = require('validator'),
moment = require('moment-timezone'),
assert = require('assert'),
Promise = require('bluebird'),
common = require('../../lib/common'),
settingsCache = require('../../services/settings/cache'),
urlUtils = require('../../lib/url-utils'),
validatePassword,
validateSchema,
validateSettings,
validate;
function assertString(input) {
assert(typeof input === 'string', 'Validator js validates strings only');
}
/**
* Counts repeated characters in a string. When 50% or more characters are the same,
* we return false and therefore invalidate the string.
* @param {String} stringToTest The password string to check.
* @return {Boolean}
*/
function characterOccurance(stringToTest) {
var chars = {},
allowedOccurancy,
valid = true;
stringToTest = _.toString(stringToTest);
allowedOccurancy = stringToTest.length / 2;
// Loop through string and accumulate character counts
_.each(stringToTest, function (char) {
if (!chars[char]) {
chars[char] = 1;
} else {
chars[char] += 1;
}
});
// check if any of the accumulated chars exceed the allowed occurancy
// of 50% of the words' length.
_.forIn(chars, function (charCount) {
if (charCount >= allowedOccurancy) {
valid = false;
}
});
return valid;
}
// extends has been removed in validator >= 5.0.0, need to monkey-patch it back in
// @TODO: We modify the global validator dependency here! https://github.com/chriso/validator.js/issues/525#issuecomment-213149570
validator.extend = function (name, fn) {
validator[name] = function () {
var args = Array.prototype.slice.call(arguments);
assertString(args[0]);
return fn.apply(validator, args);
};
};
// Provide a few custom validators
validator.extend('empty', function empty(str) {
return _.isEmpty(str);
});
validator.extend('notContains', function notContains(str, badString) {
return !_.includes(str, badString);
});
validator.extend('isTimezone', function isTimezone(str) {
return moment.tz.zone(str) ? true : false;
});
validator.extend('isEmptyOrURL', function isEmptyOrURL(str) {
return (_.isEmpty(str) || validator.isURL(str, {require_protocol: false}));
});
validator.extend('isSlug', function isSlug(str) {
return validator.matches(str, /^[a-z0-9\-_]+$/);
});
/**
* Validation against simple password rules
* Returns false when validation fails and true for a valid password
* @param {String} password The password string to check.
* @param {String} email The users email address to validate agains password.
* @param {String} blogTitle Optional blogTitle value, when blog title is not set yet, e. g. in setup process.
* @return {Object} example for returned validation Object:
* invalid password: `validationResult: {isValid: false, message: 'Sorry, you cannot use an insecure password.'}`
* valid password: `validationResult: {isValid: true}`
*/
validatePassword = function validatePassword(password, email, blogTitle) {
var validationResult = {isValid: true},
disallowedPasswords = ['password', 'ghost', 'passw0rd'],
blogUrl = urlUtils.urlFor('home', true),
badPasswords = [
'1234567890',
'qwertyuiop',
'qwertzuiop',
'asdfghjkl;',
'abcdefghij',
'0987654321',
'1q2w3e4r5t',
'12345asdfg'
];
blogTitle = blogTitle ? blogTitle : settingsCache.get('title');
blogUrl = blogUrl.replace(/^http(s?):\/\//, '');
// password must be longer than 10 characters
if (!validator.isLength(password, 10)) {
validationResult.isValid = false;
validationResult.message = common.i18n.t('errors.models.user.passwordDoesNotComplyLength', {minLength: 10});
return validationResult;
}
// dissallow password from badPasswords list (e. g. '1234567890')
_.each(badPasswords, function (badPassword) {
if (badPassword === password) {
validationResult.isValid = false;
}
});
// password must not match with users' email
if (email && email.toLowerCase() === password.toLowerCase()) {
validationResult.isValid = false;
}
// password must not contain the words 'ghost', 'password', or 'passw0rd'
_.each(disallowedPasswords, function (disallowedPassword) {
if (password.toLowerCase().indexOf(disallowedPassword) >= 0) {
validationResult.isValid = false;
}
});
// password must not match with blog title
if (blogTitle && blogTitle.toLowerCase() === password.toLowerCase()) {
validationResult.isValid = false;
}
// password must not match with blog URL (without protocol, with or without trailing slash)
if (blogUrl && (blogUrl.toLowerCase() === password.toLowerCase() || blogUrl.toLowerCase().replace(/\/$/, '') === password.toLowerCase())) {
validationResult.isValid = false;
}
// dissallow passwords where 50% or more of characters are the same
if (!characterOccurance(password)) {
validationResult.isValid = false;
}
// Generic error message for the rules where no dedicated error massage is set
if (!validationResult.isValid && !validationResult.message) {
validationResult.message = common.i18n.t('errors.models.user.passwordDoesNotComplySecurity');
}
return validationResult;
};
/**
* Validate model against schema.
*
* ## on model update
* - only validate changed fields
* - otherwise we could throw errors which the user is out of control
* - e.g.
* - we add a new field without proper validation, release goes out
* - we add proper validation for a single field
* - if you call `user.save()` the default fallback in bookshelf is `options.method=update`.
* - we set `options.method` explicit for adding resources (because otherwise bookshelf uses `update`)
*
* ## on model add
* - validate everything to catch required fields
*/
validateSchema = function validateSchema(tableName, model, options) {
options = options || {};
var columns = _.keys(schema[tableName]),
validationErrors = [];
_.each(columns, function each(columnKey) {
var message = '',
strVal = _.toString(model.get(columnKey)); // KEEP: Validator.js only validates strings.
if (options.method !== 'insert' && !_.has(model.changed, columnKey)) {
return;
}
// check nullable
if (Object.prototype.hasOwnProperty.call(schema[tableName][columnKey], 'nullable') &&
schema[tableName][columnKey].nullable !== true &&
!Object.prototype.hasOwnProperty.call(schema[tableName][columnKey], 'defaultTo')
) {
if (validator.empty(strVal)) {
message = common.i18n.t('notices.data.validation.index.valueCannotBeBlank', {
tableName: tableName,
columnKey: columnKey
});
validationErrors.push(new common.errors.ValidationError({
message: message,
context: tableName + '.' + columnKey
}));
}
}
// validate boolean columns
if (Object.prototype.hasOwnProperty.call(schema[tableName][columnKey], 'type')
&& schema[tableName][columnKey].type === 'bool') {
if (!(validator.isBoolean(strVal) || validator.empty(strVal))) {
message = common.i18n.t('notices.data.validation.index.valueMustBeBoolean', {
tableName: tableName,
columnKey: columnKey
});
validationErrors.push(new common.errors.ValidationError({
message: message,
context: tableName + '.' + columnKey
}));
}
// CASE: ensure we transform 0|1 to false|true
if (!validator.empty(strVal)) {
model.set(columnKey, !!model.get(columnKey));
}
}
// TODO: check if mandatory values should be enforced
if (model.get(columnKey) !== null && model.get(columnKey) !== undefined) {
// check length
if (Object.prototype.hasOwnProperty.call(schema[tableName][columnKey], 'maxlength')) {
if (!validator.isLength(strVal, 0, schema[tableName][columnKey].maxlength)) {
message = common.i18n.t('notices.data.validation.index.valueExceedsMaxLength',
{
tableName: tableName,
columnKey: columnKey,
maxlength: schema[tableName][columnKey].maxlength
});
validationErrors.push(new common.errors.ValidationError({
message: message,
context: tableName + '.' + columnKey
}));
}
}
// check validations objects
if (Object.prototype.hasOwnProperty.call(schema[tableName][columnKey], 'validations')) {
validationErrors = validationErrors.concat(validate(strVal, columnKey, schema[tableName][columnKey].validations, tableName));
}
// check type
if (Object.prototype.hasOwnProperty.call(schema[tableName][columnKey], 'type')) {
if (schema[tableName][columnKey].type === 'integer' && !validator.isInt(strVal)) {
message = common.i18n.t('notices.data.validation.index.valueIsNotInteger', {
tableName: tableName,
columnKey: columnKey
});
validationErrors.push(new common.errors.ValidationError({
message: message,
context: tableName + '.' + columnKey
}));
}
}
}
});
if (validationErrors.length !== 0) {
return Promise.reject(validationErrors);
}
return Promise.resolve();
};
// Validation for settings
// settings are checked against the validation objects
// form default-settings.json
validateSettings = function validateSettings(defaultSettings, model) {
var values = model.toJSON(),
validationErrors = [],
matchingDefault = defaultSettings[values.key];
if (matchingDefault && matchingDefault.validations) {
validationErrors = validationErrors.concat(validate(values.value, values.key, matchingDefault.validations, 'settings'));
}
if (validationErrors.length !== 0) {
return Promise.reject(validationErrors);
}
return Promise.resolve();
};
/**
* Validate keys using the validator module.
* Each validation's key is a method name and its value is an array of options
* eg:
* validations: { isURL: true, isLength: [20, 40] }
* will validate that a values's length is a URL between 20 and 40 chars.
*
* If you pass a boolean as the value, it will specify the "good" result. By default
* the "good" result is assumed to be true.
* eg:
* validations: { isNull: false } // means the "good" result would
* // fail the `isNull` check, so
* // not null.
*
* available validators: https://github.com/chriso/validator.js#validators
* @param {String} value the value to validate.
* @param {String} key the db column key of the value to validate.
* @param {Object} validations the validations object as described above.
* @param {String} tableName (optional) the db table of the value to validate, used for error message.
* @return {Array} returns an Array including the found validation errors (empty if none found);
*/
validate = function validate(value, key, validations, tableName) {
var validationErrors = [], translation;
value = _.toString(value);
_.each(validations, function each(validationOptions, validationName) {
var goodResult = true;
if (_.isBoolean(validationOptions)) {
goodResult = validationOptions;
validationOptions = [];
} else if (!_.isArray(validationOptions)) {
validationOptions = [validationOptions];
}
validationOptions.unshift(value);
// equivalent of validator.isSomething(option1, option2)
if (validator[validationName].apply(validator, validationOptions) !== goodResult) {
// CASE: You can define specific translations for validators e.g. isLength
if (common.i18n.doesTranslationKeyExist('notices.data.validation.index.validationFailedTypes.' + validationName)) {
translation = common.i18n.t('notices.data.validation.index.validationFailedTypes.' + validationName, _.merge({
validationName: validationName,
key: key,
tableName: tableName
}, validationOptions[1]));
} else {
translation = common.i18n.t('notices.data.validation.index.validationFailed', {
validationName: validationName,
key: key
});
}
validationErrors.push(new common.errors.ValidationError({
message: translation
}));
}
validationOptions.shift();
}, this);
return validationErrors;
};
module.exports = {
validate: validate,
validator: validator,
validatePassword: validatePassword,
validateSchema: validateSchema,
validateSettings: validateSettings
};