summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCarlton Gibson <carlton.gibson@noumenal.es>2020-10-20 09:14:48 +0200
committerCarlton Gibson <carlton.gibson@noumenal.es>2020-10-22 11:20:39 +0200
commit0e1eb90316475a46a240297acc623ba767ebe884 (patch)
tree2c469015668751e2cdcb04ee5440e763980859ac
parent284bde3fbe07485d64289e28014a4eada68aef91 (diff)
downloaddjango-c/append-slash-opt-out.tar.gz
Fixed #32124 -- Added per-view opt-out for APPEND_SLASH behavior.c/append-slash-opt-out
-rw-r--r--django/middleware/common.py9
-rw-r--r--django/urls/base.py9
-rw-r--r--django/views/decorators/common.py14
-rw-r--r--docs/ref/middleware.txt16
-rw-r--r--docs/releases/3.2.txt7
-rw-r--r--docs/topics/http/decorators.txt15
-rw-r--r--tests/middleware/tests.py11
-rw-r--r--tests/middleware/urls.py3
-rw-r--r--tests/middleware/views.py14
9 files changed, 89 insertions, 9 deletions
diff --git a/django/middleware/common.py b/django/middleware/common.py
index e6f30f44ad..3af4759109 100644
--- a/django/middleware/common.py
+++ b/django/middleware/common.py
@@ -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):
diff --git a/django/urls/base.py b/django/urls/base.py
index 3899feeefb..6cf75d3a3f 100644
--- a/django/urls/base.py
+++ b/django/urls/base.py
@@ -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
diff --git a/django/views/decorators/common.py b/django/views/decorators/common.py
new file mode 100644
index 0000000000..34b0e5a50e
--- /dev/null
+++ b/django/views/decorators/common.py
@@ -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)
diff --git a/docs/ref/middleware.txt b/docs/ref/middleware.txt
index c52bcc5d18..0078c716c0 100644
--- a/docs/ref/middleware.txt
+++ b/docs/ref/middleware.txt
@@ -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
diff --git a/docs/releases/3.2.txt b/docs/releases/3.2.txt
index 2f0f2f5a6d..18d63786d2 100644
--- a/docs/releases/3.2.txt
+++ b/docs/releases/3.2.txt
@@ -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
~~~~~
diff --git a/docs/topics/http/decorators.txt b/docs/topics/http/decorators.txt
index fe91d0cc98..cabdd4f01a 100644
--- a/docs/topics/http/decorators.txt
+++ b/docs/topics/http/decorators.txt
@@ -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.
diff --git a/tests/middleware/tests.py b/tests/middleware/tests.py
index 4b49858cd9..c7a007b821 100644
--- a/tests/middleware/tests.py
+++ b/tests/middleware/tests.py
@@ -128,6 +128,17 @@ class CommonMiddlewareTest(SimpleTestCase):
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):
"""
URLs which require quoting should be redirected to their slash version.
diff --git a/tests/middleware/urls.py b/tests/middleware/urls.py
index 8411d87b5a..e76f4ac771 100644
--- a/tests/middleware/urls.py
+++ b/tests/middleware/urls.py
@@ -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()),
]
diff --git a/tests/middleware/views.py b/tests/middleware/views.py
index 3f8e055a53..ee36f418f2 100644
--- a/tests/middleware/views.py
+++ b/tests/middleware/views.py
@@ -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()