diff --git a/src/AngularPublic.js b/src/AngularPublic.js index b225fc85e00e..1688c4214138 100755 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -126,8 +126,7 @@ function publishExternalAPI(angular){ $sniffer: $SnifferProvider, $templateCache: $TemplateCacheProvider, $timeout: $TimeoutProvider, - $window: $WindowProvider, - $$urlUtils: $$UrlUtilsProvider + $window: $WindowProvider }); } ]); diff --git a/src/ng/compile.js b/src/ng/compile.js index 55de18eca3ab..b52db6072794 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -276,9 +276,9 @@ function $CompileProvider($provide) { this.$get = [ '$injector', '$interpolate', '$exceptionHandler', '$http', '$templateCache', '$parse', - '$controller', '$rootScope', '$document', '$sce', '$$urlUtils', '$animate', + '$controller', '$rootScope', '$document', '$sce', '$animate', function($injector, $interpolate, $exceptionHandler, $http, $templateCache, $parse, - $controller, $rootScope, $document, $sce, $$urlUtils, $animate) { + $controller, $rootScope, $document, $sce, $animate) { var Attributes = function(element, attr) { this.$$element = element; @@ -370,9 +370,9 @@ function $CompileProvider($provide) { // sanitize a[href] and img[src] values if ((nodeName === 'A' && key === 'href') || (nodeName === 'IMG' && key === 'src')) { - // NOTE: $$urlUtils.resolve() doesn't support IE < 8 so we don't sanitize for that case. + // NOTE: urlResolve() doesn't support IE < 8 so we don't sanitize for that case. if (!msie || msie >= 8 ) { - normalizedVal = $$urlUtils.resolve(value); + normalizedVal = urlResolve(value).href; if (normalizedVal !== '') { if ((key === 'href' && !normalizedVal.match(aHrefSanitizationWhitelist)) || (key === 'src' && !normalizedVal.match(imgSrcSanitizationWhitelist))) { diff --git a/src/ng/http.js b/src/ng/http.js index 8fdc0708b1b6..de8e6b6d690a 100644 --- a/src/ng/http.js +++ b/src/ng/http.js @@ -132,8 +132,8 @@ function $HttpProvider() { */ var responseInterceptorFactories = this.responseInterceptors = []; - this.$get = ['$httpBackend', '$browser', '$cacheFactory', '$rootScope', '$q', '$injector', '$$urlUtils', - function($httpBackend, $browser, $cacheFactory, $rootScope, $q, $injector, $$urlUtils) { + this.$get = ['$httpBackend', '$browser', '$cacheFactory', '$rootScope', '$q', '$injector', + function($httpBackend, $browser, $cacheFactory, $rootScope, $q, $injector) { var defaultCache = $cacheFactory('$http'); @@ -649,7 +649,7 @@ function $HttpProvider() { config.headers = headers; config.method = uppercase(config.method); - var xsrfValue = $$urlUtils.isSameOrigin(config.url) + var xsrfValue = urlIsSameOrigin(config.url) ? $browser.cookies()[config.xsrfCookieName || defaults.xsrfCookieName] : undefined; if (xsrfValue) { diff --git a/src/ng/httpBackend.js b/src/ng/httpBackend.js index 3e7406b0fde7..55ea56079460 100644 --- a/src/ng/httpBackend.js +++ b/src/ng/httpBackend.js @@ -102,8 +102,7 @@ function createHttpBackend($browser, XHR, $browserDefer, callbacks, rawDocument, } function completeRequest(callback, status, response, headersString) { - // URL_MATCH is defined in src/service/location.js - var protocol = (url.match(SERVER_MATCH) || ['', locationProtocol])[1]; + var protocol = locationProtocol || urlResolve(url).protocol; // cancel timeout and subsequent timeout promise resolution timeoutId && $browserDefer.cancel(timeoutId); diff --git a/src/ng/location.js b/src/ng/location.js index 94917cd1e5cb..6be8b5d78a19 100644 --- a/src/ng/location.js +++ b/src/ng/location.js @@ -1,7 +1,6 @@ 'use strict'; -var SERVER_MATCH = /^([^:]+):\/\/(\w+:{0,1}\w*@)?(\{?[\w\.-]*\}?)(:([0-9]+))?(\/[^\?#]*)?(\?([^#]*))?(#(.*))?$/, - PATH_MATCH = /^([^\?#]*)(\?([^#]*))?(#(.*))?$/, +var PATH_MATCH = /^([^\?#]*)(\?([^#]*))?(#(.*))?$/, DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21}; var $locationMinErr = minErr('$location'); @@ -24,19 +23,22 @@ function encodePath(path) { } function matchUrl(url, obj) { - var match = SERVER_MATCH.exec(url); + var match = urlResolve(url); - obj.$$protocol = match[1]; - obj.$$host = match[3]; - obj.$$port = int(match[5]) || DEFAULT_PORTS[match[1]] || null; + obj.$$protocol = match.protocol ? match.protocol.replace(/:$/,'') : ''; + obj.$$host = match.hostname; + obj.$$port = int(match.port) || DEFAULT_PORTS[match.protocol] || null; } -function matchAppUrl(url, obj) { - var match = PATH_MATCH.exec(url); - - obj.$$path = decodeURIComponent(match[1]); - obj.$$search = parseKeyValue(match[3]); - obj.$$hash = decodeURIComponent(match[5] || ''); +function matchAppUrl(path, obj) { + var prefixed = path.charAt(0) !== '/'; + if (prefixed) { + path = '/' + path; + } + var match = urlResolve(path); + obj.$$path = decodeURIComponent(prefixed && match.pathname.charAt(0) === '/' ? match.pathname.substring(1) : match.pathname); + obj.$$search = parseKeyValue(match.search); + obj.$$hash = decodeURIComponent(match.hash); // make sure path starts with '/'; if (obj.$$path && obj.$$path.charAt(0) != '/') obj.$$path = '/' + obj.$$path; diff --git a/src/ng/sce.js b/src/ng/sce.js index 7a40fbc948a4..8d374b9af553 100644 --- a/src/ng/sce.js +++ b/src/ng/sce.js @@ -123,7 +123,7 @@ function adjustMatchers(matchers) { * // The blacklist overrides the whitelist so the open redirect here is blocked. * $sceDelegateProvider.resourceUrlBlacklist([ * 'http://myapp.example.com/clickThru**']); - * }); + * }); * */ @@ -199,8 +199,8 @@ function $SceDelegateProvider() { return resourceUrlBlacklist; }; - this.$get = ['$log', '$document', '$injector', '$$urlUtils', function( - $log, $document, $injector, $$urlUtils) { + this.$get = ['$log', '$document', '$injector', function( + $log, $document, $injector) { var htmlSanitizer = function htmlSanitizer(html) { throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.'); @@ -213,7 +213,7 @@ function $SceDelegateProvider() { function matchUrl(matcher, parsedUrl) { if (matcher === 'self') { - return $$urlUtils.isSameOrigin(parsedUrl); + return urlIsSameOrigin(parsedUrl); } else { // definitely a regex. See adjustMatchers() return !!matcher.exec(parsedUrl.href); @@ -221,7 +221,7 @@ function $SceDelegateProvider() { } function isResourceUrlAllowedByPolicy(url) { - var parsedUrl = $$urlUtils.resolve(url.toString(), true); + var parsedUrl = urlResolve(url.toString(), true); var i, n, allowed = false; // Ensure that at least one item from the whitelist allows this url. for (i = 0, n = resourceUrlWhitelist.length; i < n; i++) { diff --git a/src/ng/urlUtils.js b/src/ng/urlUtils.js index f1e31d7a3f7e..f065185b7918 100644 --- a/src/ng/urlUtils.js +++ b/src/ng/urlUtils.js @@ -1,119 +1,103 @@ 'use strict'; +// NOTE: The usage of window and document instead of $window and $document here is +// deliberate. This service depends on the specific behavior of anchor nodes created by the +// browser (resolving and parsing URLs) that is unlikely to be provided by mock objects and +// cause us to break tests. In addition, when the browser resolves a URL for XHR, it +// doesn't know about mocked locations and resolves URLs to the real document - which is +// exactly the behavior needed here. There is little value is mocking these out for this +// service. +var $$urlParsingNode = document.createElement("a"); -function $$UrlUtilsProvider() { - this.$get = [function() { - var urlParsingNode = document.createElement("a"), - // NOTE: The usage of window and document instead of $window and $document here is - // deliberate. This service depends on the specific behavior of anchor nodes created by the - // browser (resolving and parsing URLs) that is unlikely to be provided by mock objects and - // cause us to break tests. In addition, when the browser resolves a URL for XHR, it - // doesn't know about mocked locations and resolves URLs to the real document - which is - // exactly the behavior needed here. There is little value is mocking these our for this - // service. - originUrl = resolve(window.location.href, true); - - /** - * @description - * Normalizes and optionally parses a URL. - * - * NOTE: This is a private service. The API is subject to change unpredictably in any commit. - * - * Implementation Notes for non-IE browsers - * ---------------------------------------- - * Assigning a URL to the href property of an anchor DOM node, even one attached to the DOM, - * results both in the normalizing and parsing of the URL. Normalizing means that a relative - * URL will be resolved into an absolute URL in the context of the application document. - * Parsing means that the anchor node's host, hostname, protocol, port, pathname and related - * properties are all populated to reflect the normalized URL. This approach has wide - * compatibility - Safari 1+, Mozilla 1+, Opera 7+,e etc. See - * http://www.aptana.com/reference/html/api/HTMLAnchorElement.html - * - * Implementation Notes for IE - * --------------------------- - * IE >= 8 and <= 10 normalizes the URL when assigned to the anchor node similar to the other - * browsers. However, the parsed components will not be set if the URL assigned did not specify - * them. (e.g. if you assign a.href = "foo", then a.protocol, a.host, etc. will be empty.) We - * work around that by performing the parsing in a 2nd step by taking a previously normalized - * URL (e.g. by assining to a.href) and assigning it a.href again. This correctly populates the - * properties such as protocol, hostname, port, etc. - * - * IE7 does not normalize the URL when assigned to an anchor node. (Apparently, it does, if one - * uses the inner HTML approach to assign the URL as part of an HTML snippet - - * http://stackoverflow.com/a/472729) However, setting img[src] does normalize the URL. - * Unfortunately, setting img[src] to something like "javascript:foo" on IE throws an exception. - * Since the primary usage for normalizing URLs is to sanitize such URLs, we can't use that - * method and IE < 8 is unsupported. - * - * References: - * http://developer.mozilla.org/en-US/docs/Web/API/HTMLAnchorElement - * http://www.aptana.com/reference/html/api/HTMLAnchorElement.html - * http://url.spec.whatwg.org/#urlutils - * https://github.com/angular/angular.js/pull/2902 - * http://james.padolsey.com/javascript/parsing-urls-with-the-dom/ - * - * @param {string} url The URL to be parsed. - * @param {boolean=} parse When true, returns an object for the parsed URL. Otherwise, returns - * a single string that is the normalized URL. - * @returns {object|string} When parse is true, returns the normalized URL as a string. - * Otherwise, returns an object with the following members. - * - * | member name | Description | - * |---------------|----------------| - * | href | A normalized version of the provided URL if it was not an absolute URL | - * | protocol | The protocol including the trailing colon | - * | host | The host and port (if the port is non-default) of the normalizedUrl | - * - * These fields from the UrlUtils interface are currently not needed and hence not returned. - * - * | member name | Description | - * |---------------|----------------| - * | hostname | The host without the port of the normalizedUrl | - * | pathname | The path following the host in the normalizedUrl | - * | hash | The URL hash if present | - * | search | The query string | - * - */ - function resolve(url, parse) { - var href = url; - if (msie <= 11) { - // Normalize before parse. Refer Implementation Notes on why this is - // done in two steps on IE. - urlParsingNode.setAttribute("href", href); - href = urlParsingNode.href; - } - urlParsingNode.setAttribute('href', href); - - if (!parse) { - return urlParsingNode.href; - } - // urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils - return { - href: urlParsingNode.href, - protocol: urlParsingNode.protocol, - host: urlParsingNode.host - // Currently unused and hence commented out. - // hostname: urlParsingNode.hostname, - // port: urlParsingNode.port, - // pathname: urlParsingNode.pathname, - // hash: urlParsingNode.hash, - // search: urlParsingNode.search - }; - } +/** + * + * Implementation Notes for non-IE browsers + * ---------------------------------------- + * Assigning a URL to the href property of an anchor DOM node, even one attached to the DOM, + * results both in the normalizing and parsing of the URL. Normalizing means that a relative + * URL will be resolved into an absolute URL in the context of the application document. + * Parsing means that the anchor node's host, hostname, protocol, port, pathname and related + * properties are all populated to reflect the normalized URL. This approach has wide + * compatibility - Safari 1+, Mozilla 1+, Opera 7+,e etc. See + * http://www.aptana.com/reference/html/api/HTMLAnchorElement.html + * + * Implementation Notes for IE + * --------------------------- + * IE >= 8 and <= 10 normalizes the URL when assigned to the anchor node similar to the other + * browsers. However, the parsed components will not be set if the URL assigned did not specify + * them. (e.g. if you assign a.href = "foo", then a.protocol, a.host, etc. will be empty.) We + * work around that by performing the parsing in a 2nd step by taking a previously normalized + * URL (e.g. by assining to a.href) and assigning it a.href again. This correctly populates the + * properties such as protocol, hostname, port, etc. + * + * IE7 does not normalize the URL when assigned to an anchor node. (Apparently, it does, if one + * uses the inner HTML approach to assign the URL as part of an HTML snippet - + * http://stackoverflow.com/a/472729) However, setting img[src] does normalize the URL. + * Unfortunately, setting img[src] to something like "javascript:foo" on IE throws an exception. + * Since the primary usage for normalizing URLs is to sanitize such URLs, we can't use that + * method and IE < 8 is unsupported. + * + * References: + * http://developer.mozilla.org/en-US/docs/Web/API/HTMLAnchorElement + * http://www.aptana.com/reference/html/api/HTMLAnchorElement.html + * http://url.spec.whatwg.org/#urlutils + * https://github.com/angular/angular.js/pull/2902 + * http://james.padolsey.com/javascript/parsing-urls-with-the-dom/ + * + * @param {string} url The URL to be parsed. + * @description Normalizes and parses a URL. + * @returns {object} Returns the normalized URL as a dictionary. + * @name urlResolve + * @function + * + * | member name | Description | + * |---------------|----------------| + * | href | A normalized version of the provided URL if it was not an absolute URL | + * | protocol | The protocol including the trailing colon | + * | host | The host and port (if the port is non-default) of the normalizedUrl | + * | search | The search params, minus the question mark | + * | hash | The hash string, minus the hash symbol + * | hostname | The hostname + * | port | The port, without ":" + * | pathname | The pathname, beginning with "/" + * + */ +var $$urlResolve = function(url) { + var href = url; + if (msie) { + // Normalize before parse. Refer Implementation Notes on why this is + // done in two steps on IE. + $$urlParsingNode.setAttribute("href", href); + href = $$urlParsingNode.href; + } + $$urlParsingNode.setAttribute('href', href); + return $$urlParsingNode; +} - return { - resolve: resolve, - /** - * Parse a request URL and determine whether this is a same-origin request as the application document. - * - * @param {string|object} requestUrl The url of the request as a string that will be resolved - * or a parsed URL object. - * @returns {boolean} Whether the request is for the same origin as the application document. - */ - isSameOrigin: function isSameOrigin(requestUrl) { - var parsed = (typeof requestUrl === 'string') ? resolve(requestUrl, true) : requestUrl; - return (parsed.protocol === originUrl.protocol && - parsed.host === originUrl.host); - } - }; - }]; +var urlResolve = function(url) { + var $$urlParsingNode = $$urlResolve(url); + // $$urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils + return { + href: $$urlParsingNode.href, + protocol: $$urlParsingNode.protocol ? $$urlParsingNode.protocol.replace(/:$/, '') : '', + host: $$urlParsingNode.host, + search: $$urlParsingNode.search ? $$urlParsingNode.search.replace(/^\?/, '') : '', + hash: $$urlParsingNode.hash ? $$urlParsingNode.hash.replace(/^#/, '') : '', + hostname: $$urlParsingNode.hostname, + port: $$urlParsingNode.port, + pathname: $$urlParsingNode.pathname && $$urlParsingNode.pathname.charAt(0) === '/' ? $$urlParsingNode.pathname : '/' + $$urlParsingNode.pathname + }; } +/** + * Parse a request URL and determine whether this is a same-origin request as the application document. + * + * @param {string|object} requestUrl The url of the request as a string that will be resolved + * or a parsed URL object. + * @returns {boolean} Whether the request is for the same origin as the application document. + */ +var urlIsSameOrigin = function isSameOrigin(requestUrl) { + var parsed = (typeof requestUrl === 'string') ? urlResolve(requestUrl, true) : requestUrl; + return (parsed.protocol === $$originUrl.protocol && + parsed.host === $$originUrl.host); +}; + +var $$originUrl = urlResolve(window.location.href, true); diff --git a/test/ng/httpBackendSpec.js b/test/ng/httpBackendSpec.js index faec5737e4c5..f39e83eae139 100644 --- a/test/ng/httpBackendSpec.js +++ b/test/ng/httpBackendSpec.js @@ -363,7 +363,7 @@ describe('$httpBackend', function() { it('should convert 0 to 200 if content', function() { - $backend = createHttpBackend($browser, MockXhr, null, null, null, 'http'); + $backend = createHttpBackend($browser, MockXhr); $backend('GET', 'file:///whatever/index.html', null, callback); respond(0, 'SOME CONTENT'); @@ -373,19 +373,8 @@ describe('$httpBackend', function() { }); - it('should convert 0 to 200 if content - relative url', function() { - $backend = createHttpBackend($browser, MockXhr, null, null, null, 'file'); - - $backend('GET', '/whatever/index.html', null, callback); - respond(0, 'SOME CONTENT'); - - expect(callback).toHaveBeenCalled(); - expect(callback.mostRecentCall.args[0]).toBe(200); - }); - - it('should convert 0 to 404 if no content', function() { - $backend = createHttpBackend($browser, MockXhr, null, null, null, 'http'); + $backend = createHttpBackend($browser, MockXhr); $backend('GET', 'file:///whatever/index.html', null, callback); respond(0, ''); @@ -404,7 +393,6 @@ describe('$httpBackend', function() { expect(callback).toHaveBeenCalled(); expect(callback.mostRecentCall.args[0]).toBe(404); }); - }); }); diff --git a/test/ng/locationSpec.js b/test/ng/locationSpec.js index a60f1609e8e6..0fd4b81fd3cd 100644 --- a/test/ng/locationSpec.js +++ b/test/ng/locationSpec.js @@ -11,7 +11,7 @@ describe('$location', function() { }); describe('NewUrl', function() { - beforeEach(function() { + beforeEach(function () { url = new LocationHtml5Url('http://www.domain.com:9877/'); url.$$parse('http://www.domain.com:9877/path/b?search=a&b=c&d#hash'); }); @@ -695,69 +695,6 @@ describe('$location', function() { }); }); - - describe('SERVER_MATCH', function() { - - it('should parse basic url', function() { - var match = SERVER_MATCH.exec('http://www.angularjs.org/path?search#hash?x=x'); - - expect(match[1]).toBe('http'); - expect(match[3]).toBe('www.angularjs.org'); - }); - - - it('should parse file://', function() { - var match = SERVER_MATCH.exec('file:///Users/Shared/misko/work/angular.js/scenario/widgets.html'); - - expect(match[1]).toBe('file'); - expect(match[3]).toBe(''); - expect(match[5]).toBeFalsy(); - }); - - - it('should parse url with "-" in host', function() { - var match = SERVER_MATCH.exec('http://a-b1.c-d.09/path'); - - expect(match[1]).toBe('http'); - expect(match[3]).toBe('a-b1.c-d.09'); - expect(match[5]).toBeFalsy(); - }); - - - it('should parse host without "/" at the end', function() { - var match = SERVER_MATCH.exec('http://host.org'); - expect(match[3]).toBe('host.org'); - - match = SERVER_MATCH.exec('http://host.org#'); - expect(match[3]).toBe('host.org'); - - match = SERVER_MATCH.exec('http://host.org?'); - expect(match[3]).toBe('host.org'); - }); - - - it('should parse chrome extension urls', function() { - var match = SERVER_MATCH.exec('chrome-extension://jjcldkdmokihdaomalanmlohibnoplog/index.html?foo#bar'); - - expect(match[1]).toBe('chrome-extension'); - expect(match[3]).toBe('jjcldkdmokihdaomalanmlohibnoplog'); - }); - - it('should parse FFOS app:// urls', function() { - var match = SERVER_MATCH.exec('app://{d0419af1-8b42-41c5-96f4-ef4179e52315}/path'); - - expect(match[1]).toBe('app'); - expect(match[3]).toBe('{d0419af1-8b42-41c5-96f4-ef4179e52315}'); - expect(match[5]).toBeFalsy(); - expect(match[6]).toBe('/path'); - expect(match[8]).toBeFalsy(); - - match = SERVER_MATCH.exec('app://}foo{') - expect(match).toBe(null); - }); - }); - - describe('PATH_MATCH', function() { it('should parse just path', function() { @@ -1327,7 +1264,7 @@ describe('$location', function() { ); - it('should listen on click events on href and prevent browser default in hashbang 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'); diff --git a/test/ng/urlUtilsSpec.js b/test/ng/urlUtilsSpec.js index 3c9bf847602c..18675ed812a6 100644 --- a/test/ng/urlUtilsSpec.js +++ b/test/ng/urlUtilsSpec.js @@ -1,29 +1,29 @@ 'use strict'; -describe('$$urlUtils', function() { +describe('urlUtils', function() { describe('parse', function() { - it('should normalize a relative url', inject(function($$urlUtils) { - expect($$urlUtils.resolve("foo")).toMatch(/^https?:\/\/[^/]+\/foo$/); - })); + it('should normalize a relative url', function () { + expect(urlResolve("foo").href).toMatch(/^https?:\/\/[^/]+\/foo$/); + }); - it('should parse relative URL into component pieces', inject(function($$urlUtils) { - var parsed = $$urlUtils.resolve("foo", true); + it('should parse relative URL into component pieces', function () { + var parsed = urlResolve("foo"); expect(parsed.href).toMatch(/https?:\/\//); - expect(parsed.protocol).toMatch(/^https?:/); + expect(parsed.protocol).toMatch(/^https?/); expect(parsed.host).not.toBe(""); expect(parsed.hostname).not.toBe(""); expect(parsed.pathname).not.toBe(""); - })); + }); }); describe('isSameOrigin', function() { - it('should support various combinations of urls - both string and parsed', inject(function($$urlUtils, $document) { + it('should support various combinations of urls - both string and parsed', inject(function($document) { function expectIsSameOrigin(url, expectedValue) { - expect($$urlUtils.isSameOrigin(url)).toBe(expectedValue); - expect($$urlUtils.isSameOrigin($$urlUtils.resolve(url, true))).toBe(expectedValue); + expect(urlIsSameOrigin(url)).toBe(expectedValue); + expect(urlIsSameOrigin(urlResolve(url, true))).toBe(expectedValue); } expectIsSameOrigin('path', true); - var origin = $$urlUtils.resolve($document[0].location.href, true); + var origin = urlResolve($document[0].location.href, true); expectIsSameOrigin('//' + origin.host + '/path', true); // Different domain. expectIsSameOrigin('http://example.com/path', false);