Fixed #27262 -- Moved URL checks to resolver and pattern classes.
Thanks Sjoerd Job Postmus for the report and review.
This commit is contained in:
parent
d5c0eb7d68
commit
e7fa89fb58
|
@ -3,7 +3,7 @@ from __future__ import unicode_literals
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
|
|
||||||
from . import Error, Tags, Warning, register
|
from . import Error, Tags, register
|
||||||
|
|
||||||
|
|
||||||
@register(Tags.urls)
|
@register(Tags.urls)
|
||||||
|
@ -19,23 +19,13 @@ def check_resolver(resolver):
|
||||||
"""
|
"""
|
||||||
Recursively check the resolver.
|
Recursively check the resolver.
|
||||||
"""
|
"""
|
||||||
from django.urls import RegexURLPattern, RegexURLResolver
|
check_method = getattr(resolver, 'check', None)
|
||||||
warnings = []
|
if check_method is not None:
|
||||||
for pattern in resolver.url_patterns:
|
return check_method()
|
||||||
if isinstance(pattern, RegexURLResolver):
|
elif not hasattr(resolver, 'resolve'):
|
||||||
warnings.extend(check_include_trailing_dollar(pattern))
|
return get_warning_for_invalid_pattern(resolver)
|
||||||
# Check resolver recursively
|
else:
|
||||||
warnings.extend(check_resolver(pattern))
|
return []
|
||||||
elif isinstance(pattern, RegexURLPattern):
|
|
||||||
warnings.extend(check_pattern_name(pattern))
|
|
||||||
else:
|
|
||||||
# This is not a url() instance
|
|
||||||
warnings.extend(get_warning_for_invalid_pattern(pattern))
|
|
||||||
|
|
||||||
if not warnings:
|
|
||||||
warnings.extend(check_pattern_startswith_slash(pattern))
|
|
||||||
|
|
||||||
return warnings
|
|
||||||
|
|
||||||
|
|
||||||
def get_warning_for_invalid_pattern(pattern):
|
def get_warning_for_invalid_pattern(pattern):
|
||||||
|
@ -61,68 +51,3 @@ def get_warning_for_invalid_pattern(pattern):
|
||||||
hint=hint,
|
hint=hint,
|
||||||
id="urls.E004",
|
id="urls.E004",
|
||||||
)]
|
)]
|
||||||
|
|
||||||
|
|
||||||
def describe_pattern(pattern):
|
|
||||||
"""
|
|
||||||
Format the URL pattern for display in warning messages.
|
|
||||||
"""
|
|
||||||
description = "'{}'".format(pattern.regex.pattern)
|
|
||||||
if getattr(pattern, 'name', False):
|
|
||||||
description += " [name='{}']".format(pattern.name)
|
|
||||||
return description
|
|
||||||
|
|
||||||
|
|
||||||
def check_include_trailing_dollar(pattern):
|
|
||||||
"""
|
|
||||||
Check that include is not used with a regex ending with a dollar.
|
|
||||||
"""
|
|
||||||
regex_pattern = pattern.regex.pattern
|
|
||||||
if regex_pattern.endswith('$') and not regex_pattern.endswith(r'\$'):
|
|
||||||
warning = Warning(
|
|
||||||
"Your URL pattern {} uses include with a regex ending with a '$'. "
|
|
||||||
"Remove the dollar from the regex to avoid problems including "
|
|
||||||
"URLs.".format(describe_pattern(pattern)),
|
|
||||||
id="urls.W001",
|
|
||||||
)
|
|
||||||
return [warning]
|
|
||||||
else:
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def check_pattern_startswith_slash(pattern):
|
|
||||||
"""
|
|
||||||
Check that the pattern does not begin with a forward slash.
|
|
||||||
"""
|
|
||||||
regex_pattern = pattern.regex.pattern
|
|
||||||
if not settings.APPEND_SLASH:
|
|
||||||
# Skip check as it can be useful to start a URL pattern with a slash
|
|
||||||
# when APPEND_SLASH=False.
|
|
||||||
return []
|
|
||||||
if regex_pattern.startswith('/') or regex_pattern.startswith('^/'):
|
|
||||||
warning = Warning(
|
|
||||||
"Your URL pattern {} has a regex beginning with a '/'. Remove this "
|
|
||||||
"slash as it is unnecessary. If this pattern is targeted in an "
|
|
||||||
"include(), ensure the include() pattern has a trailing '/'.".format(
|
|
||||||
describe_pattern(pattern)
|
|
||||||
),
|
|
||||||
id="urls.W002",
|
|
||||||
)
|
|
||||||
return [warning]
|
|
||||||
else:
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def check_pattern_name(pattern):
|
|
||||||
"""
|
|
||||||
Check that the pattern name does not contain a colon.
|
|
||||||
"""
|
|
||||||
if pattern.name is not None and ":" in pattern.name:
|
|
||||||
warning = Warning(
|
|
||||||
"Your URL pattern {} has a name including a ':'. Remove the colon, to "
|
|
||||||
"avoid ambiguous namespace references.".format(describe_pattern(pattern)),
|
|
||||||
id="urls.W003",
|
|
||||||
)
|
|
||||||
return [warning]
|
|
||||||
else:
|
|
||||||
return []
|
|
||||||
|
|
|
@ -13,6 +13,8 @@ import threading
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.checks import Warning
|
||||||
|
from django.core.checks.urls import check_resolver
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.utils import lru_cache, six
|
from django.utils import lru_cache, six
|
||||||
from django.utils.datastructures import MultiValueDict
|
from django.utils.datastructures import MultiValueDict
|
||||||
|
@ -107,6 +109,37 @@ class LocaleRegexProvider(object):
|
||||||
self._regex_dict[language_code] = compiled_regex
|
self._regex_dict[language_code] = compiled_regex
|
||||||
return self._regex_dict[language_code]
|
return self._regex_dict[language_code]
|
||||||
|
|
||||||
|
def describe(self):
|
||||||
|
"""
|
||||||
|
Format the URL pattern for display in warning messages.
|
||||||
|
"""
|
||||||
|
description = "'{}'".format(self.regex.pattern)
|
||||||
|
if getattr(self, 'name', False):
|
||||||
|
description += " [name='{}']".format(self.name)
|
||||||
|
return description
|
||||||
|
|
||||||
|
def _check_pattern_startswith_slash(self):
|
||||||
|
"""
|
||||||
|
Check that the pattern does not begin with a forward slash.
|
||||||
|
"""
|
||||||
|
regex_pattern = self.regex.pattern
|
||||||
|
if not settings.APPEND_SLASH:
|
||||||
|
# Skip check as it can be useful to start a URL pattern with a slash
|
||||||
|
# when APPEND_SLASH=False.
|
||||||
|
return []
|
||||||
|
if (regex_pattern.startswith('/') or regex_pattern.startswith('^/')) and not regex_pattern.endswith('/'):
|
||||||
|
warning = Warning(
|
||||||
|
"Your URL pattern {} has a regex beginning with a '/'. Remove this "
|
||||||
|
"slash as it is unnecessary. If this pattern is targeted in an "
|
||||||
|
"include(), ensure the include() pattern has a trailing '/'.".format(
|
||||||
|
self.describe()
|
||||||
|
),
|
||||||
|
id="urls.W002",
|
||||||
|
)
|
||||||
|
return [warning]
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
class RegexURLPattern(LocaleRegexProvider):
|
class RegexURLPattern(LocaleRegexProvider):
|
||||||
def __init__(self, regex, callback, default_args=None, name=None):
|
def __init__(self, regex, callback, default_args=None, name=None):
|
||||||
|
@ -118,6 +151,26 @@ class RegexURLPattern(LocaleRegexProvider):
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return force_str('<%s %s %s>' % (self.__class__.__name__, self.name, self.regex.pattern))
|
return force_str('<%s %s %s>' % (self.__class__.__name__, self.name, self.regex.pattern))
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
warnings = self._check_pattern_name()
|
||||||
|
if not warnings:
|
||||||
|
warnings = self._check_pattern_startswith_slash()
|
||||||
|
return warnings
|
||||||
|
|
||||||
|
def _check_pattern_name(self):
|
||||||
|
"""
|
||||||
|
Check that the pattern name does not contain a colon.
|
||||||
|
"""
|
||||||
|
if self.name is not None and ":" in self.name:
|
||||||
|
warning = Warning(
|
||||||
|
"Your URL pattern {} has a name including a ':'. Remove the colon, to "
|
||||||
|
"avoid ambiguous namespace references.".format(self.describe()),
|
||||||
|
id="urls.W003",
|
||||||
|
)
|
||||||
|
return [warning]
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
def resolve(self, path):
|
def resolve(self, path):
|
||||||
match = self.regex.search(path)
|
match = self.regex.search(path)
|
||||||
if match:
|
if match:
|
||||||
|
@ -181,6 +234,30 @@ class RegexURLResolver(LocaleRegexProvider):
|
||||||
self.namespace, self.regex.pattern,
|
self.namespace, self.regex.pattern,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
warnings = self._check_include_trailing_dollar()
|
||||||
|
for pattern in self.url_patterns:
|
||||||
|
warnings.extend(check_resolver(pattern))
|
||||||
|
if not warnings:
|
||||||
|
warnings = self._check_pattern_startswith_slash()
|
||||||
|
return warnings
|
||||||
|
|
||||||
|
def _check_include_trailing_dollar(self):
|
||||||
|
"""
|
||||||
|
Check that include is not used with a regex ending with a dollar.
|
||||||
|
"""
|
||||||
|
regex_pattern = self.regex.pattern
|
||||||
|
if regex_pattern.endswith('$') and not regex_pattern.endswith(r'\$'):
|
||||||
|
warning = Warning(
|
||||||
|
"Your URL pattern {} uses include with a regex ending with a '$'. "
|
||||||
|
"Remove the dollar from the regex to avoid problems including "
|
||||||
|
"URLs.".format(self.describe()),
|
||||||
|
id="urls.W001",
|
||||||
|
)
|
||||||
|
return [warning]
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
def _populate(self):
|
def _populate(self):
|
||||||
# Short-circuit if called recursively in this thread to prevent
|
# Short-circuit if called recursively in this thread to prevent
|
||||||
# infinite recursion. Concurrent threads may call this at the same
|
# infinite recursion. Concurrent threads may call this at the same
|
||||||
|
|
Loading…
Reference in New Issue