/*
* Your installation or use of this SugarCRM file is subject to the applicable
* terms available at
* http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/.
* If you do not agree to all of the applicable terms or do not have the
* authority to bind the entity as an authorized representative, then do not
* install or use this SugarCRM file.
*
* Copyright (C) SugarCRM Inc. All rights reserved.
*/
/**
* Error manager for XHR errors.
*
* This module allows you to provide custom handling depending on the
* xhr error.
* Below is the exhaustive list of functions you can implement, with the
* description of the error it will handle:
*
* ### Authentication error handler functions
*
* OAuth2 uses 400 error to catch all authentication errors; see:
* http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-5.2
*
* **handleInvalidGrantError**
*
* The provided authorization grant is invalid, expired, revoked, does
* not match the redirection URI used in the authorization request, or
* was issued to another client. Note that the server implementation
* will override invalid_grant as needs_login as a special case (see below).
*
* **handleNeedLoginError**
*
* The server shall use this in place of invalid_grant to tell client to
* handle error specifically as caused due to invalid credentials being
* supplied. The reason server needs to use this is because an invalid_grant
* oauth error may also be caused by invalid or expired token.
* Using needs_login allows all clients to provide proper messaging to end
* user without the need for extra logic.
*
* **handleInvalidClientError**
*
* Client authentication failed (e.g. unknown client, no client
* authentication included, multiple client authentications included,
* or unsupported authentication method).
*
* **handleInvalidRequestError**
*
* The request is missing a required parameter, includes an unsupported
* parameter or parameter value, repeats a parameter, includes multiple
* credentials, utilizes more than one mechanism for authenticating the
* client, or is otherwise malformed.
*
* **handleUnauthorizedClientError**
*
* The authenticated client is not authorized to use this authorization
* grant type.
*
* **handleUnsupportedGrantTypeError**
*
* The authorization grant type is not supported by the authorization
* server.
*
* **handleInvalidScopeError**
*
* The requested scope is invalid, unknown, malformed, or exceeds the scope
* granted by the resource owner.
*
* ### Other error handler functions.
*
* **handleTimeoutError**
*
* **handleUnspecified400Error**
*
* **handleUnauthorizedError**
*
* **handleForbiddenError**
*
* **handleNotFoundError**
*
* **handleMethodNotAllowedError**
*
* **handleMethodConflictError**
*
* **handleHeaderPreconditionFailed**
*
* **handleValidationError**
*
* **handleMethodFailure**
*
* **ErrorhandleServerError**
*``
* ### Example of usage:
*```
* const Handlers = {
* handleTimeoutError: function() {
* console.log('The request has timed out.')
* },
* handleServerError: function() {
* console.log('Something went wrong with the server, please contact
* your admin');
* }
* }
*
* const ErrorHandler = _.extend(require('./error'), Handlers);
*```
*
* @module Core/Error
*/
const Alert = require('../view/alert');
const Utils = require('../utils/utils');
const Language = require('./language');
/**
* Calls the given custom handler callback, or falls back to
* {@link module:Core/Error#handleStatusCodesFallback the default one}
*
* @param {Api.HttpError} error AJAX error.
* @param {Function} fn The custom handler callback.
* @param {Object} params The params for the custom callback.
* @private
*/
function callCustomHandler(error, fn, params) {
if (fn) {
fn.apply(ErrorHandler, _.union([error], params || []));
} else {
ErrorHandler.handleStatusCodesFallback(error);
}
};
/**
* Calls appropriate authentication error handler.
*
* @param {Api.HttpError} error HTTP error.
* @param {Function} [alternativeCallback] If this does not match an
* expected oauth error then this callback will be called (if provided).
* @private
*/
function handleFineGrainedError(error, alternativeCallback) {
var handlerName = 'handle' + Utils.classify(error.code) + 'Error';
(ErrorHandler[handlerName] || alternativeCallback || ErrorHandler.handleStatusCodesFallback).call(ErrorHandler, error);
};
/**
* @alias module:Core/Error
*/
const ErrorHandler = {
/**
* Binds a `window.onerror` callback to log error using sidecar's logger.
*/
init: function() {
this.enableOnError();
Object.defineProperty(this, 'remoteLogging', {
get: function() {
SUGAR.App.logger.warn('`Core.Error#remoteLogging` property has been deprecated since 7.10.');
return false;
},
configurable: true
});
},
/**
* Sets properties and binds a `window.onerror` callback to log error using
* the logger.
*
* @param {Object} opts A hash of options.
* @deprecated since 7.10.
*/
initialize: function(opts) {
SUGAR.App.logger.warn('The function `Core.Error#initialize` is deprecated in 7.10. ' +
'Please do not use it. The function initializing the component is `Core.Error#init`');
opts = opts || {};
this.statusCodes = (opts.statusCodes) ? _.extend(this.statusCodes, opts.statusCodes) : this.statusCodes;
if (!opts.disableOnError) {
this.enableOnError();
}
},
/**
* A hashmap of status codes mapping to their handler functions.
*
* Each status code calls a specific method that you need to implement if
* you want a custom handling. If you do not implement it,
* {@link module:Core/Error.handleStatusCodesFallback} will be used.
*
* @class
* @name Core/Error.StatusCodes
*/
/**
* @type {Core/Error.StatusCodes}
*/
statusCodes: {
/**
* If the error has `textStatus` property set to `timeout`, calls
* `handleTimeoutError`.
*
* @param {Api.HttpError} error The AJAX error.
* @param {Data/Bean|Data/BeanCollection} model The model or collection
* on which the request was made.
* @param {Object} options A hash of options.
* @memberOf Core/Error.StatusCodes
*/
'0': function(error, model, options) {
if (error.textStatus === 'timeout') {
callCustomHandler(error, this.handleTimeoutError);
} else {
// TODO: Need invalid url, and any other possible status: 0 conditions
this.handleStatusCodesFallback(error, model, options);
}
},
/**
* Authentication or bad request error.
*
* Calls one of the authentication errors handlers, based on the error
* code. Please check the list of possible handlers function in the
* description of this module. If no authentication error handler is
* implemented, it calls `handleUnspecified400Error`.
*
* @param {Api.HttpError} error HTTP error.
* @memberOf Core/Error.StatusCodes
*/
'400': function(error) {
handleFineGrainedError(error, this.handleUnspecified400Error);
},
/**
* Unauthorized.
*
* Calls one of the authentication errors handlers, based on the error
* code. Please check the list of possible handlers function in the
* description of this module. If no authentication error handler is
* implemented, it calls `handleUnauthorizedError`.
*
* @param {Api.HttpError} error HTTP error.
*/
'401': function(error) {
handleFineGrainedError(error, this.handleUnauthorizedError);
},
/**
* Forbidden.
*
* Calls `handleForbiddenError`.
*
* @param {Api.HttpError} error HTTP error.
* @memberOf Core/Error.StatusCodes
*/
'403': function(error) {
callCustomHandler(error, this.handleForbiddenError);
},
/**
* Not found.
*
* Calls `handleNotFoundError`.
*
* @param {Api.HttpError} error HTTP error.
* @memberOf Core/Error.StatusCodes
*/
'404': function(error) {
callCustomHandler(error, this.handleNotFoundError, _.rest(arguments));
},
/**
* Method not allowed.
*
* Calls `handleMethodNotAllowedError`.
*
* @param {Api.HttpError} error HTTP error.
* @memberOf Core/Error.StatusCodes
*/
'405': function(error) {
callCustomHandler(error, this.handleMethodNotAllowedError);
},
/**
* Conflict.
*
* Calls `handleMethodConflictError`.
*
* @param {Api.HttpError} error HTTP error.
* @memberOf Core/Error.StatusCodes
*/
'409': function(error) {
callCustomHandler(error, this.handleMethodConflictError);
},
/**
* Precondition failed.
*
* Calls `handleHeaderPreconditionFailed`.
*
* @param {Api.HttpError} error HTTP error.
* @memberOf Core/Error.StatusCodes
*/
'412': function(error) {
callCustomHandler(error, this.handleHeaderPreconditionFailed);
},
/**
* Precondition failure.
*
* Clients can optionally sniff the error property in JSON for finer
* grained determination; the following values may be:
* missing_parameter, invalid_parameter
*
* Calls `handleValidationError`.
*
* @param {Api.HttpError} error HTTP error.
* @param {Data/Bean|Data/BeanCollection} model The model or collection
* on which the request was made.
* @memberOf Core/Error.StatusCodes
*/
'422': function(error, model) {
error.model = model;
callCustomHandler(error, this.handleValidationError, _.rest(arguments));
},
/**
* Request Method Failure.
*
* Calls `handleMethodFailureError`.
*
* @param {Api.HttpError} error HTTP error.
* @param {Data/Bean|Data/BeanCollection} model The model or
* collection on which the request was made.
* @memberOf Core/Error.StatusCodes
*/
'424': function(error, model) {
error.model = model;
callCustomHandler(error, this.handleMethodFailureError, _.rest(arguments));
},
/**
* Internal server error
*
* Calls `handleServerError`.
*
* @param {Api.HttpError} error HTTP error.
* @memberOf Core/Error.StatusCodes
*/
'500': function(error) {
callCustomHandler(error, this.handleServerError);
},
/**
* Bad Gateway
*
* Calls `handleServerError`.
*
* @param {Api.HttpError} error HTTP error.
* @memberOf Core/Error.StatusCodes
*/
'502': function(error) {
callCustomHandler(error, this.handleServerError);
},
/**
* Service Unavailable
*
* Calls `handleServerError`.
*
* @param {Api.HttpError} error HTTP error.
* @memberOf Core/Error.StatusCodes
*/
'503': function(error) {
callCustomHandler(error, this.handleServerError);
}
},
/**
* Maps validator names to error labels.
*/
errorName2Keys: {
'maxValue': 'ERROR_MAXVALUE',
'minValue': 'ERROR_MINVALUE',
'maxLength': 'ERROR_MAX_FIELD_LENGTH',
'minLength': 'ERROR_MIN_FIELD_LENGTH',
'datetime': 'ERROR_DATETIME',
'required': 'ERROR_FIELD_REQUIRED',
'email': 'ERROR_EMAIL',
'primaryEmail': 'ERROR_PRIMARY_EMAIL',
'duplicateEmail': 'ERROR_DUPLICATE_EMAIL',
'number': 'ERROR_NUMBER',
'isBefore': 'ERROR_IS_BEFORE',
'isAfter': 'ERROR_IS_AFTER',
'greaterThan': 'ERROR_IS_GREATER_THAN',
'lessThan': 'ERROR_IS_LESS_THAN'
},
/**
* Returns error strings given a error key and context.
*
* @param {string} errorKey The error key we want to get the error message
* from.
* @param {Object} [context] The template context to pass to the
* string/template.
* @return {string} The i18n error string associated with the given
* `errorKey` and filled in by the `context`.
*/
getErrorString: function(errorKey, context) {
var module = context.module || '';
return Language.get(this.errorName2Keys[errorKey] || errorKey, module, context);
},
/**
* Handles validation errors.
*
* By default this just pipes the error to the error logger.
*
* @param {Api.HttpError} error The AJAX error.
*/
handleValidationError: function(error) {
var errors = error.responseText;
// TODO: Right now doesn't stringify the error, add it in when we finalize the
// structure of the error.
// TODO: Likely, we'll have a 'Saving...' alert, etc., and so we just dismiss all
// since we don't know the alert key. Ostensibly, validation errors will show
// field by field; so feedback will be provided as appropriate.
Alert.dismissAll();
_.each(errors, function(fieldError, key) {
var errorMsg = '';
if (_.isObject(fieldError)) {
_.each(fieldError, function(result, fieldName) {
errorMsg += '(Message) ' + this.getErrorString(fieldName, result) + '\n';
}, this);
} else {
errorMsg = fieldError;
}
SUGAR.App.logger.debug("validation failed for field `" + key + "`:\n" + errorMsg);
}, this);
},
/**
* Handles http errors returned from AJAX calls.
*
* This method calls the relevant handler function using
* {Core.Error#statusCodes}
*
* @param {Api.HttpError} error AJAX error.
* @param {Data/Bean|Data/BeanCollection} model The model or collection on
* which the request was made.
* @param {Object} options A hash of options.
*/
handleHttpError: function(error, model, options) {
// We use `ErrorHandler` instead of `this` because this function is set to
// `Api.defaultErrorHandler`. We want to make sure the scope is always
// the Core.Error object.
if (ErrorHandler.statusCodes[error.status]) {
ErrorHandler.statusCodes[error.status].call(ErrorHandler, error, model, options);
} else {
// TODO: Default catch all error code handler
// Temporarily going to the handleStatusCodesFallback handler but will probably need
// to go to a sensible "all other errors" type of handler.
ErrorHandler.handleStatusCodesFallback(error);
}
},
/**
* Handles unhandled javascript exceptions which are reported via
* `window.onerror` event.
*
* The default implementation logs the error with level `FATAL`.
*
* @param {string} message Error message.
* @param {string} url URL of script.
* @param {string} line Line number of script.
*/
handleUnhandledError: function(message, url, line) {
SUGAR.App.logger.fatal(message + ' at ' + url + ' on line ' + line);
},
/**
* This is the fallback error handler if the custom status code specific
* handler is not implemented. To define custom error handlers, you should
* include your script from index page and do something like:
*
* @param {string} error The AJAX error.
*/
handleStatusCodesFallback: function(error) {
SUGAR.App.logger.error(error.toString());
},
/**
* Handles render related errors.
*
* @param {View/Component} component The component that triggered
* the error.
* @param {string} method The method that caught the error. Example:
* `_renderHtml`.
* @param {string} [additionalInfo] Any additional information relevant
* for that particular method.
*/
handleRenderError: function(component, method, additionalInfo) {
var id = 'render_error_' + component.module + '_' + component.name;
var level = 'error'; //Default message level
var title;
var messages;
// TODO: Add corresponding language agnostic app strings for title/message and use that instead.
switch (method) {
case '_renderHtml':
title = Language.get('ERR_RENDER_FAILED_TITLE');
messages = [Language.get('ERR_RENDER_FAILED_MSG'),
Language.get('ERR_CONTACT_TECH_SUPPORT')];
break;
case '_renderField':
title = Language.get('ERR_RENDER_FIELD_FAILED_TITLE');
messages = [Utils.formatString(Language.get('ERR_RENDER_FIELD_FAILED_MSG'),
[additionalInfo.name]), Language.get('ERR_CONTACT_TECH_SUPPORT')];
break;
case 'view_render_denied':
title = Language.get('ERR_NO_VIEW_ACCESS_TITLE');
level = 'warning'; // This isn't an application error, this is ACL enforcement.
var module = Language.getModuleName(component.module, {plural: true});
messages = [Utils.formatString(Language.get('ERR_NO_VIEW_ACCESS_MSG'),[module])];
break;
case 'layout_render':
title = Language.get('ERR_LAYOUT_RENDER_TITLE');
messages = [Language.get('ERR_LAYOUT_RENDER_MSG')];
break;
default:
// This shouldn't happen
title = Language.get('ERR_GENERIC_TITLE');
messages = [Language.get('ERR_INTERNAL_ERR_MSG'),
Language.get('ERR_CONTACT_TECH_SUPPORT')];
SUGAR.App.logger.error('handleRenderError called for render error caught in ' + method + ', but we have no corresponding handler!');
break;
}
Alert.show(id, {
level: level,
title: title,
messages: messages
});
},
/**
* Binds a custom handler for `window.onerror` event. Does nothing if it has
* already been overloaded.
*
* Calls the provided `handler`, or falls back to
* {Core.Error#handleUnhandledError} and then calls the original handler
* if defined.
*
* @param {Function} handler Callback function to call on error.
* @param {Object} context The scope of the handler.
* @return {boolean} `false` if onerror has already been overloaded.
*/
enableOnError: function(handler, context) {
var originalHandler;
var self = this;
if (this.overloaded) {
return false;
}
originalHandler = window.onerror;
window.onerror = function(mesg, url, line) {
if (handler) {
handler.call(context);
} else {
self.handleUnhandledError(mesg, url, line);
}
if (originalHandler) {
originalHandler();
}
};
this.overloaded = true;
return true;
},
/**
* Inserts call stack string to Error message.
* `window.onerror` handler is not provided with Error object
* (4th argument) [in Safari][1].
*
* [1]: https://bugs.webkit.org/show_bug.cgi?id=55092
*
* @param {string|Error} error Error text or Error object.
* @param {boolean} [skipThrow=false] If `true`, skips exception
* raising.
* @return {Error} Error object with stack trace inserted into
* `Error.message` property.
* @deprecated since 7.10.
*/
throwErrorWithCallStack: function(error, skipThrow) {
SUGAR.App.logger.warn('The function `Core.Error#throwErrorWithCallStack` is deprecated in 7.10. ' +
'Please do not use it.');
if (_.isString(error)) {
error = new Error(error);
}
error.message = error.message + '; ' + error.stack;
if (skipThrow) {
return error;
}
throw error;
},
_callCustomHandler: function(error, fn, params) {
if (!SUGAR.App.config.sidecarCompatMode) {
SUGAR.App.logger.error('Core.Error#_callCustomHandler is a private method that you are not allowed ' +
'to access. Please use only the public API.');
return;
}
SUGAR.App.logger.warn('Core.Error#_callCustomHandler is a private method that you should not access. ' +
'You will NOT be allowed to access it in the next release. Please update your code to rely on the public ' +
'API only.');
return callCustomHandler(error, fn, params);
},
_handleFineGrainedError: function(error, alternativeCallback) {
if (!SUGAR.App.config.sidecarCompatMode) {
SUGAR.App.logger.error('Core.Error#_handleFineGrainedError is a private method that you are not allowed ' +
'to access. Please use only the public API.');
return;
}
SUGAR.App.logger.warn('Core.Error#_handleFineGrainedError is a private method that you should not access. ' +
'You will NOT be allowed to access it in the next release. Please update your code to rely on the public ' +
'API only.');
return handleFineGrainedError(error, alternativeCallback);
}
};
module.exports = ErrorHandler;