diff --git a/packages/@ember/routing/history-location.ts b/packages/@ember/routing/history-location.ts index 42019c63b54..786a8fab83d 100644 --- a/packages/@ember/routing/history-location.ts +++ b/packages/@ember/routing/history-location.ts @@ -1,7 +1,7 @@ import EmberObject from '@ember/object'; import { assert } from '@ember/debug'; import type { default as EmberLocation, UpdateCallback } from '@ember/routing/location'; -import { getHash } from './lib/location-utils'; +import { escapeRegExp, getHash } from './lib/location-utils'; /** @module @ember/routing/history-location @@ -134,8 +134,8 @@ export default class HistoryLocation extends EmberObject implements EmberLocatio // remove baseURL and rootURL from start of path let url = path - .replace(new RegExp(`^${baseURL}(?=/|$)`), '') - .replace(new RegExp(`^${rootURL}(?=/|$)`), '') + .replace(new RegExp(`^${escapeRegExp(baseURL)}(?=/|$)`), '') + .replace(new RegExp(`^${escapeRegExp(rootURL)}(?=/|$)`), '') .replace(/\/\//g, '/'); // remove extra slashes let search = location.search || ''; diff --git a/packages/@ember/routing/lib/location-utils.ts b/packages/@ember/routing/lib/location-utils.ts index 0fe26430886..e7e16102ebe 100644 --- a/packages/@ember/routing/lib/location-utils.ts +++ b/packages/@ember/routing/lib/location-utils.ts @@ -1,3 +1,16 @@ +/** + @private + + Escapes any regular-expression metacharacters in `str` so it can be safely + interpolated into a `RegExp` and matched as a literal. + + TODO: delete this in favor of `RegExp.escape` once our minimum supported + browsers all include it (https://caniuse.com/mdn-javascript_builtins_regexp_escape). +*/ +export function escapeRegExp(str: string): string { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + /** @private diff --git a/packages/@ember/routing/none-location.ts b/packages/@ember/routing/none-location.ts index 1e43888207f..e396d8ef335 100644 --- a/packages/@ember/routing/none-location.ts +++ b/packages/@ember/routing/none-location.ts @@ -1,6 +1,7 @@ import EmberObject from '@ember/object'; import { assert } from '@ember/debug'; import type { default as EmberLocation, UpdateCallback } from '@ember/routing/location'; +import { escapeRegExp } from './lib/location-utils'; /** @module @ember/routing/none-location @@ -63,7 +64,7 @@ export default class NoneLocation extends EmberObject implements EmberLocation { rootURL = rootURL.replace(/\/$/, ''); // remove rootURL from url - return path.replace(new RegExp(`^${rootURL}(?=/|$)`), ''); + return path.replace(new RegExp(`^${escapeRegExp(rootURL)}(?=/|$)`), ''); } /** diff --git a/packages/@ember/routing/tests/location/history_location_test.js b/packages/@ember/routing/tests/location/history_location_test.js index 984dad9c778..68505a5a3ec 100644 --- a/packages/@ember/routing/tests/location/history_location_test.js +++ b/packages/@ember/routing/tests/location/history_location_test.js @@ -319,6 +319,23 @@ moduleFor( assert.equal(location.getURL(), '/admin/profile/'); } + ['@test HistoryLocation.getURL() treats regex metacharacters in rootURL and baseURL literally']( + assert + ) { + HistoryTestLocation.reopen({ + init() { + this._super(...arguments); + set(this, 'location', mockBrowserLocation('/axc/secret')); + set(this, 'rootURL', '/a.c/'); + set(this, 'baseURL', '/a.c/'); + }, + }); + + createLocation(); + + assert.equal(location.getURL(), '/axc/secret'); + } + ['@test Existing state is preserved on init'](assert) { let existingState = { path: '/route/path', diff --git a/packages/@ember/routing/tests/location/none_location_test.js b/packages/@ember/routing/tests/location/none_location_test.js index 3257c885eb4..04f77db057b 100644 --- a/packages/@ember/routing/tests/location/none_location_test.js +++ b/packages/@ember/routing/tests/location/none_location_test.js @@ -84,5 +84,33 @@ moduleFor( assert.equal(location.getURL(), '/bars/baz'); } + + ['@test NoneLocation.getURL() treats regex metacharacters in rootURL literally'](assert) { + NoneTestLocation.reopen({ + init() { + this._super(...arguments); + set(this, 'rootURL', '/a.c/'); + set(this, 'path', '/axc/secret'); + }, + }); + + createLocation(); + + assert.equal(location.getURL(), '/axc/secret'); + } + + ['@test NoneLocation.getURL() strips the rootURL when it has an extra trailing slash'](assert) { + NoneTestLocation.reopen({ + init() { + this._super(...arguments); + set(this, 'rootURL', '/foo//'); + set(this, 'path', '/foo//bar'); + }, + }); + + createLocation(); + + assert.equal(location.getURL(), '/bar'); + } } );