Fixed #32124 -- Added per-view opt-out for APPEND_SLASH behavior.

This commit is contained in:
Carlton Gibson 2020-10-20 09:14:48 +02:00 committed by Carlton Gibson
parent 3418092238
commit ad11f5b8c9
9 changed files with 89 additions and 9 deletions

View File

@ -67,10 +67,11 @@ class CommonMiddleware(MiddlewareMixin):
"""
if settings.APPEND_SLASH and not request.path_info.endswith('/'):
urlconf = getattr(request, 'urlconf', None)
return (
not is_valid_path(request.path_info, urlconf) and
is_valid_path('%s/' % request.path_info, urlconf)
)
if not is_valid_path(request.path_info, urlconf):
match = is_valid_path('%s/' % request.path_info, urlconf)
if match:
view = match.func
return getattr(view, 'should_append_slash', True)
return False
def get_full_path_with_slash(self, request):

View File

@ -145,13 +145,12 @@ def get_urlconf(default=None):
def is_valid_path(path, urlconf=None):
"""
Return True if the given path resolves against the default URL resolver,
False otherwise. This is a convenience method to make working with "is
this a match?" cases easier, avoiding try...except blocks.
Return the ResolverMatch if the given path resolves against the default URL
resolver, False otherwise. This is a convenience method to make working
with "is this a match?" cases easier, avoiding try...except blocks.
"""
try:
resolve(path, urlconf)
return True
return resolve(path, urlconf)
except Resolver404:
return False

View File

@ -0,0 +1,14 @@
from functools import wraps
def no_append_slash(view_func):
"""
Mark a view function as excluded from CommonMiddleware's APPEND_SLASH
redirection.
"""
# view_func.should_append_slash = False would also work, but decorators are
# nicer if they don't have side effects, so return a new function.
def wrapped_view(*args, **kwargs):
return view_func(*args, **kwargs)
wrapped_view.should_append_slash = False
return wraps(view_func)(wrapped_view)

View File

@ -61,6 +61,22 @@ Adds a few conveniences for perfectionists:
indexer would treat them as separate URLs -- so it's best practice to
normalize URLs.
If necessary, individual views may be excluded from the ``APPEND_SLASH``
behavior using the :func:`~django.views.decorators.common.no_append_slash`
decorator::
from django.views.decorators.common import no_append_slash
@no_append_slash
def sensitive_fbv(request, *args, **kwargs):
"""View to be excluded from APPEND_SLASH."""
return HttpResponse()
.. versionchanged:: 3.2
Support for the :func:`~django.views.decorators.common.no_append_slash`
decorator was added.
* Sets the ``Content-Length`` header for non-streaming responses.
.. attribute:: CommonMiddleware.response_redirect_class

View File

@ -185,6 +185,13 @@ CSRF
* ...
Decorators
~~~~~~~~~~
* The new :func:`~django.views.decorators.common.no_append_slash` decorator
allows individual views to be excluded from :setting:`APPEND_SLASH` URL
normalization.
Email
~~~~~

View File

@ -121,3 +121,18 @@ client-side caching.
This decorator adds a ``Cache-Control: max-age=0, no-cache, no-store,
must-revalidate, private`` header to a response to indicate that a page
should never be cached.
.. module:: django.views.decorators.common
Common
======
.. versionadded:: 3.2
The decorators in :mod:`django.views.decorators.common` allow per-view
customization of :class:`~django.middleware.common.CommonMiddleware` behavior.
.. function:: no_append_slash()
This decorator allows individual views to be excluded from
:setting:`APPEND_SLASH` URL normalization.

View File

@ -127,6 +127,17 @@ class CommonMiddlewareTest(SimpleTestCase):
request = self.rf.get('/slash')
self.assertEqual(CommonMiddleware(get_response_404)(request).status_code, 404)
@override_settings(APPEND_SLASH=True)
def test_append_slash_opt_out(self):
"""
Views marked with @no_append_slash should be left alone.
"""
request = self.rf.get('/sensitive_fbv')
self.assertEqual(CommonMiddleware(get_response_404)(request).status_code, 404)
request = self.rf.get('/sensitive_cbv')
self.assertEqual(CommonMiddleware(get_response_404)(request).status_code, 404)
@override_settings(APPEND_SLASH=True)
def test_append_slash_quoted(self):
"""

View File

@ -8,4 +8,7 @@ urlpatterns = [
path('needsquoting#/', views.empty_view),
# Accepts paths with two leading slashes.
re_path(r'^(.+)/security/$', views.empty_view),
# Should not append slash.
path('sensitive_fbv/', views.sensitive_fbv),
path('sensitive_cbv/', views.SensitiveCBV.as_view()),
]

View File

@ -1,5 +1,19 @@
from django.http import HttpResponse
from django.utils.decorators import method_decorator
from django.views.decorators.common import no_append_slash
from django.views.generic import View
def empty_view(request, *args, **kwargs):
return HttpResponse()
@no_append_slash
def sensitive_fbv(request, *args, **kwargs):
return HttpResponse()
@method_decorator(no_append_slash, name='dispatch')
class SensitiveCBV(View):
def get(self, *args, **kwargs):
return HttpResponse()