diff --git a/src/ng/httpBackend.js b/src/ng/httpBackend.js index 0a12aa23b4a5..97a815817029 100644 --- a/src/ng/httpBackend.js +++ b/src/ng/httpBackend.js @@ -87,7 +87,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(URL_MATCH) || ['', locationProtocol])[1]; + var protocol = (url.match(SERVER_MATCH) || ['', locationProtocol])[1]; // fix status code for file protocol (it's always 0) status = (protocol == 'file') ? (response ? 200 : 404) : status; diff --git a/src/ng/location.js b/src/ng/location.js index b5f102930365..b2f0e7d356a0 100644 --- a/src/ng/location.js +++ b/src/ng/location.js @@ -1,8 +1,7 @@ 'use strict'; -var URL_MATCH = /^([^:]+):\/\/(\w+:{0,1}\w*@)?([\w\.-]*)(:([0-9]+))?(\/[^\?#]*)?(\?([^#]*))?(#(.*))?$/, - PATH_MATCH = /^([^\?#]*)?(\?([^#]*))?(#(.*))?$/, - HASH_MATCH = PATH_MATCH, +var SERVER_MATCH = /^([^:]+):\/\/(\w+:{0,1}\w*@)?([\w\.-]*)(:([0-9]+))?/, + PATH_MATCH = /^([^\?#]*)(\?([^#]*))?(#(.*))?$/, DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21}; @@ -23,104 +22,73 @@ function encodePath(path) { return segments.join('/'); } - function matchUrl(url, obj) { - var match = URL_MATCH.exec(url); - - match = { - protocol: match[1], - host: match[3], - port: int(match[5]) || DEFAULT_PORTS[match[1]] || null, - path: match[6] || '/', - search: match[8], - hash: match[10] - }; - - if (obj) { - obj.$$protocol = match.protocol; - obj.$$host = match.host; - obj.$$port = match.port; - } + var match = SERVER_MATCH.exec(url); - return match; + obj.$$protocol = match[1]; + obj.$$host = match[3]; + obj.$$port = int(match[5]) || DEFAULT_PORTS[match[1]] || null; } +function matchAppUrl(url, obj) { + var match = PATH_MATCH.exec(url); -function composeProtocolHostPort(protocol, host, port) { - return protocol + '://' + host + (port == DEFAULT_PORTS[protocol] ? '' : ':' + port); -} - + obj.$$path = decodeURIComponent(match[1]); + obj.$$search = parseKeyValue(match[3]); + obj.$$hash = decodeURIComponent(match[5] || ''); -function pathPrefixFromBase(basePath) { - return basePath.substr(0, basePath.lastIndexOf('/')); + // make sure path starts with '/'; + if (obj.$$path && obj.$$path.charAt(0) != '/') obj.$$path = '/' + obj.$$path; } -function convertToHtml5Url(url, basePath, hashPrefix) { - var match = matchUrl(url); +function composeProtocolHostPort(protocol, host, port) { + return protocol + '://' + host + (port == DEFAULT_PORTS[protocol] ? '' : ':' + port); +} - // already html5 url - if (decodeURIComponent(match.path) != basePath || isUndefined(match.hash) || - match.hash.indexOf(hashPrefix) !== 0) { - return url; - // convert hashbang url -> html5 url - } else { - return composeProtocolHostPort(match.protocol, match.host, match.port) + - pathPrefixFromBase(basePath) + match.hash.substr(hashPrefix.length); - } +function beginsWith(begin, whole, otherwise) { + return whole.indexOf(begin) == 0 ? whole.substr(begin.length) : otherwise; } -function convertToHashbangUrl(url, basePath, hashPrefix) { - var match = matchUrl(url); +function stripHash(url) { + var index = url.indexOf('#'); + return index == -1 ? url : url.substr(0, index); +} - // already hashbang url - if (decodeURIComponent(match.path) == basePath) { - return url; - // convert html5 url -> hashbang url - } else { - var search = match.search && '?' + match.search || '', - hash = match.hash && '#' + match.hash || '', - pathPrefix = pathPrefixFromBase(basePath), - path = match.path.substr(pathPrefix.length); - if (match.path.indexOf(pathPrefix) !== 0) { - throw Error('Invalid url "' + url + '", missing path prefix "' + pathPrefix + '" !'); - } +function stripFile(url) { + return url.substr(0, stripHash(url).lastIndexOf('/') + 1); +} - return composeProtocolHostPort(match.protocol, match.host, match.port) + basePath + - '#' + hashPrefix + path + search + hash; - } +/* return the server only */ +function serverBase(url) { + return url.substring(0, url.indexOf('/', url.indexOf('//') + 2)); } /** - * LocationUrl represents an url + * LocationHtml5Url represents an url * This object is exposed as $location service when HTML5 mode is enabled and supported * * @constructor - * @param {string} url HTML5 url - * @param {string} pathPrefix + * @param {string} appBase application base URL + * @param {string} hashPrefix hasbang prefix */ -function LocationUrl(url, pathPrefix) { - pathPrefix = pathPrefix || ''; - +function LocationHtml5Url(appBase, hashPrefix) { + var appBaseNoFile = stripFile(appBase); /** * Parse given html5 (regular) url string into properties * @param {string} url HTML5 url * @private */ this.$$parse = function(url) { - var match = matchUrl(url, this); - - if (match.path.indexOf(pathPrefix) !== 0) { - throw Error('Invalid url "' + url + '", missing path prefix "' + pathPrefix + '" !'); + matchUrl(url, this); + matchAppUrl(url.substr(appBaseNoFile.length), this); + if (!this.$$path) { + this.$$path = '/'; } - this.$$path = decodeURIComponent(match.path.substr(pathPrefix.length)); - this.$$search = parseKeyValue(match.search); - this.$$hash = match.hash && decodeURIComponent(match.hash) || ''; - this.$$compose(); }; @@ -133,11 +101,24 @@ function LocationUrl(url, pathPrefix) { hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; - this.$$absUrl = composeProtocolHostPort(this.$$protocol, this.$$host, this.$$port) + - pathPrefix + this.$$url; + this.$$absUrl = appBaseNoFile + this.$$url.substr(1); // first char is always '/' }; - this.$$parse(url); + this.$$rewrite = function(url) { + var appUrl; + + if ( (appUrl = beginsWith(appBase, url)) !== undefined ) { + if ( (appUrl = beginsWith(hashPrefix, appUrl)) !== undefined ) { + return appBaseNoFile + (beginsWith('/', appUrl) || appUrl); + } else { + return appBase; + } + } else if ( (appUrl = beginsWith(appBaseNoFile, url)) ) { + return appBaseNoFile + appUrl; + } else if (appBaseNoFile == url + '/') { + return appBaseNoFile; + } + } } @@ -146,11 +127,11 @@ function LocationUrl(url, pathPrefix) { * This object is exposed as $location service when html5 history api is disabled or not supported * * @constructor - * @param {string} url Legacy url - * @param {string} hashPrefix Prefix for hash part (containing path and search) + * @param {string} appBase application base URL + * @param {string} hashPrefix hasbang prefix */ -function LocationHashbangUrl(url, hashPrefix) { - var basePath; +function LocationHashbangUrl(appBase, hashPrefix) { + var appBaseNoFile = stripFile(appBase); /** * Parse given hashbang url into properties @@ -158,24 +139,10 @@ function LocationHashbangUrl(url, hashPrefix) { * @private */ this.$$parse = function(url) { - var match = matchUrl(url, this); - - - if (match.hash && match.hash.indexOf(hashPrefix) !== 0) { - throw Error('Invalid url "' + url + '", missing hash prefix "' + hashPrefix + '" !'); - } - - basePath = match.path + (match.search ? '?' + match.search : ''); - match = HASH_MATCH.exec((match.hash || '').substr(hashPrefix.length)); - if (match[1]) { - this.$$path = (match[1].charAt(0) == '/' ? '' : '/') + decodeURIComponent(match[1]); - } else { - this.$$path = ''; - } - - this.$$search = parseKeyValue(match[3]); - this.$$hash = match[5] && decodeURIComponent(match[5]) || ''; - + matchUrl(url, this); + url = beginsWith(appBase, url) || beginsWith(appBaseNoFile, url); + url = beginsWith(hashPrefix, url, url); + matchAppUrl(url, this); this.$$compose(); }; @@ -188,15 +155,48 @@ function LocationHashbangUrl(url, hashPrefix) { hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; - this.$$absUrl = composeProtocolHostPort(this.$$protocol, this.$$host, this.$$port) + - basePath + (this.$$url ? '#' + hashPrefix + this.$$url : ''); + this.$$absUrl = appBase + (this.$$url ? hashPrefix + this.$$url : ''); }; - this.$$parse(url); + this.$$rewrite = function(url) { + if(stripHash(appBase) == stripHash(url)) { + return url; + } + } } -LocationUrl.prototype = { +/** + * 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 hasbang prefix + */ +function LocationHashbangInHtml5Url(appBase, hashPrefix) { + 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 = { /** * Has any change been replacing ? @@ -378,8 +378,6 @@ LocationUrl.prototype = { } }; -LocationHashbangUrl.prototype = inherit(LocationUrl.prototype); - function locationGetter(property) { return function() { return this[property]; @@ -476,30 +474,20 @@ function $LocationProvider(){ this.$get = ['$rootScope', '$browser', '$sniffer', '$rootElement', function( $rootScope, $browser, $sniffer, $rootElement) { var $location, - basePath, - pathPrefix, - initUrl = $browser.url(), - absUrlPrefix; + LocationMode, + baseHref = $browser.baseHref(), + initialUrl = $browser.url(), + appBase; if (html5Mode) { - basePath = $browser.baseHref() || '/'; - pathPrefix = pathPrefixFromBase(basePath); - if ($sniffer.history) { - $location = new LocationUrl( - convertToHtml5Url(initUrl, basePath, hashPrefix), - pathPrefix); - } else { - $location = new LocationHashbangUrl( - convertToHashbangUrl(initUrl, basePath, hashPrefix), - hashPrefix); - } - // link rewriting - absUrlPrefix = composeProtocolHostPort( - $location.protocol(), $location.host(), $location.port()) + pathPrefix; + appBase = baseHref ? serverBase(initialUrl) + baseHref : initialUrl; + LocationMode = $sniffer.history ? LocationHtml5Url : LocationHashbangInHtml5Url; } else { - $location = new LocationHashbangUrl(initUrl, hashPrefix); - absUrlPrefix = $location.absUrl().split('#')[0]; + appBase = stripHash(initialUrl); + LocationMode = LocationHashbangUrl; } + $location = new LocationMode(appBase, '#' + hashPrefix); + $location.$$parse($location.$$rewrite(initialUrl)); $rootElement.bind('click', function(event) { // TODO(vojta): rewrite link when opening in new tab/window (in legacy browser) @@ -515,27 +503,21 @@ function $LocationProvider(){ } var absHref = elm.prop('href'), - href; - - if (!absHref || - elm.attr('target') || - absHref.indexOf(absUrlPrefix) !== 0) { // link to different domain or base path - return; + rewrittenUrl = $location.$$rewrite(absHref); + + 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; }); // rewrite hashbang url <> html5 url - if ($location.absUrl() != initUrl) { + if ($location.absUrl() != initialUrl) { $browser.url($location.absUrl(), true); } diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js index 1e229214ecbd..9fec9710de28 100644 --- a/src/ngMock/angular-mocks.js +++ b/src/ngMock/angular-mocks.js @@ -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 = []; diff --git a/test/ng/locationSpec.js b/test/ng/locationSpec.js index 91f3688c57fc..26316f4bf751 100644 --- a/test/ng/locationSpec.js +++ b/test/ng/locationSpec.js @@ -14,7 +14,7 @@ function spyOnlyCallsWithArgs(obj, method) { } -describe('$location', function() { +ddescribe('$location', function() { var url; afterEach(function() { @@ -25,7 +25,8 @@ describe('$location', function() { describe('NewUrl', function() { beforeEach(function() { - url = new LocationUrl('http://www.domain.com:9877/path/b?search=a&b=c&d#hash'); + url = new LocationHtml5Url('http://www.domain.com:9877/'); + url.$$parse('http://www.domain.com:9877/path/b?search=a&b=c&d#hash'); }); @@ -132,16 +133,18 @@ describe('$location', function() { it('should parse new url', function() { - url = new LocationUrl('http://host.com/base'); + url = new LocationHtml5Url('http://host.com/'); + url.$$parse('http://host.com/base'); expect(url.path()).toBe('/base'); - url = new LocationUrl('http://host.com/base#'); + url = new LocationHtml5Url('http://host.com/'); + url.$$parse('http://host.com/base#'); expect(url.path()).toBe('/base'); }); it('should prefix path with forward-slash', function() { - url = new LocationUrl('http://server/a'); + url = new LocationHtml5Url('http://server/'); url.path('b'); expect(url.path()).toBe('/b'); @@ -150,7 +153,8 @@ describe('$location', function() { it('should set path to forward-slash when empty', function() { - url = new LocationUrl('http://server'); + url = new LocationHtml5Url('http://server/'); + url.$$parse('http://server/') expect(url.path()).toBe('/'); expect(url.absUrl()).toBe('http://server/'); }); @@ -175,7 +179,8 @@ describe('$location', function() { it('should prepend path with basePath', function() { - url = new LocationUrl('http://server/base/abc?a', '/base'); + url = new LocationHtml5Url('http://server/base/'); + url.$$parse('http://server/base/abc?a'); expect(url.path()).toBe('/abc'); expect(url.search()).toEqual({a: true}); @@ -184,15 +189,6 @@ describe('$location', function() { }); - it('should throw error when invalid url given', function() { - url = new LocationUrl('http://server.org/base/abc', '/base'); - - expect(function() { - url.$$parse('http://server.org/path#/path'); - }).toThrow('Invalid url "http://server.org/path#/path", missing path prefix "/base" !'); - }); - - describe('encoding', function() { it('should encode special characters', function() { @@ -217,7 +213,8 @@ describe('$location', function() { it('should decode special characters', function() { - url = new LocationUrl('http://host.com/a%20%3C%3E%23?i%20j=%3C%3E%23#x%20%3C%3E%23'); + url = new LocationHtml5Url('http://host.com/'); + url.$$parse('http://host.com/a%20%3C%3E%23?i%20j=%3C%3E%23#x%20%3C%3E%23'); expect(url.path()).toBe('/a <>#'); expect(url.search()).toEqual({'i j': '<>#'}); expect(url.hash()).toBe('x <>#'); @@ -229,7 +226,8 @@ describe('$location', function() { describe('HashbangUrl', function() { beforeEach(function() { - url = new LocationHashbangUrl('http://www.server.org:1234/base#!/path?a=b&c#hash', '!'); + url = new LocationHashbangUrl('http://www.server.org:1234/base', '#!'); + url.$$parse('http://www.server.org:1234/base#!/path?a=b&c#hash'); }); @@ -254,7 +252,8 @@ describe('$location', function() { it('should preserve query params in base', function() { - url = new LocationHashbangUrl('http://www.server.org:1234/base?base=param#/path?a=b&c#hash', ''); + url = new LocationHashbangUrl('http://www.server.org:1234/base?base=param', '#'); + url.$$parse('http://www.server.org:1234/base?base=param#/path?a=b&c#hash'); expect(url.absUrl()).toBe('http://www.server.org:1234/base?base=param#/path?a=b&c#hash'); url.path('/new/path'); @@ -265,7 +264,8 @@ describe('$location', function() { it('should prefix path with forward-slash', function() { - url = new LocationHashbangUrl('http://host.com/base#path', ''); + url = new LocationHashbangUrl('http://host.com/base', '#'); + url.$$parse('http://host.com/base#path'); expect(url.path()).toBe('/path'); expect(url.absUrl()).toBe('http://host.com/base#/path'); @@ -276,7 +276,8 @@ describe('$location', function() { it('should set path to forward-slash when empty', function() { - url = new LocationHashbangUrl('http://server/base#!', '!'); + url = new LocationHashbangUrl('http://server/base', '#!'); + url.$$parse('http://server/base'); url.path('aaa'); expect(url.path()).toBe('/aaa'); @@ -294,13 +295,6 @@ describe('$location', function() { }); - it('should throw error when invalid url given', function() { - expect(function() { - url.$$parse('http://server.org/path#/path'); - }).toThrow('Invalid url "http://server.org/path#/path", missing hash prefix "!" !'); - }); - - describe('encoding', function() { it('should encode special characters', function() { @@ -325,7 +319,8 @@ describe('$location', function() { it('should decode special characters', function() { - url = new LocationHashbangUrl('http://host.com/a#/%20%3C%3E%23?i%20j=%3C%3E%23#x%20%3C%3E%23', ''); + url = new LocationHashbangUrl('http://host.com/a', '#'); + url.$$parse('http://host.com/a#/%20%3C%3E%23?i%20j=%3C%3E%23#x%20%3C%3E%23'); expect(url.path()).toBe('/ <>#'); expect(url.search()).toEqual({'i j': '<>#'}); expect(url.hash()).toBe('x <>#'); @@ -333,13 +328,15 @@ describe('$location', function() { it('should return decoded characters for search specified in URL', function() { - var locationUrl = new LocationUrl('http://host.com/?q=1%2F2%203'); + var locationUrl = new LocationHtml5Url('http://host.com/'); + locationUrl.$$parse('http://host.com/?q=1%2F2%203'); expect(locationUrl.search()).toEqual({'q': '1/2 3'}); }); it('should return decoded characters for search specified with setter', function() { - var locationUrl = new LocationUrl('http://host.com/'); + var locationUrl = new LocationHtml5Url('http://host.com/'); + locationUrl.$$parse('http://host.com/') locationUrl.search('q', '1/2 3'); expect(locationUrl.search()).toEqual({'q': '1/2 3'}); }); @@ -364,7 +361,7 @@ describe('$location', function() { describe('wiring', function() { beforeEach(initService(false, '!', true)); - beforeEach(inject(initBrowser('http://new.com/a/b#!', '/a/b'))); + beforeEach(inject(initBrowser('http://new.com/a/b#!', 'http://new.com/a/b'))); it('should update $location when browser url changes', inject(function($browser, $location) { @@ -381,7 +378,7 @@ describe('$location', function() { it('should not $apply when browser url changed inside $apply', inject( function($rootScope, $browser, $location) { var OLD_URL = $browser.url(), - NEW_URL = 'http://updated.com/url'; + NEW_URL = 'http://new.com/a/b#!/new'; $rootScope.$apply(function() { @@ -397,7 +394,7 @@ describe('$location', function() { it('should not $apply when browser url changed inside $digest', inject( function($rootScope, $browser, $location) { var OLD_URL = $browser.url(), - NEW_URL = 'http://updated.com/url', + NEW_URL = 'http://new.com/a/b#!/new', notRunYet = true; $rootScope.$watch(function() { @@ -580,68 +577,51 @@ describe('$location', function() { }); - describe('URL_MATCH', function() { + describe('SERVER_MATCH', function() { it('should parse basic url', function() { - var match = URL_MATCH.exec('http://www.angularjs.org/path?search#hash?x=x'); + 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'); - expect(match[6]).toBe('/path'); - expect(match[8]).toBe('search'); - expect(match[10]).toBe('hash?x=x'); }); it('should parse file://', function() { - var match = URL_MATCH.exec('file:///Users/Shared/misko/work/angular.js/scenario/widgets.html'); + 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(); - expect(match[6]).toBe('/Users/Shared/misko/work/angular.js/scenario/widgets.html'); - expect(match[8]).toBeFalsy(); }); it('should parse url with "-" in host', function() { - var match = URL_MATCH.exec('http://a-b1.c-d.09/path'); + 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(); - expect(match[6]).toBe('/path'); - expect(match[8]).toBeFalsy(); }); it('should parse host without "/" at the end', function() { - var match = URL_MATCH.exec('http://host.org'); + var match = SERVER_MATCH.exec('http://host.org'); expect(match[3]).toBe('host.org'); - match = URL_MATCH.exec('http://host.org#'); + match = SERVER_MATCH.exec('http://host.org#'); expect(match[3]).toBe('host.org'); - match = URL_MATCH.exec('http://host.org?'); + match = SERVER_MATCH.exec('http://host.org?'); expect(match[3]).toBe('host.org'); }); - it('should match with just "/" path', function() { - var match = URL_MATCH.exec('http://server/#?book=moby'); - - expect(match[10]).toBe('?book=moby'); - }); - - it('should parse chrome extension urls', function() { - var match = URL_MATCH.exec('chrome-extension://jjcldkdmokihdaomalanmlohibnoplog/index.html?foo#bar'); + var match = SERVER_MATCH.exec('chrome-extension://jjcldkdmokihdaomalanmlohibnoplog/index.html?foo#bar'); expect(match[1]).toBe('chrome-extension'); expect(match[3]).toBe('jjcldkdmokihdaomalanmlohibnoplog'); - expect(match[6]).toBe('/index.html'); - expect(match[8]).toBe('foo'); - expect(match[10]).toBe('bar'); }); }); @@ -1029,6 +1009,35 @@ describe('$location', function() { expect($browser.url()).toEqual(base + '#!/view2'); }); }); + + + it('should not intercept clicks outside the current hash prefix', 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($location) { + // make IE happy + jqLite(window.document.body).html('link'); + + var event = { + target: jqLite(window.document.body).find('a')[0], + preventDefault: jasmine.createSpy('preventDefault') + }; + + + clickHandler(event); + expect(event.preventDefault).not.toHaveBeenCalled(); + }); + }); }); @@ -1155,7 +1164,7 @@ describe('$location', function() { inject(function($location, $rootScope, $browser, $rootElement) { var log = '', - link = $rootElement.find('a'); + link = $rootElement.find('a'); $rootScope.$on('$locationChangeStart', function(event) { event.preventDefault();