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('/');
}
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;
rewrittenUrl = $location.$$rewriteAppUrl(absHref);
if (!absHref ||
elm.attr('target') ||
absHref.indexOf(absUrlPrefix) !== 0) { // link to different domain or base path
return;
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;
});

View file

@ -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 = [];

View file

@ -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('<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() {
return function($rootElement, $compile, $rootScope) {
$rootElement.html('<a href="http://server/#/somePath">link</a>');
@ -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');