Browse Source

Refs #30997 -- Removed HttpRequest.is_ajax() usage.

Claude Paroz 5 years ago
parent
commit
7fa0fa45c5

+ 4 - 4
django/views/debug.py

@@ -48,12 +48,12 @@ def technical_500_response(request, exc_type, exc_value, tb, status_code=500):
     the values returned from sys.exc_info() and friends.
     """
     reporter = get_exception_reporter_class(request)(request, exc_type, exc_value, tb)
-    if request.is_ajax():
-        text = reporter.get_traceback_text()
-        return HttpResponse(text, status=status_code, content_type='text/plain; charset=utf-8')
-    else:
+    if request.accepts('text/html'):
         html = reporter.get_traceback_html()
         return HttpResponse(html, status=status_code, content_type='text/html')
+    else:
+        text = reporter.get_traceback_text()
+        return HttpResponse(text, status=status_code, content_type='text/plain; charset=utf-8')
 
 
 @functools.lru_cache()

+ 1 - 1
django/views/i18n.py

@@ -33,7 +33,7 @@ def set_language(request):
     """
     next_url = request.POST.get('next', request.GET.get('next'))
     if (
-        (next_url or not request.is_ajax()) and
+        (next_url or request.accepts('text/html')) and
         not url_has_allowed_host_and_scheme(
             url=next_url,
             allowed_hosts={request.get_host()},

+ 5 - 0
docs/releases/3.1.txt

@@ -388,6 +388,11 @@ Miscellaneous
   Django 3.1, the first request to any previously cached template fragment will
   be a cache miss.
 
+* The logic behind the decision to return a redirection fallback or a 204 HTTP
+  response from the :func:`~django.views.i18n.set_language` view is now based
+  on the ``Accept`` HTTP header instead of the ``X-Requested-With`` HTTP header
+  presence.
+
 * The compatibility imports of ``django.core.exceptions.EmptyResultSet`` in
   ``django.db.models.query``, ``django.db.models.sql``, and
   ``django.db.models.sql.datastructures`` are removed.

+ 12 - 3
docs/topics/i18n/translation.txt

@@ -1803,9 +1803,18 @@ redirect to that URL will be performed. Otherwise, Django may fall back to
 redirecting the user to the URL from the ``Referer`` header or, if it is not
 set, to ``/``, depending on the nature of the request:
 
-* For AJAX requests, the fallback will be performed only if the ``next``
-  parameter was set. Otherwise a 204 status code (No Content) will be returned.
-* For non-AJAX requests, the fallback will always be performed.
+* If the request accepts HTML content (based on its ``Accept`` HTTP header),
+  the fallback will always be performed.
+
+* If the request doesn't accept HTML, the fallback will be performed only if
+  the ``next`` parameter was set. Otherwise a 204 status code (No Content) will
+  be returned.
+
+.. versionchanged:: 3.1
+
+    In older versions, the distinction for the fallback is based on whether the
+    ``X-Requested-With`` header is set to the value ``XMLHttpRequest``. This is
+    set by the jQuery ``ajax()`` method.
 
 Here's example HTML template code:
 

+ 4 - 4
docs/topics/testing/tools.txt

@@ -159,11 +159,11 @@ Use the ``django.test.Client`` class to make requests.
 
             >>> c = Client()
             >>> c.get('/customers/details/', {'name': 'fred', 'age': 7},
-            ...       HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+            ...       HTTP_ACCEPT='application/json')
 
-        ...will send the HTTP header ``HTTP_X_REQUESTED_WITH`` to the
-        details view, which is a good way to test code paths that use the
-        :meth:`django.http.HttpRequest.is_ajax()` method.
+        ...will send the HTTP header ``HTTP_ACCEPT`` to the details view, which
+        is a good way to test code paths that use the
+        :meth:`django.http.HttpRequest.accepts()` method.
 
         .. admonition:: CGI specification
 

+ 9 - 9
tests/view_tests/tests/test_debug.py

@@ -1247,7 +1247,7 @@ class ExceptionReporterFilterTests(ExceptionReportTestMixin, LoggingCaptureMixin
         response = self.client.get(
             '/raises500/',
             HTTP_SECRET_HEADER='super_secret',
-            HTTP_X_REQUESTED_WITH='XMLHttpRequest',
+            HTTP_ACCEPT='application/json',
         )
         self.assertNotIn(b'super_secret', response.content)
 
@@ -1289,17 +1289,17 @@ class CustomExceptionReporterFilterTests(SimpleTestCase):
         )
 
 
-class AjaxResponseExceptionReporterFilter(ExceptionReportTestMixin, LoggingCaptureMixin, SimpleTestCase):
+class NonHTMLResponseExceptionReporterFilter(ExceptionReportTestMixin, LoggingCaptureMixin, SimpleTestCase):
     """
     Sensitive information can be filtered out of error reports.
 
-    Here we specifically test the plain text 500 debug-only error page served
-    when it has been detected the request was sent by JS code. We don't check
-    for (non)existence of frames vars in the traceback information section of
-    the response content because we don't include them in these error pages.
+    The plain text 500 debug-only error page is served when it has been
+    detected the request doesn't accept HTML content. Don't check for
+    (non)existence of frames vars in the traceback information section of the
+    response content because they're not included in these error pages.
     Refs #14614.
     """
-    rf = RequestFactory(HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+    rf = RequestFactory(HTTP_ACCEPT='application/json')
 
     def test_non_sensitive_request(self):
         """
@@ -1346,8 +1346,8 @@ class AjaxResponseExceptionReporterFilter(ExceptionReportTestMixin, LoggingCaptu
             self.verify_unsafe_response(custom_exception_reporter_filter_view, check_for_vars=False)
 
     @override_settings(DEBUG=True, ROOT_URLCONF='view_tests.urls')
-    def test_ajax_response_encoding(self):
-        response = self.client.get('/raises500/', HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+    def test_non_html_response_encoding(self):
+        response = self.client.get('/raises500/', HTTP_ACCEPT='application/json')
         self.assertEqual(response['Content-Type'], 'text/plain; charset=utf-8')
 
 

+ 12 - 9
tests/view_tests/tests/test_i18n.py

@@ -111,11 +111,12 @@ class SetLanguageTests(TestCase):
 
     def test_setlang_performs_redirect_for_ajax_if_explicitly_requested(self):
         """
-        The set_language view redirects to the "next" parameter for AJAX calls.
+        The set_language view redirects to the "next" parameter for requests
+        not accepting HTML response content.
         """
         lang_code = self._get_inactive_language_code()
         post_data = {'language': lang_code, 'next': '/'}
-        response = self.client.post('/i18n/setlang/', post_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+        response = self.client.post('/i18n/setlang/', post_data, HTTP_ACCEPT='application/json')
         self.assertRedirects(response, '/')
         self.assertEqual(self.client.cookies[settings.LANGUAGE_COOKIE_NAME].value, lang_code)
         with ignore_warnings(category=RemovedInDjango40Warning):
@@ -123,12 +124,12 @@ class SetLanguageTests(TestCase):
 
     def test_setlang_doesnt_perform_a_redirect_to_referer_for_ajax(self):
         """
-        The set_language view doesn't redirect to the HTTP referer header for
-        AJAX calls.
+        The set_language view doesn't redirect to the HTTP referer header if
+        the request doesn't accept HTML response content.
         """
         lang_code = self._get_inactive_language_code()
         post_data = {'language': lang_code}
-        headers = {'HTTP_REFERER': '/', 'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
+        headers = {'HTTP_REFERER': '/', 'HTTP_ACCEPT': 'application/json'}
         response = self.client.post('/i18n/setlang/', post_data, **headers)
         self.assertEqual(response.status_code, 204)
         self.assertEqual(self.client.cookies[settings.LANGUAGE_COOKIE_NAME].value, lang_code)
@@ -137,11 +138,12 @@ class SetLanguageTests(TestCase):
 
     def test_setlang_doesnt_perform_a_default_redirect_for_ajax(self):
         """
-        The set_language view returns 204 for AJAX calls by default.
+        The set_language view returns 204 by default for requests not accepting
+        HTML response content.
         """
         lang_code = self._get_inactive_language_code()
         post_data = {'language': lang_code}
-        response = self.client.post('/i18n/setlang/', post_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+        response = self.client.post('/i18n/setlang/', post_data, HTTP_ACCEPT='application/json')
         self.assertEqual(response.status_code, 204)
         self.assertEqual(self.client.cookies[settings.LANGUAGE_COOKIE_NAME].value, lang_code)
         with ignore_warnings(category=RemovedInDjango40Warning):
@@ -149,11 +151,12 @@ class SetLanguageTests(TestCase):
 
     def test_setlang_unsafe_next_for_ajax(self):
         """
-        The fallback to root URL for the set_language view works for AJAX calls.
+        The fallback to root URL for the set_language view works for requests
+        not accepting HTML response content.
         """
         lang_code = self._get_inactive_language_code()
         post_data = {'language': lang_code, 'next': '//unsafe/redirection/'}
-        response = self.client.post('/i18n/setlang/', post_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+        response = self.client.post('/i18n/setlang/', post_data, HTTP_ACCEPT='application/json')
         self.assertEqual(response.url, '/')
         self.assertEqual(self.client.cookies[settings.LANGUAGE_COOKIE_NAME].value, lang_code)