diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/Browser.js | 20 | ||||
| -rw-r--r-- | src/angular-mocks.js | 27 | ||||
| -rw-r--r-- | src/service/xhr.js | 78 | 
3 files changed, 98 insertions, 27 deletions
| diff --git a/src/Browser.js b/src/Browser.js index fe6220ed..abafb2a5 100644 --- a/src/Browser.js +++ b/src/Browser.js @@ -7,6 +7,11 @@ var XHR = window.XMLHttpRequest || function () {    try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e3) {}    throw new Error("This browser does not support XMLHttpRequest.");  }; +var XHR_HEADERS = { +  "Content-Type": "application/x-www-form-urlencoded", +  "Accept": "application/json, text/plain, */*", +  "X-Requested-With": "XMLHttpRequest" +};  /**   * @private @@ -72,11 +77,18 @@ function Browser(window, document, body, XHR, $log) {     * @param {string} url Requested url     * @param {?string} post Post data to send (null if nothing to post)     * @param {function(number, string)} callback Function that will be called on response +   * @param {object=} header additional HTTP headers to send with XHR. +   *   Standard headers are: +   *   <ul> +   *     <li><tt>Content-Type</tt>: <tt>application/x-www-form-urlencoded</tt></li> +   *     <li><tt>Accept</tt>: <tt>application/json, text/plain, */*</tt></li> +   *     <li><tt>X-Requested-With</tt>: <tt>XMLHttpRequest</tt></li> +   *   </ul>     *     * @description     * Send ajax request     */ -  self.xhr = function(method, url, post, callback) { +  self.xhr = function(method, url, post, callback, headers) {      outstandingRequestCount ++;      if (lowercase(method) == 'json') {        var callbackId = "angular_" + Math.random() + '_' + (idCounter++); @@ -92,9 +104,9 @@ function Browser(window, document, body, XHR, $log) {      } else {        var xhr = new XHR();        xhr.open(method, url, true); -      xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); -      xhr.setRequestHeader("Accept", "application/json, text/plain, */*"); -      xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); +      forEach(extend(XHR_HEADERS, headers || {}), function(value, key){ +        if (value) xhr.setRequestHeader(key, value); +      });        xhr.onreadystatechange = function() {          if (xhr.readyState == 4) {            completeOutstandingRequest(callback, xhr.status || 200, xhr.responseText); diff --git a/src/angular-mocks.js b/src/angular-mocks.js index 762148fd..558e71e3 100644 --- a/src/angular-mocks.js +++ b/src/angular-mocks.js @@ -101,28 +101,27 @@ function MockBrowser() {    }; -  self.xhr = function(method, url, data, callback) { -    if (angular.isFunction(data)) { -      callback = data; -      data = null; -    } +  self.xhr = function(method, url, data, callback, headers) { +    headers = headers || {};      if (data && angular.isObject(data)) data = angular.toJson(data);      if (data && angular.isString(data)) url += "|" + data;      var expect = expectations[method] || {}; -    var response = expect[url]; -    if (!response) { -      throw { -        message: "Unexpected request for method '" + method + "' and url '" + url + "'.", -        name: "Unexpected Request" -      }; +    var expectation = expect[url]; +    if (!expectation) { +      throw new Error("Unexpected request for method '" + method + "' and url '" + url + "'.");      }      requests.push(function(){ -      callback(response.code, response.response); +      forEach(expectation.headers, function(value, key){ +        if (headers[key] !== value) { +          throw new Error("Missing HTTP request header: " + key + ": " + value); +        } +      }); +      callback(expectation.code, expectation.response);      });    };    self.xhr.expectations = expectations;    self.xhr.requests = requests; -  self.xhr.expect = function(method, url, data) { +  self.xhr.expect = function(method, url, data, headers) {      if (data && angular.isObject(data)) data = angular.toJson(data);      if (data && angular.isString(data)) url += "|" + data;      var expect = expectations[method] || (expectations[method] = {}); @@ -132,7 +131,7 @@ function MockBrowser() {            response = code;            code = 200;          } -        expect[url] = {code:code, response:response}; +        expect[url] = {code:code, response:response, headers: headers || {}};        }      };    }; diff --git a/src/service/xhr.js b/src/service/xhr.js index b1b57a82..1de72b22 100644 --- a/src/service/xhr.js +++ b/src/service/xhr.js @@ -3,13 +3,71 @@   * @ngdoc service   * @name angular.service.$xhr   * @function - * @requires $browser - * @requires $xhr.error - * @requires $log + * @requires $browser $xhr delegates all XHR requests to the `$browser.xhr()`. A mock version + *                    of the $browser exists which allows setting expectaitions on XHR requests + *                    in your tests + * @requires $xhr.error $xhr delegates all non `2xx` response code to this service. + * @requires $log $xhr delegates all exceptions to `$log.error()`. + * @requires $updateView After a server response the view needs to be updated for data-binding to + *           take effect.   *   * @description - * Generates an XHR request. The $xhr service adds error handling then delegates all requests to - * {@link angular.service.$browser $browser.xhr()}. + * Generates an XHR request. The $xhr service delegates all requests to + * {@link angular.service.$browser $browser.xhr()} and adds error handling and security features. + * While $xhr service provides nicer api than raw XmlHttpRequest, it is still considered a lower + * level api in angular. For a higher level abstraction that utilizes `$xhr`, please check out the + * {@link angular.service$resource $resource} service. + * + * # Error handling + * All XHR responses with response codes other then `2xx` are delegated to + * {@link angular.service.$xhr.error $xhr.error}. The `$xhr.error` can intercept the request + * and process it in application specific way, or resume normal execution by calling the + * request callback method. + * + * # Security Considerations + * When designing web applications your design needs to consider security threats from + * {@link http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx + * JSON Vulnerability} and {@link http://en.wikipedia.org/wiki/Cross-site_request_forgery XSRF}. + * Both server and the client must cooperate in order to eliminate these threats. Angular comes + * pre-configured with strategies that address these issues, but for this to work backend server + * cooperation is required. + * + * ## JSON Vulnerability Protection + * A {@link http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx + * JSON Vulnerability} allows third party web-site to turn your JSON resource URL into + * {@link http://en.wikipedia.org/wiki/JSON#JSONP JSONP} request under some conditions. To + * counter this your server can prefix all JSON requests with following string `")]}',\n"`. + * Angular will automatically strip the prefix before processing it as JSON. + * + * For example if your server needs to return: + * <pre> + * ['one','two'] + * </pre> + * + * which is vulnerable to attack, your server can return: + * <pre> + * )]}', + * ['one','two'] + * </pre> + * + * angular will strip the prefix, before processing the JSON. + * + * + * ## Cross Site Request Forgery (XSRF) Protection + * {@link http://en.wikipedia.org/wiki/Cross-site_request_forgery XSRF} is a technique by which an + * unauthorized site can gain your user's private data. Angular provides following mechanism to + * counter XSRF. When performing XHR requests, the $xhr service reads a token from a cookie + * called `XSRF-TOKEN` and sets it as the HTTP header `X-XSRF-TOKEN`. Since only JavaScript that + * runs on your domain could read the cookie, your server can be assured that the XHR came from + * JavaScript running on your domain. + * + * To take advantage of this, your server needs to set a token in a JavaScript readable session + * cookie called `XSRF-TOKEN` on first HTTP GET request. On subsequent non-GET requests the server + * can verify that the cookie matches `X-XSRF-TOKEN` HTTP header, and therefore be sure that only + * JavaScript running on your domain could have read the token. The token must be unique for each + * user and must be verifiable by  the server (to prevent the JavaScript making up its own tokens). + * We recommend that the token is a digest of your site's authentication cookie with + * {@link http://en.wikipedia.org/wiki/Rainbow_table salt for added security}.   *   * @param {string} method HTTP method to use. Valid values are: `GET`, `POST`, `PUT`, `DELETE`, and   *   `JSON`. `JSON` is a special case which causes a @@ -67,8 +125,7 @@       </doc:source>     </doc:example>   */ -angularServiceInject('$xhr', function($browser, $error, $log){ -  var self = this; +angularServiceInject('$xhr', function($browser, $error, $log, $updateView){    return function(method, url, post, callback){      if (isFunction(post)) {        callback = post; @@ -77,6 +134,7 @@ angularServiceInject('$xhr', function($browser, $error, $log){      if (post && isObject(post)) {        post = toJson(post);      } +      $browser.xhr(method, url, post, function(code, response){        try {          if (isString(response)) { @@ -95,8 +153,10 @@ angularServiceInject('$xhr', function($browser, $error, $log){        } catch (e) {          $log.error(e);        } finally { -        self.$eval(); +        $updateView();        } +    }, { +        'X-XSRF-TOKEN': $browser.cookies()['XSRF-TOKEN']      });    }; -}, ['$browser', '$xhr.error', '$log']); +}, ['$browser', '$xhr.error', '$log', '$updateView']); | 
