2017-05-29 21:50:03 +03:00
|
|
|
import AjaxService from 'ember-ajax/services/ajax';
|
|
|
|
import config from 'ghost-admin/config/environment';
|
|
|
|
import {AjaxError, isAjaxError} from 'ember-ajax/errors';
|
2017-08-22 10:53:26 +03:00
|
|
|
import {computed} from '@ember/object';
|
|
|
|
import {get} from '@ember/object';
|
|
|
|
import {isArray as isEmberArray} from '@ember/array';
|
|
|
|
import {isNone} from '@ember/utils';
|
2017-10-30 12:38:01 +03:00
|
|
|
import {inject as service} from '@ember/service';
|
2016-01-18 18:37:14 +03:00
|
|
|
|
2018-04-16 19:55:21 +03:00
|
|
|
const JSON_CONTENT_TYPE = 'application/json';
|
|
|
|
const GHOST_REQUEST = /\/ghost\/api\//;
|
|
|
|
const TOKEN_REQUEST = /authentication\/(?:token|ghost|revoke)/;
|
2016-09-26 19:59:04 +03:00
|
|
|
|
|
|
|
function isJSONContentType(header) {
|
2016-10-03 21:08:23 +03:00
|
|
|
if (!header || isNone(header)) {
|
2016-09-26 19:59:04 +03:00
|
|
|
return false;
|
|
|
|
}
|
2018-04-16 19:55:21 +03:00
|
|
|
return header.indexOf(JSON_CONTENT_TYPE) === 0;
|
2016-09-26 19:59:04 +03:00
|
|
|
}
|
|
|
|
|
2016-06-30 17:45:02 +03:00
|
|
|
/* Version mismatch error */
|
|
|
|
|
2017-11-04 01:59:39 +03:00
|
|
|
export function VersionMismatchError(payload) {
|
|
|
|
AjaxError.call(this, payload, 'API server is running a newer version of Ghost, please upgrade.');
|
2016-06-30 17:45:02 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
VersionMismatchError.prototype = Object.create(AjaxError.prototype);
|
|
|
|
|
|
|
|
export function isVersionMismatchError(errorOrStatus, payload) {
|
|
|
|
if (isAjaxError(errorOrStatus)) {
|
|
|
|
return errorOrStatus instanceof VersionMismatchError;
|
|
|
|
} else {
|
|
|
|
return get(payload || {}, 'errors.firstObject.errorType') === 'VersionMismatchError';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Request entity too large error */
|
|
|
|
|
2017-11-04 01:59:39 +03:00
|
|
|
export function ServerUnreachableError(payload) {
|
|
|
|
AjaxError.call(this, payload, 'Server was unreachable');
|
2016-06-14 14:46:24 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
ServerUnreachableError.prototype = Object.create(AjaxError.prototype);
|
|
|
|
|
|
|
|
export function isServerUnreachableError(error) {
|
|
|
|
if (isAjaxError(error)) {
|
|
|
|
return error instanceof ServerUnreachableError;
|
|
|
|
} else {
|
|
|
|
return error === 0 || error === '0';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-04 01:59:39 +03:00
|
|
|
export function RequestEntityTooLargeError(payload) {
|
|
|
|
AjaxError.call(this, payload, 'Request is larger than the maximum file size the server allows');
|
2016-02-26 16:25:47 +03:00
|
|
|
}
|
|
|
|
|
2016-05-22 11:20:02 +03:00
|
|
|
RequestEntityTooLargeError.prototype = Object.create(AjaxError.prototype);
|
|
|
|
|
2016-06-30 17:45:02 +03:00
|
|
|
export function isRequestEntityTooLargeError(errorOrStatus) {
|
|
|
|
if (isAjaxError(errorOrStatus)) {
|
|
|
|
return errorOrStatus instanceof RequestEntityTooLargeError;
|
2016-05-22 11:20:02 +03:00
|
|
|
} else {
|
2016-06-30 17:45:02 +03:00
|
|
|
return errorOrStatus === 413;
|
2016-05-22 11:20:02 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-06-30 17:45:02 +03:00
|
|
|
/* Unsupported media type error */
|
|
|
|
|
2017-11-04 01:59:39 +03:00
|
|
|
export function UnsupportedMediaTypeError(payload) {
|
|
|
|
AjaxError.call(this, payload, 'Request contains an unknown or unsupported file type.');
|
2016-02-26 16:25:47 +03:00
|
|
|
}
|
|
|
|
|
2016-05-22 11:20:02 +03:00
|
|
|
UnsupportedMediaTypeError.prototype = Object.create(AjaxError.prototype);
|
2016-04-12 14:34:40 +03:00
|
|
|
|
2016-06-30 17:45:02 +03:00
|
|
|
export function isUnsupportedMediaTypeError(errorOrStatus) {
|
|
|
|
if (isAjaxError(errorOrStatus)) {
|
|
|
|
return errorOrStatus instanceof UnsupportedMediaTypeError;
|
2016-05-22 11:20:02 +03:00
|
|
|
} else {
|
2016-06-30 17:45:02 +03:00
|
|
|
return errorOrStatus === 415;
|
2016-05-22 11:20:02 +03:00
|
|
|
}
|
|
|
|
}
|
2016-04-12 14:34:40 +03:00
|
|
|
|
2016-07-08 16:54:36 +03:00
|
|
|
/* Maintenance error */
|
|
|
|
|
2017-11-04 01:59:39 +03:00
|
|
|
export function MaintenanceError(payload) {
|
|
|
|
AjaxError.call(this, payload, 'Ghost is currently undergoing maintenance, please wait a moment then retry.');
|
2016-07-08 16:54:36 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
MaintenanceError.prototype = Object.create(AjaxError.prototype);
|
|
|
|
|
|
|
|
export function isMaintenanceError(errorOrStatus) {
|
|
|
|
if (isAjaxError(errorOrStatus)) {
|
|
|
|
return errorOrStatus instanceof MaintenanceError;
|
|
|
|
} else {
|
|
|
|
return errorOrStatus === 503;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-08-24 21:22:20 +03:00
|
|
|
/* Theme validation error */
|
|
|
|
|
2017-11-04 01:59:39 +03:00
|
|
|
export function ThemeValidationError(payload) {
|
|
|
|
AjaxError.call(this, payload, 'Theme is not compatible or contains errors.');
|
2016-08-24 21:22:20 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
ThemeValidationError.prototype = Object.create(AjaxError.prototype);
|
|
|
|
|
|
|
|
export function isThemeValidationError(errorOrStatus, payload) {
|
|
|
|
if (isAjaxError(errorOrStatus)) {
|
|
|
|
return errorOrStatus instanceof ThemeValidationError;
|
|
|
|
} else {
|
|
|
|
return get(payload || {}, 'errors.firstObject.errorType') === 'ThemeValidationError';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-06-30 17:45:02 +03:00
|
|
|
/* end: custom error types */
|
|
|
|
|
2016-10-03 21:08:23 +03:00
|
|
|
let ajaxService = AjaxService.extend({
|
2017-10-30 12:38:01 +03:00
|
|
|
session: service(),
|
2016-01-18 18:37:14 +03:00
|
|
|
|
|
|
|
headers: computed('session.isAuthenticated', function () {
|
|
|
|
let session = this.get('session');
|
2016-06-03 13:51:06 +03:00
|
|
|
let headers = {};
|
2016-01-18 18:37:14 +03:00
|
|
|
|
2016-06-03 13:51:06 +03:00
|
|
|
headers['X-Ghost-Version'] = config.APP.version;
|
2017-08-24 16:36:31 +03:00
|
|
|
headers['App-Pragma'] = 'no-cache';
|
2016-01-18 18:37:14 +03:00
|
|
|
|
2016-06-03 13:51:06 +03:00
|
|
|
if (session.get('isAuthenticated')) {
|
2018-04-23 13:53:42 +03:00
|
|
|
/* eslint-disable camelcase */
|
|
|
|
let {access_token} = session.get('data.authenticated');
|
|
|
|
headers.Authorization = `Bearer ${access_token}`;
|
|
|
|
/* eslint-enable camelcase */
|
2016-01-18 18:37:14 +03:00
|
|
|
}
|
2016-06-03 13:51:06 +03:00
|
|
|
|
|
|
|
return headers;
|
2016-06-13 13:40:08 +03:00
|
|
|
}).volatile(),
|
2016-01-18 18:37:14 +03:00
|
|
|
|
2016-09-26 19:59:04 +03:00
|
|
|
// ember-ajax recognises `application/vnd.api+json` as a JSON-API request
|
|
|
|
// and formats appropriately, we want to handle `application/json` the same
|
2016-10-03 21:08:23 +03:00
|
|
|
_makeRequest(hash) {
|
2016-10-17 16:32:22 +03:00
|
|
|
let isAuthenticated = this.get('session.isAuthenticated');
|
2018-04-16 19:55:21 +03:00
|
|
|
let isGhostRequest = GHOST_REQUEST.test(hash.url);
|
|
|
|
let isTokenRequest = isGhostRequest && TOKEN_REQUEST.test(hash.url);
|
2016-10-17 16:32:22 +03:00
|
|
|
let tokenExpiry = this.get('session.authenticated.expires_at');
|
|
|
|
let isTokenExpired = tokenExpiry < (new Date()).getTime();
|
|
|
|
|
2016-10-03 21:08:23 +03:00
|
|
|
if (isJSONContentType(hash.contentType) && hash.type !== 'GET') {
|
2016-09-26 19:59:04 +03:00
|
|
|
if (typeof hash.data === 'object') {
|
|
|
|
hash.data = JSON.stringify(hash.data);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-10-17 16:32:22 +03:00
|
|
|
// we can get into a situation where the app is left open without a
|
|
|
|
// network connection and the token subsequently expires, this will
|
|
|
|
// result in the next network request returning a 401 and killing the
|
|
|
|
// session. This is an attempt to detect that and restore the session
|
|
|
|
// using the stored refresh token before continuing with the request
|
|
|
|
//
|
|
|
|
// TODO:
|
|
|
|
// - this might be quite blunt, if we have a lot of requests at once
|
|
|
|
// we probably want to queue the requests until the restore completes
|
|
|
|
// BUG:
|
|
|
|
// - the original caller gets a rejected promise with `undefined` instead
|
|
|
|
// of the AjaxError object when session restore fails. This isn't a
|
|
|
|
// huge deal because the session will be invalidated and app reloaded
|
|
|
|
// but it would be nice to be consistent
|
|
|
|
if (isAuthenticated && isGhostRequest && !isTokenRequest && isTokenExpired) {
|
2018-01-05 18:38:23 +03:00
|
|
|
return this.get('session').restore().then(() => this._makeRequest(hash));
|
2016-10-17 16:32:22 +03:00
|
|
|
}
|
|
|
|
|
2016-09-26 19:59:04 +03:00
|
|
|
return this._super(...arguments);
|
|
|
|
},
|
|
|
|
|
2018-04-16 19:55:21 +03:00
|
|
|
handleResponse(status, headers, payload, request) {
|
2016-06-30 17:45:02 +03:00
|
|
|
if (this.isVersionMismatchError(status, headers, payload)) {
|
2017-11-04 01:59:39 +03:00
|
|
|
return new VersionMismatchError(payload);
|
2016-06-14 14:46:24 +03:00
|
|
|
} else if (this.isServerUnreachableError(status, headers, payload)) {
|
2017-11-04 01:59:39 +03:00
|
|
|
return new ServerUnreachableError(payload);
|
2016-06-30 17:45:02 +03:00
|
|
|
} else if (this.isRequestEntityTooLargeError(status, headers, payload)) {
|
2017-11-04 01:59:39 +03:00
|
|
|
return new RequestEntityTooLargeError(payload);
|
2016-05-22 11:20:02 +03:00
|
|
|
} else if (this.isUnsupportedMediaTypeError(status, headers, payload)) {
|
2017-11-04 01:59:39 +03:00
|
|
|
return new UnsupportedMediaTypeError(payload);
|
2016-07-08 16:54:36 +03:00
|
|
|
} else if (this.isMaintenanceError(status, headers, payload)) {
|
2017-11-04 01:59:39 +03:00
|
|
|
return new MaintenanceError(payload);
|
2016-08-24 21:22:20 +03:00
|
|
|
} else if (this.isThemeValidationError(status, headers, payload)) {
|
2017-11-04 01:59:39 +03:00
|
|
|
return new ThemeValidationError(payload);
|
2016-02-26 16:25:47 +03:00
|
|
|
}
|
|
|
|
|
2018-04-16 19:55:21 +03:00
|
|
|
let isGhostRequest = GHOST_REQUEST.test(request.url);
|
|
|
|
let isAuthenticated = this.get('session.isAuthenticated');
|
|
|
|
let isUnauthorized = this.isUnauthorizedError(status, headers, payload);
|
|
|
|
|
|
|
|
if (isAuthenticated && isGhostRequest && isUnauthorized) {
|
2016-09-26 19:59:04 +03:00
|
|
|
this.get('session').invalidate();
|
|
|
|
}
|
|
|
|
|
2016-02-26 16:25:47 +03:00
|
|
|
return this._super(...arguments);
|
|
|
|
},
|
|
|
|
|
2016-01-18 18:37:14 +03:00
|
|
|
normalizeErrorResponse(status, headers, payload) {
|
|
|
|
if (payload && typeof payload === 'object') {
|
2016-09-26 19:59:04 +03:00
|
|
|
let errors = payload.error || payload.errors || payload.message || undefined;
|
2016-09-26 16:07:18 +03:00
|
|
|
|
2016-09-26 19:59:04 +03:00
|
|
|
if (errors) {
|
|
|
|
if (!isEmberArray(errors)) {
|
|
|
|
errors = [errors];
|
2016-09-26 16:07:18 +03:00
|
|
|
}
|
2016-09-26 19:59:04 +03:00
|
|
|
|
2018-01-05 18:38:23 +03:00
|
|
|
payload.errors = errors.map(function (error) {
|
2016-09-26 19:59:04 +03:00
|
|
|
if (typeof error === 'string') {
|
|
|
|
return {message: error};
|
|
|
|
} else {
|
|
|
|
return error;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2016-01-18 18:37:14 +03:00
|
|
|
}
|
2016-04-04 13:44:54 +03:00
|
|
|
|
|
|
|
return this._super(status, headers, payload);
|
2016-02-26 16:25:47 +03:00
|
|
|
},
|
|
|
|
|
2016-06-30 17:45:02 +03:00
|
|
|
isVersionMismatchError(status, headers, payload) {
|
|
|
|
return isVersionMismatchError(status, payload);
|
|
|
|
},
|
|
|
|
|
2016-11-14 16:16:51 +03:00
|
|
|
isServerUnreachableError(status) {
|
2016-06-14 14:46:24 +03:00
|
|
|
return isServerUnreachableError(status);
|
|
|
|
},
|
|
|
|
|
2016-11-14 16:16:51 +03:00
|
|
|
isRequestEntityTooLargeError(status) {
|
2016-05-22 11:20:02 +03:00
|
|
|
return isRequestEntityTooLargeError(status);
|
2016-04-12 14:34:40 +03:00
|
|
|
},
|
|
|
|
|
2016-11-14 16:16:51 +03:00
|
|
|
isUnsupportedMediaTypeError(status) {
|
2016-05-22 11:20:02 +03:00
|
|
|
return isUnsupportedMediaTypeError(status);
|
2016-07-08 16:54:36 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
isMaintenanceError(status, headers, payload) {
|
|
|
|
return isMaintenanceError(status, payload);
|
2016-08-24 21:22:20 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
isThemeValidationError(status, headers, payload) {
|
|
|
|
return isThemeValidationError(status, payload);
|
2016-01-18 18:37:14 +03:00
|
|
|
}
|
|
|
|
});
|
2016-10-03 21:08:23 +03:00
|
|
|
|
|
|
|
// we need to reopen so that internal methods use the correct contentType
|
|
|
|
ajaxService.reopen({
|
|
|
|
contentType: 'application/json; charset=UTF-8'
|
|
|
|
});
|
|
|
|
|
|
|
|
export default ajaxService;
|