From deaf461fb9191869d57fc27a7cd38fd9147db066 Mon Sep 17 00:00:00 2001 From: David Chang Date: Wed, 30 Jan 2013 21:40:24 -0800 Subject: [PATCH 1/2] Adding PATCH HTTP method and urlencode request data --- src/Angular.js | 51 +++++++++++++++++++++++++++++++++++++-------- src/ng/http.js | 39 +++++++++++++++++++++++++++++----- test/AngularSpec.js | 18 ++++++++++++---- test/ng/httpSpec.js | 49 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 139 insertions(+), 18 deletions(-) diff --git a/src/Angular.js b/src/Angular.js index 0b8f03395218..37beb5026a60 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -49,7 +49,8 @@ if ('i' !== 'I'.toLowerCase()) { function fromCharCode(code) {return String.fromCharCode(code);} -var /** holds major version number for IE or NaN for real browsers */ +var Error = window.Error, + /** holds major version number for IE or NaN for real browsers */ msie = int((/msie (\d+)/.exec(lowercase(navigator.userAgent)) || [])[1]), jqLite, // delay binding since jQuery could be loaded after us. jQuery, // delay binding @@ -146,7 +147,7 @@ function reverseParams(iteratorFn) { /** * A consistent way of creating unique IDs in angular. The ID is a sequence of alpha numeric * characters such as '012ABC'. The reason why we are not using simply a number counter is that - * the number string gets longer over time, and it can also overflow, where as the nextId + * the number string gets longer over time, and it can also overflow, where as the the nextId * will grow much slower, it is a string, and it will never overflow. * * @returns an unique alpha-numeric string @@ -619,18 +620,23 @@ function equals(o1, o2) { } else { if (isScope(o1) || isScope(o2) || isWindow(o1) || isWindow(o2)) return false; keySet = {}; + length = 0; for(key in o1) { - if (key.charAt(0) === '$' || isFunction(o1[key])) continue; - if (!equals(o1[key], o2[key])) return false; + if (key.charAt(0) === '$') continue; + + if (!isFunction(o1[key]) && !equals(o1[key], o2[key])) return false; + + length++; keySet[key] = true; } for(key in o2) { - if (!keySet[key] && - key.charAt(0) !== '$' && - o2[key] !== undefined && - !isFunction(o2[key])) return false; + if (key.charAt(0) === '$') { + continue; + } + if (!keySet[key] && !isFunction(o2[key])) return false; + length--; } - return true; + return length === 0; } } } @@ -734,6 +740,33 @@ function fromJson(json) { : json; } +function serialize(obj, prefix) { + var str = []; + for(var p in obj) { + var k = prefix ? prefix + "[" + p + "]" : p, v = obj[p]; + str.push(typeof v == "object" ? + serialize(v, k) : + encodeURIComponent(k) + "=" + encodeURIComponent(v)); + } + return str.join("&"); +} + +/** + * @ngdoc function + * @name angular.toUrlEncodedString + * @function + * + * @description + * URL encodes a string, following jQuery's param function, + * but using a pure JavaScript solution from + * http://stackoverflow.com/questions/1714786/querystring-encoding-of-a-javascript-object + * + * @param {Object} object Object to serialize into a url encoded string + * @returns {string} A url encoded string + */ +function toUrlEncodedString(object) { + return (isString(object) && object) || serialize(object); +} function toBoolean(value) { if (value && value.length !== 0) { diff --git a/src/ng/http.js b/src/ng/http.js index ed9e6712c89b..36901543fe99 100644 --- a/src/ng/http.js +++ b/src/ng/http.js @@ -142,13 +142,19 @@ function $HttpProvider() { return isObject(d) && !isFile(d) ? toJson(d) : d; }], + // transform outgoing request data by Url Encoding + transformRequestByUrlEncode: [function(d) { + return isObject(d) && !isFile(d) ? toUrlEncodedString(d) : d; + }], + // default headers headers: { common: { 'Accept': 'application/json, text/plain, */*' }, post: {'Content-Type': 'application/json;charset=utf-8'}, - put: {'Content-Type': 'application/json;charset=utf-8'} + put: {'Content-Type': 'application/json;charset=utf-8'}, + patch: {'Content-Type': 'application/json;charset=utf-8'} } }; @@ -239,6 +245,7 @@ function $HttpProvider() { * - {@link ng.$http#head $http.head} * - {@link ng.$http#post $http.post} * - {@link ng.$http#put $http.put} + * - {@link ng.$http#patch $http.patch} * - {@link ng.$http#delete $http.delete} * - {@link ng.$http#jsonp $http.jsonp} * @@ -255,9 +262,11 @@ function $HttpProvider() { * - `Content-Type: application/json` * - `$httpProvider.defaults.headers.put` (header defaults for HTTP PUT requests) * - `Content-Type: application/json` + * - `$httpProvider.defaults.headers.patch` (header defaults for HTTP PATCH 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 + * objects. To add headers for an HTTP method other than POST, PUT, or PATCH, simply add a new object * with name equal to the lower-cased http method name, e.g. * `$httpProvider.defaults.headers.get['My-Header']='value'`. * @@ -416,11 +425,13 @@ function $HttpProvider() { * {@link ng.$cacheFactory $cacheFactory}, this cache will be used for * caching. * - **timeout** – `{number}` – timeout in milliseconds. - * - **withCredentials** - `{boolean}` - whether to to set the `withCredentials` flag on the + * - **withCredentials** - `{boolean}` - whether or not to set the `withCredentials` flag on the * XHR object. See {@link https://developer.mozilla.org/en/http_access_control#section_5 * requests with credentials} for more information. * - **responseType** - `{string}` - see {@link * https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType requestType}. + * - **urlEncodeRequestData** - `{boolean}` - whether or not to URL encode request data. Default + * behavior is to JSON stringify the request data * * @returns {HttpPromise} Returns a {@link ng.$q promise} object with the * standard `then` method and two http specific methods: `success` and `error`. The `then` @@ -513,7 +524,7 @@ function $HttpProvider() { function $http(config) { config.method = uppercase(config.method); - var reqTransformFn = config.transformRequest || defaults.transformRequest, + var reqTransformFn = config.transformRequest || ((config.urlEncodeRequestData || defaults.urlEncodeRequestData) && defaults.transformRequestByUrlEncode) || defaults.transformRequest, respTransformFn = config.transformResponse || defaults.transformResponse, defHeaders = defaults.headers, xsrfToken = isSameDomain(config.url, $browser.url()) ? @@ -523,6 +534,10 @@ function $HttpProvider() { reqData = transformData(config.data, headersGetter(reqHeaders), reqTransformFn), promise; + if(config.urlEncodeRequestData || defaults.urlEncodeRequestData) { + reqHeaders['Content-Type'] = 'application/x-www-form-urlencoded;charset=utf-8'; + } + // strip content-type if data is undefined if (isUndefined(config.data)) { delete reqHeaders['Content-Type']; @@ -654,7 +669,21 @@ function $HttpProvider() { * @param {Object=} config Optional configuration object * @returns {HttpPromise} Future object */ - createShortMethodsWithData('post', 'put'); + + /** + * @ngdoc method + * @name ng.$http#patch + * @methodOf ng.$http + * + * @description + * Shortcut method to perform `PATCH` 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 + */ + createShortMethodsWithData('post', 'put', 'patch'); /** * @ngdoc property diff --git a/test/AngularSpec.js b/test/AngularSpec.js index 09bc902fec73..a297e0e0e519 100644 --- a/test/AngularSpec.js +++ b/test/AngularSpec.js @@ -74,7 +74,7 @@ describe('angular', function() { it('should throw an exception when source and destination are equivalent', function() { var src, dst; - src = dst = {key: 'value'}; + src = dst = {key: 'value'}; expect(function() { copy(src, dst); }).toThrow("Can't copy equivalent objects or arrays"); src = dst = [2, 4]; expect(function() { copy(src, dst); }).toThrow("Can't copy equivalent objects or arrays"); @@ -126,12 +126,12 @@ describe('angular', function() { expect(equals(['misko'], ['misko', 'adam'])).toEqual(false); }); - it('should ignore undefined member variables during comparison', function() { + it('should ignore undefined member variables', function() { var obj1 = {name: 'misko'}, obj2 = {name: 'misko', undefinedvar: undefined}; - expect(equals(obj1, obj2)).toBe(true); - expect(equals(obj2, obj1)).toBe(true); + expect(equals(obj1, obj2)).toBe(false); + expect(equals(obj2, obj1)).toBe(false); }); it('should ignore $ member variables', function() { @@ -640,4 +640,14 @@ describe('angular', function() { expect(toJson({key: $rootScope})).toEqual('{"key":"$SCOPE"}'); })); }); + + describe('toUrlEncodedString', function() { + + it('should encode objects properly', function() { + expect(toUrlEncodedString({ })).toEqual(''); + expect(toUrlEncodedString({ one: "one", two: 2 })).toEqual('one=one&two=2'); + expect(toUrlEncodedString({ a:1, b:{ c:3, d:2 } })).toEqual('a=1&b%5Bc%5D=3&b%5Bd%5D=2'); + expect(toUrlEncodedString({ a:1, b:{ c:3, d:[1,2,3] } })).toEqual('a=1&b%5Bc%5D=3&b%5Bd%5D%5B0%5D=1&b%5Bd%5D%5B1%5D=2&b%5Bd%5D%5B2%5D=3'); + }); + }); }); diff --git a/test/ng/httpSpec.js b/test/ng/httpSpec.js index 1473ab1ccc3f..e42cc208e675 100644 --- a/test/ng/httpSpec.js +++ b/test/ng/httpSpec.js @@ -406,6 +406,15 @@ describe('$http', function() { $httpBackend.flush(); }); + it('should set default headers for PATCH request', function() { + $httpBackend.expect('PATCH', '/url', 'messageBody', function(headers) { + return headers['Accept'] == 'application/json, text/plain, */*' && + headers['Content-Type'] == 'application/json;charset=utf-8'; + }).respond(''); + + $http({url: '/url', method: 'PATCH', headers: {}, data: 'messageBody'}); + $httpBackend.flush(); + }); it('should set default headers for custom HTTP method', function() { $httpBackend.expect('FOO', '/url', undefined, function(headers) { @@ -430,6 +439,33 @@ describe('$http', function() { $httpBackend.flush(); }); + it('should change content-type header to urlencoded if specified in config', function() { + $httpBackend.expect('POST', '/url', 'messageBody', function(headers) { + return headers['Content-Type'] == 'application/x-www-form-urlencoded;charset=utf-8'; + }).respond(''); + + $http({url: '/url', method: 'POST', data: 'messageBody', urlEncodeRequestData: true }); + $httpBackend.flush(); + }); + + it('should automatically JSON encode request data if not specified in config', function() { + $httpBackend.expect('POST', '/url', '{"one":"one","two":2}', function(headers) { + return headers['Content-Type'] == 'application/json;charset=utf-8'; + }).respond(''); + + $http({url: '/url', method: 'POST', data: { one: 'one', two: 2 } }); + $httpBackend.flush(); + }); + + it('should URL encode request data if specified in config', function() { + $httpBackend.expect('POST', '/url', 'one=one&two=2', function(headers) { + return headers['Content-Type'] == 'application/x-www-form-urlencoded;charset=utf-8'; + }).respond(''); + + $http({url: '/url', method: 'POST', data: { one: 'one', two: 2 }, urlEncodeRequestData: true }); + $httpBackend.flush(); + }); + it('should not set XSRF cookie for cross-domain requests', inject(function($browser) { $browser.cookies('XSRF-TOKEN', 'secret'); $browser.url('http://host.com/base'); @@ -464,11 +500,13 @@ describe('$http', function() { $httpBackend.expect('POST', '/url', undefined, checkXSRF('secret')).respond(''); $httpBackend.expect('PUT', '/url', undefined, checkXSRF('secret')).respond(''); $httpBackend.expect('DELETE', '/url', undefined, checkXSRF('secret')).respond(''); + $httpBackend.expect('PATCH', '/url', undefined, checkXSRF('secret')).respond(''); $http({url: '/url', method: 'GET'}); $http({url: '/url', method: 'POST', headers: {'S-ome': 'Header'}}); $http({url: '/url', method: 'PUT', headers: {'Another': 'Header'}}); $http({url: '/url', method: 'DELETE', headers: {}}); + $http({url: '/url', method: 'PATCH'}); $httpBackend.flush(); })); @@ -553,6 +591,17 @@ describe('$http', function() { $httpBackend.expect('JSONP', '/url', undefined, checkHeader('Custom', 'Header')).respond(''); $http.jsonp('/url', {headers: {'Custom': 'Header'}}); }); + + it('should have patch()', function() { + $httpBackend.expect('PATCH', '/url').respond(''); + $http.patch('/url'); + }); + + + it('patch() should allow config param', function() { + $httpBackend.expect('PATCH', '/url', 'some-data', checkHeader('Custom', 'Header')).respond(''); + $http.patch('/url', 'some-data', {headers: {'Custom': 'Header'}}); + }); }); From d28d7ca6d32e3d0d5a778bdd118e2eb816a16332 Mon Sep 17 00:00:00 2001 From: David Chang Date: Wed, 30 Jan 2013 21:48:51 -0800 Subject: [PATCH 2/2] undoing my accidental changes to Angular.js and its test --- src/Angular.js | 24 +++++++++--------------- test/AngularSpec.js | 8 ++++---- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/src/Angular.js b/src/Angular.js index 37beb5026a60..5221638b3444 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -49,8 +49,7 @@ if ('i' !== 'I'.toLowerCase()) { function fromCharCode(code) {return String.fromCharCode(code);} -var Error = window.Error, - /** holds major version number for IE or NaN for real browsers */ +var /** holds major version number for IE or NaN for real browsers */ msie = int((/msie (\d+)/.exec(lowercase(navigator.userAgent)) || [])[1]), jqLite, // delay binding since jQuery could be loaded after us. jQuery, // delay binding @@ -147,7 +146,7 @@ function reverseParams(iteratorFn) { /** * A consistent way of creating unique IDs in angular. The ID is a sequence of alpha numeric * characters such as '012ABC'. The reason why we are not using simply a number counter is that - * the number string gets longer over time, and it can also overflow, where as the the nextId + * the number string gets longer over time, and it can also overflow, where as the nextId * will grow much slower, it is a string, and it will never overflow. * * @returns an unique alpha-numeric string @@ -620,23 +619,18 @@ function equals(o1, o2) { } else { if (isScope(o1) || isScope(o2) || isWindow(o1) || isWindow(o2)) return false; keySet = {}; - length = 0; for(key in o1) { - if (key.charAt(0) === '$') continue; - - if (!isFunction(o1[key]) && !equals(o1[key], o2[key])) return false; - - length++; + if (key.charAt(0) === '$' || isFunction(o1[key])) continue; + if (!equals(o1[key], o2[key])) return false; keySet[key] = true; } for(key in o2) { - if (key.charAt(0) === '$') { - continue; - } - if (!keySet[key] && !isFunction(o2[key])) return false; - length--; + if (!keySet[key] && + key.charAt(0) !== '$' && + o2[key] !== undefined && + !isFunction(o2[key])) return false; } - return length === 0; + return true; } } } diff --git a/test/AngularSpec.js b/test/AngularSpec.js index a297e0e0e519..fec7dd060611 100644 --- a/test/AngularSpec.js +++ b/test/AngularSpec.js @@ -74,7 +74,7 @@ describe('angular', function() { it('should throw an exception when source and destination are equivalent', function() { var src, dst; - src = dst = {key: 'value'}; + src = dst = {key: 'value'}; expect(function() { copy(src, dst); }).toThrow("Can't copy equivalent objects or arrays"); src = dst = [2, 4]; expect(function() { copy(src, dst); }).toThrow("Can't copy equivalent objects or arrays"); @@ -126,12 +126,12 @@ describe('angular', function() { expect(equals(['misko'], ['misko', 'adam'])).toEqual(false); }); - it('should ignore undefined member variables', function() { + it('should ignore undefined member variables during comparison', function() { var obj1 = {name: 'misko'}, obj2 = {name: 'misko', undefinedvar: undefined}; - expect(equals(obj1, obj2)).toBe(false); - expect(equals(obj2, obj1)).toBe(false); + expect(equals(obj1, obj2)).toBe(true); + expect(equals(obj2, obj1)).toBe(true); }); it('should ignore $ member variables', function() {