diff --git a/src/state.js b/src/state.js index 5bac8bfb8..8c88cb0d6 100644 --- a/src/state.js +++ b/src/state.js @@ -22,7 +22,7 @@ $StateProvider.$inject = ['$urlRouterProvider', '$urlMatcherFactoryProvider']; function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { - var root, states = {}, $state, queue = {}, abstractKey = 'abstract'; + var root, states = {}, $state, queue = {}, abstractKey = 'abstract', maximumRedirects = 10; // Builds state properties from definition passed to registerState() var stateBuilder = { @@ -509,6 +509,40 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { return this; } + /** + * @ngdoc function + * @name ui.router.state.$stateProvider#maxRedirects + * @methodOf ui.router.state.$stateProvider + * + * @description + * A method that sets the maximum number of nested Transition Superceded (application redirects) before UI-Router + * throws an Error. + * + * @example + * Set max redirects: + *
+ * angular.module('app', ['ui.router']); + * app.config(function ($stateProvider) { + * var newMax = $stateProvider.maxRedirects(30); + * }); + *+ * + * Get max redirects + *
+ * angular.module('app', ['ui.router']); + * app.config(function ($stateProvider) { + * var max = $stateProvider.maxRedirects(); + * }); + *+ */ + this.maxRedirects = maxRedirects; + function maxRedirects(newMaximum) { + if (isDefined(newMaximum)) { + maximumRedirects = newMaximum; + } + return maximumRedirects; + } + /** * @ngdoc object * @name ui.router.state.$state @@ -613,7 +647,8 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { params: {}, current: root.self, $current: root, - transition: null + transition: null, + redirects: [] }; /** @@ -784,6 +819,11 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { throw new Error("Could not resolve '" + to + "' from state '" + options.relative + "'"); } } + + $state.redirects.push(toState.name); + if ($state.redirects.length >= maximumRedirects) + throw new Error("Redirect count exceeded " + maximumRedirects + ". [ " + $state.redirects.join(" -> ") + " ]"); + if (toState[abstractKey]) throw new Error("Cannot transition to abstract state '" + to + "'"); if (options.inherit) toParams = inheritParams($stateParams, toParams || {}, $state.$current, toState); to = toState; @@ -955,6 +995,16 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { $urlRouter.update(); } + return $q.reject(error); + }) + .then(function(result) { + $state.redirects.pop(); + return result; + }, function(error) { + if (error === TransitionSuperseded) { + $state.redirects.push($state.current.name); + return error; + } return $q.reject(error); }); diff --git a/test/stateSpec.js b/test/stateSpec.js index 3d504f538..3660b010c 100644 --- a/test/stateSpec.js +++ b/test/stateSpec.js @@ -75,6 +75,10 @@ describe('state', function () { $state.transitionTo("about"); } }) + .state('home.redirectloop', { + onEnter: function($state) { $state.transitionTo("home.redirectloop.infinite"); } + }) + .state('home.redirectloop.infinite', { }) .state('resolveFail', { url: "/resolve-fail", resolve: { @@ -721,6 +725,8 @@ describe('state', function () { 'home', 'home.item', 'home.redirect', + 'home.redirectloop', + 'home.redirectloop.infinite', 'resolveFail', 'resolveTimeout', 'root', @@ -1015,6 +1021,16 @@ describe('state', function () { })); }); + + describe('infinite loop detection', function () { + it('should detect looping onEnter TransitionSuperceded', inject(function ($q, $state) { + $state.transitionTo('home'); + $q.flush(); + $state.transitionTo('home.redirectloop'); + expect(function() { $q.flush() }).toThrow(new Error("Redirect count exceeded " + stateProvider.maxRedirects() + ". [ home.redirectloop -> home.redirectloop.infinite -> home.redirectloop.infinite -> home.redirectloop.infinite -> home.redirectloop.infinite -> home.redirectloop.infinite -> home.redirectloop.infinite -> home.redirectloop.infinite -> home.redirectloop.infinite -> home.redirectloop.infinite ]") ); + expect($state.current.name).toBe('home'); + })); + }); }); describe('state queue', function(){