Skip to content

Commit 8791d06

Browse files
committed
fix($location): fix URL interception in hash-bang mode
Closes angular#1051
1 parent b0322f2 commit 8791d06

File tree

4 files changed

+189
-198
lines changed

4 files changed

+189
-198
lines changed

src/ng/httpBackend.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ function createHttpBackend($browser, XHR, $browserDefer, callbacks, rawDocument,
8787

8888
function completeRequest(callback, status, response, headersString) {
8989
// URL_MATCH is defined in src/service/location.js
90-
var protocol = (url.match(URL_MATCH) || ['', locationProtocol])[1];
90+
var protocol = (url.match(SERVER_MATCH) || ['', locationProtocol])[1];
9191

9292
// fix status code for file protocol (it's always 0)
9393
status = (protocol == 'file') ? (response ? 200 : 404) : status;

src/ng/location.js

+118-136
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
'use strict';
22

3-
var URL_MATCH = /^([^:]+):\/\/(\w+:{0,1}\w*@)?([\w\.-]*)(:([0-9]+))?(\/[^\?#]*)?(\?([^#]*))?(#(.*))?$/,
4-
PATH_MATCH = /^([^\?#]*)?(\?([^#]*))?(#(.*))?$/,
5-
HASH_MATCH = PATH_MATCH,
3+
var SERVER_MATCH = /^([^:]+):\/\/(\w+:{0,1}\w*@)?([\w\.-]*)(:([0-9]+))?/,
4+
PATH_MATCH = /^([^\?#]*)(\?([^#]*))?(#(.*))?$/,
65
DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21};
76

87

@@ -23,104 +22,73 @@ function encodePath(path) {
2322
return segments.join('/');
2423
}
2524

26-
2725
function matchUrl(url, obj) {
28-
var match = URL_MATCH.exec(url);
29-
30-
match = {
31-
protocol: match[1],
32-
host: match[3],
33-
port: int(match[5]) || DEFAULT_PORTS[match[1]] || null,
34-
path: match[6] || '/',
35-
search: match[8],
36-
hash: match[10]
37-
};
38-
39-
if (obj) {
40-
obj.$$protocol = match.protocol;
41-
obj.$$host = match.host;
42-
obj.$$port = match.port;
43-
}
26+
var match = SERVER_MATCH.exec(url);
4427

45-
return match;
28+
obj.$$protocol = match[1];
29+
obj.$$host = match[3];
30+
obj.$$port = int(match[5]) || DEFAULT_PORTS[match[1]] || null;
4631
}
4732

33+
function matchAppUrl(url, obj) {
34+
var match = PATH_MATCH.exec(url);
4835

49-
function composeProtocolHostPort(protocol, host, port) {
50-
return protocol + '://' + host + (port == DEFAULT_PORTS[protocol] ? '' : ':' + port);
51-
}
52-
36+
obj.$$path = decodeURIComponent(match[1]);
37+
obj.$$search = parseKeyValue(match[3]);
38+
obj.$$hash = decodeURIComponent(match[5] || '');
5339

54-
function pathPrefixFromBase(basePath) {
55-
return basePath.substr(0, basePath.lastIndexOf('/'));
40+
// make sure path starts with '/';
41+
if (obj.$$path && obj.$$path.charAt(0) != '/') obj.$$path = '/' + obj.$$path;
5642
}
5743

5844

59-
function convertToHtml5Url(url, basePath, hashPrefix) {
60-
var match = matchUrl(url);
45+
function composeProtocolHostPort(protocol, host, port) {
46+
return protocol + '://' + host + (port == DEFAULT_PORTS[protocol] ? '' : ':' + port);
47+
}
6148

62-
// already html5 url
63-
if (decodeURIComponent(match.path) != basePath || isUndefined(match.hash) ||
64-
match.hash.indexOf(hashPrefix) !== 0) {
65-
return url;
66-
// convert hashbang url -> html5 url
67-
} else {
68-
return composeProtocolHostPort(match.protocol, match.host, match.port) +
69-
pathPrefixFromBase(basePath) + match.hash.substr(hashPrefix.length);
70-
}
49+
function beginsWith(begin, whole, otherwise) {
50+
return whole.indexOf(begin) == 0 ? whole.substr(begin.length) : otherwise;
7151
}
7252

7353

74-
function convertToHashbangUrl(url, basePath, hashPrefix) {
75-
var match = matchUrl(url);
54+
function stripHash(url) {
55+
var index = url.indexOf('#');
56+
return index == -1 ? url : url.substr(0, index);
57+
}
7658

77-
// already hashbang url
78-
if (decodeURIComponent(match.path) == basePath) {
79-
return url;
80-
// convert html5 url -> hashbang url
81-
} else {
82-
var search = match.search && '?' + match.search || '',
83-
hash = match.hash && '#' + match.hash || '',
84-
pathPrefix = pathPrefixFromBase(basePath),
85-
path = match.path.substr(pathPrefix.length);
8659

87-
if (match.path.indexOf(pathPrefix) !== 0) {
88-
throw Error('Invalid url "' + url + '", missing path prefix "' + pathPrefix + '" !');
89-
}
60+
function stripFile(url) {
61+
return url.substr(0, stripHash(url).lastIndexOf('/') + 1);
62+
}
9063

91-
return composeProtocolHostPort(match.protocol, match.host, match.port) + basePath +
92-
'#' + hashPrefix + path + search + hash;
93-
}
64+
/* return the server only */
65+
function serverBase(url) {
66+
return url.substring(0, url.indexOf('/', url.indexOf('//') + 2));
9467
}
9568

9669

9770
/**
98-
* LocationUrl represents an url
71+
* LocationHtml5Url represents an url
9972
* This object is exposed as $location service when HTML5 mode is enabled and supported
10073
*
10174
* @constructor
102-
* @param {string} url HTML5 url
103-
* @param {string} pathPrefix
75+
* @param {string} appBase application base URL
76+
* @param {string} hashPrefix hasbang prefix
10477
*/
105-
function LocationUrl(url, pathPrefix) {
106-
pathPrefix = pathPrefix || '';
107-
78+
function LocationHtml5Url(appBase, hashPrefix) {
79+
var appBaseNoFile = stripFile(appBase);
10880
/**
10981
* Parse given html5 (regular) url string into properties
11082
* @param {string} url HTML5 url
11183
* @private
11284
*/
11385
this.$$parse = function(url) {
114-
var match = matchUrl(url, this);
115-
116-
if (match.path.indexOf(pathPrefix) !== 0) {
117-
throw Error('Invalid url "' + url + '", missing path prefix "' + pathPrefix + '" !');
86+
matchUrl(url, this);
87+
matchAppUrl(url.substr(appBaseNoFile.length), this);
88+
if (!this.$$path) {
89+
this.$$path = '/';
11890
}
11991

120-
this.$$path = decodeURIComponent(match.path.substr(pathPrefix.length));
121-
this.$$search = parseKeyValue(match.search);
122-
this.$$hash = match.hash && decodeURIComponent(match.hash) || '';
123-
12492
this.$$compose();
12593
};
12694

@@ -133,11 +101,24 @@ function LocationUrl(url, pathPrefix) {
133101
hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : '';
134102

135103
this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash;
136-
this.$$absUrl = composeProtocolHostPort(this.$$protocol, this.$$host, this.$$port) +
137-
pathPrefix + this.$$url;
104+
this.$$absUrl = appBaseNoFile + this.$$url.substr(1); // first char is always '/'
138105
};
139106

140-
this.$$parse(url);
107+
this.$$rewrite = function(url) {
108+
var appUrl;
109+
110+
if ( (appUrl = beginsWith(appBase, url)) !== undefined ) {
111+
if ( (appUrl = beginsWith(hashPrefix, appUrl)) !== undefined ) {
112+
return appBaseNoFile + (beginsWith('/', appUrl) || appUrl);
113+
} else {
114+
return appBase;
115+
}
116+
} else if ( (appUrl = beginsWith(appBaseNoFile, url)) ) {
117+
return appBaseNoFile + appUrl;
118+
} else if (appBaseNoFile == url + '/') {
119+
return appBaseNoFile;
120+
}
121+
}
141122
}
142123

143124

@@ -146,36 +127,22 @@ function LocationUrl(url, pathPrefix) {
146127
* This object is exposed as $location service when html5 history api is disabled or not supported
147128
*
148129
* @constructor
149-
* @param {string} url Legacy url
150-
* @param {string} hashPrefix Prefix for hash part (containing path and search)
130+
* @param {string} appBase application base URL
131+
* @param {string} hashPrefix hasbang prefix
151132
*/
152-
function LocationHashbangUrl(url, hashPrefix) {
153-
var basePath;
133+
function LocationHashbangUrl(appBase, hashPrefix) {
134+
var appBaseNoFile = stripFile(appBase);
154135

155136
/**
156137
* Parse given hashbang url into properties
157138
* @param {string} url Hashbang url
158139
* @private
159140
*/
160141
this.$$parse = function(url) {
161-
var match = matchUrl(url, this);
162-
163-
164-
if (match.hash && match.hash.indexOf(hashPrefix) !== 0) {
165-
throw Error('Invalid url "' + url + '", missing hash prefix "' + hashPrefix + '" !');
166-
}
167-
168-
basePath = match.path + (match.search ? '?' + match.search : '');
169-
match = HASH_MATCH.exec((match.hash || '').substr(hashPrefix.length));
170-
if (match[1]) {
171-
this.$$path = (match[1].charAt(0) == '/' ? '' : '/') + decodeURIComponent(match[1]);
172-
} else {
173-
this.$$path = '';
174-
}
175-
176-
this.$$search = parseKeyValue(match[3]);
177-
this.$$hash = match[5] && decodeURIComponent(match[5]) || '';
178-
142+
matchUrl(url, this);
143+
url = beginsWith(appBase, url) || beginsWith(appBaseNoFile, url);
144+
url = beginsWith(hashPrefix, url, url);
145+
matchAppUrl(url, this);
179146
this.$$compose();
180147
};
181148

@@ -188,15 +155,48 @@ function LocationHashbangUrl(url, hashPrefix) {
188155
hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : '';
189156

190157
this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash;
191-
this.$$absUrl = composeProtocolHostPort(this.$$protocol, this.$$host, this.$$port) +
192-
basePath + (this.$$url ? '#' + hashPrefix + this.$$url : '');
158+
this.$$absUrl = appBase + (this.$$url ? hashPrefix + this.$$url : '');
193159
};
194160

195-
this.$$parse(url);
161+
this.$$rewrite = function(url) {
162+
if(stripHash(appBase) == stripHash(url)) {
163+
return url;
164+
}
165+
}
196166
}
197167

198168

199-
LocationUrl.prototype = {
169+
/**
170+
* LocationHashbangUrl represents url
171+
* This object is exposed as $location service when html5 history api is enabled but the browser
172+
* does not support it.
173+
*
174+
* @constructor
175+
* @param {string} appBase application base URL
176+
* @param {string} hashPrefix hasbang prefix
177+
*/
178+
function LocationHashbangInHtml5Url(appBase, hashPrefix) {
179+
LocationHashbangUrl.apply(this, arguments);
180+
181+
var appBaseNoFile = stripFile(appBase);
182+
183+
this.$$rewrite = function(url) {
184+
var appUrl;
185+
186+
if ( appBase == stripHash(url) ) {
187+
return url;
188+
} else if ( (appUrl = beginsWith(appBaseNoFile, url)) ) {
189+
return appBase + hashPrefix + appUrl;
190+
} else if ( appBaseNoFile === url + '/') {
191+
return appBaseNoFile;
192+
}
193+
}
194+
}
195+
196+
197+
LocationHashbangInHtml5Url.prototype =
198+
LocationHashbangUrl.prototype =
199+
LocationHtml5Url.prototype = {
200200

201201
/**
202202
* Has any change been replacing ?
@@ -378,8 +378,6 @@ LocationUrl.prototype = {
378378
}
379379
};
380380

381-
LocationHashbangUrl.prototype = inherit(LocationUrl.prototype);
382-
383381
function locationGetter(property) {
384382
return function() {
385383
return this[property];
@@ -476,30 +474,20 @@ function $LocationProvider(){
476474
this.$get = ['$rootScope', '$browser', '$sniffer', '$rootElement',
477475
function( $rootScope, $browser, $sniffer, $rootElement) {
478476
var $location,
479-
basePath,
480-
pathPrefix,
481-
initUrl = $browser.url(),
482-
absUrlPrefix;
477+
LocationMode,
478+
baseHref = $browser.baseHref(),
479+
initialUrl = $browser.url(),
480+
appBase;
483481

484482
if (html5Mode) {
485-
basePath = $browser.baseHref() || '/';
486-
pathPrefix = pathPrefixFromBase(basePath);
487-
if ($sniffer.history) {
488-
$location = new LocationUrl(
489-
convertToHtml5Url(initUrl, basePath, hashPrefix),
490-
pathPrefix);
491-
} else {
492-
$location = new LocationHashbangUrl(
493-
convertToHashbangUrl(initUrl, basePath, hashPrefix),
494-
hashPrefix);
495-
}
496-
// link rewriting
497-
absUrlPrefix = composeProtocolHostPort(
498-
$location.protocol(), $location.host(), $location.port()) + pathPrefix;
483+
appBase = baseHref ? serverBase(initialUrl) + baseHref : initialUrl;
484+
LocationMode = $sniffer.history ? LocationHtml5Url : LocationHashbangInHtml5Url;
499485
} else {
500-
$location = new LocationHashbangUrl(initUrl, hashPrefix);
501-
absUrlPrefix = $location.absUrl().split('#')[0];
486+
appBase = stripHash(initialUrl);
487+
LocationMode = LocationHashbangUrl;
502488
}
489+
$location = new LocationMode(appBase, '#' + hashPrefix);
490+
$location.$$parse($location.$$rewrite(initialUrl));
503491

504492
$rootElement.bind('click', function(event) {
505493
// TODO(vojta): rewrite link when opening in new tab/window (in legacy browser)
@@ -515,27 +503,21 @@ function $LocationProvider(){
515503
}
516504

517505
var absHref = elm.prop('href'),
518-
href;
519-
520-
if (!absHref ||
521-
elm.attr('target') ||
522-
absHref.indexOf(absUrlPrefix) !== 0) { // link to different domain or base path
523-
return;
506+
rewrittenUrl = $location.$$rewrite(absHref);
507+
508+
if (absHref && !elm.attr('target') && rewrittenUrl) {
509+
// update location manually
510+
$location.$$parse(rewrittenUrl);
511+
$rootScope.$apply();
512+
event.preventDefault();
513+
// hack to work around FF6 bug 684208 when scenario runner clicks on links
514+
window.angular['ff-684208-preventDefault'] = true;
524515
}
525-
526-
// update location with href without the prefix
527-
href = absHref.substr(absUrlPrefix.length);
528-
if (href.indexOf('#' + hashPrefix) == 0) href = href.substr(hashPrefix.length + 1);
529-
$location.url(href);
530-
$rootScope.$apply();
531-
event.preventDefault();
532-
// hack to work around FF6 bug 684208 when scenario runner clicks on links
533-
window.angular['ff-684208-preventDefault'] = true;
534516
});
535517

536518

537519
// rewrite hashbang url <> html5 url
538-
if ($location.absUrl() != initUrl) {
520+
if ($location.absUrl() != initialUrl) {
539521
$browser.url($location.absUrl(), true);
540522
}
541523

src/ngMock/angular-mocks.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ angular.mock.$Browser = function() {
3939
var self = this;
4040

4141
this.isMock = true;
42-
self.$$url = "http://server";
42+
self.$$url = "http://server/";
4343
self.$$lastUrl = self.$$url; // used by url polling fn
4444
self.pollFns = [];
4545

0 commit comments

Comments
 (0)