Browse Source

Fixed #34343 -- Moved built-in templates to filesystem.

Nick Pope 6 years ago
parent
commit
8eef22dfed

+ 1 - 0
.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

+ 13 - 86
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,93 +14,17 @@ 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 = """
-<!DOCTYPE html>
-<html lang="en">
-<head>
-  <meta http-equiv="content-type" content="text/html; charset=utf-8">
-  <meta name="robots" content="NONE,NOARCHIVE">
-  <title>403 Forbidden</title>
-  <style type="text/css">
-    html * { padding:0; margin:0; }
-    body * { padding:10px 20px; }
-    body * * { padding:0; }
-    body { font:small sans-serif; background:#eee; color:#000; }
-    body>div { border-bottom:1px solid #ddd; }
-    h1 { font-weight:normal; margin-bottom:.4em; }
-    h1 span { font-size:60%; color:#666; font-weight:normal; }
-    #info { background:#f6f6f6; }
-    #info ul { margin: 0.5em 4em; }
-    #info p, #summary p { padding-top:10px; }
-    #summary { background: #ffc; }
-    #explanation { background:#eee; border-bottom: 0px none; }
-  </style>
-</head>
-<body>
-<div id="summary">
-  <h1>{{ title }} <span>(403)</span></h1>
-  <p>{{ main }}</p>
-{% if no_referer %}
-  <p>{{ no_referer1 }}</p>
-  <p>{{ no_referer2 }}</p>
-  <p>{{ no_referer3 }}</p>
-{% endif %}
-{% if no_cookie %}
-  <p>{{ no_cookie1 }}</p>
-  <p>{{ no_cookie2 }}</p>
-{% endif %}
-</div>
-{% if DEBUG %}
-<div id="info">
-  <h2>Help</h2>
-    {% if reason %}
-    <p>Reason given for failure:</p>
-    <pre>
-    {{ reason }}
-    </pre>
-    {% endif %}
-
-  <p>In general, this can occur when there is a genuine Cross Site Request Forgery, or when
-  <a
-  href="https://docs.djangoproject.com/en/{{ docs_version }}/ref/csrf/">Django’s
-  CSRF mechanism</a> has not been used correctly.  For POST forms, you need to
-  ensure:</p>
-
-  <ul>
-    <li>Your browser is accepting cookies.</li>
-
-    <li>The view function passes a <code>request</code> to the template’s <a
-    href="https://docs.djangoproject.com/en/dev/topics/templates/#django.template.backends.base.Template.render"><code>render</code></a>
-    method.</li>
-
-    <li>In the template, there is a <code>{% templatetag openblock %} csrf_token
-    {% templatetag closeblock %}</code> template tag inside each POST form that
-    targets an internal URL.</li>
-
-    <li>If you are not using <code>CsrfViewMiddleware</code>, then you must use
-    <code>csrf_protect</code> on any views that use the <code>csrf_token</code>
-    template tag, as well as those that accept the POST data.</li>
+CSRF_FAILURE_TEMPLATE_NAME = "403_csrf.html"
 
-    <li>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.</li>
-  </ul>
 
-  <p>You’re seeing the help section of this page because you have <code>DEBUG =
-  True</code> in your Django settings file. Change that to <code>False</code>,
-  and only the initial error message will be displayed.  </p>
+def builtin_template_path(name):
+    """
+    Return a path to a builtin template.
 
-  <p>You can customize this page using the CSRF_FAILURE_VIEW setting.</p>
-</div>
-{% else %}
-<div id="explanation">
-  <p><small>{{ more }}</small></p>
-</div>
-{% endif %}
-</body>
-</html>
-"""  # NOQA
-CSRF_FAILURE_TEMPLATE_NAME = "403_csrf.html"
+    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):
@@ -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.

+ 13 - 107
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"]

+ 15 - 26
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 %}
-<!DOCTYPE html>
-<html lang="en">
-  <head>
-    <meta http-equiv="Content-type" content="text/html; charset=utf-8">
-    <meta http-equiv="Content-Language" content="en-us">
-    <meta name="robots" content="NONE,NOARCHIVE">
-    <title>{% blocktranslate %}Index of {{ directory }}{% endblocktranslate %}</title>
-  </head>
-  <body>
-    <h1>{% blocktranslate %}Index of {{ directory }}{% endblocktranslate %}</h1>
-    <ul>
-      {% if directory != "/" %}
-      <li><a href="../">../</a></li>
-      {% endif %}
-      {% for f in file_list %}
-      <li><a href="{{ f|urlencode }}">{{ f }}</a></li>
-      {% endfor %}
-    </ul>
-  </body>
-</html>
-"""
+# 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 = {}

