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();