diff --git a/core/server/api/authentication.js b/core/server/api/authentication.js index fc5aef900a..96b6b0ff19 100644 --- a/core/server/api/authentication.js +++ b/core/server/api/authentication.js @@ -55,7 +55,7 @@ authentication = { }] }; - return mail.send(payload); + return mail.send(payload, {context: {internal: true}}); }).then(function () { return when.resolve({passwordreset: [{message: 'Check your email for further instructions.'}]}); }).otherwise(function (error) { @@ -197,7 +197,7 @@ authentication = { }] }; - return mail.send(payload).otherwise(function (error) { + return mail.send(payload, {context: {internal: true}}).otherwise(function (error) { errors.logError( error.message, "Unable to send welcome email, your blog will continue to function.", diff --git a/core/server/api/mail.js b/core/server/api/mail.js index 79a52438b6..f14f50255b 100644 --- a/core/server/api/mail.js +++ b/core/server/api/mail.js @@ -2,6 +2,7 @@ // API for sending Mail var when = require('when'), config = require('../config'), + canThis = require('../permissions').canThis, errors = require('../errors'), mail; @@ -21,23 +22,27 @@ mail = { * @param {Mail} object details of the email to send * @returns {Promise} */ - send: function (object) { + send: function (object, options) { var mailer = require('../mail'); - // TODO: permissions - return mailer.send(object.mail[0].message) - .then(function (data) { - delete object.mail[0].options; - // Sendmail returns extra details we don't need and that don't convert to JSON - delete object.mail[0].message.transport; - object.mail[0].status = { - message: data.message - }; - return object; - }) - .otherwise(function (error) { - return when.reject(new errors.EmailError(error.message)); - }); + return canThis(options.context).send.mail().then(function () { + return mailer.send(object.mail[0].message) + .then(function (data) { + delete object.mail[0].options; + // Sendmail returns extra details we don't need and that don't convert to JSON + delete object.mail[0].message.transport; + object.mail[0].status = { + message: data.message + }; + return object; + }) + .otherwise(function (error) { + return when.reject(new errors.EmailError(error.message)); + }); + + }, function () { + return when.reject(new errors.NoPermissionError('You do not have permission to send mail.')); + }); }, /** @@ -48,7 +53,7 @@ mail = { * @param {Object} required property 'to' which contains the recipient address * @returns {Promise} */ - sendTest: function (object) { + sendTest: function (object, options) { var html = '

Hello there!

' + '

Excellent!' + ' You\'ve successfully setup your email config for your Ghost blog over on ' + config().url + '

' + @@ -65,7 +70,7 @@ mail = { } }]}; - return mail.send(payload); + return mail.send(payload, options); } }; diff --git a/core/server/api/notifications.js b/core/server/api/notifications.js index 06468b728c..649e0bf718 100644 --- a/core/server/api/notifications.js +++ b/core/server/api/notifications.js @@ -2,6 +2,7 @@ // RESTful API for creating notifications var when = require('when'), _ = require('lodash'), + canThis = require('../permissions').canThis, errors = require('../errors'), utils = require('./utils'), @@ -23,8 +24,56 @@ notifications = { * Fetch all notifications * @returns {Promise(Notifications)} */ - browse: function browse() { - return when({ 'notifications': notificationsStore }); + browse: function browse(options) { + return canThis(options.context).browse.notification().then(function () { + return when({ 'notifications': notificationsStore }); + }, function () { + return when.reject(new errors.NoPermissionError('You do not have permission to browse notifications.')); + }); + }, + + /** + * ### Add + * + * + * **takes:** a notification object of the form + * ``` + * msg = { notifications: [{ + * type: 'error', // this can be 'error', 'success', 'warn' and 'info' + * message: 'This is an error', // A string. Should fit in one line. + * location: 'bottom', // A string where this notification should appear. can be 'bottom' or 'top' + * dismissible: true // A Boolean. Whether the notification is dismissible or not. + * }] }; + * ``` + */ + add: function add(object, options) { + + var defaults = { + dismissible: true, + location: 'bottom', + status: 'persistent' + }, + addedNotifications = []; + + return canThis(options.context).add.notification().then(function () { + return utils.checkObject(object, 'notifications').then(function (checkedNotificationData) { + _.each(checkedNotificationData.notifications, function (notification) { + notificationCounter = notificationCounter + 1; + + notification = _.assign(defaults, notification, { + id: notificationCounter + //status: 'persistent' + }); + + notificationsStore.push(notification); + addedNotifications.push(notification); + }); + + return when({ notifications: addedNotifications}); + }); + }, function () { + return when.reject(new errors.NoPermissionError('You do not have permission to add notifications.')); + }); }, /** @@ -35,24 +84,28 @@ notifications = { * @returns {Promise(Notifications)} */ destroy: function destroy(options) { - var notification = _.find(notificationsStore, function (element) { - return element.id === parseInt(options.id, 10); + return canThis(options.context).destroy.notification().then(function () { + var notification = _.find(notificationsStore, function (element) { + return element.id === parseInt(options.id, 10); + }); + + if (notification && !notification.dismissible) { + return when.reject( + new errors.NoPermissionError('You do not have permission to dismiss this notification.') + ); + } + + if (!notification) { + return when.reject(new errors.NotFoundError('Notification does not exist.')); + } + + notificationsStore = _.reject(notificationsStore, function (element) { + return element.id === parseInt(options.id, 10); + }); + return when({notifications: [notification]}); + }, function () { + return when.reject(new errors.NoPermissionError('You do not have permission to destroy notifications.')); }); - - if (notification && !notification.dismissible) { - return when.reject( - new errors.NoPermissionError('You do not have permission to dismiss this notification.') - ); - } - - if (!notification) { - return when.reject(new errors.NotFoundError('Notification does not exist.')); - } - - notificationsStore = _.reject(notificationsStore, function (element) { - return element.id === parseInt(options.id, 10); - }); - return when({notifications: [notification]}); }, /** @@ -62,50 +115,13 @@ notifications = { * @private Not exposed over HTTP * @returns {Promise} */ - destroyAll: function destroyAll() { - notificationsStore = []; - notificationCounter = 0; - return when(notificationsStore); - }, - - /** - * ### Add - * - * - * **takes:** a notification object of the form - * ``` - * msg = { notifications: [{ - * type: 'error', // this can be 'error', 'success', 'warn' and 'info' - * message: 'This is an error', // A string. Should fit in one line. - * location: 'bottom', // A string where this notification should appear. can be 'bottom' or 'top' - * dismissible: true // A Boolean. Whether the notification is dismissible or not. - * }] }; - * ``` - */ - add: function add(object) { - - var defaults = { - dismissible: true, - location: 'bottom', - status: 'persistent' - }, - addedNotifications = []; - - - return utils.checkObject(object, 'notifications').then(function (checkedNotificationData) { - _.each(checkedNotificationData.notifications, function (notification) { - notificationCounter = notificationCounter + 1; - - notification = _.assign(defaults, notification, { - id: notificationCounter - //status: 'persistent' - }); - - notificationsStore.push(notification); - addedNotifications.push(notification); - }); - - return when({ notifications: addedNotifications}); + destroyAll: function destroyAll(options) { + return canThis(options.context).destroy.notification().then(function () { + notificationsStore = []; + notificationCounter = 0; + return when(notificationsStore); + }, function () { + return when.reject(new errors.NoPermissionError('You do not have permission to destroy notifications.')); }); } }; diff --git a/core/server/api/tags.js b/core/server/api/tags.js index 184c701c6f..ba29198a57 100644 --- a/core/server/api/tags.js +++ b/core/server/api/tags.js @@ -1,6 +1,9 @@ // # Tag API // RESTful API for the Tag resource -var dataProvider = require('../models'), +var when = require('when'), + canThis = require('../permissions').canThis, + dataProvider = require('../models'), + errors = require('../errors'), tags; /** @@ -15,8 +18,13 @@ tags = { * @returns {Promise(Tags)} */ browse: function browse(options) { - return dataProvider.Tag.findAll(options).then(function (result) { - return { tags: result.toJSON() }; + return canThis(options.context).browse.tag().then(function () { + return dataProvider.Tag.findAll(options).then(function (result) { + return { tags: result.toJSON() }; + }); + + }, function () { + return when.reject(new errors.NoPermissionError('You do not have permission to browse tags.')); }); } }; diff --git a/core/server/api/users.js b/core/server/api/users.js index 1efd770857..5d26f482af 100644 --- a/core/server/api/users.js +++ b/core/server/api/users.js @@ -192,7 +192,7 @@ users = { options: {} }] }; - return mail.send(payload).then(function () { + return mail.send(payload, {context: {internal: true}}).then(function () { // If status was invited-pending and sending the invitation succeeded, set status to invited. if (user.status === 'invited-pending') { return dataProvider.User.edit({status: 'invited'}, {id: user.id}); @@ -211,7 +211,7 @@ users = { return when.reject(error); }); }, function () { - return when.reject(new errors.NoPermissionError('You do not have permission to add a users.')); + return when.reject(new errors.NoPermissionError('You do not have permission to add a user.')); }); }, diff --git a/core/server/controllers/admin.js b/core/server/controllers/admin.js index 31c638e406..503d90d8b3 100644 --- a/core/server/controllers/admin.js +++ b/core/server/controllers/admin.js @@ -43,7 +43,7 @@ adminControllers = { return api.notifications.browse().then(function (results) { if (!_.some(results.notifications, { message: notification.message })) { - return api.notifications.add({ notifications: [notification] }); + return api.notifications.add({ notifications: [notification] }, {context: {internal: true}}); } }); }).finally(function () { diff --git a/core/server/index.js b/core/server/index.js index 6087f270f7..d7c898e917 100644 --- a/core/server/index.js +++ b/core/server/index.js @@ -48,7 +48,7 @@ function doFirstRun() { return api.notifications.add({ notifications: [{ type: 'info', message: firstRunMessage.join(' ') - }] }); + }] }, {context: {internal: true}}); } function initDbHashAndFirstRun() { @@ -176,7 +176,7 @@ function initNotifications() { "It is recommended that you explicitly configure an e-mail service,", "See http://docs.ghost.org/mail for instructions" ].join(' ') - }] }); + }] }, {context: {internal: true}}); } if (mailer.state && mailer.state.emailDisabled) { api.notifications.add({ notifications: [{ @@ -185,7 +185,7 @@ function initNotifications() { "Ghost is currently unable to send e-mail.", "See http://docs.ghost.org/mail for instructions" ].join(' ') - }] }); + }] }, {context: {internal: true}}); } } diff --git a/core/server/middleware/index.js b/core/server/middleware/index.js index d6d76b1117..d7401dd0d2 100644 --- a/core/server/middleware/index.js +++ b/core/server/middleware/index.js @@ -299,10 +299,6 @@ setupMiddleware = function (server) { // local data expressServer.use(ghostLocals); - // So on every request we actually clean out redundant passive notifications from the server side - // ToDo: Remove when ember handles passive notifications. - expressServer.use(middleware.cleanNotifications); - // ### Routing // Set up API routes expressServer.use(subdir + routes.apiBaseUri, routes.api(middleware)); diff --git a/core/server/middleware/middleware.js b/core/server/middleware/middleware.js index c8bc7b1bdf..403f4a029a 100644 --- a/core/server/middleware/middleware.js +++ b/core/server/middleware/middleware.js @@ -79,23 +79,6 @@ var middleware = { next(); }, - // While we're here, let's clean up on aisle 5 - // That being ghost.notifications, and let's remove the passives from there - // plus the local messages, as they have already been added at this point - // otherwise they'd appear one too many times - // ToDo: Remove once ember handles passive notifications. - cleanNotifications: function (req, res, next) { - /*jslint unparam:true*/ - api.notifications.browse().then(function (notifications) { - _.each(notifications.notifications, function (notification) { - if (notification.status === 'passive') { - api.notifications.destroy(notification); - } - }); - next(); - }); - }, - // ### CacheControl Middleware // provide sensible cache control headers cacheControl: function (options) { diff --git a/core/test/integration/api/api_db_spec.js b/core/test/integration/api/api_db_spec.js index 3ca5734256..5a51c173e9 100644 --- a/core/test/integration/api/api_db_spec.js +++ b/core/test/integration/api/api_db_spec.js @@ -36,6 +36,7 @@ describe('DB API', function () { }); it('delete all content', function (done) { + var options = {context: {user: 1}}; permissions.init().then(function () { return dbAPI.deleteAllContent({context: {user: 1}}); }).then(function (result) { @@ -43,13 +44,13 @@ describe('DB API', function () { result.db.should.be.instanceof(Array); result.db.should.be.empty; }).then(function () { - return TagsAPI.browse().then(function (results) { + return TagsAPI.browse(options).then(function (results) { should.exist(results); should.exist(results.tags); results.tags.length.should.equal(0); }); }).then(function () { - return PostAPI.browse().then(function (results) { + return PostAPI.browse(options).then(function (results) { should.exist(results); results.posts.length.should.equal(0); done(); @@ -102,17 +103,17 @@ describe('DB API', function () { it('import content is denied', function (done) { permissions.init().then(function () { return dbAPI.importContent({context: {user: 2}}); - }).then(function (result){ + }).then(function (result) { done(new Error("Import content is not denied for editor.")); }, function (error) { error.type.should.eql('NoPermissionError'); return dbAPI.importContent({context: {user: 3}}); - }).then(function (result){ + }).then(function (result) { done(new Error("Import content is not denied for author.")); }, function (error) { error.type.should.eql('NoPermissionError'); return dbAPI.importContent(); - }).then(function (result){ + }).then(function (result) { done(new Error("Import content is not denied without authentication.")); }).catch(function (error) { error.type.should.eql('NoPermissionError'); diff --git a/core/test/integration/api/api_mail_spec.js b/core/test/integration/api/api_mail_spec.js index 62bbf746b1..238e114160 100644 --- a/core/test/integration/api/api_mail_spec.js +++ b/core/test/integration/api/api_mail_spec.js @@ -3,6 +3,7 @@ var testUtils = require('../../utils'), should = require('should'), // Stuff we are testing + permissions = require('../../../server/permissions'), MailAPI = require('../../../server/api/mail'); @@ -22,11 +23,11 @@ describe('Mail API', function () { testUtils.clearData() .then(function () { return testUtils.initData(); - }) - .then(function () { + }).then(function () { return testUtils.insertDefaultFixtures(); - }) - .then(function () { + }).then(function () { + return permissions.init(); + }).then(function () { done(); }).catch(done); }); @@ -40,7 +41,8 @@ describe('Mail API', function () { it('return correct failure message', function (done) { - MailAPI.send(mailData).then(function (response) { + MailAPI.send(mailData, {context: {internal: true}}).then(function (response) { + /*jshint unused:false */ done(); }).catch(function (error) { error.type.should.eql('EmailError'); diff --git a/core/test/integration/api/api_notifications_spec.js b/core/test/integration/api/api_notifications_spec.js index 188ac6bfba..b239d2f0af 100644 --- a/core/test/integration/api/api_notifications_spec.js +++ b/core/test/integration/api/api_notifications_spec.js @@ -3,6 +3,7 @@ var testUtils = require('../../utils'), should = require('should'), // Stuff we are testing + permissions = require('../../../server/permissions'), DataGenerator = require('../../utils/fixtures/data-generator'), NotificationsAPI = require('../../../server/api/notifications'); @@ -18,8 +19,9 @@ describe('Notifications API', function () { testUtils.initData() .then(function () { return testUtils.insertDefaultFixtures(); - }) - .then(function () { + }).then(function () { + return permissions.init(); + }).then(function () { done(); }).catch(done); }); @@ -30,29 +32,13 @@ describe('Notifications API', function () { }).catch(done); }); - it('can browse', function (done) { - var msg = { - type: 'error', // this can be 'error', 'success', 'warn' and 'info' - message: 'This is an error', // A string. Should fit in one line. - }; - NotificationsAPI.add({ notifications: [msg] }).then(function (notification) { - NotificationsAPI.browse().then(function (results) { - should.exist(results); - should.exist(results.notifications); - results.notifications.length.should.be.above(0); - testUtils.API.checkResponse(results.notifications[0], 'notification'); - done(); - }).catch(done); - }); - }); - - it('can add, adds defaults', function (done) { + it('can add, adds defaults (internal)', function (done) { var msg = { type: 'info', message: 'Hello, this is dog' }; - NotificationsAPI.add({ notifications: [msg] }).then(function (result) { + NotificationsAPI.add({ notifications: [msg] }, {context: {internal: true}}).then(function (result) { var notification; should.exist(result); @@ -67,14 +53,35 @@ describe('Notifications API', function () { }).catch(done); }); - it('can add, adds id and status', function (done) { + it('can add, adds defaults (owner)', function (done) { + var msg = { + type: 'info', + message: 'Hello, this is dog' + }; + + NotificationsAPI.add({ notifications: [msg] }, {context: {user: 1}}).then(function (result) { + var notification; + + should.exist(result); + should.exist(result.notifications); + + notification = result.notifications[0]; + notification.dismissible.should.be.true; + should.exist(notification.location); + notification.location.should.equal('bottom'); + + done(); + }).catch(done); + }); + + it('can add, adds id and status (internal)', function (done) { var msg = { type: 'info', message: 'Hello, this is dog', id: 99 }; - NotificationsAPI.add({ notifications: [msg] }).then(function (result) { + NotificationsAPI.add({ notifications: [msg] }, {context: {internal: true}}).then(function (result) { var notification; should.exist(result); @@ -84,22 +91,56 @@ describe('Notifications API', function () { notification.id.should.be.a.Number; notification.id.should.not.equal(99); should.exist(notification.status); - notification.status.should.equal('persistent') + notification.status.should.equal('persistent'); done(); }).catch(done); }); - it('can destroy', function (done) { + it('can browse (internal)', function (done) { + var msg = { + type: 'error', // this can be 'error', 'success', 'warn' and 'info' + message: 'This is an error' // A string. Should fit in one line. + }; + NotificationsAPI.add({ notifications: [msg] }, {context: {internal: true}}).then(function (notification) { + NotificationsAPI.browse({context: {internal: true}}).then(function (results) { + should.exist(results); + should.exist(results.notifications); + results.notifications.length.should.be.above(0); + testUtils.API.checkResponse(results.notifications[0], 'notification'); + done(); + }).catch(done); + }); + }); + + it('can browse (owner)', function (done) { + var msg = { + type: 'error', // this can be 'error', 'success', 'warn' and 'info' + message: 'This is an error' // A string. Should fit in one line. + }; + NotificationsAPI.add({ notifications: [msg] }, {context: {internal: true}}).then(function (notification) { + NotificationsAPI.browse({context: {user: 1}}).then(function (results) { + should.exist(results); + should.exist(results.notifications); + results.notifications.length.should.be.above(0); + testUtils.API.checkResponse(results.notifications[0], 'notification'); + done(); + }).catch(done); + }); + }); + + + + it('can destroy (internal)', function (done) { var msg = { type: 'error', message: 'Goodbye, cruel world!' }; - NotificationsAPI.add({ notifications: [msg] }).then(function (result) { + NotificationsAPI.add({ notifications: [msg] }, {context: {internal: true}}).then(function (result) { var notification = result.notifications[0]; - NotificationsAPI.destroy({ id: notification.id }).then(function (result) { + NotificationsAPI.destroy({ id: notification.id, context: {internal: true}}).then(function (result) { should.exist(result); should.exist(result.notifications); result.notifications[0].id.should.equal(notification.id); @@ -108,4 +149,23 @@ describe('Notifications API', function () { }).catch(done); }); }); + + it('can destroy (owner)', function (done) { + var msg = { + type: 'error', + message: 'Goodbye, cruel world!' + }; + + NotificationsAPI.add({ notifications: [msg] }, {context: {internal: true}}).then(function (result) { + var notification = result.notifications[0]; + + NotificationsAPI.destroy({ id: notification.id, context: {user: 1}}).then(function (result) { + should.exist(result); + should.exist(result.notifications); + result.notifications[0].id.should.equal(notification.id); + + done(); + }).catch(done); + }); + }); }); diff --git a/core/test/integration/api/api_tags_spec.js b/core/test/integration/api/api_tags_spec.js index bfadb3c95a..2a12d381de 100644 --- a/core/test/integration/api/api_tags_spec.js +++ b/core/test/integration/api/api_tags_spec.js @@ -3,6 +3,7 @@ var testUtils = require('../../utils'), should = require('should'), // Stuff we are testing + permissions = require('../../../server/permissions'), DataGenerator = require('../../utils/fixtures/data-generator'), TagsAPI = require('../../../server/api/tags'); @@ -18,8 +19,13 @@ describe('Tags API', function () { testUtils.initData() .then(function () { return testUtils.insertDefaultFixtures(); - }) - .then(function () { + }).then(function () { + return testUtils.insertEditorUser(); + }).then(function () { + return testUtils.insertAuthorUser(); + }).then(function () { + return permissions.init(); + }).then(function () { done(); }).catch(done); }); @@ -30,8 +36,44 @@ describe('Tags API', function () { }).catch(done); }); - it('can browse', function (done) { - TagsAPI.browse().then(function (results) { + it('can browse (internal)', function (done) { + TagsAPI.browse({context: {internal: true}}).then(function (results) { + should.exist(results); + should.exist(results.tags); + results.tags.length.should.be.above(0); + testUtils.API.checkResponse(results.tags[0], 'tag'); + results.tags[0].created_at.should.be.an.instanceof(Date); + + done(); + }).catch(done); + }); + + it('can browse (admin)', function (done) { + TagsAPI.browse({context: {user: 1}}).then(function (results) { + should.exist(results); + should.exist(results.tags); + results.tags.length.should.be.above(0); + testUtils.API.checkResponse(results.tags[0], 'tag'); + results.tags[0].created_at.should.be.an.instanceof(Date); + + done(); + }).catch(done); + }); + + it('can browse (editor)', function (done) { + TagsAPI.browse({context: {user: 2}}).then(function (results) { + should.exist(results); + should.exist(results.tags); + results.tags.length.should.be.above(0); + testUtils.API.checkResponse(results.tags[0], 'tag'); + results.tags[0].created_at.should.be.an.instanceof(Date); + + done(); + }).catch(done); + }); + + it('can browse (author)', function (done) { + TagsAPI.browse({context: {user: 3}}).then(function (results) { should.exist(results); should.exist(results.tags); results.tags.length.should.be.above(0); diff --git a/core/test/unit/middleware_spec.js b/core/test/unit/middleware_spec.js index 078ab2f9a4..d874c2e010 100644 --- a/core/test/unit/middleware_spec.js +++ b/core/test/unit/middleware_spec.js @@ -39,44 +39,6 @@ describe('Middleware', function () { // }); // }); - describe('cleanNotifications', function () { - - beforeEach(function (done) { - api.notifications.add({ notifications: [{ - id: 0, - status: 'passive', - message: 'passive-one' - }] }).then(function () { - return api.notifications.add({ notifications: [{ - id: 1, - status: 'passive', - message: 'passive-two' - }] }); - }).then(function () { - return api.notifications.add({ notifications: [{ - id: 2, - status: 'aggressive', - message: 'aggressive' - }] }); - }).then(function () { - done(); - }).catch(done); - }); - - it('should clean all passive messages', function (done) { - middleware.cleanNotifications(null, null, function () { - api.notifications.browse().then(function (notifications) { - should(notifications.notifications.length).eql(1); - var passiveMsgs = _.filter(notifications.notifications, function (notification) { - return notification.status === 'passive'; - }); - assert.equal(passiveMsgs.length, 0); - done(); - }).catch(done); - }); - }); - }); - describe('cacheControl', function () { var res;