diff --git a/.eslintignore b/.eslintignore
index 9c273ed532..6e4edbd66d 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,6 +1,7 @@
**/*.min.js
**/vendor/**/*.js
django/contrib/gis/templates/**/*.js
+django/views/templates/*.js
docs/_build/**/*.js
node_modules/**.js
tests/**/*.js
diff --git a/django/views/csrf.py b/django/views/csrf.py
index 53ca2cb823..1b34adbe4b 100644
--- a/django/views/csrf.py
+++ b/django/views/csrf.py
@@ -1,3 +1,5 @@
+from pathlib import Path
+
from django.conf import settings
from django.http import HttpResponseForbidden
from django.template import Context, Engine, TemplateDoesNotExist, loader
@@ -12,95 +14,19 @@ from django.utils.version import get_docs_version
# tags cannot be used with this inline templates as makemessages would not be
# able to discover the strings.
-CSRF_FAILURE_TEMPLATE = """
-
-
-
-
-
- 403 Forbidden
-
-
-
-
-
{{ title }} (403)
-
{{ main }}
-{% if no_referer %}
-
{{ no_referer1 }}
-
{{ no_referer2 }}
-
{{ no_referer3 }}
-{% endif %}
-{% if no_cookie %}
-
{{ no_cookie1 }}
-
{{ no_cookie2 }}
-{% endif %}
-
-{% if DEBUG %}
-
-
Help
- {% if reason %}
-
Reason given for failure:
-
- {{ reason }}
-
- {% endif %}
-
-
In general, this can occur when there is a genuine Cross Site Request Forgery, or when
- Django’s
- CSRF mechanism has not been used correctly. For POST forms, you need to
- ensure:
-
-
- - Your browser is accepting cookies.
-
- - The view function passes a
request
to the template’s render
- method.
-
- - In the template, there is a
{% templatetag openblock %} csrf_token
- {% templatetag closeblock %}
template tag inside each POST form that
- targets an internal URL.
-
- - If you are not using
CsrfViewMiddleware
, then you must use
- csrf_protect
on any views that use the csrf_token
- template tag, as well as those that accept the POST data.
-
- - The form has a valid CSRF token. After logging in in another browser
- tab or hitting the back button after a login, you may need to reload the
- page with the form, because the token is rotated after a login.
-
-
-
You’re seeing the help section of this page because you have DEBUG =
- True
in your Django settings file. Change that to False
,
- and only the initial error message will be displayed.
-
-
You can customize this page using the CSRF_FAILURE_VIEW setting.
-
-{% else %}
-
-{% endif %}
-
-
-""" # NOQA
CSRF_FAILURE_TEMPLATE_NAME = "403_csrf.html"
+def builtin_template_path(name):
+ """
+ Return a path to a builtin template.
+
+ Avoid calling this function at the module level or in a class-definition
+ because __file__ may not exist, e.g. in frozen environments.
+ """
+ return Path(__file__).parent / "templates" / name
+
+
def csrf_failure(request, reason="", template_name=CSRF_FAILURE_TEMPLATE_NAME):
"""
Default view used when request fails CSRF protection
@@ -151,8 +77,9 @@ def csrf_failure(request, reason="", template_name=CSRF_FAILURE_TEMPLATE_NAME):
t = loader.get_template(template_name)
except TemplateDoesNotExist:
if template_name == CSRF_FAILURE_TEMPLATE_NAME:
- # If the default template doesn't exist, use the string template.
- t = Engine().from_string(CSRF_FAILURE_TEMPLATE)
+ # If the default template doesn't exist, use the fallback template.
+ with builtin_template_path("csrf_403.html").open(encoding="utf-8") as fh:
+ t = Engine().from_string(fh.read())
c = Context(c)
else:
# Raise if a developer-specified template doesn't exist.
diff --git a/django/views/i18n.py b/django/views/i18n.py
index 91f797dce9..771035d8ab 100644
--- a/django/views/i18n.py
+++ b/django/views/i18n.py
@@ -1,6 +1,7 @@
import json
import os
import re
+from pathlib import Path
from django.apps import apps
from django.conf import settings
@@ -16,6 +17,16 @@ from django.views.generic import View
LANGUAGE_QUERY_PARAMETER = "language"
+def builtin_template_path(name):
+ """
+ Return a path to a builtin template.
+
+ Avoid calling this function at the module level or in a class-definition
+ because __file__ may not exist, e.g. in frozen environments.
+ """
+ return Path(__file__).parent / "templates" / name
+
+
def set_language(request):
"""
Redirect to a given URL while setting the chosen language in the session
@@ -84,112 +95,6 @@ def get_formats():
return {attr: get_format(attr) for attr in FORMAT_SETTINGS}
-js_catalog_template = r"""
-{% autoescape off %}
-'use strict';
-{
- const globals = this;
- const django = globals.django || (globals.django = {});
-
- {% if plural %}
- django.pluralidx = function(n) {
- const v = {{ plural }};
- if (typeof v === 'boolean') {
- return v ? 1 : 0;
- } else {
- return v;
- }
- };
- {% else %}
- django.pluralidx = function(count) { return (count == 1) ? 0 : 1; };
- {% endif %}
-
- /* gettext library */
-
- django.catalog = django.catalog || {};
- {% if catalog_str %}
- const newcatalog = {{ catalog_str }};
- for (const key in newcatalog) {
- django.catalog[key] = newcatalog[key];
- }
- {% endif %}
-
- if (!django.jsi18n_initialized) {
- django.gettext = function(msgid) {
- const value = django.catalog[msgid];
- if (typeof value === 'undefined') {
- return msgid;
- } else {
- return (typeof value === 'string') ? value : value[0];
- }
- };
-
- django.ngettext = function(singular, plural, count) {
- const value = django.catalog[singular];
- if (typeof value === 'undefined') {
- return (count == 1) ? singular : plural;
- } else {
- return value.constructor === Array ? value[django.pluralidx(count)] : value;
- }
- };
-
- django.gettext_noop = function(msgid) { return msgid; };
-
- django.pgettext = function(context, msgid) {
- let value = django.gettext(context + '\x04' + msgid);
- if (value.includes('\x04')) {
- value = msgid;
- }
- return value;
- };
-
- django.npgettext = function(context, singular, plural, count) {
- let value = django.ngettext(context + '\x04' + singular, context + '\x04' + plural, count);
- if (value.includes('\x04')) {
- value = django.ngettext(singular, plural, count);
- }
- return value;
- };
-
- django.interpolate = function(fmt, obj, named) {
- if (named) {
- return fmt.replace(/%\(\w+\)s/g, function(match){return String(obj[match.slice(2,-2)])});
- } else {
- return fmt.replace(/%s/g, function(match){return String(obj.shift())});
- }
- };
-
-
- /* formatting library */
-
- django.formats = {{ formats_str }};
-
- django.get_format = function(format_type) {
- const value = django.formats[format_type];
- if (typeof value === 'undefined') {
- return format_type;
- } else {
- return value;
- }
- };
-
- /* add to global namespace */
- globals.pluralidx = django.pluralidx;
- globals.gettext = django.gettext;
- globals.ngettext = django.ngettext;
- globals.gettext_noop = django.gettext_noop;
- globals.pgettext = django.pgettext;
- globals.npgettext = django.npgettext;
- globals.interpolate = django.interpolate;
- globals.get_format = django.get_format;
-
- django.jsi18n_initialized = true;
- }
-};
-{% endautoescape %}
-""" # NOQA
-
-
class JavaScriptCatalog(View):
"""
Return the selected language catalog as a JavaScript library.
@@ -308,7 +213,8 @@ class JavaScriptCatalog(View):
def indent(s):
return s.replace("\n", "\n ")
- template = Engine().from_string(js_catalog_template)
+ with builtin_template_path("i18n_catalog.js").open(encoding="utf-8") as fh:
+ template = Engine().from_string(fh.read())
context["catalog_str"] = (
indent(json.dumps(context["catalog"], sort_keys=True, indent=2))
if context["catalog"]
diff --git a/django/views/static.py b/django/views/static.py
index f75b86f970..df46c53093 100644
--- a/django/views/static.py
+++ b/django/views/static.py
@@ -14,6 +14,16 @@ from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy
+def builtin_template_path(name):
+ """
+ Return a path to a builtin template.
+
+ Avoid calling this function at the module level or in a class-definition
+ because __file__ may not exist, e.g. in frozen environments.
+ """
+ return Path(__file__).parent / "templates" / name
+
+
def serve(request, path, document_root=None, show_indexes=False):
"""
Serve static files below a given point in the directory structure.
@@ -53,29 +63,7 @@ def serve(request, path, document_root=None, show_indexes=False):
return response
-DEFAULT_DIRECTORY_INDEX_TEMPLATE = """
-{% load i18n %}
-
-
-
-
-
-
- {% blocktranslate %}Index of {{ directory }}{% endblocktranslate %}
-
-
- {% blocktranslate %}Index of {{ directory }}{% endblocktranslate %}
-
- {% if directory != "/" %}
- - ../
- {% endif %}
- {% for f in file_list %}
- - {{ f }}
- {% endfor %}
-
-
-
-"""
+# Translatable string for static directory index template title.
template_translatable = gettext_lazy("Index of %(directory)s")
@@ -88,9 +76,10 @@ def directory_index(path, fullpath):
]
)
except TemplateDoesNotExist:
- t = Engine(libraries={"i18n": "django.templatetags.i18n"}).from_string(
- DEFAULT_DIRECTORY_INDEX_TEMPLATE
- )
+ with builtin_template_path("directory_index.html").open(encoding="utf-8") as fh:
+ t = Engine(libraries={"i18n": "django.templatetags.i18n"}).from_string(
+ fh.read()
+ )
c = Context()
else:
c = {}
diff --git a/django/views/templates/csrf_403.html b/django/views/templates/csrf_403.html
new file mode 100644
index 0000000000..402a2c6cdd
--- /dev/null
+++ b/django/views/templates/csrf_403.html
@@ -0,0 +1,84 @@
+
+
+
+
+
+ 403 Forbidden
+
+
+
+
+
{{ title }} (403)
+
{{ main }}
+{% if no_referer %}
+
{{ no_referer1 }}
+
{{ no_referer2 }}
+
{{ no_referer3 }}
+{% endif %}
+{% if no_cookie %}
+
{{ no_cookie1 }}
+
{{ no_cookie2 }}
+{% endif %}
+
+{% if DEBUG %}
+
+
Help
+ {% if reason %}
+
Reason given for failure:
+
+ {{ reason }}
+
+ {% endif %}
+
+
In general, this can occur when there is a genuine Cross Site Request Forgery, or when
+ Django’s
+ CSRF mechanism has not been used correctly. For POST forms, you need to
+ ensure:
+
+
+ - Your browser is accepting cookies.
+
+ - The view function passes a
request
to the template’s render
+ method.
+
+ - In the template, there is a
{% templatetag openblock %} csrf_token
+ {% templatetag closeblock %}
template tag inside each POST form that
+ targets an internal URL.
+
+ - If you are not using
CsrfViewMiddleware
, then you must use
+ csrf_protect
on any views that use the csrf_token
+ template tag, as well as those that accept the POST data.
+
+ - The form has a valid CSRF token. After logging in in another browser
+ tab or hitting the back button after a login, you may need to reload the
+ page with the form, because the token is rotated after a login.
+
+
+
You’re seeing the help section of this page because you have DEBUG =
+ True
in your Django settings file. Change that to False
,
+ and only the initial error message will be displayed.
+
+
You can customize this page using the CSRF_FAILURE_VIEW setting.
+
+{% else %}
+
+{% endif %}
+
+
diff --git a/django/views/templates/directory_index.html b/django/views/templates/directory_index.html
new file mode 100644
index 0000000000..d67e5e0edd
--- /dev/null
+++ b/django/views/templates/directory_index.html
@@ -0,0 +1,21 @@
+{% load i18n %}
+
+
+
+
+
+
+ {% blocktranslate %}Index of {{ directory }}{% endblocktranslate %}
+
+
+ {% blocktranslate %}Index of {{ directory }}{% endblocktranslate %}
+
+ {% if directory != "/" %}
+ - ../
+ {% endif %}
+ {% for f in file_list %}
+ - {{ f }}
+ {% endfor %}
+
+
+
diff --git a/django/views/templates/i18n_catalog.js b/django/views/templates/i18n_catalog.js
new file mode 100644
index 0000000000..b1fe4a9aac
--- /dev/null
+++ b/django/views/templates/i18n_catalog.js
@@ -0,0 +1,102 @@
+{% autoescape off %}
+'use strict';
+{
+ const globals = this;
+ const django = globals.django || (globals.django = {});
+
+ {% if plural %}
+ django.pluralidx = function(n) {
+ const v = {{ plural }};
+ if (typeof v === 'boolean') {
+ return v ? 1 : 0;
+ } else {
+ return v;
+ }
+ };
+ {% else %}
+ django.pluralidx = function(count) { return (count == 1) ? 0 : 1; };
+ {% endif %}
+
+ /* gettext library */
+
+ django.catalog = django.catalog || {};
+ {% if catalog_str %}
+ const newcatalog = {{ catalog_str }};
+ for (const key in newcatalog) {
+ django.catalog[key] = newcatalog[key];
+ }
+ {% endif %}
+
+ if (!django.jsi18n_initialized) {
+ django.gettext = function(msgid) {
+ const value = django.catalog[msgid];
+ if (typeof value === 'undefined') {
+ return msgid;
+ } else {
+ return (typeof value === 'string') ? value : value[0];
+ }
+ };
+
+ django.ngettext = function(singular, plural, count) {
+ const value = django.catalog[singular];
+ if (typeof value === 'undefined') {
+ return (count == 1) ? singular : plural;
+ } else {
+ return value.constructor === Array ? value[django.pluralidx(count)] : value;
+ }
+ };
+
+ django.gettext_noop = function(msgid) { return msgid; };
+
+ django.pgettext = function(context, msgid) {
+ let value = django.gettext(context + '\x04' + msgid);
+ if (value.includes('\x04')) {
+ value = msgid;
+ }
+ return value;
+ };
+
+ django.npgettext = function(context, singular, plural, count) {
+ let value = django.ngettext(context + '\x04' + singular, context + '\x04' + plural, count);
+ if (value.includes('\x04')) {
+ value = django.ngettext(singular, plural, count);
+ }
+ return value;
+ };
+
+ django.interpolate = function(fmt, obj, named) {
+ if (named) {
+ return fmt.replace(/%\(\w+\)s/g, function(match){return String(obj[match.slice(2,-2)])});
+ } else {
+ return fmt.replace(/%s/g, function(match){return String(obj.shift())});
+ }
+ };
+
+
+ /* formatting library */
+
+ django.formats = {{ formats_str }};
+
+ django.get_format = function(format_type) {
+ const value = django.formats[format_type];
+ if (typeof value === 'undefined') {
+ return format_type;
+ } else {
+ return value;
+ }
+ };
+
+ /* add to global namespace */
+ globals.pluralidx = django.pluralidx;
+ globals.gettext = django.gettext;
+ globals.ngettext = django.ngettext;
+ globals.gettext_noop = django.gettext_noop;
+ globals.pgettext = django.pgettext;
+ globals.npgettext = django.npgettext;
+ globals.interpolate = django.interpolate;
+ globals.get_format = django.get_format;
+
+ django.jsi18n_initialized = true;
+ }
+};
+{% endautoescape %}
diff --git a/tests/view_tests/tests/test_csrf.py b/tests/view_tests/tests/test_csrf.py
index 68198672ed..ef4a50dd45 100644
--- a/tests/view_tests/tests/test_csrf.py
+++ b/tests/view_tests/tests/test_csrf.py
@@ -1,3 +1,5 @@
+from unittest import mock
+
from django.template import TemplateDoesNotExist
from django.test import Client, RequestFactory, SimpleTestCase, override_settings
from django.utils.translation import override
@@ -117,3 +119,15 @@ class CsrfViewTests(SimpleTestCase):
request = factory.post("/")
with self.assertRaises(TemplateDoesNotExist):
csrf_failure(request, template_name="nonexistent.html")
+
+ def test_template_encoding(self):
+ """
+ The template is loaded directly, not via a template loader, and should
+ be opened as utf-8 charset as is the default specified on template
+ engines.
+ """
+ from django.views.csrf import Path
+
+ with mock.patch.object(Path, "open") as m:
+ csrf_failure(mock.MagicMock(), mock.Mock())
+ m.assert_called_once_with(encoding="utf-8")
diff --git a/tests/view_tests/tests/test_i18n.py b/tests/view_tests/tests/test_i18n.py
index e9f3e984b5..93e91bcc83 100644
--- a/tests/view_tests/tests/test_i18n.py
+++ b/tests/view_tests/tests/test_i18n.py
@@ -1,6 +1,7 @@
import gettext
import json
from os import path
+from unittest import mock
from django.conf import settings
from django.test import (
@@ -507,6 +508,20 @@ class I18NViewTests(SimpleTestCase):
with self.assertRaisesMessage(ValueError, msg):
view(request, packages="unknown_package+unknown_package2")
+ def test_template_encoding(self):
+ """
+ The template is loaded directly, not via a template loader, and should
+ be opened as utf-8 charset as is the default specified on template
+ engines.
+ """
+ from django.views.i18n import Path
+
+ view = JavaScriptCatalog.as_view()
+ request = RequestFactory().get("/")
+ with mock.patch.object(Path, "open") as m:
+ view(request)
+ m.assert_called_once_with(encoding="utf-8")
+
@override_settings(ROOT_URLCONF="view_tests.urls")
class I18nSeleniumTests(SeleniumTestCase):
diff --git a/tests/view_tests/tests/test_static.py b/tests/view_tests/tests/test_static.py
index 309b81f8fa..3fa382749b 100644
--- a/tests/view_tests/tests/test_static.py
+++ b/tests/view_tests/tests/test_static.py
@@ -1,6 +1,7 @@
import mimetypes
import unittest
from os import path
+from unittest import mock
from urllib.parse import quote
from django.conf.urls.static import static
@@ -8,7 +9,7 @@ from django.core.exceptions import ImproperlyConfigured
from django.http import FileResponse, HttpResponseNotModified
from django.test import SimpleTestCase, override_settings
from django.utils.http import http_date
-from django.views.static import was_modified_since
+from django.views.static import directory_index, was_modified_since
from .. import urls
from ..urls import media_dir
@@ -152,6 +153,18 @@ class StaticTests(SimpleTestCase):
response = self.client.get("/%s/" % self.prefix)
self.assertEqual(response.content, b"Test index")
+ def test_template_encoding(self):
+ """
+ The template is loaded directly, not via a template loader, and should
+ be opened as utf-8 charset as is the default specified on template
+ engines.
+ """
+ from django.views.static import Path
+
+ with mock.patch.object(Path, "open") as m:
+ directory_index(mock.MagicMock(), mock.MagicMock())
+ m.assert_called_once_with(encoding="utf-8")
+
class StaticHelperTest(StaticTests):
"""