/** * Interceptor * Implements functionality to catch various requests and fire events when they happen. This is generally to ensure * that responses from the server are handled in a uniform fashion across the application. Also, by firing events * it allows to have any number of handlers attach to the response. * * @author Kirk Bushell * @date 28th March 2013 */ var module = angular.module('core.interceptor', []); module.service( 'Messages', [ '$filter', function( $filter ) { var service = { /** * Store all the messages here. The default (base) or the custom messages. * * @type {Object} */ messages: { base: { error: { create: 'Could not create :resource. Please try again.', read: 'Could not load :resource. Please try again.', update: 'Could not update :resource. Please try again.', 'delete': 'Could not delete :resource. Please try again.' }, success: { create: ':resource created.', read: 'Loaded :resource successfully.', update: ':resource saved.', 'delete': ':resource deleted.' } }, // Custom messages are stored here. custom: {} }, get: function( resource , action , type ) { resource.split( '-' ).join( ' ' ); resource = $filter( 'ucfirst' )( resource ); var customResource = get( service.messages.custom[ resource.toLowerCase() ] ); if ( customResource != null ) { var custom = get( customResource[ type ][ action ] ); if ( custom != null ) return custom; } // Get the default message, if available, and perform the replacement. var msg = get( service.messages.base[ type ][ action ] ); if ( msg && msg.length > 0 ) { msg = msg.split( ':resource' ).join( resource.singularize() ); } return msg; }, /** * Registers a resource and all its custom messages. * * @param {String} resource Resource name * @param {Object} messages An object with create, read, update, delete keys * * @return {void} */ register: function( resource , messages ) { service.messages.custom[ resource ] = messages; } }; return service; }]); module.config(['$httpProvider', function($httpProvider) { var interceptor = ['$rootScope', '$q', 'Notify', 'Messages', 'Analytics', function($rootScope, $q, Notify, Messages, Analytics ) { /** * Parses the resource based on the url that was sent. * * @param {object} response Response object * * @return {string} */ var getResourceFromResponse = function( response ) { var urlParts = response.config.url.split('?'), url = urlParts[0].replace(/^\/|\/$/g, ''), // strip first and last slash. base = $rootScope.config.app.base.replace(/^\/|\/$/g, ''); // strip first and last slash. // If there's a base, let's strip it out. if ( base.length ) { url = url.replace( base , '' ); } urlParts = url.split('/'); url = urlParts[0]; return url; } /** * Extracts an ID from a URL, if it's available. * * @param {string} resource The name of the resource. * @param {string} url The URL to parse. * * @return {mixed} Returns the extracted ID or null. */ var getIDFromURL = function( resource , url ) { var urlParts = url.split( resource ), possibleID = urlParts[ 1 ], id; // If there is nothing, we know we're creating and not updating. // Therefore, there will be no ID to return. if ( !possibleID ) return null; // Remove the first occurance of a slash. possibleID = possibleID.replace('/', ''); // Check if there are any more slashes, so we know whether we should split // the possible ID variable or just return it. if ( possibleID.indexOf('/') === -1 ) { return isNaN( possibleID ) ? null : possibleID; } // Since at this point we know that the url still has a slash in it, we will // split on that slash and get the first part of the array. id = possibleID.split('/')[0]; // Now we want to check if the value is a number or not. return isNaN( id ) ? null : id; }; /** * Returns a custom resource action, if it has been supplied in the URL. This is defined * by a string representation AFTER the resource id. Eg. * * /entries/1/submit * * @param string resource * @param string url * @return mixed string on success, null on failure */ var getActionFromUrl = function( resource, url ) { url = url.replace( config.app.base, '' ).split( '/' ); url.shift(); // Could be dealing with an integer or extra action if ( url.length > 1 ) { if ( isNaN( url[ 1 ] ) ) { // custom action return url[ 1 ]; } if ( url.length > 2 && isNaN( url[ 2 ] ) ) { return url[ 2 ]; } } return null; }; /** * Based on the data returned from the server whenever there's a validation error * we will construct a single validation message that is displayed in an alert. * * @param {Object} data The data object returned from the server. * * @return {String} */ var getValidationMessages = function( response ) { var errors = []; // Check if the response is empty, meaning there are no validation errors. if ( getResponseType(response) != 'validation' || $.isEmptyObject( response.data ) || $.isEmptyObject( response.data.errors ) ) return errors; // Put all the errors from all the fields into 1 array. Basically flatenning the array. angular.forEach( response.data.errors , function( issues ) { angular.forEach( issues , function( error ) { errors.push( error ); }); }); return errors; } var getResponseType = function( response ) { return get( response.headers()[ 'x-response-type' ] ); } var notificationate = function( response ) { var method = response.config.method.toLowerCase(), action = '', status = response.status, type = 'Error', // notification type. Error, Success, Info or Warning. responseType = getResponseType( response ), // custom response type. message = null, resource, possibleAction; // Ignore GET requests. if ( method == 'get' && status == 200 ) return; // Any status 200 responses at this point are for successful operations. // So we will change the notification type to Success. if ( status == 200 ) type = 'Success'; // Parse the resource name. resource = getResourceFromResponse( response ); // Do not display any message for exceptions. if ( resource == 'exceptions' ) return; // Let's determine what action is taken based on the request method. switch ( method ) { case 'post': action = 'create'; break; case 'get': action = 'read'; break; case 'put': action = 'update'; break; case 'delete': action = 'delete'; break; } // Check if there's an ID in the URL whenever we send a post request because the action // could be update instead of create. // This is done because ngResource sends a POST request for updates instead of PUT. if ( method == 'post' ) { if ( getIDFromURL( resource , response.config.url ) ) { action = 'update'; } } // Set up our action based on whether or not a custom one has been defined in the URL possibleAction = getActionFromUrl( resource, response.config.url ); if ( possibleAction ) { action = possibleAction; } // Based on the response status. switch ( status ) { case 400: if ( responseType == 'validation' ) { if ( typeof response.data.message == 'string' ) { message = response.data.message; } } else { message = response.data; } break; case 401: message = 'Your current session has expired. Please log in.'; break; case 403: message = 'You do not have sufficient permission to access this resource.'; break; case 500: if ( angular.isString( response.data ) && response.data.length ) { message = response.data; } break; } if ( !message ) { message = Messages.get( resource , action , type.toLowerCase() ); } // Send an update to the validation-errors directive to show/hide validation errors. $rootScope.$broadcast( 'validation.errors', getValidationMessages( response ) ); if ( message ) { Notify[type]( message ); // Google Analytics event tracking. Analytics.trackEvent( resource , type , message ); } } /** * Broadcasts an event that any part of the app can listen to * and perform custom actions. * * @param string name The event name * @param mixed response The response that comes back from the server * * @return void */ var broadcast = function( name , response ) { $rootScope.$broadcast( name , response ); notificationate( response ); } /** * Successful response handler. * * @param mixed response The response th at comes back from the server * * @return mixed */ var success = function( response ) { broadcast( 'app.success' , response ); return response; } /** * Invalid response handler. * * @param mixed response The response th at comes back from the server * * @return mixed */ var error = function( response ) { // This is the default error event to broadcast. // It may be overwritten depending on the response status. var event = 'app.unknown-error'; switch (response.status) { case 400: event = 'app.error'; break; // Bad requests (validation errors.etc.) case 401: event = 'app.unauthorised'; break; // Unauthorised, should require login case 403: event = 'app.forbidden'; break; // Forbidden, user is simply not allowed access case 500: event = 'app.failure'; break; // Critical error on the server, catch and display } broadcast( event , response ); return $q.reject( response ); } return function(promise) { return promise.then( success, error ); }; }]; $httpProvider.responseInterceptors.push( interceptor ); }]);