From 49fa2b1d2910cd06fe09624ae8459fe4b24cb2ed Mon Sep 17 00:00:00 2001 From: hamfastgamgee Date: Tue, 14 Apr 2015 14:06:22 -0500 Subject: [PATCH] fix($browser): prevent infinite $digest from no trailing slash in IE9 fix($browser): prevent infinite $digest from no trailing slash in IE9 This fix prevents IE9 from throwing an infinite $digest error when the user accesses the base URL of the site without a trailing slash. Suppose you owned http://www.mysite.com/app and had an Angular app hosted in a subdirectory "app". If an IE9 user accessed http://www.mysite.com/app infinite $digest errors would be thrown on the console, but the app itself would eventually resolve properly and work fine. Now the infinite $digest errors will not be thrown. Closes #11439 --- src/ng/browser.js | 20 ++ src/ng/location.js | 11 +- test/ngRoute/routeSpec.js | 377 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 407 insertions(+), 1 deletion(-) diff --git a/src/ng/browser.js b/src/ng/browser.js index 8bd6b424bff8..fbebe01b3b8b 100644 --- a/src/ng/browser.js +++ b/src/ng/browser.js @@ -135,6 +135,26 @@ function Browser(window, document, $log, $sniffer) { cacheState(); lastHistoryState = cachedState; + /** + * @name $browser#forceReloadLocationUpdate + * + * @description + * This method is a setter. + * + * If the reloadLocation variable is already set, it will be reset to + * the passed-in URL. + * + * NOTE: this api is intended for use only by the $location service in the + * $locationWatch function. + * + * @param {string} url New url + */ + self.forceReloadLocationUpdate = function(url) { + if (reloadLocation) { + reloadLocation = url; + } + }; + /** * @name $browser#url * diff --git a/src/ng/location.js b/src/ng/location.js index ffee4cd4820f..77edb6c2a018 100644 --- a/src/ng/location.js +++ b/src/ng/location.js @@ -883,7 +883,7 @@ function $LocationProvider() { $browser.url($location.absUrl(), true); } - var initializing = true; + var initializing = true, previousOldUrl = null, previousNewUrl = null; // update $location when $browser url changes $browser.onUrlChange(function(newUrl, newState) { @@ -918,6 +918,15 @@ function $LocationProvider() { $rootScope.$watch(function $locationWatch() { var oldUrl = trimEmptyHash($browser.url()); var newUrl = trimEmptyHash($location.absUrl()); + if ($location.$$html5 && !$sniffer.history) { + if (previousOldUrl === oldUrl && previousNewUrl === newUrl) { + // break out of infinite $digest loops caused by default routes in hashbang mode + $browser.forceReloadLocationUpdate(newUrl); + previousOldUrl = previousNewUrl = null; + return; + } + previousOldUrl = oldUrl, previousNewUrl = newUrl; + } var oldState = $browser.state(); var currentReplace = $location.$$replace; var urlOrStateChanged = oldUrl !== newUrl || diff --git a/test/ngRoute/routeSpec.js b/test/ngRoute/routeSpec.js index 368dadf3c9ae..d4f502689bc3 100644 --- a/test/ngRoute/routeSpec.js +++ b/test/ngRoute/routeSpec.js @@ -1037,6 +1037,383 @@ describe('$route', function() { }); }); + function initBrowserForInfiniteDigestTests() { + return function($browser) { + $browser.url = function(url, replace, state) { + if (angular.isUndefined(state)) { + state = null; + } + if (url) { + if (this.$$lastUrl === url) { + return this; + } + + var index = this.$$lastUrl.indexOf('#'); + var lastUrlStripped = (index === -1 ? this.$$lastUrl : this.$$lastUrl.substr(0, index)); + index = url.indexOf('#'); + var urlStripped = (index === -1 ? url : url.substr(0, index)); + + var sameBase = this.$$lastUrl && lastUrlStripped === urlStripped; + if (!sameBase) { + this.$$reloadLocation = url; + } + this.$$url = url; + this.$$lastUrl = url; + // Native pushState serializes & copies the object; simulate it. + this.$$state = angular.copy(state); + return this; + } + + return this.$$reloadLocation || this.$$url; + }; + $browser.$$lastUrl = 'http://server/'; + $browser.$$baseHref = '/app/'; + $browser.forceReloadLocationUpdate = function(url) { + if (this.$$reloadLocation) { + this.$$reloadLocation = url; + } + }; + }; + } + + function initLocationAndRouteServices(options) { + return module(function($provide, $locationProvider, $routeProvider) { + $locationProvider.html5Mode(options.html5Mode); + $provide.value('$sniffer', {history: options.history}); + + $routeProvider. + when(options.matchingRoute, { + templateUrl: 'foo.html', + }). + otherwise({ + redirectTo: options.otherwiseRoute + }); + }); + } + + describe('location watch for hashbang browsers with routing taken into account', function() { + beforeEach(initLocationAndRouteServices({html5Mode: true, history: false, matchingRoute: '/Home', otherwiseRoute: '/Home'})); + beforeEach(inject(initBrowserForInfiniteDigestTests())); + + it('should not infinite $digest when going to base URL with home route without trailing slash in non-history browser', function() { + inject(function($rootScope, $injector, $browser) { + var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough(); + var $browserForceReloadLocationUpdate = spyOn($browser, 'forceReloadLocationUpdate').andCallThrough(); + + $browser.url('http://server/app'); + + var $location = $injector.get('$location'); + var $route = $injector.get('$route'); + + $browser.poll(); + + $rootScope.$digest(); + + expect($route.current.loadedTemplateUrl).toEqual('foo.html'); + expect($browser.url()).toEqual('http://server/app/#/Home'); + expect($location.path()).toEqual('/Home'); + expect($browserUrl.calls.length).toEqual(3); + expect($browserForceReloadLocationUpdate).toHaveBeenCalledOnce(); + }); + }); + }); + + describe('location watch for hashbang browsers with routing taken into account', function() { + beforeEach(initLocationAndRouteServices({html5Mode: true, history: false, matchingRoute: '/', otherwiseRoute: '/'})); + beforeEach(inject(initBrowserForInfiniteDigestTests())); + + it('should not infinite $digest when going to base URL without home route without trailing slash in non-history browser', function() { + inject(function($rootScope, $injector, $browser) { + var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough(); + var $browserForceReloadLocationUpdate = spyOn($browser, 'forceReloadLocationUpdate').andCallThrough(); + + $browser.url('http://server/app'); + + var $location = $injector.get('$location'); + var $route = $injector.get('$route'); + + $browser.poll(); + + $rootScope.$digest(); + + expect($route.current.loadedTemplateUrl).toEqual('foo.html'); + expect($browser.url()).toEqual('http://server/app/#/'); + expect($location.path()).toEqual('/'); + expect($browserUrl.calls.length).toEqual(3); + expect($browserForceReloadLocationUpdate).toHaveBeenCalledOnce(); + }); + }); + }); + + describe('location watch for hashbang browsers with routing taken into account', function() { + beforeEach(initLocationAndRouteServices({html5Mode: true, history: false, matchingRoute: '/', otherwiseRoute: '/Home'})); + beforeEach(inject(initBrowserForInfiniteDigestTests())); + + it('should not infinite $digest when going to base URL without trailing slash when otherwise route shouldn\'t be called in non-history browser', function() { + inject(function($rootScope, $injector, $browser) { + var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough(); + var $browserForceReloadLocationUpdate = spyOn($browser, 'forceReloadLocationUpdate').andCallThrough(); + + $browser.url('http://server/app'); + + var $location = $injector.get('$location'); + var $route = $injector.get('$route'); + + $browser.poll(); + + $rootScope.$digest(); + + expect($route.current.loadedTemplateUrl).toEqual('foo.html'); + expect($browser.url()).toEqual('http://server/app/#/'); + expect($location.path()).toEqual('/'); + expect($browserUrl.calls.length).toEqual(3); + expect($browserForceReloadLocationUpdate).toHaveBeenCalledOnce(); + }); + }); + }); + + describe('location watch for hashbang browsers with routing taken into account', function() { + beforeEach(initLocationAndRouteServices({html5Mode: true, history: false, matchingRoute: '/Home', otherwiseRoute: '/Home'})); + beforeEach(inject(initBrowserForInfiniteDigestTests())); + + it('should not infinite $digest when going to base URL with home route with trailing slash in non-history browser', function() { + inject(function($rootScope, $injector, $browser) { + var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough(); + var $browserForceReloadLocationUpdate = spyOn($browser, 'forceReloadLocationUpdate').andCallThrough(); + + $browser.url('http://server/app/'); + + var $location = $injector.get('$location'); + var $route = $injector.get('$route'); + + $browser.poll(); + + $rootScope.$digest(); + + expect($route.current.loadedTemplateUrl).toEqual('foo.html'); + expect($browser.url()).toEqual('http://server/app/#/Home'); + expect($location.path()).toEqual('/Home'); + expect($browserUrl.calls.length).toEqual(2); + expect($browserForceReloadLocationUpdate).toHaveBeenCalledOnce(); + }); + }); + }); + + describe('location watch for hashbang browsers with routing taken into account', function() { + beforeEach(initLocationAndRouteServices({html5Mode: true, history: false, matchingRoute: '/', otherwiseRoute: '/'})); + beforeEach(inject(initBrowserForInfiniteDigestTests())); + + it('should not infinite $digest when going to base URL without home route with trailing slash in non-history browser', function() { + inject(function($rootScope, $injector, $browser) { + var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough(); + var $browserForceReloadLocationUpdate = spyOn($browser, 'forceReloadLocationUpdate').andCallThrough(); + + $browser.url('http://server/app/'); + + var $location = $injector.get('$location'); + var $route = $injector.get('$route'); + + $browser.poll(); + + $rootScope.$digest(); + + expect($route.current.loadedTemplateUrl).toEqual('foo.html'); + expect($browser.url()).toEqual('http://server/app/#/'); + expect($location.path()).toEqual('/'); + expect($browserUrl.calls.length).toEqual(2); + expect($browserForceReloadLocationUpdate).toHaveBeenCalledOnce(); + }); + }); + }); + + describe('location watch for hashbang browsers with routing taken into account', function() { + beforeEach(initLocationAndRouteServices({html5Mode: true, history: false, matchingRoute: '/', otherwiseRoute: '/Home'})); + beforeEach(inject(initBrowserForInfiniteDigestTests())); + + it('should not infinite $digest when going to base URL with trailing slash when otherwise route shouldn\'t be called in non-history browser', function() { + inject(function($rootScope, $injector, $browser) { + var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough(); + var $browserForceReloadLocationUpdate = spyOn($browser, 'forceReloadLocationUpdate').andCallThrough(); + + $browser.url('http://server/app/'); + + var $location = $injector.get('$location'); + var $route = $injector.get('$route'); + + $browser.poll(); + + $rootScope.$digest(); + + expect($route.current.loadedTemplateUrl).toEqual('foo.html'); + expect($browser.url()).toEqual('http://server/app/#/'); + expect($location.path()).toEqual('/'); + expect($browserUrl.calls.length).toEqual(2); + expect($browserForceReloadLocationUpdate).toHaveBeenCalledOnce(); + }); + }); + }); + + describe('location watch for HTML5 browsers with routing taken into account', function() { + beforeEach(initLocationAndRouteServices({html5Mode: true, history: true, matchingRoute: '/Home', otherwiseRoute: '/Home'})); + beforeEach(inject(initBrowserForInfiniteDigestTests())); + + it('should not infinite $digest when going to base URL with home route without trailing slash in history browser', function() { + inject(function($rootScope, $injector, $browser) { + var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough(); + var $browserForceReloadLocationUpdate = spyOn($browser, 'forceReloadLocationUpdate').andCallThrough(); + + $browser.url('http://server/app'); + + var $location = $injector.get('$location'); + var $route = $injector.get('$route'); + + $browser.poll(); + + $rootScope.$digest(); + + expect($route.current.loadedTemplateUrl).toEqual('foo.html'); + expect($browser.url()).toEqual('http://server/app/Home'); + expect($location.path()).toEqual('/Home'); + expect($browserUrl.calls.length).toEqual(3); + expect($browserForceReloadLocationUpdate).not.toHaveBeenCalled(); + }); + }); + }); + + describe('location watch for HTML5 browsers with routing taken into account', function() { + beforeEach(initLocationAndRouteServices({html5Mode: true, history: true, matchingRoute: '/', otherwiseRoute: '/'})); + beforeEach(inject(initBrowserForInfiniteDigestTests())); + + it('should not infinite $digest when going to base URL without home route without trailing slash in history browser', function() { + inject(function($rootScope, $injector, $browser) { + var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough(); + var $browserForceReloadLocationUpdate = spyOn($browser, 'forceReloadLocationUpdate').andCallThrough(); + + $browser.url('http://server/app'); + + var $location = $injector.get('$location'); + var $route = $injector.get('$route'); + + $browser.poll(); + + $rootScope.$digest(); + + expect($route.current.loadedTemplateUrl).toEqual('foo.html'); + expect($browser.url()).toEqual('http://server/app/'); + expect($location.path()).toEqual('/'); + expect($browserUrl.calls.length).toEqual(2); + expect($browserForceReloadLocationUpdate).not.toHaveBeenCalled(); + }); + }); + }); + + describe('location watch for HTML5 browsers with routing taken into account', function() { + beforeEach(initLocationAndRouteServices({html5Mode: true, history: true, matchingRoute: '/', otherwiseRoute: '/Home'})); + beforeEach(inject(initBrowserForInfiniteDigestTests())); + + it('should not infinite $digest when going to base URL without trailing slash when otherwise route shouldn\'t be called in history browser', function() { + inject(function($rootScope, $injector, $browser) { + var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough(); + var $browserForceReloadLocationUpdate = spyOn($browser, 'forceReloadLocationUpdate').andCallThrough(); + + $browser.url('http://server/app'); + + var $location = $injector.get('$location'); + var $route = $injector.get('$route'); + + $browser.poll(); + + $rootScope.$digest(); + + expect($route.current.loadedTemplateUrl).toEqual('foo.html'); + expect($browser.url()).toEqual('http://server/app/'); + expect($location.path()).toEqual('/'); + expect($browserUrl.calls.length).toEqual(2); + expect($browserForceReloadLocationUpdate).not.toHaveBeenCalled(); + }); + }); + }); + + describe('location watch for HTML5 browsers with routing taken into account', function() { + beforeEach(initLocationAndRouteServices({html5Mode: true, history: true, matchingRoute: '/Home', otherwiseRoute: '/Home'})); + beforeEach(inject(initBrowserForInfiniteDigestTests())); + + it('should not infinite $digest when going to base URL with home route with trailing slash in history browser', function() { + inject(function($rootScope, $injector, $browser) { + var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough(); + var $browserForceReloadLocationUpdate = spyOn($browser, 'forceReloadLocationUpdate').andCallThrough(); + + $browser.url('http://server/app/'); + + var $location = $injector.get('$location'); + var $route = $injector.get('$route'); + + $browser.poll(); + + $rootScope.$digest(); + + expect($route.current.loadedTemplateUrl).toEqual('foo.html'); + expect($browser.url()).toEqual('http://server/app/Home'); + expect($location.path()).toEqual('/Home'); + expect($browserUrl.calls.length).toEqual(2); + expect($browserForceReloadLocationUpdate).not.toHaveBeenCalled(); + }); + }); + }); + + describe('location watch for HTML5 browsers with routing taken into account', function() { + beforeEach(initLocationAndRouteServices({html5Mode: true, history: true, matchingRoute: '/', otherwiseRoute: '/'})); + beforeEach(inject(initBrowserForInfiniteDigestTests())); + + it('should not infinite $digest when going to base URL without home route with trailing slash in history browser', function() { + inject(function($rootScope, $injector, $browser) { + var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough(); + var $browserForceReloadLocationUpdate = spyOn($browser, 'forceReloadLocationUpdate').andCallThrough(); + + $browser.url('http://server/app/'); + + var $location = $injector.get('$location'); + var $route = $injector.get('$route'); + + $browser.poll(); + + $rootScope.$digest(); + + expect($route.current.loadedTemplateUrl).toEqual('foo.html'); + expect($browser.url()).toEqual('http://server/app/'); + expect($location.path()).toEqual('/'); + expect($browserUrl.calls.length).toEqual(1); + expect($browserForceReloadLocationUpdate).not.toHaveBeenCalled(); + }); + }); + }); + + describe('location watch for HTML5 browsers with routing taken into account', function() { + beforeEach(initLocationAndRouteServices({html5Mode: true, history: true, matchingRoute: '/', otherwiseRoute: '/Home'})); + beforeEach(inject(initBrowserForInfiniteDigestTests())); + + it('should not infinite $digest when going to base URL with trailing slash when otherwise route shouldn\'t be called in history browser', function() { + inject(function($rootScope, $injector, $browser) { + var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough(); + var $browserForceReloadLocationUpdate = spyOn($browser, 'forceReloadLocationUpdate').andCallThrough(); + + $browser.url('http://server/app/'); + + var $location = $injector.get('$location'); + var $route = $injector.get('$route'); + + $browser.poll(); + + $rootScope.$digest(); + + expect($route.current.loadedTemplateUrl).toEqual('foo.html'); + expect($browser.url()).toEqual('http://server/app/'); + expect($location.path()).toEqual('/'); + expect($browserUrl.calls.length).toEqual(1); + expect($browserForceReloadLocationUpdate).not.toHaveBeenCalled(); + }); + }); + }); describe('reloadOnSearch', function() { it('should reload a route when reloadOnSearch is enabled and .search() changes', function() {