(function () { angular.module('umbraco.security.retryQueue', []); angular.module('umbraco.security.interceptor', ['umbraco.security.retryQueue']); angular.module('umbraco.security', [ 'umbraco.security.retryQueue', 'umbraco.security.interceptor' ]); //TODO: This is silly and unecessary to have a separate module for this angular.module('umbraco.security.retryQueue', []) // This is a generic retry queue for security failures. Each item is expected to expose two functions: retry and cancel. .factory('securityRetryQueue', [ '$q', '$log', function ($q, $log) { var retryQueue = []; var retryUser = null; var service = { // The security service puts its own handler in here! onItemAddedCallbacks: [], hasMore: function () { return retryQueue.length > 0; }, push: function (retryItem) { retryQueue.push(retryItem); // Call all the onItemAdded callbacks angular.forEach(service.onItemAddedCallbacks, function (cb) { try { cb(retryItem); } catch (e) { $log.error('securityRetryQueue.push(retryItem): callback threw an error' + e); } }); }, pushRetryFn: function (reason, userName, retryFn) { // The reason parameter is optional if (arguments.length === 2) { retryFn = userName; userName = reason; reason = undefined; } if (retryUser && retryUser !== userName || userName === null) { throw new Error('invalid user'); } retryUser = userName; // The deferred object that will be resolved or rejected by calling retry or cancel var deferred = $q.defer(); var retryItem = { reason: reason, retry: function () { // Wrap the result of the retryFn into a promise if it is not already $q.when(retryFn()).then(function (value) { // If it was successful then resolve our deferred deferred.resolve(value); }, function (value) { // Othewise reject it deferred.reject(value); }); }, cancel: function () { // Give up on retrying and reject our deferred deferred.reject(); } }; service.push(retryItem); return deferred.promise; }, retryReason: function () { return service.hasMore() && retryQueue[0].reason; }, cancelAll: function () { while (service.hasMore()) { retryQueue.shift().cancel(); } retryUser = null; }, retryAll: function (userName) { if (retryUser == null) { return; } if (retryUser !== userName) { service.cancelAll(); return; } while (service.hasMore()) { retryQueue.shift().retry(); } } }; return service; } ]); angular.module('umbraco.security.interceptor') // This http interceptor listens for authentication successes and failures .factory('securityInterceptor', [ '$injector', 'securityRetryQueue', 'notificationsService', 'eventsService', 'requestInterceptorFilter', function ($injector, queue, notifications, eventsService, requestInterceptorFilter) { return function (promise) { return promise.then(function (originalResponse) { // Intercept successful requests //Here we'll check if our custom header is in the response which indicates how many seconds the user's session has before it //expires. Then we'll update the user in the user service accordingly. var headers = originalResponse.headers(); if (headers['x-umb-user-seconds']) { // We must use $injector to get the $http service to prevent circular dependency var userService = $injector.get('userService'); userService.setUserTimeout(headers['x-umb-user-seconds']); } //this checks if the user's values have changed, in which case we need to update the user details throughout //the back office similar to how we do when a user logs in if (headers['x-umb-user-modified']) { eventsService.emit('app.userRefresh'); } return promise; }, function (originalResponse) { // Intercept failed requests // Make sure we have the configuration of the request (don't we always?) var config = originalResponse.config ? originalResponse.config : {}; // Make sure we have an object for the headers of the request var headers = config.headers ? config.headers : {}; //Here we'll check if we should ignore the error (either based on the original header set or the request configuration) if (headers['x-umb-ignore-error'] === 'ignore' || config.umbIgnoreErrors === true || angular.isArray(config.umbIgnoreStatus) && config.umbIgnoreStatus.indexOf(originalResponse.status) !== -1) { //exit/ignore return promise; } var filtered = _.find(requestInterceptorFilter(), function (val) { return config.url.indexOf(val) > 0; }); if (filtered) { return promise; } if (originalResponse.status === 401) { //A 401 means that the user is not logged in //avoid an infinite loop var umbRequestHelper = $injector.get('umbRequestHelper'); var getCurrentUserPath = umbRequestHelper.getApiUrl('authenticationApiBaseUrl', 'GetCurrentUser'); if (!originalResponse.config.url.endsWith(getCurrentUserPath)) { var userService = $injector.get('userService'); // see above //Associate the user name with the retry to ensure we retry for the right user promise = userService.getCurrentUser().then(function (user) { var userName = user ? user.name : null; //The request bounced because it was not authorized - add a new request to the retry queue return queue.pushRetryFn('unauthorized-server', userName, function retryRequest() { // We must use $injector to get the $http service to prevent circular dependency return $injector.get('$http')(originalResponse.config); }); }); } } else if (originalResponse.status === 404) { //a 404 indicates that the request was not found - this could be due to a non existing url, or it could //be due to accessing a url with a parameter that doesn't exist, either way we should notifiy the user about it var errMsg = 'The URL returned a 404 (not found):
' + originalResponse.config.url.split('?')[0] + ''; if (originalResponse.data && originalResponse.data.ExceptionMessage) { errMsg += '
with error:
' + originalResponse.data.ExceptionMessage + ''; } if (originalResponse.config.data) { errMsg += '
with data:
' + angular.toJson(originalResponse.config.data) + '
Contact your administrator for information.'; } notifications.error('Request error', errMsg); } else if (originalResponse.status === 403) { //if the status was a 403 it means the user didn't have permission to do what the request was trying to do. //How do we deal with this now, need to tell the user somehow that they don't have permission to do the thing that was //requested. We can either deal with this globally here, or we can deal with it globally for individual requests on the umbRequestHelper, // or completely custom for services calling resources. //http://issues.umbraco.org/issue/U4-2749 //It was decided to just put these messages into the normal status messages. var msg = 'Unauthorized access to URL:
' + originalResponse.config.url.split('?')[0] + ''; if (originalResponse.config.data) { msg += '
with data:
' + angular.toJson(originalResponse.config.data) + '
Contact your administrator for information.'; } notifications.error('Authorization error', msg); } return promise; }); }; } ]) //used to set headers on all requests where necessary .factory('umbracoRequestInterceptor', function ($q, queryStrings) { return { //dealing with requests: 'request': function (config) { if (queryStrings.getParams().umbDebug === 'true' || queryStrings.getParams().umbdebug === 'true') { config.headers['X-UMB-DEBUG'] = 'true'; } return config; } }; }).value('requestInterceptorFilter', function () { return ['www.gravatar.com']; }) // We have to add the interceptor to the queue as a string because the interceptor depends upon service instances that are not available in the config block. .config([ '$httpProvider', function ($httpProvider) { $httpProvider.defaults.xsrfHeaderName = 'X-UMB-XSRF-TOKEN'; $httpProvider.defaults.xsrfCookieName = 'UMB-XSRF-TOKEN'; $httpProvider.responseInterceptors.push('securityInterceptor'); $httpProvider.interceptors.push('umbracoRequestInterceptor'); } ]); }());