mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-28 22:43:30 +03:00
Extracted Bookshelf sanitization code to plugins
no issue - this commit extracts all options + data sanitization code from the Base model into a plugin
This commit is contained in:
parent
f4f31027b7
commit
763d368c6e
@ -50,6 +50,8 @@ ghostBookshelf.plugin(require('./events'));
|
||||
|
||||
ghostBookshelf.plugin(require('./raw-knex'));
|
||||
|
||||
ghostBookshelf.plugin(require('./sanitize'));
|
||||
|
||||
// Manages nested updates (relationships)
|
||||
ghostBookshelf.plugin('bookshelf-relations', {
|
||||
allowedOptions: ['context', 'importing', 'migrating'],
|
||||
|
@ -19,11 +19,6 @@ const tpl = require('@tryghost/tpl');
|
||||
|
||||
const ghostBookshelf = require('./bookshelf');
|
||||
|
||||
const messages = {
|
||||
missingContext: 'missing context',
|
||||
invalidDate: 'Date format for `{key}` is invalid.'
|
||||
};
|
||||
|
||||
let proto;
|
||||
|
||||
// Cache an instance of the base model prototype
|
||||
@ -41,12 +36,6 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
|
||||
return false;
|
||||
},
|
||||
|
||||
// Ghost option handling - get permitted attributes from server/data/schema.js, where the DB schema is defined
|
||||
permittedAttributes: function permittedAttributes() {
|
||||
return _.keys(schema.tables[this.tableName])
|
||||
.filter(key => key.indexOf('@@') === -1);
|
||||
},
|
||||
|
||||
// Ghost ordering handling, allows to order by permitted attributes by default and can be overriden on specific model level
|
||||
orderAttributes: function orderAttributes() {
|
||||
return Object.keys(schema.tables[this.tableName])
|
||||
@ -338,160 +327,6 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
|
||||
return id === ghostBookshelf.Model.externalUser || id === ghostBookshelf.Model.externalUser.toString();
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns an array of keys permitted in every method's `options` hash.
|
||||
* Can be overridden and added to by a model's `permittedOptions` method.
|
||||
*
|
||||
* importing: is used when import a JSON file or when migrating the database
|
||||
*
|
||||
* @return {Object} Keys allowed in the `options` hash of every model's method.
|
||||
*/
|
||||
permittedOptions: function permittedOptions(methodName) {
|
||||
const baseOptions = ['context', 'withRelated'];
|
||||
const extraOptions = ['transacting', 'importing', 'forUpdate', 'migrating'];
|
||||
|
||||
switch (methodName) {
|
||||
case 'toJSON':
|
||||
return baseOptions.concat('shallow', 'columns', 'previous');
|
||||
case 'destroy':
|
||||
return baseOptions.concat(extraOptions, ['id', 'destroyBy', 'require']);
|
||||
case 'edit':
|
||||
return baseOptions.concat(extraOptions, ['id', 'require']);
|
||||
case 'findOne':
|
||||
return baseOptions.concat(extraOptions, ['columns', 'require', 'mongoTransformer']);
|
||||
case 'findAll':
|
||||
return baseOptions.concat(extraOptions, ['filter', 'columns', 'mongoTransformer']);
|
||||
case 'findPage':
|
||||
return baseOptions.concat(extraOptions, ['filter', 'order', 'autoOrder', 'page', 'limit', 'columns', 'mongoTransformer']);
|
||||
default:
|
||||
return baseOptions.concat(extraOptions);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Filters potentially unsafe model attributes, so you can pass them to Bookshelf / Knex.
|
||||
* This filter should be called before each insert/update operation.
|
||||
*
|
||||
* @param {Object} data Has keys representing the model's attributes/fields in the database.
|
||||
* @return {Object} The filtered results of the passed in data, containing only what's allowed in the schema.
|
||||
*/
|
||||
filterData: function filterData(data) {
|
||||
const permittedAttributes = this.prototype.permittedAttributes();
|
||||
const filteredData = _.pick(data, permittedAttributes);
|
||||
const sanitizedData = this.sanitizeData(filteredData);
|
||||
|
||||
return sanitizedData;
|
||||
},
|
||||
|
||||
/**
|
||||
* `sanitizeData` ensures that client data is in the correct format for further operations.
|
||||
*
|
||||
* Dates:
|
||||
* - client dates are sent as ISO 8601 format (moment(..).format())
|
||||
* - server dates are in JS Date format
|
||||
* >> when bookshelf fetches data from the database, all dates are in JS Dates
|
||||
* >> see `parse`
|
||||
* - Bookshelf updates the model with the new client data via the `set` function
|
||||
* - Bookshelf uses a simple `isEqual` function from lodash to detect real changes
|
||||
* - .previous(attr) and .get(attr) returns false obviously
|
||||
* - internally we use our `hasDateChanged` if we have to compare previous dates
|
||||
* - but Bookshelf is not in our control for this case
|
||||
*
|
||||
* @IMPORTANT
|
||||
* Before the new client data get's inserted again, the dates get's re-transformed into
|
||||
* proper strings, see `format`.
|
||||
*
|
||||
* @IMPORTANT
|
||||
* Sanitize relations.
|
||||
*/
|
||||
sanitizeData: function sanitizeData(data) {
|
||||
const tableName = _.result(this.prototype, 'tableName');
|
||||
let date;
|
||||
|
||||
_.each(data, (value, property) => {
|
||||
if (value !== null
|
||||
&& Object.prototype.hasOwnProperty.call(schema.tables[tableName], property)
|
||||
&& schema.tables[tableName][property].type === 'dateTime'
|
||||
&& typeof value === 'string'
|
||||
) {
|
||||
date = new Date(value);
|
||||
|
||||
// CASE: client sends `0000-00-00 00:00:00`
|
||||
if (isNaN(date)) {
|
||||
throw new errors.ValidationError({
|
||||
message: tpl(messages.invalidDate, {key: property}),
|
||||
code: 'DATE_INVALID'
|
||||
});
|
||||
}
|
||||
|
||||
data[property] = moment(value).toDate();
|
||||
}
|
||||
|
||||
if (this.prototype.relationships && this.prototype.relationships.indexOf(property) !== -1) {
|
||||
let relations = data[property];
|
||||
|
||||
// CASE: 1:1 relation will have single data point
|
||||
if (!_.isArray(data[property])) {
|
||||
relations = [data[property]];
|
||||
}
|
||||
_.each(relations, (relation, indexInArr) => {
|
||||
_.each(relation, (relationValue, relationProperty) => {
|
||||
if (relationValue !== null
|
||||
&& Object.prototype.hasOwnProperty.call(schema.tables[this.prototype.relationshipBelongsTo[property]], relationProperty)
|
||||
&& schema.tables[this.prototype.relationshipBelongsTo[property]][relationProperty].type === 'dateTime'
|
||||
&& typeof relationValue === 'string'
|
||||
) {
|
||||
date = new Date(relationValue);
|
||||
|
||||
// CASE: client sends `0000-00-00 00:00:00`
|
||||
if (isNaN(date)) {
|
||||
throw new errors.ValidationError({
|
||||
message: tpl(messages.invalidDate, {key: relationProperty}),
|
||||
code: 'DATE_INVALID'
|
||||
});
|
||||
}
|
||||
|
||||
data[property][indexInArr][relationProperty] = moment(relationValue).toDate();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Filters potentially unsafe `options` in a model method's arguments, so you can pass them to Bookshelf / Knex.
|
||||
* @param {Object} unfilteredOptions Represents options to filter in order to be passed to the Bookshelf query.
|
||||
* @param {String} methodName The name of the method to check valid options for.
|
||||
* @return {Object} The filtered results of `options`.
|
||||
*/
|
||||
filterOptions: function filterOptions(unfilteredOptions, methodName, filterConfig) {
|
||||
unfilteredOptions = unfilteredOptions || {};
|
||||
filterConfig = filterConfig || {};
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(unfilteredOptions, 'include')) {
|
||||
throw new errors.IncorrectUsageError({
|
||||
message: 'The model layer expects using `withRelated`.'
|
||||
});
|
||||
}
|
||||
|
||||
let options = _.cloneDeep(unfilteredOptions);
|
||||
const extraAllowedProperties = filterConfig.extraAllowedProperties || [];
|
||||
let permittedOptions;
|
||||
|
||||
permittedOptions = this.permittedOptions(methodName, options);
|
||||
permittedOptions = _.union(permittedOptions, extraAllowedProperties);
|
||||
options = _.pick(options, permittedOptions);
|
||||
|
||||
if (this.defaultRelations) {
|
||||
options = this.defaultRelations(methodName, options);
|
||||
}
|
||||
|
||||
return options;
|
||||
},
|
||||
|
||||
// ## Model Data Functions
|
||||
|
||||
getFilteredCollection: function getFilteredCollection(options) {
|
||||
|
178
core/server/models/base/sanitize.js
Normal file
178
core/server/models/base/sanitize.js
Normal file
@ -0,0 +1,178 @@
|
||||
const _ = require('lodash');
|
||||
const errors = require('@tryghost/errors');
|
||||
const moment = require('moment');
|
||||
const tpl = require('@tryghost/tpl');
|
||||
|
||||
const schema = require('../../data/schema');
|
||||
|
||||
const messages = {
|
||||
missingContext: 'missing context',
|
||||
invalidDate: 'Date format for `{key}` is invalid.'
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {import('bookshelf')} Bookshelf
|
||||
*/
|
||||
module.exports = function (Bookshelf) {
|
||||
Bookshelf.Model = Bookshelf.Model.extend({
|
||||
// Ghost option handling - get permitted attributes from server/data/schema.js, where the DB schema is defined
|
||||
permittedAttributes: function permittedAttributes() {
|
||||
return _.keys(schema.tables[this.tableName])
|
||||
.filter(key => key.indexOf('@@') === -1);
|
||||
}
|
||||
}, {
|
||||
/**
|
||||
* Returns an array of keys permitted in every method's `options` hash.
|
||||
* Can be overridden and added to by a model's `permittedOptions` method.
|
||||
*
|
||||
* importing: is used when import a JSON file or when migrating the database
|
||||
*
|
||||
* @return {Object} Keys allowed in the `options` hash of every model's method.
|
||||
*/
|
||||
permittedOptions: function permittedOptions(methodName) {
|
||||
const baseOptions = ['context', 'withRelated'];
|
||||
const extraOptions = ['transacting', 'importing', 'forUpdate', 'migrating'];
|
||||
|
||||
switch (methodName) {
|
||||
case 'toJSON':
|
||||
return baseOptions.concat('shallow', 'columns', 'previous');
|
||||
case 'destroy':
|
||||
return baseOptions.concat(extraOptions, ['id', 'destroyBy', 'require']);
|
||||
case 'edit':
|
||||
return baseOptions.concat(extraOptions, ['id', 'require']);
|
||||
case 'findOne':
|
||||
return baseOptions.concat(extraOptions, ['columns', 'require', 'mongoTransformer']);
|
||||
case 'findAll':
|
||||
return baseOptions.concat(extraOptions, ['filter', 'columns', 'mongoTransformer']);
|
||||
case 'findPage':
|
||||
return baseOptions.concat(extraOptions, ['filter', 'order', 'autoOrder', 'page', 'limit', 'columns', 'mongoTransformer']);
|
||||
default:
|
||||
return baseOptions.concat(extraOptions);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Filters potentially unsafe model attributes, so you can pass them to Bookshelf / Knex.
|
||||
* This filter should be called before each insert/update operation.
|
||||
*
|
||||
* @param {Object} data Has keys representing the model's attributes/fields in the database.
|
||||
* @return {Object} The filtered results of the passed in data, containing only what's allowed in the schema.
|
||||
*/
|
||||
filterData: function filterData(data) {
|
||||
const permittedAttributes = this.prototype.permittedAttributes();
|
||||
const filteredData = _.pick(data, permittedAttributes);
|
||||
const sanitizedData = this.sanitizeData(filteredData);
|
||||
|
||||
return sanitizedData;
|
||||
},
|
||||
|
||||
/**
|
||||
* `sanitizeData` ensures that client data is in the correct format for further operations.
|
||||
*
|
||||
* Dates:
|
||||
* - client dates are sent as ISO 8601 format (moment(..).format())
|
||||
* - server dates are in JS Date format
|
||||
* >> when bookshelf fetches data from the database, all dates are in JS Dates
|
||||
* >> see `parse`
|
||||
* - Bookshelf updates the model with the new client data via the `set` function
|
||||
* - Bookshelf uses a simple `isEqual` function from lodash to detect real changes
|
||||
* - .previous(attr) and .get(attr) returns false obviously
|
||||
* - internally we use our `hasDateChanged` if we have to compare previous dates
|
||||
* - but Bookshelf is not in our control for this case
|
||||
*
|
||||
* @IMPORTANT
|
||||
* Before the new client data get's inserted again, the dates get's re-transformed into
|
||||
* proper strings, see `format`.
|
||||
*
|
||||
* @IMPORTANT
|
||||
* Sanitize relations.
|
||||
*/
|
||||
sanitizeData: function sanitizeData(data) {
|
||||
const tableName = _.result(this.prototype, 'tableName');
|
||||
let date;
|
||||
|
||||
_.each(data, (value, property) => {
|
||||
if (value !== null
|
||||
&& Object.prototype.hasOwnProperty.call(schema.tables[tableName], property)
|
||||
&& schema.tables[tableName][property].type === 'dateTime'
|
||||
&& typeof value === 'string'
|
||||
) {
|
||||
date = new Date(value);
|
||||
|
||||
// CASE: client sends `0000-00-00 00:00:00`
|
||||
if (isNaN(date)) {
|
||||
throw new errors.ValidationError({
|
||||
message: tpl(messages.invalidDate, {key: property}),
|
||||
code: 'DATE_INVALID'
|
||||
});
|
||||
}
|
||||
|
||||
data[property] = moment(value).toDate();
|
||||
}
|
||||
|
||||
if (this.prototype.relationships && this.prototype.relationships.indexOf(property) !== -1) {
|
||||
let relations = data[property];
|
||||
|
||||
// CASE: 1:1 relation will have single data point
|
||||
if (!_.isArray(data[property])) {
|
||||
relations = [data[property]];
|
||||
}
|
||||
_.each(relations, (relation, indexInArr) => {
|
||||
_.each(relation, (relationValue, relationProperty) => {
|
||||
if (relationValue !== null
|
||||
&& Object.prototype.hasOwnProperty.call(schema.tables[this.prototype.relationshipBelongsTo[property]], relationProperty)
|
||||
&& schema.tables[this.prototype.relationshipBelongsTo[property]][relationProperty].type === 'dateTime'
|
||||
&& typeof relationValue === 'string'
|
||||
) {
|
||||
date = new Date(relationValue);
|
||||
|
||||
// CASE: client sends `0000-00-00 00:00:00`
|
||||
if (isNaN(date)) {
|
||||
throw new errors.ValidationError({
|
||||
message: tpl(messages.invalidDate, {key: relationProperty}),
|
||||
code: 'DATE_INVALID'
|
||||
});
|
||||
}
|
||||
|
||||
data[property][indexInArr][relationProperty] = moment(relationValue).toDate();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Filters potentially unsafe `options` in a model method's arguments, so you can pass them to Bookshelf / Knex.
|
||||
* @param {Object} unfilteredOptions Represents options to filter in order to be passed to the Bookshelf query.
|
||||
* @param {String} methodName The name of the method to check valid options for.
|
||||
* @return {Object} The filtered results of `options`.
|
||||
*/
|
||||
filterOptions: function filterOptions(unfilteredOptions, methodName, filterConfig) {
|
||||
unfilteredOptions = unfilteredOptions || {};
|
||||
filterConfig = filterConfig || {};
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(unfilteredOptions, 'include')) {
|
||||
throw new errors.IncorrectUsageError({
|
||||
message: 'The model layer expects using `withRelated`.'
|
||||
});
|
||||
}
|
||||
|
||||
let options = _.cloneDeep(unfilteredOptions);
|
||||
const extraAllowedProperties = filterConfig.extraAllowedProperties || [];
|
||||
let permittedOptions;
|
||||
|
||||
permittedOptions = this.permittedOptions(methodName, options);
|
||||
permittedOptions = _.union(permittedOptions, extraAllowedProperties);
|
||||
options = _.pick(options, permittedOptions);
|
||||
|
||||
if (this.defaultRelations) {
|
||||
options = this.defaultRelations(methodName, options);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
});
|
||||
};
|
Loading…
Reference in New Issue
Block a user