mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-04-10 10:01:00 +00:00
The location service, and other portions of the application, were relying on a complicated regular expression to get parts of a URL. But there is already a private urlUtils provider, which relies on HTMLAnchorElement to provide this information, and is suitable for most cases. In order to make urlUtils more accessible in the absence of DI, its methods were converted to standalone functions available globally. The urlUtils.resolve method was renamed urlResolve, and was refactored to only take 1 argument, url, and not the 2nd "parse" boolean. The method now always returns a parsed url. All places in code which previously wanted a string instead of a parsed url can now get the value from the href property of the returned object. Tests were also added to ensure IPv6 addresses were handled correctly. Closes #3533 Closes #2950 Closes #3249
619 lines
17 KiB
JavaScript
619 lines
17 KiB
JavaScript
'use strict';
|
|
|
|
var PATH_MATCH = /^([^\?#]*)(\?([^#]*))?(#(.*))?$/,
|
|
DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21};
|
|
var $locationMinErr = minErr('$location');
|
|
|
|
|
|
/**
|
|
* Encode path using encodeUriSegment, ignoring forward slashes
|
|
*
|
|
* @param {string} path Path to encode
|
|
* @returns {string}
|
|
*/
|
|
function encodePath(path) {
|
|
var segments = path.split('/'),
|
|
i = segments.length;
|
|
|
|
while (i--) {
|
|
segments[i] = encodeUriSegment(segments[i]);
|
|
}
|
|
|
|
return segments.join('/');
|
|
}
|
|
|
|
function parseAbsoluteUrl(absoluteUrl, locationObj) {
|
|
var parsedUrl = urlResolve(absoluteUrl);
|
|
|
|
locationObj.$$protocol = parsedUrl.protocol;
|
|
locationObj.$$host = parsedUrl.hostname;
|
|
locationObj.$$port = int(parsedUrl.port) || DEFAULT_PORTS[parsedUrl.protocol] || null;
|
|
}
|
|
|
|
|
|
function parseAppUrl(relativeUrl, locationObj) {
|
|
var prefixed = (relativeUrl.charAt(0) !== '/');
|
|
if (prefixed) {
|
|
relativeUrl = '/' + relativeUrl;
|
|
}
|
|
var match = urlResolve(relativeUrl);
|
|
locationObj.$$path = decodeURIComponent(prefixed && match.pathname.charAt(0) === '/' ? match.pathname.substring(1) : match.pathname);
|
|
locationObj.$$search = parseKeyValue(match.search);
|
|
locationObj.$$hash = decodeURIComponent(match.hash);
|
|
|
|
// make sure path starts with '/';
|
|
if (locationObj.$$path && locationObj.$$path.charAt(0) != '/') locationObj.$$path = '/' + locationObj.$$path;
|
|
}
|
|
|
|
|
|
/**
|
|
*
|
|
* @param {string} begin
|
|
* @param {string} whole
|
|
* @returns {string} returns text from whole after begin or undefined if it does not begin with expected string.
|
|
*/
|
|
function beginsWith(begin, whole) {
|
|
if (whole.indexOf(begin) == 0) {
|
|
return whole.substr(begin.length);
|
|
}
|
|
}
|
|
|
|
|
|
function stripHash(url) {
|
|
var index = url.indexOf('#');
|
|
return index == -1 ? url : url.substr(0, index);
|
|
}
|
|
|
|
|
|
function stripFile(url) {
|
|
return url.substr(0, stripHash(url).lastIndexOf('/') + 1);
|
|
}
|
|
|
|
/* return the server only (scheme://host:port) */
|
|
function serverBase(url) {
|
|
return url.substring(0, url.indexOf('/', url.indexOf('//') + 2));
|
|
}
|
|
|
|
|
|
/**
|
|
* LocationHtml5Url represents an url
|
|
* This object is exposed as $location service when HTML5 mode is enabled and supported
|
|
*
|
|
* @constructor
|
|
* @param {string} appBase application base URL
|
|
* @param {string} basePrefix url path prefix
|
|
*/
|
|
function LocationHtml5Url(appBase, basePrefix) {
|
|
this.$$html5 = true;
|
|
basePrefix = basePrefix || '';
|
|
var appBaseNoFile = stripFile(appBase);
|
|
parseAbsoluteUrl(appBase, this);
|
|
|
|
|
|
/**
|
|
* Parse given html5 (regular) url string into properties
|
|
* @param {string} newAbsoluteUrl HTML5 url
|
|
* @private
|
|
*/
|
|
this.$$parse = function(url) {
|
|
var pathUrl = beginsWith(appBaseNoFile, url);
|
|
if (!isString(pathUrl)) {
|
|
throw $locationMinErr('ipthprfx', 'Invalid url "{0}", missing path prefix "{1}".', url, appBaseNoFile);
|
|
}
|
|
|
|
parseAppUrl(pathUrl, this);
|
|
|
|
if (!this.$$path) {
|
|
this.$$path = '/';
|
|
}
|
|
|
|
this.$$compose();
|
|
};
|
|
|
|
/**
|
|
* Compose url and update `absUrl` property
|
|
* @private
|
|
*/
|
|
this.$$compose = function() {
|
|
var search = toKeyValue(this.$$search),
|
|
hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : '';
|
|
|
|
this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash;
|
|
this.$$absUrl = appBaseNoFile + this.$$url.substr(1); // first char is always '/'
|
|
};
|
|
|
|
this.$$rewrite = function(url) {
|
|
var appUrl, prevAppUrl;
|
|
|
|
if ( (appUrl = beginsWith(appBase, url)) !== undefined ) {
|
|
prevAppUrl = appUrl;
|
|
if ( (appUrl = beginsWith(basePrefix, appUrl)) !== undefined ) {
|
|
return appBaseNoFile + (beginsWith('/', appUrl) || appUrl);
|
|
} else {
|
|
return appBase + prevAppUrl;
|
|
}
|
|
} else if ( (appUrl = beginsWith(appBaseNoFile, url)) !== undefined ) {
|
|
return appBaseNoFile + appUrl;
|
|
} else if (appBaseNoFile == url + '/') {
|
|
return appBaseNoFile;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* LocationHashbangUrl represents url
|
|
* This object is exposed as $location service when developer doesn't opt into html5 mode.
|
|
* It also serves as the base class for html5 mode fallback on legacy browsers.
|
|
*
|
|
* @constructor
|
|
* @param {string} appBase application base URL
|
|
* @param {string} hashPrefix hashbang prefix
|
|
*/
|
|
function LocationHashbangUrl(appBase, hashPrefix) {
|
|
var appBaseNoFile = stripFile(appBase);
|
|
|
|
parseAbsoluteUrl(appBase, this);
|
|
|
|
|
|
/**
|
|
* Parse given hashbang url into properties
|
|
* @param {string} url Hashbang url
|
|
* @private
|
|
*/
|
|
this.$$parse = function(url) {
|
|
var withoutBaseUrl = beginsWith(appBase, url) || beginsWith(appBaseNoFile, url);
|
|
var withoutHashUrl = withoutBaseUrl.charAt(0) == '#'
|
|
? beginsWith(hashPrefix, withoutBaseUrl)
|
|
: (this.$$html5)
|
|
? withoutBaseUrl
|
|
: '';
|
|
|
|
if (!isString(withoutHashUrl)) {
|
|
throw $locationMinErr('ihshprfx', 'Invalid url "{0}", missing hash prefix "{1}".', url, hashPrefix);
|
|
}
|
|
parseAppUrl(withoutHashUrl, this);
|
|
this.$$compose();
|
|
};
|
|
|
|
/**
|
|
* Compose hashbang url and update `absUrl` property
|
|
* @private
|
|
*/
|
|
this.$$compose = function() {
|
|
var search = toKeyValue(this.$$search),
|
|
hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : '';
|
|
|
|
this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash;
|
|
this.$$absUrl = appBase + (this.$$url ? hashPrefix + this.$$url : '');
|
|
};
|
|
|
|
this.$$rewrite = function(url) {
|
|
if(stripHash(appBase) == stripHash(url)) {
|
|
return url;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* LocationHashbangUrl represents url
|
|
* This object is exposed as $location service when html5 history api is enabled but the browser
|
|
* does not support it.
|
|
*
|
|
* @constructor
|
|
* @param {string} appBase application base URL
|
|
* @param {string} hashPrefix hashbang prefix
|
|
*/
|
|
function LocationHashbangInHtml5Url(appBase, hashPrefix) {
|
|
this.$$html5 = true;
|
|
LocationHashbangUrl.apply(this, arguments);
|
|
|
|
var appBaseNoFile = stripFile(appBase);
|
|
|
|
this.$$rewrite = function(url) {
|
|
var appUrl;
|
|
|
|
if ( appBase == stripHash(url) ) {
|
|
return url;
|
|
} else if ( (appUrl = beginsWith(appBaseNoFile, url)) ) {
|
|
return appBase + hashPrefix + appUrl;
|
|
} else if ( appBaseNoFile === url + '/') {
|
|
return appBaseNoFile;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
LocationHashbangInHtml5Url.prototype =
|
|
LocationHashbangUrl.prototype =
|
|
LocationHtml5Url.prototype = {
|
|
|
|
/**
|
|
* Are we in html5 mode?
|
|
* @private
|
|
*/
|
|
$$html5: false,
|
|
|
|
/**
|
|
* Has any change been replacing ?
|
|
* @private
|
|
*/
|
|
$$replace: false,
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ng.$location#absUrl
|
|
* @methodOf ng.$location
|
|
*
|
|
* @description
|
|
* This method is getter only.
|
|
*
|
|
* Return full url representation with all segments encoded according to rules specified in
|
|
* {@link http://www.ietf.org/rfc/rfc3986.txt RFC 3986}.
|
|
*
|
|
* @return {string} full url
|
|
*/
|
|
absUrl: locationGetter('$$absUrl'),
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ng.$location#url
|
|
* @methodOf ng.$location
|
|
*
|
|
* @description
|
|
* This method is getter / setter.
|
|
*
|
|
* Return url (e.g. `/path?a=b#hash`) when called without any parameter.
|
|
*
|
|
* Change path, search and hash, when called with parameter and return `$location`.
|
|
*
|
|
* @param {string=} url New url without base prefix (e.g. `/path?a=b#hash`)
|
|
* @param {string=} replace The path that will be changed
|
|
* @return {string} url
|
|
*/
|
|
url: function(url, replace) {
|
|
if (isUndefined(url))
|
|
return this.$$url;
|
|
|
|
var match = PATH_MATCH.exec(url);
|
|
if (match[1]) this.path(decodeURIComponent(match[1]));
|
|
if (match[2] || match[1]) this.search(match[3] || '');
|
|
this.hash(match[5] || '', replace);
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ng.$location#protocol
|
|
* @methodOf ng.$location
|
|
*
|
|
* @description
|
|
* This method is getter only.
|
|
*
|
|
* Return protocol of current url.
|
|
*
|
|
* @return {string} protocol of current url
|
|
*/
|
|
protocol: locationGetter('$$protocol'),
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ng.$location#host
|
|
* @methodOf ng.$location
|
|
*
|
|
* @description
|
|
* This method is getter only.
|
|
*
|
|
* Return host of current url.
|
|
*
|
|
* @return {string} host of current url.
|
|
*/
|
|
host: locationGetter('$$host'),
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ng.$location#port
|
|
* @methodOf ng.$location
|
|
*
|
|
* @description
|
|
* This method is getter only.
|
|
*
|
|
* Return port of current url.
|
|
*
|
|
* @return {Number} port
|
|
*/
|
|
port: locationGetter('$$port'),
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ng.$location#path
|
|
* @methodOf ng.$location
|
|
*
|
|
* @description
|
|
* This method is getter / setter.
|
|
*
|
|
* Return path of current url when called without any parameter.
|
|
*
|
|
* Change path when called with parameter and return `$location`.
|
|
*
|
|
* Note: Path should always begin with forward slash (/), this method will add the forward slash
|
|
* if it is missing.
|
|
*
|
|
* @param {string=} path New path
|
|
* @return {string} path
|
|
*/
|
|
path: locationGetterSetter('$$path', function(path) {
|
|
return path.charAt(0) == '/' ? path : '/' + path;
|
|
}),
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ng.$location#search
|
|
* @methodOf ng.$location
|
|
*
|
|
* @description
|
|
* This method is getter / setter.
|
|
*
|
|
* Return search part (as object) of current url when called without any parameter.
|
|
*
|
|
* Change search part when called with parameter and return `$location`.
|
|
*
|
|
* @param {string|Object.<string>|Object.<Array.<string>>} search New search params - string or hash object. Hash object
|
|
* may contain an array of values, which will be decoded as duplicates in the url.
|
|
* @param {string=} paramValue If `search` is a string, then `paramValue` will override only a
|
|
* single search parameter. If the value is `null`, the parameter will be deleted.
|
|
*
|
|
* @return {string} search
|
|
*/
|
|
search: function(search, paramValue) {
|
|
switch (arguments.length) {
|
|
case 0:
|
|
return this.$$search;
|
|
case 1:
|
|
if (isString(search)) {
|
|
this.$$search = parseKeyValue(search);
|
|
} else if (isObject(search)) {
|
|
this.$$search = search;
|
|
} else {
|
|
throw $locationMinErr('isrcharg', 'The first argument of the `$location#search()` call must be a string or an object.');
|
|
}
|
|
break;
|
|
default:
|
|
if (paramValue == undefined || paramValue == null) {
|
|
delete this.$$search[search];
|
|
} else {
|
|
this.$$search[search] = paramValue;
|
|
}
|
|
}
|
|
|
|
this.$$compose();
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ng.$location#hash
|
|
* @methodOf ng.$location
|
|
*
|
|
* @description
|
|
* This method is getter / setter.
|
|
*
|
|
* Return hash fragment when called without any parameter.
|
|
*
|
|
* Change hash fragment when called with parameter and return `$location`.
|
|
*
|
|
* @param {string=} hash New hash fragment
|
|
* @return {string} hash
|
|
*/
|
|
hash: locationGetterSetter('$$hash', identity),
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ng.$location#replace
|
|
* @methodOf ng.$location
|
|
*
|
|
* @description
|
|
* If called, all changes to $location during current `$digest` will be replacing current history
|
|
* record, instead of adding new one.
|
|
*/
|
|
replace: function() {
|
|
this.$$replace = true;
|
|
return this;
|
|
}
|
|
};
|
|
|
|
function locationGetter(property) {
|
|
return function() {
|
|
return this[property];
|
|
};
|
|
}
|
|
|
|
|
|
function locationGetterSetter(property, preprocess) {
|
|
return function(value) {
|
|
if (isUndefined(value))
|
|
return this[property];
|
|
|
|
this[property] = preprocess(value);
|
|
this.$$compose();
|
|
|
|
return this;
|
|
};
|
|
}
|
|
|
|
|
|
/**
|
|
* @ngdoc object
|
|
* @name ng.$location
|
|
*
|
|
* @requires $browser
|
|
* @requires $sniffer
|
|
* @requires $rootElement
|
|
*
|
|
* @description
|
|
* The $location service parses the URL in the browser address bar (based on the
|
|
* {@link https://developer.mozilla.org/en/window.location window.location}) and makes the URL
|
|
* available to your application. Changes to the URL in the address bar are reflected into
|
|
* $location service and changes to $location are reflected into the browser address bar.
|
|
*
|
|
* **The $location service:**
|
|
*
|
|
* - Exposes the current URL in the browser address bar, so you can
|
|
* - Watch and observe the URL.
|
|
* - Change the URL.
|
|
* - Synchronizes the URL with the browser when the user
|
|
* - Changes the address bar.
|
|
* - Clicks the back or forward button (or clicks a History link).
|
|
* - Clicks on a link.
|
|
* - Represents the URL object as a set of methods (protocol, host, port, path, search, hash).
|
|
*
|
|
* For more information see {@link guide/dev_guide.services.$location Developer Guide: Angular
|
|
* Services: Using $location}
|
|
*/
|
|
|
|
/**
|
|
* @ngdoc object
|
|
* @name ng.$locationProvider
|
|
* @description
|
|
* Use the `$locationProvider` to configure how the application deep linking paths are stored.
|
|
*/
|
|
function $LocationProvider(){
|
|
var hashPrefix = '',
|
|
html5Mode = false;
|
|
|
|
/**
|
|
* @ngdoc property
|
|
* @name ng.$locationProvider#hashPrefix
|
|
* @methodOf ng.$locationProvider
|
|
* @description
|
|
* @param {string=} prefix Prefix for hash part (containing path and search)
|
|
* @returns {*} current value if used as getter or itself (chaining) if used as setter
|
|
*/
|
|
this.hashPrefix = function(prefix) {
|
|
if (isDefined(prefix)) {
|
|
hashPrefix = prefix;
|
|
return this;
|
|
} else {
|
|
return hashPrefix;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @ngdoc property
|
|
* @name ng.$locationProvider#html5Mode
|
|
* @methodOf ng.$locationProvider
|
|
* @description
|
|
* @param {boolean=} mode Use HTML5 strategy if available.
|
|
* @returns {*} current value if used as getter or itself (chaining) if used as setter
|
|
*/
|
|
this.html5Mode = function(mode) {
|
|
if (isDefined(mode)) {
|
|
html5Mode = mode;
|
|
return this;
|
|
} else {
|
|
return html5Mode;
|
|
}
|
|
};
|
|
|
|
this.$get = ['$rootScope', '$browser', '$sniffer', '$rootElement',
|
|
function( $rootScope, $browser, $sniffer, $rootElement) {
|
|
var $location,
|
|
LocationMode,
|
|
baseHref = $browser.baseHref(), // if base[href] is undefined, it defaults to ''
|
|
initialUrl = $browser.url(),
|
|
appBase;
|
|
|
|
if (html5Mode) {
|
|
appBase = serverBase(initialUrl) + (baseHref || '/');
|
|
LocationMode = $sniffer.history ? LocationHtml5Url : LocationHashbangInHtml5Url;
|
|
} else {
|
|
appBase = stripHash(initialUrl);
|
|
LocationMode = LocationHashbangUrl;
|
|
}
|
|
$location = new LocationMode(appBase, '#' + hashPrefix);
|
|
$location.$$parse($location.$$rewrite(initialUrl));
|
|
|
|
$rootElement.on('click', function(event) {
|
|
// TODO(vojta): rewrite link when opening in new tab/window (in legacy browser)
|
|
// currently we open nice url link and redirect then
|
|
|
|
if (event.ctrlKey || event.metaKey || event.which == 2) return;
|
|
|
|
var elm = jqLite(event.target);
|
|
|
|
// traverse the DOM up to find first A tag
|
|
while (lowercase(elm[0].nodeName) !== 'a') {
|
|
// ignore rewriting if no A tag (reached root element, or no parent - removed from document)
|
|
if (elm[0] === $rootElement[0] || !(elm = elm.parent())[0]) return;
|
|
}
|
|
|
|
var absHref = elm.prop('href');
|
|
var rewrittenUrl = $location.$$rewrite(absHref);
|
|
|
|
if (absHref && !elm.attr('target') && rewrittenUrl && !event.isDefaultPrevented()) {
|
|
event.preventDefault();
|
|
if (rewrittenUrl != $browser.url()) {
|
|
// update location manually
|
|
$location.$$parse(rewrittenUrl);
|
|
$rootScope.$apply();
|
|
// hack to work around FF6 bug 684208 when scenario runner clicks on links
|
|
window.angular['ff-684208-preventDefault'] = true;
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
// rewrite hashbang url <> html5 url
|
|
if ($location.absUrl() != initialUrl) {
|
|
$browser.url($location.absUrl(), true);
|
|
}
|
|
|
|
// update $location when $browser url changes
|
|
$browser.onUrlChange(function(newUrl) {
|
|
if ($location.absUrl() != newUrl) {
|
|
if ($rootScope.$broadcast('$locationChangeStart', newUrl, $location.absUrl()).defaultPrevented) {
|
|
$browser.url($location.absUrl());
|
|
return;
|
|
}
|
|
$rootScope.$evalAsync(function() {
|
|
var oldUrl = $location.absUrl();
|
|
|
|
$location.$$parse(newUrl);
|
|
afterLocationChange(oldUrl);
|
|
});
|
|
if (!$rootScope.$$phase) $rootScope.$digest();
|
|
}
|
|
});
|
|
|
|
// update browser
|
|
var changeCounter = 0;
|
|
$rootScope.$watch(function $locationWatch() {
|
|
var oldUrl = $browser.url();
|
|
var currentReplace = $location.$$replace;
|
|
|
|
if (!changeCounter || oldUrl != $location.absUrl()) {
|
|
changeCounter++;
|
|
$rootScope.$evalAsync(function() {
|
|
if ($rootScope.$broadcast('$locationChangeStart', $location.absUrl(), oldUrl).
|
|
defaultPrevented) {
|
|
$location.$$parse(oldUrl);
|
|
} else {
|
|
$browser.url($location.absUrl(), currentReplace);
|
|
afterLocationChange(oldUrl);
|
|
}
|
|
});
|
|
}
|
|
$location.$$replace = false;
|
|
|
|
return changeCounter;
|
|
});
|
|
|
|
return $location;
|
|
|
|
function afterLocationChange(oldUrl) {
|
|
$rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl);
|
|
}
|
|
}];
|
|
}
|