From d81bc91bd251bc4dc8d9c6aedb749f76c987ae18 Mon Sep 17 00:00:00 2001 From: Katharina Irrgang Date: Thu, 6 Oct 2016 14:27:35 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Error=20creation=20(#7477)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refs #7116, refs #2001 - Changes the way Ghost errors are implemented to benefit from proper inheritance - Moves all error definitions into a single file - Changes the error constructor to take an options object, rather than needing the arguments to be passed in the correct order. - Provides a wrapper so that any errors that haven't already been converted to GhostErrors get converted before they are displayed. Summary of changes: * 🐛 set NODE_ENV in config handler * ✨ add GhostError implementation (core/server/errors.js) - register all errors in one file - inheritance from GhostError - option pattern * 🔥 remove all error files * ✨ wrap all errors into GhostError in case of HTTP * 🎨 adaptions - option pattern for errors - use GhostError when needed * 🎨 revert debug deletion and add TODO for error id's --- core/server/api/authentication.js | 58 ++++--- core/server/api/clients.js | 2 +- core/server/api/db.js | 8 +- core/server/api/invites.js | 12 +- core/server/api/mail.js | 2 +- core/server/api/notifications.js | 17 +- core/server/api/posts.js | 6 +- core/server/api/schedules.js | 6 +- core/server/api/settings.js | 20 ++- core/server/api/slugs.js | 4 +- core/server/api/subscribers.js | 12 +- core/server/api/tags.js | 4 +- core/server/api/themes.js | 30 ++-- core/server/api/users.js | 27 ++- core/server/api/utils.js | 22 ++- .../apps/amp/lib/helpers/amp_content.js | 11 +- core/server/apps/amp/tests/router_spec.js | 3 +- core/server/apps/index.js | 18 +- core/server/apps/private-blogging/index.js | 15 +- .../apps/private-blogging/lib/middleware.js | 15 +- core/server/apps/subscribers/lib/router.js | 2 +- core/server/auth/auth-strategies.js | 6 +- core/server/auth/authenticate.js | 32 ++-- core/server/auth/authorize.js | 4 +- core/server/auth/oauth.js | 6 +- core/server/auth/passport.js | 11 +- core/server/config/index.js | 3 +- core/server/controllers/admin.js | 5 +- core/server/controllers/frontend/channels.js | 3 +- .../controllers/frontend/render-channel.js | 2 +- core/server/data/export/index.js | 20 ++- core/server/data/import/index.js | 2 +- core/server/data/import/utils.js | 8 +- core/server/data/importer/handlers/json.js | 12 +- core/server/data/importer/index.js | 33 ++-- core/server/data/migration/populate.js | 2 +- core/server/data/migration/update.js | 14 +- core/server/data/schema/bootup.js | 12 +- core/server/data/schema/versioning.js | 2 +- core/server/data/slack/index.js | 10 +- core/server/data/validation/index.js | 15 +- core/server/data/xml/rss/index.js | 2 +- core/server/data/xml/xmlrpc.js | 11 +- core/server/errors.js | 162 ++++++++++++++++++ core/server/errors/bad-request-error.js | 16 -- core/server/errors/data-import-error.js | 16 -- core/server/errors/database-not-populated.js | 11 -- core/server/errors/database-version.js | 13 -- core/server/errors/email-error.js | 16 -- core/server/errors/incorrect-usage.js | 11 -- core/server/errors/index.js | 41 ----- core/server/errors/internal-server-error.js | 16 -- core/server/errors/maintenance.js | 11 -- .../server/errors/method-not-allowed-error.js | 14 -- core/server/errors/no-permission-error.js | 14 -- core/server/errors/not-found-error.js | 14 -- core/server/errors/request-too-large-error.js | 14 -- core/server/errors/theme-validation-error.js | 18 -- core/server/errors/token-revocation-error.js | 14 -- core/server/errors/too-many-requests-error.js | 16 -- core/server/errors/unauthorized-error.js | 16 -- .../errors/unsupported-media-type-error.js | 14 -- core/server/errors/validation-error.js | 19 -- core/server/errors/version-mismatch-error.js | 14 -- core/server/ghost-server.js | 20 +-- core/server/helpers/get.js | 14 +- core/server/helpers/index.js | 38 ++-- core/server/helpers/navigation.js | 22 ++- core/server/helpers/pagination.js | 16 +- core/server/helpers/plural.js | 6 +- core/server/helpers/template.js | 4 +- core/server/index.js | 2 + core/server/middleware/api/version-match.js | 9 +- core/server/middleware/error-handler.js | 7 + core/server/middleware/index.js | 17 +- core/server/middleware/maintenance.js | 4 +- core/server/middleware/spam-prevention.js | 37 ++-- core/server/middleware/theme-handler.js | 7 +- core/server/middleware/validation/upload.js | 4 +- core/server/models/base/index.js | 5 +- core/server/models/base/listeners.js | 12 +- core/server/models/base/token.js | 2 +- core/server/models/plugins/filter.js | 14 +- core/server/models/post.js | 49 +++--- core/server/models/role.js | 2 +- core/server/models/settings.js | 6 +- core/server/models/subscriber.js | 2 +- core/server/models/user.js | 91 +++++----- core/server/permissions/effective.js | 2 +- core/server/permissions/index.js | 4 +- core/server/scheduling/SchedulingDefault.js | 6 +- .../scheduling/post-scheduling/index.js | 4 +- core/server/scheduling/utils.js | 18 +- core/server/storage/index.js | 21 ++- core/server/storage/local-file-store.js | 4 +- core/server/translations/en.json | 3 +- core/server/update-check.js | 11 +- .../unit/controllers/frontend/error_spec.js | 7 +- core/test/unit/exporter_spec.js | 18 +- .../unit/middleware/theme-handler_spec.js | 10 +- core/test/unit/migration_spec.js | 6 +- .../scheduling/post-scheduling/index_spec.js | 2 +- core/test/unit/scheduling/utils_spec.js | 2 +- core/test/unit/server_spec.js | 4 +- core/test/unit/storage/index_spec.js | 4 +- core/test/unit/versioning_spec.js | 6 +- core/test/unit/xmlrpc_spec.js | 62 +++---- index.js | 3 +- 108 files changed, 766 insertions(+), 810 deletions(-) create mode 100644 core/server/errors.js delete mode 100644 core/server/errors/bad-request-error.js delete mode 100644 core/server/errors/data-import-error.js delete mode 100644 core/server/errors/database-not-populated.js delete mode 100644 core/server/errors/database-version.js delete mode 100644 core/server/errors/email-error.js delete mode 100644 core/server/errors/incorrect-usage.js delete mode 100644 core/server/errors/index.js delete mode 100644 core/server/errors/internal-server-error.js delete mode 100644 core/server/errors/maintenance.js delete mode 100644 core/server/errors/method-not-allowed-error.js delete mode 100644 core/server/errors/no-permission-error.js delete mode 100644 core/server/errors/not-found-error.js delete mode 100644 core/server/errors/request-too-large-error.js delete mode 100644 core/server/errors/theme-validation-error.js delete mode 100644 core/server/errors/token-revocation-error.js delete mode 100644 core/server/errors/too-many-requests-error.js delete mode 100644 core/server/errors/unauthorized-error.js delete mode 100644 core/server/errors/unsupported-media-type-error.js delete mode 100644 core/server/errors/validation-error.js delete mode 100644 core/server/errors/version-mismatch-error.js diff --git a/core/server/api/authentication.js b/core/server/api/authentication.js index 705ab62ea5f..ef3f4d8ccc8 100644 --- a/core/server/api/authentication.js +++ b/core/server/api/authentication.js @@ -8,8 +8,8 @@ var _ = require('lodash'), globalUtils = require('../utils'), utils = require('./utils'), errors = require('../errors'), - logging = require('../logging'), models = require('../models'), + logging = require('../logging'), events = require('../events'), config = require('../config'), i18n = require('../i18n'), @@ -43,7 +43,7 @@ function assertSetupCompleted(status) { notCompleted = i18n.t('errors.api.authentication.setupMustBeCompleted'); function throwReason(reason) { - throw new errors.NoPermissionError(reason); + throw new errors.NoPermissionError({message: reason}); } if (isSetup) { @@ -78,9 +78,9 @@ function setupTasks(setupData) { return User.findOne({role: 'Owner', status: 'all'}).then(function then(owner) { if (!owner) { - throw new errors.InternalServerError( - i18n.t('errors.api.authentication.setupUnableToRun') - ); + throw new errors.GhostError({ + message: i18n.t('errors.api.authentication.setupUnableToRun') + }); } return User.setup(userData, _.extend({id: owner.id}, context)); @@ -175,9 +175,9 @@ authentication = { var email = data.passwordreset[0].email; if (typeof email !== 'string' || !validator.isEmail(email)) { - throw new errors.BadRequestError( - i18n.t('errors.api.authentication.noEmailProvided') - ); + throw new errors.BadRequestError({ + message: i18n.t('errors.api.authentication.noEmailProvided') + }); } return email; @@ -274,8 +274,8 @@ authentication = { ne2Password: ne2Password, dbHash: response.settings[0].value }); - }).catch(function (error) { - throw new errors.UnauthorizedError(error.message); + }).catch(function (err) { + throw new errors.UnauthorizedError({err: err}); }); } @@ -309,19 +309,19 @@ authentication = { return utils.checkObject(invitation, 'invitation') .then(function () { if (!invitation.invitation[0].token) { - return Promise.reject(new errors.ValidationError(i18n.t('errors.api.authentication.noTokenProvided'))); + return Promise.reject(new errors.ValidationError({message: i18n.t('errors.api.authentication.noTokenProvided')})); } if (!invitation.invitation[0].email) { - return Promise.reject(new errors.ValidationError(i18n.t('errors.api.authentication.noEmailProvided'))); + return Promise.reject(new errors.ValidationError({message: i18n.t('errors.api.authentication.noEmailProvided')})); } if (!invitation.invitation[0].password) { - return Promise.reject(new errors.ValidationError(i18n.t('errors.api.authentication.noPasswordProvided'))); + return Promise.reject(new errors.ValidationError({message: i18n.t('errors.api.authentication.noPasswordProvided')})); } if (!invitation.invitation[0].name) { - return Promise.reject(new errors.ValidationError(i18n.t('errors.api.authentication.noNameProvided'))); + return Promise.reject(new errors.ValidationError({message: i18n.t('errors.api.authentication.noNameProvided')})); } return invitation; @@ -336,11 +336,11 @@ authentication = { invite = _invite; if (!invite) { - throw new errors.NotFoundError(i18n.t('errors.api.invites.inviteNotFound')); + throw new errors.NotFoundError({message: i18n.t('errors.api.invites.inviteNotFound')}); } if (invite.get('expires') < Date.now()) { - throw new errors.NotFoundError(i18n.t('errors.api.invites.inviteExpired')); + throw new errors.NotFoundError({message: i18n.t('errors.api.invites.inviteExpired')}); } return models.User.add({ @@ -386,9 +386,9 @@ authentication = { var email = options.email; if (typeof email !== 'string' || !validator.isEmail(email)) { - throw new errors.BadRequestError( - i18n.t('errors.api.authentication.invalidEmailReceived') - ); + throw new errors.BadRequestError({ + message: i18n.t('errors.api.authentication.invalidEmailReceived') + }); } return email; @@ -489,10 +489,12 @@ authentication = { }] }; - apiMail.send(payload, {context: {internal: true}}).catch(function (err) { - err.context = i18n.t('errors.api.authentication.unableToSendWelcomeEmail'); - err.help = i18n.t('errors.api.authentication.checkEmailConfigInstructions', {url: 'http://support.ghost.org/mail/'}); - logging.error(err); + apiMail.send(payload, {context: {internal: true}}).catch(function (error) { + logging.error(new errors.EmailError({ + err: error, + context: i18n.t('errors.api.authentication.unableToSendWelcomeEmail'), + help: i18n.t('errors.api.authentication.checkEmailConfigInstructions', {url: 'http://support.ghost.org/mail/'}) + })); }); }) .return(setupUser); @@ -524,7 +526,7 @@ authentication = { function processArgs(setupDetails, options) { if (!options.context || !options.context.user) { - throw new errors.NoPermissionError(i18n.t('errors.api.authentication.notTheBlogOwner')); + throw new errors.NoPermissionError({message: i18n.t('errors.api.authentication.notTheBlogOwner')}); } return _.assign({setupDetails: setupDetails}, options); @@ -534,7 +536,7 @@ authentication = { return models.User.findOne({role: 'Owner', status: 'all'}) .then(function (owner) { if (owner.id !== options.context.user) { - throw new errors.NoPermissionError(i18n.t('errors.api.authentication.notTheBlogOwner')); + throw new errors.NoPermissionError({message: i18n.t('errors.api.authentication.notTheBlogOwner')}); } return options.setupDetails; @@ -591,9 +593,9 @@ authentication = { return destroyToken(providers.pop(), options, providers); }) .catch(function () { - throw new errors.TokenRevocationError( - i18n.t('errors.api.authentication.tokenRevocationFailed') - ); + throw new errors.TokenRevocationError({ + message: i18n.t('errors.api.authentication.tokenRevocationFailed') + }); }); } diff --git a/core/server/api/clients.js b/core/server/api/clients.js index ec1f406c5cb..5bba9a24fb7 100644 --- a/core/server/api/clients.js +++ b/core/server/api/clients.js @@ -53,7 +53,7 @@ clients = { return {clients: [result.toJSON(options)]}; } - return Promise.reject(new errors.NotFoundError(i18n.t('common.api.clients.clientNotFound'))); + return Promise.reject(new errors.NotFoundError({message: i18n.t('common.api.clients.clientNotFound')})); }); } }; diff --git a/core/server/api/db.js b/core/server/api/db.js index 24e39f5fd8a..6d22aa1ef4e 100644 --- a/core/server/api/db.js +++ b/core/server/api/db.js @@ -37,8 +37,8 @@ db = { function exportContent() { return exporter.doExport().then(function (exportedData) { return {db: [exportedData]}; - }).catch(function (error) { - return Promise.reject(new errors.InternalServerError(error.message || error)); + }).catch(function (err) { + return Promise.reject(new errors.GhostError({err: err})); }); } @@ -99,8 +99,8 @@ db = { return Promise.each(collections, function then(Collection) { return Collection.invokeThen('destroy'); }).return({db: []}) - .catch(function (error) { - throw new errors.InternalServerError(error.message || error); + .catch(function (err) { + throw new errors.GhostError({err: err}); }); } diff --git a/core/server/api/invites.js b/core/server/api/invites.js index 9374c1c1781..acf5769f06c 100644 --- a/core/server/api/invites.js +++ b/core/server/api/invites.js @@ -54,7 +54,7 @@ invites = { return {invites: [result.toJSON(options)]}; } - return Promise.reject(new errors.NotFoundError(i18n.t('errors.api.invites.inviteNotFound'))); + return Promise.reject(new errors.NotFoundError({message: i18n.t('errors.api.invites.inviteNotFound')})); }); }, @@ -65,7 +65,7 @@ invites = { return dataProvider.Invite.findOne({id: options.id}, _.omit(options, ['data'])) .then(function (invite) { if (!invite) { - throw new errors.NotFoundError(i18n.t('errors.api.invites.inviteNotFound')); + throw new errors.NotFoundError({message: i18n.t('errors.api.invites.inviteNotFound')}); } return invite.destroy(options).return(null); @@ -94,7 +94,7 @@ invites = { return dataProvider.User.findOne({id: loggedInUser}, options) .then(function (user) { if (!user) { - return Promise.reject(new errors.NotFoundError(i18n.t('errors.api.users.userNotFound'))); + return Promise.reject(new errors.NotFoundError({message: i18n.t('errors.api.users.userNotFound')})); } loggedInUser = user; @@ -172,11 +172,11 @@ invites = { var roleId; if (!options.data.invites[0].email) { - return Promise.reject(new errors.ValidationError(i18n.t('errors.api.invites.emailIsRequired'))); + return Promise.reject(new errors.ValidationError({message: i18n.t('errors.api.invites.emailIsRequired')})); } if (!options.data.invites[0].roles || !options.data.invites[0].roles[0]) { - return Promise.reject(new errors.ValidationError(i18n.t('errors.api.invites.roleIsRequired'))); + return Promise.reject(new errors.ValidationError({message: i18n.t('errors.api.invites.roleIsRequired')})); } roleId = parseInt(options.data.invites[0].roles[0].id || options.data.invites[0].roles[0], 10); @@ -185,7 +185,7 @@ invites = { // Make sure user is allowed to add a user with this role return dataProvider.Role.findOne({id: roleId}).then(function (role) { if (role.get('name') === 'Owner') { - return Promise.reject(new errors.NoPermissionError(i18n.t('errors.api.invites.notAllowedToInviteOwner'))); + return Promise.reject(new errors.NoPermissionError({message: i18n.t('errors.api.invites.notAllowedToInviteOwner')})); } }).then(function () { return options; diff --git a/core/server/api/mail.js b/core/server/api/mail.js index fd3bc1a70b9..0f13d697b33 100644 --- a/core/server/api/mail.js +++ b/core/server/api/mail.js @@ -38,7 +38,7 @@ function sendMail(object) { ); } - return Promise.reject(new errors.EmailError(err.message)); + return Promise.reject(new errors.EmailError({err: err})); }); } diff --git a/core/server/api/notifications.js b/core/server/api/notifications.js index e736bcd634b..af689a034ee 100644 --- a/core/server/api/notifications.js +++ b/core/server/api/notifications.js @@ -32,7 +32,7 @@ notifications = { return canThis(options.context).browse.notification().then(function () { return {notifications: notificationsStore}; }, function () { - return Promise.reject(new errors.NoPermissionError(i18n.t('errors.api.notifications.noPermissionToBrowseNotif'))); + return Promise.reject(new errors.NoPermissionError({message: i18n.t('errors.api.notifications.noPermissionToBrowseNotif')})); }); }, @@ -72,7 +72,7 @@ notifications = { return canThis(options.context).add.notification().then(function () { return options; }, function () { - return Promise.reject(new errors.NoPermissionError(i18n.t('errors.api.notifications.noPermissionToAddNotif'))); + return Promise.reject(new errors.NoPermissionError({message: i18n.t('errors.api.notifications.noPermissionToAddNotif')})); }); } @@ -155,7 +155,7 @@ notifications = { return canThis(options.context).destroy.notification().then(function () { return options; }, function () { - return Promise.reject(new errors.NoPermissionError(i18n.t('errors.api.notifications.noPermissionToDestroyNotif'))); + return Promise.reject(new errors.NoPermissionError({message: i18n.t('errors.api.notifications.noPermissionToDestroyNotif')})); }); } @@ -166,12 +166,12 @@ notifications = { if (notification && !notification.dismissible) { return Promise.reject( - new errors.NoPermissionError(i18n.t('errors.api.notifications.noPermissionToDismissNotif')) + new errors.NoPermissionError({message: i18n.t('errors.api.notifications.noPermissionToDismissNotif')}) ); } if (!notification) { - return Promise.reject(new errors.NotFoundError(i18n.t('errors.api.notifications.notificationDoesNotExist'))); + return Promise.reject(new errors.NotFoundError({message: i18n.t('errors.api.notifications.notificationDoesNotExist')})); } notificationsStore = _.reject(notificationsStore, function (element) { @@ -206,8 +206,11 @@ notifications = { notificationCounter = 0; return notificationsStore; - }, function () { - return Promise.reject(new errors.NoPermissionError(i18n.t('errors.api.notifications.noPermissionToDestroyNotif'))); + }, function (err) { + return Promise.reject(new errors.NoPermissionError({ + err: err, + context: i18n.t('errors.api.notifications.noPermissionToDestroyNotif') + })); }); } }; diff --git a/core/server/api/posts.js b/core/server/api/posts.js index e0a3fb39b02..ef4bc69bf82 100644 --- a/core/server/api/posts.js +++ b/core/server/api/posts.js @@ -107,7 +107,7 @@ posts = { return {posts: [result.toJSON(options)]}; } - return Promise.reject(new errors.NotFoundError(i18n.t('errors.api.posts.postNotFound'))); + return Promise.reject(new errors.NotFoundError({message: i18n.t('errors.api.posts.postNotFound')})); }); }, @@ -154,7 +154,7 @@ posts = { return {posts: [post]}; } - return Promise.reject(new errors.NotFoundError(i18n.t('errors.api.posts.postNotFound'))); + return Promise.reject(new errors.NotFoundError({message: i18n.t('errors.api.posts.postNotFound')})); }); }, @@ -223,7 +223,7 @@ posts = { return Post.findOne(data, fetchOpts).then(function () { return Post.destroy(options).return(null); }).catch(Post.NotFoundError, function () { - throw new errors.NotFoundError(i18n.t('errors.api.posts.postNotFound')); + throw new errors.NotFoundError({message: i18n.t('errors.api.posts.postNotFound')}); }); } diff --git a/core/server/api/schedules.js b/core/server/api/schedules.js index 18918b424c7..1bfe83d63fd 100644 --- a/core/server/api/schedules.js +++ b/core/server/api/schedules.js @@ -25,7 +25,7 @@ exports.publishPost = function publishPost(object, options) { // CASE: only the scheduler client is allowed to publish (hardcoded because of missing client permission system) if (!options.context || !options.context.client || options.context.client !== 'ghost-scheduler') { - return Promise.reject(new errors.NoPermissionError(i18n.t('errors.permissions.noPermissionToAction'))); + return Promise.reject(new errors.NoPermissionError({message: i18n.t('errors.permissions.noPermissionToAction')})); } options.context = {internal: true}; @@ -41,11 +41,11 @@ exports.publishPost = function publishPost(object, options) { publishedAtMoment = moment(post.published_at); if (publishedAtMoment.diff(moment(), 'minutes') > publishAPostBySchedulerToleranceInMinutes) { - return Promise.reject(new errors.NotFoundError(i18n.t('errors.api.job.notFound'))); + return Promise.reject(new errors.NotFoundError({message: i18n.t('errors.api.job.notFound')})); } if (publishedAtMoment.diff(moment(), 'minutes') < publishAPostBySchedulerToleranceInMinutes * -1 && object.force !== true) { - return Promise.reject(new errors.NotFoundError(i18n.t('errors.api.job.publishInThePast'))); + return Promise.reject(new errors.NotFoundError({message: i18n.t('errors.api.job.publishInThePast')})); } return apiPosts.edit({posts: [{status: 'published'}]}, _.pick(cleanOptions, ['context', 'id'])); diff --git a/core/server/api/settings.js b/core/server/api/settings.js index 92c595faf39..52e90f278fd 100644 --- a/core/server/api/settings.js +++ b/core/server/api/settings.js @@ -43,10 +43,12 @@ updateConfigCache = function () { try { labsValue = JSON.parse(settingsCache.labs.value); } catch (err) { - err.message = i18n.t('errors.api.settings.invalidJsonInLabs'); - err.context = i18n.t('errors.api.settings.labsColumnCouldNotBeParsed'); - err.help = i18n.t('errors.api.settings.tryUpdatingLabs'); - logging.error(err); + logging.error(new errors.GhostError({ + err: err, + message: i18n.t('errors.api.settings.invalidJsonInLabs'), + context: i18n.t('errors.api.settings.labsColumnCouldNotBeParsed'), + help: i18n.t('errors.api.settings.tryUpdatingLabs') + })); } } @@ -250,7 +252,7 @@ populateDefaultSetting = function (key) { } // TODO: Different kind of error? - return Promise.reject(new errors.NotFoundError(i18n.t('errors.api.settings.problemFindingSetting', {key: key}))); + return Promise.reject(new errors.NotFoundError({message: i18n.t('errors.api.settings.problemFindingSetting', {key: key})})); }); }; @@ -265,12 +267,12 @@ canEditAllSettings = function (settingsInfo, options) { var checkSettingPermissions = function (setting) { if (setting.type === 'core' && !(options.context && options.context.internal)) { return Promise.reject( - new errors.NoPermissionError(i18n.t('errors.api.settings.accessCoreSettingFromExtReq')) + new errors.NoPermissionError({message: i18n.t('errors.api.settings.accessCoreSettingFromExtReq')}) ); } return canThis(options.context).edit.setting(setting.key).catch(function () { - return Promise.reject(new errors.NoPermissionError(i18n.t('errors.api.settings.noPermissionToEditSettings'))); + return Promise.reject(new errors.NoPermissionError({message: i18n.t('errors.api.settings.noPermissionToEditSettings')})); }); }, checks = _.map(settingsInfo, function (settingInfo) { @@ -349,7 +351,7 @@ settings = { if (setting.type === 'core' && !(options.context && options.context.internal)) { return Promise.reject( - new errors.NoPermissionError(i18n.t('errors.api.settings.accessCoreSettingFromExtReq')) + new errors.NoPermissionError({message: i18n.t('errors.api.settings.accessCoreSettingFromExtReq')}) ); } @@ -360,7 +362,7 @@ settings = { return canThis(options.context).read.setting(options.key).then(function () { return settingsResult(result); }, function () { - return Promise.reject(new errors.NoPermissionError(i18n.t('errors.api.settings.noPermissionToReadSettings'))); + return Promise.reject(new errors.NoPermissionError({message: i18n.t('errors.api.settings.noPermissionToReadSettings')})); }); }; diff --git a/core/server/api/slugs.js b/core/server/api/slugs.js index 0fb5f6ab25a..8105d34b07b 100644 --- a/core/server/api/slugs.js +++ b/core/server/api/slugs.js @@ -46,7 +46,7 @@ slugs = { */ function checkAllowedTypes(options) { if (allowedTypes[options.type] === undefined) { - return Promise.reject(new errors.BadRequestError(i18n.t('errors.api.slugs.unknownSlugType', {type: options.type}))); + return Promise.reject(new errors.BadRequestError({message: i18n.t('errors.api.slugs.unknownSlugType', {type: options.type})})); } return options; } @@ -72,7 +72,7 @@ slugs = { // Pipeline calls each task passing the result of one to be the arguments for the next return pipeline(tasks, options).then(function (slug) { if (!slug) { - return Promise.reject(new errors.InternalServerError(i18n.t('errors.api.slugs.couldNotGenerateSlug'))); + return Promise.reject(new errors.GhostError({message: i18n.t('errors.api.slugs.couldNotGenerateSlug')})); } return {slugs: [{slug: slug}]}; diff --git a/core/server/api/subscribers.js b/core/server/api/subscribers.js index ad4c0e965e6..2f42e4e7470 100644 --- a/core/server/api/subscribers.js +++ b/core/server/api/subscribers.js @@ -80,7 +80,7 @@ subscribers = { return {subscribers: [result.toJSON(options)]}; } - return Promise.reject(new errors.NotFoundError(i18n.t('errors.api.subscribers.subscriberNotFound'))); + return Promise.reject(new errors.NotFoundError({message: i18n.t('errors.api.subscribers.subscriberNotFound')})); }); }, @@ -105,12 +105,12 @@ subscribers = { // we don't expose this information return Promise.resolve(subscriber); } else if (subscriber) { - return Promise.reject(new errors.ValidationError(i18n.t('errors.api.subscribers.subscriberAlreadyExists'))); + return Promise.reject(new errors.ValidationError({message: i18n.t('errors.api.subscribers.subscriberAlreadyExists')})); } return dataProvider.Subscriber.add(options.data.subscribers[0], _.omit(options, ['data'])).catch(function (error) { if (error.code && error.message.toLowerCase().indexOf('unique') !== -1) { - return Promise.reject(new errors.ValidationError(i18n.t('errors.api.subscribers.subscriberAlreadyExists'))); + return Promise.reject(new errors.ValidationError({message: i18n.t('errors.api.subscribers.subscriberAlreadyExists')})); } return Promise.reject(error); @@ -167,7 +167,7 @@ subscribers = { return {subscribers: [subscriber]}; } - return Promise.reject(new errors.NotFoundError(i18n.t('errors.api.subscribers.subscriberNotFound'))); + return Promise.reject(new errors.NotFoundError({message: i18n.t('errors.api.subscribers.subscriberNotFound')})); }); }, @@ -241,8 +241,8 @@ subscribers = { function exportSubscribers() { return dataProvider.Subscriber.findPage(options).then(function (data) { return formatCSV(data.subscribers); - }).catch(function (error) { - return Promise.reject(new errors.InternalServerError(error.message || error)); + }).catch(function (err) { + return Promise.reject(new errors.GhostError({err: err})); }); } diff --git a/core/server/api/tags.js b/core/server/api/tags.js index c47b7d919e7..d569186c0ef 100644 --- a/core/server/api/tags.js +++ b/core/server/api/tags.js @@ -81,7 +81,7 @@ tags = { return {tags: [result.toJSON(options)]}; } - return Promise.reject(new errors.NotFoundError(i18n.t('errors.api.tags.tagNotFound'))); + return Promise.reject(new errors.NotFoundError({message: i18n.t('errors.api.tags.tagNotFound')})); }); }, @@ -155,7 +155,7 @@ tags = { return {tags: [tag]}; } - return Promise.reject(new errors.NotFoundError(i18n.t('errors.api.tags.tagNotFound'))); + return Promise.reject(new errors.NotFoundError({message: i18n.t('errors.api.tags.tagNotFound')})); }); }, diff --git a/core/server/api/themes.js b/core/server/api/themes.js index 7578735aa92..404427d80e6 100644 --- a/core/server/api/themes.js +++ b/core/server/api/themes.js @@ -35,15 +35,15 @@ themes = { options.originalname = options.originalname.toLowerCase(); var storageAdapter = storage.getStorage('themes'), - zip = { - path: options.path, - name: options.originalname, - shortName: storageAdapter.getSanitizedFileName(options.originalname.split('.zip')[0]) - }, theme; + zip = { + path: options.path, + name: options.originalname, + shortName: storageAdapter.getSanitizedFileName(options.originalname.split('.zip')[0]) + }, theme; // check if zip name is casper.zip if (zip.name === 'casper.zip') { - throw new errors.ValidationError(i18n.t('errors.api.themes.overrideCasper')); + throw new errors.ValidationError({message: i18n.t('errors.api.themes.overrideCasper')}); } return apiUtils.handlePermissions('themes', 'add')(options) @@ -58,10 +58,10 @@ themes = { return; } - throw new errors.ThemeValidationError( - i18n.t('errors.api.themes.invalidTheme'), - theme.results.error - ); + throw new errors.ThemeValidationError({ + message: i18n.t('errors.api.themes.invalidTheme'), + errorDetails: theme.results.error + }); }) .then(function () { return storageAdapter.exists(config.getContentPath('themes') + '/' + zip.shortName); @@ -104,7 +104,7 @@ themes = { // happens in background Promise.promisify(fs.removeSync)(zip.path) .catch(function (err) { - logging.error(err); + logging.error(new errors.GhostError({err: err})); }); // remove extracted dir from gscan @@ -112,7 +112,7 @@ themes = { if (theme) { Promise.promisify(fs.removeSync)(theme.path) .catch(function (err) { - logging.error(err); + logging.error(new errors.GhostError({err: err})); }); } }); @@ -124,7 +124,7 @@ themes = { storageAdapter = storage.getStorage('themes'); if (!theme) { - return Promise.reject(new errors.BadRequestError(i18n.t('errors.api.themes.invalidRequest'))); + return Promise.reject(new errors.BadRequestError({message: i18n.t('errors.api.themes.invalidRequest')})); } return apiUtils.handlePermissions('themes', 'read')(options) @@ -146,13 +146,13 @@ themes = { return apiUtils.handlePermissions('themes', 'destroy')(options) .then(function () { if (name === 'casper') { - throw new errors.ValidationError(i18n.t('errors.api.themes.destroyCasper')); + throw new errors.ValidationError({message: i18n.t('errors.api.themes.destroyCasper')}); } theme = config.get('paths').availableThemes[name]; if (!theme) { - throw new errors.NotFoundError(i18n.t('errors.api.themes.themeDoesNotExist')); + throw new errors.NotFoundError({message: i18n.t('errors.api.themes.themeDoesNotExist')}); } events.emit('theme.deleted', name); diff --git a/core/server/api/users.js b/core/server/api/users.js index a2120309b24..0d09d14f6c8 100644 --- a/core/server/api/users.js +++ b/core/server/api/users.js @@ -90,7 +90,7 @@ users = { return {users: [result.toJSON(options)]}; } - return Promise.reject(new errors.NotFoundError(i18n.t('errors.api.users.userNotFound'))); + return Promise.reject(new errors.NotFoundError({message: i18n.t('errors.api.users.userNotFound')})); }); }, @@ -145,14 +145,14 @@ users = { var contextRoleId = contextUser.related('roles').toJSON(options)[0].id; if (roleId !== contextRoleId && editedUserId === contextUser.id) { - return Promise.reject(new errors.NoPermissionError(i18n.t('errors.api.users.cannotChangeOwnRole'))); + return Promise.reject(new errors.NoPermissionError({message: i18n.t('errors.api.users.cannotChangeOwnRole')})); } return dataProvider.User.findOne({role: 'Owner'}).then(function (owner) { if (contextUser.id !== owner.id) { if (editedUserId === owner.id) { if (owner.related('roles').at(0).id !== roleId) { - return Promise.reject(new errors.NoPermissionError(i18n.t('errors.api.users.cannotChangeOwnersRole'))); + return Promise.reject(new errors.NoPermissionError({message: i18n.t('errors.api.users.cannotChangeOwnersRole')})); } } else if (roleId !== contextRoleId) { return canThis(options.context).assign.role(role).then(function () { @@ -165,7 +165,10 @@ users = { }); }); }).catch(function handleError(err) { - return Promise.reject(new errors.NoPermissionError(err.message, i18n.t('errors.api.users.noPermissionToEditUser'))); + return Promise.reject(new errors.NoPermissionError({ + err: err, + context: i18n.t('errors.api.users.noPermissionToEditUser') + })); }); } @@ -192,7 +195,7 @@ users = { return {users: [result.toJSON(options)]}; } - return Promise.reject(new errors.NotFoundError(i18n.t('errors.api.users.userNotFound'))); + return Promise.reject(new errors.NotFoundError({message: i18n.t('errors.api.users.userNotFound')})); }); }, @@ -215,7 +218,10 @@ users = { options.status = 'all'; return options; }).catch(function handleError(err) { - return Promise.reject(new errors.NoPermissionError(err.message, i18n.t('errors.api.users.noPermissionToDestroyUser'))); + return Promise.reject(new errors.NoPermissionError({ + err: err, + context: i18n.t('errors.api.users.noPermissionToDestroyUser') + })); }); } @@ -236,7 +242,9 @@ users = { return dataProvider.User.destroy(options); }).return(null); }).catch(function (err) { - return Promise.reject(new errors.NoPermissionError(err.message)); + return Promise.reject(new errors.NoPermissionError({ + err: err + })); }); } @@ -271,7 +279,10 @@ users = { return canThis(options.context).edit.user(options.data.password[0].user_id).then(function permissionGranted() { return options; }).catch(function (err) { - return Promise.reject(new errors.NoPermissionError(err.message, i18n.t('errors.api.users.noPermissionToChangeUsersPwd'))); + return Promise.reject(new errors.NoPermissionError({ + err: err, + context: i18n.t('errors.api.users.noPermissionToChangeUsersPwd') + })); }); } diff --git a/core/server/api/utils.js b/core/server/api/utils.js index b9be62ee4cc..597b0dd161b 100644 --- a/core/server/api/utils.js +++ b/core/server/api/utils.js @@ -211,11 +211,15 @@ utils = { return permsPromise.then(function permissionGranted() { return options; - }).catch(errors.NoPermissionError, function handleNoPermissionError(error) { - // pimp error message - error.message = i18n.t('errors.api.utils.noPermissionToCall', {method: method, docName: docName}); - // forward error to next catch() - return Promise.reject(error); + }).catch(function handleNoPermissionError(err) { + if (err instanceof errors.NoPermissionError) { + err.message = i18n.t('errors.api.utils.noPermissionToCall', {method: method, docName: docName}); + return Promise.reject(err); + } + + return Promise.reject(new errors.GhostError({ + err: err + })); }); }; }, @@ -271,7 +275,9 @@ utils = { */ checkObject: function (object, docName, editId) { if (_.isEmpty(object) || _.isEmpty(object[docName]) || _.isEmpty(object[docName][0])) { - return Promise.reject(new errors.BadRequestError(i18n.t('errors.api.utils.noRootKeyProvided', {docName: docName}))); + return Promise.reject(new errors.BadRequestError({ + message: i18n.t('errors.api.utils.noRootKeyProvided', {docName: docName}) + })); } // convert author property to author_id to match the name in the database @@ -292,7 +298,9 @@ utils = { }); if (editId && object[docName][0].id && parseInt(editId, 10) !== parseInt(object[docName][0].id, 10)) { - return Promise.reject(new errors.BadRequestError(i18n.t('errors.api.utils.invalidIdProvided'))); + return Promise.reject(new errors.BadRequestError({ + message: i18n.t('errors.api.utils.invalidIdProvided') + })); } return Promise.resolve(object); diff --git a/core/server/apps/amp/lib/helpers/amp_content.js b/core/server/apps/amp/lib/helpers/amp_content.js index 8f74764a7f5..9259c41e67c 100644 --- a/core/server/apps/amp/lib/helpers/amp_content.js +++ b/core/server/apps/amp/lib/helpers/amp_content.js @@ -13,6 +13,8 @@ var hbs = require('express-hbs'), sanitizeHtml = require('sanitize-html'), config = require('../../../../config'), logging = require('../../../../logging'), + i18n = require('../../../../i18n'), + errors = require('../../../../errors'), makeAbsoluteUrl = require('../../../../utils/make-absolute-urls'), cheerio = require('cheerio'), amperize = new Amperize(), @@ -126,10 +128,13 @@ function getAmperizeHTML(html, post) { amperize.parse(html, function (err, res) { if (err) { if (err.src) { - err.context = 'AMP HTML couldn\'t get parsed: ' + err.src; - logging.error(err); + logging.error(new errors.GhostError({ + err: err, + context: 'AMP HTML couldn\'t get parsed: ' + err.src, + help: i18n.t('errors.apps.appWillNotBeLoaded.help') + })); } else { - logging.error(err); + logging.error(new errors.GhostError({err: err})); } // save it in cache to prevent multiple calls to Amperize until diff --git a/core/server/apps/amp/tests/router_spec.js b/core/server/apps/amp/tests/router_spec.js index 4b16fb3dbb0..79184fb7fb5 100644 --- a/core/server/apps/amp/tests/router_spec.js +++ b/core/server/apps/amp/tests/router_spec.js @@ -182,9 +182,10 @@ describe('AMP getPostData', function () { done(); }); }); + it('should return error if postlookup returns NotFoundError', function (done) { postLookupStub = sandbox.stub(); - postLookupStub.returns(new Promise.reject(new errors.NotFoundError('not found'))); + postLookupStub.returns(new Promise.reject(new errors.NotFoundError({message: 'not found'}))); ampController.__set__('postLookup', postLookupStub); diff --git a/core/server/apps/index.js b/core/server/apps/index.js index e62dccf602b..c0c5f2e3da5 100644 --- a/core/server/apps/index.js +++ b/core/server/apps/index.js @@ -2,6 +2,7 @@ var _ = require('lodash'), Promise = require('bluebird'), logging = require('../logging'), + errors = require('../errors'), api = require('../api'), loader = require('./loader'), i18n = require('../i18n'), @@ -47,10 +48,11 @@ module.exports = { appsToLoad = appsToLoad.concat(config.get('internalApps')); }); } catch (err) { - err.message = i18n.t('errors.apps.failedToParseActiveAppsSettings.error', {message: err.message}); - err.help = i18n.t('errors.apps.failedToParseActiveAppsSettings.context'); - err.context = i18n.t('errors.apps.failedToParseActiveAppsSettings.help'); - logging.error(err); + logging.error(new errors.GhostError({ + err: err, + context: i18n.t('errors.apps.failedToParseActiveAppsSettings.context'), + help: i18n.t('errors.apps.failedToParseActiveAppsSettings.help') + })); return Promise.resolve(); } @@ -87,9 +89,11 @@ module.exports = { // Extend the loadedApps onto the available apps _.extend(availableApps, loadedApps); }).catch(function (err) { - err.context = i18n.t('errors.apps.appWillNotBeLoaded.error'); - err.help = i18n.t('errors.apps.appWillNotBeLoaded.help'); - logging.error(err); + logging.error(new errors.GhostError({ + err: err, + context: i18n.t('errors.apps.appWillNotBeLoaded.error'), + help: i18n.t('errors.apps.appWillNotBeLoaded.help') + })); }); }); }, diff --git a/core/server/apps/private-blogging/index.js b/core/server/apps/private-blogging/index.js index 4279d32db2c..d206580783f 100644 --- a/core/server/apps/private-blogging/index.js +++ b/core/server/apps/private-blogging/index.js @@ -1,5 +1,6 @@ var config = require('../../config'), utils = require('../../utils'), + errors = require('../../errors'), logging = require('../../logging'), i18n = require('../../i18n'), middleware = require('./lib/middleware'), @@ -8,19 +9,19 @@ var config = require('../../config'), module.exports = { activate: function activate(ghost) { - var err, paths; + var paths; if (utils.url.getSubdir()) { paths = utils.url.getSubdir().split('/'); if (paths.pop() === config.get('routeKeywords').private) { - err = new Error(); - err.message = i18n.t('errors.config.urlCannotContainPrivateSubdir.error'); - err.context = i18n.t('errors.config.urlCannotContainPrivateSubdir.description'); - err.help = i18n.t('errors.config.urlCannotContainPrivateSubdir.help'); - logging.error(err); + logging.error(new errors.GhostError({ + message: i18n.t('errors.config.urlCannotContainPrivateSubdir.error'), + context: i18n.t('errors.config.urlCannotContainPrivateSubdir.description'), + help: i18n.t('errors.config.urlCannotContainPrivateSubdir.help') + })); - // @TODO: why? + // @TODO: why process.exit(0); } } diff --git a/core/server/apps/private-blogging/lib/middleware.js b/core/server/apps/private-blogging/lib/middleware.js index 42bc9a962e0..5db76ccccfa 100644 --- a/core/server/apps/private-blogging/lib/middleware.js +++ b/core/server/apps/private-blogging/lib/middleware.js @@ -7,7 +7,7 @@ var _ = require('lodash'), config = require('../../../config'), api = require('../../../api'), errors = require('../../../errors'), - logging = require('../../../logging'), + logging = require('../../../logging'), utils = require('../../../utils'), i18n = require('../../../i18n'), privateRoute = '/' + config.get('routeKeywords').private + '/', @@ -56,7 +56,7 @@ privateBlogging = { if (req.path.lastIndexOf('/rss/', 0) === 0 || req.path.lastIndexOf('/rss/') === req.url.length - 5 || (req.path.lastIndexOf('/sitemap', 0) === 0 && req.path.lastIndexOf('.xml') === req.path.length - 4)) { - return next(new errors.NotFoundError(i18n.t('errors.errors.pageNotFound'))); + return next(new errors.NotFoundError({message: i18n.t('errors.errors.pageNotFound')})); } else if (req.url.lastIndexOf('/robots.txt', 0) === 0) { fs.readFile(path.resolve(__dirname, '../', 'robots.txt'), function readFile(err, buf) { if (err) { @@ -146,8 +146,7 @@ privateBlogging = { ipCount = '', message = i18n.t('errors.middleware.spamprevention.tooManyAttempts'), deniedRateLimit = '', - password = req.body.password, - err; + password = req.body.password; if (password) { protectedSecurity.push({ip: remoteAddress, time: currentTime}); @@ -167,10 +166,10 @@ privateBlogging = { deniedRateLimit = (ipCount[remoteAddress] > rateProtectedAttempts); if (deniedRateLimit) { - err = new Error(); - err.message = i18n.t('errors.middleware.spamprevention.forgottenPasswordIp.error', {rfa: rateProtectedAttempts, rfp: rateProtectedPeriod}); - err.context = i18n.t('errors.middleware.spamprevention.forgottenPasswordIp.context'); - logging.error(err); + logging.error(new errors.GhostError({ + message: i18n.t('errors.middleware.spamprevention.forgottenPasswordIp.error', {rfa: rateProtectedAttempts, rfp: rateProtectedPeriod}), + context: i18n.t('errors.middleware.spamprevention.forgottenPasswordIp.context') + })); message += rateProtectedPeriod === 3600 ? i18n.t('errors.middleware.spamprevention.waitOneHour') : i18n.t('errors.middleware.spamprevention.tryAgainLater'); diff --git a/core/server/apps/subscribers/lib/router.js b/core/server/apps/subscribers/lib/router.js index 4be47769206..352710f74b2 100644 --- a/core/server/apps/subscribers/lib/router.js +++ b/core/server/apps/subscribers/lib/router.js @@ -71,7 +71,7 @@ function storeSubscriber(req, res, next) { req.body.status = 'subscribed'; if (_.isEmpty(req.body.email)) { - return next(new errors.ValidationError('Email cannot be blank.')); + return next(new errors.ValidationError({message: 'Email cannot be blank.'})); } return api.subscribers.add({subscribers: [req.body]}, {context: {external: true}}) diff --git a/core/server/auth/auth-strategies.js b/core/server/auth/auth-strategies.js index b2838932f6a..b51f0401a03 100644 --- a/core/server/auth/auth-strategies.js +++ b/core/server/auth/auth-strategies.js @@ -86,11 +86,11 @@ strategies = { invite = _invite; if (!invite) { - throw new errors.NotFoundError(i18n.t('errors.api.invites.inviteNotFound')); + throw new errors.NotFoundError({message: i18n.t('errors.api.invites.inviteNotFound')}); } if (invite.get('expires') < Date.now()) { - throw new errors.NotFoundError(i18n.t('errors.api.invites.inviteExpired')); + throw new errors.NotFoundError({message: i18n.t('errors.api.invites.inviteExpired')}); } return models.User.add({ @@ -113,7 +113,7 @@ strategies = { return models.User.findOne({slug: 'ghost-owner', status: 'all'}, options) .then(function fetchedOwner(owner) { if (!owner) { - throw new errors.NotFoundError(i18n.t('errors.models.user.userNotFound')); + throw new errors.NotFoundError({message: i18n.t('errors.models.user.userNotFound')}); } return models.User.edit({ diff --git a/core/server/auth/authenticate.js b/core/server/auth/authenticate.js index 0fc96bfbe55..4aaee3cb99c 100644 --- a/core/server/auth/authenticate.js +++ b/core/server/auth/authenticate.js @@ -44,11 +44,11 @@ authenticate = { } if (!req.body.client_id || !req.body.client_secret) { - return next(new errors.UnauthorizedError( - i18n.t('errors.middleware.auth.accessDenied')), - i18n.t('errors.middleware.auth.clientCredentialsNotProvided'), - i18n.t('errors.middleware.auth.forInformationRead', {url: 'http://api.ghost.org/docs/client-authentication'}) - ); + return next(new errors.UnauthorizedError({ + message: i18n.t('errors.middleware.auth.accessDenied'), + context: i18n.t('errors.middleware.auth.clientCredentialsNotProvided'), + help: i18n.t('errors.middleware.auth.forInformationRead', {url: 'http://api.ghost.org/docs/client-authentication'}) + })); } return passport.authenticate(['oauth2-client-password'], {session: false, failWithError: false}, @@ -62,11 +62,11 @@ authenticate = { delete req.body.client_secret; if (!client) { - return next(new errors.UnauthorizedError( - i18n.t('errors.middleware.auth.accessDenied')), - i18n.t('errors.middleware.auth.clientCredentialsNotValid'), - i18n.t('errors.middleware.auth.forInformationRead', {url: 'http://api.ghost.org/docs/client-authentication'}) - ); + return next(new errors.UnauthorizedError({ + message: i18n.t('errors.middleware.auth.accessDenied'), + context: i18n.t('errors.middleware.auth.clientCredentialsNotValid'), + help: i18n.t('errors.middleware.auth.forInformationRead', {url: 'http://api.ghost.org/docs/client-authentication'}) + })); } req.client = client; @@ -92,13 +92,17 @@ authenticate = { events.emit('user.authenticated', user); return next(null, user, info); } else if (isBearerAutorizationHeader(req)) { - return next(new errors.UnauthorizedError(i18n.t('errors.middleware.auth.accessDenied'))); + return next(new errors.UnauthorizedError({ + message: i18n.t('errors.middleware.auth.accessDenied') + })); } else if (req.client) { req.user = {id: 0}; return next(); } - return next(new errors.UnauthorizedError(i18n.t('errors.middleware.auth.accessDenied'))); + return next(new errors.UnauthorizedError({ + message: i18n.t('errors.middleware.auth.accessDenied') + })); } )(req, res, next); }, @@ -108,7 +112,7 @@ authenticate = { req.query.code = req.body.authorizationCode; if (!req.query.code) { - return next(new errors.UnauthorizedError(i18n.t('errors.middleware.auth.accessDenied'))); + return next(new errors.UnauthorizedError({message: i18n.t('errors.middleware.auth.accessDenied')})); } passport.authenticate('ghost', {session: false, failWithError: false}, function authenticate(err, user, info) { @@ -117,7 +121,7 @@ authenticate = { } if (!user) { - return next(new errors.UnauthorizedError(i18n.t('errors.middleware.auth.accessDenied'))); + return next(new errors.UnauthorizedError({message: i18n.t('errors.middleware.auth.accessDenied')})); } req.authInfo = info; diff --git a/core/server/auth/authorize.js b/core/server/auth/authorize.js index 43df39bb56e..1c7824a5fa7 100644 --- a/core/server/auth/authorize.js +++ b/core/server/auth/authorize.js @@ -10,7 +10,7 @@ authorize = { if (req.user && req.user.id) { return next(); } else { - return next(new errors.NoPermissionError(i18n.t('errors.middleware.auth.pleaseSignIn'))); + return next(new errors.NoPermissionError({message: i18n.t('errors.middleware.auth.pleaseSignIn')})); } }, @@ -22,7 +22,7 @@ authorize = { if (req.user && req.user.id) { return next(); } else { - return next(new errors.NoPermissionError(i18n.t('errors.middleware.auth.pleaseSignIn'))); + return next(new errors.NoPermissionError({message: i18n.t('errors.middleware.auth.pleaseSignIn')})); } } } diff --git a/core/server/auth/oauth.js b/core/server/auth/oauth.js index 3be66e5fab8..f5702bde2cc 100644 --- a/core/server/auth/oauth.js +++ b/core/server/auth/oauth.js @@ -12,7 +12,7 @@ function exchangeRefreshToken(client, refreshToken, scope, done) { models.Refreshtoken.findOne({token: refreshToken}) .then(function then(model) { if (!model) { - return done(new errors.NoPermissionError(i18n.t('errors.middleware.oauth.invalidRefreshToken')), false); + return done(new errors.NoPermissionError({message: i18n.t('errors.middleware.oauth.invalidRefreshToken')}), false); } else { var token = model.toJSON(), accessToken = utils.uid(191), @@ -33,7 +33,7 @@ function exchangeRefreshToken(client, refreshToken, scope, done) { return done(error, false); }); } else { - done(new errors.UnauthorizedError(i18n.t('errors.middleware.oauth.refreshTokenExpired')), false); + done(new errors.UnauthorizedError({message: i18n.t('errors.middleware.oauth.refreshTokenExpired')}), false); } } }); @@ -44,7 +44,7 @@ function exchangePassword(client, username, password, scope, done) { models.Client.findOne({slug: client.slug}) .then(function then(client) { if (!client) { - return done(new errors.NoPermissionError(i18n.t('errors.middleware.oauth.invalidClient')), false); + return done(new errors.NoPermissionError({message: i18n.t('errors.middleware.oauth.invalidClient')}), false); } // Validate the user diff --git a/core/server/auth/passport.js b/core/server/auth/passport.js index 9bb40d0db2f..8c280e19a0a 100644 --- a/core/server/auth/passport.js +++ b/core/server/auth/passport.js @@ -6,6 +6,7 @@ var ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy authStrategies = require('./auth-strategies'), utils = require('../utils'), errors = require('../errors'), + logging = require('../logging'), models = require('../models'), _private = {}; @@ -44,11 +45,13 @@ _private.registerClient = function registerClient(options) { }); }) .catch(function publicClientRegistrationError(err) { + logging.error(err); + if (retryCount < 0) { - return done(new errors.IncorrectUsage( - 'Public client registration failed: ' + err.code || err.message, - 'Please verify that the url is reachable: ' + ghostOAuth2Strategy.url - )); + return done(new errors.IncorrectUsageError({ + message: 'Public client registration failed: ' + err.code || err.message, + context: 'Please verify that the url is reachable: ' + ghostOAuth2Strategy.url + })); } console.log('RETRY: Public Client Registration...'); diff --git a/core/server/config/index.js b/core/server/config/index.js index 924339566a0..37a45b8cd29 100644 --- a/core/server/config/index.js +++ b/core/server/config/index.js @@ -4,8 +4,6 @@ var nconf = require('nconf'), packageInfo = require('../../../package.json'), env = process.env.NODE_ENV || 'development'; -nconf.set('NODE_ENV', env); - /** * command line arguments */ @@ -38,6 +36,7 @@ localUtils.makePathsAbsolute.bind(nconf)(); * @TODO: ghost-cli? */ nconf.set('ghostVersion', packageInfo.version); +nconf.set('env', env); module.exports = nconf; module.exports.isPrivacyDisabled = localUtils.isPrivacyDisabled.bind(nconf); diff --git a/core/server/controllers/admin.js b/core/server/controllers/admin.js index c1d96308327..0b37ba915ab 100644 --- a/core/server/controllers/admin.js +++ b/core/server/controllers/admin.js @@ -3,6 +3,7 @@ var debug = require('debug')('ghost:admin:controller'), Promise = require('bluebird'), api = require('../api'), config = require('../config'), + errors = require('../errors'), logging = require('../logging'), updateCheck = require('../update-check'), i18n = require('../i18n'), @@ -67,7 +68,9 @@ adminControllers = { }); }).finally(function noMatterWhat() { renderIndex(); - }).catch(logging.error); + }).catch(function (err) { + logging.error(new errors.GhostError({err: err})); + }); } }; diff --git a/core/server/controllers/frontend/channels.js b/core/server/controllers/frontend/channels.js index 1c7b8d16d2e..94d1368bf25 100644 --- a/core/server/controllers/frontend/channels.js +++ b/core/server/controllers/frontend/channels.js @@ -7,7 +7,6 @@ var express = require('express'), utils = require('../../utils'), channelConfig = require('./channel-config'), renderChannel = require('./render-channel'), - rssRouter, channelRouter; @@ -26,7 +25,7 @@ function handlePageParam(req, res, next, page) { } } else if (page < 1 || isNaN(page)) { // Nothing less than 1 is a valid page number, go straight to a 404 - return next(new errors.NotFoundError(i18n.t('errors.errors.pageNotFound'))); + return next(new errors.NotFoundError({message: i18n.t('errors.errors.pageNotFound')})); } else { // Set req.params.page to the already parsed number, and continue req.params.page = page; diff --git a/core/server/controllers/frontend/render-channel.js b/core/server/controllers/frontend/render-channel.js index c0327805215..ac39f1cd87f 100644 --- a/core/server/controllers/frontend/render-channel.js +++ b/core/server/controllers/frontend/render-channel.js @@ -38,7 +38,7 @@ function renderChannel(req, res, next) { return fetchData(channelOpts).then(function handleResult(result) { // If page is greater than number of pages we have, go straight to 404 if (pageParam > result.meta.pagination.pages) { - return next(new errors.NotFoundError(i18n.t('errors.errors.pageNotFound'))); + return next(new errors.NotFoundError({message: i18n.t('errors.errors.pageNotFound')})); } // @TODO: figure out if this can be removed, it's supposed to ensure that absolutely URLs get generated diff --git a/core/server/data/export/index.js b/core/server/data/export/index.js index a35ef2d8648..7ffbd3726ac 100644 --- a/core/server/data/export/index.js +++ b/core/server/data/export/index.js @@ -1,14 +1,13 @@ -var _ = require('lodash'), - Promise = require('bluebird'), - db = require('../../data/db'), - commands = require('../schema').commands, - versioning = require('../schema').versioning, +var _ = require('lodash'), + Promise = require('bluebird'), + db = require('../../data/db'), + commands = require('../schema').commands, + versioning = require('../schema').versioning, serverUtils = require('../../utils'), errors = require('../../errors'), logging = require('../../logging'), settings = require('../../api/settings'), i18n = require('../../i18n'), - excludedTables = ['accesstokens', 'refreshtokens', 'clients', 'client_trusted_domains'], modelOptions = {context: {internal: true}}, @@ -30,7 +29,7 @@ exportFileName = function exportFileName() { } return title + 'ghost.' + datetime + '.json'; }).catch(function (err) { - logging.error(err); + logging.error(new errors.GhostError({err: err})); return 'ghost.' + datetime + '.json'; }); }; @@ -38,7 +37,7 @@ exportFileName = function exportFileName() { getVersionAndTables = function getVersionAndTables() { var props = { version: versioning.getDatabaseVersion(), - tables: commands.getTables() + tables: commands.getTables() }; return Promise.props(props); @@ -75,7 +74,10 @@ doExport = function doExport() { return exportData; }).catch(function (err) { - return Promise.reject(new errors.InternalServerError(err.message, i18n.t('errors.data.export.errorExportingData'))); + return Promise.reject(new errors.DataExportError({ + err: err, + context: i18n.t('errors.data.export.errorExportingData') + })); }); }; diff --git a/core/server/data/import/index.js b/core/server/data/import/index.js index 274f3f5efc8..072f08f3288 100644 --- a/core/server/data/import/index.js +++ b/core/server/data/import/index.js @@ -44,7 +44,7 @@ cleanError = function cleanError(error) { value = value || 'unknown'; message = message || error.raw.message; - return new errors.DataImportError(message, offendingProperty, value); + return new errors.DataImportError({message: message, property: offendingProperty, value: value}); }; handleErrors = function handleErrors(errorList) { diff --git a/core/server/data/import/utils.js b/core/server/data/import/utils.js index 9cfef3da7c3..2f9201a7f6d 100644 --- a/core/server/data/import/utils.js +++ b/core/server/data/import/utils.js @@ -81,9 +81,11 @@ utils = { // CASE: external context userMap[userToMap] = '0'; } else { - throw new errors.DataImportError( - i18n.t('errors.data.import.utils.dataLinkedToUnknownUser', {userToMap: userToMap}), 'user.id', userToMap - ); + throw new errors.DataImportError({ + message: i18n.t('errors.data.import.utils.dataLinkedToUnknownUser', {userToMap: userToMap}), + property: 'user.id', + value: userToMap + }); } }); diff --git a/core/server/data/importer/handlers/json.js b/core/server/data/importer/handlers/json.js index d3da0aba20b..6c93192bedf 100644 --- a/core/server/data/importer/handlers/json.js +++ b/core/server/data/importer/handlers/json.js @@ -24,7 +24,7 @@ JSONHandler = { // if importData follows JSON-API format `{ db: [exportedData] }` if (_.keys(importData).length === 1) { if (!importData.db || !Array.isArray(importData.db)) { - throw new Error(i18n.t('errors.data.importer.handlers.json.invalidJsonFormat')); + throw new errors.GhostError({message: i18n.t('errors.data.importer.handlers.json.invalidJsonFormat')}); } importData = importData.db[0]; @@ -32,11 +32,11 @@ JSONHandler = { return importData; } catch (err) { - return Promise.reject(new errors.BadRequestError( - i18n.t('errors.data.importer.handlers.json.failedToParseImportJson'), - i18n.t('errors.data.importer.handlers.json.apiDbImportContent'), - i18n.t('errors.data.importer.handlers.json.checkImportJsonIsValid') - )); + return Promise.reject(new errors.BadRequestError({ + err: err, + context: i18n.t('errors.data.importer.handlers.json.apiDbImportContent'), + help: i18n.t('errors.data.importer.handlers.json.checkImportJsonIsValid') + })); } }); } diff --git a/core/server/data/importer/index.js b/core/server/data/importer/index.js index 77157ae388d..a4c48167157 100644 --- a/core/server/data/importer/index.js +++ b/core/server/data/importer/index.js @@ -109,9 +109,11 @@ _.extend(ImportManager.prototype, { _.each(filesToDelete, function (fileToDelete) { fs.remove(fileToDelete, function (err) { if (err) { - err.context = i18n.t('errors.data.importer.index.couldNotCleanUpFile.error'); - err.help = i18n.t('errors.data.importer.index.couldNotCleanUpFile.context'); - logging.error(err); + logging.error(new errors.GhostError({ + err: err, + context: i18n.t('errors.data.importer.index.couldNotCleanUpFile.error'), + help: i18n.t('errors.data.importer.index.couldNotCleanUpFile.context') + })); } }); }); @@ -150,9 +152,7 @@ _.extend(ImportManager.prototype, { // This is a temporary extra message for the old format roon export which doesn't work with Ghost if (oldRoonMatches.length > 0) { - throw new errors.UnsupportedMediaTypeError( - i18n.t('errors.data.importer.index.unsupportedRoonExport') - ); + throw new errors.UnsupportedMediaTypeError({message: i18n.t('errors.data.importer.index.unsupportedRoonExport')}); } // If this folder contains importable files or a content or images directory @@ -161,12 +161,10 @@ _.extend(ImportManager.prototype, { } if (extMatchesAll.length < 1) { - throw new errors.UnsupportedMediaTypeError( - i18n.t('errors.data.importer.index.noContentToImport')); + throw new errors.UnsupportedMediaTypeError({message: i18n.t('errors.data.importer.index.noContentToImport')}); } - throw new errors.UnsupportedMediaTypeError( - i18n.t('errors.data.importer.index.invalidZipStructure')); + throw new errors.UnsupportedMediaTypeError({message: i18n.t('errors.data.importer.index.invalidZipStructure')}); }, /** * Use the extract module to extract the given zip file to a temp directory & return the temp directory path @@ -213,8 +211,9 @@ _.extend(ImportManager.prototype, { this.getExtensionGlob(this.getExtensions(), ALL_DIRS), {cwd: directory} ); if (extMatchesAll.length < 1 || extMatchesAll[0].split('/') < 1) { - throw new errors.ValidationError(i18n.t('errors.data.importer.index.invalidZipFileBaseDirectory')); + throw new errors.ValidationError({message: i18n.t('errors.data.importer.index.invalidZipFileBaseDirectory')}); } + return extMatchesAll[0].split('/')[0]; }, /** @@ -240,9 +239,9 @@ _.extend(ImportManager.prototype, { _.each(self.handlers, function (handler) { if (importData.hasOwnProperty(handler.type)) { // This limitation is here to reduce the complexity of the importer for now - return Promise.reject(new errors.UnsupportedMediaTypeError( - i18n.t('errors.data.importer.index.zipContainsMultipleDataFormats') - )); + return Promise.reject(new errors.UnsupportedMediaTypeError({ + message: i18n.t('errors.data.importer.index.zipContainsMultipleDataFormats') + })); } var files = self.getFilesFromZip(handler, zipDirectory); @@ -257,9 +256,9 @@ _.extend(ImportManager.prototype, { }); if (ops.length === 0) { - return Promise.reject(new errors.UnsupportedMediaTypeError( - i18n.t('errors.data.importer.index.noContentToImport') - )); + return Promise.reject(new errors.UnsupportedMediaTypeError({ + message: i18n.t('errors.data.importer.index.noContentToImport') + })); } return sequence(ops).then(function () { diff --git a/core/server/data/migration/populate.js b/core/server/data/migration/populate.js index 5e1d48f7502..461846b373d 100644 --- a/core/server/data/migration/populate.js +++ b/core/server/data/migration/populate.js @@ -49,7 +49,7 @@ populate = function populate(options) { }); }).catch(function populateDatabaseError(err) { logger.warn('rolling back...'); - return Promise.reject(new errors.InternalServerError('Unable to populate database: ' + err.message)); + return Promise.reject(new errors.GhostError({err: err, context: 'Unable to populate database!'})); }); }; diff --git a/core/server/data/migration/update.js b/core/server/data/migration/update.js index fd652f2bebb..2f5f238a519 100644 --- a/core/server/data/migration/update.js +++ b/core/server/data/migration/update.js @@ -124,11 +124,13 @@ isDatabaseOutOfDate = function isDatabaseOutOfDate(options) { // CASE: current database version is lower then we support if (fromVersion < versioning.canMigrateFromVersion) { - return {error: new errors.DatabaseVersion( - i18n.t('errors.data.versioning.index.cannotMigrate.error'), - i18n.t('errors.data.versioning.index.cannotMigrate.context'), - i18n.t('common.seeLinkForInstructions', {link: 'http://support.ghost.org/how-to-upgrade/'}) - )}; + return { + error: new errors.DatabaseVersionError({ + message: i18n.t('errors.data.versioning.index.cannotMigrate.error'), + context: i18n.t('errors.data.versioning.index.cannotMigrate.context'), + help: i18n.t('common.seeLinkForInstructions', {link: 'http://support.ghost.org/how-to-upgrade/'}) + }) + }; } // CASE: the database exists but is out of date else if (fromVersion < toVersion || forceMigration) { @@ -140,7 +142,7 @@ isDatabaseOutOfDate = function isDatabaseOutOfDate(options) { } // CASE: we don't understand the version else { - return {error: new errors.DatabaseVersion(i18n.t('errors.data.versioning.index.dbVersionNotRecognized'))}; + return {error: new errors.DatabaseVersionError({message: i18n.t('errors.data.versioning.index.dbVersionNotRecognized')})}; } }; diff --git a/core/server/data/schema/bootup.js b/core/server/data/schema/bootup.js index e472f85beb3..e17b3deff93 100644 --- a/core/server/data/schema/bootup.js +++ b/core/server/data/schema/bootup.js @@ -9,16 +9,16 @@ module.exports = function bootUp() { .then(function successHandler(result) { if (!/^alpha/.test(result)) { // This database was not created with Ghost alpha, and is not compatible - throw new errors.DatabaseVersion( - 'Your database version is not compatible with Ghost 1.0.0 Alpha (master branch)', - 'Want to keep your DB? Use Ghost < 1.0.0 or the "stable" branch. Otherwise please delete your DB and restart Ghost', - 'More information on the Ghost 1.0.0 Alpha at https://support.ghost.org/v1-0-alpha' - ); + throw new errors.DatabaseVersionError({ + message: 'Your database version is not compatible with Ghost 1.0.0 Alpha (master branch)', + context: 'Want to keep your DB? Use Ghost < 1.0.0 or the "stable" branch. Otherwise please delete your DB and restart Ghost', + help: 'More information on the Ghost 1.0.0 Alpha at https://support.ghost.org/v1-0-alpha' + }); } }, // We don't use .catch here, as it would catch the error from the successHandler function errorHandler(err) { - if (err instanceof errors.DatabaseNotPopulated) { + if (err instanceof errors.DatabaseNotPopulatedError) { return populate(); } diff --git a/core/server/data/schema/versioning.js b/core/server/data/schema/versioning.js index 57b2a194a0e..484867859a5 100644 --- a/core/server/data/schema/versioning.js +++ b/core/server/data/schema/versioning.js @@ -35,7 +35,7 @@ function getDatabaseVersion() { }); } - return Promise.reject(new errors.DatabaseNotPopulated(i18n.t('errors.data.versioning.index.databaseNotPopulated'))); + return Promise.reject(new errors.DatabaseNotPopulatedError({message: i18n.t('errors.data.versioning.index.databaseNotPopulated')})); }); } diff --git a/core/server/data/slack/index.js b/core/server/data/slack/index.js index a813ec3dda8..2dbce1b49aa 100644 --- a/core/server/data/slack/index.js +++ b/core/server/data/slack/index.js @@ -1,6 +1,8 @@ var https = require('https'), url = require('url'), Promise = require('bluebird'), + errors = require('../../errors'), + logging = require('../../logging'), utils = require('../../utils'), events = require('../../events'), logging = require('../../logging'), @@ -32,9 +34,11 @@ function makeRequest(reqOptions, reqPayload) { req.write(reqPayload); req.on('error', function (err) { - err.context = i18n.t('errors.data.xml.xmlrpc.pingUpdateFailed.error'); - err.help = i18n.t('errors.data.xml.xmlrpc.pingUpdateFailed.help', {url: 'http://support.ghost.org'}); - logging.error(err); + logging.error(new errors.GhostError({ + err: err, + context: i18n.t('errors.data.xml.xmlrpc.pingUpdateFailed.error'), + help: i18n.t('errors.data.xml.xmlrpc.pingUpdateFailed.help', {url: 'http://support.ghost.org'}) + })); }); req.end(); diff --git a/core/server/data/validation/index.js b/core/server/data/validation/index.js index 4f4c4452bab..a3dc623e8b9 100644 --- a/core/server/data/validation/index.js +++ b/core/server/data/validation/index.js @@ -65,7 +65,7 @@ validateSchema = function validateSchema(tableName, model) { && schema[tableName][columnKey].nullable !== true) { if (validator.empty(strVal)) { message = i18n.t('notices.data.validation.index.valueCannotBeBlank', {tableName: tableName, columnKey: columnKey}); - validationErrors.push(new errors.ValidationError(message, tableName + '.' + columnKey)); + validationErrors.push(new errors.ValidationError({message: message, context: tableName + '.' + columnKey})); } } @@ -74,7 +74,7 @@ validateSchema = function validateSchema(tableName, model) { && schema[tableName][columnKey].type === 'bool') { if (!(validator.isBoolean(strVal) || validator.empty(strVal))) { message = i18n.t('notices.data.validation.index.valueMustBeBoolean', {tableName: tableName, columnKey: columnKey}); - validationErrors.push(new errors.ValidationError(message, tableName + '.' + columnKey)); + validationErrors.push(new errors.ValidationError({message: message, context: tableName + '.' + columnKey})); } } @@ -85,7 +85,7 @@ validateSchema = function validateSchema(tableName, model) { if (!validator.isLength(strVal, 0, schema[tableName][columnKey].maxlength)) { message = i18n.t('notices.data.validation.index.valueExceedsMaxLength', {tableName: tableName, columnKey: columnKey, maxlength: schema[tableName][columnKey].maxlength}); - validationErrors.push(new errors.ValidationError(message, tableName + '.' + columnKey)); + validationErrors.push(new errors.ValidationError({message: message, context: tableName + '.' + columnKey})); } } @@ -98,7 +98,7 @@ validateSchema = function validateSchema(tableName, model) { if (schema[tableName][columnKey].hasOwnProperty('type')) { if (schema[tableName][columnKey].type === 'integer' && !validator.isInt(strVal)) { message = i18n.t('notices.data.validation.index.valueIsNotInteger', {tableName: tableName, columnKey: columnKey}); - validationErrors.push(new errors.ValidationError(message, tableName + '.' + columnKey)); + validationErrors.push(new errors.ValidationError({message: message, context: tableName + '.' + columnKey})); } } } @@ -146,7 +146,7 @@ validateActiveTheme = function validateActiveTheme(themeName) { return availableThemes.then(function then(themes) { if (!themes.hasOwnProperty(themeName)) { - return Promise.reject(new errors.ValidationError(i18n.t('notices.data.validation.index.themeCannotBeActivated', {themeName: themeName}), 'activeTheme')); + return Promise.reject(new errors.ValidationError({message: i18n.t('notices.data.validation.index.themeCannotBeActivated', {themeName: themeName}), context: 'activeTheme'})); } }); }; @@ -186,8 +186,9 @@ validate = function validate(value, key, validations) { // equivalent of validator.isSomething(option1, option2) if (validator[validationName].apply(validator, validationOptions) !== goodResult) { - validationErrors.push(new errors.ValidationError(i18n.t('notices.data.validation.index.validationFailed', - {validationName: validationName, key: key}))); + validationErrors.push(new errors.ValidationError({ + message: i18n.t('notices.data.validation.index.validationFailed', {validationName: validationName, key: key}) + })); } validationOptions.shift(); diff --git a/core/server/data/xml/rss/index.js b/core/server/data/xml/rss/index.js index fb8daffc1e9..b584f056860 100644 --- a/core/server/data/xml/rss/index.js +++ b/core/server/data/xml/rss/index.js @@ -174,7 +174,7 @@ generate = function generate(req, res, next) { // If page is greater than number of pages we have, redirect to last page if (pageParam > maxPage) { - return next(new errors.NotFoundError(i18n.t('errors.errors.pageNotFound'))); + return next(new errors.NotFoundError({message: i18n.t('errors.errors.pageNotFound')})); } data.version = res.locals.safeVersion; diff --git a/core/server/data/xml/xmlrpc.js b/core/server/data/xml/xmlrpc.js index f5e7ea744a2..d25c3d6947d 100644 --- a/core/server/data/xml/xmlrpc.js +++ b/core/server/data/xml/xmlrpc.js @@ -3,6 +3,7 @@ var _ = require('lodash'), xml = require('xml'), config = require('../../config'), utils = require('../../utils'), + errors = require('../../errors'), logging = require('../../logging'), events = require('../../events'), i18n = require('../../i18n'), @@ -67,11 +68,15 @@ function ping(post) { req = http.request(options); req.write(pingXML); + req.on('error', function handleError(err) { - err.context = i18n.t('errors.data.xml.xmlrpc.pingUpdateFailed.error'); - err.help = i18n.t('errors.data.xml.xmlrpc.pingUpdateFailed.help', {url: 'http://support.ghost.org'}); - logging.error(err); + logging.error(new errors.GhostError({ + err: err, + context: i18n.t('errors.data.xml.xmlrpc.pingUpdateFailed.error'), + help: i18n.t('errors.data.xml.xmlrpc.pingUpdateFailed.help', {url: 'http://support.ghost.org'}) + })); }); + req.end(); }); } diff --git a/core/server/errors.js b/core/server/errors.js new file mode 100644 index 00000000000..583a35c45e2 --- /dev/null +++ b/core/server/errors.js @@ -0,0 +1,162 @@ +var _ = require('lodash'), + util = require('util'); + +function GhostError(options) { + options = options || {}; + + if (_.isString(options)) { + throw new Error('Please instantiate Errors with the option pattern. e.g. new errors.GhostError({message: ...})'); + } + + Error.call(this); + Error.captureStackTrace(this, GhostError); + + /** + * defaults + * @TODO: I'd like to add the usage of an individual ID to errors, as we have in ignition + */ + this.statusCode = 500; + this.errorType = 'InternalServerError'; + this.level = 'normal'; + + /** + * custom overrides + */ + this.statusCode = options.statusCode || this.statusCode; + this.level = options.level || this.level; + this.context = options.context || this.context; + this.help = options.help || this.help; + this.errorType = this.name = options.errorType || this.errorType; + this.errorDetails = options.errorDetails; + + // @TODO: ? + this.property = options.property; + this.value = options.value; + + this.message = options.message; + this.hideStack = options.hideStack; + + // error to inherit from, override! + if (options.err) { + this.message = options.err.message; + this.stack = options.err.stack; + } +} + +// jscs:disable +var errors = { + DataExportError: function DataExportError(options) { + GhostError.call(this, _.merge({ + statusCode: 500, + errorType: 'DataExportError' + }, options)); + }, + DataImportError: function DataImportError(options) { + GhostError.call(this, _.merge({ + statusCode: 500, + errorType: 'DataImportError' + }, options)); + }, + IncorrectUsageError: function IncorrectUsageError(options) { + GhostError.call(this, _.merge({ + statusCode: 400, + level: 'critical', + errorType: 'IncorrectUsageError' + }, options)); + }, + NotFoundError: function NotFoundError(options) { + GhostError.call(this, _.merge({ + statusCode: 404, + errorType: 'NotFoundError' + }, options)); + }, + BadRequestError: function BadRequestError(options) { + GhostError.call(this, _.merge({ + statusCode: 400, + errorType: 'BadRequestError' + }, options)); + }, + DatabaseVersionError: function DatabaseVersionError(options) { + GhostError.call(this, _.merge({ + hideStack: true, + statusCode: 500, + errorType: 'DatabaseVersionError' + }, options)); + }, + DatabaseNotPopulatedError: function DatabaseNotPopulatedError(options) { + GhostError.call(this, _.merge({ + statusCode: 500, + errorType: 'DatabaseNotPopulatedError' + }, options)); + }, + UnauthorizedError: function UnauthorizedError(options) { + GhostError.call(this, _.merge({ + statusCode: 401, + errorType: 'UnauthorizedError' + }, options)); + }, + NoPermissionError: function NoPermissionError(options) { + GhostError.call(this, _.merge({ + statusCode: 403, + errorType: 'NoPermissionError' + }, options)); + }, + ValidationError: function ValidationError(options) { + GhostError.call(this, _.merge({ + statusCode: 422, + errorType: 'ValidationError' + }, options)); + }, + UnsupportedMediaTypeError: function UnsupportedMediaTypeError(options) { + GhostError.call(this, _.merge({ + statusCode: 415, + errorType: 'UnsupportedMediaTypeError' + }, options)); + }, + VersionMismatchError: function VersionMismatchError(options) { + GhostError.call(this, _.merge({ + statusCode: 400, + errorType: 'VersionMismatchError' + }, options)); + }, + TokenRevocationError: function TokenRevocationError(options) { + GhostError.call(this, _.merge({ + statusCode: 503, + errorType: 'TokenRevocationError' + }, options)); + }, + EmailError: function EmailError(options) { + GhostError.call(this, _.merge({ + statusCode: 500, + errorType: 'EmailError' + }, options)); + }, + TooManyRequestsError: function TooManyRequestsError(options) { + GhostError.call(this, _.merge({ + statusCode: 429, + errorType: 'TooManyRequestsError' + }, options)); + }, + MaintenanceError: function MaintenanceError(options) { + GhostError.call(this, _.merge({ + statusCode: 503, + errorType: 'MaintenanceError' + }, options)); + }, + ThemeValidationError: function ThemeValidationError(options) { + GhostError.call(this, _.merge({ + statusCode: 422, + errorType: 'ThemeValidationError', + errorDetails: {} + }, options)); + } +}; + +_.each(errors, function (error) { + util.inherits(error, GhostError); +}); + +module.exports = errors; +module.exports.GhostError = GhostError; + + diff --git a/core/server/errors/bad-request-error.js b/core/server/errors/bad-request-error.js deleted file mode 100644 index caba83c4d20..00000000000 --- a/core/server/errors/bad-request-error.js +++ /dev/null @@ -1,16 +0,0 @@ -// # Bad request error -// Custom error class with status code and type prefilled. - -function BadRequestError(message, context, help) { - this.message = message; - this.stack = new Error().stack; - this.statusCode = 400; - this.errorType = this.name; - this.context = context; - this.help = help; -} - -BadRequestError.prototype = Object.create(Error.prototype); -BadRequestError.prototype.name = 'BadRequestError'; - -module.exports = BadRequestError; diff --git a/core/server/errors/data-import-error.js b/core/server/errors/data-import-error.js deleted file mode 100644 index 48343435b55..00000000000 --- a/core/server/errors/data-import-error.js +++ /dev/null @@ -1,16 +0,0 @@ -// # Data import error -// Custom error class with status code and type prefilled. - -function DataImportError(message, offendingProperty, value) { - this.message = message; - this.stack = new Error().stack; - this.statusCode = 500; - this.errorType = this.name; - this.property = offendingProperty || undefined; - this.value = value || undefined; -} - -DataImportError.prototype = Object.create(Error.prototype); -DataImportError.prototype.name = 'DataImportError'; - -module.exports = DataImportError; diff --git a/core/server/errors/database-not-populated.js b/core/server/errors/database-not-populated.js deleted file mode 100644 index 30030c885f9..00000000000 --- a/core/server/errors/database-not-populated.js +++ /dev/null @@ -1,11 +0,0 @@ -function DatabaseNotPopulated(message) { - this.message = message; - this.stack = new Error().stack; - this.statusCode = 500; - this.errorType = this.name; -} - -DatabaseNotPopulated.prototype = Object.create(Error.prototype); -DatabaseNotPopulated.prototype.name = 'DatabaseNotPopulated'; - -module.exports = DatabaseNotPopulated; diff --git a/core/server/errors/database-version.js b/core/server/errors/database-version.js deleted file mode 100644 index 4b7156d355b..00000000000 --- a/core/server/errors/database-version.js +++ /dev/null @@ -1,13 +0,0 @@ -function DatabaseVersion(message, context, help) { - this.message = message; - this.stack = new Error().stack; - this.statusCode = 500; - this.errorType = this.name; - this.context = context; - this.help = help; -} - -DatabaseVersion.prototype = Object.create(Error.prototype); -DatabaseVersion.prototype.name = 'DatabaseVersion'; - -module.exports = DatabaseVersion; diff --git a/core/server/errors/email-error.js b/core/server/errors/email-error.js deleted file mode 100644 index a9e8632245f..00000000000 --- a/core/server/errors/email-error.js +++ /dev/null @@ -1,16 +0,0 @@ -// # Email error -// Custom error class with status code and type prefilled. - -function EmailError(message, context, help) { - this.message = message; - this.stack = new Error().stack; - this.statusCode = 500; - this.errorType = this.name; - this.context = context; - this.help = help; -} - -EmailError.prototype = Object.create(Error.prototype); -EmailError.prototype.name = 'EmailError'; - -module.exports = EmailError; diff --git a/core/server/errors/incorrect-usage.js b/core/server/errors/incorrect-usage.js deleted file mode 100644 index 24a851a3e9c..00000000000 --- a/core/server/errors/incorrect-usage.js +++ /dev/null @@ -1,11 +0,0 @@ -function IncorrectUsage(message, context) { - this.name = 'IncorrectUsage'; - this.stack = new Error().stack; - this.statusCode = 400; - this.errorType = this.name; - this.message = message; - this.context = context; -} - -IncorrectUsage.prototype = Object.create(Error.prototype); -module.exports = IncorrectUsage; diff --git a/core/server/errors/index.js b/core/server/errors/index.js deleted file mode 100644 index 7db1bcb74fa..00000000000 --- a/core/server/errors/index.js +++ /dev/null @@ -1,41 +0,0 @@ -// # Errors -/*jslint regexp: true */ -var NotFoundError = require('./not-found-error'), - BadRequestError = require('./bad-request-error'), - InternalServerError = require('./internal-server-error'), - NoPermissionError = require('./no-permission-error'), - MethodNotAllowedError = require('./method-not-allowed-error'), - RequestEntityTooLargeError = require('./request-too-large-error'), - UnauthorizedError = require('./unauthorized-error'), - ValidationError = require('./validation-error'), - ThemeValidationError = require('./theme-validation-error'), - UnsupportedMediaTypeError = require('./unsupported-media-type-error'), - EmailError = require('./email-error'), - DataImportError = require('./data-import-error'), - TooManyRequestsError = require('./too-many-requests-error'), - TokenRevocationError = require('./token-revocation-error'), - VersionMismatchError = require('./version-mismatch-error'), - IncorrectUsage = require('./incorrect-usage'), - Maintenance = require('./maintenance'), - DatabaseNotPopulated = require('./database-not-populated'), - DatabaseVersion = require('./database-version'); - -module.exports.NotFoundError = NotFoundError; -module.exports.BadRequestError = BadRequestError; -module.exports.InternalServerError = InternalServerError; -module.exports.NoPermissionError = NoPermissionError; -module.exports.UnauthorizedError = UnauthorizedError; -module.exports.ValidationError = ValidationError; -module.exports.ThemeValidationError = ThemeValidationError; -module.exports.RequestEntityTooLargeError = RequestEntityTooLargeError; -module.exports.UnsupportedMediaTypeError = UnsupportedMediaTypeError; -module.exports.EmailError = EmailError; -module.exports.DataImportError = DataImportError; -module.exports.MethodNotAllowedError = MethodNotAllowedError; -module.exports.TooManyRequestsError = TooManyRequestsError; -module.exports.TokenRevocationError = TokenRevocationError; -module.exports.VersionMismatchError = VersionMismatchError; -module.exports.IncorrectUsage = IncorrectUsage; -module.exports.Maintenance = Maintenance; -module.exports.DatabaseNotPopulated = DatabaseNotPopulated; -module.exports.DatabaseVersion = DatabaseVersion; diff --git a/core/server/errors/internal-server-error.js b/core/server/errors/internal-server-error.js deleted file mode 100644 index 2b67acbc447..00000000000 --- a/core/server/errors/internal-server-error.js +++ /dev/null @@ -1,16 +0,0 @@ -// # Internal Server Error -// Custom error class with status code and type prefilled. - -function InternalServerError(message, context, help) { - this.message = message; - this.stack = new Error().stack; - this.statusCode = 500; - this.errorType = this.name; - this.context = context; - this.help = help; -} - -InternalServerError.prototype = Object.create(Error.prototype); -InternalServerError.prototype.name = 'InternalServerError'; - -module.exports = InternalServerError; diff --git a/core/server/errors/maintenance.js b/core/server/errors/maintenance.js deleted file mode 100644 index 309f61fcc83..00000000000 --- a/core/server/errors/maintenance.js +++ /dev/null @@ -1,11 +0,0 @@ -function Maintenance(message) { - this.message = message; - this.stack = new Error().stack; - this.statusCode = 503; - this.errorType = this.name; -} - -Maintenance.prototype = Object.create(Error.prototype); -Maintenance.prototype.name = 'Maintenance'; - -module.exports = Maintenance; diff --git a/core/server/errors/method-not-allowed-error.js b/core/server/errors/method-not-allowed-error.js deleted file mode 100644 index 8efe5e375f0..00000000000 --- a/core/server/errors/method-not-allowed-error.js +++ /dev/null @@ -1,14 +0,0 @@ -// # Not found error -// Custom error class with status code and type prefilled. - -function MethodNotAllowedError(message) { - this.message = message; - this.stack = new Error().stack; - this.statusCode = 405; - this.errorType = this.name; -} - -MethodNotAllowedError.prototype = Object.create(Error.prototype); -MethodNotAllowedError.prototype.name = 'MethodNotAllowedError'; - -module.exports = MethodNotAllowedError; diff --git a/core/server/errors/no-permission-error.js b/core/server/errors/no-permission-error.js deleted file mode 100644 index c60dacce1b3..00000000000 --- a/core/server/errors/no-permission-error.js +++ /dev/null @@ -1,14 +0,0 @@ -// # No Permission Error -// Custom error class with status code and type prefilled. - -function NoPermissionError(message) { - this.message = message; - this.stack = new Error().stack; - this.statusCode = 403; - this.errorType = this.name; -} - -NoPermissionError.prototype = Object.create(Error.prototype); -NoPermissionError.prototype.name = 'NoPermissionError'; - -module.exports = NoPermissionError; diff --git a/core/server/errors/not-found-error.js b/core/server/errors/not-found-error.js deleted file mode 100644 index ee5bd393fe4..00000000000 --- a/core/server/errors/not-found-error.js +++ /dev/null @@ -1,14 +0,0 @@ -// # Not found error -// Custom error class with status code and type prefilled. - -function NotFoundError(message) { - this.message = message; - this.stack = new Error().stack; - this.statusCode = 404; - this.errorType = this.name; -} - -NotFoundError.prototype = Object.create(Error.prototype); -NotFoundError.prototype.name = 'NotFoundError'; - -module.exports = NotFoundError; diff --git a/core/server/errors/request-too-large-error.js b/core/server/errors/request-too-large-error.js deleted file mode 100644 index 71c2848a72e..00000000000 --- a/core/server/errors/request-too-large-error.js +++ /dev/null @@ -1,14 +0,0 @@ -// # Request Entity Too Large Error -// Custom error class with status code and type prefilled. - -function RequestEntityTooLargeError(message) { - this.message = message; - this.stack = new Error().stack; - this.statusCode = 413; - this.errorType = this.name; -} - -RequestEntityTooLargeError.prototype = Object.create(Error.prototype); -RequestEntityTooLargeError.prototype.name = 'RequestEntityTooLargeError'; - -module.exports = RequestEntityTooLargeError; diff --git a/core/server/errors/theme-validation-error.js b/core/server/errors/theme-validation-error.js deleted file mode 100644 index 93ddd7c820d..00000000000 --- a/core/server/errors/theme-validation-error.js +++ /dev/null @@ -1,18 +0,0 @@ -// # Theme Validation Error -// Custom error class with status code and type prefilled. - -function ThemeValidationError(message, details) { - this.message = message; - this.stack = new Error().stack; - this.statusCode = 422; - if (details) { - this.errorDetails = details; - } - - this.errorType = this.name; -} - -ThemeValidationError.prototype = Object.create(Error.prototype); -ThemeValidationError.prototype.name = 'ThemeValidationError'; - -module.exports = ThemeValidationError; diff --git a/core/server/errors/token-revocation-error.js b/core/server/errors/token-revocation-error.js deleted file mode 100644 index 445db3732d6..00000000000 --- a/core/server/errors/token-revocation-error.js +++ /dev/null @@ -1,14 +0,0 @@ -// # Token Revocation ERror -// Custom error class with status code and type prefilled. - -function TokenRevocationError(message) { - this.message = message; - this.stack = new Error().stack; - this.statusCode = 503; - this.errorType = this.name; -} - -TokenRevocationError.prototype = Object.create(Error.prototype); -TokenRevocationError.prototype.name = 'TokenRevocationError'; - -module.exports = TokenRevocationError; diff --git a/core/server/errors/too-many-requests-error.js b/core/server/errors/too-many-requests-error.js deleted file mode 100644 index 875ae37436d..00000000000 --- a/core/server/errors/too-many-requests-error.js +++ /dev/null @@ -1,16 +0,0 @@ -// # Too Many Requests Error -// Custom error class with status code and type prefilled. - -function TooManyRequestsError(message, context, help) { - this.message = message; - this.stack = new Error().stack; - this.statusCode = 429; - this.errorType = this.name; - this.context = context; - this.help = help; -} - -TooManyRequestsError.prototype = Object.create(Error.prototype); -TooManyRequestsError.prototype.name = 'TooManyRequestsError'; - -module.exports = TooManyRequestsError; diff --git a/core/server/errors/unauthorized-error.js b/core/server/errors/unauthorized-error.js deleted file mode 100644 index 7473bfa7c3b..00000000000 --- a/core/server/errors/unauthorized-error.js +++ /dev/null @@ -1,16 +0,0 @@ -// # Unauthorized error -// Custom error class with status code and type prefilled. - -function UnauthorizedError(message, context, help) { - this.message = message; - this.stack = new Error().stack; - this.statusCode = 401; - this.errorType = this.name; - this.context = context; - this.help = help; -} - -UnauthorizedError.prototype = Object.create(Error.prototype); -UnauthorizedError.prototype.name = 'UnauthorizedError'; - -module.exports = UnauthorizedError; diff --git a/core/server/errors/unsupported-media-type-error.js b/core/server/errors/unsupported-media-type-error.js deleted file mode 100644 index 1d16691c4ff..00000000000 --- a/core/server/errors/unsupported-media-type-error.js +++ /dev/null @@ -1,14 +0,0 @@ -// # Unsupported Media Type -// Custom error class with status code and type prefilled. - -function UnsupportedMediaTypeError(message) { - this.message = message; - this.stack = new Error().stack; - this.statusCode = 415; - this.errorType = this.name; -} - -UnsupportedMediaTypeError.prototype = Object.create(Error.prototype); -UnsupportedMediaTypeError.prototype.name = 'UnsupportedMediaTypeError'; - -module.exports = UnsupportedMediaTypeError; diff --git a/core/server/errors/validation-error.js b/core/server/errors/validation-error.js deleted file mode 100644 index 5f21021098f..00000000000 --- a/core/server/errors/validation-error.js +++ /dev/null @@ -1,19 +0,0 @@ -// # Validation Error -// Custom error class with status code and type prefilled. - -function ValidationError(message, offendingProperty, context, help) { - this.message = message; - this.stack = new Error().stack; - this.statusCode = 422; - if (offendingProperty) { - this.property = offendingProperty; - } - this.errorType = this.name; - this.context = context; - this.help = help; -} - -ValidationError.prototype = Object.create(Error.prototype); -ValidationError.prototype.name = 'ValidationError'; - -module.exports = ValidationError; diff --git a/core/server/errors/version-mismatch-error.js b/core/server/errors/version-mismatch-error.js deleted file mode 100644 index 7ab49560ff3..00000000000 --- a/core/server/errors/version-mismatch-error.js +++ /dev/null @@ -1,14 +0,0 @@ -// # Version mismatch error -// Custom error class with status code and type prefilled. - -function VersionMismatchError(message) { - this.message = message; - this.stack = new Error().stack; - this.statusCode = 400; - this.errorType = this.name; -} - -VersionMismatchError.prototype = Object.create(Error.prototype); -VersionMismatchError.prototype.name = 'VersionMismatchError'; - -module.exports = VersionMismatchError; diff --git a/core/server/ghost-server.js b/core/server/ghost-server.js index 634a49e64db..566c958bcc2 100644 --- a/core/server/ghost-server.js +++ b/core/server/ghost-server.js @@ -76,17 +76,17 @@ GhostServer.prototype.start = function (externalApp) { self.httpServer.on('error', function (error) { if (error.errno === 'EADDRINUSE') { - logging.error(new errors.InternalServerError( - i18n.t('errors.httpServer.addressInUse.error'), - i18n.t('errors.httpServer.addressInUse.context', {port: config.get('server').port}), - i18n.t('errors.httpServer.addressInUse.help') - )); + logging.error(new errors.GhostError({ + message: i18n.t('errors.httpServer.addressInUse.error'), + context: i18n.t('errors.httpServer.addressInUse.context', {port: config.get('server').port}), + help: i18n.t('errors.httpServer.addressInUse.help') + })); } else { - logging.error(new errors.InternalServerError( - i18n.t('errors.httpServer.otherError.error', {errorNumber: error.errno}), - i18n.t('errors.httpServer.otherError.context'), - i18n.t('errors.httpServer.otherError.help') - )); + logging.error(new errors.GhostError({ + message: i18n.t('errors.httpServer.otherError.error', {errorNumber: error.errno}), + context: i18n.t('errors.httpServer.otherError.context'), + help: i18n.t('errors.httpServer.otherError.help') + })); } process.exit(-1); diff --git a/core/server/helpers/get.js b/core/server/helpers/get.js index d76c858e190..aa483f59643 100644 --- a/core/server/helpers/get.js +++ b/core/server/helpers/get.js @@ -4,6 +4,7 @@ var _ = require('lodash'), hbs = require('express-hbs'), Promise = require('bluebird'), + errors = require('../errors'), logging = require('../logging'), api = require('../api'), jsonpath = require('jsonpath'), @@ -144,17 +145,18 @@ get = function get(resource, options) { }; module.exports = function getWithLabs(resource, options) { - var self = this, - err; + var self = this, err; if (labs.isSet('publicAPI') === true) { // get helper is active return get.call(self, resource, options); } else { - err = new Error(); - err.message = i18n.t('warnings.helpers.get.helperNotAvailable'); - err.context = i18n.t('warnings.helpers.get.apiMustBeEnabled'); - err.help = i18n.t('warnings.helpers.get.seeLink', {url: 'http://support.ghost.org/public-api-beta'}); + err = new errors.GhostError({ + message: i18n.t('warnings.helpers.get.helperNotAvailable'), + context: i18n.t('warnings.helpers.get.apiMustBeEnabled'), + help: i18n.t('warnings.helpers.get.seeLink', {url: 'http://support.ghost.org/public-api-beta'}) + }); + logging.error(err); return Promise.resolve(function noGetHelper() { diff --git a/core/server/helpers/index.js b/core/server/helpers/index.js index c27e5933a85..7e4aad47fc7 100644 --- a/core/server/helpers/index.js +++ b/core/server/helpers/index.js @@ -1,23 +1,23 @@ -var hbs = require('express-hbs'), - Promise = require('bluebird'), - errors = require('../errors'), - logging = require('../logging'), - utils = require('./utils'), - i18n = require('../i18n'), - coreHelpers = {}, +var hbs = require('express-hbs'), + Promise = require('bluebird'), + errors = require('../errors'), + logging = require('../logging'), + utils = require('./utils'), + i18n = require('../i18n'), + coreHelpers = {}, registerHelpers; if (!utils.isProduction) { hbs.handlebars.logger.level = 0; } -coreHelpers.asset = require('./asset'); -coreHelpers.author = require('./author'); -coreHelpers.body_class = require('./body_class'); -coreHelpers.content = require('./content'); -coreHelpers.date = require('./date'); -coreHelpers.encode = require('./encode'); -coreHelpers.excerpt = require('./excerpt'); +coreHelpers.asset = require('./asset'); +coreHelpers.author = require('./author'); +coreHelpers.body_class = require('./body_class'); +coreHelpers.content = require('./content'); +coreHelpers.date = require('./date'); +coreHelpers.encode = require('./encode'); +coreHelpers.excerpt = require('./excerpt'); coreHelpers.facebook_url = require('./facebook_url'); coreHelpers.foreach = require('./foreach'); coreHelpers.get = require('./get'); @@ -48,7 +48,9 @@ coreHelpers.helperMissing = function (arg) { return undefined; } - logging.error(new errors.InternalServerError(i18n.t('warnings.helpers.index.missingHelper', {arg: arg}))); + logging.error(new errors.GhostError({ + message: i18n.t('warnings.helpers.index.missingHelper', {arg: arg}) + })); }; // Register an async handlebars helper for a given handlebars instance @@ -64,8 +66,10 @@ function registerAsyncHelper(hbs, name, fn) { Promise.resolve(fn.call(this, context, options)).then(function (result) { cb(result); }).catch(function (err) { - logging.warn('registerAsyncThemeHelper: ' + name); - throw err; + throw new errors.IncorrectUsageError({ + err: err, + context: 'registerAsyncThemeHelper: ' + name + }); }); }); } diff --git a/core/server/helpers/navigation.js b/core/server/helpers/navigation.js index e48aac7ed5e..759c1bbf3ff 100644 --- a/core/server/helpers/navigation.js +++ b/core/server/helpers/navigation.js @@ -2,11 +2,11 @@ // `{{navigation}}` // Outputs navigation menu of static urls -var _ = require('lodash'), - hbs = require('express-hbs'), - i18n = require('../i18n'), - errors = require('../errors'), - template = require('./template'), +var _ = require('lodash'), + hbs = require('express-hbs'), + i18n = require('../i18n'), + errors = require('../errors'), + template = require('./template'), navigation; navigation = function (options) { @@ -18,13 +18,17 @@ navigation = function (options) { data; if (!_.isObject(navigationData) || _.isFunction(navigationData)) { - throw new errors.IncorrectUsage(i18n.t('warnings.helpers.navigation.invalidData')); + throw new errors.IncorrectUsageError({ + message: i18n.t('warnings.helpers.navigation.invalidData') + }); } if (navigationData.filter(function (e) { return (_.isUndefined(e.label) || _.isUndefined(e.url)); }).length > 0) { - throw new errors.IncorrectUsage(i18n.t('warnings.helpers.navigation.valuesMustBeDefined')); + throw new errors.IncorrectUsageError({ + message: i18n.t('warnings.helpers.navigation.valuesMustBeDefined') + }); } // check for non-null string values @@ -32,7 +36,9 @@ navigation = function (options) { return ((!_.isNull(e.label) && !_.isString(e.label)) || (!_.isNull(e.url) && !_.isString(e.url))); }).length > 0) { - throw new errors.IncorrectUsage(i18n.t('warnings.helpers.navigation.valuesMustBeString')); + throw new errors.IncorrectUsageError({ + message: i18n.t('warnings.helpers.navigation.valuesMustBeString') + }); } function _slugify(label) { diff --git a/core/server/helpers/pagination.js b/core/server/helpers/pagination.js index 528ea418e0e..d7f2e32d06d 100644 --- a/core/server/helpers/pagination.js +++ b/core/server/helpers/pagination.js @@ -4,29 +4,35 @@ var _ = require('lodash'), errors = require('../errors'), - template = require('./template'), i18n = require('../i18n'), + template = require('./template'), pagination; pagination = function (options) { /*jshint unused:false*/ if (!_.isObject(this.pagination) || _.isFunction(this.pagination)) { - throw new errors.IncorrectUsage(i18n.t('warnings.helpers.pagination.invalidData')); + throw new errors.IncorrectUsageError({ + message: i18n.t('warnings.helpers.pagination.invalidData') + }); } if (_.isUndefined(this.pagination.page) || _.isUndefined(this.pagination.pages) || _.isUndefined(this.pagination.total) || _.isUndefined(this.pagination.limit)) { - throw new errors.IncorrectUsage(i18n.t('warnings.helpers.pagination.valuesMustBeDefined')); + throw new errors.IncorrectUsageError({ + message: i18n.t('warnings.helpers.pagination.valuesMustBeDefined') + }); } if ((!_.isNull(this.pagination.next) && !_.isNumber(this.pagination.next)) || (!_.isNull(this.pagination.prev) && !_.isNumber(this.pagination.prev))) { - throw new errors.IncorrectUsage(i18n.t('warnings.helpers.pagination.nextPrevValuesMustBeNumeric')); + throw new errors.IncorrectUsageError({ + message: i18n.t('warnings.helpers.pagination.nextPrevValuesMustBeNumeric') + }); } if (!_.isNumber(this.pagination.page) || !_.isNumber(this.pagination.pages) || !_.isNumber(this.pagination.total) || !_.isNumber(this.pagination.limit)) { - throw new errors.IncorrectUsage(i18n.t('warnings.helpers.pagination.valuesMustBeNumeric')); + throw new errors.IncorrectUsageError({message: i18n.t('warnings.helpers.pagination.valuesMustBeNumeric')}); } var data = _.merge({}, this.pagination); diff --git a/core/server/helpers/plural.js b/core/server/helpers/plural.js index 6bd593a8c91..dfac8ecba71 100644 --- a/core/server/helpers/plural.js +++ b/core/server/helpers/plural.js @@ -10,14 +10,16 @@ var hbs = require('express-hbs'), errors = require('../errors'), - _ = require('lodash'), i18n = require('../i18n'), + _ = require('lodash'), plural; plural = function (number, options) { if (_.isUndefined(options.hash) || _.isUndefined(options.hash.empty) || _.isUndefined(options.hash.singular) || _.isUndefined(options.hash.plural)) { - throw new errors.IncorrectUsage(i18n.t('warnings.helpers.plural.valuesMustBeDefined')); + throw new errors.IncorrectUsageError({ + message: i18n.t('warnings.helpers.plural.valuesMustBeDefined') + }); } if (number === 0) { diff --git a/core/server/helpers/template.js b/core/server/helpers/template.js index 1ef41e00975..2475618f476 100644 --- a/core/server/helpers/template.js +++ b/core/server/helpers/template.js @@ -11,7 +11,9 @@ templates.execute = function (name, context, options) { var partial = hbs.handlebars.partials[name]; if (partial === undefined) { - throw new errors.IncorrectUsage(i18n.t('warnings.helpers.template.templateNotFound', {name: name})); + throw new errors.IncorrectUsageError({ + message: i18n.t('warnings.helpers.template.templateNotFound', {name: name}) + }); } // If the partial view is not compiled, it compiles and saves in handlebars diff --git a/core/server/index.js b/core/server/index.js index 2b543b6d878..0a852a3566d 100644 --- a/core/server/index.js +++ b/core/server/index.js @@ -108,8 +108,10 @@ function init(options) { ); }).then(function () { debug('Apps, XMLRPC, Slack done'); + // Get reference to an express app instance. parentApp = express(); + // ## Middleware and Routing middleware(parentApp); debug('Express done'); diff --git a/core/server/middleware/api/version-match.js b/core/server/middleware/api/version-match.js index ea668b00702..bcfbaa22930 100644 --- a/core/server/middleware/api/version-match.js +++ b/core/server/middleware/api/version-match.js @@ -6,12 +6,9 @@ function checkVersionMatch(req, res, next) { currentVersion = res.locals.safeVersion; if (requestVersion && requestVersion !== currentVersion) { - return next(new errors.VersionMismatchError( - i18n.t( - 'errors.middleware.api.versionMismatch', - {requestVersion: requestVersion, currentVersion: currentVersion} - ) - )); + return next(new errors.VersionMismatchError({ + message: i18n.t('errors.middleware.api.versionMismatch', {requestVersion: requestVersion, currentVersion: currentVersion}) + })); } next(); diff --git a/core/server/middleware/error-handler.js b/core/server/middleware/error-handler.js index aed2a472d30..62843ab422f 100644 --- a/core/server/middleware/error-handler.js +++ b/core/server/middleware/error-handler.js @@ -2,6 +2,7 @@ var _ = require('lodash'), path = require('path'), hbs = require('express-hbs'), config = require('../config'), + errors = require('../errors'), i18n = require('../i18n'), _private = {}; @@ -88,6 +89,12 @@ module.exports = function errorHandler(err, req, res, next) { err = err[0]; } + if (!(err instanceof errors.GhostError)) { + err = new errors.GhostError({ + err: err + }); + } + req.err = err; res.statusCode = err.statusCode; diff --git a/core/server/middleware/index.js b/core/server/middleware/index.js index de5fc218530..92431c01554 100644 --- a/core/server/middleware/index.js +++ b/core/server/middleware/index.js @@ -1,21 +1,22 @@ var debug = require('debug')('ghost:middleware'), bodyParser = require('body-parser'), compress = require('compression'), - config = require('../config'), - errors = require('../errors'), express = require('express'), hbs = require('express-hbs'), path = require('path'), - routes = require('../routes'), + netjet = require('netjet'), + multer = require('multer'), + tmpdir = require('os').tmpdir, serveStatic = require('express').static, slashes = require('connect-slashes'), + routes = require('../routes'), + config = require('../config'), storage = require('../storage'), logging = require('../logging'), + errors = require('../errors'), i18n = require('../i18n'), utils = require('../utils'), sitemapHandler = require('../data/xml/sitemap/handler'), - multer = require('multer'), - tmpdir = require('os').tmpdir, cacheControl = require('./cache-control'), checkSSL = require('./check-ssl'), decideIsAdmin = require('./decide-is-admin'), @@ -30,7 +31,6 @@ var debug = require('debug')('ghost:middleware'), versionMatch = require('./api/version-match'), cors = require('./cors'), validation = require('./validation'), - netjet = require('netjet'), labs = require('./labs'), helpers = require('../helpers'), middleware, @@ -52,6 +52,7 @@ middleware = { setupMiddleware = function setupMiddleware(blogApp) { debug('Middleware start'); + var corePath = config.get('paths').corePath, adminApp = express(), adminHbs = hbs.create(); @@ -123,9 +124,11 @@ setupMiddleware = function setupMiddleware(blogApp) { path.join(corePath, '/shared'), {maxAge: utils.ONE_HOUR_MS, fallthrough: false} )); + blogApp.use('/content/images', storage.getStorage().serve()); debug('Static content done'); + // First determine whether we're serving admin or theme content blogApp.use(decideIsAdmin); blogApp.use(themeHandler.updateActiveTheme); @@ -211,7 +214,7 @@ setupMiddleware = function setupMiddleware(blogApp) { // ### Error handlers blogApp.use(function pageNotFound(req, res, next) { - next(new errors.NotFoundError(i18n.t('errors.errors.pageNotFound'))); + next(new errors.NotFoundError({message: i18n.t('errors.errors.pageNotFound')})); }); blogApp.use(errorHandler); diff --git a/core/server/middleware/maintenance.js b/core/server/middleware/maintenance.js index 5b7a68baf77..b9497924587 100644 --- a/core/server/middleware/maintenance.js +++ b/core/server/middleware/maintenance.js @@ -4,9 +4,7 @@ var config = require('../config'), module.exports = function maintenance(req, res, next) { if (config.get('maintenance').enabled) { - return next(new errors.Maintenance( - i18n.t('errors.general.maintenance') - )); + return next(new errors.MaintenanceError({message: i18n.t('errors.general.maintenance')})); } next(); diff --git a/core/server/middleware/spam-prevention.js b/core/server/middleware/spam-prevention.js index 38a355ba7d3..e2d94ecea8d 100644 --- a/core/server/middleware/spam-prevention.js +++ b/core/server/middleware/spam-prevention.js @@ -30,7 +30,7 @@ spamPrevention = { } else if (req.body.grant_type === 'refresh_token') { return next(); } else { - return next(new errors.BadRequestError(i18n.t('errors.middleware.spamprevention.noUsername'))); + return next(new errors.BadRequestError({message: i18n.t('errors.middleware.spamprevention.noUsername')})); } // filter entries that are older than rateSigninPeriod @@ -43,11 +43,11 @@ spamPrevention = { deniedRateLimit = (ipCount[remoteAddress] > rateSigninAttempts); if (deniedRateLimit) { - return next(new errors.TooManyRequestsError( - i18n.t('errors.middleware.spamprevention.tooManyAttempts') + rateSigninPeriod === 3600 ? i18n.t('errors.middleware.spamprevention.waitOneHour') : i18n.t('errors.middleware.spamprevention.tryAgainLater'), - i18n.t('errors.middleware.spamprevention.tooManySigninAttempts.error', {rateSigninAttempts: rateSigninAttempts, rateSigninPeriod: rateSigninPeriod}), - i18n.t('errors.middleware.spamprevention.tooManySigninAttempts.context') - )); + return next(new errors.TooManyRequestsError({ + message: i18n.t('errors.middleware.spamprevention.tooManyAttempts') + rateSigninPeriod === 3600 ? i18n.t('errors.middleware.spamprevention.waitOneHour') : i18n.t('errors.middleware.spamprevention.tryAgainLater'), + context: i18n.t('errors.middleware.spamprevention.tooManySigninAttempts.error', {rateSigninAttempts: rateSigninAttempts, rateSigninPeriod: rateSigninPeriod}), + help: i18n.t('errors.middleware.spamprevention.tooManySigninAttempts.context') + })); } next(); }, @@ -74,7 +74,7 @@ spamPrevention = { forgottenSecurity.push({ip: remoteAddress, time: currentTime, email: email, count: 0}); } } else { - return next(new errors.BadRequestError(i18n.t('errors.middleware.spamprevention.noEmail'))); + return next(new errors.BadRequestError({message: i18n.t('errors.middleware.spamprevention.noEmail')})); } // filter entries that are older than rateForgottenPeriod @@ -91,19 +91,22 @@ spamPrevention = { } if (deniedEmailRateLimit) { - return next(new errors.TooManyRequestsError( - i18n.t('errors.middleware.spamprevention.tooManyAttempts') + rateForgottenPeriod === 3600 ? i18n.t('errors.middleware.spamprevention.waitOneHour') : i18n.t('errors.middleware.spamprevention.tryAgainLater'), - i18n.t('errors.middleware.spamprevention.forgottenPasswordEmail.error', {rfa: rateForgottenAttempts, rfp: rateForgottenPeriod}), - i18n.t('errors.middleware.spamprevention.forgottenPasswordEmail.context') - )); + return next(new errors.TooManyRequestsError({ + message: i18n.t('errors.middleware.spamprevention.tooManyAttempts') + rateForgottenPeriod === 3600 ? i18n.t('errors.middleware.spamprevention.waitOneHour') : i18n.t('errors.middleware.spamprevention.tryAgainLater'), + context: i18n.t('errors.middleware.spamprevention.forgottenPasswordEmail.error', { + rfa: rateForgottenAttempts, + rfp: rateForgottenPeriod + }), + help: i18n.t('errors.middleware.spamprevention.forgottenPasswordEmail.context') + })); } if (deniedRateLimit) { - return next(new errors.TooManyRequestsError( - i18n.t('errors.middleware.spamprevention.tooManyAttempts') + rateForgottenPeriod === 3600 ? i18n.t('errors.middleware.spamprevention.waitOneHour') : i18n.t('errors.middleware.spamprevention.tryAgainLater'), - i18n.t('errors.middleware.spamprevention.forgottenPasswordIp.error', {rfa: rateForgottenAttempts, rfp: rateForgottenPeriod}), - i18n.t('errors.middleware.spamprevention.forgottenPasswordIp.context') - )); + return next(new errors.TooManyRequestsError({ + message: i18n.t('errors.middleware.spamprevention.tooManyAttempts') + rateForgottenPeriod === 3600 ? i18n.t('errors.middleware.spamprevention.waitOneHour') : i18n.t('errors.middleware.spamprevention.tryAgainLater'), + context: i18n.t('errors.middleware.spamprevention.forgottenPasswordIp.error', {rfa: rateForgottenAttempts, rfp: rateForgottenPeriod}), + help: i18n.t('errors.middleware.spamprevention.forgottenPasswordIp.context') + })); } next(); diff --git a/core/server/middleware/theme-handler.js b/core/server/middleware/theme-handler.js index ab49a3cda64..39d7b810bb0 100644 --- a/core/server/middleware/theme-handler.js +++ b/core/server/middleware/theme-handler.js @@ -6,7 +6,7 @@ var _ = require('lodash'), config = require('../config'), logging = require('../logging'), errors = require('../errors'), - i18n = require('../i18n'), + i18n = require('../i18n'), themeHandler; themeHandler = { @@ -100,7 +100,9 @@ themeHandler = { // Change theme if (!config.get('paths').availableThemes.hasOwnProperty(activeTheme.value)) { if (!res.isAdmin) { - return next(new errors.InternalServerError(i18n.t('errors.middleware.themehandler.missingTheme', {theme: activeTheme.value}))); + return next(new errors.NotFoundError({ + message: i18n.t('errors.middleware.themehandler.missingTheme', {theme: activeTheme.value}) + })); } else { // At this point the activated theme is not present and the current // request is for the admin client. In order to allow the user access @@ -108,7 +110,6 @@ themeHandler = { // processing can continue. blogApp.engine('hbs', hbs.express3()); logging.warn(i18n.t('errors.middleware.themehandler.missingTheme', {theme: activeTheme.value})); - return next(); } } else { diff --git a/core/server/middleware/validation/upload.js b/core/server/middleware/validation/upload.js index 5ca643a0971..f37513c67b5 100644 --- a/core/server/middleware/validation/upload.js +++ b/core/server/middleware/validation/upload.js @@ -17,12 +17,12 @@ module.exports = function upload(options) { // Check if a file was provided if (!apiUtils.checkFileExists(req.file)) { - return next(new errors.NoPermissionError(i18n.t('errors.api.' + type + '.missingFile'))); + return next(new errors.NoPermissionError({message: i18n.t('errors.api.' + type + '.missingFile')})); } // Check if the file is valid if (!apiUtils.checkFileIsValid(req.file, contentTypes, extensions)) { - return next(new errors.UnsupportedMediaTypeError(i18n.t('errors.api.' + type + '.invalidFile', {extensions: extensions}))); + return next(new errors.UnsupportedMediaTypeError({message: i18n.t('errors.api.' + type + '.invalidFile', {extensions: extensions})})); } next(); diff --git a/core/server/models/base/index.js b/core/server/models/base/index.js index 1d937480af1..5672c17732e 100644 --- a/core/server/models/base/index.js +++ b/core/server/models/base/index.js @@ -173,7 +173,10 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({ } else if (options.context && options.context.external) { return 0; } else { - throw new errors.IncorrectUsage(i18n.t('errors.models.base.index.missingContext')); + throw new errors.NotFoundError({ + message: i18n.t('errors.models.base.index.missingContext'), + level: 'critical' + }); } }, diff --git a/core/server/models/base/listeners.js b/core/server/models/base/listeners.js index d8a2a090bd2..6219048b21d 100644 --- a/core/server/models/base/listeners.js +++ b/core/server/models/base/listeners.js @@ -1,6 +1,7 @@ var config = require('../../config'), events = require(config.get('paths:corePath') + '/server/events'), models = require(config.get('paths:corePath') + '/server/models'), + errors = require(config.get('paths:corePath') + '/server/errors'), logging = require(config.get('paths:corePath') + '/server/logging'), sequence = require(config.get('paths:corePath') + '/server/utils/sequence'), moment = require('moment-timezone'); @@ -11,7 +12,7 @@ var config = require('../../config'), events.on('token.added', function (tokenModel) { models.User.edit({last_login: moment().toDate()}, {id: tokenModel.get('user_id')}) .catch(function (err) { - logging.error(err); + logging.error(new errors.GhostError({err: err, level: 'critical'})); }); }); @@ -61,11 +62,16 @@ events.on('settings.activeTimezone.edited', function (settingModel) { }; })).each(function (result) { if (!result.isFulfilled()) { - logging.error(result.reason()); + logging.error(new errors.GhostError({ + err: result.reason() + })); } }); }) .catch(function (err) { - logging.error(err); + logging.error(new errors.GhostError({ + err: err, + level: 'critical' + })); }); }); diff --git a/core/server/models/base/token.js b/core/server/models/base/token.js index 04cdcdf175e..83ed8e9aaff 100644 --- a/core/server/models/base/token.js +++ b/core/server/models/base/token.js @@ -57,7 +57,7 @@ Basetoken = ghostBookshelf.Model.extend({ }); } - return Promise.reject(new errors.NotFoundError(i18n.t('errors.models.base.token.noUserFound'))); + return Promise.reject(new errors.NotFoundError({message: i18n.t('errors.models.base.token.noUserFound')})); }, /** diff --git a/core/server/models/plugins/filter.js b/core/server/models/plugins/filter.js index 551f4189bda..ec20e59290b 100644 --- a/core/server/models/plugins/filter.js +++ b/core/server/models/plugins/filter.js @@ -24,13 +24,13 @@ filterUtils = { custom = _.map(custom, function (arg) { return _.isString(arg) ? gql.parse(arg) : arg; }); - } catch (error) { - throw new errors.ValidationError( - error.message, - 'filter', - i18n.t('errors.models.plugins.filter.errorParsing'), - i18n.t('errors.models.plugins.filter.forInformationRead', {url: 'http://api.ghost.org/docs/filter'}) - ); + } catch (err) { + throw new errors.ValidationError({ + err: err, + property: 'filter', + context: i18n.t('errors.models.plugins.filter.errorParsing'), + help: i18n.t('errors.models.plugins.filter.forInformationRead', {url: 'http://api.ghost.org/docs/filter'}) + }); } // Merge custom filter options into a single set of statements diff --git a/core/server/models/post.js b/core/server/models/post.js index d2c7ede7838..6226b3edf27 100644 --- a/core/server/models/post.js +++ b/core/server/models/post.js @@ -5,7 +5,6 @@ var _ = require('lodash'), Promise = require('bluebird'), sequence = require('../utils/sequence'), errors = require('../errors'), - logging = require('../logging'), Showdown = require('showdown-ghost'), legacyConverter = new Showdown.converter({extensions: ['ghostgfm', 'footnotes', 'highlight']}), Mobiledoc = require('mobiledoc-html-renderer').default, @@ -165,28 +164,28 @@ Post = ghostBookshelf.Model.extend({ // CASE: disallow published -> scheduled // @TODO: remove when we have versioning based on updated_at if (newStatus !== olderStatus && newStatus === 'scheduled' && olderStatus === 'published') { - return Promise.reject(new errors.ValidationError( - i18n.t('errors.models.post.isAlreadyPublished', {key: 'status'}) - )); + return Promise.reject(new errors.ValidationError({ + message: i18n.t('errors.models.post.isAlreadyPublished', {key: 'status'}) + })); } // CASE: both page and post can get scheduled if (newStatus === 'scheduled') { if (!publishedAt) { - return Promise.reject(new errors.ValidationError( - i18n.t('errors.models.post.valueCannotBeBlank', {key: 'published_at'}) - )); + return Promise.reject(new errors.ValidationError({ + message: i18n.t('errors.models.post.valueCannotBeBlank', {key: 'published_at'}) + })); } else if (!moment(publishedAt).isValid()) { - return Promise.reject(new errors.ValidationError( - i18n.t('errors.models.post.valueCannotBeBlank', {key: 'published_at'}) - )); + return Promise.reject(new errors.ValidationError({ + message: i18n.t('errors.models.post.valueCannotBeBlank', {key: 'published_at'}) + })); // CASE: to schedule/reschedule a post, a minimum diff of x minutes is needed (default configured is 2minutes) } else if (publishedAtHasChanged && moment(publishedAt).isBefore(moment().add(config.get('times').cannotScheduleAPostBeforeInMinutes, 'minutes'))) { - return Promise.reject(new errors.ValidationError( - i18n.t('errors.models.post.expectedPublishedAtInFuture', { + return Promise.reject(new errors.ValidationError({ + message: i18n.t('errors.models.post.expectedPublishedAtInFuture', { cannotScheduleAPostBeforeInMinutes: config.get('times').cannotScheduleAPostBeforeInMinutes }) - )); + })); } } @@ -376,16 +375,12 @@ Post = ghostBookshelf.Model.extend({ return doTagUpdates(options); }).then(function () { // Don't do anything, the transaction processed ok - }).catch(function failure(error) { - logging.error(new errors.InternalServerError( - error.message, - i18n.t('errors.models.post.tagUpdates.error'), - i18n.t('errors.models.post.tagUpdates.help') - )); - - return Promise.reject(new errors.InternalServerError( - i18n.t('errors.models.post.tagUpdates.error') + ' ' + i18n.t('errors.models.post.tagUpdates.help') + error - )); + }).catch(function failure(err) { + return Promise.reject(new errors.GhostError({ + err: err, + context: i18n.t('errors.models.post.tagUpdates.error'), + help: i18n.t('errors.models.post.tagUpdates.help') + })); }); } }, @@ -673,15 +668,15 @@ Post = ghostBookshelf.Model.extend({ options = this.filterOptions(options, 'destroyByAuthor'); if (!authorId) { - throw new errors.NotFoundError(i18n.t('errors.models.post.noUserFound')); + throw new errors.NotFoundError({message: i18n.t('errors.models.post.noUserFound')}); } return postCollection .query('where', 'author_id', '=', authorId) .fetch(options) .call('invokeThen', 'destroy', options) - .catch(function (error) { - throw new errors.InternalServerError(error.message || error); + .catch(function (err) { + return Promise.reject(new errors.GhostError({err: err})); }); }), @@ -714,7 +709,7 @@ Post = ghostBookshelf.Model.extend({ return Promise.resolve(); } - return Promise.reject(new errors.NoPermissionError(i18n.t('errors.models.post.notEnoughPermission'))); + return Promise.reject(new errors.NoPermissionError({message: i18n.t('errors.models.post.notEnoughPermission')})); } }); diff --git a/core/server/models/role.js b/core/server/models/role.js index 32a31a45528..4e57a787bb3 100644 --- a/core/server/models/role.js +++ b/core/server/models/role.js @@ -78,7 +78,7 @@ Role = ghostBookshelf.Model.extend({ return Promise.resolve(); } - return Promise.reject(new errors.NoPermissionError(i18n.t('errors.models.role.notEnoughPermission'))); + return Promise.reject(new errors.NoPermissionError({message: i18n.t('errors.models.role.notEnoughPermission')})); } }); diff --git a/core/server/models/settings.js b/core/server/models/settings.js index 933d797f4e6..4273e05759f 100644 --- a/core/server/models/settings.js +++ b/core/server/models/settings.js @@ -114,7 +114,7 @@ Settings = ghostBookshelf.Model.extend({ // Accept an array of models as input if (item.toJSON) { item = item.toJSON(); } if (!(_.isString(item.key) && item.key.length > 0)) { - return Promise.reject(new errors.ValidationError(i18n.t('errors.models.settings.valueCannotBeBlank'))); + return Promise.reject(new errors.ValidationError({message: i18n.t('errors.models.settings.valueCannotBeBlank')})); } item = self.filterData(item); @@ -138,14 +138,14 @@ Settings = ghostBookshelf.Model.extend({ return setting.save(saveData, options); } - return Promise.reject(new errors.NotFoundError(i18n.t('errors.models.settings.unableToFindSetting', {key: item.key}))); + return Promise.reject(new errors.NotFoundError({message: i18n.t('errors.models.settings.unableToFindSetting', {key: item.key})})); }); }); }, populateDefault: function (key) { if (!getDefaultSettings()[key]) { - return Promise.reject(new errors.NotFoundError(i18n.t('errors.models.settings.unableToFindDefaultSetting', {key: key}))); + return Promise.reject(new errors.NotFoundError({message: i18n.t('errors.models.settings.unableToFindDefaultSetting', {key: key})})); } return this.findOne({key: key}).then(function then(foundSetting) { diff --git a/core/server/models/subscriber.js b/core/server/models/subscriber.js index cd8937373c9..b65cc840b3a 100644 --- a/core/server/models/subscriber.js +++ b/core/server/models/subscriber.js @@ -72,7 +72,7 @@ Subscriber = ghostBookshelf.Model.extend({ return Promise.resolve(); } - return Promise.reject(new errors.NoPermissionError(i18n.t('errors.models.subscriber.notEnoughPermission'))); + return Promise.reject(new errors.NoPermissionError({message: i18n.t('errors.models.subscriber.notEnoughPermission')})); }, // TODO: This is a copy paste of models/user.js! diff --git a/core/server/models/user.js b/core/server/models/user.js index 968a0ef4c98..9ae4de16d86 100644 --- a/core/server/models/user.js +++ b/core/server/models/user.js @@ -1,15 +1,15 @@ var _ = require('lodash'), Promise = require('bluebird'), - errors = require('../errors'), - utils = require('../utils'), - gravatar = require('../utils/gravatar'), bcrypt = require('bcryptjs'), - ghostBookshelf = require('./base'), crypto = require('crypto'), validator = require('validator'), + ghostBookshelf = require('./base'), + errors = require('../errors'), + logging = require('../logging'), + utils = require('../utils'), + gravatar = require('../utils/gravatar'), validation = require('../data/validation'), events = require('../events'), - logging = require('../logging'), i18n = require('../i18n'), bcryptGenSalt = Promise.promisify(bcrypt.genSalt), @@ -122,7 +122,9 @@ User = ghostBookshelf.Model.extend({ } else if (this.get('id')) { return this.get('id'); } else { - throw new errors.IncorrectUsage(i18n.t('errors.models.user.missingContext')); + throw new errors.NotFoundError({ + message: i18n.t('errors.models.user.missingContext') + }); } }, @@ -312,7 +314,7 @@ User = ghostBookshelf.Model.extend({ if (data.roles && data.roles.length > 1) { return Promise.reject( - new errors.ValidationError(i18n.t('errors.models.user.onlyOneRolePerUserSupported')) + new errors.ValidationError({message: i18n.t('errors.models.user.onlyOneRolePerUserSupported')}) ); } @@ -335,7 +337,7 @@ User = ghostBookshelf.Model.extend({ }).then(function then(roleToAssign) { if (roleToAssign && roleToAssign.get('name') === 'Owner') { return Promise.reject( - new errors.ValidationError(i18n.t('errors.models.user.methodDoesNotSupportOwnerRole')) + new errors.ValidationError({message: i18n.t('errors.models.user.methodDoesNotSupportOwnerRole')}) ); } else { // assign all other roles @@ -370,11 +372,11 @@ User = ghostBookshelf.Model.extend({ // check for too many roles if (data.roles && data.roles.length > 1) { - return Promise.reject(new errors.ValidationError(i18n.t('errors.models.user.onlyOneRolePerUserSupported'))); + return Promise.reject(new errors.ValidationError({message: i18n.t('errors.models.user.onlyOneRolePerUserSupported')})); } if (!validatePasswordLength(userData.password)) { - return Promise.reject(new errors.ValidationError(i18n.t('errors.models.user.passwordDoesNotComplyLength'))); + return Promise.reject(new errors.ValidationError({message: i18n.t('errors.models.user.passwordDoesNotComplyLength')})); } function getAuthorRole() { @@ -422,7 +424,7 @@ User = ghostBookshelf.Model.extend({ userData = this.filterData(data); if (!validatePasswordLength(userData.password)) { - return Promise.reject(new errors.ValidationError(i18n.t('errors.models.user.passwordDoesNotComplyLength'))); + return Promise.reject(new errors.ValidationError({message: i18n.t('errors.models.user.passwordDoesNotComplyLength')})); } options = this.filterOptions(options, 'setup'); @@ -458,6 +460,7 @@ User = ghostBookshelf.Model.extend({ if (_.isNumber(userModelOrId) || _.isString(userModelOrId)) { // Grab the original args without the first one origArgs = _.toArray(arguments).slice(1); + // Get the actual user model return this.findOne({id: userModelOrId, status: 'all'}, {include: ['roles']}).then(function then(foundUserModel) { // Build up the original args but substitute with actual model @@ -491,7 +494,7 @@ User = ghostBookshelf.Model.extend({ if (action === 'destroy') { // Owner cannot be deleted EVER if (loadedPermissions.user && userModel.hasRole('Owner')) { - return Promise.reject(new errors.NoPermissionError(i18n.t('errors.models.user.notEnoughPermission'))); + return Promise.reject(new errors.NoPermissionError({message: i18n.t('errors.models.user.notEnoughPermission')})); } // Users with the role 'Editor' have complex permissions when the action === 'destroy' @@ -508,7 +511,7 @@ User = ghostBookshelf.Model.extend({ return Promise.resolve(); } - return Promise.reject(new errors.NoPermissionError(i18n.t('errors.models.user.notEnoughPermission'))); + return Promise.reject(new errors.NoPermissionError({message: i18n.t('errors.models.user.notEnoughPermission')})); }, setWarning: function setWarning(user, options) { @@ -538,7 +541,7 @@ User = ghostBookshelf.Model.extend({ s; return this.getByEmail(object.email).then(function then(user) { if (!user) { - return Promise.reject(new errors.NotFoundError(i18n.t('errors.models.user.noUserWithEnteredEmailAddr'))); + return Promise.reject(new errors.NotFoundError({message: i18n.t('errors.models.user.noUserWithEnteredEmailAddr')})); } if (user.get('status') !== 'locked') { @@ -547,22 +550,19 @@ User = ghostBookshelf.Model.extend({ return Promise.resolve(self.setWarning(user, {validate: false})).then(function then(remaining) { if (remaining === 0) { // If remaining attempts = 0, the account has been locked, so show a locked account message - return Promise.reject(new errors.NoPermissionError( - i18n.t('errors.models.user.accountLocked'))); + return Promise.reject(new errors.NoPermissionError({message: i18n.t('errors.models.user.accountLocked')})); } s = (remaining > 1) ? 's' : ''; - return Promise.reject(new errors.UnauthorizedError(i18n.t('errors.models.user.incorrectPasswordAttempts', {remaining: remaining, s: s}))); + return Promise.reject(new errors.UnauthorizedError({message: i18n.t('errors.models.user.incorrectPasswordAttempts', {remaining: remaining, s: s})})); // Use comma structure, not .catch, because we don't want to catch incorrect passwords }, function handleError(err) { - // If we get a validation or other error during this save, catch it and log it, but don't - // cause a login error because of it. The user validation is not important here. - err.context = i18n.t('errors.models.user.userUpdateError.context'); - err.help = i18n.t('errors.models.user.userUpdateError.help'); - logging.error(err); - - return Promise.reject(new errors.UnauthorizedError(i18n.t('errors.models.user.incorrectPassword'))); + return Promise.reject(new errors.UnauthorizedError({ + err: err, + context: i18n.t('errors.models.user.incorrectPassword'), + help: i18n.t('errors.models.user.userUpdateError.help') + })); }); } @@ -570,19 +570,20 @@ User = ghostBookshelf.Model.extend({ .catch(function handleError(err) { // If we get a validation or other error during this save, catch it and log it, but don't // cause a login error because of it. The user validation is not important here. - err.context = i18n.t('errors.models.user.userUpdateError.context'); - err.help = i18n.t('errors.models.user.userUpdateError.help'); - logging.error(err); + logging.error(new errors.GhostError({ + err: err, + context: i18n.t('errors.models.user.userUpdateError.context'), + help: i18n.t('errors.models.user.userUpdateError.help') + })); return user; }); }); } - return Promise.reject(new errors.NoPermissionError( - i18n.t('errors.models.user.accountLocked'))); + return Promise.reject(new errors.NoPermissionError({message: i18n.t('errors.models.user.accountLocked')})); }, function handleError(error) { if (error.message === 'NotFound' || error.message === 'EmptyResponse') { - return Promise.reject(new errors.NotFoundError(i18n.t('errors.models.user.noUserWithEnteredEmailAddr'))); + return Promise.reject(new errors.NotFoundError({message: i18n.t('errors.models.user.noUserWithEnteredEmailAddr')})); } return Promise.reject(error); @@ -604,17 +605,17 @@ User = ghostBookshelf.Model.extend({ // If the two passwords do not match if (newPassword !== ne2Password) { - return Promise.reject(new errors.ValidationError(i18n.t('errors.models.user.newPasswordsDoNotMatch'))); + return Promise.reject(new errors.ValidationError({message: i18n.t('errors.models.user.newPasswordsDoNotMatch')})); } // If the old password is empty when changing current user's password if (userId === options.context.user && _.isEmpty(oldPassword)) { - return Promise.reject(new errors.ValidationError(i18n.t('errors.models.user.passwordRequiredForOperation'))); + return Promise.reject(new errors.ValidationError({message: i18n.t('errors.models.user.passwordRequiredForOperation')})); } // If password is not complex enough if (!validatePasswordLength(newPassword)) { - return Promise.reject(new errors.ValidationError(i18n.t('errors.models.user.passwordDoesNotComplyLength'))); + return Promise.reject(new errors.ValidationError({message: i18n.t('errors.models.user.passwordDoesNotComplyLength')})); } return self.forge({id: userId}).fetch({require: true}).then(function then(_user) { @@ -627,7 +628,7 @@ User = ghostBookshelf.Model.extend({ return true; }).then(function then(matched) { if (!matched) { - return Promise.reject(new errors.ValidationError(i18n.t('errors.models.user.incorrectPassword'))); + return Promise.reject(new errors.ValidationError({message: i18n.t('errors.models.user.incorrectPassword')})); } return generatePasswordHash(newPassword); @@ -639,7 +640,7 @@ User = ghostBookshelf.Model.extend({ generateResetToken: function generateResetToken(email, expires, dbHash) { return this.getByEmail(email).then(function then(foundUser) { if (!foundUser) { - return Promise.reject(new errors.NotFoundError(i18n.t('errors.models.user.noUserWithEnteredEmailAddr'))); + return Promise.reject(new errors.NotFoundError({message: i18n.t('errors.models.user.noUserWithEnteredEmailAddr')})); } var hash = crypto.createHash('sha256'), @@ -669,25 +670,25 @@ User = ghostBookshelf.Model.extend({ // Check if invalid structure if (!parts || parts.length !== 3) { - return Promise.reject(new errors.BadRequestError(i18n.t('errors.models.user.invalidTokenStructure'))); + return Promise.reject(new errors.BadRequestError({message: i18n.t('errors.models.user.invalidTokenStructure')})); } expires = parseInt(parts[0], 10); email = parts[1]; if (isNaN(expires)) { - return Promise.reject(new errors.BadRequestError(i18n.t('errors.models.user.invalidTokenExpiration'))); + return Promise.reject(new errors.BadRequestError({message: i18n.t('errors.models.user.invalidTokenExpiration')})); } // Check if token is expired to prevent replay attacks if (expires < Date.now()) { - return Promise.reject(new errors.ValidationError(i18n.t('errors.models.user.expiredToken'))); + return Promise.reject(new errors.ValidationError({message: i18n.t('errors.models.user.expiredToken')})); } // to prevent brute force attempts to reset the password the combination of email+expires is only allowed for // 10 attempts if (tokenSecurity[email + '+' + expires] && tokenSecurity[email + '+' + expires].count >= 10) { - return Promise.reject(new errors.NoPermissionError(i18n.t('errors.models.user.tokenLocked'))); + return Promise.reject(new errors.NoPermissionError({message: i18n.t('errors.models.user.tokenLocked')})); } return this.generateResetToken(email, expires, dbHash).then(function then(generatedToken) { @@ -712,7 +713,7 @@ User = ghostBookshelf.Model.extend({ tokenSecurity[email + '+' + expires] = { count: tokenSecurity[email + '+' + expires] ? tokenSecurity[email + '+' + expires].count + 1 : 1 }; - return Promise.reject(new errors.BadRequestError(i18n.t('errors.models.user.invalidToken'))); + return Promise.reject(new errors.BadRequestError({message: i18n.t('errors.models.user.invalidToken')})); }); }, @@ -724,11 +725,11 @@ User = ghostBookshelf.Model.extend({ dbHash = options.dbHash; if (newPassword !== ne2Password) { - return Promise.reject(new errors.ValidationError(i18n.t('errors.models.user.newPasswordsDoNotMatch'))); + return Promise.reject(new errors.ValidationError({message: i18n.t('errors.models.user.newPasswordsDoNotMatch')})); } if (!validatePasswordLength(newPassword)) { - return Promise.reject(new errors.ValidationError(i18n.t('errors.models.user.passwordDoesNotComplyLength'))); + return Promise.reject(new errors.ValidationError({message: i18n.t('errors.models.user.passwordDoesNotComplyLength')})); } // Validate the token; returns the email address from token @@ -740,7 +741,7 @@ User = ghostBookshelf.Model.extend({ ); }).then(function then(results) { if (!results[0]) { - return Promise.reject(new errors.NotFoundError(i18n.t('errors.models.user.userNotFound'))); + return Promise.reject(new errors.NotFoundError({message: i18n.t('errors.models.user.userNotFound')})); } // Update the user with the new password hash @@ -764,7 +765,7 @@ User = ghostBookshelf.Model.extend({ // check if user has the owner role var currentRoles = contextUser.toJSON(options).roles; if (!_.some(currentRoles, {id: ownerRole.id})) { - return Promise.reject(new errors.NoPermissionError(i18n.t('errors.models.user.onlyOwnerCanTransferOwnerRole'))); + return Promise.reject(new errors.NoPermissionError({message: i18n.t('errors.models.user.onlyOwnerCanTransferOwnerRole')})); } return Promise.join(ghostBookshelf.model('Role').findOne({name: 'Administrator'}), @@ -775,7 +776,7 @@ User = ghostBookshelf.Model.extend({ currentRoles = user.toJSON(options).roles; if (!_.some(currentRoles, {id: adminRole.id})) { - return Promise.reject(new errors.ValidationError('errors.models.user.onlyAdmCanBeAssignedOwnerRole')); + return Promise.reject(new errors.ValidationError({message: i18n.t('errors.models.user.onlyAdmCanBeAssignedOwnerRole')})); } // convert owner to admin diff --git a/core/server/permissions/effective.js b/core/server/permissions/effective.js index 3022bb5a7e2..1b315f87d55 100644 --- a/core/server/permissions/effective.js +++ b/core/server/permissions/effective.js @@ -11,7 +11,7 @@ effective = { .then(function (foundUser) { // CASE: {context: {user: id}} where the id is not in our database if (!foundUser) { - return Promise.reject(new errors.NotFoundError(i18n.t('errors.models.user.userNotFound'))); + return Promise.reject(new errors.NotFoundError({message: i18n.t('errors.models.user.userNotFound')})); } var seenPerms = {}, diff --git a/core/server/permissions/index.js b/core/server/permissions/index.js index c59ca76f49a..fdbdd4d7386 100644 --- a/core/server/permissions/index.js +++ b/core/server/permissions/index.js @@ -56,7 +56,7 @@ function parseContext(context) { } function applyStatusRules(docName, method, opts) { - var err = new errors.NoPermissionError(i18n.t('errors.permissions.applyStatusRules.error', {docName: docName})); + var err = new errors.NoPermissionError({message: i18n.t('errors.permissions.applyStatusRules.error', {docName: docName})}); // Enforce status 'active' for users if (docName === 'users') { @@ -202,7 +202,7 @@ CanThisResult.prototype.buildObjectTypeHandlers = function (objTypes, actType, c return; } - return Promise.reject(new errors.NoPermissionError(i18n.t('errors.permissions.noPermissionToAction'))); + return Promise.reject(new errors.NoPermissionError({message: i18n.t('errors.permissions.noPermissionToAction')})); }); }; diff --git a/core/server/scheduling/SchedulingDefault.js b/core/server/scheduling/SchedulingDefault.js index 2989c10c715..d206e78b9a0 100644 --- a/core/server/scheduling/SchedulingDefault.js +++ b/core/server/scheduling/SchedulingDefault.js @@ -2,6 +2,7 @@ var util = require('util'), moment = require('moment'), request = require('superagent'), SchedulingBase = require(__dirname + '/SchedulingBase'), + errors = require(__dirname + '/../errors'), logging = require(__dirname + '/../logging'); /** @@ -212,7 +213,10 @@ SchedulingDefault.prototype._pingUrl = function (object) { }, self.retryTimeoutInMs); } - logging.error(err); + logging.error(new errors.GhostError({ + err: err, + level: 'critical' + })); } }); }; diff --git a/core/server/scheduling/post-scheduling/index.js b/core/server/scheduling/post-scheduling/index.js index 41a0598fe70..9f0d4be556b 100644 --- a/core/server/scheduling/post-scheduling/index.js +++ b/core/server/scheduling/post-scheduling/index.js @@ -42,11 +42,11 @@ exports.init = function init(options) { client = null; if (!config) { - return Promise.reject(new errors.IncorrectUsage('post-scheduling: no config was provided')); + return Promise.reject(new errors.IncorrectUsageError({message: 'post-scheduling: no config was provided'})); } if (!apiUrl) { - return Promise.reject(new errors.IncorrectUsage('post-scheduling: no apiUrl was provided')); + return Promise.reject(new errors.IncorrectUsageError({message: 'post-scheduling: no apiUrl was provided'})); } return _private.loadClient() diff --git a/core/server/scheduling/utils.js b/core/server/scheduling/utils.js index 7e2bff57a83..b4f1b4554d3 100644 --- a/core/server/scheduling/utils.js +++ b/core/server/scheduling/utils.js @@ -12,7 +12,7 @@ exports.createAdapter = function (options) { contentPath = options.contentPath; if (!activeAdapter) { - return Promise.reject(new errors.IncorrectUsage('Please provide an active adapter.')); + return Promise.reject(new errors.IncorrectUsageError({message: 'Please provide an active adapter.'})); } /** @@ -22,7 +22,7 @@ exports.createAdapter = function (options) { adapter = new (require(activeAdapter))(options); } catch (err) { if (err.code !== 'MODULE_NOT_FOUND') { - return Promise.reject(new errors.IncorrectUsage(err.message)); + return Promise.reject(new errors.IncorrectUsageError({err: err})); } } @@ -34,11 +34,11 @@ exports.createAdapter = function (options) { } catch (err) { // CASE: only throw error if module does exist if (err.code !== 'MODULE_NOT_FOUND') { - return Promise.reject(new errors.IncorrectUsage(err.message)); + return Promise.reject(new errors.IncorrectUsageError({err: err})); } // CASE: if module not found it can be an error within the adapter (cannot find bluebird for example) else if (err.code === 'MODULE_NOT_FOUND' && err.message.indexOf(contentPath + activeAdapter) === -1) { - return Promise.reject(new errors.IncorrectUsage(err.message)); + return Promise.reject(new errors.IncorrectUsageError({err: err})); } } @@ -50,22 +50,22 @@ exports.createAdapter = function (options) { } catch (err) { // CASE: only throw error if module does exist if (err.code === 'MODULE_NOT_FOUND') { - return Promise.reject(new errors.IncorrectUsage('We cannot find your adapter in: ' + contentPath + ' or: ' + internalPath)); + return Promise.reject(new errors.IncorrectUsageError({message: 'We cannot find your adapter in: ' + contentPath + ' or: ' + internalPath})); } - return Promise.reject(new errors.IncorrectUsage(err.message)); + return Promise.reject(new errors.IncorrectUsageError({err: err})); } if (!(adapter instanceof SchedulingBase)) { - return Promise.reject(new errors.IncorrectUsage('Your adapter does not inherit from the SchedulingBase.')); + return Promise.reject(new errors.IncorrectUsageError({message: 'Your adapter does not inherit from the SchedulingBase.'})); } if (!adapter.requiredFns) { - return Promise.reject(new errors.IncorrectUsage('Your adapter does not provide the minimum required functions.')); + return Promise.reject(new errors.IncorrectUsageError({message: 'Your adapter does not provide the minimum required functions.'})); } if (_.xor(adapter.requiredFns, Object.keys(_.pick(Object.getPrototypeOf(adapter), adapter.requiredFns))).length) { - return Promise.reject(new errors.IncorrectUsage('Your adapter does not provide the minimum required functions.')); + return Promise.reject(new errors.IncorrectUsageError({message: 'Your adapter does not provide the minimum required functions.'})); } return Promise.resolve(adapter); diff --git a/core/server/storage/index.js b/core/server/storage/index.js index cb0743958c1..0cf1000638c 100644 --- a/core/server/storage/index.js +++ b/core/server/storage/index.js @@ -23,7 +23,9 @@ function getStorage(type) { // CASE: type does not exist if (!storageChoice) { - throw new errors.IncorrectUsage('No adapter found for type: ' + type); + throw new errors.IncorrectUsageError({ + message: 'No adapter found for type: ' + type + }); } // cache? @@ -37,11 +39,11 @@ function getStorage(type) { } catch (err) { // CASE: only throw error if module does exist if (err.code !== 'MODULE_NOT_FOUND') { - throw new errors.IncorrectUsage(err.message); + throw new errors.IncorrectUsageError({err: err}); } // CASE: if module not found it can be an error within the adapter (cannot find bluebird for example) else if (err.code === 'MODULE_NOT_FOUND' && err.message.indexOf(config.getContentPath('storage') + storageChoice) === -1) { - throw new errors.IncorrectUsage(err.message); + throw new errors.IncorrectUsageError({err: err}); } } @@ -50,24 +52,27 @@ function getStorage(type) { storage[storageChoice] = storage[storageChoice] || require(config.get('paths').internalStoragePath + storageChoice); } catch (err) { if (err.code === 'MODULE_NOT_FOUND') { - throw new errors.IncorrectUsage('We cannot find your adapter in: ' + config.getContentPath('storage') + ' or: ' + config.get('paths').internalStoragePath); + throw new errors.IncorrectUsageError({ + err: err, + context: 'We cannot find your adapter in: ' + config.getContentPath('storage') + ' or: ' + config.get('paths').internalStoragePath + }); } else { - throw new errors.IncorrectUsage(err.message); + throw new errors.IncorrectUsageError({err: err}); } } storage[storageChoice] = new storage[storageChoice](storageConfig); if (!(storage[storageChoice] instanceof Base)) { - throw new errors.IncorrectUsage('Your storage adapter does not inherit from the Storage Base.'); + throw new errors.IncorrectUsageError({message: 'Your storage adapter does not inherit from the Storage Base.'}); } if (!storage[storageChoice].requiredFns) { - throw new errors.IncorrectUsage('Your storage adapter does not provide the minimum required functions.'); + throw new errors.IncorrectUsageError({message:'Your storage adapter does not provide the minimum required functions.'}); } if (_.xor(storage[storageChoice].requiredFns, Object.keys(_.pick(Object.getPrototypeOf(storage[storageChoice]), storage[storageChoice].requiredFns))).length) { - throw new errors.IncorrectUsage('Your storage adapter does not provide the minimum required functions.'); + throw new errors.IncorrectUsageError({message:'Your storage adapter does not provide the minimum required functions.'}); } return storage[storageChoice]; diff --git a/core/server/storage/local-file-store.js b/core/server/storage/local-file-store.js index a1525a78a18..0ce4b4327e9 100644 --- a/core/server/storage/local-file-store.js +++ b/core/server/storage/local-file-store.js @@ -105,10 +105,10 @@ LocalFileStore.prototype.serve = function (options) { return serveStatic(config.getContentPath('images'), {maxAge: utils.ONE_YEAR_MS, fallthrough: false})(req, res, function (err) { if (err) { if (err.statusCode === 404) { - return next(new errors.NotFoundError(i18n.t('errors.errors.pageNotFound'))); + return next(new errors.NotFoundError({message: i18n.t('errors.errors.pageNotFound')})); } - return next(err); + return next(new errors.GhostError({err: err})); } next(); diff --git a/core/server/translations/en.json b/core/server/translations/en.json index 4b12346dcad..4bf4a1b7101 100644 --- a/core/server/translations/en.json +++ b/core/server/translations/en.json @@ -175,7 +175,8 @@ "general": { "maintenance": "Ghost is currently undergoing maintenance, please wait a moment then retry.", "moreInfo": "\nMore info: {info}", - "requiredOnFuture": "This will be required in future. Please see {link}" + "requiredOnFuture": "This will be required in future. Please see {link}", + "internalError": "Something went wrong." }, "httpServer": { "addressInUse": { diff --git a/core/server/update-check.js b/core/server/update-check.js index f5ff2a043ab..a207f4a5406 100644 --- a/core/server/update-check.js +++ b/core/server/update-check.js @@ -31,21 +31,24 @@ var crypto = require('crypto'), api = require('./api'), config = require('./config'), logging = require('./logging'), + errors = require('./errors'), i18n = require('./i18n'), internal = {context: {internal: true}}, allowedCheckEnvironments = ['development', 'production'], checkEndpoint = 'updates.ghost.org', currentVersion = config.get('ghostVersion'); -function updateCheckError(error) { +function updateCheckError(err) { api.settings.edit( {settings: [{key: 'nextUpdateCheck', value: Math.round(Date.now() / 1000 + 24 * 3600)}]}, internal ); - error.context = i18n.t('errors.update-check.checkingForUpdatesFailed.error'); - error.help = i18n.t('errors.update-check.checkingForUpdatesFailed.help', {url: 'http://support.ghost.org'}); - logging.error(error); + logging.error(new errors.GhostError({ + err: err, + context: i18n.t('errors.update-check.checkingForUpdatesFailed.error'), + help: i18n.t('errors.update-check.checkingForUpdatesFailed.help', {url: 'http://support.ghost.org'}) + })); } /** diff --git a/core/test/unit/controllers/frontend/error_spec.js b/core/test/unit/controllers/frontend/error_spec.js index 4f43e3fb950..55e3fd94f87 100644 --- a/core/test/unit/controllers/frontend/error_spec.js +++ b/core/test/unit/controllers/frontend/error_spec.js @@ -2,7 +2,7 @@ var should = require('should'), sinon = require('sinon'), errors = require('../../../../server/errors'), -// Stuff we are testing + // Stuff we are testing handleError = require('../../../../server/controllers/frontend/error'), sandbox = sinon.sandbox.create(); @@ -21,7 +21,7 @@ describe('handleError', function () { }); it('should call next with no args for 404 errors', function () { - var notFoundError = new errors.NotFoundError('Something wasn\'t found'); + var notFoundError = new errors.NotFoundError({message: 'Something wasn\'t found'}); handleError(next)(notFoundError); next.calledOnce.should.be.true(); @@ -29,7 +29,8 @@ describe('handleError', function () { }); it('should call next with error for other errors', function () { - var otherError = new errors.MethodNotAllowedError('Something wasn\'t allowed'); + var otherError = new Error(); + otherError.message = 'Something wasn\'t allowed'; handleError(next)(otherError); diff --git a/core/test/unit/exporter_spec.js b/core/test/unit/exporter_spec.js index 9edff94f95f..f58f12280f2 100644 --- a/core/test/unit/exporter_spec.js +++ b/core/test/unit/exporter_spec.js @@ -1,16 +1,12 @@ var should = require('should'), sinon = require('sinon'), Promise = require('bluebird'), - - // Stuff we're testing db = require('../../server/data/db'), errors = require('../../server/errors'), exporter = require('../../server/data/export'), schema = require('../../server/data/schema'), settings = require('../../server/api/settings'), - schemaTables = Object.keys(schema.tables), - sandbox = sinon.sandbox.create(); describe('Exporter', function () { @@ -85,12 +81,14 @@ describe('Exporter', function () { queryMock.select.returns(new Promise.reject({})); // Execute - exporter.doExport().then(function () { - done(new Error('expected error on export data')); - }).catch(function (err) { - (err instanceof errors.InternalServerError).should.eql(true); - done(); - }); + exporter.doExport() + .then(function () { + done(new Error('expected error for export')); + }) + .catch(function (err) { + (err instanceof errors.DataExportError).should.eql(true); + done(); + }); }); }); diff --git a/core/test/unit/middleware/theme-handler_spec.js b/core/test/unit/middleware/theme-handler_spec.js index 1682c67c786..cd0fed68802 100644 --- a/core/test/unit/middleware/theme-handler_spec.js +++ b/core/test/unit/middleware/theme-handler_spec.js @@ -122,6 +122,7 @@ describe('Theme Handler', function () { describe('updateActiveTheme', function () { it('updates the active theme if changed', function (done) { var activateThemeSpy = sandbox.spy(themeHandler, 'activateTheme'); + sandbox.stub(api.settings, 'read').withArgs(sandbox.match.has('key', 'activeTheme')).returns(Promise.resolve({ settings: [{ key: 'activeKey', @@ -176,8 +177,8 @@ describe('Theme Handler', function () { }); it('throws only warns if theme is missing for admin req', function (done) { - var warnSpy = sandbox.spy(logging, 'warn'), - activateThemeSpy = sandbox.spy(themeHandler, 'activateTheme'); + var activateThemeSpy = sandbox.spy(themeHandler, 'activateTheme'), + loggingWarnStub = sandbox.spy(logging, 'warn'); sandbox.stub(api.settings, 'read').withArgs(sandbox.match.has('key', 'activeTheme')).returns(Promise.resolve({ settings: [{ @@ -185,14 +186,15 @@ describe('Theme Handler', function () { value: 'rasper' }] })); + res.isAdmin = true; blogApp.set('activeTheme', 'not-casper'); configUtils.set({paths: {availableThemes: {casper: {}}}}); themeHandler.updateActiveTheme(req, res, function () { activateThemeSpy.called.should.be.false(); - warnSpy.called.should.be.true(); - warnSpy.calledWith('The currently active theme "rasper" is missing.').should.be.true(); + loggingWarnStub.called.should.be.true(); + loggingWarnStub.calledWith('The currently active theme "rasper" is missing.').should.be.true(); done(); }); }); diff --git a/core/test/unit/migration_spec.js b/core/test/unit/migration_spec.js index 53c190b1214..c4f8104a514 100644 --- a/core/test/unit/migration_spec.js +++ b/core/test/unit/migration_spec.js @@ -201,7 +201,7 @@ describe('Migrations', function () { }) .catch(function (err) { should.exist(err); - (err instanceof errors.InternalServerError).should.eql(true); + (err instanceof errors.GhostError).should.eql(true); createStub.callCount.should.eql(11); done(); }); @@ -226,7 +226,7 @@ describe('Migrations', function () { it('should throw error if versions are too old', function () { var response = update.isDatabaseOutOfDate({fromVersion: '000', toVersion: '002'}); updateDatabaseSchemaStub.calledOnce.should.be.false(); - (response.error instanceof errors.DatabaseVersion).should.eql(true); + (response.error instanceof errors.DatabaseVersionError).should.eql(true); }); it('should just return if versions are the same', function () { @@ -243,7 +243,7 @@ describe('Migrations', function () { it('should throw an error if the database version is higher than the default', function () { var response = update.isDatabaseOutOfDate({fromVersion: '010', toVersion: '004'}); updateDatabaseSchemaStub.calledOnce.should.be.false(); - (response.error instanceof errors.DatabaseVersion).should.eql(true); + (response.error instanceof errors.DatabaseVersionError).should.eql(true); }); }); }); diff --git a/core/test/unit/scheduling/post-scheduling/index_spec.js b/core/test/unit/scheduling/post-scheduling/index_spec.js index f66dda4bb0f..111c80bcf70 100644 --- a/core/test/unit/scheduling/post-scheduling/index_spec.js +++ b/core/test/unit/scheduling/post-scheduling/index_spec.js @@ -104,7 +104,7 @@ describe('Scheduling: Post Scheduling', function () { postScheduling.init() .catch(function (err) { should.exist(err); - (err instanceof errors.IncorrectUsage).should.eql(true); + (err instanceof errors.IncorrectUsageError).should.eql(true); done(); }); }); diff --git a/core/test/unit/scheduling/utils_spec.js b/core/test/unit/scheduling/utils_spec.js index 5220d98de27..f7f41a92ad1 100644 --- a/core/test/unit/scheduling/utils_spec.js +++ b/core/test/unit/scheduling/utils_spec.js @@ -75,7 +75,7 @@ describe('Scheduling: utils', function () { active: __dirname + '/bad-adapter' }).catch(function (err) { should.exist(err); - (err instanceof errors.IncorrectUsage).should.eql(true); + (err instanceof errors.IncorrectUsageError).should.eql(true); done(); }).finally(function () { fs.unlinkSync(__dirname + '/bad-adapter.js'); diff --git a/core/test/unit/server_spec.js b/core/test/unit/server_spec.js index 967bc6371eb..3ab516c11ea 100644 --- a/core/test/unit/server_spec.js +++ b/core/test/unit/server_spec.js @@ -140,7 +140,7 @@ describe('server bootstrap', function () { done('This should not be called'); }) .catch(function (err) { - err.errorType.should.eql('DatabaseVersion'); + err.errorType.should.eql('DatabaseVersionError'); err.message.should.eql('Your database version is not compatible with Ghost 1.0.0 Alpha (master branch)'); done(); }); @@ -159,7 +159,7 @@ describe('server bootstrap', function () { done('This should not be called'); }) .catch(function (err) { - err.errorType.should.eql('DatabaseVersion'); + err.errorType.should.eql('DatabaseVersionError'); err.message.should.eql('Your database version is not compatible with Ghost 1.0.0 Alpha (master branch)'); done(); }); diff --git a/core/test/unit/storage/index_spec.js b/core/test/unit/storage/index_spec.js index 274f0b2f2e0..5deaa84770d 100644 --- a/core/test/unit/storage/index_spec.js +++ b/core/test/unit/storage/index_spec.js @@ -41,7 +41,7 @@ describe('storage: index_spec', function () { try { storage.getStorage('theme'); } catch (err) { - (err instanceof errors.IncorrectUsage).should.eql(true); + (err instanceof errors.IncorrectUsageError).should.eql(true); } }); }); @@ -140,7 +140,7 @@ describe('storage: index_spec', function () { storage.getStorage(); } catch (err) { should.exist(err); - (err instanceof errors.IncorrectUsage).should.eql(true); + (err instanceof errors.IncorrectUsageError).should.eql(true); } }); }); diff --git a/core/test/unit/versioning_spec.js b/core/test/unit/versioning_spec.js index f5eeaff26b8..deb989668b0 100644 --- a/core/test/unit/versioning_spec.js +++ b/core/test/unit/versioning_spec.js @@ -75,7 +75,7 @@ describe('Versioning', function () { done('Should throw an error if the settings table does not exist'); }).catch(function (err) { should.exist(err); - (err instanceof errors.DatabaseNotPopulated).should.eql(true); + (err instanceof errors.DatabaseNotPopulatedError).should.eql(true); knexStub.get.calledOnce.should.be.true(); knexMock.schema.hasTable.calledOnce.should.be.true(); @@ -120,7 +120,7 @@ describe('Versioning', function () { done('Should throw an error if version does not exist'); }).catch(function (err) { should.exist(err); - (err instanceof errors.DatabaseVersion).should.eql(true); + (err instanceof errors.DatabaseVersionError).should.eql(true); knexStub.get.calledTwice.should.be.true(); knexMock.schema.hasTable.calledOnce.should.be.true(); @@ -144,7 +144,7 @@ describe('Versioning', function () { done('Should throw an error if version is not a number'); }).catch(function (err) { should.exist(err); - (err instanceof errors.DatabaseVersion).should.eql(true); + (err instanceof errors.DatabaseVersionError).should.eql(true); knexStub.get.calledTwice.should.be.true(); knexMock.schema.hasTable.calledOnce.should.be.true(); diff --git a/core/test/unit/xmlrpc_spec.js b/core/test/unit/xmlrpc_spec.js index f07747a44db..10f11f5a746 100644 --- a/core/test/unit/xmlrpc_spec.js +++ b/core/test/unit/xmlrpc_spec.js @@ -1,14 +1,16 @@ -var _ = require('lodash'), - nock = require('nock'), - should = require('should'), - sinon = require('sinon'), - rewire = require('rewire'), - testUtils = require('../utils'), - configUtils = require('../utils/configUtils'), - xmlrpc = rewire('../../server/data/xml/xmlrpc'), - events = require('../../server/events'), +var _ = require('lodash'), + nock = require('nock'), + should = require('should'), + sinon = require('sinon'), + http = require('http'), + rewire = require('rewire'), + testUtils = require('../utils'), + configUtils = require('../utils/configUtils'), + xmlrpc = rewire('../../server/data/xml/xmlrpc'), + events = require('../../server/events'), + logging = require('../../server/logging'), // storing current environment - currentEnv = process.env.NODE_ENV; + currentEnv = process.env.NODE_ENV; // To stop jshint complaining should.equal(true, true); @@ -39,7 +41,11 @@ describe('XMLRPC', function () { it('listener() calls ping() with toJSONified model', function () { var testPost = _.clone(testUtils.DataGenerator.Content.posts[2]), - testModel = {toJSON: function () {return testPost; }}, + testModel = { + toJSON: function () { + return testPost; + } + }, pingStub = sandbox.stub(), resetXmlRpc = xmlrpc.__set__('ping', pingStub), listener = xmlrpc.__get__('listener'); @@ -104,29 +110,23 @@ describe('XMLRPC', function () { ping2.isDone().should.be.false(); }); - it('captures errors from requests', function (done) { - var ping1 = nock('http://blogsearch.google.com').post('/ping/RPC2').reply(200), - ping2 = nock('http://rpc.pingomatic.com').post('/').replyWithError('ping site is down'), - testPost = _.clone(testUtils.DataGenerator.Content.posts[2]), - loggingMock, resetXmlRpc; - - loggingMock = { - error: function onError(err) { - should.exist(err); - err.message.should.eql('ping site is down'); - - // Reset xmlrpc handleError method and exit test - resetXmlRpc(); - done(); - } - }; - - resetXmlRpc = xmlrpc.__set__('logging', loggingMock); + it('captures && logs errors from requests', function () { + var testPost = _.clone(testUtils.DataGenerator.Content.posts[2]), + httpMock = sandbox.stub(http, 'request').returns({ + write: function () {}, + end: function () {}, + on: function (eventName, eventDone) { + eventDone(new Error('ping site is down')); + } + }), + loggingStub = sandbox.stub(logging, 'error'); ping(testPost); - ping1.isDone().should.be.true(); - ping2.isDone().should.be.true(); + should.exist(httpMock); + // pinglist contains 2 endpoints, both return ping site is down + loggingStub.calledTwice.should.eql(true); + loggingStub.args[0][0].message.should.eql('ping site is down'); }); }); }); diff --git a/index.js b/index.js index e06e40a4153..7e63be21dad 100644 --- a/index.js +++ b/index.js @@ -4,6 +4,7 @@ var ghost = require('./core'), debug = require('debug')('ghost:boot:index'), express = require('express'), logging = require('./core/server/logging'), + errors = require('./core/server/errors'), utils = require('./core/server/utils'), parentApp = express(); @@ -16,6 +17,6 @@ ghost().then(function (ghostServer) { // Let Ghost handle starting our server instance. ghostServer.start(parentApp); }).catch(function (err) { - logging.error(err); + logging.error(new errors.GhostError({err: err})); process.exit(0); });