Commit 224ca5dd authored by Nick Stenning's avatar Nick Stenning

Encode semicolons in query parameters

Angular 1.4.x introduced a breaking change (undocumented in the
upgrading guide as far as I can tell) to the way URL query parameters
are handled in ajax requests.

Specifically, semicolons in query parameter values are no longer encoded
by default. This causes problems for us in request urls such as

    /api/search?uri=http:%2F%2Fexample.com/?id=4;display=print

because Pyramid interprets the semicolon (correctly according to
RFC3986) as a query string delimiter.

This commit fixes the issue by overriding the default parameter
serializer (although only for the ngResource objects in the store
service) with a much more conservative one that encodes everything with
`encodeURIComponent`.

The bulk of the code here is a slightly modified version of the default
serializer used by Angular.
parent 1f2d0813
...@@ -24,6 +24,56 @@ function stripInternalProperties(obj) { ...@@ -24,6 +24,56 @@ function stripInternalProperties(obj) {
return result; return result;
} }
function forEachSorted(obj, iterator, context) {
var keys = Object.keys(obj).sort();
for (var i = 0; i < keys.length; i++) {
iterator.call(context, obj[keys[i]], keys[i]);
}
return keys;
}
function serializeValue(v) {
if (angular.isObject(v)) {
return angular.isDate(v) ? v.toISOString() : angular.toJson(v);
}
return v;
}
function encodeUriQuery(val) {
return encodeURIComponent(val).replace(/%20/g, '+');
}
// Serialize an object containing parameters into a form suitable for a query
// string.
//
// This is an almost identical copy of the default Angular parameter serializer
// ($httpParamSerializer), with one important change. In Angular 1.4.x
// semicolons are not encoded in query parameter values. This is a problem for
// us as URIs around the web may well contain semicolons, which our backend will
// then proceed to parse as a delimiter in the query string. To avoid this
// problem we use a very conservative encoder, found above.
function serializeParams(params) {
if (!params) return '';
var parts = [];
forEachSorted(params, function(value, key) {
if (value === null || typeof value === 'undefined') return;
if (angular.isArray(value)) {
angular.forEach(value, function(v, k) {
parts.push(encodeUriQuery(key) + '=' + encodeUriQuery(serializeValue(v)));
});
} else {
parts.push(encodeUriQuery(key) + '=' + encodeUriQuery(serializeValue(value)));
}
});
return parts.join('&');
}
/** /**
* @ngdoc factory * @ngdoc factory
* @name store * @name store
...@@ -41,6 +91,7 @@ function stripInternalProperties(obj) { ...@@ -41,6 +91,7 @@ function stripInternalProperties(obj) {
function store($http, $resource, settings) { function store($http, $resource, settings) {
var instance = {}; var instance = {};
var defaultOptions = { var defaultOptions = {
paramSerializer: serializeParams,
transformRequest: prependTransform( transformRequest: prependTransform(
$http.defaults.transformRequest, $http.defaults.transformRequest,
stripInternalProperties stripInternalProperties
...@@ -54,9 +105,15 @@ function store($http, $resource, settings) { ...@@ -54,9 +105,15 @@ function store($http, $resource, settings) {
.then(function (response) { .then(function (response) {
var links = response.data.links; var links = response.data.links;
instance.SearchResource = $resource(links.search.url, {}, defaultOptions); // N.B. in both cases below we explicitly override the default `get`
// action because there is no way to provide defaultOptions to the default
// action.
instance.SearchResource = $resource(links.search.url, {}, {
get: angular.extend({url: links.search.url}, defaultOptions),
});
instance.AnnotationResource = $resource(links.annotation.read.url, {}, { instance.AnnotationResource = $resource(links.annotation.read.url, {}, {
get: angular.extend(links.annotation.read, defaultOptions),
create: angular.extend(links.annotation.create, defaultOptions), create: angular.extend(links.annotation.create, defaultOptions),
update: angular.extend(links.annotation.update, defaultOptions), update: angular.extend(links.annotation.update, defaultOptions),
delete: angular.extend(links.annotation.delete, defaultOptions), delete: angular.extend(links.annotation.delete, defaultOptions),
......
...@@ -42,7 +42,7 @@ describe('store', function () { ...@@ -42,7 +42,7 @@ describe('store', function () {
update: {}, update: {},
}, },
search: { search: {
url: 'http://0.0.0.0:5000/api/search', url: 'http://example.com/api/search',
}, },
}, },
}); });
...@@ -83,4 +83,13 @@ describe('store', function () { ...@@ -83,4 +83,13 @@ describe('store', function () {
.respond(function () { return {id: 'test'}; }); .respond(function () { return {id: 'test'}; });
$httpBackend.flush(); $httpBackend.flush();
}); });
// Our backend service interprets semicolons as query param delimiters, so we
// must ensure to encode them in the query string.
it('encodes semicolons in query parameters', function () {
store.SearchResource.get({'uri': 'http://example.com/?foo=bar;baz=qux'});
$httpBackend.expectGET('http://example.com/api/search?uri=http%3A%2F%2Fexample.com%2F%3Ffoo%3Dbar%3Bbaz%3Dqux')
.respond(function () { return [200, {}, {}]; });
$httpBackend.flush();
});
}); });
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment