diff options
Diffstat (limited to 'src/ng/http.js')
| -rw-r--r-- | src/ng/http.js | 743 |
1 files changed, 743 insertions, 0 deletions
diff --git a/src/ng/http.js b/src/ng/http.js new file mode 100644 index 00000000..c2cbd161 --- /dev/null +++ b/src/ng/http.js @@ -0,0 +1,743 @@ +'use strict'; +'use strict'; + +/** + * Parse headers into key value object + * + * @param {string} headers Raw headers as a string + * @returns {Object} Parsed headers as key value object + */ +function parseHeaders(headers) { + var parsed = {}, key, val, i; + + if (!headers) return parsed; + + forEach(headers.split('\n'), function(line) { + i = line.indexOf(':'); + key = lowercase(trim(line.substr(0, i))); + val = trim(line.substr(i + 1)); + + if (key) { + if (parsed[key]) { + parsed[key] += ', ' + val; + } else { + parsed[key] = val; + } + } + }); + + return parsed; +} + + +/** + * Returns a function that provides access to parsed headers. + * + * Headers are lazy parsed when first requested. + * @see parseHeaders + * + * @param {(string|Object)} headers Headers to provide access to. + * @returns {function(string=)} Returns a getter function which if called with: + * + * - if called with single an argument returns a single header value or null + * - if called with no arguments returns an object containing all headers. + */ +function headersGetter(headers) { + var headersObj = isObject(headers) ? headers : undefined; + + return function(name) { + if (!headersObj) headersObj = parseHeaders(headers); + + if (name) { + return headersObj[lowercase(name)] || null; + } + + return headersObj; + }; +} + + +/** + * Chain all given functions + * + * This function is used for both request and response transforming + * + * @param {*} data Data to transform. + * @param {function(string=)} headers Http headers getter fn. + * @param {(function|Array.<function>)} fns Function or an array of functions. + * @returns {*} Transformed data. + */ +function transformData(data, headers, fns) { + if (isFunction(fns)) + return fns(data, headers); + + forEach(fns, function(fn) { + data = fn(data, headers); + }); + + return data; +} + + +function isSuccess(status) { + return 200 <= status && status < 300; +} + + +function $HttpProvider() { + var JSON_START = /^\s*(\[|\{[^\{])/, + JSON_END = /[\}\]]\s*$/, + PROTECTION_PREFIX = /^\)\]\}',?\n/; + + var $config = this.defaults = { + // transform incoming response data + transformResponse: function(data) { + if (isString(data)) { + // strip json vulnerability protection prefix + data = data.replace(PROTECTION_PREFIX, ''); + if (JSON_START.test(data) && JSON_END.test(data)) + data = fromJson(data, true); + } + return data; + }, + + // transform outgoing request data + transformRequest: function(d) { + return isObject(d) && !isFile(d) ? toJson(d) : d; + }, + + // default headers + headers: { + common: { + 'Accept': 'application/json, text/plain, */*', + 'X-Requested-With': 'XMLHttpRequest' + }, + post: {'Content-Type': 'application/json'}, + put: {'Content-Type': 'application/json'} + } + }; + + var providerResponseInterceptors = this.responseInterceptors = []; + + this.$get = ['$httpBackend', '$browser', '$cacheFactory', '$rootScope', '$q', '$injector', + function($httpBackend, $browser, $cacheFactory, $rootScope, $q, $injector) { + + var defaultCache = $cacheFactory('$http'), + responseInterceptors = []; + + forEach(providerResponseInterceptors, function(interceptor) { + responseInterceptors.push( + isString(interceptor) + ? $injector.get(interceptor) + : $injector.invoke(interceptor) + ); + }); + + + /** + * @ngdoc function + * @name angular.module.ng.$http + * @requires $httpBacked + * @requires $browser + * @requires $cacheFactory + * @requires $rootScope + * @requires $q + * @requires $injector + * + * @description + * The `$http` service is a core Angular service that facilitates communication with the remote + * HTTP servers via browser's {@link https://developer.mozilla.org/en/xmlhttprequest + * XMLHttpRequest} object or via {@link http://en.wikipedia.org/wiki/JSONP JSONP}. + * + * For unit testing applications that use `$http` service, see + * {@link angular.module.ngMock.$httpBackend $httpBackend mock}. + * + * For a higher level of abstraction, please check out the {@link angular.module.ng.$resource + * $resource} service. + * + * The $http API is based on the {@link angular.module.ng.$q deferred/promise APIs} exposed by + * the $q service. While for simple usage patters this doesn't matter much, for advanced usage, + * it is important to familiarize yourself with these apis and guarantees they provide. + * + * + * # General usage + * The `$http` service is a function which takes a single argument — a configuration object — + * that is used to generate an http request and returns a {@link angular.module.ng.$q promise} + * with two $http specific methods: `success` and `error`. + * + * <pre> + * $http({method: 'GET', url: '/someUrl'}). + * success(function(data, status, headers, config) { + * // this callback will be called asynchronously + * // when the response is available + * }). + * error(function(data, status, headers, config) { + * // called asynchronously if an error occurs + * // or server returns response with status + * // code outside of the <200, 400) range + * }); + * </pre> + * + * Since the returned value of calling the $http function is a Promise object, you can also use + * the `then` method to register callbacks, and these callbacks will receive a single argument – + * an object representing the response. See the api signature and type info below for more + * details. + * + * + * # Shortcut methods + * + * Since all invocation of the $http service require definition of the http method and url and + * POST and PUT requests require response body/data to be provided as well, shortcut methods + * were created to simplify using the api: + * + * <pre> + * $http.get('/someUrl').success(successCallback); + * $http.post('/someUrl', data).success(successCallback); + * </pre> + * + * Complete list of shortcut methods: + * + * - {@link angular.module.ng.$http#get $http.get} + * - {@link angular.module.ng.$http#head $http.head} + * - {@link angular.module.ng.$http#post $http.post} + * - {@link angular.module.ng.$http#put $http.put} + * - {@link angular.module.ng.$http#delete $http.delete} + * - {@link angular.module.ng.$http#jsonp $http.jsonp} + * + * + * # Setting HTTP Headers + * + * The $http service will automatically add certain http headers to all requests. These defaults + * can be fully configured by accessing the `$httpProvider.defaults.headers` configuration + * object, which currently contains this default configuration: + * + * - `$httpProvider.defaults.headers.common` (headers that are common for all requests): + * - `Accept: application/json, text/plain, * / *` + * - `X-Requested-With: XMLHttpRequest` + * - `$httpProvider.defaults.headers.post`: (header defaults for HTTP POST requests) + * - `Content-Type: application/json` + * - `$httpProvider.defaults.headers.put` (header defaults for HTTP PUT requests) + * - `Content-Type: application/json` + * + * To add or overwrite these defaults, simply add or remove a property from this configuration + * objects. To add headers for an HTTP method other than POST or PUT, simply add a new object + * with name equal to the lower-cased http method name, e.g. + * `$httpProvider.defaults.headers.get['My-Header']='value'`. + * + * + * # Transforming Requests and Responses + * + * Both requests and responses can be transformed using transform functions. By default, Angular + * applies these transformations: + * + * Request transformations: + * + * - if the `data` property of the request config object contains an object, serialize it into + * JSON format. + * + * Response transformations: + * + * - if XSRF prefix is detected, strip it (see Security Considerations section below) + * - if json response is detected, deserialize it using a JSON parser + * + * To override these transformation locally, specify transform functions as `transformRequest` + * and/or `transformResponse` properties of the config object. To globally override the default + * transforms, override the `$httpProvider.defaults.transformRequest` and + * `$httpProvider.defaults.transformResponse` properties of the `$httpProvider`. + * + * + * # Caching + * + * To enable caching set the configuration property `cache` to `true`. When the cache is + * enabled, `$http` stores the response from the server in local cache. Next time the + * response is served from the cache without sending a request to the server. + * + * Note that even if the response is served from cache, delivery of the data is asynchronous in + * the same way that real requests are. + * + * If there are multiple GET requests for the same url that should be cached using the same + * cache, but the cache is not populated yet, only one request to the server will be made and + * the remaining requests will be fulfilled using the response for the first request. + * + * + * # Response interceptors + * + * Before you start creating interceptors, be sure to understand the + * {@link angular.module.ng.$q $q and deferred/promise APIs}. + * + * For purposes of global error handling, authentication or any kind of synchronous or + * asynchronous preprocessing of received responses, it is desirable to be able to intercept + * responses for http requests before they are handed over to the application code that + * initiated these requests. The response interceptors leverage the {@link angular.module.ng.$q + * promise apis} to fulfil this need for both synchronous and asynchronous preprocessing. + * + * The interceptors are service factories that are registered with the $httpProvider by + * adding them to the `$httpProvider.responseInterceptors` array. The factory is called and + * injected with dependencies (if specified) and returns the interceptor — a function that + * takes a {@link angular.module.ng.$q promise} and returns the original or a new promise. + * + * <pre> + * // register the interceptor as a service + * $provide.factory('myHttpInterceptor', function($q, dependency1, dependency2) { + * return function(promise) { + * return promise.then(function(response) { + * // do something on success + * }, function(response) { + * // do something on error + * if (canRecover(response)) { + * return responseOrNewPromise + * } + * return $q.reject(response); + * }); + * } + * }); + * + * $httpProvider.responseInterceptors.push('myHttpInterceptor'); + * + * + * // register the interceptor via an anonymous factory + * $httpProvider.responseInterceptors.push(function($q, dependency1, dependency2) { + * return function(promise) { + * // same as above + * } + * }); + * </pre> + * + * + * # Security Considerations + * + * When designing web applications, consider security threats from: + * + * - {@link http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx + * JSON Vulnerability} + * - {@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 $http 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 {object} config Object describing the request to be made and how it should be + * processed. The object has following properties: + * + * - **method** – `{string}` – HTTP method (e.g. 'GET', 'POST', etc) + * - **url** – `{string}` – Absolute or relative URL of the resource that is being requested. + * - **params** – `{Object.<string|Object>}` – Map of strings or objects which will be turned to + * `?key1=value1&key2=value2` after the url. If the value is not a string, it will be JSONified. + * - **data** – `{string|Object}` – Data to be sent as the request message data. + * - **headers** – `{Object}` – Map of strings representing HTTP headers to send to the server. + * - **transformRequest** – `{function(data, headersGetter)|Array.<function(data, headersGetter)>}` – + * transform function or an array of such functions. The transform function takes the http + * request body and headers and returns its transformed (typically serialized) version. + * - **transformResponse** – `{function(data, headersGetter)|Array.<function(data, headersGetter)>}` – + * transform function or an array of such functions. The transform function takes the http + * response body and headers and returns its transformed (typically deserialized) version. + * - **cache** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the + * GET request, otherwise if a cache instance built with + * {@link angular.module.ng.$cacheFactory $cacheFactory}, this cache will be used for + * caching. + * - **timeout** – `{number}` – timeout in milliseconds. + * + * @returns {HttpPromise} Returns a {@link angular.module.ng.$q promise} object with the + * standard `then` method and two http specific methods: `success` and `error`. The `then` + * method takes two arguments a success and an error callback which will be called with a + * response object. The `success` and `error` methods take a single argument - a function that + * will be called when the request succeeds or fails respectively. The arguments passed into + * these functions are destructured representation of the response object passed into the + * `then` method. The response object has these properties: + * + * - **data** – `{string|Object}` – The response body transformed with the transform functions. + * - **status** – `{number}` – HTTP status code of the response. + * - **headers** – `{function([headerName])}` – Header getter function. + * - **config** – `{Object}` – The configuration object that was used to generate the request. + * + * @property {Array.<Object>} pendingRequests Array of config objects for currently pending + * requests. This is primarily meant to be used for debugging purposes. + * + * + * @example + <doc:example> + <doc:source jsfiddle="false"> + <script> + function FetchCtrl($scope, $http) { + $scope.method = 'GET'; + $scope.url = 'examples/http-hello.html'; + + $scope.fetch = function() { + $scope.code = null; + $scope.response = null; + + $http({method: $scope.method, url: $scope.url}). + success(function(data, status) { + $scope.status = status; + $scope.data = data; + }). + error(function(data, status) { + $scope.data = data || "Request failed"; + $scope.status = status; + }); + }; + + $scope.updateModel = function(method, url) { + $scope.method = method; + $scope.url = url; + }; + } + </script> + <div ng-controller="FetchCtrl"> + <select ng-model="method"> + <option>GET</option> + <option>JSONP</option> + </select> + <input type="text" ng-model="url" size="80"/> + <button ng-click="fetch()">fetch</button><br> + <button ng-click="updateModel('GET', 'examples/http-hello.html')">Sample GET</button> + <button ng-click="updateModel('JSONP', 'http://angularjs.org/greet.php?callback=JSON_CALLBACK&name=Super%20Hero')">Sample JSONP</button> + <button ng-click="updateModel('JSONP', 'http://angularjs.org/doesntexist&callback=JSON_CALLBACK')">Invalid JSONP</button> + <pre>http status code: {{status}}</pre> + <pre>http response data: {{data}}</pre> + </div> + </doc:source> + <doc:scenario> + it('should make an xhr GET request', function() { + element(':button:contains("Sample GET")').click(); + element(':button:contains("fetch")').click(); + expect(binding('status')).toBe('200'); + expect(binding('data')).toBe('Hello, $http!\n'); + }); + + it('should make a JSONP request to angularjs.org', function() { + element(':button:contains("Sample JSONP")').click(); + element(':button:contains("fetch")').click(); + expect(binding('status')).toBe('200'); + expect(binding('data')).toMatch(/Super Hero!/); + }); + + it('should make JSONP request to invalid URL and invoke the error handler', + function() { + element(':button:contains("Invalid JSONP")').click(); + element(':button:contains("fetch")').click(); + expect(binding('status')).toBe('0'); + expect(binding('data')).toBe('Request failed'); + }); + </doc:scenario> + </doc:example> + */ + function $http(config) { + config.method = uppercase(config.method); + + var reqTransformFn = config.transformRequest || $config.transformRequest, + respTransformFn = config.transformResponse || $config.transformResponse, + defHeaders = $config.headers, + reqHeaders = extend({'X-XSRF-TOKEN': $browser.cookies()['XSRF-TOKEN']}, + defHeaders.common, defHeaders[lowercase(config.method)], config.headers), + reqData = transformData(config.data, headersGetter(reqHeaders), reqTransformFn), + promise; + + // strip content-type if data is undefined + if (isUndefined(config.data)) { + delete reqHeaders['Content-Type']; + } + + // send request + promise = sendReq(config, reqData, reqHeaders); + + + // transform future response + promise = promise.then(transformResponse, transformResponse); + + // apply interceptors + forEach(responseInterceptors, function(interceptor) { + promise = interceptor(promise); + }); + + promise.success = function(fn) { + promise.then(function(response) { + fn(response.data, response.status, response.headers, config); + }); + return promise; + }; + + promise.error = function(fn) { + promise.then(null, function(response) { + fn(response.data, response.status, response.headers, config); + }); + return promise; + }; + + return promise; + + function transformResponse(response) { + // make a copy since the response must be cacheable + var resp = extend({}, response, { + data: transformData(response.data, response.headers, respTransformFn) + }); + return (isSuccess(response.status)) + ? resp + : $q.reject(resp); + } + } + + $http.pendingRequests = []; + + /** + * @ngdoc method + * @name angular.module.ng.$http#get + * @methodOf angular.module.ng.$http + * + * @description + * Shortcut method to perform `GET` request + * + * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {Object=} config Optional configuration object + * @returns {HttpPromise} Future object + */ + + /** + * @ngdoc method + * @name angular.module.ng.$http#delete + * @methodOf angular.module.ng.$http + * + * @description + * Shortcut method to perform `DELETE` request + * + * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {Object=} config Optional configuration object + * @returns {HttpPromise} Future object + */ + + /** + * @ngdoc method + * @name angular.module.ng.$http#head + * @methodOf angular.module.ng.$http + * + * @description + * Shortcut method to perform `HEAD` request + * + * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {Object=} config Optional configuration object + * @returns {XhrFuture} Future object + */ + + /** + * @ngdoc method + * @name angular.module.ng.$http#jsonp + * @methodOf angular.module.ng.$http + * + * @description + * Shortcut method to perform `JSONP` request + * + * @param {string} url Relative or absolute URL specifying the destination of the request. + * Should contain `JSON_CALLBACK` string. + * @param {Object=} config Optional configuration object + * @returns {XhrFuture} Future object + */ + createShortMethods('get', 'delete', 'head', 'jsonp'); + + /** + * @ngdoc method + * @name angular.module.ng.$http#post + * @methodOf angular.module.ng.$http + * + * @description + * Shortcut method to perform `POST` request + * + * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {*} data Request content + * @param {Object=} config Optional configuration object + * @returns {HttpPromise} Future object + */ + + /** + * @ngdoc method + * @name angular.module.ng.$http#put + * @methodOf angular.module.ng.$http + * + * @description + * Shortcut method to perform `PUT` request + * + * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {*} data Request content + * @param {Object=} config Optional configuration object + * @returns {XhrFuture} Future object + */ + createShortMethodsWithData('post', 'put'); + + + return $http; + + + function createShortMethods(names) { + forEach(arguments, function(name) { + $http[name] = function(url, config) { + return $http(extend(config || {}, { + method: name, + url: url + })); + }; + }); + } + + + function createShortMethodsWithData(name) { + forEach(arguments, function(name) { + $http[name] = function(url, data, config) { + return $http(extend(config || {}, { + method: name, + url: url, + data: data + })); + }; + }); + } + + + /** + * Makes the request + * + * !!! ACCESSES CLOSURE VARS: + * $httpBackend, $config, $log, $rootScope, defaultCache, $http.pendingRequests + */ + function sendReq(config, reqData, reqHeaders) { + var deferred = $q.defer(), + promise = deferred.promise, + cache, + cachedResp, + url = buildUrl(config.url, config.params); + + $http.pendingRequests.push(config); + promise.then(removePendingReq, removePendingReq); + + + if (config.cache && config.method == 'GET') { + cache = isObject(config.cache) ? config.cache : defaultCache; + } + + if (cache) { + cachedResp = cache.get(url); + if (cachedResp) { + if (cachedResp.then) { + // cached request has already been sent, but there is no response yet + cachedResp.then(removePendingReq, removePendingReq); + return cachedResp; + } else { + // serving from cache + if (isArray(cachedResp)) { + resolvePromise(cachedResp[1], cachedResp[0], copy(cachedResp[2])); + } else { + resolvePromise(cachedResp, 200, {}); + } + } + } else { + // put the promise for the non-transformed response into cache as a placeholder + cache.put(url, promise); + } + } + + // if we won't have the response in cache, send the request to the backend + if (!cachedResp) { + $httpBackend(config.method, url, reqData, done, reqHeaders, config.timeout); + } + + return promise; + + + /** + * Callback registered to $httpBackend(): + * - caches the response if desired + * - resolves the raw $http promise + * - calls $apply + */ + function done(status, response, headersString) { + if (cache) { + if (isSuccess(status)) { + cache.put(url, [status, response, parseHeaders(headersString)]); + } else { + // remove promise from the cache + cache.remove(url); + } + } + + resolvePromise(response, status, headersString); + $rootScope.$apply(); + } + + + /** + * Resolves the raw $http promise. + */ + function resolvePromise(response, status, headers) { + // normalize internal statuses to 0 + status = Math.max(status, 0); + + (isSuccess(status) ? deferred.resolve : deferred.reject)({ + data: response, + status: status, + headers: headersGetter(headers), + config: config + }); + } + + + function removePendingReq() { + var idx = indexOf($http.pendingRequests, config); + if (idx !== -1) $http.pendingRequests.splice(idx, 1); + } + } + + + function buildUrl(url, params) { + if (!params) return url; + var parts = []; + forEachSorted(params, function(value, key) { + if (value == null || value == undefined) return; + if (isObject(value)) { + value = toJson(value); + } + parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(value)); + }); + return url + ((url.indexOf('?') == -1) ? '?' : '&') + parts.join('&'); + } + + + }]; +} |
