From c02ebb0dcfdd57f9cb0f2801751b6783631904bd Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Thu, 8 May 2014 13:41:19 +0100 Subject: [PATCH] Refactor API arguments closes #2610, refs #2697 - cleanup API index.js, and add docs - all API methods take consistent arguments: object & options - browse, read, destroy take options, edit and add take object and options - the context is passed as part of options, meaning no more .call everywhere - destroy expects an object, rather than an id all the way down to the model layer - route params such as :id, :slug, and :key are passed as an option & used to perform reads, updates and deletes where possible - settings / themes may need work here still - HTTP posts api can find a post by slug - Add API utils for checkData --- core/server/api/db.js | 20 +- core/server/api/index.js | 258 ++++--- core/server/api/notifications.js | 42 +- core/server/api/posts.js | 161 +++-- core/server/api/settings.js | 330 +++++---- core/server/api/tags.js | 7 +- core/server/api/themes.js | 28 +- core/server/api/users.js | 122 ++-- core/server/api/utils.js | 14 + core/server/apps/index.js | 6 +- core/server/apps/proxy.js | 24 +- core/server/config/theme.js | 8 +- core/server/config/url.js | 4 +- core/server/controllers/admin.js | 9 +- core/server/controllers/frontend.js | 14 +- core/server/data/import/000.js | 6 +- core/server/helpers/index.js | 6 +- core/server/index.js | 11 +- core/server/mail.js | 2 +- core/server/middleware/index.js | 6 +- core/server/middleware/middleware.js | 2 +- core/server/models/base.js | 29 +- core/server/models/index.js | 4 +- core/server/models/post.js | 161 +++-- core/server/models/role.js | 2 +- core/server/models/session.js | 2 +- core/server/models/settings.js | 16 +- core/server/models/tag.js | 2 +- core/server/models/user.js | 15 +- core/server/permissions/index.js | 13 +- core/server/routes/admin.js | 10 +- core/server/routes/api.js | 76 ++- core/server/routes/frontend.js | 10 +- core/server/update-check.js | 32 +- core/test/blanket_coverage.js | 2 + core/test/functional/routes/api/posts_test.js | 48 +- .../functional/routes/api/settings_test.js | 2 +- core/test/functional/routes/api/users_test.js | 15 +- core/test/integration/api/api_db_spec.js | 14 +- core/test/integration/api/api_posts_spec.js | 2 +- .../test/integration/api/api_settings_spec.js | 101 +-- core/test/integration/api/api_themes_spec.js | 19 +- core/test/integration/api/api_users_spec.js | 29 +- .../test/integration/model/model_apps_spec.js | 21 +- .../model/model_permissions_spec.js | 22 +- .../integration/model/model_posts_spec.js | 166 +++-- .../integration/model/model_roles_spec.js | 22 +- .../integration/model/model_settings_spec.js | 36 +- .../integration/model/model_users_spec.js | 78 +-- core/test/unit/frontend_spec.js | 41 +- core/test/unit/permissions_spec.js | 635 +++++++++--------- core/test/utils/api.js | 2 +- 52 files changed, 1522 insertions(+), 1185 deletions(-) create mode 100644 core/server/api/utils.js diff --git a/core/server/api/db.js b/core/server/api/db.js index b41edcf0be..03c8abfd36 100644 --- a/core/server/api/db.js +++ b/core/server/api/db.js @@ -15,11 +15,11 @@ api.notifications = require('./notifications'); api.settings = require('./settings'); db = { - 'exportContent': function () { - var self = this; + 'exportContent': function (options) { + options = options || {}; // Export data, otherwise send error 500 - return canThis(self.user).exportContent.db().then(function () { + return canThis(options.context).exportContent.db().then(function () { return dataExport().then(function (exportedData) { return when.resolve({ db: [exportedData] }); }).otherwise(function (error) { @@ -30,10 +30,10 @@ db = { }); }, 'importContent': function (options) { - var databaseVersion, - self = this; + options = options || {}; + var databaseVersion; - return canThis(self.user).importContent.db().then(function () { + return canThis(options.context).importContent.db().then(function () { if (!options.importfile || !options.importfile.path || options.importfile.name.indexOf('json') === -1) { /** * Notify of an error if it occurs @@ -46,7 +46,7 @@ db = { return when.reject(new errors.InternalServerError('Please select a .json file to import.')); } - return api.settings.read.call({ internal: true }, { key: 'databaseVersion' }).then(function (response) { + return api.settings.read({key: 'databaseVersion', context: { internal: true }}).then(function (response) { var setting = response.settings[0]; return when(setting.value); @@ -108,10 +108,10 @@ db = { return when.reject(new errors.NoPermissionError('You do not have permission to export data. (no rights)')); }); }, - 'deleteAllContent': function () { - var self = this; + 'deleteAllContent': function (options) { + options = options || {}; - return canThis(self.user).deleteAllContent.db().then(function () { + return canThis(options.context).deleteAllContent.db().then(function () { return when(dataProvider.deleteAllContent()) .then(function () { return when.resolve({ db: [] }); diff --git a/core/server/api/index.js b/core/server/api/index.js index 1617dccb3e..8ae5353e70 100644 --- a/core/server/api/index.js +++ b/core/server/api/index.js @@ -4,20 +4,46 @@ var _ = require('lodash'), when = require('when'), config = require('../config'), + // Include Endpoints db = require('./db'), - settings = require('./settings'), + mail = require('./mail'), notifications = require('./notifications'), posts = require('./posts'), - users = require('./users'), + settings = require('./settings'), tags = require('./tags'), themes = require('./themes'), - mail = require('./mail'), - requestHandler, - init; + users = require('./users'), -// ## Request Handlers + http, + formatHttpErrors, + cacheInvalidationHeader, + locationHeader, + contentDispositionHeader, + init, -function cacheInvalidationHeader(req, result) { +/** + * ### Init + * Initialise the API - populate the settings cache + * @return {Promise(Settings)} Resolves to Settings Collection + */ +init = function () { + return settings.updateSettingsCache(); +}; + +/** + * ### Cache Invalidation Header + * Calculate the header string for the X-Cache-Invalidate: header. + * The resulting string instructs any cache in front of the blog that request has occurred which invalidates any cached + * versions of the listed URIs. + * + * `/*` is used to mean the entire cache is invalid + * + * @private + * @param {Express.request} req Original HTTP Request + * @param {Object} result API method result + * @return {Promise(String)} Resolves to header string + */ +cacheInvalidationHeader = function (req, result) { var parsedUrl = req._parsedUrl.pathname.replace(/\/$/, '').split('/'), method = req.method, endpoint = parsedUrl[4], @@ -54,15 +80,20 @@ function cacheInvalidationHeader(req, result) { } return when(cacheInvalidate); -} +}; -// if api request results in the creation of a new object, construct -// a Location: header that points to the new resource. -// -// arguments: request object, result object from the api call -// returns: a promise that will be fulfilled with the location of the -// resource -function locationHeader(req, result) { +/** + * ### Location Header + * + * If the API request results in the creation of a new object, construct a Location: header which points to the new + * resource. + * + * @private + * @param {Express.request} req Original HTTP Request + * @param {Object} result API method result + * @return {Promise(String)} Resolves to header string + */ +locationHeader = function (req, result) { var apiRoot = config.urlFor('api'), location, post, @@ -81,98 +112,137 @@ function locationHeader(req, result) { } return when(location); -} +}; -// create a header that invokes the 'Save As' dialog -// in the browser when exporting the database to file. -// The 'filename' parameter is governed by [RFC6266](http://tools.ietf.org/html/rfc6266#section-4.3). -// -// for encoding whitespace and non-ISO-8859-1 characters, you MUST -// use the "filename*=" attribute, NOT "filename=". Ideally, both. -// see: http://tools.ietf.org/html/rfc598 -// examples: http://tools.ietf.org/html/rfc6266#section-5 -// -// we'll use ISO-8859-1 characters here to keep it simple. -function dbExportSaveAsHeader() { +/** + * ### Content Disposition Header + * create a header that invokes the 'Save As' dialog in the browser when exporting the database to file. The 'filename' + * parameter is governed by [RFC6266](http://tools.ietf.org/html/rfc6266#section-4.3). + * + * For encoding whitespace and non-ISO-8859-1 characters, you MUST use the "filename*=" attribute, NOT "filename=". + * Ideally, both. Examples: http://tools.ietf.org/html/rfc6266#section-5 + * + * We'll use ISO-8859-1 characters here to keep it simple. + * + * @private + * @see http://tools.ietf.org/html/rfc598 + * @return {string} + */ +contentDispositionHeader = function () { // replace ':' with '_' for OS that don't support it var now = (new Date()).toJSON().replace(/:/g, '_'); return 'Attachment; filename="ghost-' + now + '.json"'; -} +}; -// ### requestHandler -// decorator for api functions which are called via an HTTP request -// takes the API method and wraps it so that it gets data from the request and returns a sensible JSON response -requestHandler = function (apiMethod) { + +/** + * ### Format HTTP Errors + * Converts the error response from the API into a format which can be returned over HTTP + * + * @private + * @param {Array} error + * @return {{errors: Array, statusCode: number}} + */ +formatHttpErrors = function (error) { + var statusCode = 500, + errors = []; + + if (!_.isArray(error)) { + error = [].concat(error); + } + + _.each(error, function (errorItem) { + var errorContent = {}; + + //TODO: add logic to set the correct status code + statusCode = errorItem.code || 500; + + errorContent.message = _.isString(errorItem) ? errorItem : + (_.isObject(errorItem) ? errorItem.message : 'Unknown API Error'); + errorContent.type = errorItem.type || 'InternalServerError'; + errors.push(errorContent); + }); + + return {errors: errors, statusCode: statusCode}; +}; + +/** + * ### HTTP + * + * Decorator for API functions which are called via an HTTP request. Takes the API method and wraps it so that it gets + * data from the request and returns a sensible JSON response. + * + * @public + * @param {Function} apiMethod API method to call + * @return {Function} middleware format function to be called by the route when a matching request is made + */ +http = function (apiMethod) { return function (req, res) { - var options = _.extend(req.body, req.files, req.query, req.params), - apiContext = { - user: (req.session && req.session.user) ? req.session.user : null - }; - - return apiMethod.call(apiContext, options).then(function (result) { - return cacheInvalidationHeader(req, result).then(function (header) { - if (header) { - res.set({ - "X-Cache-Invalidate": header - }); + // We define 2 properties for using as arguments in API calls: + var object = req.body, + options = _.extend({}, req.files, req.query, req.params, { + context: { + user: (req.session && req.session.user) ? req.session.user : null } - }) - .then(function () { - if (apiMethod === db.exportContent) { - res.set({ - "Content-Disposition": dbExportSaveAsHeader() - }); - } - }) - .then(function () { - return locationHeader(req, result).then(function (header) { - if (header) { - res.set({ - 'Location': header - }); - } - - res.json(result || {}); - }); - }); - }, function (error) { - var errorCode, - errors = []; - - if (!_.isArray(error)) { - error = [].concat(error); - } - - _.each(error, function (errorItem) { - var errorContent = {}; - - //TODO: add logic to set the correct status code - errorCode = errorItem.code || 500; - - errorContent['message'] = _.isString(errorItem) ? errorItem : (_.isObject(errorItem) ? errorItem.message : 'Unknown API Error'); - errorContent['type'] = errorItem.type || 'InternalServerError'; - errors.push(errorContent); }); - res.json(errorCode, {errors: errors}); - }); + // If this is a GET, or a DELETE, req.body should be null, so we only have options (route and query params) + // If this is a PUT, POST, or PATCH, req.body is an object + if (_.isEmpty(object)) { + object = options; + options = {}; + } + + return apiMethod(object, options) + // Handle adding headers + .then(function onSuccess(result) { + // Add X-Cache-Invalidate header + return cacheInvalidationHeader(req, result) + .then(function addCacheHeader(header) { + if (header) { + res.set({'X-Cache-Invalidate': header}); + } + + // Add Location header + return locationHeader(req, result); + }).then(function addLocationHeader(header) { + if (header) { + res.set({'Location': header}); + } + + // Add Content-Disposition Header + if (apiMethod === db.exportContent) { + res.set({ + 'Content-Disposition': contentDispositionHeader() + }); + } + // #### Success + // Send a properly formatting HTTP response containing the data with correct headers + res.json(result || {}); + }); + }).catch(function onError(error) { + // #### Error + var httpErrors = formatHttpErrors(error); + // Send a properly formatted HTTP response containing the errors + res.json(httpErrors.statusCode, {errors: httpErrors.errors}); + }); }; }; -init = function () { - return settings.updateSettingsCache(); -}; - -// Public API +/** + * ## Public API + */ module.exports = { - posts: posts, - users: users, - tags: tags, - themes: themes, - notifications: notifications, - settings: settings, + // Extras + init: init, + http: http, + // API Endpoints db: db, mail: mail, - requestHandler: requestHandler, - init: init + notifications: notifications, + posts: posts, + settings: settings, + tags: tags, + themes: themes, + users: users }; diff --git a/core/server/api/notifications.js b/core/server/api/notifications.js index 6a5360d299..63478103c3 100644 --- a/core/server/api/notifications.js +++ b/core/server/api/notifications.js @@ -4,7 +4,7 @@ var when = require('when'), // Holds the persistent notifications notificationsStore = [], - // Holds the last used id + // Holds the last used id notificationCounter = 0, notifications; @@ -15,16 +15,15 @@ notifications = { return when({ 'notifications': notificationsStore }); }, - // #### Destroy - - // **takes:** an identifier object ({id: id}) - destroy: function destroy(i) { + destroy: function destroy(options) { var notification = _.find(notificationsStore, function (element) { - return element.id === parseInt(i.id, 10); + return element.id === parseInt(options.id, 10); }); if (notification && !notification.dismissable) { - return when.reject(new errors.NoPermissionError('You do not have permission to dismiss this notification.')); + return when.reject( + new errors.NoPermissionError('You do not have permission to dismiss this notification.') + ); } if (!notification) { @@ -32,7 +31,7 @@ notifications = { } notificationsStore = _.reject(notificationsStore, function (element) { - return element.id === parseInt(i.id, 10); + return element.id === parseInt(options.id, 10); }); // **returns:** a promise for the deleted object return when({notifications: [notification]}); @@ -44,17 +43,20 @@ notifications = { return when(notificationsStore); }, - // #### Add - - // **takes:** a notification object of the form - // ``` - // msg = { - // 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' - // dismissable: true // A Boolean. Whether the notification is dismissable or not. - // }; - // ``` + /** + * ### Add + * + * + * **takes:** a notification object of the form + * ``` + * msg = { + * 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' + * dismissable: true // A Boolean. Whether the notification is dismissable or not. + * }; + * ``` + */ add: function add(notification) { var defaults = { @@ -64,7 +66,7 @@ notifications = { }; notificationCounter = notificationCounter + 1; - + notification = _.assign(defaults, notification, { id: notificationCounter //status: 'persistent' diff --git a/core/server/api/posts.js b/core/server/api/posts.js index 4f9caaa9f7..0a79ce3bfb 100644 --- a/core/server/api/posts.js +++ b/core/server/api/posts.js @@ -1,23 +1,20 @@ -var when = require('when'), - _ = require('lodash'), - dataProvider = require('../models'), - canThis = require('../permissions').canThis, - errors = require('../errors'), +// # Posts API +var when = require('when'), + _ = require('lodash'), + dataProvider = require('../models'), + canThis = require('../permissions').canThis, + errors = require('../errors'), + utils = require('./utils'), - allowedIncludes = ['created_by', 'updated_by', 'published_by', 'author', 'tags', 'fields'], + docName = 'posts', + allowedIncludes = ['created_by', 'updated_by', 'published_by', 'author', 'tags', 'fields'], posts; -function checkPostData(postData) { - if (_.isEmpty(postData) || _.isEmpty(postData.posts) || _.isEmpty(postData.posts[0])) { - return when.reject(new errors.BadRequestError('No root key (\'posts\') provided.')); - } - return when.resolve(postData); -} - +// ## Helpers function prepareInclude(include) { var index; - include = _.intersection(include.split(","), allowedIncludes); + include = _.intersection(include.split(','), allowedIncludes); index = include.indexOf('author'); if (index !== -1) { @@ -27,16 +24,20 @@ function prepareInclude(include) { return include; } -// ## Posts +// ## API Methods posts = { - // #### Browse - // **takes:** filter / pagination parameters + /** + * ### Browse + * Find a paginated set of posts + * @param {{context, page, limit, status, staticPages, tag}} options (optional) + * @returns {Promise(Posts)} Posts Collection with Meta + */ browse: function browse(options) { options = options || {}; - + // only published posts if no user is present - if (!this.user) { + if (!(options.context && options.context.user)) { options.status = 'published'; } @@ -44,28 +45,30 @@ posts = { options.include = prepareInclude(options.include); } - // **returns:** a promise for a page of posts in a json object return dataProvider.Post.findPage(options); }, - // #### Read - // **takes:** an identifier (id or slug?) + /** + * ### Read + * Find a post, by ID or Slug + * @param {{id_or_slug (required), context, status, include, ...}} options + * @return {Promise(Post)} Post + */ read: function read(options) { - var include; - options = options || {}; + var attrs = ['id', 'slug', 'status'], + data = _.pick(options, attrs); + options = _.omit(options, attrs); // only published posts if no user is present - if (!this.user) { - options.status = 'published'; + if (!(options.context && options.context.user)) { + data.status = 'published'; } if (options.include) { - include = prepareInclude(options.include); - delete options.include; + options.include = prepareInclude(options.include); } - // **returns:** a promise for a single post in a json object - return dataProvider.Post.findOne(options, {include: include}).then(function (result) { + return dataProvider.Post.findOne(data, options).then(function (result) { if (result) { return { posts: [ result.toJSON() ]}; } @@ -75,21 +78,21 @@ posts = { }); }, - // #### Edit - // **takes:** a json object with all the properties which should be updated - edit: function edit(postData) { - // **returns:** a promise for the resulting post in a json object - var self = this, - include; - - return canThis(this).edit.post(postData.id).then(function () { - return checkPostData(postData).then(function (checkedPostData) { - - if (postData.include) { - include = prepareInclude(postData.include); + /** + * ### Edit + * Update properties of a post + * @param {Post} object Post or specific properties to update + * @param {{id (required), context, include,...}} options + * @return {Promise(Post)} Edited Post + */ + edit: function edit(object, options) { + return canThis(options.context).edit.post(options.id).then(function () { + return utils.checkObject(object, docName).then(function (checkedPostData) { + if (options.include) { + options.include = prepareInclude(options.include); } - return dataProvider.Post.edit(checkedPostData.posts[0], {user: self.user, include: include}); + return dataProvider.Post.edit(checkedPostData.posts[0], options); }).then(function (result) { if (result) { var post = result.toJSON(); @@ -108,20 +111,23 @@ posts = { }); }, - // #### Add - // **takes:** a json object representing a post, - add: function add(postData) { - var self = this, - include; + /** + * ### Add + * Create a new post along with any tags + * @param {Post} object + * @param {{context, include,...}} options + * @return {Promise(Post)} Created Post + */ + add: function add(object, options) { + options = options || {}; - // **returns:** a promise for the resulting post in a json object - return canThis(this).create.post().then(function () { - return checkPostData(postData).then(function (checkedPostData) { - if (postData.include) { - include = prepareInclude(postData.include); + return canThis(options.context).create.post().then(function () { + return utils.checkObject(object, docName).then(function (checkedPostData) { + if (options.include) { + options.include = prepareInclude(options.include); } - return dataProvider.Post.add(checkedPostData.posts[0], {user: self.user, include: include}); + return dataProvider.Post.add(checkedPostData.posts[0], options); }).then(function (result) { var post = result.toJSON(); @@ -136,15 +142,18 @@ posts = { }); }, - // #### Destroy - // **takes:** an identifier (id or slug?) - destroy: function destroy(args) { - var self = this; - // **returns:** a promise for a json response with the id of the deleted post - return canThis(this).remove.post(args.id).then(function () { - // TODO: Would it be good to get rid of .call()? - return posts.read.call({user: self.user}, {id : args.id, status: 'all'}).then(function (result) { - return dataProvider.Post.destroy(args.id).then(function () { + + /** + * ### Destroy + * Delete a post, cleans up tag relations, but not unused tags + * @param {{id (required), context,...}} options + * @return {Promise(Post)} Deleted Post + */ + destroy: function destroy(options) { + return canThis(options.context).remove.post(options.id).then(function () { + var readOptions = _.extend({}, options, {status: 'all'}); + return posts.read(readOptions).then(function (result) { + return dataProvider.Post.destroy(options).then(function () { var deletedObj = result; if (deletedObj.posts) { @@ -161,17 +170,21 @@ posts = { }); }, - // #### Generate slug - // **takes:** a string to generate the slug from - generateSlug: function generateSlug(args) { - - return canThis(this).slug.post().then(function () { - return dataProvider.Base.Model.generateSlug(dataProvider.Post, args.title, {status: 'all'}).then(function (slug) { - if (slug) { - return slug; - } - return when.reject(new errors.InternalServerError('Could not generate slug')); - }); + /** + * ## Generate Slug + * Create a unique slug for a given post title + * @param {{title (required), transacting}} options + * @returns {Promise(String)} Unique string + */ + generateSlug: function generateSlug(options) { + return canThis(options.context).slug.post().then(function () { + return dataProvider.Base.Model.generateSlug(dataProvider.Post, options.title, {status: 'all'}) + .then(function (slug) { + if (slug) { + return slug; + } + return when.reject(new errors.InternalServerError('Could not generate slug')); + }); }, function () { return when.reject(new errors.NoPermissionError('You do not have permission.')); }); diff --git a/core/server/api/settings.js b/core/server/api/settings.js index fe06daed23..1af7ce8526 100644 --- a/core/server/api/settings.js +++ b/core/server/api/settings.js @@ -1,33 +1,38 @@ +// # Settings API var _ = require('lodash'), dataProvider = require('../models'), when = require('when'), config = require('../config'), canThis = require('../permissions').canThis, errors = require('../errors'), + utils = require('./utils'), + + docName = 'settings', settings, - settingsFilter, + updateSettingsCache, - readSettingsResult, + settingsFilter, filterPaths, + readSettingsResult, settingsResult, - // Holds cached settings + canEditAllSettings, + + /** + * ## Cache + * Holds cached settings + * @private + * @type {{}} + */ settingsCache = {}; -// ### Helpers -// Filters an object based on a given filter object -settingsFilter = function (settings, filter) { - return _.object(_.filter(_.pairs(settings), function (setting) { - if (filter) { - return _.some(filter.split(','), function (f) { - return setting[1].type === f; - }); - } - return true; - })); -}; - -// Maintain the internal cache of the settings object +/** + * ### Update Settings Cache + * Maintain the internal cache of the settings object + * @public + * @param settings + * @returns {Settings} + */ updateSettingsCache = function (settings) { settings = settings || {}; @@ -47,6 +52,78 @@ updateSettingsCache = function (settings) { }); }; +// ## Helpers + +/** + * ### Settings Filter + * Filters an object based on a given filter object + * @private + * @param settings + * @param filter + * @returns {*} + */ +settingsFilter = function (settings, filter) { + return _.object(_.filter(_.pairs(settings), function (setting) { + if (filter) { + return _.some(filter.split(','), function (f) { + return setting[1].type === f; + }); + } + return true; + })); +}; + +/** + * ### Filter Paths + * Normalizes paths read by require-tree so that the apps and themes modules can use them. Creates an empty + * array (res), and populates it with useful info about the read packages like name, whether they're active + * (comparison with the second argument), and if they have a package.json, that, otherwise false + * @private + * @param {object} paths as returned by require-tree() + * @param {array/string} active as read from the settings object + * @returns {Array} of objects with useful info about apps / themes + */ +filterPaths = function (paths, active) { + var pathKeys = Object.keys(paths), + res = [], + item; + + // turn active into an array (so themes and apps can be checked the same) + if (!Array.isArray(active)) { + active = [active]; + } + + _.each(pathKeys, function (key) { + //do not include hidden files or _messages + if (key.indexOf('.') !== 0 && + key !== '_messages' && + key !== 'README.md' + ) { + item = { + name: key + }; + if (paths[key].hasOwnProperty('package.json')) { + item.package = paths[key]['package.json']; + } else { + item.package = false; + } + + if (_.indexOf(active, key) !== -1) { + item.active = true; + } + res.push(item); + } + }); + return res; +}; + + +/** + * ### Read Settings Result + * @private + * @param settingsModels + * @returns {Settings} + */ readSettingsResult = function (settingsModels) { var settings = _.reduce(settingsModels, function (memo, member) { if (!memo.hasOwnProperty(member.attributes.key)) { @@ -82,79 +159,78 @@ readSettingsResult = function (settingsModels) { return settings; }; - -// Normalizes paths read by require-tree so that the apps and themes modules can use them. -// Creates an empty array (res), and populates it with useful info about the read packages -// like name, whether they're active (comparison with the second argument), and if they -// have a package.json, that, otherwise false -// @param {object} paths as returned by require-tree() -// @param {array/string} active as read from the settings object -// @return {array} of objects with useful info about apps / themes - -filterPaths = function (paths, active) { - var pathKeys = Object.keys(paths), - res = [], - item; - - // turn active into an array (so themes and apps can be checked the same) - if (!Array.isArray(active)) { - active = [active]; - } - - _.each(pathKeys, function (key) { - //do not include hidden files or _messages - if (key.indexOf('.') !== 0 && - key !== '_messages' && - key !== 'README.md' - ) { - item = { - name: key - }; - if (paths[key].hasOwnProperty('package.json')) { - item.package = paths[key]['package.json']; - } else { - item.package = false; - } - - if (_.indexOf(active, key) !== -1) { - item.active = true; - } - res.push(item); - } - }); - return res; -}; - +/** + * ### Settings Result + * @private + * @param settings + * @param type + * @returns {{settings: *}} + */ settingsResult = function (settings, type) { var filteredSettings = _.values(settingsFilter(settings, type)), result = { - settings: filteredSettings + settings: filteredSettings, + meta: {} }; if (type) { - result.meta = { - filters: { - type: type - } + result.meta.filters = { + type: type }; } return result; }; +/** + * ### Can Edit All Settings + * Check that this edit request is allowed for all settings requested to be updated + * @private + * @param settingsInfo + * @returns {*} + */ +canEditAllSettings = function (settingsInfo, options) { + var checks = _.map(settingsInfo, function (settingInfo) { + var setting = settingsCache[settingInfo.key]; + + if (!setting) { + return when.reject(new errors.NotFoundError('Unable to find setting: ' + settingInfo.key)); + } + + if (setting.type === 'core' && !(options.context && options.context.internal)) { + return when.reject( + new errors.NoPermissionError('Attempted to access core setting from external request') + ); + } + + return canThis(options.context).edit.setting(settingInfo.key); + }); + + return when.all(checks); +}; + +// ## API Methods settings = { - // #### Browse - // **takes:** options object + /** + * ### Browse + * @param options + * @returns {*} + */ browse: function browse(options) { - var self = this; + options = options || {}; - // **returns:** a promise for a settings json object - return canThis(this).browse.setting().then(function () { - var result = settingsResult(settingsCache, options.type); + var result = settingsResult(settingsCache, options.type); + // If there is no context, return only blog settings + if (!options.context) { + return when(_.filter(result.settings, function (setting) { return setting.type === 'blog'; })); + } + + // Otherwise return whatever this context is allowed to browse + return canThis(options.context).browse.setting().then(function () { // Omit core settings unless internal request - if (!self.internal) { + if (!options.context.internal) { result.settings = _.filter(result.settings, function (setting) { return setting.type !== 'core'; }); } @@ -162,90 +238,88 @@ settings = { }); }, - // #### Read - - // **takes:** either a json object containing a key, or a single key string + /** + * ### Read + * @param options + * @returns {*} + */ read: function read(options) { if (_.isString(options)) { options = { key: options }; } - var self = this; + var setting = settingsCache[options.key], + result = {}; - return canThis(this).read.setting(options.key).then(function () { - var setting = settingsCache[options.key], - result = {}; + if (!setting) { + return when.reject(new errors.NotFoundError('Unable to find setting: ' + options.key)); + } - if (!setting) { - return when.reject(new errors.NotFoundError('Unable to find setting: ' + options.key)); - } + result[options.key] = setting; - if (!self.internal && setting.type === 'core') { - return when.reject(new errors.NoPermissionError('Attempted to access core setting on external request')); - } + if (setting.type === 'core' && !(options.context && options.context.internal)) { + return when.reject( + new errors.NoPermissionError('Attempted to access core setting from external request') + ); + } - result[options.key] = setting; + if (setting.type === 'blog') { + return when(settingsResult(result)); + } + return canThis(options.context).read.setting(options.key).then(function () { return settingsResult(result); + }, function () { + return when.reject(new errors.NoPermissionError('You do not have permission to read settings.')); }); }, - // #### Edit - - // **takes:** either a json object representing a collection of settings, or a key and value pair - edit: function edit(key, value) { + /** + * ### Edit + * Update properties of a post + * @param {{settings: }} object Setting or a single string name + * @param {{id (required), include,...}} options (optional) or a single string value + * @return {Promise(Setting)} Edited Setting + */ + edit: function edit(object, options) { + options = options || {}; var self = this, - type, - canEditAllSettings = function (settingsInfo) { - var checks = _.map(settingsInfo, function (settingInfo) { - var setting = settingsCache[settingInfo.key]; + type; - if (!setting) { - return when.reject(new errors.NotFoundError('Unable to find setting: ' + settingInfo.key)); - } - - if (!self.internal && setting.type === 'core') { - return when.reject(new errors.NoPermissionError('Attempted to access core setting on external request')); - } - - return canThis(self).edit.setting(settingInfo.key); - }); - - return when.all(checks); - }; - - if (!_.isString(value)) { - value = JSON.stringify(value); - } - - // Allow shorthand syntax - if (_.isString(key)) { - key = { settings: [{ key: key, value: value }]}; + // Allow shorthand syntax where a single key and value are passed to edit instead of object and options + if (_.isString(object)) { + object = { settings: [{ key: object, value: options }]}; } //clean data - type = _.find(key.settings, function (setting) { return setting.key === 'type'; }); + _.each(object.settings, function (setting) { + if (!_.isString(setting.value)) { + setting.value = JSON.stringify(setting.value); + } + }); + + type = _.find(object.settings, function (setting) { return setting.key === 'type'; }); if (_.isObject(type)) { type = type.value; } - key = _.reject(key.settings, function (setting) { + object.settings = _.reject(object.settings, function (setting) { return setting.key === 'type' || setting.key === 'availableThemes' || setting.key === 'availableApps'; }); - return canEditAllSettings(key).then(function () { - return dataProvider.Settings.edit(key, {user: self.user}); - }).then(function (result) { - var readResult = readSettingsResult(result); + return canEditAllSettings(object.settings, options).then(function () { + return utils.checkObject(object, docName).then(function (checkedData) { + options.user = self.user; + return dataProvider.Settings.edit(checkedData.settings, options); + }).then(function (result) { + var readResult = readSettingsResult(result); - return updateSettingsCache(readResult).then(function () { - return config.theme.update(settings, config().url); - }).then(function () { - return settingsResult(readResult, type); + return updateSettingsCache(readResult).then(function () { + return config.theme.update(settings, config().url); + }).then(function () { + return settingsResult(readResult, type); + }); }); - }).catch(function (error) { - // Pass along API error - return when.reject(error); }); } }; diff --git a/core/server/api/tags.js b/core/server/api/tags.js index 5cd7c4d502..f56b205a51 100644 --- a/core/server/api/tags.js +++ b/core/server/api/tags.js @@ -3,11 +3,8 @@ var dataProvider = require('../models'), tags = { - // #### Browse - // **takes:** Nothing yet - browse: function browse() { - // **returns:** a promise for all tags which have previously been used in a json object - return dataProvider.Tag.findAll().then(function (result) { + browse: function browse(options) { + return dataProvider.Tag.findAll(options).then(function (result) { return { tags: result.toJSON() }; }); } diff --git a/core/server/api/themes.js b/core/server/api/themes.js index f4f8e91ab7..6df7c629f4 100644 --- a/core/server/api/themes.js +++ b/core/server/api/themes.js @@ -10,11 +10,12 @@ var when = require('when'), // ## Themes themes = { - browse: function browse() { - // **returns:** a promise for a collection of themes in a json object - return canThis(this).browse.theme().then(function () { + browse: function browse(options) { + options = options || {}; + + return canThis(options.context).browse.theme().then(function () { return when.all([ - settings.read.call({ internal: true }, 'activeTheme'), + settings.read({key: 'activeTheme', context: {internal: true}}), config().paths.availableThemes ]).then(function (result) { var activeTheme = result[0].settings[0].value, @@ -49,19 +50,18 @@ themes = { }); }, - edit: function edit(themeData) { - var self = this, - themeName; + edit: function edit(object, options) { + var themeName; // Check whether the request is properly formatted. - if (!_.isArray(themeData.themes)) { + if (!_.isArray(object.themes)) { return when.reject({type: 'BadRequest', message: 'Invalid request.'}); } - themeName = themeData.themes[0].uuid; + themeName = object.themes[0].uuid; - return canThis(this).edit.theme().then(function () { - return themes.browse.call(self).then(function (availableThemes) { + return canThis(options.context).edit.theme().then(function () { + return themes.browse(options).then(function (availableThemes) { var theme; // Check if the theme exists @@ -73,8 +73,10 @@ themes = { return when.reject(new errors.BadRequestError('Theme does not exist.')); } - // Activate the theme - return settings.edit.call({ internal: true }, 'activeTheme', themeName).then(function () { + // Activate the theme + return settings.edit( + {settings: [{ key: 'activeTheme', value: themeName }]}, {context: {internal: true }} + ).then(function () { theme.active = true; return { themes: [theme]}; }); diff --git a/core/server/api/users.js b/core/server/api/users.js index f4585d08d1..79dcfd622f 100644 --- a/core/server/api/users.js +++ b/core/server/api/users.js @@ -1,27 +1,26 @@ -var when = require('when'), - _ = require('lodash'), - dataProvider = require('../models'), - settings = require('./settings'), - canThis = require('../permissions').canThis, - errors = require('../errors'), - ONE_DAY = 86400000, +var when = require('when'), + _ = require('lodash'), + dataProvider = require('../models'), + settings = require('./settings'), + canThis = require('../permissions').canThis, + errors = require('../errors'), + utils = require('./utils'), + + docName = 'users', + ONE_DAY = 86400000, users; - -function checkUserData(userData) { - if (_.isEmpty(userData) || _.isEmpty(userData.users) || _.isEmpty(userData.users[0])) { - return when.reject(new errors.BadRequestError('No root key (\'users\') provided.')); - } - return when.resolve(userData); -} -// ## Users users = { - // #### Browse - // **takes:** options object + /** + * ## Browse + * Fetch all users + * @param {object} options (optional) + * @returns {Promise(Users)} Users Collection + */ browse: function browse(options) { - // **returns:** a promise for a collection of users in a json object - return canThis(this).browse.user().then(function () { + options = options || {}; + return canThis(options.context).browse.user().then(function () { return dataProvider.User.findAll(options).then(function (result) { return { users: result.toJSON() }; }); @@ -30,15 +29,17 @@ users = { }); }, - // #### Read - // **takes:** an identifier (id or slug?) - read: function read(args) { - // **returns:** a promise for a single user in a json object - if (args.id === 'me') { - args = {id: this.user}; + read: function read(options) { + var attrs = ['id'], + data = _.pick(options, attrs); + + options = _.omit(options, attrs); + + if (data.id === 'me' && options.context && options.context.user) { + data.id = options.context.user; } - return dataProvider.User.findOne(args).then(function (result) { + return dataProvider.User.findOne(data, options).then(function (result) { if (result) { return { users: [result.toJSON()] }; } @@ -47,14 +48,15 @@ users = { }); }, - // #### Edit - // **takes:** a json object representing a user - edit: function edit(userData) { - // **returns:** a promise for the resulting user in a json object - var self = this; - return canThis(this).edit.user(userData.users[0].id).then(function () { - return checkUserData(userData).then(function (checkedUserData) { - return dataProvider.User.edit(checkedUserData.users[0], {user: self.user}); + edit: function edit(object, options) { + if (options.id === 'me' && options.context && options.context.user) { + options.id = options.context.user; + } + + return canThis(options.context).edit.user(options.id).then(function () { + return utils.checkObject(object, docName).then(function (checkedUserData) { + + return dataProvider.User.edit(checkedUserData.users[0], options); }).then(function (result) { if (result) { return { users: [result.toJSON()]}; @@ -66,19 +68,17 @@ users = { }); }, - // #### Add - // **takes:** a json object representing a user - add: function add(userData) { - // **returns:** a promise for the resulting user in a json object - var self = this; - return canThis(this).add.user().then(function () { - return checkUserData(userData).then(function (checkedUserData) { - // if the user is created by users.register(), use id: 1 - // as the creator for now - if (self.internal) { - self.user = 1; + add: function add(object, options) { + options = options || {}; + + return canThis(options.context).add.user().then(function () { + return utils.checkObject(object, docName).then(function (checkedUserData) { + // if the user is created by users.register(), use id: 1 as the creator for now + if (options.context.internal) { + options.context.user = 1; } - return dataProvider.User.add(checkedUserData.users[0], {user: self.user}); + + return dataProvider.User.add(checkedUserData.users[0], options); }).then(function (result) { if (result) { return { users: [result.toJSON()]}; @@ -89,47 +89,37 @@ users = { }); }, - // #### Register - // **takes:** a json object representing a user - register: function register(userData) { - // TODO: if we want to prevent users from being created with the signup form - // this is the right place to do it - return users.add.call({internal: true}, userData); + register: function register(object) { + // TODO: if we want to prevent users from being created with the signup form this is the right place to do it + return users.add(object, {context: {internal: true}}); }, - // #### Check - // Checks a password matches the given email address - // **takes:** a json object representing a user - check: function check(userData) { - // **returns:** on success, returns a promise for the resulting user in a json object - return dataProvider.User.check(userData); + check: function check(object) { + return dataProvider.User.check(object); }, - // #### Change Password - // **takes:** a json object representing a user - changePassword: function changePassword(userData) { - // **returns:** on success, returns a promise for the resulting user in a json object - return dataProvider.User.changePassword(userData); + changePassword: function changePassword(object) { + return dataProvider.User.changePassword(object); }, generateResetToken: function generateResetToken(email) { var expires = Date.now() + ONE_DAY; - return settings.read.call({ internal: true }, 'dbHash').then(function (response) { + return settings.read({context: {internal: true}, key: 'dbHash'}).then(function (response) { var dbHash = response.settings[0].value; return dataProvider.User.generateResetToken(email, expires, dbHash); }); }, validateToken: function validateToken(token) { - return settings.read.call({ internal: true }, 'dbHash').then(function (response) { + return settings.read({context: {internal: true}, key: 'dbHash'}).then(function (response) { var dbHash = response.settings[0].value; return dataProvider.User.validateToken(token, dbHash); }); }, resetPassword: function resetPassword(token, newPassword, ne2Password) { - return settings.read.call({ internal: true }, 'dbHash').then(function (response) { + return settings.read({context: {internal: true}, key: 'dbHash'}).then(function (response) { var dbHash = response.settings[0].value; return dataProvider.User.resetPassword(token, newPassword, ne2Password, dbHash); }); diff --git a/core/server/api/utils.js b/core/server/api/utils.js new file mode 100644 index 0000000000..a8fc211b78 --- /dev/null +++ b/core/server/api/utils.js @@ -0,0 +1,14 @@ +var when = require('when'), + _ = require('lodash'), + utils; + +utils = { + checkObject: function (object, docName) { + if (_.isEmpty(object) || _.isEmpty(object[docName]) || _.isEmpty(object[docName][0])) { + return when.reject({type: 'BadRequest', message: 'No root key (\'' + docName + '\') provided.'}); + } + return when.resolve(object); + } +}; + +module.exports = utils; \ No newline at end of file diff --git a/core/server/apps/index.js b/core/server/apps/index.js index 82013eee9b..9ed75d47be 100644 --- a/core/server/apps/index.js +++ b/core/server/apps/index.js @@ -9,7 +9,7 @@ var _ = require('lodash'), function getInstalledApps() { - return api.settings.read.call({ internal: true }, 'installedApps').then(function (response) { + return api.settings.read({context: {internal: true}, key: 'installedApps'}).then(function (response) { var installed = response.settings[0]; installed.value = installed.value || '[]'; @@ -28,7 +28,7 @@ function saveInstalledApps(installedApps) { return getInstalledApps().then(function (currentInstalledApps) { var updatedAppsInstalled = _.uniq(installedApps.concat(currentInstalledApps)); - return api.settings.edit.call({internal: true}, 'installedApps', updatedAppsInstalled); + return api.settings.edit({context: {internal: true}, key: 'installedApps'}, updatedAppsInstalled); }); } @@ -38,7 +38,7 @@ module.exports = { try { // We have to parse the value because it's a string - api.settings.read.call({ internal: true }, 'activeApps').then(function (response) { + api.settings.read({context: {internal: true}, key: 'activeApps'}).then(function (response) { var aApps = response.settings[0]; appsToLoad = JSON.parse(aApps.value) || []; diff --git a/core/server/apps/proxy.js b/core/server/apps/proxy.js index 97b1424750..1d4d397be0 100644 --- a/core/server/apps/proxy.js +++ b/core/server/apps/proxy.js @@ -39,7 +39,13 @@ var generateProxyFunctions = function (name, permissions) { return _.reduce(apiMethods, function (memo, apiMethod, methodName) { memo[methodName] = function () { - return apiMethod.apply(_.clone(appContext), _.toArray(arguments)); + var args = _.toArray(arguments), + options = args[args.length - 1]; + + if (_.isObject(options)) { + options.context = _.clone(appContext); + } + return apiMethod.apply({}, args); }; return memo; @@ -57,10 +63,18 @@ var generateProxyFunctions = function (name, permissions) { registerAsync: checkRegisterPermissions('helpers', helpers.registerAsyncThemeHelper.bind(helpers)) }, api: { - posts: passThruAppContextToApi('posts', _.pick(api.posts, 'browse', 'read', 'edit', 'add', 'destroy')), - tags: passThruAppContextToApi('tags', _.pick(api.tags, 'browse')), - notifications: passThruAppContextToApi('notifications', _.pick(api.notifications, 'browse', 'add', 'destroy')), - settings: passThruAppContextToApi('settings', _.pick(api.settings, 'browse', 'read', 'edit')) + posts: passThruAppContextToApi('posts', + _.pick(api.posts, 'browse', 'read', 'edit', 'add', 'destroy') + ), + tags: passThruAppContextToApi('tags', + _.pick(api.tags, 'browse') + ), + notifications: passThruAppContextToApi('notifications', + _.pick(api.notifications, 'browse', 'add', 'destroy') + ), + settings: passThruAppContextToApi('settings', + _.pick(api.settings, 'browse', 'read', 'edit') + ) } }; diff --git a/core/server/config/theme.js b/core/server/config/theme.js index d64a9e89db..23686ac396 100644 --- a/core/server/config/theme.js +++ b/core/server/config/theme.js @@ -19,10 +19,10 @@ function theme() { function update(settings, configUrl) { // TODO: Pass the context into this method instead of hard coding internal: true? return when.all([ - settings.read.call({ internal: true }, 'title'), - settings.read.call({ internal: true }, 'description'), - settings.read.call({ internal: true }, 'logo'), - settings.read.call({ internal: true }, 'cover') + settings.read('title'), + settings.read('description'), + settings.read('logo'), + settings.read('cover') ]).then(function (globals) { // normalise the URL by removing any trailing slash themeConfig.url = configUrl.replace(/\/$/, ''); diff --git a/core/server/config/url.js b/core/server/config/url.js index 252f36bf7d..293b3b1ea0 100644 --- a/core/server/config/url.js +++ b/core/server/config/url.js @@ -148,9 +148,9 @@ function urlFor(context, data, absolute) { // - post - a json object representing a post // - absolute (optional, default:false) - boolean whether or not the url should be absolute function urlForPost(settings, post, absolute) { - return settings.read.call({ internal: true }, 'permalinks').then(function (response) { + return settings.read('permalinks').then(function (response) { var permalinks = response.settings[0]; - + return urlFor('post', {post: post, permalinks: permalinks}, absolute); }); } diff --git a/core/server/controllers/admin.js b/core/server/controllers/admin.js index d8a1fb13f4..46992b6d4e 100644 --- a/core/server/controllers/admin.js +++ b/core/server/controllers/admin.js @@ -129,7 +129,7 @@ adminControllers = { }, // frontend route for downloading a file exportContent: function (req, res) { - api.db.exportContent.call({user: req.session.user}).then(function (exportData) { + api.db.exportContent({context: {user: req.session.user}}).then(function (exportData) { // send a file to the client res.set('Content-Disposition', 'attachment; filename="GhostData.json"'); res.json(exportData); @@ -260,10 +260,9 @@ adminControllers = { password: password }]; - api.users.register({users: users}).then(function (apiResp) { - var user = apiResp.users[0]; - - api.settings.edit.call({user: 1}, 'email', email).then(function () { + api.users.register({users: users}).then(function (response) { + var user = response.users[0]; + api.settings.edit({settings: [{key: 'email', value: email}]}, {context: {user: 1}}).then(function () { var message = { to: email, subject: 'Your New Ghost Blog', diff --git a/core/server/controllers/frontend.js b/core/server/controllers/frontend.js index 11a288b155..fddbdcd3ad 100644 --- a/core/server/controllers/frontend.js +++ b/core/server/controllers/frontend.js @@ -22,7 +22,7 @@ var moment = require('moment'), staticPostPermalink = new Route(null, '/:slug/:edit?'); function getPostPage(options) { - return api.settings.read.call({ internal: true }, 'postsPerPage').then(function (response) { + return api.settings.read('postsPerPage').then(function (response) { var postPP = response.settings[0], postsPerPage = parseInt(postPP.value, 10); @@ -123,7 +123,7 @@ frontendControllers = { // Render the page of posts filters.doFilter('prePostsRender', page.posts).then(function (posts) { - api.settings.read.call({ internal: true }, 'activeTheme').then(function (response) { + api.settings.read({key: 'activeTheme', context: {internal: true}}).then(function (response) { var activeTheme = response.settings[0], paths = config().paths.availableThemes[activeTheme.value], view = paths.hasOwnProperty('tag.hbs') ? 'tag' : 'index', @@ -148,7 +148,7 @@ frontendControllers = { editFormat, usingStaticPermalink = false; - api.settings.read.call({ internal: true }, 'permalinks').then(function (response) { + api.settings.read('permalinks').then(function (response) { var permalink = response.settings[0], postLookup; @@ -203,7 +203,7 @@ frontendControllers = { setReqCtx(req, post); filters.doFilter('prePostsRender', post).then(function (post) { - api.settings.read.call({ internal: true }, 'activeTheme').then(function (response) { + api.settings.read({key: 'activeTheme', context: {internal: true}}).then(function (response) { var activeTheme = response.settings[0], paths = config().paths.availableThemes[activeTheme.value], view = template.getThemeViewForPost(paths, post); @@ -282,9 +282,9 @@ frontendControllers = { } return when.settle([ - api.settings.read.call({ internal: true }, 'title'), - api.settings.read.call({ internal: true }, 'description'), - api.settings.read.call({ internal: true }, 'permalinks') + api.settings.read('title'), + api.settings.read('description'), + api.settings.read('permalinks') ]).then(function (result) { var options = {}; diff --git a/core/server/data/import/000.js b/core/server/data/import/000.js index 66110fb990..6ffcea0ef7 100644 --- a/core/server/data/import/000.js +++ b/core/server/data/import/000.js @@ -112,7 +112,7 @@ function importUsers(ops, tableData, transaction) { // don't override the users credentials tableData = stripProperties(['id', 'email', 'password'], tableData); tableData[0].id = 1; - ops.push(models.User.edit(tableData[0], {user: 1, transacting: transaction}) + ops.push(models.User.edit(tableData[0], {id: 1, user: 1, transacting: transaction}) // add pass-through error handling so that bluebird doesn't think we've dropped it .otherwise(function (error) { return when.reject(error); })); } @@ -157,9 +157,9 @@ function importApps(ops, tableData, transaction) { // var appsData = tableData.apps, // appSettingsData = tableData.app_settings, // appName; -// +// // appSettingsData = stripProperties(['id'], appSettingsData); -// +// // _.each(appSettingsData, function (appSetting) { // // Find app to attach settings to // appName = _.find(appsData, function (app) { diff --git a/core/server/helpers/index.js b/core/server/helpers/index.js index a96018e5ef..b8aedd6580 100644 --- a/core/server/helpers/index.js +++ b/core/server/helpers/index.js @@ -389,7 +389,7 @@ coreHelpers.body_class = function (options) { classes.push('page'); } - return api.settings.read.call({ internal: true }, 'activeTheme').then(function (response) { + return api.settings.read({context: {internal: true}, key: 'activeTheme'}).then(function (response) { var activeTheme = response.settings[0], paths = config().paths.availableThemes[activeTheme.value], view; @@ -532,8 +532,8 @@ coreHelpers.meta_description = function (options) { coreHelpers.e = function (key, defaultString, options) { var output; when.all([ - api.settings.read.call({ internal: true }, 'defaultLang'), - api.settings.read.call({ internal: true }, 'forceI18n') + api.settings.read('defaultLang'), + api.settings.read('forceI18n') ]).then(function (values) { if (values[0].settings.value === 'en' && _.isEmpty(options.hash) && diff --git a/core/server/index.js b/core/server/index.js index 3cd9c7650d..8acbd25e7f 100644 --- a/core/server/index.js +++ b/core/server/index.js @@ -52,7 +52,7 @@ function doFirstRun() { } function initDbHashAndFirstRun() { - return api.settings.read.call({ internal: true }, 'dbHash').then(function (response) { + return api.settings.read({key: 'dbHash', context: {internal: true}}).then(function (response) { var hash = response.settings[0].value, initHash; @@ -60,10 +60,11 @@ function initDbHashAndFirstRun() { if (dbHash === null) { initHash = uuid.v4(); - return api.settings.edit.call({ internal: true }, 'dbHash', initHash).then(function (response) { - dbHash = response.settings[0].value; - return dbHash; - }).then(doFirstRun); + return api.settings.edit({settings: [{key: 'dbHash', value: initHash}]}, {context: {internal: true}}) + .then(function (response) { + dbHash = response.settings[0].value; + return dbHash; + }).then(doFirstRun); } return dbHash; diff --git a/core/server/mail.js b/core/server/mail.js index fccd4e4e4f..e07447a4a6 100644 --- a/core/server/mail.js +++ b/core/server/mail.js @@ -106,7 +106,7 @@ GhostMailer.prototype.send = function (payload) { return when.reject(new Error('Email Error: Incomplete message data.')); } - return api.settings.read.call({ internal: true }, 'email').then(function (response) { + return api.settings.read('email').then(function (response) { var email = response.settings[0], to = message.to || email.value; diff --git a/core/server/middleware/index.js b/core/server/middleware/index.js index c0eb3787ad..ada3465597 100644 --- a/core/server/middleware/index.js +++ b/core/server/middleware/index.js @@ -39,7 +39,7 @@ function ghostLocals(req, res, next) { if (res.isAdmin) { res.locals.csrfToken = req.csrfToken(); when.all([ - api.users.read.call({user: req.session.user}, {id: req.session.user}), + api.users.read({id: req.session.user}, {context: {user: req.session.user}}), api.notifications.browse() ]).then(function (values) { var currentUser = values[0].users[0], @@ -150,9 +150,9 @@ function manageAdminAndTheme(req, res, next) { expressServer.enable(expressServer.get('activeTheme')); expressServer.disable('admin'); } - api.settings.read.call({ internal: true }, 'activeTheme').then(function (response) { + api.settings.read({context: {internal: true}, key: 'activeTheme'}).then(function (response) { var activeTheme = response.settings[0]; - + // Check if the theme changed if (activeTheme.value !== expressServer.get('activeTheme')) { // Change theme diff --git a/core/server/middleware/middleware.js b/core/server/middleware/middleware.js index 648db3cf13..37bc060175 100644 --- a/core/server/middleware/middleware.js +++ b/core/server/middleware/middleware.js @@ -170,7 +170,7 @@ var middleware = { // to allow unit testing forwardToExpressStatic: function (req, res, next) { - api.settings.read.call({ internal: true }, 'activeTheme').then(function (response) { + api.settings.read({context: {internal: true}, key: 'activeTheme'}).then(function (response) { var activeTheme = response.settings[0]; // For some reason send divides the max age number by 1000 express['static'](path.join(config().paths.themePath, activeTheme.value), {maxAge: ONE_HOUR_MS})(req, res, next); diff --git a/core/server/models/base.js b/core/server/models/base.js index 942142525b..7b5fbc3ed8 100644 --- a/core/server/models/base.js +++ b/core/server/models/base.js @@ -63,18 +63,20 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({ }, creating: function (newObj, attr, options) { + var user = options.context && options.context.user ? options.context.user : 1; if (!this.get('created_by')) { - this.set('created_by', options.user); + this.set('created_by', user); } }, saving: function (newObj, attr, options) { + var user = options.context && options.context.user ? options.context.user : 1; // Remove any properties which don't belong on the model this.attributes = this.pick(this.permittedAttributes()); // Store the previous attributes so we can tell what was updated later this._updatedAttributes = newObj.previousAttributes(); - this.set('updated_by', options.user); + this.set('updated_by', user); }, // Base prototype properties will go here @@ -153,8 +155,7 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({ } }, { - - // ## Model Data Functions + // ## Data Utility Functions /** * Returns an array of keys permitted in every method's `options` hash. @@ -191,6 +192,8 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({ return filteredOptions; }, + // ## Model Data Functions + /** * ### Find All * Naive find all fetches all the data for a particular model @@ -219,6 +222,7 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({ findOne: function (data, options) { data = this.filterData(data); options = this.filterOptions(options, 'findOne'); + // We pass include to forge so that toJSON has access return this.forge(data, {include: options.include}).fetch(options); }, @@ -230,9 +234,11 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({ * @return {Promise(ghostBookshelf.Model)} Edited Model */ edit: function (data, options) { + var id = options.id; data = this.filterData(data); options = this.filterOptions(options, 'edit'); - return this.forge({id: data.id}).fetch(options).then(function (object) { + + return this.forge({id: id}).fetch(options).then(function (object) { if (object) { return object.save(data, options); } @@ -250,11 +256,8 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({ data = this.filterData(data); options = this.filterOptions(options, 'add'); var instance = this.forge(data); - // We allow you to disable timestamps - // when importing posts so that - // the new posts `updated_at` value - // is the same as the import json blob. - // More details refer to https://github.com/TryGhost/Ghost/issues/1696 + // We allow you to disable timestamps when importing posts so that the new posts `updated_at` value is the same + // as the import json blob. More details refer to https://github.com/TryGhost/Ghost/issues/1696 if (options.importing) { instance.hasTimestamps = false; } @@ -264,13 +267,13 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({ /** * ### Destroy * Naive destroy - * @param {Object} data * @param {Object} options (optional) * @return {Promise(ghostBookshelf.Model)} Empty Model */ - destroy: function (data, options) { + destroy: function (options) { + var id = options.id; options = this.filterOptions(options, 'destroy'); - return this.forge({id: data}).destroy(options); + return this.forge({id: id}).destroy(options); }, /** diff --git a/core/server/models/index.js b/core/server/models/index.js index 64ef0f7dd2..2d4fd774a4 100644 --- a/core/server/models/index.js +++ b/core/server/models/index.js @@ -25,12 +25,12 @@ module.exports = { return self.Post.findAll().then(function (posts) { return when.all(_.map(posts.toJSON(), function (post) { - return self.Post.destroy(post.id); + return self.Post.destroy({id: post.id}); })); }).then(function () { return self.Tag.findAll().then(function (tags) { return when.all(_.map(tags.toJSON(), function (tag) { - return self.Tag.destroy(tag.id); + return self.Tag.destroy({id: tag.id}); })); }); }); diff --git a/core/server/models/post.js b/core/server/models/post.js index 58ffe7648b..d4ab8c0a5c 100644 --- a/core/server/models/post.js +++ b/core/server/models/post.js @@ -44,7 +44,8 @@ Post = ghostBookshelf.Model.extend({ /*jshint unused:false*/ var self = this, tagsToCheck, - i; + i, + user = options.context && options.context.user ? options.context.user : 1; options = options || {}; // keep tags for 'saved' event and deduplicate upper/lowercase tags @@ -75,7 +76,7 @@ Post = ghostBookshelf.Model.extend({ this.set('published_at', new Date()); } // This will need to go elsewhere in the API layer. - this.set('published_by', options.user); + this.set('published_by', user); } if (this.hasChanged('slug') || !this.get('slug')) { @@ -93,9 +94,11 @@ Post = ghostBookshelf.Model.extend({ /*jshint unused:false*/ options = options || {}; + var user = options.context && options.context.user ? options.context.user : 1; + // set any dynamic default properties if (!this.get('author_id')) { - this.set('author_id', options.user); + this.set('author_id', user); } ghostBookshelf.Model.prototype.creating.call(this, newPage, attr, options); @@ -105,7 +108,7 @@ Post = ghostBookshelf.Model.extend({ * ### updateTags * Update tags that are attached to a post. Create any tags that don't already exist. * @param {Object} newPost - * @param {Object} attr + * @param {Object} attr * @param {Object} options * @return {Promise(ghostBookshelf.Models.Post)} Updated Post model */ @@ -243,8 +246,14 @@ Post = ghostBookshelf.Model.extend({ return filteredData; }, - // #### findAll - // Extends base model findAll to eager-fetch author and user relationships. + // ## Model Data Functions + + /** + * ### Find All + * + * @param options + * @returns {*} + */ findAll: function (options) { options = options || {}; options.withRelated = _.union([ 'tags', 'fields' ], options.include); @@ -252,24 +261,24 @@ Post = ghostBookshelf.Model.extend({ }, - // #### findPage - // Find results by page - returns an object containing the - // information about the request (page, limit), along with the - // info needed for pagination (pages, total). - - // **response:** - - // { - // posts: [ - // {...}, {...}, {...} - // ], - // page: __, - // limit: __, - // pages: __, - // total: __ - // } - - /* + /** + * #### findPage + * Find results by page - returns an object containing the + * information about the request (page, limit), along with the + * info needed for pagination (pages, total). + * + * **response:** + * + * { + * posts: [ + * {...}, {...}, {...} + * ], + * page: __, + * limit: __, + * pages: __, + * total: __ + * } + * * @params {Object} options */ findPage: function (options) { @@ -377,22 +386,23 @@ Post = ghostBookshelf.Model.extend({ meta = {}, data = {}; - pagination['page'] = parseInt(options.page, 10); - pagination['limit'] = options.limit; - pagination['pages'] = calcPages === 0 ? 1 : calcPages; - pagination['total'] = totalPosts; - pagination['next'] = null; - pagination['prev'] = null; + pagination.page = parseInt(options.page, 10); + pagination.limit = options.limit; + pagination.pages = calcPages === 0 ? 1 : calcPages; + pagination.total = totalPosts; + pagination.next = null; + pagination.prev = null; + // Pass include to each model so that toJSON works correctly if (options.include) { _.each(postCollection.models, function (item) { item.include = options.include; }); } - data['posts'] = postCollection.toJSON(); - data['meta'] = meta; - meta['pagination'] = pagination; + data.posts = postCollection.toJSON(); + data.meta = meta; + meta.pagination = pagination; if (pagination.pages > 1) { if (pagination.page === 1) { @@ -406,9 +416,9 @@ Post = ghostBookshelf.Model.extend({ } if (tagInstance) { - meta['filters'] = {}; + meta.filters = {}; if (!tagInstance.isNew()) { - meta.filters['tags'] = [tagInstance.toJSON()]; + meta.filters.tags = [tagInstance.toJSON()]; } } @@ -417,59 +427,76 @@ Post = ghostBookshelf.Model.extend({ .catch(errors.logAndThrowError); }, - // #### findOne - // Extends base model read to eager-fetch author and user relationships. - findOne: function (args, options) { + /** + * ### Find One + * @extends ghostBookshelf.Model.findOne to handle post status + * **See:** [ghostBookshelf.Model.findOne](base.js.html#Find%20One) + */ + findOne: function (data, options) { options = options || {}; - args = _.extend({ + data = _.extend({ status: 'published' - }, args || {}); + }, data || {}); - if (args.status === 'all') { - delete args.status; + if (data.status === 'all') { + delete data.status; } // Add related objects options.withRelated = _.union([ 'tags', 'fields' ], options.include); - return ghostBookshelf.Model.findOne.call(this, args, options); + return ghostBookshelf.Model.findOne.call(this, data, options); }, - add: function (newPostData, options) { + /** + * ### Edit + * @extends ghostBookshelf.Model.edit to handle returning the full object and manage _updatedAttributes + * **See:** [ghostBookshelf.Model.edit](base.js.html#edit) + */ + edit: function (data, options) { var self = this; options = options || {}; - return ghostBookshelf.Model.add.call(this, newPostData, options).then(function (post) { - return self.findOne({status: 'all', id: post.id}, options); - }); - }, - edit: function (editedPost, options) { - var self = this; - options = options || {}; - return ghostBookshelf.Model.edit.call(this, editedPost, options).then(function (post) { - if (post) { - return self.findOne({status: 'all', id: post.id}, options) - .then(function (found) { + return ghostBookshelf.Model.edit.call(this, data, options).then(function (post) { + return self.findOne({status: 'all', id: options.id}, options) + .then(function (found) { + if (found) { // Pass along the updated attributes for checking status changes found._updatedAttributes = post._updatedAttributes; return found; - }); - } + } + }); }); }, - destroy: function (_identifier, options) { + + /** + * ### Add + * @extends ghostBookshelf.Model.add to handle returning the full object + * **See:** [ghostBookshelf.Model.add](base.js.html#add) + */ + add: function (data, options) { + var self = this; + options = options || {}; + + return ghostBookshelf.Model.add.call(this, data, options).then(function (post) { + return self.findOne({status: 'all', id: post.id}, options); + }); + }, + + /** + * ### Destroy + * @extends ghostBookshelf.Model.destroy to clean up tag relations + * **See:** [ghostBookshelf.Model.destroy](base.js.html#destroy) + */ + destroy: function (options) { + var id = options.id; options = this.filterOptions(options, 'destroy'); - return this.forge({id: _identifier}).fetch({withRelated: ['tags']}).then(function destroyTags(post) { - var tagIds = _.pluck(post.related('tags').toJSON(), 'id'); - if (tagIds) { - return post.tags().detach(tagIds).then(function destroyPost() { - return post.destroy(options); - }); - } - - return post.destroy(options); + return this.forge({id: id}).fetch({withRelated: ['tags']}).then(function destroyTagsAndPost(post) { + return post.related('tags').detach().then(function () { + return post.destroy(options); + }); }); }, diff --git a/core/server/models/role.js b/core/server/models/role.js index d6b2eb93c6..f1d0c24665 100644 --- a/core/server/models/role.js +++ b/core/server/models/role.js @@ -37,7 +37,7 @@ Role = ghostBookshelf.Model.extend({ } return options; - }, + } }); Roles = ghostBookshelf.Collection.extend({ diff --git a/core/server/models/session.js b/core/server/models/session.js index 81dfcee78f..94216ecdc7 100644 --- a/core/server/models/session.js +++ b/core/server/models/session.js @@ -19,7 +19,7 @@ Session = ghostBookshelf.Model.extend({ /*jshint unused:false*/ // Remove any properties which don't belong on the model this.attributes = this.pick(this.permittedAttributes()); - }, + } }, { destroyAll: function (options) { diff --git a/core/server/models/settings.js b/core/server/models/settings.js index 53319ea9f2..3fe34612d8 100644 --- a/core/server/models/settings.js +++ b/core/server/models/settings.js @@ -80,23 +80,23 @@ Settings = ghostBookshelf.Model.extend({ return options; }, - findOne: function (_key) { + findOne: function (options) { // Allow for just passing the key instead of attributes - if (!_.isObject(_key)) { - _key = { key: _key }; + if (!_.isObject(options)) { + options = { key: options }; } - return when(ghostBookshelf.Model.findOne.call(this, _key)); + return when(ghostBookshelf.Model.findOne.call(this, options)); }, - edit: function (_data, options) { + edit: function (data, options) { var self = this; options = this.filterOptions(options, 'edit'); - if (!Array.isArray(_data)) { - _data = [_data]; + if (!Array.isArray(data)) { + data = [data]; } - return when.map(_data, function (item) { + return when.map(data, function (item) { // Accept an array of models as input if (item.toJSON) { item = item.toJSON(); } if (!(_.isString(item.key) && item.key.length > 0)) { diff --git a/core/server/models/tag.js b/core/server/models/tag.js index 7ed3af746b..ad89d02932 100644 --- a/core/server/models/tag.js +++ b/core/server/models/tag.js @@ -57,7 +57,7 @@ Tag = ghostBookshelf.Model.extend({ } return options; - }, + } }); Tags = ghostBookshelf.Collection.extend({ diff --git a/core/server/models/user.js b/core/server/models/user.js index 6651aca802..af7fc3351c 100644 --- a/core/server/models/user.js +++ b/core/server/models/user.js @@ -106,16 +106,20 @@ User = ghostBookshelf.Model.extend({ }, /** + * ## Add * Naive user add - * @param {object} _user - * * Hashes the password provided before saving to the database. + * + * @param {object} data + * @param {object} options + * @extends ghostBookshelf.Model.add to manage all aspects of user signup + * **See:** [ghostBookshelf.Model.add](base.js.html#Add) */ - add: function (_user, options) { + add: function (data, options) { var self = this, // Clone the _user so we don't expose the hashed password unnecessarily - userData = this.filterData(_user); + userData = this.filterData(data); options = this.filterOptions(options, 'add'); @@ -133,7 +137,7 @@ User = ghostBookshelf.Model.extend({ } }).then(function () { // Generate a new password hash - return generatePasswordHash(_user.password); + return generatePasswordHash(data.password); }).then(function (hash) { // Assign the hashed password userData.password = hash; @@ -143,6 +147,7 @@ User = ghostBookshelf.Model.extend({ // Save the user with the hashed password return ghostBookshelf.Model.add.call(self, userData, options); }).then(function (addedUser) { + // Assign the userData to our created user so we can pass it back userData = addedUser; // Add this user to the admin role (assumes admin = role_id: 1) diff --git a/core/server/permissions/index.js b/core/server/permissions/index.js index 5d4dccf545..b8998b1b35 100644 --- a/core/server/permissions/index.js +++ b/core/server/permissions/index.js @@ -35,16 +35,11 @@ function parseContext(context) { parsed.internal = true; } - // @TODO: Refactor canThis() references to pass { user: id } explicitly instead of primitives. - if (context && context.id) { - // Handle passing of just user.id string - parsed.user = context.id; - } else if (_.isNumber(context)) { - // Handle passing of just user id number - parsed.user = context; - } else if (_.isObject(context)) { - // Otherwise, use the new hotness { user: id, app: id } format + if (context && context.user) { parsed.user = context.user; + } + + if (context && context.app) { parsed.app = context.app; } diff --git a/core/server/routes/admin.js b/core/server/routes/admin.js index ed4ec99d9d..b354f9a284 100644 --- a/core/server/routes/admin.js +++ b/core/server/routes/admin.js @@ -3,9 +3,11 @@ var admin = require('../controllers/admin'), middleware = require('../middleware').middleware, ONE_HOUR_S = 60 * 60, - ONE_YEAR_S = 365 * 24 * ONE_HOUR_S; + ONE_YEAR_S = 365 * 24 * ONE_HOUR_S, -module.exports = function (server) { + adminRoutes; + +adminRoutes = function (server) { // Have ember route look for hits first // to prevent conflicts with pre-existing routes server.get('/ghost/ember/*', admin.index); @@ -65,4 +67,6 @@ module.exports = function (server) { res.redirect(subdir + '/ghost/'); }); server.get('/ghost/', admin.indexold); -}; \ No newline at end of file +}; + +module.exports = adminRoutes; \ No newline at end of file diff --git a/core/server/routes/api.js b/core/server/routes/api.js index d82431e9c7..76e7686c52 100644 --- a/core/server/routes/api.js +++ b/core/server/routes/api.js @@ -1,37 +1,43 @@ +// # API routes var middleware = require('../middleware').middleware, - api = require('../api'); + api = require('../api'), + apiRoutes; -module.exports = function (server) { - // ### API routes - // #### Posts - server.get('/ghost/api/v0.1/posts', api.requestHandler(api.posts.browse)); - server.post('/ghost/api/v0.1/posts', api.requestHandler(api.posts.add)); - server.get('/ghost/api/v0.1/posts/:id', api.requestHandler(api.posts.read)); - server.put('/ghost/api/v0.1/posts/:id', api.requestHandler(api.posts.edit)); - server.del('/ghost/api/v0.1/posts/:id', api.requestHandler(api.posts.destroy)); - server.get('/ghost/api/v0.1/posts/slug/:title', middleware.authAPI, api.requestHandler(api.posts.generateSlug)); - // #### Settings - server.get('/ghost/api/v0.1/settings/', api.requestHandler(api.settings.browse)); - server.get('/ghost/api/v0.1/settings/:key/', api.requestHandler(api.settings.read)); - server.put('/ghost/api/v0.1/settings/', api.requestHandler(api.settings.edit)); - // #### Users - server.get('/ghost/api/v0.1/users/', api.requestHandler(api.users.browse)); - server.get('/ghost/api/v0.1/users/:id/', api.requestHandler(api.users.read)); - server.put('/ghost/api/v0.1/users/:id/', api.requestHandler(api.users.edit)); - // #### Tags - server.get('/ghost/api/v0.1/tags/', api.requestHandler(api.tags.browse)); - // #### Themes - server.get('/ghost/api/v0.1/themes/', api.requestHandler(api.themes.browse)); - server.put('/ghost/api/v0.1/themes/:name', api.requestHandler(api.themes.edit)); - // #### Notifications - server.del('/ghost/api/v0.1/notifications/:id', api.requestHandler(api.notifications.destroy)); - server.post('/ghost/api/v0.1/notifications/', api.requestHandler(api.notifications.add)); - server.get('/ghost/api/v0.1/notifications/', api.requestHandler(api.notifications.browse)); - // #### Import/Export - server.get('/ghost/api/v0.1/db/', api.requestHandler(api.db.exportContent)); - server.post('/ghost/api/v0.1/db/', middleware.busboy, api.requestHandler(api.db.importContent)); - server.del('/ghost/api/v0.1/db/', api.requestHandler(api.db.deleteAllContent)); - // #### Mail - server.post('/ghost/api/v0.1/mail', api.requestHandler(api.mail.send)); - server.post('/ghost/api/v0.1/mail/test', api.requestHandler(api.mail.sendTest)); -}; \ No newline at end of file +apiRoutes = function (server) { + // ## Posts + server.get('/ghost/api/v0.1/posts', api.http(api.posts.browse)); + server.post('/ghost/api/v0.1/posts', api.http(api.posts.add)); + server.get('/ghost/api/v0.1/posts/:id(\\d+)', api.http(api.posts.read)); + server.get('/ghost/api/v0.1/posts/:slug([a-z-]+)', api.http(api.posts.read)); + server.put('/ghost/api/v0.1/posts/:id', api.http(api.posts.edit)); + server.del('/ghost/api/v0.1/posts/:id', api.http(api.posts.destroy)); + server.get('/ghost/api/v0.1/posts/slug/:title', api.http(api.posts.generateSlug)); + // ## Settings + server.get('/ghost/api/v0.1/settings/', api.http(api.settings.browse)); + server.get('/ghost/api/v0.1/settings/:key/', api.http(api.settings.read)); + server.put('/ghost/api/v0.1/settings/', api.http(api.settings.edit)); + // ## Users + server.get('/ghost/api/v0.1/users/', api.http(api.users.browse)); + server.get('/ghost/api/v0.1/users/:id/', api.http(api.users.read)); + server.put('/ghost/api/v0.1/users/:id/', api.http(api.users.edit)); + // ## Tags + server.get('/ghost/api/v0.1/tags/', api.http(api.tags.browse)); + // ## Themes + server.get('/ghost/api/v0.1/themes/', api.http(api.themes.browse)); + server.put('/ghost/api/v0.1/themes/:name', api.http(api.themes.edit)); + // ## Notifications + server.del('/ghost/api/v0.1/notifications/:id', api.http(api.notifications.destroy)); + server.post('/ghost/api/v0.1/notifications/', api.http(api.notifications.add)); + server.get('/ghost/api/v0.1/notifications/', api.http(api.notifications.browse)); + server.post('/ghost/api/v0.1/notifications/', api.http(api.notifications.add)); + server.del('/ghost/api/v0.1/notifications/:id', api.http(api.notifications.destroy)); + // ## DB + server.get('/ghost/api/v0.1/db/', api.http(api.db.exportContent)); + server.post('/ghost/api/v0.1/db/', middleware.busboy, api.http(api.db.importContent)); + server.del('/ghost/api/v0.1/db/', api.http(api.db.deleteAllContent)); + // ## Mail + server.post('/ghost/api/v0.1/mail', api.http(api.mail.send)); + server.post('/ghost/api/v0.1/mail/test', api.http(api.mail.sendTest)); +}; + +module.exports = apiRoutes; \ No newline at end of file diff --git a/core/server/routes/frontend.js b/core/server/routes/frontend.js index 8eb09590d1..91b471abc3 100644 --- a/core/server/routes/frontend.js +++ b/core/server/routes/frontend.js @@ -2,9 +2,11 @@ var frontend = require('../controllers/frontend'), config = require('../config'), ONE_HOUR_S = 60 * 60, - ONE_YEAR_S = 365 * 24 * ONE_HOUR_S; + ONE_YEAR_S = 365 * 24 * ONE_HOUR_S, -module.exports = function (server) { + frontendRoutes; + +frontendRoutes = function (server) { var subdir = config().paths.subdir; // ### Frontend routes @@ -24,6 +26,6 @@ module.exports = function (server) { server.get('/page/:page/', frontend.homepage); server.get('/', frontend.homepage); server.get('*', frontend.single); +}; - -}; \ No newline at end of file +module.exports = frontendRoutes; \ No newline at end of file diff --git a/core/server/update-check.js b/core/server/update-check.js index d0f0cf4d1b..b1a3818d0c 100644 --- a/core/server/update-check.js +++ b/core/server/update-check.js @@ -50,9 +50,9 @@ function updateCheckData() { ops = [], mailConfig = config().mail; - ops.push(api.settings.read.call({ internal: true }, 'dbHash').otherwise(errors.rejectError)); - ops.push(api.settings.read.call({ internal: true }, 'activeTheme').otherwise(errors.rejectError)); - ops.push(api.settings.read.call({ internal: true }, 'activeApps') + ops.push(api.settings.read({context: {internal: true}, key: 'dbHash'}).otherwise(errors.rejectError)); + ops.push(api.settings.read({context: {internal: true}, key: 'activeTheme'}).otherwise(errors.rejectError)); + ops.push(api.settings.read({context: {internal: true}, key: 'activeApps'}) .then(function (response) { var apps = response.settings[0]; try { @@ -64,7 +64,7 @@ function updateCheckData() { return _.reduce(apps, function (memo, item) { return memo === '' ? memo + item : memo + ', ' + item; }, ''); }).otherwise(errors.rejectError)); ops.push(api.posts.browse().otherwise(errors.rejectError)); - ops.push(api.users.browse.call({user: 1}).otherwise(errors.rejectError)); + ops.push(api.users.browse({context: {user: 1}}).otherwise(errors.rejectError)); ops.push(nodefn.call(exec, 'npm -v').otherwise(errors.rejectError)); data.ghost_version = currentVersion; @@ -142,13 +142,21 @@ function updateCheckRequest() { // 1. Updates the time we can next make a check // 2. Checks if the version in the response is new, and updates the notification setting function updateCheckResponse(response) { - var ops = []; + var ops = [], + internalContext = {context: {internal: true}}; - ops.push(api.settings.edit.call({internal: true}, 'nextUpdateCheck', response.next_check) - .otherwise(errors.rejectError)); - - ops.push(api.settings.edit.call({internal: true}, 'displayUpdateNotification', response.version) - .otherwise(errors.rejectError)); + ops.push( + api.settings.edit( + {settings: [{key: 'nextUpdateCheck', value: response.next_check}]}, + internalContext + ) + .otherwise(errors.rejectError), + api.settings.edit( + {settings: [{key: 'displayUpdateNotification', value: response.version}]}, + internalContext + ) + .otherwise(errors.rejectError) + ); return when.settle(ops).then(function (descriptors) { descriptors.forEach(function (d) { @@ -171,7 +179,7 @@ function updateCheck() { // No update check deferred.resolve(); } else { - api.settings.read.call({ internal: true }, 'nextUpdateCheck').then(function (result) { + api.settings.read({context: {internal: true}, key: 'nextUpdateCheck'}).then(function (result) { var nextUpdateCheck = result.settings[0]; if (nextUpdateCheck && nextUpdateCheck.value && nextUpdateCheck.value > moment().unix()) { @@ -191,7 +199,7 @@ function updateCheck() { } function showUpdateNotification() { - return api.settings.read.call({ internal: true }, 'displayUpdateNotification').then(function (response) { + return api.settings.read({context: {internal: true}, key: 'displayUpdateNotification'}).then(function (response) { var display = response.settings[0]; // Version 0.4 used boolean to indicate the need for an update. This special case is diff --git a/core/test/blanket_coverage.js b/core/test/blanket_coverage.js index fac2487ffd..e00d312d0d 100644 --- a/core/test/blanket_coverage.js +++ b/core/test/blanket_coverage.js @@ -1,9 +1,11 @@ +// Posts var blanket = require("blanket")({ "pattern": ["/core/server/", "/core/clientold/", "/core/shared/"], "data-cover-only": ["/core/server/", "/core/clientold/", "/core/shared/"] }), requireDir = require("require-dir"); + requireDir("./unit"); requireDir("./integration"); requireDir("./functional/routes"); diff --git a/core/test/functional/routes/api/posts_test.js b/core/test/functional/routes/api/posts_test.js index b3d890bb36..cf0000d949 100644 --- a/core/test/functional/routes/api/posts_test.js +++ b/core/test/functional/routes/api/posts_test.js @@ -61,7 +61,7 @@ describe('Post API', function () { if (err) { return done(err); } - + csrfToken = res.text.match(pattern_meta)[1]; done(); }); @@ -124,7 +124,7 @@ describe('Post API', function () { testUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination'); done(); }); - + }); // Test bits of the API we don't use in the app yet to ensure the API behaves properly @@ -193,7 +193,7 @@ describe('Post API', function () { // ## Read describe('Read', function () { - it('can retrieve a post', function (done) { + it('can retrieve a post by id', function (done) { request.get(testUtils.API.getApiQuery('posts/1/')) .end(function (err, res) { if (err) { @@ -207,6 +207,32 @@ describe('Post API', function () { jsonResponse.should.exist; jsonResponse.posts.should.exist; testUtils.API.checkResponse(jsonResponse.posts[0], 'post'); + jsonResponse.posts[0].id.should.equal(1); + jsonResponse.posts[0].page.should.eql(0); + _.isBoolean(jsonResponse.posts[0].featured).should.eql(true); + _.isBoolean(jsonResponse.posts[0].page).should.eql(true); + jsonResponse.posts[0].author.should.be.a.Number; + jsonResponse.posts[0].created_by.should.be.a.Number; + jsonResponse.posts[0].tags[0].should.be.a.Number; + done(); + }); + }); + + it('can retrieve a post by slug', function (done) { + request.get(testUtils.API.getApiQuery('posts/welcome-to-ghost/')) + .end(function (err, res) { + if (err) { + return done(err); + } + + res.should.have.status(200); + should.not.exist(res.headers['x-cache-invalidate']); + res.should.be.json; + var jsonResponse = res.body; + jsonResponse.should.exist; + jsonResponse.posts.should.exist; + testUtils.API.checkResponse(jsonResponse.posts[0], 'post'); + jsonResponse.posts[0].slug.should.equal('welcome-to-ghost'); jsonResponse.posts[0].page.should.eql(0); _.isBoolean(jsonResponse.posts[0].featured).should.eql(true); _.isBoolean(jsonResponse.posts[0].page).should.eql(true); @@ -477,7 +503,6 @@ describe('Post API', function () { }); }); - it('can change a static page to a post', function (done) { request.get(testUtils.API.getApiQuery('posts/7/')) .end(function (err, res) { @@ -501,11 +526,11 @@ describe('Post API', function () { } var putBody = res.body; + _.has(res.headers, 'x-cache-invalidate').should.equal(false); res.should.be.json; putBody.should.exist; putBody.posts[0].page.should.eql(changedValue); - testUtils.API.checkResponse(putBody.posts[0], 'post'); done(); }); @@ -605,10 +630,6 @@ describe('Post API', function () { }); }); - }); - - // ## delete - describe('Delete', function () { it('can\'t edit non existent post', function (done) { request.get(testUtils.API.getApiQuery('posts/1/')) .end(function (err, res) { @@ -630,7 +651,6 @@ describe('Post API', function () { return done(err); } - var putBody = res.body; _.has(res.headers, 'x-cache-invalidate').should.equal(false); res.should.be.json; jsonResponse = res.body; @@ -641,6 +661,10 @@ describe('Post API', function () { }); }); + }); + + // ## delete + describe('Delete', function () { it('can delete a post', function (done) { var deletePostId = 1; request.del(testUtils.API.getApiQuery('posts/' + deletePostId + '/')) @@ -829,9 +853,5 @@ describe('Post API', function () { }); }); }); - - }); - - }); diff --git a/core/test/functional/routes/api/settings_test.js b/core/test/functional/routes/api/settings_test.js index 51708fd4cc..3988cacfab 100644 --- a/core/test/functional/routes/api/settings_test.js +++ b/core/test/functional/routes/api/settings_test.js @@ -214,7 +214,7 @@ describe('Settings API', function () { newValue = 'new value'; jsonResponse.should.exist; should.exist(jsonResponse.settings); - jsonResponse.settings.push({ key: 'testvalue', value: newValue }); + jsonResponse.settings = [{ key: 'testvalue', value: newValue }]; request.put(testUtils.API.getApiQuery('settings/')) .set('X-CSRF-Token', csrfToken) diff --git a/core/test/functional/routes/api/users_test.js b/core/test/functional/routes/api/users_test.js index 7a7be8ee99..49f59da95c 100644 --- a/core/test/functional/routes/api/users_test.js +++ b/core/test/functional/routes/api/users_test.js @@ -42,7 +42,7 @@ describe('User API', function () { pattern_meta.should.exist; csrfToken = res.text.match(pattern_meta)[1]; - process.nextTick(function() { + process.nextTick(function () { request.post('/ghost/signin/') .set('X-CSRF-Token', csrfToken) .send({email: user.email, password: user.password}) @@ -144,13 +144,16 @@ describe('User API', function () { } var jsonResponse = res.body, - changedValue = 'joe-bloggs.ghost.org'; + changedValue = 'joe-bloggs.ghost.org', + dataToSend; jsonResponse.users[0].should.exist; - jsonResponse.users[0].website = changedValue; + testUtils.API.checkResponse(jsonResponse.users[0], 'user'); + + dataToSend = { users: [{website: changedValue}]}; request.put(testUtils.API.getApiQuery('users/me/')) .set('X-CSRF-Token', csrfToken) - .send(jsonResponse) + .send(dataToSend) .expect(200) .end(function (err, res) { if (err) { @@ -162,7 +165,7 @@ describe('User API', function () { res.should.be.json; putBody.users[0].should.exist; putBody.users[0].website.should.eql(changedValue); - + putBody.users[0].email.should.eql(jsonResponse.users[0].email); testUtils.API.checkResponse(putBody.users[0], 'user'); done(); }); @@ -195,6 +198,4 @@ describe('User API', function () { }); }); - - }); \ No newline at end of file diff --git a/core/test/integration/api/api_db_spec.js b/core/test/integration/api/api_db_spec.js index 51d43b03f3..9ac5d2c628 100644 --- a/core/test/integration/api/api_db_spec.js +++ b/core/test/integration/api/api_db_spec.js @@ -37,7 +37,7 @@ describe('DB API', function () { it('delete all content', function (done) { permissions.init().then(function () { - return dbAPI.deleteAllContent.call({user: 1}); + return dbAPI.deleteAllContent({context: {user: 1}}); }).then(function (result) { should.exist(result.db); result.db.should.be.instanceof(Array); @@ -61,12 +61,12 @@ describe('DB API', function () { it('delete all content is denied', function (done) { permissions.init().then(function () { - return dbAPI.deleteAllContent.call({user: 2}); + return dbAPI.deleteAllContent({context: {user: 2}}); }).then(function (){ done(new Error("Delete all content is not denied for editor.")); }, function (error) { error.type.should.eql('NoPermissionError'); - return dbAPI.deleteAllContent.call({user: 3}); + return dbAPI.deleteAllContent({context: {user: 3}}); }).then(function (){ done(new Error("Delete all content is not denied for author.")); }, function (error) { @@ -82,12 +82,12 @@ describe('DB API', function () { it('export content is denied', function (done) { permissions.init().then(function () { - return dbAPI.exportContent.call({user: 2}); + return dbAPI.exportContent({context: {user: 2}}); }).then(function (){ done(new Error("Export content is not denied for editor.")); }, function (error) { error.type.should.eql('NoPermissionError'); - return dbAPI.exportContent.call({user: 3}); + return dbAPI.exportContent({context: {user: 3}}); }).then(function (){ done(new Error("Export content is not denied for author.")); }, function (error) { @@ -103,12 +103,12 @@ describe('DB API', function () { it('import content is denied', function (done) { permissions.init().then(function () { - return dbAPI.importContent.call({user: 2}); + return dbAPI.importContent({context: {user: 2}}); }).then(function (result){ done(new Error("Import content is not denied for editor.")); }, function (error) { error.type.should.eql('NoPermissionError'); - return dbAPI.importContent.call({user: 3}); + return dbAPI.importContent({context: {user: 3}}); }).then(function (result){ done(new Error("Import content is not denied for author.")); }, function (error) { diff --git a/core/test/integration/api/api_posts_spec.js b/core/test/integration/api/api_posts_spec.js index 290d4d2774..3203520021 100644 --- a/core/test/integration/api/api_posts_spec.js +++ b/core/test/integration/api/api_posts_spec.js @@ -1,4 +1,4 @@ -/*globals describe, before, beforeEach, afterEach, it */ + /*globals describe, before, beforeEach, afterEach, it */ var testUtils = require('../../utils'), should = require('should'), diff --git a/core/test/integration/api/api_settings_spec.js b/core/test/integration/api/api_settings_spec.js index 402e30564d..931181a17a 100644 --- a/core/test/integration/api/api_settings_spec.js +++ b/core/test/integration/api/api_settings_spec.js @@ -17,7 +17,14 @@ describe('Settings API', function () { internal: true }, callApiWithContext = function (context, method) { - return SettingsAPI[method].apply(context, _.toArray(arguments).slice(2)); + var args = _.toArray(arguments), + options = args[args.length - 1]; + + if (_.isObject(options)) { + options.context = _.clone(context); + } + + return SettingsAPI[method].apply({}, args.slice(2)); }, getErrorDetails = function (done) { return function (err) { @@ -58,7 +65,7 @@ describe('Settings API', function () { }); it('can browse', function (done) { - return callApiWithContext(defaultContext, 'browse', 'blog').then(function (results) { + return callApiWithContext(defaultContext, 'browse', {}).then(function (results) { should.exist(results); testUtils.API.checkResponse(results, 'settings'); results.settings.length.should.be.above(0); @@ -71,8 +78,23 @@ describe('Settings API', function () { }).catch(getErrorDetails(done)); }); - it('returns core settings for internal requests when browsing', function (done){ - return callApiWithContext(internalContext, 'browse', 'blog').then(function (results) { + + it('can browse by type', function (done) { + return callApiWithContext(defaultContext, 'browse', {type: 'blog'}).then(function (results) { + should.exist(results); + testUtils.API.checkResponse(results, 'settings'); + results.settings.length.should.be.above(0); + testUtils.API.checkResponse(results.settings[0], 'setting'); + + // Check for a core setting + should.not.exist(_.find(results.settings, function (setting) { return setting.type === 'core'; })); + + done(); + }).catch(getErrorDetails(done)); + }); + + it('returns core settings for internal requests when browsing', function (done) { + return callApiWithContext(internalContext, 'browse', {}).then(function (results) { should.exist(results); testUtils.API.checkResponse(results, 'settings'); results.settings.length.should.be.above(0); @@ -82,11 +104,11 @@ describe('Settings API', function () { should.exist(_.find(results.settings, function (setting) { return setting.type === 'core'; })); done(); - }).catch(getErrorDetails(done)); + }).catch(getErrorDetails(done)); }); - it('can read by string', function (done) { - return callApiWithContext(defaultContext, 'read', 'title').then(function (response) { + it('can read blog settings by string', function (done) { + return SettingsAPI.read('title').then(function (response) { should.exist(response); testUtils.API.checkResponse(response, 'settings'); response.settings.length.should.equal(1); @@ -97,19 +119,17 @@ describe('Settings API', function () { }); it('cannot read core settings if not an internal request', function (done) { - return callApiWithContext(defaultContext, 'read', 'databaseVersion').then(function (response) { + return callApiWithContext(defaultContext, 'read', {key: 'databaseVersion'}).then(function (response) { done(new Error('Allowed to read databaseVersion with external request')); - }).catch(function (err) { - should.exist(err); - - err.message.should.equal('Attempted to access core setting on external request'); - + }).catch(function (error) { + should.exist(error); + error.type.should.eql('NoPermissionError'); done(); }); }); it('can read core settings if an internal request', function (done) { - return callApiWithContext(internalContext, 'read', 'databaseVersion').then(function (response) { + return callApiWithContext(internalContext, 'read', {key: 'databaseVersion'}).then(function (response) { should.exist(response); testUtils.API.checkResponse(response, 'settings'); response.settings.length.should.equal(1); @@ -131,37 +151,40 @@ describe('Settings API', function () { }); it('can edit', function (done) { - return callApiWithContext(defaultContext, 'edit', { settings: [{ key: 'title', value: 'UpdatedGhost'}]}).then(function (response) { - should.exist(response); - testUtils.API.checkResponse(response, 'settings'); - response.settings.length.should.equal(1); - testUtils.API.checkResponse(response.settings[0], 'setting'); + return callApiWithContext(defaultContext, 'edit', {settings: [{ key: 'title', value: 'UpdatedGhost'}]}, {}) + .then(function (response) { + should.exist(response); + testUtils.API.checkResponse(response, 'settings'); + response.settings.length.should.equal(1); + testUtils.API.checkResponse(response.settings[0], 'setting'); - done(); - }).catch(done); - }); - - it('can edit, by key/value', function (done) { - return callApiWithContext(defaultContext, 'edit', 'title', 'UpdatedGhost').then(function (response) { - should.exist(response); - testUtils.API.checkResponse(response, 'settings'); - response.settings.length.should.equal(1); - testUtils.API.checkResponse(response.settings[0], 'setting'); - - done(); - }).catch(getErrorDetails(done)); + done(); + }).catch(done); }); it('cannot edit a core setting if not an internal request', function (done) { - return callApiWithContext(defaultContext, 'edit', 'databaseVersion', '999').then(function (response) { - done(new Error('Allowed to edit a core setting as external request')); - }).catch(function (err) { - should.exist(err); + return callApiWithContext(defaultContext, 'edit', {settings: [{ key: 'databaseVersion', value: '999'}]}, {}) + .then(function () { + done(new Error('Allowed to edit a core setting as external request')); + }).catch(function (err) { + should.exist(err); - err.message.should.equal('Attempted to access core setting on external request'); + err.type.should.eql('NoPermissionError'); - done(); - }); + done(); + }); + }); + + it('can edit a core setting with an internal request', function (done) { + return callApiWithContext(internalContext, 'edit', {settings: [{ key: 'databaseVersion', value: '999'}]}, {}) + .then(function (response) { + should.exist(response); + testUtils.API.checkResponse(response, 'settings'); + response.settings.length.should.equal(1); + testUtils.API.checkResponse(response.settings[0], 'setting'); + + done(); + }).catch(done); }); it('ensures values are stringified before saving to database', function (done) { diff --git a/core/test/integration/api/api_themes_spec.js b/core/test/integration/api/api_themes_spec.js index 4e925845c6..2ba55929bc 100644 --- a/core/test/integration/api/api_themes_spec.js +++ b/core/test/integration/api/api_themes_spec.js @@ -8,7 +8,7 @@ var _ = require('lodash'), // Stuff we are testing permissions = require('../../../server/permissions'), - settings = require('../../../server/api/settings'), + SettingsAPI = require('../../../server/api/settings'), ThemeAPI = rewire('../../../server/api/themes'); describe('Themes API', function () { @@ -26,12 +26,15 @@ describe('Themes API', function () { testUtils.initData().then(function () { return testUtils.insertDefaultFixtures(); }).then(function () { + return SettingsAPI.updateSettingsCache(); + }).then(function () { + return permissions.init(); }).then(function () { sandbox = sinon.sandbox.create(); // Override settings.read for activeTheme - settingsReadStub = sandbox.stub(settings, 'read', function () { + settingsReadStub = sandbox.stub(SettingsAPI, 'read', function () { return when({ settings: [{value: 'casper'}] }); }); @@ -67,14 +70,14 @@ describe('Themes API', function () { _.extend(configStub, config); ThemeAPI.__set__('config', configStub); - ThemeAPI.browse.call({user: 1}).then(function (result) { + ThemeAPI.browse({context: {user: 1}}).then(function (result) { should.exist(result); result.themes.length.should.be.above(0); testUtils.API.checkResponse(result.themes[0], 'theme'); done(); - }, function (error) { + }).catch(function (error) { done(new Error(JSON.stringify(error))); - }) + }); }); it('can edit', function (done) { @@ -84,15 +87,15 @@ describe('Themes API', function () { _.extend(configStub, config); ThemeAPI.__set__('config', configStub); - ThemeAPI.edit.call({user: 1}, {themes: [{uuid: 'rasper', active: true }]}).then(function (result) { + ThemeAPI.edit({themes: [{uuid: 'rasper', active: true }]}, {context: {user: 1}}).then(function (result) { should.exist(result); should.exist(result.themes); result.themes.length.should.be.above(0); testUtils.API.checkResponse(result.themes[0], 'theme'); result.themes[0].uuid.should.equal('rasper'); done(); - }, function (error) { + }).catch(function (error) { done(new Error(JSON.stringify(error))); - }) + }); }) }); \ No newline at end of file diff --git a/core/test/integration/api/api_users_spec.js b/core/test/integration/api/api_users_spec.js index 0ff3f5bb1d..87bfbf0975 100644 --- a/core/test/integration/api/api_users_spec.js +++ b/core/test/integration/api/api_users_spec.js @@ -2,6 +2,8 @@ var testUtils = require('../../utils'), should = require('should'), + permissions = require('../../../server/permissions'), + // Stuff we are testing UsersAPI = require('../../../server/api/users'); @@ -22,11 +24,12 @@ describe('Users API', function () { describe('No User', function () { beforeEach(function (done) { testUtils.initData().then(function () { + return permissions.init(); + }).then(function () { done(); }).catch(done); }); - it('can add with internal user', function (done) { UsersAPI.register({ users: [{ 'name': 'Hello World', @@ -52,13 +55,15 @@ describe('Users API', function () { return testUtils.insertEditorUser(); }).then(function () { return testUtils.insertAuthorUser(); + }).then(function () { + return permissions.init(); }).then(function () { done(); }).catch(done); }); it('admin can browse', function (done) { - UsersAPI.browse.call({user: 1}).then(function (results) { + UsersAPI.browse({context: {user: 1}}).then(function (results) { should.exist(results); testUtils.API.checkResponse(results, 'users'); should.exist(results.users); @@ -71,7 +76,7 @@ describe('Users API', function () { }); it('editor can browse', function (done) { - UsersAPI.browse.call({user: 2}).then(function (results) { + UsersAPI.browse({context: {user: 2}}).then(function (results) { should.exist(results); testUtils.API.checkResponse(results, 'users'); should.exist(results.users); @@ -84,7 +89,7 @@ describe('Users API', function () { }); it('author can browse', function (done) { - UsersAPI.browse.call({user: 3}).then(function (results) { + UsersAPI.browse({context: {user: 3}}).then(function (results) { should.exist(results); testUtils.API.checkResponse(results, 'users'); should.exist(results.users); @@ -105,7 +110,7 @@ describe('Users API', function () { }); it('admin can read', function (done) { - UsersAPI.read.call({user: 1}, {id: 1}).then(function (results) { + UsersAPI.read({id: 1, context: {user: 1}}).then(function (results) { should.exist(results); testUtils.API.checkResponse(results, 'users'); results.users[0].id.should.eql(1); @@ -115,7 +120,7 @@ describe('Users API', function () { }); it('editor can read', function (done) { - UsersAPI.read.call({user: 2}, {id: 1}).then(function (results) { + UsersAPI.read({id: 1, context: {user: 2}}).then(function (results) { should.exist(results); testUtils.API.checkResponse(results, 'users'); results.users[0].id.should.eql(1); @@ -125,7 +130,7 @@ describe('Users API', function () { }); it('author can read', function (done) { - UsersAPI.read.call({user: 3}, {id: 1}).then(function (results) { + UsersAPI.read({id: 1, context: {user: 3}}).then(function (results) { should.exist(results); testUtils.API.checkResponse(results, 'users'); results.users[0].id.should.eql(1); @@ -145,7 +150,7 @@ describe('Users API', function () { }); it('admin can edit', function (done) { - UsersAPI.edit.call({user: 1}, {users: [{id: 1, name: 'Joe Blogger'}]}).then(function (response) { + UsersAPI.edit({users: [{name: 'Joe Blogger'}]}, {id: 1, context: {user: 1}}).then(function (response) { should.exist(response); testUtils.API.checkResponse(response, 'users'); response.users.should.have.length(1); @@ -157,7 +162,7 @@ describe('Users API', function () { }); it('editor can edit', function (done) { - UsersAPI.edit.call({user: 2}, {users: [{id: 1, name: 'Joe Blogger'}]}).then(function (response) { + UsersAPI.edit({users: [{name: 'Joe Blogger'}]}, {id: 1, context: {user: 2}}).then(function (response) { should.exist(response); testUtils.API.checkResponse(response, 'users'); response.users.should.have.length(1); @@ -170,13 +175,13 @@ describe('Users API', function () { it('author can edit only self', function (done) { // Test author cannot edit admin user - UsersAPI.edit.call({user: 3}, {users: [{id: 1, name: 'Joe Blogger'}]}).then(function () { + UsersAPI.edit({users: [{name: 'Joe Blogger'}]}, {id: 1, context: {user: 3}}).then(function () { done(new Error('Author should not be able to edit account which is not their own')); }).catch(function (error) { - error.code.should.eql(403); + error.type.should.eql('NoPermissionError'); }).finally(function () { // Next test that author CAN edit self - return UsersAPI.edit.call({user: 3}, {users: [{id: 3, name: 'Timothy Bogendath'}]}) + return UsersAPI.edit({users: [{name: 'Timothy Bogendath'}]}, {id: 3, context: {user: 3}}) .then(function (response) { should.exist(response); testUtils.API.checkResponse(response, 'users'); diff --git a/core/test/integration/model/model_apps_spec.js b/core/test/integration/model/model_apps_spec.js index 38a990d416..a5e9d78ffa 100644 --- a/core/test/integration/model/model_apps_spec.js +++ b/core/test/integration/model/model_apps_spec.js @@ -86,19 +86,20 @@ describe('App Model', function () { }).catch(done); }); - it("can delete", function (done) { - AppModel.findOne({id: 1}).then(function (foundApp) { + it("can destroy", function (done) { + var firstApp = {id: 1}; + + AppModel.findOne(firstApp).then(function (foundApp) { should.exist(foundApp); + foundApp.attributes.id.should.equal(firstApp.id); - return AppModel.destroy(1); - }).then(function () { - return AppModel.findAll(); - }).then(function (foundApp) { - var hasRemovedId = foundApp.any(function (foundApp) { - return foundApp.id === 1; - }); + return AppModel.destroy(firstApp); + }).then(function (response) { + response.toJSON().should.be.empty; - hasRemovedId.should.equal(false); + return AppModel.findOne(firstApp); + }).then(function (newResults) { + should.equal(newResults, null); done(); }).catch(done); diff --git a/core/test/integration/model/model_permissions_spec.js b/core/test/integration/model/model_permissions_spec.js index 370a3e571d..192b9e4038 100644 --- a/core/test/integration/model/model_permissions_spec.js +++ b/core/test/integration/model/model_permissions_spec.js @@ -80,19 +80,19 @@ describe('Permission Model', function () { }).catch(done); }); - it('can delete', function (done) { - PermissionModel.findOne({id: 1}).then(function (foundPermission) { + it('can destroy', function (done) { + var firstPermission = {id: 1}; + + PermissionModel.findOne(firstPermission).then(function (foundPermission) { should.exist(foundPermission); + foundPermission.attributes.id.should.equal(firstPermission.id); - return PermissionModel.destroy(1); - }).then(function () { - return PermissionModel.findAll(); - }).then(function (foundPermissions) { - var hasRemovedId = foundPermissions.any(function (permission) { - return permission.id === 1; - }); - - hasRemovedId.should.equal(false); + return PermissionModel.destroy(firstPermission); + }).then(function (response) { + response.toJSON().should.be.empty; + return PermissionModel.findOne(firstPermission); + }).then(function (newResults) { + should.equal(newResults, null); done(); }).catch(done); diff --git a/core/test/integration/model/model_posts_spec.js b/core/test/integration/model/model_posts_spec.js index 78d44e5310..800606269d 100644 --- a/core/test/integration/model/model_posts_spec.js +++ b/core/test/integration/model/model_posts_spec.js @@ -35,19 +35,80 @@ describe('Post Model', function () { }).catch(done); }); + function checkFirstPostData(firstPost) { + should.not.exist(firstPost.author_id); + firstPost.author.should.be.an.Object; + firstPost.fields.should.be.an.Array; + firstPost.tags.should.be.an.Array; + firstPost.author.name.should.equal(DataGenerator.Content.users[0].name); + firstPost.fields[0].key.should.equal(DataGenerator.Content.app_fields[0].key); + firstPost.created_by.should.be.an.Object; + firstPost.updated_by.should.be.an.Object; + firstPost.published_by.should.be.an.Object; + firstPost.created_by.name.should.equal(DataGenerator.Content.users[0].name); + firstPost.updated_by.name.should.equal(DataGenerator.Content.users[0].name); + firstPost.published_by.name.should.equal(DataGenerator.Content.users[0].name); + firstPost.tags[0].name.should.equal('Getting Started'); + } + it('can findAll', function (done) { PostModel.findAll().then(function (results) { should.exist(results); results.length.should.be.above(1); - // should be in published_at, DESC order - // model and API differ here - need to fix - //results.models[0].attributes.published_at.should.be.above(results.models[1].attributes.published_at); + done(); + }).catch(done); + }); + + it('can findAll, returning all related data', function (done) { + var firstPost; + + PostModel.findAll({include: ['author_id', 'fields', 'tags', 'created_by', 'updated_by', 'published_by']}) + .then(function (results) { + should.exist(results); + results.length.should.be.above(0); + firstPost = results.models[0].toJSON(); + + checkFirstPostData(firstPost); + + done(); + }).catch(done); + }); + + it('can findPage (default)', function (done) { + PostModel.findPage().then(function (results) { + should.exist(results); + + results.meta.pagination.page.should.equal(1); + results.meta.pagination.limit.should.equal(15); + results.meta.pagination.pages.should.equal(1); + results.posts.length.should.equal(5); done(); }).catch(done); }); + it('can findPage, returning all related data', function (done) { + var firstPost; + + PostModel.findPage({include: ['author_id', 'fields', 'tags', 'created_by', 'updated_by', 'published_by']}) + .then(function (results) { + should.exist(results); + + results.meta.pagination.page.should.equal(1); + results.meta.pagination.limit.should.equal(15); + results.meta.pagination.pages.should.equal(1); + results.posts.length.should.equal(5); + + firstPost = results.posts[0]; + + checkFirstPostData(firstPost); + + done(); + }).catch(done); + }); + + it('can findOne', function (done) { var firstPost; @@ -66,50 +127,31 @@ describe('Post Model', function () { }).catch(done); }); - it('can findAll, returning author and field data', function (done) { - var firstPost; - - PostModel.findAll({include: ['author_id', 'fields']}).then(function (results) { - should.exist(results); - results.length.should.be.above(0); - firstPost = results.models[0].toJSON(); - - should.not.exist(firstPost.author_id); - firstPost.author.should.be.an.Object; - firstPost.fields.should.be.an.Array; - firstPost.author.name.should.equal(DataGenerator.Content.users[0].name); - firstPost.fields[0].key.should.equal(DataGenerator.Content.app_fields[0].key); - - done(); - }).catch(done); - }); - - it('can findOne, returning author and field data', function (done) { + it('can findOne, returning all related data', function (done) { var firstPost; // TODO: should take author :-/ - PostModel.findOne({}, {include: ['author_id', 'fields']}).then(function (result) { - should.exist(result); - firstPost = result.toJSON(); + PostModel.findOne({}, {include: ['author_id', 'fields', 'tags', 'created_by', 'updated_by', 'published_by']}) + .then(function (result) { + should.exist(result); + firstPost = result.toJSON(); - should.not.exist(firstPost.author_id); - firstPost.author.should.be.an.Object; - firstPost.fields.should.be.an.Array; - firstPost.author.name.should.equal(testUtils.DataGenerator.Content.users[0].name); - firstPost.fields[0].key.should.equal(DataGenerator.Content.app_fields[0].key); + checkFirstPostData(firstPost); - done(); - }).catch(done); + done(); + }).catch(done); }); it('can edit', function (done) { - var firstPost; + var firstPost = 1; - PostModel.findAll().then(function (results) { + PostModel.findOne({id: firstPost}).then(function (results) { + var post; should.exist(results); - results.length.should.be.above(0); - firstPost = results.models[0]; + post = results.toJSON(); + post.id.should.equal(firstPost); + post.title.should.not.equal('new title'); - return PostModel.edit({id: firstPost.id, title: 'new title'}); + return PostModel.edit({title: 'new title'}, {id: firstPost}); }).then(function (edited) { should.exist(edited); edited.attributes.title.should.equal('new title'); @@ -118,6 +160,7 @@ describe('Post Model', function () { }).catch(done); }); + it('can add, defaults are all correct', function (done) { var createdPostUpdatedDate, newPost = testUtils.DataGenerator.forModel.posts[2], @@ -330,18 +373,32 @@ describe('Post Model', function () { }).catch(done); }); - it('can delete', function (done) { - var firstPostId; - PostModel.findAll().then(function (results) { - should.exist(results); - results.length.should.be.above(0); - firstPostId = results.models[0].id; + it('can destroy', function (done) { + // We're going to try deleting post id 1 which also has tag id 1 + var firstItemData = {id: 1}; - return PostModel.destroy(firstPostId); - }).then(function () { - return PostModel.findOne({id: firstPostId}); + // Test that we have the post we expect, with exactly one tag + PostModel.findOne(firstItemData).then(function (results) { + var post; + should.exist(results); + post = results.toJSON(); + post.id.should.equal(firstItemData.id); + post.tags.should.have.length(1); + post.tags[0].should.equal(firstItemData.id); + + // Destroy the post + return PostModel.destroy(firstItemData); + }).then(function (response) { + var deleted = response.toJSON(); + + deleted.tags.should.be.empty; + should.equal(deleted.author, undefined); + + // Double check we can't find the post again + return PostModel.findOne(firstItemData); }).then(function (newResults) { should.equal(newResults, null); + done(); }).catch(done); }); @@ -372,6 +429,7 @@ describe('Post Model', function () { paginationResult.meta.pagination.pages.should.equal(2); paginationResult.posts.length.should.equal(30); + // Test both boolean formats return PostModel.findPage({limit: 10, staticPages: true}); }).then(function (paginationResult) { paginationResult.meta.pagination.page.should.equal(1); @@ -379,10 +437,26 @@ describe('Post Model', function () { paginationResult.meta.pagination.pages.should.equal(1); paginationResult.posts.length.should.equal(1); + // Test both boolean formats + return PostModel.findPage({limit: 10, staticPages: '1'}); + }).then(function (paginationResult) { + paginationResult.meta.pagination.page.should.equal(1); + paginationResult.meta.pagination.limit.should.equal(10); + paginationResult.meta.pagination.pages.should.equal(1); + paginationResult.posts.length.should.equal(1); + return PostModel.findPage({limit: 10, page: 2, status: 'all'}); }).then(function (paginationResult) { paginationResult.meta.pagination.pages.should.equal(11); + done(); + }).catch(done); + }); + it('can findPage for tag, with various options', function (done) { + testUtils.insertMorePosts().then(function () { + + return testUtils.insertMorePostsTags(); + }).then(function () { // Test tag filter return PostModel.findPage({page: 1, tag: 'bacon'}); }).then(function (paginationResult) { diff --git a/core/test/integration/model/model_roles_spec.js b/core/test/integration/model/model_roles_spec.js index e26191fd89..d39ffe7036 100644 --- a/core/test/integration/model/model_roles_spec.js +++ b/core/test/integration/model/model_roles_spec.js @@ -79,19 +79,19 @@ describe('Role Model', function () { }).catch(done); }); - it('can delete', function (done) { - RoleModel.findOne({id: 1}).then(function (foundRole) { + it('can destroy', function (done) { + var firstRole = {id: 1}; + + RoleModel.findOne(firstRole).then(function (foundRole) { should.exist(foundRole); + foundRole.attributes.id.should.equal(firstRole.id); - return RoleModel.destroy(1); - }).then(function (destResp) { - return RoleModel.findAll(); - }).then(function (foundRoles) { - var hasRemovedId = foundRoles.any(function (role) { - return role.id === 1; - }); - - hasRemovedId.should.equal(false); + return RoleModel.destroy(firstRole); + }).then(function (response) { + response.toJSON().should.be.empty; + return RoleModel.findOne(firstRole); + }).then(function (newResults) { + should.equal(newResults, null); done(); }).catch(done); diff --git a/core/test/integration/model/model_settings_spec.js b/core/test/integration/model/model_settings_spec.js index eac7361963..93be546d91 100644 --- a/core/test/integration/model/model_settings_spec.js +++ b/core/test/integration/model/model_settings_spec.js @@ -153,39 +153,23 @@ describe('Settings Model', function () { }).catch(done); }); - it('can delete', function (done) { - var settingId; - - SettingsModel.findAll().then(function (results) { + it('can destroy', function (done) { + // dont't use id 1, since it will delete databaseversion + var settingToDestroy = {id: 2}; + SettingsModel.findOne(settingToDestroy).then(function (results) { should.exist(results); + results.attributes.id.should.equal(settingToDestroy.id); - results.length.should.be.above(0); - - // dont't use results.models[0], since it will delete databaseversion - // which is used for testUtils.reset() - settingId = results.models[1].id; - - return SettingsModel.destroy(settingId); - - }).then(function () { - - return SettingsModel.findAll(); + return SettingsModel.destroy(settingToDestroy); + }).then(function (response) { + response.toJSON().should.be.empty; + return SettingsModel.findOne(settingToDestroy); }).then(function (newResults) { - - var ids, hasDeletedId; - - ids = _.pluck(newResults.models, 'id'); - - hasDeletedId = _.any(ids, function (id) { - return id === settingId; - }); - - hasDeletedId.should.equal(false); + should.equal(newResults, null); done(); - }).catch(done); }); }); diff --git a/core/test/integration/model/model_users_spec.js b/core/test/integration/model/model_users_spec.js index fbca93a2f2..e9bb65aecc 100644 --- a/core/test/integration/model/model_users_spec.js +++ b/core/test/integration/model/model_users_spec.js @@ -144,7 +144,7 @@ describe('User Model', function run() { it('sets last login time on successful login', function (done) { var userData = testUtils.DataGenerator.forModel.users[0]; - UserModel.check({email: userData.email, pw:userData.password}).then(function (activeUser) { + UserModel.check({email: userData.email, pw: userData.password}).then(function (activeUser) { should.exist(activeUser.get('last_login')); done(); }).catch(done); @@ -175,19 +175,13 @@ describe('User Model', function run() { var firstUser; UserModel.findAll().then(function (results) { - should.exist(results); - results.length.should.be.above(0); - firstUser = results.models[0]; return UserModel.findOne({email: firstUser.attributes.email}); - }).then(function (found) { - should.exist(found); - found.attributes.name.should.equal(firstUser.attributes.name); done(); @@ -197,22 +191,18 @@ describe('User Model', function run() { }); it('can edit', function (done) { - var firstUser; - - UserModel.findAll().then(function (results) { + var firstUser = 1; + UserModel.findOne({id: firstUser}).then(function (results) { + var user; should.exist(results); + user = results.toJSON(); + user.id.should.equal(firstUser); + should.equal(user.website, null); - results.length.should.be.above(0); - - firstUser = results.models[0]; - - return UserModel.edit({id: firstUser.id, website: "some.newurl.com"}); - + return UserModel.edit({website: 'some.newurl.com'}, {id: firstUser}); }).then(function (edited) { - should.exist(edited); - edited.attributes.website.should.equal('some.newurl.com'); done(); @@ -220,41 +210,43 @@ describe('User Model', function run() { }).catch(done); }); - it('can delete', function (done) { - var firstUserId; + it('can destroy', function (done) { + var firstUser = {id: 1}; - UserModel.findAll().then(function (results) { + // Test that we have the user we expect + UserModel.findOne(firstUser).then(function (results) { + var user; should.exist(results); + user = results.toJSON(); + user.id.should.equal(firstUser.id); - results.length.should.be.above(0); - - firstUserId = results.models[0].id; - - return UserModel.destroy(firstUserId); - - }).then(function () { - - return UserModel.findAll(); + // Destroy the user + return UserModel.destroy(firstUser); + }).then(function (response) { + response.toJSON().should.be.empty; + // Double check we can't find the user again + return UserModel.findOne(firstUser); }).then(function (newResults) { - var ids, hasDeletedId; + should.equal(newResults, null); - if (newResults.length < 1) { - // Bug out if we only had one user and deleted it. - return done(); - } - - ids = _.pluck(newResults.models, "id"); - hasDeletedId = _.any(ids, function (id) { - return id === firstUserId; - }); - - hasDeletedId.should.equal(false); done(); - }).catch(done); }); + }); + + describe('Password Reset', function () { + + beforeEach(function (done) { + testUtils.initData() + .then(function () { + return when(testUtils.insertDefaultUser()); + }) + .then(function () { + done(); + }).catch(done); + }); it('can generate reset token', function (done) { // Expires in one minute diff --git a/core/test/unit/frontend_spec.js b/core/test/unit/frontend_spec.js index 48a840c2ae..118632a13f 100644 --- a/core/test/unit/frontend_spec.js +++ b/core/test/unit/frontend_spec.js @@ -44,7 +44,7 @@ describe('Frontend Controller', function () { }); apiSettingsStub = sandbox.stub(api.settings, 'read'); - apiSettingsStub.withArgs('postsPerPage').returns(when({ + apiSettingsStub.withArgs('postsPerPage').returns(when({ settings: [{ 'key': 'postsPerPage', 'value': 6 @@ -181,7 +181,7 @@ describe('Frontend Controller', function () { done(new Error(msg)); }; }; - + beforeEach(function () { sandbox.stub(api.posts, 'browse', function (args) { return when({ @@ -197,23 +197,23 @@ describe('Frontend Controller', function () { } }); }); - + apiSettingsStub = sandbox.stub(api.settings, 'read'); - - apiSettingsStub.withArgs('activeTheme').returns(when({ + + apiSettingsStub.withArgs(sinon.match.has('key', 'activeTheme')).returns(when({ settings: [{ 'key': 'activeTheme', 'value': 'casper' }] })); - + apiSettingsStub.withArgs('postsPerPage').returns(when({ settings: [{ 'key': 'postsPerPage', 'value': '10' }] })); - + frontend.__set__('config', sandbox.stub().returns({ 'paths': { 'subdir': '', @@ -229,15 +229,18 @@ describe('Frontend Controller', function () { } })); }); - + describe('custom tag template', function () { - + beforeEach(function () { apiSettingsStub.withArgs('permalinks').returns(when({ - value: '/tag/:slug/' + settings: [{ + key: 'permalinks', + value: '/tag/:slug/' + }] })); }); - + it('it will render custom tag template if it exists', function (done) { var req = { path: '/tag/' + mockTags[0].slug, @@ -250,7 +253,7 @@ describe('Frontend Controller', function () { done(); } }; - + frontend.tag(req, res, failTest(done)); }); }); @@ -422,7 +425,7 @@ describe('Frontend Controller', function () { apiSettingsStub = sandbox.stub(api.settings, 'read'); - apiSettingsStub.withArgs('activeTheme').returns(when({ + apiSettingsStub.withArgs(sinon.match.has('key', 'activeTheme')).returns(when({ settings: [{ 'key': 'activeTheme', 'value': 'casper' @@ -451,7 +454,7 @@ describe('Frontend Controller', function () { describe('custom page templates', function () { beforeEach(function () { apiSettingsStub.withArgs('permalinks').returns(when({ - settings: [{ + settings: [{ value: '/:slug/' }] })); @@ -547,8 +550,8 @@ describe('Frontend Controller', function () { beforeEach(function () { apiSettingsStub.withArgs('permalinks').returns(when({ settings: [{ - value: '/:year/:month/:day/:slug/' - }] + value: '/:year/:month/:day/:slug/' + }] })); }); @@ -621,7 +624,7 @@ describe('Frontend Controller', function () { apiSettingsStub.withArgs('permalinks').returns(when({ settings: [{ value: '/:slug' - }] + }] })); }); @@ -694,7 +697,7 @@ describe('Frontend Controller', function () { apiSettingsStub.withArgs('permalinks').returns(when({ settings: [{ value: '/:year/:month/:day/:slug' - }] + }] })); }); @@ -784,7 +787,7 @@ describe('Frontend Controller', function () { apiSettingsStub.withArgs('permalinks').returns(when({ settings: [{ value: '/:year/:slug' - }] + }] })); }); diff --git a/core/test/unit/permissions_spec.js b/core/test/unit/permissions_spec.js index 413c9959dc..11db5ae4f6 100644 --- a/core/test/unit/permissions_spec.js +++ b/core/test/unit/permissions_spec.js @@ -24,16 +24,6 @@ describe('Permissions', function () { }).catch(done); }); - beforeEach(function (done) { - sandbox = sinon.sandbox.create(); - testUtils.initData() - .then(testUtils.insertDefaultUser) - .then(testUtils.insertDefaultApp) - .then(function () { - done(); - }).catch(done); - }); - afterEach(function (done) { sandbox.restore(); testUtils.clearData() @@ -60,21 +50,7 @@ describe('Permissions', function () { { act: "remove", obj: "user" } ], currTestPermId = 1, - // currTestUserId = 1, - // createTestUser = function (email) { - // if (!email) { - // currTestUserId += 1; - // email = "test" + currTestPermId + "@test.com"; - // } - // var newUser = { - // id: currTestUserId, - // email: email, - // password: "testing123" - // }; - - // return UserProvider.add(newUser); - // }, createPermission = function (name, act, obj) { if (!name) { currTestPermId += 1; @@ -97,347 +73,374 @@ describe('Permissions', function () { return when.all(createActions); }; - it('can load an actions map from existing permissions', function (done) { + describe('Init Permissions', function () { - createTestPermissions() - .then(permissions.init) - .then(function (actionsMap) { - should.exist(actionsMap); + beforeEach(function (done) { + sandbox = sinon.sandbox.create(); + testUtils.initData() + .then(testUtils.insertDefaultUser) + .then(testUtils.insertDefaultApp) + .then(function () { + done(); + }).catch(done); + }); - actionsMap.edit.sort().should.eql(['post', 'tag', 'user', 'page', 'theme', 'setting'].sort()); + it('can load an actions map from existing permissions', function (done) { + createTestPermissions() + .then(permissions.init) + .then(function (actionsMap) { + should.exist(actionsMap); - actionsMap.should.equal(permissions.actionsMap); + actionsMap.edit.sort().should.eql(['post', 'tag', 'user', 'page', 'theme', 'setting'].sort()); + + actionsMap.should.equal(permissions.actionsMap); + + done(); + }).catch(done); + }); + + it('can add user to role', function (done) { + var existingUserRoles; + + UserProvider.findOne({id: 1}, { withRelated: ['roles'] }).then(function (foundUser) { + var testRole = new Models.Role({ + name: 'testrole1', + description: 'testrole1 description' + }); + + should.exist(foundUser); + + should.exist(foundUser.roles()); + + existingUserRoles = foundUser.related('roles').length; + + return testRole.save(null, {user: 1}).then(function () { + return foundUser.roles().attach(testRole); + }); + }).then(function () { + return UserProvider.findOne({id: 1}, { withRelated: ['roles'] }); + }).then(function (updatedUser) { + should.exist(updatedUser); + + updatedUser.related('roles').length.should.equal(existingUserRoles + 1); done(); }).catch(done); - }); - - it('can add user to role', function (done) { - var existingUserRoles; - - UserProvider.findOne({id: 1}, { withRelated: ['roles'] }).then(function (foundUser) { - var testRole = new Models.Role({ - name: 'testrole1', - description: 'testrole1 description' - }); - - should.exist(foundUser); - - should.exist(foundUser.roles()); - - existingUserRoles = foundUser.related('roles').length; - - return testRole.save(null, {user: 1}).then(function () { - return foundUser.roles().attach(testRole); - }); - }).then(function () { - return UserProvider.findOne({id: 1}, { withRelated: ['roles'] }); - }).then(function (updatedUser) { - should.exist(updatedUser); - - updatedUser.related('roles').length.should.equal(existingUserRoles + 1); - - done(); - }).catch(done); - }); - - it('can add user permissions', function (done) { - UserProvider.findOne({id: 1}, { withRelated: ['permissions']}).then(function (testUser) { - var testPermission = new Models.Permission({ - name: "test edit posts", - action_type: 'edit', - object_type: 'post' - }); - - testUser.related('permissions').length.should.equal(0); - - return testPermission.save(null, {user: 1}).then(function () { - return testUser.permissions().attach(testPermission); - }); - }).then(function () { - return UserProvider.findOne({id: 1}, { withRelated: ['permissions']}); - }).then(function (updatedUser) { - should.exist(updatedUser); - - updatedUser.related('permissions').length.should.equal(1); - - done(); - }).catch(done); - }); - - it('can add role permissions', function (done) { - var testRole = new Models.Role({ - name: "test2", - description: "test2 description" }); - testRole.save(null, {user: 1}) - .then(function () { - return testRole.load('permissions'); - }) - .then(function () { - var rolePermission = new Models.Permission({ + it('can add user permissions', function (done) { + UserProvider.findOne({id: 1}, { withRelated: ['permissions']}).then(function (testUser) { + var testPermission = new Models.Permission({ name: "test edit posts", action_type: 'edit', object_type: 'post' }); - testRole.related('permissions').length.should.equal(0); + testUser.related('permissions').length.should.equal(0); - return rolePermission.save(null, {user: 1}).then(function () { - return testRole.permissions().attach(rolePermission); + return testPermission.save(null, {user: 1}).then(function () { + return testUser.permissions().attach(testPermission); }); - }) - .then(function () { - return Models.Role.findOne({id: testRole.id}, { withRelated: ['permissions']}); - }) - .then(function (updatedRole) { - should.exist(updatedRole); - - updatedRole.related('permissions').length.should.equal(1); - - done(); - }).catch(done); - }); - - it('does not allow edit post without permission', function (done) { - var fakePage = { - id: 1 - }; - - createTestPermissions() - .then(permissions.init) - .then(function () { - return UserProvider.findOne({id: 1}); - }) - .then(function (foundUser) { - var canThisResult = permissions.canThis(foundUser); - - should.exist(canThisResult.edit); - should.exist(canThisResult.edit.post); - - return canThisResult.edit.page(fakePage); - }) - .then(function () { - errors.logError(new Error("Allowed edit post without permission")); - }).catch(done); - }); - - it('allows edit post with permission', function (done) { - var fakePost = { - id: "1" - }; - - createTestPermissions() - .then(permissions.init) - .then(function () { - return UserProvider.findOne({id: 1}); - }) - .then(function (foundUser) { - var newPerm = new Models.Permission({ - name: "test3 edit post", - action_type: "edit", - object_type: "post" - }); - - return newPerm.save(null, {user: 1}).then(function () { - return foundUser.permissions().attach(newPerm); - }); - }) - .then(function () { + }).then(function () { return UserProvider.findOne({id: 1}, { withRelated: ['permissions']}); - }) - .then(function (updatedUser) { + }).then(function (updatedUser) { + should.exist(updatedUser); - // TODO: Verify updatedUser.related('permissions') has the permission? - var canThisResult = permissions.canThis(updatedUser.id); + updatedUser.related('permissions').length.should.equal(1); - should.exist(canThisResult.edit); - should.exist(canThisResult.edit.post); - - return canThisResult.edit.post(fakePost); - }) - .then(function () { done(); }).catch(done); - }); + }); - it('can use permissable function on Model to allow something', function (done) { - var testUser, - permissableStub = sandbox.stub(PostProvider, 'permissable', function () { - return when.resolve(); + it('can add role permissions', function (done) { + var testRole = new Models.Role({ + name: "test2", + description: "test2 description" }); - testUtils.insertAuthorUser() - .then(function () { - return UserProvider.findAll(); - }) - .then(function (foundUser) { - testUser = foundUser.models[1]; + testRole.save(null, {user: 1}) + .then(function () { + return testRole.load('permissions'); + }) + .then(function () { + var rolePermission = new Models.Permission({ + name: "test edit posts", + action_type: 'edit', + object_type: 'post' + }); - return permissions.canThis(testUser).edit.post(123); - }) - .then(function () { - permissableStub.restore(); - permissableStub.calledWith(123, { user: testUser.id, app: null, internal: false }).should.equal(true); + testRole.related('permissions').length.should.equal(0); - done(); - }) - .catch(function () { - permissableStub.restore(); - errors.logError(new Error("Did not allow testUser")); + return rolePermission.save(null, {user: 1}).then(function () { + return testRole.permissions().attach(rolePermission); + }); + }) + .then(function () { + return Models.Role.findOne({id: testRole.id}, { withRelated: ['permissions']}); + }) + .then(function (updatedRole) { + should.exist(updatedRole); + + updatedRole.related('permissions').length.should.equal(1); + + done(); + }).catch(done); + }); - done(); - }); }); - it('can use permissable function on Model to forbid something', function (done) { - var testUser, - permissableStub = sandbox.stub(PostProvider, 'permissable', function () { - return when.reject(); - }); + describe('With Permissions', function () { - testUtils.insertAuthorUser() - .then(function () { - return UserProvider.findAll(); - }) - .then(function (foundUser) { - testUser = foundUser.models[1]; + beforeEach(function (done) { + sandbox = sinon.sandbox.create(); + testUtils.initData() + .then(testUtils.insertDefaultUser) + .then(testUtils.insertDefaultApp) + .then(function () { + done(); + }).catch(done); + }); - return permissions.canThis(testUser).edit.post(123); - }) - .then(function () { - permissableStub.restore(); - done(new Error("Allowed testUser to edit post")); - }) - .catch(function () { - permissableStub.restore(); - permissableStub.calledWith(123, { user: testUser.id, app: null, internal: false }).should.equal(true); - done(); - }); - }); + it('does not allow edit post without permission', function (done) { + var fakePage = { + id: 1 + }; - it("can get effective user permissions", function (done) { - effectivePerms.user(1).then(function (effectivePermissions) { - should.exist(effectivePermissions); + createTestPermissions() + .then(permissions.init) + .then(function () { + return UserProvider.findOne({id: 1}); + }) + .then(function (foundUser) { + var canThisResult = permissions.canThis(foundUser); - effectivePermissions.length.should.be.above(0); + should.exist(canThisResult.edit); + should.exist(canThisResult.edit.post); - done(); - }).catch(done); - }); + return canThisResult.edit.page(fakePage); + }) + .then(function () { + errors.logError(new Error("Allowed edit post without permission")); + }).catch(done); + }); - it('can check an apps effective permissions', function (done) { - effectivePerms.app('Kudos') - .then(function (effectivePermissions) { + it('allows edit post with permission', function (done) { + var fakePost = { + id: "1" + }; + + createTestPermissions() + .then(permissions.init) + .then(function () { + return UserProvider.findOne({id: 1}); + }) + .then(function (foundUser) { + var newPerm = new Models.Permission({ + name: "test3 edit post", + action_type: "edit", + object_type: "post" + }); + + return newPerm.save(null, {user: 1}).then(function () { + return foundUser.permissions().attach(newPerm); + }); + }) + .then(function () { + return UserProvider.findOne({id: 1}, { withRelated: ['permissions']}); + }) + .then(function (updatedUser) { + + // TODO: Verify updatedUser.related('permissions') has the permission? + var canThisResult = permissions.canThis(updatedUser.id); + + should.exist(canThisResult.edit); + should.exist(canThisResult.edit.post); + + return canThisResult.edit.post(fakePost); + }) + .then(function () { + done(); + }).catch(done); + }); + + it('can use permissable function on Model to allow something', function (done) { + var testUser, + permissableStub = sandbox.stub(PostProvider, 'permissable', function () { + return when.resolve(); + }); + + testUtils.insertAuthorUser() + .then(function () { + return UserProvider.findAll(); + }) + .then(function (foundUser) { + testUser = foundUser.models[1]; + + return permissions.canThis({user: testUser.id}).edit.post(123); + }) + .then(function () { + permissableStub.restore(); + permissableStub.calledWith(123, { user: testUser.id, app: null, internal: false }).should.equal(true); + + done(); + }) + .catch(function () { + permissableStub.restore(); + errors.logError(new Error("Did not allow testUser")); + + done(); + }); + }); + + it('can use permissable function on Model to forbid something', function (done) { + var testUser, + permissableStub = sandbox.stub(PostProvider, 'permissable', function () { + return when.reject(); + }); + + testUtils.insertAuthorUser() + .then(function () { + return UserProvider.findAll(); + }) + .then(function (foundUser) { + testUser = foundUser.models[1]; + + return permissions.canThis({user: testUser.id}).edit.post(123); + }) + .then(function () { + + permissableStub.restore(); + done(new Error("Allowed testUser to edit post")); + }) + .catch(function () { + permissableStub.calledWith(123, { user: testUser.id, app: null, internal: false }).should.equal(true); + permissableStub.restore(); + done(); + }); + }); + + it("can get effective user permissions", function (done) { + effectivePerms.user(1).then(function (effectivePermissions) { should.exist(effectivePermissions); effectivePermissions.length.should.be.above(0); done(); - }) - .catch(done); - }); + }).catch(done); + }); - it('does not allow an app to edit a post without permission', function (done) { - // Change the author of the post so the author override doesn't affect the test - PostProvider.edit({id: 1, 'author_id': 2}) - .then(function (updatedPost) { - // Add user permissions - return UserProvider.findOne({id: 1}) - .then(function (foundUser) { - var newPerm = new Models.Permission({ - name: "app test edit post", - action_type: "edit", - object_type: "post" - }); + it('can check an apps effective permissions', function (done) { + effectivePerms.app('Kudos') + .then(function (effectivePermissions) { + should.exist(effectivePermissions); - return newPerm.save(null, {user: 1}).then(function () { - return foundUser.permissions().attach(newPerm).then(function () { - return when.all([updatedPost, foundUser]); + effectivePermissions.length.should.be.above(0); + + done(); + }) + .catch(done); + }); + + it('does not allow an app to edit a post without permission', function (done) { + // Change the author of the post so the author override doesn't affect the test + PostProvider.edit({'author_id': 2}, {id: 1}) + .then(function (updatedPost) { + // Add user permissions + return UserProvider.findOne({id: 1}) + .then(function (foundUser) { + var newPerm = new Models.Permission({ + name: "app test edit post", + action_type: "edit", + object_type: "post" + }); + + return newPerm.save(null, {user: 1}).then(function () { + return foundUser.permissions().attach(newPerm).then(function () { + return when.all([updatedPost, foundUser]); + }); }); }); - }); - }) - .then(function (results) { - var updatedPost = results[0], - updatedUser = results[1]; + }) + .then(function (results) { + var updatedPost = results[0], + updatedUser = results[1]; - return permissions.canThis({ user: updatedUser.id }) - .edit - .post(updatedPost.id) - .then(function () { - return results; - }) - .catch(function (err) { - done(new Error("Did not allow user 1 to edit post 1")); - }); - }) - .then(function (results) { - var updatedPost = results[0], - updatedUser = results[1]; + return permissions.canThis({ user: updatedUser.id }) + .edit + .post(updatedPost.id) + .then(function () { + return results; + }) + .catch(function (err) { + done(new Error("Did not allow user 1 to edit post 1")); + }); + }) + .then(function (results) { + var updatedPost = results[0], + updatedUser = results[1]; - // Confirm app cannot edit it. - return permissions.canThis({ app: 'Hemingway', user: updatedUser.id }) - .edit - .post(updatedPost.id) - .then(function () { - done(new Error("Allowed an edit of post 1")); - }) - .catch(function () { - done(); - }); - }).catch(done); - }); + // Confirm app cannot edit it. + return permissions.canThis({ app: 'Hemingway', user: updatedUser.id }) + .edit + .post(updatedPost.id) + .then(function () { + done(new Error("Allowed an edit of post 1")); + }) + .catch(function () { + done(); + }); + }).catch(done); + }); - it('allows an app to edit a post with permission', function (done) { - permissions.canThis({ app: 'Kudos', user: 1 }) - .edit - .post(1) - .then(function () { - done(); - }) - .catch(function () { - done(new Error("Allowed an edit of post 1")); - }); - }); + it('allows an app to edit a post with permission', function (done) { + permissions.canThis({ app: 'Kudos', user: 1 }) + .edit + .post(1) + .then(function () { + done(); + }) + .catch(function () { + done(new Error("Allowed an edit of post 1")); + }); + }); - it('checks for null context passed and rejects', function (done) { - permissions.canThis(undefined) - .edit - .post(1) - .then(function () { - done(new Error("Should not allow editing post")); - }) - .catch(function () { - done(); - }); - }); + it('checks for null context passed and rejects', function (done) { + permissions.canThis(undefined) + .edit + .post(1) + .then(function () { + done(new Error("Should not allow editing post")); + }) + .catch(function () { + done(); + }); + }); - it('allows \'internal\' to be passed for internal requests', function (done) { - // Using tag here because post implements the custom permissable interface - permissions.canThis('internal') - .edit - .tag(1) - .then(function () { - done(); - }) - .catch(function () { - done(new Error("Should allow editing post with 'internal'")); - }); - }); + it('allows \'internal\' to be passed for internal requests', function (done) { + // Using tag here because post implements the custom permissable interface + permissions.canThis('internal') + .edit + .tag(1) + .then(function () { + done(); + }) + .catch(function () { + done(new Error("Should allow editing post with 'internal'")); + }); + }); - it('allows { internal: true } to be passed for internal requests', function (done) { - // Using tag here because post implements the custom permissable interface - permissions.canThis({ internal: true }) - .edit - .tag(1) - .then(function () { - done(); - }) - .catch(function () { - done(new Error("Should allow editing post with { internal: true }")); - }); + it('allows { internal: true } to be passed for internal requests', function (done) { + // Using tag here because post implements the custom permissable interface + permissions.canThis({ internal: true }) + .edit + .tag(1) + .then(function () { + done(); + }) + .catch(function () { + done(new Error("Should allow editing post with { internal: true }")); + }); + }); }); }); \ No newline at end of file diff --git a/core/test/utils/api.js b/core/test/utils/api.js index 780ecdd46b..b0321cd406 100644 --- a/core/test/utils/api.js +++ b/core/test/utils/api.js @@ -10,7 +10,7 @@ var url = require('url'), post: ['id', 'uuid', 'title', 'slug', 'markdown', 'html', 'meta_title', 'meta_description', 'featured', 'image', 'status', 'language', 'created_at', 'created_by', 'updated_at', 'updated_by', 'published_at', 'published_by', 'page', 'author', 'tags', 'fields'], - settings: ['settings'], + settings: ['settings', 'meta'], setting: ['id', 'uuid', 'key', 'value', 'type', 'created_at', 'created_by', 'updated_at', 'updated_by'], tag: ['id', 'uuid', 'name', 'slug', 'description', 'parent', 'meta_title', 'meta_description', 'created_at', 'created_by', 'updated_at', 'updated_by'],