Ghost/ghost/admin/app/services/ajax.js
Rish 59ce29e19a Added custom error message handling for newsletter email failures
refs https://github.com/TryGhost/Ghost/issues/11971

- Newsletter preview email request has been using hardcoded fixed error message
- Reads custom error message from server's API response to show when available
2020-07-17 13:54:31 +05:30

276 lines
8.3 KiB
JavaScript

import AjaxService from 'ember-ajax/services/ajax';
import config from 'ghost-admin/config/environment';
import {AjaxError, isAjaxError} from 'ember-ajax/errors';
import {get} from '@ember/object';
import {isArray as isEmberArray} from '@ember/array';
import {isNone} from '@ember/utils';
import {inject as service} from '@ember/service';
const JSON_CONTENT_TYPE = 'application/json';
const GHOST_REQUEST = /\/ghost\/api\//;
function isJSONContentType(header) {
if (!header || isNone(header)) {
return false;
}
return header.indexOf(JSON_CONTENT_TYPE) === 0;
}
/* Version mismatch error */
export class VersionMismatchError extends AjaxError {
constructor(payload) {
super(payload, 'API server is running a newer version of Ghost, please upgrade.');
}
}
export function isVersionMismatchError(errorOrStatus, payload) {
if (isAjaxError(errorOrStatus)) {
return errorOrStatus instanceof VersionMismatchError;
} else {
return get(payload || {}, 'errors.firstObject.type') === 'VersionMismatchError';
}
}
/* Server unreachable error */
export class ServerUnreachableError extends AjaxError {
constructor(payload) {
super(payload, 'Server was unreachable');
}
}
export function isServerUnreachableError(error) {
if (isAjaxError(error)) {
return error instanceof ServerUnreachableError;
} else {
return error === 0 || error === '0';
}
}
/* Request entity too large error */
export class RequestEntityTooLargeError extends AjaxError {
constructor(payload) {
super(payload, 'Request is larger than the maximum file size the server allows');
}
}
export function isRequestEntityTooLargeError(errorOrStatus) {
if (isAjaxError(errorOrStatus)) {
return errorOrStatus instanceof RequestEntityTooLargeError;
} else {
return errorOrStatus === 413;
}
}
/* Unsupported media type error */
export class UnsupportedMediaTypeError extends AjaxError {
constructor(payload) {
super(payload, 'Request contains an unknown or unsupported file type.');
}
}
export function isUnsupportedMediaTypeError(errorOrStatus) {
if (isAjaxError(errorOrStatus)) {
return errorOrStatus instanceof UnsupportedMediaTypeError;
} else {
return errorOrStatus === 415;
}
}
/* Maintenance error */
export class MaintenanceError extends AjaxError {
constructor(payload) {
super(payload, 'Ghost is currently undergoing maintenance, please wait a moment then retry.');
}
}
export function isMaintenanceError(errorOrStatus) {
if (isAjaxError(errorOrStatus)) {
return errorOrStatus instanceof MaintenanceError;
} else {
return errorOrStatus === 503;
}
}
/* Theme validation error */
export class ThemeValidationError extends AjaxError {
constructor(payload) {
super(payload, 'Theme is not compatible or contains errors.');
}
}
export function isThemeValidationError(errorOrStatus, payload) {
if (isAjaxError(errorOrStatus)) {
return errorOrStatus instanceof ThemeValidationError;
} else {
return get(payload || {}, 'errors.firstObject.type') === 'ThemeValidationError';
}
}
/* Host limit reached/exceeded error */
export class HostLimitError extends AjaxError {
constructor(payload) {
super(payload, 'A hosting plan limit was reached or exceeded.');
}
}
export function isHostLimitError(errorOrStatus, payload) {
if (isAjaxError(errorOrStatus)) {
return errorOrStatus instanceof HostLimitError;
} else {
return get(payload || {}, 'errors.firstObject.type') === 'HostLimitError';
}
}
export class EmailError extends AjaxError {
constructor(payload) {
super(payload, 'Please verify your email settings');
}
}
export function isEmailError(errorOrStatus, payload) {
if (isAjaxError(errorOrStatus)) {
return errorOrStatus instanceof EmailError;
} else {
return get(payload || {}, 'errors.firstObject.type') === 'EmailError';
}
}
/* end: custom error types */
let ajaxService = AjaxService.extend({
session: service(),
// flag to tell our ESA authenticator not to try an invalidate DELETE request
// because it's been triggered by this service's 401 handling which means the
// DELETE would fail and get stuck in an infinite loop
// TODO: find a more elegant way to handle this
skipSessionDeletion: false,
get headers() {
return {
'X-Ghost-Version': config.APP.version,
'App-Pragma': 'no-cache'
};
},
init() {
this._super(...arguments);
if (this.isTesting === undefined) {
this.isTesting = config.environment === 'test';
}
},
// ember-ajax recognises `application/vnd.api+json` as a JSON-API request
// and formats appropriately, we want to handle `application/json` the same
_makeRequest(hash) {
if (isJSONContentType(hash.contentType) && hash.type !== 'GET') {
if (typeof hash.data === 'object') {
hash.data = JSON.stringify(hash.data);
}
}
hash.withCredentials = true;
return this._super(hash);
},
handleResponse(status, headers, payload, request) {
if (this.isVersionMismatchError(status, headers, payload)) {
return new VersionMismatchError(payload);
} else if (this.isServerUnreachableError(status, headers, payload)) {
return new ServerUnreachableError(payload);
} else if (this.isRequestEntityTooLargeError(status, headers, payload)) {
return new RequestEntityTooLargeError(payload);
} else if (this.isUnsupportedMediaTypeError(status, headers, payload)) {
return new UnsupportedMediaTypeError(payload);
} else if (this.isMaintenanceError(status, headers, payload)) {
return new MaintenanceError(payload);
} else if (this.isThemeValidationError(status, headers, payload)) {
return new ThemeValidationError(payload);
} else if (this.isHostLimitError(status, headers, payload)) {
return new HostLimitError(payload);
} else if (this.isEmailError(status, headers, payload)) {
return new EmailError(payload);
}
let isGhostRequest = GHOST_REQUEST.test(request.url);
let isAuthenticated = this.get('session.isAuthenticated');
let isUnauthorized = this.isUnauthorizedError(status, headers, payload);
if (isAuthenticated && isGhostRequest && isUnauthorized) {
this.skipSessionDeletion = true;
this.session.invalidate();
}
return this._super(...arguments);
},
normalizeErrorResponse(status, headers, payload) {
if (payload && typeof payload === 'object') {
let errors = payload.error || payload.errors || payload.message || undefined;
if (errors) {
if (!isEmberArray(errors)) {
errors = [errors];
}
payload.errors = errors.map(function (error) {
if (typeof error === 'string') {
return {message: error};
} else {
return error;
}
});
}
}
return this._super(status, headers, payload);
},
isVersionMismatchError(status, headers, payload) {
return isVersionMismatchError(status, payload);
},
isServerUnreachableError(status) {
return isServerUnreachableError(status);
},
isRequestEntityTooLargeError(status) {
return isRequestEntityTooLargeError(status);
},
isUnsupportedMediaTypeError(status) {
return isUnsupportedMediaTypeError(status);
},
isMaintenanceError(status, headers, payload) {
return isMaintenanceError(status, payload);
},
isThemeValidationError(status, headers, payload) {
return isThemeValidationError(status, payload);
},
isHostLimitError(status, headers, payload) {
return isHostLimitError(status, payload);
},
isEmailError(status, headers, payload) {
return isEmailError(status, payload);
}
});
// we need to reopen so that internal methods use the correct contentType
ajaxService.reopen({
contentType: 'application/json; charset=UTF-8'
});
export default ajaxService;