Fixed #24013 -- Fixed escaping of reverse() prefix.

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.
This commit is contained in:
Bas Peschier 2015-03-08 11:07:55 +01:00 committed by Tim Graham
parent e4a578e70e
commit c9f1a12925
2 changed files with 24 additions and 14 deletions

View File

@ -453,16 +453,15 @@ class RegexURLResolver(LocaleRegexProvider):
) )
possibilities = self.reverse_dict.getlist(lookup_view) possibilities = self.reverse_dict.getlist(lookup_view)
prefix_norm, prefix_args = normalize(urlquote(_prefix))[0]
for possibility, pattern, defaults in possibilities: for possibility, pattern, defaults in possibilities:
for result, params in possibility: for result, params in possibility:
if args: if args:
if len(args) != len(params) + len(prefix_args): if len(args) != len(params):
continue continue
candidate_subs = dict(zip(prefix_args + params, text_args)) candidate_subs = dict(zip(params, text_args))
else: else:
if (set(kwargs.keys()) | set(defaults.keys()) != set(params) | if (set(kwargs.keys()) | set(defaults.keys()) != set(params) |
set(defaults.keys()) | set(prefix_args)): set(defaults.keys())):
continue continue
matches = True matches = True
for k, v in defaults.items(): for k, v in defaults.items():
@ -477,12 +476,10 @@ class RegexURLResolver(LocaleRegexProvider):
# without quoting to build a decoded URL and look for a match. # without quoting to build a decoded URL and look for a match.
# Then, if we have a match, redo the substitution with quoted # Then, if we have a match, redo the substitution with quoted
# arguments in order to return a properly encoded URL. # arguments in order to return a properly encoded URL.
candidate_pat = prefix_norm.replace('%', '%%') + result candidate_pat = _prefix.replace('%', '%%') + result
if re.search('^%s%s' % (prefix_norm, pattern), candidate_pat % candidate_subs, re.UNICODE): if re.search('^%s%s' % (re.escape(_prefix), pattern), candidate_pat % candidate_subs, re.UNICODE):
# safe characters from `pchar` definition of RFC 3986 # safe characters from `pchar` definition of RFC 3986
candidate_subs = dict((k, urlquote(v, safe=RFC3986_SUBDELIMS + str('/~:@'))) url = urlquote(candidate_pat % candidate_subs, safe=RFC3986_SUBDELIMS + str('/~:@'))
for (k, v) in candidate_subs.items())
url = candidate_pat % candidate_subs
# Don't allow construction of scheme relative urls. # Don't allow construction of scheme relative urls.
if url.startswith('//'): if url.startswith('//'):
url = '/%%2F%s' % url[2:] url = '/%%2F%s' % url[2:]

View File

@ -202,17 +202,24 @@ class URLPatternReverse(TestCase):
reverse('non_path_include', prefix='/{{invalid}}/')) reverse('non_path_include', prefix='/{{invalid}}/'))
def test_prefix_parenthesis(self): def test_prefix_parenthesis(self):
self.assertEqual('/bogus%29/includes/non_path_include/', # Parentheses are allowed and should not cause errors or be escaped
reverse('non_path_include', prefix='/bogus)/')) 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): def test_prefix_format_char(self):
self.assertEqual('/bump%2520map/includes/non_path_include/', self.assertEqual('/bump%2520map/includes/non_path_include/',
reverse('non_path_include', prefix='/bump%20map/')) reverse('non_path_include', prefix='/bump%20map/'))
def test_non_urlsafe_prefix_with_args(self): def test_non_urlsafe_prefix_with_args(self):
# Regression for #20022 # Regression for #20022, adjusted for #24013 because ~ is an unreserved
self.assertEqual('/%7Eme/places/1/', # character. Tests whether % is escaped.
reverse('places', args=[1], prefix='/~me/')) self.assertEqual('/%257Eme/places/1/', reverse('places', args=[1], prefix='/%7Eme/'))
def test_patterns_reported(self): def test_patterns_reported(self):
# Regression for #17076 # Regression for #17076
@ -227,6 +234,12 @@ class URLPatternReverse(TestCase):
# exception # exception
self.fail("Expected a NoReverseMatch, but none occurred.") 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): def test_reverse_returns_unicode(self):
name, expected, args, kwargs = test_data[0] name, expected, args, kwargs = test_data[0]
self.assertIsInstance( self.assertIsInstance(