diff --git a/core/client/tpl/settings/general.hbs b/core/client/tpl/settings/general.hbs index c2c91f313f..96a2eaf336 100644 --- a/core/client/tpl/settings/general.hbs +++ b/core/client/tpl/settings/general.hbs @@ -70,7 +70,7 @@

Select a theme for your blog

diff --git a/core/server/bookshelf-session.js b/core/server/bookshelf-session.js index a25a29a91b..b1d57794b4 100644 --- a/core/server/bookshelf-session.js +++ b/core/server/bookshelf-session.js @@ -1,17 +1,16 @@ -var Store = require('express').session.Store, +var Store = require('express').session.Store, + models = require('./models'), time12h = 12 * 60 * 60 * 1000, - BSStore, - db, - client; + + BSStore; // Initialize store and clean old sessions -BSStore = function BSStore(dataProvider, options) { +BSStore = function BSStore(options) { var self = this; - this.dataProvider = dataProvider; options = options || {}; Store.call(this, options); - this.dataProvider.Session.findAll() + models.Session.findAll() .then(function (model) { var i, now = new Date().getTime(); @@ -30,7 +29,7 @@ BSStore.prototype.set = function (sid, sessData, callback) { var maxAge = sessData.cookie.maxAge, now = new Date().getTime(), expires = maxAge ? now + maxAge : now + time12h, - sessionModel = this.dataProvider.Session; + sessionModel = models.Session; sessData = JSON.stringify(sessData); @@ -54,7 +53,7 @@ BSStore.prototype.get = function (sid, callback) { sess, expires; - this.dataProvider.Session.forge({id: sid}) + models.Session.forge({id: sid}) .fetch() .then(function (model) { if (model) { @@ -73,7 +72,7 @@ BSStore.prototype.get = function (sid, callback) { // delete a given sessions BSStore.prototype.destroy = function (sid, callback) { - this.dataProvider.Session.forge({id: sid}) + models.Session.forge({id: sid}) .destroy() .then(function () { // check if callback is null @@ -87,7 +86,7 @@ BSStore.prototype.destroy = function (sid, callback) { // get the count of all stored sessions BSStore.prototype.length = function (callback) { - this.dataProvider.Session.findAll() + models.Session.findAll() .then(function (model) { callback(null, model.length); }); @@ -95,7 +94,7 @@ BSStore.prototype.length = function (callback) { // delete all sessions BSStore.prototype.clear = function (callback) { - this.dataProvider.Session.destroyAll() + models.Session.destroyAll() .then(function () { callback(); }); diff --git a/core/server/middleware/index.js b/core/server/middleware/index.js index b95b7fd4c9..7099537948 100644 --- a/core/server/middleware/index.js +++ b/core/server/middleware/index.js @@ -264,7 +264,7 @@ module.exports = function (server, dbHash) { expressServer.use(express.cookieParser()); expressServer.use(express.session({ - store: new BSStore(models), + store: new BSStore(), proxy: true, secret: dbHash, cookie: cookie diff --git a/core/server/models/base.js b/core/server/models/base.js index f306635efc..47d47e9b3f 100644 --- a/core/server/models/base.js +++ b/core/server/models/base.js @@ -1,5 +1,4 @@ -var ghostBookshelf, - Bookshelf = require('bookshelf'), +var Bookshelf = require('bookshelf'), when = require('when'), moment = require('moment'), _ = require('lodash'), @@ -7,7 +6,10 @@ var ghostBookshelf, config = require('../config'), Validator = require('validator').Validator, unidecode = require('unidecode'), - sanitize = require('validator').sanitize; + sanitize = require('validator').sanitize, + schema = require('../data/schema'), + + ghostBookshelf; // Initializes a new Bookshelf instance, for reference elsewhere in Ghost. ghostBookshelf = Bookshelf.ghost = Bookshelf.initialize(config().database); @@ -21,6 +23,11 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({ hasTimestamps: true, + // get permitted attributs from schema.js + permittedAttributes: function () { + return _.keys(schema.tables[this.tableName]); + }, + defaults: function () { return { uuid: uuid.v4() @@ -28,9 +35,17 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({ }, initialize: function () { + var self = this; this.on('creating', this.creating, this); - this.on('saving', this.saving, this); - this.on('saving', this.validate, this); + this.on('saving', function (model, attributes, options) { + return when(self.saving(model, attributes, options)).then(function () { + return self.validate(model, attributes, options); + }); + }); + }, + + validate: function () { + return true; }, creating: function () { @@ -40,10 +55,13 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({ }, saving: function () { - // Remove any properties which don't belong on the post model - this.attributes = this.pick(this.permittedAttributes); + // Remove any properties which don't belong on the model + this.attributes = this.pick(this.permittedAttributes()); - this.set('updated_by', 1); + // sessions do not have 'updated_by' column + if (this.tableName !== 'sessions') { + this.set('updated_by', 1); + } }, // Base prototype properties will go here diff --git a/core/server/models/permission.js b/core/server/models/permission.js index 98f9c41724..77064be098 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, + Permission, Permissions; @@ -8,10 +9,6 @@ Permission = ghostBookshelf.Model.extend({ tableName: 'permissions', - permittedAttributes: ['id', 'uuid', 'name', 'object_type', 'action_type', 'object_id', 'created_at', 'created_by', - 'updated_at', 'updated_by'], - - validate: function () { // TODO: validate object_type, action_type and object_id ghostBookshelf.validator.check(this.get('name'), "Permission name cannot be blank").notEmpty(); diff --git a/core/server/models/post.js b/core/server/models/post.js index f0ae44d28d..1deb5640ea 100644 --- a/core/server/models/post.js +++ b/core/server/models/post.js @@ -1,27 +1,23 @@ -var Post, +var _ = require('lodash'), + uuid = require('node-uuid'), + when = require('when'), + errors = require('../errorHandling'), + Showdown = require('showdown'), + github = require('../../shared/vendor/showdown/extensions/github'), + converter = new Showdown.converter({extensions: [github]}), + User = require('./user').User, + Tag = require('./tag').Tag, + Tags = require('./tag').Tags, + ghostBookshelf = require('./base'), + + Post, Posts, - _ = require('lodash'), - uuid = require('node-uuid'), - when = require('when'), - errors = require('../errorHandling'), - Showdown = require('showdown'), - github = require('../../shared/vendor/showdown/extensions/github'), - converter = new Showdown.converter({extensions: [github]}), - User = require('./user').User, - Tag = require('./tag').Tag, - Tags = require('./tag').Tags, - ghostBookshelf = require('./base'); + myTags; Post = ghostBookshelf.Model.extend({ tableName: 'posts', - permittedAttributes: [ - 'id', 'uuid', 'title', 'slug', 'markdown', 'html', 'meta_title', 'meta_description', - 'featured', 'image', 'status', 'language', 'author_id', 'created_at', 'created_by', 'updated_at', 'updated_by', - 'page', 'published_at', 'published_by' - ], - defaults: function () { return { uuid: uuid.v4(), @@ -30,15 +26,22 @@ Post = ghostBookshelf.Model.extend({ }, initialize: function () { + var self = this; this.on('creating', this.creating, this); - this.on('saving', this.updateTags, this); - this.on('saving', this.saving, this); - this.on('saving', this.validate, this); + this.on('saved', this.updateTags, this); + this.on('saving', function (model, attributes, options) { + return when(self.saving(model, attributes, options)).then(function () { + return self.validate(model, attributes, options); + }); + }); }, 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; }, @@ -46,8 +49,10 @@ Post = ghostBookshelf.Model.extend({ /*jslint unparam:true*/ var self = this; - // Remove any properties which don't belong on the post model - this.attributes = this.pick(this.permittedAttributes); + // keep tags for 'saved' event + myTags = this.get('tags'); + + ghostBookshelf.Model.prototype.saving.call(this); this.set('html', converter.makeHtml(this.get('markdown'))); @@ -55,65 +60,45 @@ Post = ghostBookshelf.Model.extend({ //this.set('title', this.sanitize('title').trim()); this.set('title', this.get('title').trim()); - if (this.hasChanged('status') && this.get('status') === 'published') { + if ((this.hasChanged('status') || !this.get('published_at')) && this.get('status') === 'published') { if (!this.get('published_at')) { this.set('published_at', new Date()); } // This will need to go elsewhere in the API layer. this.set('published_by', 1); - } else if (this.get('status') === 'published' && !this.get('published_at')) { - // If somehow this is a published post with no date, fix it... see #2015 - this.set('published_at', new Date()); } - ghostBookshelf.Model.prototype.saving.call(this); - - if (this.hasChanged('slug')) { + if (this.hasChanged('slug') || !this.get('slug')) { // Pass the new slug through the generator to strip illegal characters, detect duplicates - return ghostBookshelf.Model.generateSlug(Post, this.get('slug'), {status: 'all', transacting: options.transacting}) + return ghostBookshelf.Model.generateSlug(Post, this.get('slug') || this.get('title'), + {status: 'all', transacting: options.transacting}) .then(function (slug) { self.set({slug: slug}); }); } + }, creating: function (newPage, attr, options) { /*jslint unparam:true*/ - // set any dynamic default properties - var self = this; + // set any dynamic default properties if (!this.get('author_id')) { this.set('author_id', 1); } ghostBookshelf.Model.prototype.creating.call(this); - - // We require a slug be set when creating a new post - // as the database doesn't allow null slug values. - if (!this.get('slug')) { - // Generating a slug requires a db call to look for conflicting slugs - return ghostBookshelf.Model.generateSlug(Post, this.get('title'), {status: 'all', transacting: options.transacting}) - .then(function (slug) { - self.set({slug: slug}); - }); - } }, - updateTags: function (newTags, attr, options) { + updateTags: function (newPost, attr, options) { /*jslint unparam:true*/ - var self = this; options = options || {}; - - if (newTags === this) { - newTags = this.get('tags'); - } - - if (!newTags || !this.id) { + if (!myTags) { return; } - return Post.forge({id: this.id}).fetch({withRelated: ['tags'], transacting: options.transacting}).then(function (thisPostWithTags) { + return Post.forge({id: newPost.id}).fetch({withRelated: ['tags'], transacting: options.transacting}).then(function (thisPostWithTags) { var existingTags = thisPostWithTags.related('tags').toJSON(), tagOperations = [], @@ -123,17 +108,17 @@ Post = ghostBookshelf.Model.extend({ // First find any tags which have been removed _.each(existingTags, function (existingTag) { - if (!_.some(newTags, function (newTag) { return newTag.name === existingTag.name; })) { + if (!_.some(myTags, function (newTag) { return newTag.name === existingTag.name; })) { tagsToDetach.push(existingTag.id); } }); if (tagsToDetach.length > 0) { - tagOperations.push(self.tags().detach(tagsToDetach, options)); + tagOperations.push(newPost.tags().detach(tagsToDetach, options)); } // Next check if new tags are all exactly the same as what is set on the model - _.each(newTags, function (newTag) { + _.each(myTags, function (newTag) { if (!_.some(existingTags, function (existingTag) { return newTag.name === existingTag.name; })) { // newTag isn't on this post yet tagsToAttach.push(newTag); @@ -143,7 +128,7 @@ Post = ghostBookshelf.Model.extend({ if (!_.isEmpty(tagsToAttach)) { return Tags.forge().query('whereIn', 'name', _.pluck(tagsToAttach, 'name')).fetch(options).then(function (matchingTags) { _.each(matchingTags.toJSON(), function (matchingTag) { - tagOperations.push(self.tags().attach(matchingTag.id, options)); + tagOperations.push(newPost.tags().attach(matchingTag.id, options)); tagsToAttach = _.reject(tagsToAttach, function (tagToAttach) { return tagToAttach.name === matchingTag.name; }); @@ -172,7 +157,7 @@ Post = ghostBookshelf.Model.extend({ // Attach each newly created tag _.each(createdTagsToAttach, function (tagToAttach) { - self.tags().attach(tagToAttach.id, tagToAttach.name, options); + newPost.tags().attach(tagToAttach.id, tagToAttach.name, options); }); } @@ -432,18 +417,16 @@ Post = ghostBookshelf.Model.extend({ }, add: function (newPostData, options) { var self = this; + return ghostBookshelf.Model.add.call(this, newPostData, options).then(function (post) { - // associated models can't be created until the post has an ID, so run this after - return when(post.updateTags(newPostData.tags, null, options)).then(function () { - return self.findOne({status: 'all', id: post.id}, options); - }); + return self.findOne({status: 'all', id: post.id}, options); }); }, edit: function (editedPost, options) { var self = this; - return ghostBookshelf.Model.edit.call(this, editedPost, options).then(function (editedObj) { - return self.findOne({status: 'all', id: editedObj.id}, options); + return ghostBookshelf.Model.edit.call(this, editedPost, options).then(function (post) { + return self.findOne({status: 'all', id: post.id}, options); }); }, destroy: function (_identifier, options) { diff --git a/core/server/models/role.js b/core/server/models/role.js index a0863f03ab..66729ef11a 100644 --- a/core/server/models/role.js +++ b/core/server/models/role.js @@ -1,6 +1,7 @@ var User = require('./user').User, Permission = require('./permission').Permission, ghostBookshelf = require('./base'), + Role, Roles; @@ -8,8 +9,6 @@ Role = ghostBookshelf.Model.extend({ tableName: 'roles', - permittedAttributes: ['id', 'uuid', 'name', 'description', 'created_at', 'created_by', 'updated_at', 'updated_by'], - 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(); diff --git a/core/server/models/session.js b/core/server/models/session.js index 493b8dd4a7..298a021a0e 100644 --- a/core/server/models/session.js +++ b/core/server/models/session.js @@ -1,17 +1,12 @@ var ghostBookshelf = require('./base'), + Session, Sessions; Session = ghostBookshelf.Model.extend({ - tableName: 'sessions', + tableName: 'sessions' - permittedAttributes: ['id', 'expires', 'sess'], - - saving: function () { - // Remove any properties which don't belong on the session model - this.attributes = this.pick(this.permittedAttributes); - } }, { destroyAll: function (options) { options = options || {}; diff --git a/core/server/models/settings.js b/core/server/models/settings.js index fb5cc70a71..49a38bdfcf 100644 --- a/core/server/models/settings.js +++ b/core/server/models/settings.js @@ -5,6 +5,7 @@ var Settings, _ = require('lodash'), errors = require('../errorHandling'), when = require('when'), + defaultSettings; // For neatness, the defaults file is split into categories. @@ -33,8 +34,6 @@ Settings = ghostBookshelf.Model.extend({ tableName: 'settings', - permittedAttributes: ['id', 'uuid', 'key', 'value', 'type', 'created_at', 'created_by', 'updated_at', 'update_by'], - defaults: function () { return { uuid: uuid.v4(), diff --git a/core/server/models/tag.js b/core/server/models/tag.js index b8d66bec85..a251dc0e3b 100644 --- a/core/server/models/tag.js +++ b/core/server/models/tag.js @@ -1,26 +1,21 @@ -var Tag, - Tags, - Posts = require('./post').Posts, - ghostBookshelf = require('./base'); +var Posts = require('./post').Posts, + ghostBookshelf = require('./base'), + + Tag, + Tags; Tag = ghostBookshelf.Model.extend({ tableName: 'tags', - permittedAttributes: [ - 'id', 'uuid', 'name', 'slug', 'description', 'parent_id', 'meta_title', 'meta_description', 'created_at', - 'created_by', 'updated_at', 'updated_by' - ], - validate: function () { return true; }, - creating: function () { + saving: function () { var self = this; - - ghostBookshelf.Model.prototype.creating.call(this); + ghostBookshelf.Model.prototype.saving.apply(this, arguments); if (!this.get('slug')) { // Generating a slug requires a db call to look for conflicting slugs diff --git a/core/server/models/user.js b/core/server/models/user.js index 9b66d91bb8..165738f57e 100644 --- a/core/server/models/user.js +++ b/core/server/models/user.js @@ -1,6 +1,4 @@ -var User, - Users, - _ = require('lodash'), +var _ = require('lodash'), uuid = require('node-uuid'), when = require('when'), errors = require('../errorHandling'), @@ -13,7 +11,9 @@ var User, http = require('http'), crypto = require('crypto'), - tokenSecurity = {}; + tokenSecurity = {}, + User, + Users; function validatePasswordLength(password) { try { @@ -37,12 +37,6 @@ User = ghostBookshelf.Model.extend({ tableName: 'users', - permittedAttributes: [ - 'id', 'uuid', 'name', 'slug', 'password', 'email', 'image', 'cover', 'bio', 'website', 'location', - 'accessibility', 'status', 'language', 'meta_title', 'meta_description', 'last_login', 'created_at', - 'created_by', 'updated_at', 'updated_by' - ], - 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); @@ -53,10 +47,16 @@ User = ghostBookshelf.Model.extend({ return true; }, - creating: function () { + saving: function () { var self = this; + // disabling sanitization until we can implement a better version + // this.set('name', this.sanitize('name')); + // this.set('email', this.sanitize('email')); + // this.set('location', this.sanitize('location')); + // this.set('website', this.sanitize('website')); + // this.set('bio', this.sanitize('bio')); - ghostBookshelf.Model.prototype.creating.call(this); + ghostBookshelf.Model.prototype.saving.apply(this, arguments); if (!this.get('slug')) { // Generating a slug requires a db call to look for conflicting slugs @@ -65,18 +65,7 @@ User = ghostBookshelf.Model.extend({ self.set({slug: slug}); }); } - }, - saving: function () { - - // disabling sanitization until we can implement a better version - // this.set('name', this.sanitize('name')); - // this.set('email', this.sanitize('email')); - // this.set('location', this.sanitize('location')); - // this.set('website', this.sanitize('website')); - // this.set('bio', this.sanitize('bio')); - - return ghostBookshelf.Model.prototype.saving.apply(this, arguments); }, posts: function () { diff --git a/core/test/functional/api/posts_test.js b/core/test/functional/api/posts_test.js index 37af6cc524..08096110c8 100644 --- a/core/test/functional/api/posts_test.js +++ b/core/test/functional/api/posts_test.js @@ -250,11 +250,7 @@ describe('Post API', function () { it('can\'t edit a post with invalid CSRF token', function (done) { request.get(testUtils.API.getApiURL('posts/1/'), function (error, response, body) { - var jsonResponse = JSON.parse(body), - changedValue = 'My new Title'; - jsonResponse.should.exist; - jsonResponse.title = changedValue; - + var jsonResponse = JSON.parse(body); request.put({uri: testUtils.API.getApiURL('posts/1/'), headers: {'X-CSRF-Token': 'invalid-token'}, json: jsonResponse}, function (error, response, putBody) { @@ -263,6 +259,31 @@ describe('Post API', function () { }); }); }); + + it('published_at = null', function (done) { + request.get(testUtils.API.getApiURL('posts/1/'), function (error, response, body) { + var jsonResponse = JSON.parse(body), + changedValue = 'My new Title'; + jsonResponse.should.exist; + jsonResponse.title = changedValue; + jsonResponse.published_at = null; + request.put({uri: testUtils.API.getApiURL('posts/1/'), + headers: {'X-CSRF-Token': csrfToken}, + json: jsonResponse}, function (error, response, putBody) { + response.should.have.status(200); + response.headers['x-cache-invalidate'].should.eql('/, /page/*, /rss/, /rss/*, /' + putBody.slug + '/'); + response.should.be.json; + putBody.should.exist; + putBody.title.should.eql(changedValue); + if (_.isEmpty(putBody.published_at)) { + should.fail('null', 'valid date', 'publish_at should not be empty'); + done(); + } + testUtils.API.checkResponse(putBody, 'post'); + done(); + }); + }); + }); }); // ## delete