From 8eef22dfed2d53df0da10c0090d9cb04f66efb15 Mon Sep 17 00:00:00 2001 From: Nick Pope Date: Sun, 7 Apr 2019 21:01:47 +0100 Subject: [PATCH] Fixed #34343 -- Moved built-in templates to filesystem. --- .eslintignore | 1 + django/views/csrf.py | 103 +++-------------- django/views/i18n.py | 120 +++----------------- django/views/static.py | 41 +++---- django/views/templates/csrf_403.html | 84 ++++++++++++++ django/views/templates/directory_index.html | 21 ++++ django/views/templates/i18n_catalog.js | 102 +++++++++++++++++ tests/view_tests/tests/test_csrf.py | 14 +++ tests/view_tests/tests/test_i18n.py | 15 +++ tests/view_tests/tests/test_static.py | 15 ++- 10 files changed, 294 insertions(+), 222 deletions(-) create mode 100644 django/views/templates/csrf_403.html create mode 100644 django/views/templates/directory_index.html create mode 100644 django/views/templates/i18n_catalog.js diff --git a/.eslintignore b/.eslintignore index 9c273ed5320..6e4edbd66d6 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 53ca2cb8236..1b34adbe4b9 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:

- - - -

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 %} -
-

{{ more }}

-
-{% 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 91f797dce90..771035d8ab2 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 f75b86f9703..df46c53093e 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 %}

- - - -""" +# 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 00000000000..402a2c6cdd4 --- /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:

+ + + +

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 %} +
+

{{ more }}

+
+{% endif %} + + diff --git a/django/views/templates/directory_index.html b/django/views/templates/directory_index.html new file mode 100644 index 00000000000..d67e5e0edd5 --- /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 %}

+ + + diff --git a/django/views/templates/i18n_catalog.js b/django/views/templates/i18n_catalog.js new file mode 100644 index 00000000000..b1fe4a9aac3 --- /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 68198672ede..ef4a50dd450 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 e9f3e984b56..93e91bcc83e 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 309b81f8fa9..3fa382749b8 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): """