diff options
| -rwxr-xr-x | angularFiles.js | 1 | ||||
| -rwxr-xr-x | src/AngularPublic.js | 3 | ||||
| -rw-r--r-- | src/ng/compile.js | 25 | ||||
| -rw-r--r-- | src/ng/http.js | 43 | ||||
| -rw-r--r-- | src/ng/urlUtils.js | 111 | ||||
| -rw-r--r-- | test/ng/httpSpec.js | 21 | ||||
| -rw-r--r-- | test/ng/urlUtilsSpec.js | 31 | 
7 files changed, 160 insertions, 75 deletions
| diff --git a/angularFiles.js b/angularFiles.js index a9d636b1..b93283a7 100755 --- a/angularFiles.js +++ b/angularFiles.js @@ -30,6 +30,7 @@ angularFiles = {      'src/ng/httpBackend.js',      'src/ng/locale.js',      'src/ng/timeout.js', +    'src/ng/urlUtils.js',      'src/ng/filter.js',      'src/ng/filter/filter.js', diff --git a/src/AngularPublic.js b/src/AngularPublic.js index 03d465ff..99f6cdbd 100755 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -125,7 +125,8 @@ function publishExternalAPI(angular){          $sniffer: $SnifferProvider,          $templateCache: $TemplateCacheProvider,          $timeout: $TimeoutProvider, -        $window: $WindowProvider +        $window: $WindowProvider, +        $$urlUtils: $$UrlUtilsProvider        });      }    ]); diff --git a/src/ng/compile.js b/src/ng/compile.js index 7d2b6dc7..46ebe71a 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -274,9 +274,9 @@ function $CompileProvider($provide) {    this.$get = [              '$injector', '$interpolate', '$exceptionHandler', '$http', '$templateCache', '$parse', -            '$controller', '$rootScope', '$document', +            '$controller', '$rootScope', '$document', '$$urlUtils',      function($injector,   $interpolate,   $exceptionHandler,   $http,   $templateCache,   $parse, -             $controller,   $rootScope,   $document) { +             $controller,   $rootScope,   $document,   $$urlUtils) {      var Attributes = function(element, attr) {        this.$$element = element; @@ -319,24 +319,23 @@ function $CompileProvider($provide) {            }          } +        nodeName = nodeName_(this.$$element);          // sanitize a[href] and img[src] values -        nodeName = nodeName_(this.$$element);          if ((nodeName === 'A' && key === 'href') || -            (nodeName === 'IMG' && key === 'src')){ -          urlSanitizationNode.setAttribute('href', value); - -          // href property always returns normalized absolute url, so we can match against that -          normalizedVal = urlSanitizationNode.href; -          if (normalizedVal !== '') { -            if ((key === 'href' && !normalizedVal.match(aHrefSanitizationWhitelist)) || -                (key === 'src' && !normalizedVal.match(imgSrcSanitizationWhitelist))) { -              this[key] = value = 'unsafe:' + normalizedVal; +            (nodeName === 'IMG' && key === 'src')) { +          // NOTE: $$urlUtils.resolve() doesn't support IE < 8 so we don't sanitize for that case. +          if (!msie || msie >= 8 ) { +            normalizedVal = $$urlUtils.resolve(value); +            if (normalizedVal !== '') { +              if ((key === 'href' && !normalizedVal.match(aHrefSanitizationWhitelist)) || +                  (key === 'src' && !normalizedVal.match(imgSrcSanitizationWhitelist))) { +                this[key] = value = 'unsafe:' + normalizedVal; +              }              }            }          } -          if (writeAttr !== false) {            if (value === null || value === undefined) {              this.$$element.removeAttr(attrName); diff --git a/src/ng/http.js b/src/ng/http.js index a44da3a4..2aedeacb 100644 --- a/src/ng/http.js +++ b/src/ng/http.js @@ -29,43 +29,6 @@ function parseHeaders(headers) {  } -var IS_SAME_DOMAIN_URL_MATCH = /^(([^:]+):)?\/\/(\w+:{0,1}\w*@)?([\w\.-]*)?(:([0-9]+))?(.*)$/; - - -/** - * Parse a request and location URL and determine whether this is a same-domain request. - * - * @param {string} requestUrl The url of the request. - * @param {string} locationUrl The current browser location url. - * @returns {boolean} Whether the request is for the same domain. - */ -function isSameDomain(requestUrl, locationUrl) { -  var match = IS_SAME_DOMAIN_URL_MATCH.exec(requestUrl); -  // if requestUrl is relative, the regex does not match. -  if (match == null) return true; - -  var domain1 = { -      protocol: match[2], -      host: match[4], -      port: int(match[6]) || DEFAULT_PORTS[match[2]] || null, -      // IE8 sets unmatched groups to '' instead of undefined. -      relativeProtocol: match[2] === undefined || match[2] === '' -    }; - -  match = SERVER_MATCH.exec(locationUrl); -  var domain2 = { -      protocol: match[1], -      host: match[3], -      port: int(match[5]) || DEFAULT_PORTS[match[1]] || null -    }; - -  return (domain1.protocol == domain2.protocol || domain1.relativeProtocol) && -         domain1.host == domain2.host && -         (domain1.port == domain2.port || (domain1.relativeProtocol && -             domain2.port == DEFAULT_PORTS[domain2.protocol])); -} - -  /**   * Returns a function that provides access to parsed headers.   * @@ -168,8 +131,8 @@ function $HttpProvider() {     */    var responseInterceptorFactories = this.responseInterceptors = []; -  this.$get = ['$httpBackend', '$browser', '$cacheFactory', '$rootScope', '$q', '$injector', -      function($httpBackend, $browser, $cacheFactory, $rootScope, $q, $injector) { +  this.$get = ['$httpBackend', '$browser', '$cacheFactory', '$rootScope', '$q', '$injector', '$$urlUtils', +      function($httpBackend, $browser, $cacheFactory, $rootScope, $q, $injector, $$urlUtils) {      var defaultCache = $cacheFactory('$http'); @@ -657,7 +620,7 @@ function $HttpProvider() {        config.headers = headers;        config.method = uppercase(config.method); -      var xsrfValue = isSameDomain(config.url, $browser.url()) +      var xsrfValue = $$urlUtils.isSameOrigin(config.url)            ? $browser.cookies()[config.xsrfCookieName || defaults.xsrfCookieName]            : undefined;        if (xsrfValue) { diff --git a/src/ng/urlUtils.js b/src/ng/urlUtils.js new file mode 100644 index 00000000..e19f9860 --- /dev/null +++ b/src/ng/urlUtils.js @@ -0,0 +1,111 @@ +'use strict'; + +function $$UrlUtilsProvider() { +  this.$get = ['$window', '$document', function($window, $document) { +    var urlParsingNode = $document[0].createElement("a"), +        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) { +        // 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 +      }; +    } + +    return { +      resolve: resolve, +      /** +       * Parse a request URL and determine whether this is a same-origin request as the application document. +       * +       * @param {string} requestUrl The url of the request. +       * @returns {boolean} Whether the request is for the same origin as the application document. +       */ +      isSameOrigin: function isSameOrigin(requestUrl) { +        var parsed = resolve(requestUrl, true); +        return (parsed.protocol === originUrl.protocol && +                parsed.host === originUrl.host); +      } +    }; +  }]; +} diff --git a/test/ng/httpSpec.js b/test/ng/httpSpec.js index cefc88e1..ec1cb7f1 100644 --- a/test/ng/httpSpec.js +++ b/test/ng/httpSpec.js @@ -1476,25 +1476,4 @@ describe('$http', function() {      $httpBackend.verifyNoOutstandingExpectation = noop;    }); - -  describe('isSameDomain', function() { -    it('should support various combinations of urls', function() { -      expect(isSameDomain('path/morepath', -                          'http://www.adomain.com')).toBe(true); -      expect(isSameDomain('http://www.adomain.com/path', -                          'http://www.adomain.com')).toBe(true); -      expect(isSameDomain('//www.adomain.com/path', -                          'http://www.adomain.com')).toBe(true); -      expect(isSameDomain('//www.adomain.com/path', -                          'https://www.adomain.com')).toBe(true); -      expect(isSameDomain('//www.adomain.com/path', -                          'http://www.adomain.com:1234')).toBe(false); -      expect(isSameDomain('https://www.adomain.com/path', -                          'http://www.adomain.com')).toBe(false); -      expect(isSameDomain('http://www.adomain.com:1234/path', -                          'http://www.adomain.com')).toBe(false); -      expect(isSameDomain('http://www.anotherdomain.com/path', -                          'http://www.adomain.com')).toBe(false); -    }); -  });  }); diff --git a/test/ng/urlUtilsSpec.js b/test/ng/urlUtilsSpec.js new file mode 100644 index 00000000..57043a5a --- /dev/null +++ b/test/ng/urlUtilsSpec.js @@ -0,0 +1,31 @@ +'use strict'; + +describe('$$urlUtils', function() { +  describe('parse', function() { +    it('should normalize a relative url', inject(function($$urlUtils) { +      expect($$urlUtils.resolve("foo")).toMatch(/^https?:\/\/[^/]+\/foo$/); +    })); + +    it('should parse relative URL into component pieces', inject(function($$urlUtils) { +      var parsed = $$urlUtils.resolve("foo", true); +      expect(parsed.href).toMatch(/https?:\/\//); +      expect(parsed.protocol).toMatch(/^https?:/); +      expect(parsed.host).not.toBe(""); +    })); +  }); + +  describe('isSameOrigin', function() { +    it('should support various combinations of urls', inject(function($$urlUtils, $document) { +      expect($$urlUtils.isSameOrigin('path')).toBe(true); +      var origin = $$urlUtils.resolve($document[0].location.href, true); +      expect($$urlUtils.isSameOrigin('//' + origin.host + '/path')).toBe(true); +      // Different domain. +      expect($$urlUtils.isSameOrigin('http://example.com/path')).toBe(false); +      // Auto fill protocol. +      expect($$urlUtils.isSameOrigin('//example.com/path')).toBe(false); +      // Should not match when the ports are different. +      // This assumes that the test is *not* running on port 22 (very unlikely). +      expect($$urlUtils.isSameOrigin('//' + origin.hostname + ':22/path')).toBe(false); +    })); +  }); +}); | 
