diff --git a/src/ng/location.js b/src/ng/location.js index b5f102930365..727a0c2beddd 100644 --- a/src/ng/location.js +++ b/src/ng/location.js @@ -23,6 +23,10 @@ function encodePath(path) { return segments.join('/'); } +function stripHash(url) { + return url.split('#')[0]; +} + function matchUrl(url, obj) { var match = URL_MATCH.exec(url); @@ -102,19 +106,19 @@ function convertToHashbangUrl(url, basePath, hashPrefix) { * @param {string} url HTML5 url * @param {string} pathPrefix */ -function LocationUrl(url, pathPrefix) { +function LocationUrl(url, pathPrefix, appBaseUrl) { pathPrefix = pathPrefix || ''; /** * Parse given html5 (regular) url string into properties - * @param {string} url HTML5 url + * @param {string} newAbsoluteUrl HTML5 url * @private */ - this.$$parse = function(url) { - var match = matchUrl(url, this); + this.$$parse = function(newAbsoluteUrl) { + var match = matchUrl(newAbsoluteUrl, this); if (match.path.indexOf(pathPrefix) !== 0) { - throw Error('Invalid url "' + url + '", missing path prefix "' + pathPrefix + '" !'); + throw Error('Invalid url "' + newAbsoluteUrl + '", missing path prefix "' + pathPrefix + '" !'); } this.$$path = decodeURIComponent(match.path.substr(pathPrefix.length)); @@ -137,6 +141,14 @@ function LocationUrl(url, pathPrefix) { pathPrefix + this.$$url; }; + + this.$$rewriteAppUrl = function(absoluteLinkUrl) { + if(absoluteLinkUrl.indexOf(appBaseUrl) == 0) { + return absoluteLinkUrl; + } + } + + this.$$parse(url); } @@ -149,7 +161,7 @@ function LocationUrl(url, pathPrefix) { * @param {string} url Legacy url * @param {string} hashPrefix Prefix for hash part (containing path and search) */ -function LocationHashbangUrl(url, hashPrefix) { +function LocationHashbangUrl(url, hashPrefix, appBaseUrl) { var basePath; /** @@ -192,6 +204,13 @@ function LocationHashbangUrl(url, hashPrefix) { basePath + (this.$$url ? '#' + hashPrefix + this.$$url : ''); }; + this.$$rewriteAppUrl = function(absoluteLinkUrl) { + if(absoluteLinkUrl.indexOf(appBaseUrl) == 0) { + return absoluteLinkUrl; + } + } + + this.$$parse(url); } @@ -380,6 +399,19 @@ LocationUrl.prototype = { LocationHashbangUrl.prototype = inherit(LocationUrl.prototype); +function LocationHashbangInHtml5Url(url, hashPrefix, appBaseUrl, baseExtra) { + LocationHashbangUrl.apply(this, arguments); + + + this.$$rewriteAppUrl = function(absoluteLinkUrl) { + if (absoluteLinkUrl.indexOf(appBaseUrl) == 0) { + return appBaseUrl + baseExtra + '#' + hashPrefix + absoluteLinkUrl.substr(appBaseUrl.length); + } + } +} + +LocationHashbangInHtml5Url.prototype = inherit(LocationHashbangUrl.prototype); + function locationGetter(property) { return function() { return this[property]; @@ -479,26 +511,33 @@ function $LocationProvider(){ basePath, pathPrefix, initUrl = $browser.url(), - absUrlPrefix; + initUrlParts = matchUrl(initUrl), + appBaseUrl; if (html5Mode) { basePath = $browser.baseHref() || '/'; pathPrefix = pathPrefixFromBase(basePath); + appBaseUrl = + composeProtocolHostPort(initUrlParts.protocol, initUrlParts.host, initUrlParts.port) + + pathPrefix + '/'; + if ($sniffer.history) { $location = new LocationUrl( convertToHtml5Url(initUrl, basePath, hashPrefix), - pathPrefix); + pathPrefix, appBaseUrl); } else { - $location = new LocationHashbangUrl( + $location = new LocationHashbangInHtml5Url( convertToHashbangUrl(initUrl, basePath, hashPrefix), - hashPrefix); + hashPrefix, appBaseUrl, basePath.substr(pathPrefix.length + 1)); } - // link rewriting - absUrlPrefix = composeProtocolHostPort( - $location.protocol(), $location.host(), $location.port()) + pathPrefix; } else { - $location = new LocationHashbangUrl(initUrl, hashPrefix); - absUrlPrefix = $location.absUrl().split('#')[0]; + appBaseUrl = + composeProtocolHostPort(initUrlParts.protocol, initUrlParts.host, initUrlParts.port) + + (initUrlParts.path || '') + + (initUrlParts.search ? ('?' + initUrlParts.search) : '') + + '#' + hashPrefix + '/'; + + $location = new LocationHashbangUrl(initUrl, hashPrefix, appBaseUrl); } $rootElement.bind('click', function(event) { @@ -510,27 +549,22 @@ function $LocationProvider(){ var elm = jqLite(event.target); // traverse the DOM up to find first A tag - while (elm.length && lowercase(elm[0].nodeName) !== 'a') { + while (lowercase(elm[0].nodeName) !== 'a') { + if (elm[0] === $rootElement[0]) return; elm = elm.parent(); } var absHref = elm.prop('href'), - href; - - if (!absHref || - elm.attr('target') || - absHref.indexOf(absUrlPrefix) !== 0) { // link to different domain or base path - return; + rewrittenUrl = $location.$$rewriteAppUrl(absHref); + + if (absHref && !elm.attr('target') && rewrittenUrl) { + // update location manually + $location.$$parse(rewrittenUrl); + $rootScope.$apply(); + event.preventDefault(); + // hack to work around FF6 bug 684208 when scenario runner clicks on links + window.angular['ff-684208-preventDefault'] = true; } - - // update location with href without the prefix - href = absHref.substr(absUrlPrefix.length); - if (href.indexOf('#' + hashPrefix) == 0) href = href.substr(hashPrefix.length + 1); - $location.url(href); - $rootScope.$apply(); - event.preventDefault(); - // hack to work around FF6 bug 684208 when scenario runner clicks on links - window.angular['ff-684208-preventDefault'] = true; }); diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js index 1e229214ecbd..9fec9710de28 100644 --- a/src/ngMock/angular-mocks.js +++ b/src/ngMock/angular-mocks.js @@ -39,7 +39,7 @@ angular.mock.$Browser = function() { var self = this; this.isMock = true; - self.$$url = "http://server"; + self.$$url = "http://server/"; self.$$lastUrl = self.$$url; // used by url polling fn self.pollFns = []; diff --git a/test/ng/locationSpec.js b/test/ng/locationSpec.js index 91f3688c57fc..88747dff1cb1 100644 --- a/test/ng/locationSpec.js +++ b/test/ng/locationSpec.js @@ -1029,6 +1029,64 @@ describe('$location', function() { expect($browser.url()).toEqual(base + '#!/view2'); }); }); + + + it('should not intercept link clicks outside the app base url space', function() { + var base, clickHandler; + module(function($provide) { + $provide.value('$rootElement', { + bind: function(event, handler) { + expect(event).toEqual('click'); + clickHandler = handler; + } + }); + return function($browser) { + $browser.url(base = 'http://server/'); + } + }); + inject(function($rootScope, $compile, $browser, $rootElement, $document, $location) { + // make IE happy + jqLite(window.document.body).html('link'); + + var event = { + target: jqLite(window.document.body).find('a')[0], + preventDefault: jasmine.createSpy('preventDefault') + }; + + + clickHandler(event); + expect(event.preventDefault).not.toHaveBeenCalled(); + }); + }); + + + it('should not intercept hash link clicks outside the app base url space', function() { + var base, clickHandler; + module(function($provide) { + $provide.value('$rootElement', { + bind: function(event, handler) { + expect(event).toEqual('click'); + clickHandler = handler; + } + }); + return function($browser) { + $browser.url(base = 'http://server/'); + } + }); + inject(function($rootScope, $compile, $browser, $rootElement, $document, $location) { + // make IE happy + jqLite(window.document.body).html('link'); + + var event = { + target: jqLite(window.document.body).find('a')[0], + preventDefault: jasmine.createSpy('preventDefault') + }; + + + clickHandler(event); + expect(event.preventDefault).not.toHaveBeenCalled(); + }); + }); }); @@ -1111,7 +1169,7 @@ describe('$location', function() { ); - it('should listen on click events on href and prevent browser default in hasbang mode', function() { + it('should listen on click events on href and prevent browser default in hashbang mode', function() { module(function() { return function($rootElement, $compile, $rootScope) { $rootElement.html('link'); @@ -1162,7 +1220,7 @@ describe('$location', function() { log += '$locationChangeStart'; }); $rootScope.$on('$locationChangeSuccess', function() { - throw new Error('after cancalation in html5 mode'); + throw new Error('after cancelation in html5 mode'); }); browserTrigger(link, 'click');