Skip to content

Commit

Permalink
Fixed #24013 -- Fixed escaping of reverse() prefix.
Browse files Browse the repository at this point in the history
Prefix was treated as a part of the url pattern, which it is not.
Improved tests to conform with RFC 3986 which allows certain
characters in path segments without being escaped.
  • Loading branch information
bpeschier authored and timgraham committed Mar 12, 2015
1 parent e4a578e commit c9f1a12
Show file tree
Hide file tree
Showing 2 changed files with 24 additions and 14 deletions.
15 changes: 6 additions & 9 deletions django/core/urlresolvers.py
Expand Up @@ -453,16 +453,15 @@ def _reverse_with_prefix(self, lookup_view, _prefix, *args, **kwargs):
)
possibilities = self.reverse_dict.getlist(lookup_view)

prefix_norm, prefix_args = normalize(urlquote(_prefix))[0]
for possibility, pattern, defaults in possibilities:
for result, params in possibility:
if args:
if len(args) != len(params) + len(prefix_args):
if len(args) != len(params):
continue
candidate_subs = dict(zip(prefix_args + params, text_args))
candidate_subs = dict(zip(params, text_args))
else:
if (set(kwargs.keys()) | set(defaults.keys()) != set(params) |
set(defaults.keys()) | set(prefix_args)):
set(defaults.keys())):
continue
matches = True
for k, v in defaults.items():
Expand All @@ -477,12 +476,10 @@ def _reverse_with_prefix(self, lookup_view, _prefix, *args, **kwargs):
# without quoting to build a decoded URL and look for a match.
# Then, if we have a match, redo the substitution with quoted
# arguments in order to return a properly encoded URL.
candidate_pat = prefix_norm.replace('%', '%%') + result
if re.search('^%s%s' % (prefix_norm, pattern), candidate_pat % candidate_subs, re.UNICODE):
candidate_pat = _prefix.replace('%', '%%') + result
if re.search('^%s%s' % (re.escape(_prefix), pattern), candidate_pat % candidate_subs, re.UNICODE):
# safe characters from `pchar` definition of RFC 3986
candidate_subs = dict((k, urlquote(v, safe=RFC3986_SUBDELIMS + str('/~:@')))
for (k, v) in candidate_subs.items())
url = candidate_pat % candidate_subs
url = urlquote(candidate_pat % candidate_subs, safe=RFC3986_SUBDELIMS + str('/~:@'))
# Don't allow construction of scheme relative urls.
if url.startswith('//'):
url = '/%%2F%s' % url[2:]
Expand Down
23 changes: 18 additions & 5 deletions tests/urlpatterns_reverse/tests.py
Expand Up @@ -202,17 +202,24 @@ def test_prefix_braces(self):
reverse('non_path_include', prefix='/{{invalid}}/'))

def test_prefix_parenthesis(self):
self.assertEqual('/bogus%29/includes/non_path_include/',
reverse('non_path_include', prefix='/bogus)/'))
# Parentheses are allowed and should not cause errors or be escaped
self.assertEqual(
'/bogus)/includes/non_path_include/',
reverse('non_path_include', prefix='/bogus)/')
)
self.assertEqual(
'/(bogus)/includes/non_path_include/',
reverse('non_path_include', prefix='/(bogus)/')
)

def test_prefix_format_char(self):
self.assertEqual('/bump%2520map/includes/non_path_include/',
reverse('non_path_include', prefix='/bump%20map/'))

def test_non_urlsafe_prefix_with_args(self):
# Regression for #20022
self.assertEqual('/%7Eme/places/1/',
reverse('places', args=[1], prefix='/~me/'))
# Regression for #20022, adjusted for #24013 because ~ is an unreserved
# character. Tests whether % is escaped.
self.assertEqual('/%257Eme/places/1/', reverse('places', args=[1], prefix='/%7Eme/'))

def test_patterns_reported(self):
# Regression for #17076
Expand All @@ -227,6 +234,12 @@ def test_patterns_reported(self):
# exception
self.fail("Expected a NoReverseMatch, but none occurred.")

def test_script_name_escaping(self):
self.assertEqual(
reverse('optional', args=['foo:bar'], prefix='/script:name/'),
'/script:name/optional/foo:bar/'
)

def test_reverse_returns_unicode(self):
name, expected, args, kwargs = test_data[0]
self.assertIsInstance(
Expand Down

0 comments on commit c9f1a12

Please sign in to comment.