diff --git a/docs/content/error/$http/badjsonp.ngdoc b/docs/content/error/$http/badjsonp.ngdoc new file mode 100644 index 000000000000..ba7db5806c7b --- /dev/null +++ b/docs/content/error/$http/badjsonp.ngdoc @@ -0,0 +1,18 @@ +@ngdoc error +@name $http:badjsonp +@fullName Bad JSONP Request Configuration +@description + +This error occurs when the URL generated from the configuration object contains a parameter with the same name as the configured `callbackParam` +property; or when it contains a parameter whose value is `JSON_CALLBACK`. + +`$http` JSON requests need to attach a callback query parameter to the URL. The name of this parameter is specified in the configuration +object (or in the defaults) via the `callbackParam` property. You must not provide your own parameter with this name in the configuration +of the request. + +In previous versions of Angular, you specified where to add the callback parameter value via the `JSON_CALLBACK` placeholder. This is no longer +allowed. + +To resolve this error, remove any parameters that have the same name as the `callbackParam`; and/or remove any parameters that have a value of `JSON_CALLBACK`. + +For more information, see the {@link ng.$http#jsonp `$http.jsonp()`} method API documentation. diff --git a/docs/content/guide/concepts.ngdoc b/docs/content/guide/concepts.ngdoc index 615da692d5f5..319e1d8c1d63 100644 --- a/docs/content/guide/concepts.ngdoc +++ b/docs/content/guide/concepts.ngdoc @@ -326,7 +326,7 @@ The following example shows how this is done with Angular: var YAHOO_FINANCE_URL_PATTERN = '//query.yahooapis.com/v1/public/yql?q=select * from ' + 'yahoo.finance.xchange where pair in ("PAIRS")&format=json&' + - 'env=store://datatables.org/alltableswithkeys&callback=JSON_CALLBACK'; + 'env=store://datatables.org/alltableswithkeys'; var currencies = ['USD', 'EUR', 'CNY']; var usdToForeignRates = {}; diff --git a/src/ng/http.js b/src/ng/http.js index 09673af5810f..4e4006aa137d 100644 --- a/src/ng/http.js +++ b/src/ng/http.js @@ -286,6 +286,10 @@ function $HttpProvider() { * If specified as string, it is interpreted as a function registered with the {@link auto.$injector $injector}. * Defaults to {@link ng.$httpParamSerializer $httpParamSerializer}. * + * - **`defaults.callbackParam`** - `{string} - the name of the query parameter that passes the callback in a JSONP + * request. The value of this parameter will be replaced with the expression generated by the {@link jsonpCallbacks} + * service. Defaults to `'callback'`. + * **/ var defaults = this.defaults = { // transform incoming response data @@ -309,7 +313,9 @@ function $HttpProvider() { xsrfCookieName: 'XSRF-TOKEN', xsrfHeaderName: 'X-XSRF-TOKEN', - paramSerializer: '$httpParamSerializer' + paramSerializer: '$httpParamSerializer', + + callbackParam: 'callback' }; var useApplyAsync = false; @@ -964,7 +970,8 @@ function $HttpProvider() { method: 'get', transformRequest: defaults.transformRequest, transformResponse: defaults.transformResponse, - paramSerializer: defaults.paramSerializer + paramSerializer: defaults.paramSerializer, + callbackParam: defaults.callbackParam }, requestConfig); config.headers = mergeHeaders(requestConfig); @@ -1168,6 +1175,22 @@ function $HttpProvider() { * {@link $sceDelegateProvider#resourceUrlWhitelist `$sceDelegateProvider.resourceUrlWhitelist`} or * by explicitly trusted the URL via {@link $sce#trustAsResourceUrl `$sce.trustAsResourceUrl(url)`}. * + * JSONP requests must specify a callback to be used in the response from the server. This callback + * is passed as as a query parameter in the request. You must specify the name of this parameter by + * setting the `callbackParam` property on the request config object. + * + * ``` + * $http.jsonp('some/trusted/url', {callbackParam: 'callback'}) + * ``` + * + * You can also specify a global callback parameter key in `$http.defaults.callbackParam`. + * By default this is set to `callback`. + * + *
+ * You can no longer use the `JSON_CALLBACK` string as a placeholder for specifying where the callback + * parameter value should go. + *
+ * * If you would like to customise where and how the callbacks are stored then try overriding * or decorating the {@link $jsonpCallbacks} service. * @@ -1271,9 +1294,10 @@ function $HttpProvider() { cache, cachedResp, reqHeaders = config.headers, + isJsonp = lowercase(config.method) === 'jsonp', url = config.url; - if (lowercase(config.method) === 'jsonp') { + if (isJsonp) { // JSONP is a pretty sensitive operation where we're allowing a script to have full access to // our DOM and JS space. So we require that the URL satisfies SCE.RESOURCE_URL. url = $sce.getTrustedResourceUrl(url); @@ -1284,6 +1308,11 @@ function $HttpProvider() { url = buildUrl(url, config.paramSerializer(config.params)); + if (isJsonp) { + // Check the url and add the JSONP callback placeholder + url = sanitizeJsonpCallbackParam(url, config.callbackParam); + } + $http.pendingRequests.push(config); promise.then(removePendingReq, removePendingReq); @@ -1418,5 +1447,23 @@ function $HttpProvider() { } return url; } + + function sanitizeJsonpCallbackParam(url, key) { + if (/[&?][^=]+=JSON_CALLBACK/.test(url)) { + // Throw if the url already contains a reference to JSON_CALLBACK + throw $httpMinErr('badjsonp', 'Illegal use of JSON_CALLBACK in url, "{0}"', url); + } + + var callbackParamRegex = new RegExp('([&?]' + key + '=)'); + if (callbackParamRegex.test(url)) { + // Throw if the callback param was already provided + throw $httpMinErr('badjsonp', 'Illegal use of callback param, "{0}", in url, "{1}"', key, url); + } + + // Add in the JSON_CALLBACK callback param value + url += ((url.indexOf('?') === -1) ? '?' : '&') + key + '=JSON_CALLBACK'; + + return url; + } }]; } diff --git a/test/ng/httpSpec.js b/test/ng/httpSpec.js index f846ab661b7e..bd6d9dbae966 100644 --- a/test/ng/httpSpec.js +++ b/test/ng/httpSpec.js @@ -622,7 +622,7 @@ describe('$http', function() { expect(r.headers()).toEqual(Object.create(null)); }); - $httpBackend.expect('JSONP', '/some').respond(200); + $httpBackend.expect('JSONP', '/some?callback=JSON_CALLBACK').respond(200); $http({url: $sce.trustAsResourceUrl('/some'), method: 'JSONP'}).then(callback); $httpBackend.flush(); expect(callback).toHaveBeenCalledOnce(); @@ -1030,13 +1030,13 @@ describe('$http', function() { }); it('should have jsonp()', function() { - $httpBackend.expect('JSONP', '/url').respond(''); + $httpBackend.expect('JSONP', '/url?callback=JSON_CALLBACK').respond(''); $http.jsonp($sce.trustAsResourceUrl('/url')); }); it('jsonp() should allow config param', function() { - $httpBackend.expect('JSONP', '/url', undefined, checkHeader('Custom', 'Header')).respond(''); + $httpBackend.expect('JSONP', '/url?callback=JSON_CALLBACK', undefined, checkHeader('Custom', 'Header')).respond(''); $http.jsonp($sce.trustAsResourceUrl('/url'), {headers: {'Custom': 'Header'}}); }); }); @@ -1044,25 +1044,66 @@ describe('$http', function() { describe('jsonp trust', function() { it('should throw error if the url is not a trusted resource', function() { var success, error; - $http({method: 'JSONP', url: 'http://example.org/path?cb=JSON_CALLBACK'}).catch( - function(e) { error = e; } - ); + $http({method: 'JSONP', url: 'http://example.org/path'}) + .catch(function(e) { error = e; }); $rootScope.$digest(); expect(error.message).toContain('[$sce:insecurl]'); }); it('should accept an explicitly trusted resource url', function() { - $httpBackend.expect('JSONP', 'http://example.org/path?cb=JSON_CALLBACK').respond(''); - $http({ method: 'JSONP', url: $sce.trustAsResourceUrl('http://example.org/path?cb=JSON_CALLBACK')}); + $httpBackend.expect('JSONP', 'http://example.org/path?callback=JSON_CALLBACK').respond(''); + $http({ method: 'JSONP', url: $sce.trustAsResourceUrl('http://example.org/path')}); }); it('jsonp() should accept explictly trusted urls', function() { - $httpBackend.expect('JSONP', '/url').respond(''); + $httpBackend.expect('JSONP', '/url?callback=JSON_CALLBACK').respond(''); $http({method: 'JSONP', url: $sce.trustAsResourceUrl('/url')}); - $httpBackend.expect('JSONP', '/url?a=b').respond(''); + $httpBackend.expect('JSONP', '/url?a=b&callback=JSON_CALLBACK').respond(''); $http({method: 'JSONP', url: $sce.trustAsResourceUrl('/url'), params: {a: 'b'}}); }); + + it('should error if the URL contains a JSON_CALLBACK parameter', function() { + var error; + $http({ method: 'JSONP', url: $sce.trustAsResourceUrl('http://example.org/path?callback=JSON_CALLBACK')}) + .catch(function(e) { error = e; }); + $rootScope.$digest(); + expect(error.message).toContain('[$http:badjsonp]'); + + error = undefined; + $http({ method: 'JSONP', url: $sce.trustAsResourceUrl('http://example.org/path?other=JSON_CALLBACK')}) + .catch(function(e) { error = e; }); + $rootScope.$digest(); + expect(error.message).toContain('[$http:badjsonp]'); + }); + + it('should error if a param contains a JSON_CALLBACK value', function() { + var error; + $http({ method: 'JSONP', url: $sce.trustAsResourceUrl('http://example.org/path'), params: {callback: 'JSON_CALLBACK'}}) + .catch(function(e) { error = e; }); + $rootScope.$digest(); + expect(error.message).toContain('[$http:badjsonp]'); + + error = undefined; + $http({ method: 'JSONP', url: $sce.trustAsResourceUrl('http://example.org/path'), params: {other: 'JSON_CALLBACK'}}) + .catch(function(e) { error = e; }); + $rootScope.$digest(); + expect(error.message).toContain('[$http:badjsonp]'); + }); + + it('should error if there is already a param matching the callbackParam key', function() { + var error; + $http({ method: 'JSONP', url: $sce.trustAsResourceUrl('http://example.org/path'), params: {callback: 'evilThing'}}) + .catch(function(e) { error = e; }); + $rootScope.$digest(); + expect(error.message).toContain('[$http:badjsonp]'); + + error = undefined; + $http({ method: 'JSONP', callbackParam: 'cb', url: $sce.trustAsResourceUrl('http://example.org/path'), params: {cb: 'evilThing'}}) + .catch(function(e) { error = e; }); + $rootScope.$digest(); + expect(error.message).toContain('[$http:badjsonp]'); + }); }); describe('callbacks', function() { @@ -1524,11 +1565,11 @@ describe('$http', function() { })); it('should cache JSONP request when cache is provided', inject(function($rootScope) { - $httpBackend.expect('JSONP', '/url?cb=JSON_CALLBACK').respond('content'); - $http({method: 'JSONP', url: $sce.trustAsResourceUrl('/url?cb=JSON_CALLBACK'), cache: cache}); + $httpBackend.expect('JSONP', '/url?callback=JSON_CALLBACK').respond('content'); + $http({method: 'JSONP', url: $sce.trustAsResourceUrl('/url'), cache: cache}); $httpBackend.flush(); - $http({method: 'JSONP', url: $sce.trustAsResourceUrl('/url?cb=JSON_CALLBACK'), cache: cache}).success(callback); + $http({method: 'JSONP', url: $sce.trustAsResourceUrl('/url'), cache: cache}).success(callback); $rootScope.$digest(); expect(callback).toHaveBeenCalledOnce();