fix($location): fix URL interception in hash-bang mode

Closes #1051
This commit is contained in:
Misko Hevery 2012-06-13 15:37:52 -07:00 committed by Igor Minar
parent 0f44964e5e
commit 6593a3e082
3 changed files with 125 additions and 33 deletions

View file

@ -23,6 +23,10 @@ function encodePath(path) {
return segments.join('/'); return segments.join('/');
} }
function stripHash(url) {
return url.split('#')[0];
}
function matchUrl(url, obj) { function matchUrl(url, obj) {
var match = URL_MATCH.exec(url); var match = URL_MATCH.exec(url);
@ -102,19 +106,19 @@ function convertToHashbangUrl(url, basePath, hashPrefix) {
* @param {string} url HTML5 url * @param {string} url HTML5 url
* @param {string} pathPrefix * @param {string} pathPrefix
*/ */
function LocationUrl(url, pathPrefix) { function LocationUrl(url, pathPrefix, appBaseUrl) {
pathPrefix = pathPrefix || ''; pathPrefix = pathPrefix || '';
/** /**
* Parse given html5 (regular) url string into properties * Parse given html5 (regular) url string into properties
* @param {string} url HTML5 url * @param {string} newAbsoluteUrl HTML5 url
* @private * @private
*/ */
this.$$parse = function(url) { this.$$parse = function(newAbsoluteUrl) {
var match = matchUrl(url, this); var match = matchUrl(newAbsoluteUrl, this);
if (match.path.indexOf(pathPrefix) !== 0) { 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)); this.$$path = decodeURIComponent(match.path.substr(pathPrefix.length));
@ -137,6 +141,14 @@ function LocationUrl(url, pathPrefix) {
pathPrefix + this.$$url; pathPrefix + this.$$url;
}; };
this.$$rewriteAppUrl = function(absoluteLinkUrl) {
if(absoluteLinkUrl.indexOf(appBaseUrl) == 0) {
return absoluteLinkUrl;
}
}
this.$$parse(url); this.$$parse(url);
} }
@ -149,7 +161,7 @@ function LocationUrl(url, pathPrefix) {
* @param {string} url Legacy url * @param {string} url Legacy url
* @param {string} hashPrefix Prefix for hash part (containing path and search) * @param {string} hashPrefix Prefix for hash part (containing path and search)
*/ */
function LocationHashbangUrl(url, hashPrefix) { function LocationHashbangUrl(url, hashPrefix, appBaseUrl) {
var basePath; var basePath;
/** /**
@ -192,6 +204,13 @@ function LocationHashbangUrl(url, hashPrefix) {
basePath + (this.$$url ? '#' + hashPrefix + this.$$url : ''); basePath + (this.$$url ? '#' + hashPrefix + this.$$url : '');
}; };
this.$$rewriteAppUrl = function(absoluteLinkUrl) {
if(absoluteLinkUrl.indexOf(appBaseUrl) == 0) {
return absoluteLinkUrl;
}
}
this.$$parse(url); this.$$parse(url);
} }
@ -380,6 +399,19 @@ LocationUrl.prototype = {
LocationHashbangUrl.prototype = inherit(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) { function locationGetter(property) {
return function() { return function() {
return this[property]; return this[property];
@ -479,26 +511,33 @@ function $LocationProvider(){
basePath, basePath,
pathPrefix, pathPrefix,
initUrl = $browser.url(), initUrl = $browser.url(),
absUrlPrefix; initUrlParts = matchUrl(initUrl),
appBaseUrl;
if (html5Mode) { if (html5Mode) {
basePath = $browser.baseHref() || '/'; basePath = $browser.baseHref() || '/';
pathPrefix = pathPrefixFromBase(basePath); pathPrefix = pathPrefixFromBase(basePath);
appBaseUrl =
composeProtocolHostPort(initUrlParts.protocol, initUrlParts.host, initUrlParts.port) +
pathPrefix + '/';
if ($sniffer.history) { if ($sniffer.history) {
$location = new LocationUrl( $location = new LocationUrl(
convertToHtml5Url(initUrl, basePath, hashPrefix), convertToHtml5Url(initUrl, basePath, hashPrefix),
pathPrefix); pathPrefix, appBaseUrl);
} else { } else {
$location = new LocationHashbangUrl( $location = new LocationHashbangInHtml5Url(
convertToHashbangUrl(initUrl, basePath, hashPrefix), convertToHashbangUrl(initUrl, basePath, hashPrefix),
hashPrefix); hashPrefix, appBaseUrl, basePath.substr(pathPrefix.length + 1));
} }
// link rewriting
absUrlPrefix = composeProtocolHostPort(
$location.protocol(), $location.host(), $location.port()) + pathPrefix;
} else { } else {
$location = new LocationHashbangUrl(initUrl, hashPrefix); appBaseUrl =
absUrlPrefix = $location.absUrl().split('#')[0]; 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) { $rootElement.bind('click', function(event) {
@ -510,27 +549,22 @@ function $LocationProvider(){
var elm = jqLite(event.target); var elm = jqLite(event.target);
// traverse the DOM up to find first A tag // 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(); elm = elm.parent();
} }
var absHref = elm.prop('href'), var absHref = elm.prop('href'),
href; rewrittenUrl = $location.$$rewriteAppUrl(absHref);
if (!absHref || if (absHref && !elm.attr('target') && rewrittenUrl) {
elm.attr('target') || // update location manually
absHref.indexOf(absUrlPrefix) !== 0) { // link to different domain or base path $location.$$parse(rewrittenUrl);
return; $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;
}); });

View file

@ -39,7 +39,7 @@ angular.mock.$Browser = function() {
var self = this; var self = this;
this.isMock = true; this.isMock = true;
self.$$url = "http://server"; self.$$url = "http://server/";
self.$$lastUrl = self.$$url; // used by url polling fn self.$$lastUrl = self.$$url; // used by url polling fn
self.pollFns = []; self.pollFns = [];

View file

@ -1029,6 +1029,64 @@ describe('$location', function() {
expect($browser.url()).toEqual(base + '#!/view2'); 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('<a href="http://server/test.html">link</a>');
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('<a href="http://server/index.html#test">link</a>');
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() { module(function() {
return function($rootElement, $compile, $rootScope) { return function($rootElement, $compile, $rootScope) {
$rootElement.html('<a href="http://server/#/somePath">link</a>'); $rootElement.html('<a href="http://server/#/somePath">link</a>');
@ -1162,7 +1220,7 @@ describe('$location', function() {
log += '$locationChangeStart'; log += '$locationChangeStart';
}); });
$rootScope.$on('$locationChangeSuccess', function() { $rootScope.$on('$locationChangeSuccess', function() {
throw new Error('after cancalation in html5 mode'); throw new Error('after cancelation in html5 mode');
}); });
browserTrigger(link, 'click'); browserTrigger(link, 'click');