Skip to content

Commit 7742a05

Browse files
authored
Fix URL encoding to mimic Symfony URL Generator (#387)
1 parent d9aecdc commit 7742a05

File tree

4 files changed

+119
-10
lines changed

4 files changed

+119
-10
lines changed

Resources/js/router.js

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ class Router {
223223

224224
route.tokens.forEach((token) => {
225225
if ('text' === token[0]) {
226-
url = token[1] + url;
226+
url = Router.encodePathComponent(token[1]) + url;
227227
optional = false;
228228

229229
return;
@@ -248,7 +248,7 @@ class Router {
248248
let empty = true === value || false === value || '' === value;
249249

250250
if (!empty || !optional) {
251-
let encodedValue = encodeURIComponent(value).replace(/%2F/g, '/');
251+
let encodedValue = Router.encodePathComponent(value);
252252

253253
if ('null' === encodedValue && null === value) {
254254
encodedValue = '';
@@ -319,19 +319,67 @@ class Router {
319319
// change null to empty string
320320
value = (value === null) ? '' : value;
321321

322-
queryParams.push(encodeURIComponent(key) + '=' + encodeURIComponent(value));
322+
queryParams.push(Router.encodeQueryComponent(key) + '=' + Router.encodeQueryComponent(value));
323323
};
324324

325325
for (prefix in unusedParams) {
326326
this.buildQueryParams(prefix, unusedParams[prefix], add);
327327
}
328328

329-
url = url + '?' + queryParams.join('&').replace(/%20/g, '+');
329+
url = url + '?' + queryParams.join('&');
330330
}
331331

332332
return url;
333333
}
334334

335+
/**
336+
* Returns the given string encoded to mimic Symfony URL generator.
337+
*
338+
* @param {string} value
339+
* @return {string}
340+
*/
341+
static customEncodeURIComponent(value) {
342+
return encodeURIComponent(value)
343+
.replace(/%2F/g, '/')
344+
.replace(/%40/g, '@')
345+
.replace(/%3A/g, ':')
346+
.replace(/%21/g, '!')
347+
.replace(/%3B/g, ';')
348+
.replace(/%2C/g, ',')
349+
.replace(/%2A/g, '*')
350+
.replace(/\(/g, '%28')
351+
.replace(/\)/g, '%29')
352+
.replace(/'/g, '%27')
353+
;
354+
}
355+
356+
/**
357+
* Returns the given path properly encoded to mimic Symfony URL generator.
358+
*
359+
* @param {string} value
360+
* @return {string}
361+
*/
362+
static encodePathComponent(value) {
363+
return Router.customEncodeURIComponent(value)
364+
.replace(/%3D/g, '=')
365+
.replace(/%2B/g, '+')
366+
.replace(/%21/g, '!')
367+
.replace(/%7C/g, '|')
368+
;
369+
}
370+
371+
/**
372+
* Returns the given query parameter or value properly encoded to mimic Symfony URL generator.
373+
*
374+
* @param {string} value
375+
* @return {string}
376+
*/
377+
static encodeQueryComponent(value) {
378+
return Router.customEncodeURIComponent(value)
379+
.replace(/%3F/g, '?')
380+
;
381+
}
382+
335383
}
336384

337385
/**

Resources/js/router.test.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ function testGenerateWithExtraParamsDeep() {
359359
}
360360
});
361361

362-
assertEquals('/baz?foo%5B%5D=1&foo%5B1%5D%5B%5D=1&foo%5B1%5D%5B%5D=2&foo%5B1%5D%5B%5D=3&foo%5B1%5D%5B%5D=foo&foo%5B%5D=3&foo%5B%5D=4&foo%5B%5D=bar&foo%5B5%5D%5B%5D=1&foo%5B5%5D%5B%5D=2&foo%5B5%5D%5B%5D=3&foo%5B5%5D%5B%5D=baz&baz%5Bfoo%5D=bar+foo&baz%5Bbar%5D=baz&bob=cat', router.generate('foo', {
362+
assertEquals('/baz?foo%5B%5D=1&foo%5B1%5D%5B%5D=1&foo%5B1%5D%5B%5D=2&foo%5B1%5D%5B%5D=3&foo%5B1%5D%5B%5D=foo&foo%5B%5D=3&foo%5B%5D=4&foo%5B%5D=bar&foo%5B5%5D%5B%5D=1&foo%5B5%5D%5B%5D=2&foo%5B5%5D%5B%5D=3&foo%5B5%5D%5B%5D=baz&baz%5Bfoo%5D=bar%20foo&baz%5Bbar%5D=baz&bob=cat', router.generate('foo', {
363363
bar: 'baz', // valid param, not included in the query string
364364
foo: [1, [1, 2, 3, 'foo'], 3, 4, 'bar', [1, 2, 3, 'baz']],
365365
baz: {
@@ -370,6 +370,28 @@ function testGenerateWithExtraParamsDeep() {
370370
}));
371371
}
372372

373+
function testUrlEncoding() {
374+
// This test was copied from Symfony URL Generator
375+
376+
// This tests the encoding of reserved characters that are used for delimiting of URI components (defined in RFC 3986)
377+
// and other special ASCII chars. These chars are tested as static text path, variable path and query param.
378+
var chars = '@:[]/()*\'" +,;-._~&$<>|{}%\\^`!?foo=bar#id';
379+
380+
var router = new fos.Router({base_url: '/app.php'}, {
381+
posts: {
382+
tokens: [['variable', '/', '.+', 'varpath'], ['text', '/'+chars]],
383+
defaults: {},
384+
requirements: {},
385+
hosttokens: []
386+
}
387+
});
388+
389+
assertEquals(
390+
'/app.php/@:%5B%5D/%28%29*%27%22%20+,;-._~%26%24%3C%3E|%7B%7D%25%5C%5E%60!%3Ffoo=bar%23id/@:%5B%5D/%28%29*%27%22%20+,;-._~%26%24%3C%3E|%7B%7D%25%5C%5E%60!%3Ffoo=bar%23id?query=@:%5B%5D/%28%29*%27%22%20%2B,;-._~%26%24%3C%3E%7C%7B%7D%25%5C%5E%60!?foo%3Dbar%23id',
391+
router.generate('posts', {varpath: chars, query: chars})
392+
);
393+
}
394+
373395
function testGenerateThrowsErrorWhenRequiredParameterWasNotGiven() {
374396
var router = new fos.Router({base_url: ''}, {
375397
foo: {

Resources/public/js/router.js

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ var Router = function () {
296296

297297
route.tokens.forEach(function (token) {
298298
if ('text' === token[0]) {
299-
url = token[1] + url;
299+
url = Router.encodePathComponent(token[1]) + url;
300300
optional = false;
301301

302302
return;
@@ -321,7 +321,7 @@ var Router = function () {
321321
var empty = true === value || false === value || '' === value;
322322

323323
if (!empty || !optional) {
324-
var encodedValue = encodeURIComponent(value).replace(/%2F/g, '/');
324+
var encodedValue = Router.encodePathComponent(value);
325325

326326
if ('null' === encodedValue && null === value) {
327327
encodedValue = '';
@@ -392,18 +392,26 @@ var Router = function () {
392392
// change null to empty string
393393
value = value === null ? '' : value;
394394

395-
queryParams.push(encodeURIComponent(key) + '=' + encodeURIComponent(value));
395+
queryParams.push(Router.encodeQueryComponent(key) + '=' + Router.encodeQueryComponent(value));
396396
};
397397

398398
for (prefix in unusedParams) {
399399
this.buildQueryParams(prefix, unusedParams[prefix], add);
400400
}
401401

402-
url = url + '?' + queryParams.join('&').replace(/%20/g, '+');
402+
url = url + '?' + queryParams.join('&');
403403
}
404404

405405
return url;
406406
}
407+
408+
/**
409+
* Returns the given string encoded to mimic Symfony URL generator.
410+
*
411+
* @param {string} value
412+
* @return {string}
413+
*/
414+
407415
}], [{
408416
key: 'getInstance',
409417
value: function getInstance() {
@@ -422,6 +430,37 @@ var Router = function () {
422430

423431
router.setRoutingData(data);
424432
}
433+
}, {
434+
key: 'customEncodeURIComponent',
435+
value: function customEncodeURIComponent(value) {
436+
return encodeURIComponent(value).replace(/%2F/g, '/').replace(/%40/g, '@').replace(/%3A/g, ':').replace(/%21/g, '!').replace(/%3B/g, ';').replace(/%2C/g, ',').replace(/%2A/g, '*').replace(/\(/g, '%28').replace(/\)/g, '%29').replace(/'/g, '%27');
437+
}
438+
439+
/**
440+
* Returns the given path properly encoded to mimic Symfony URL generator.
441+
*
442+
* @param {string} value
443+
* @return {string}
444+
*/
445+
446+
}, {
447+
key: 'encodePathComponent',
448+
value: function encodePathComponent(value) {
449+
return Router.customEncodeURIComponent(value).replace(/%3D/g, '=').replace(/%2B/g, '+').replace(/%21/g, '!').replace(/%7C/g, '|');
450+
}
451+
452+
/**
453+
* Returns the given query parameter or value properly encoded to mimic Symfony URL generator.
454+
*
455+
* @param {string} value
456+
* @return {string}
457+
*/
458+
459+
}, {
460+
key: 'encodeQueryComponent',
461+
value: function encodeQueryComponent(value) {
462+
return Router.customEncodeURIComponent(value).replace(/%3F/g, '?');
463+
}
425464
}]);
426465

427466
return Router;

Resources/public/js/router.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)