+ 84 - 0
django/views/templates/csrf_403.html

@@ -0,0 +1,84 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta http-equiv="content-type" content="text/html; charset=utf-8">
+  <meta name="robots" content="NONE,NOARCHIVE">
+  <title>403 Forbidden</title>
+  <style type="text/css">
+    html * { padding:0; margin:0; }
+    body * { padding:10px 20px; }
+    body * * { padding:0; }
+    body { font:small sans-serif; background:#eee; color:#000; }
+    body>div { border-bottom:1px solid #ddd; }
+    h1 { font-weight:normal; margin-bottom:.4em; }
+    h1 span { font-size:60%; color:#666; font-weight:normal; }
+    #info { background:#f6f6f6; }
+    #info ul { margin: 0.5em 4em; }
+    #info p, #summary p { padding-top:10px; }
+    #summary { background: #ffc; }
+    #explanation { background:#eee; border-bottom: 0px none; }
+  </style>
+</head>
+<body>
+<div id="summary">
+  <h1>{{ title }} <span>(403)</span></h1>
+  <p>{{ main }}</p>
+{% if no_referer %}
+  <p>{{ no_referer1 }}</p>
+  <p>{{ no_referer2 }}</p>
+  <p>{{ no_referer3 }}</p>
+{% endif %}
+{% if no_cookie %}
+  <p>{{ no_cookie1 }}</p>
+  <p>{{ no_cookie2 }}</p>
+{% endif %}
+</div>
+{% if DEBUG %}
+<div id="info">
+  <h2>Help</h2>
+    {% if reason %}
+    <p>Reason given for failure:</p>
+    <pre>
+    {{ reason }}
+    </pre>
+    {% endif %}
+
+  <p>In general, this can occur when there is a genuine Cross Site Request Forgery, or when
+  <a
+  href="https://docs.djangoproject.com/en/{{ docs_version }}/ref/csrf/">Django’s
+  CSRF mechanism</a> has not been used correctly.  For POST forms, you need to
+  ensure:</p>
+
+  <ul>
+    <li>Your browser is accepting cookies.</li>
+
+    <li>The view function passes a <code>request</code> to the template’s <a
+    href="https://docs.djangoproject.com/en/dev/topics/templates/#django.template.backends.base.Template.render"><code>render</code></a>
+    method.</li>
+
+    <li>In the template, there is a <code>{% templatetag openblock %} csrf_token
+    {% templatetag closeblock %}</code> template tag inside each POST form that
+    targets an internal URL.</li>
+
+    <li>If you are not using <code>CsrfViewMiddleware</code>, then you must use
+    <code>csrf_protect</code> on any views that use the <code>csrf_token</code>
+    template tag, as well as those that accept the POST data.</li>
+
+    <li>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.</li>
+  </ul>
+
+  <p>You’re seeing the help section of this page because you have <code>DEBUG =
+  True</code> in your Django settings file. Change that to <code>False</code>,
+  and only the initial error message will be displayed.  </p>
+
+  <p>You can customize this page using the CSRF_FAILURE_VIEW setting.</p>
+</div>
+{% else %}
+<div id="explanation">
+  <p><small>{{ more }}</small></p>
+</div>
+{% endif %}
+</body>
+</html>

+ 21 - 0
django/views/templates/directory_index.html

@@ -0,0 +1,21 @@
+{% load i18n %}
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta http-equiv="Content-type" content="text/html; charset=utf-8">
+    <meta http-equiv="Content-Language" content="en-us">
+    <meta name="robots" content="NONE,NOARCHIVE">
+    <title>{% blocktranslate %}Index of {{ directory }}{% endblocktranslate %}</title>
+  </head>
+  <body>
+    <h1>{% blocktranslate %}Index of {{ directory }}{% endblocktranslate %}</h1>
+    <ul>
+      {% if directory != "/" %}
+      <li><a href="../">../</a></li>
+      {% endif %}
+      {% for f in file_list %}
+      <li><a href="{{ f|urlencode }}">{{ f }}</a></li>
+      {% endfor %}
+    </ul>
+  </body>
+</html>

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

+ 14 - 0
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")

+ 15 - 0
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):

+ 14 - 1
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):
     """