Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Added some better error reporting and path handling when creating tem…

…plate paths.

We now raise UnicodeDecodeError for non-UTF-8 bytestrings (thanks to Daniel
Pope for diagnosing this was being swallowed by ValueError) and allow UTF-8
bytestrings as template directories.

Refs #8965.


git-svn-id: http://code.djangoproject.com/svn/django/trunk@9161 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 8fb1459b5294fb9327b241fffec8576c5aa3fc7e 1 parent fb62bcc
Malcolm Tredinnick authored October 06, 2008
8  django/template/loaders/app_directories.py
@@ -33,11 +33,19 @@
33 33
 app_template_dirs = tuple(app_template_dirs)
34 34
 
35 35
 def get_template_sources(template_name, template_dirs=None):
  36
+    """
  37
+    Returns the absolute paths to "template_name", when appended to each
  38
+    directory in "template_dirs". Any paths that don't lie inside one of the
  39
+    template dirs are excluded from the result set, for security reasons.
  40
+    """
36 41
     if not template_dirs:
37 42
         template_dirs = app_template_dirs
38 43
     for template_dir in template_dirs:
39 44
         try:
40 45
             yield safe_join(template_dir, template_name)
  46
+        except UnicodeDecodeError:
  47
+            # The template dir name was a bytestring that wasn't valid UTF-8.
  48
+            raise
41 49
         except ValueError:
42 50
             # The joined path was located outside of template_dir.
43 51
             pass
12  django/template/loaders/filesystem.py
@@ -7,13 +7,23 @@
7 7
 from django.utils._os import safe_join
8 8
 
9 9
 def get_template_sources(template_name, template_dirs=None):
  10
+    """
  11
+    Returns the absolute paths to "template_name", when appended to each
  12
+    directory in "template_dirs". Any paths that don't lie inside one of the
  13
+    template dirs are excluded from the result set, for security reasons.
  14
+    """
10 15
     if not template_dirs:
11 16
         template_dirs = settings.TEMPLATE_DIRS
12 17
     for template_dir in template_dirs:
13 18
         try:
14 19
             yield safe_join(template_dir, template_name)
  20
+        except UnicodeDecodeError:
  21
+            # The template dir name was a bytestring that wasn't valid UTF-8.
  22
+            raise
15 23
         except ValueError:
16  
-            # The joined path was located outside of template_dir.
  24
+            # The joined path was located outside of this particular
  25
+            # template_dir (it might be inside another one, so this isn't
  26
+            # fatal).
17 27
             pass
18 28
 
19 29
 def load_template_source(template_name, template_dirs=None):
3  django/utils/_os.py
... ...
@@ -1,4 +1,5 @@
1 1
 from os.path import join, normcase, abspath, sep
  2
+from django.utils.encoding import force_unicode
2 3
 
3 4
 def safe_join(base, *paths):
4 5
     """
@@ -10,6 +11,8 @@ def safe_join(base, *paths):
10 11
     """
11 12
     # We need to use normcase to ensure we don't false-negative on case
12 13
     # insensitive operating systems (like Windows).
  14
+    base = force_unicode(base)
  15
+    paths = [force_unicode(p) for p in paths]
13 16
     final_path = normcase(abspath(join(base, *paths)))
14 17
     base_path = normcase(abspath(base))
15 18
     base_path_len = len(base_path)
49  tests/regressiontests/templates/tests.py
@@ -92,34 +92,45 @@ def __str__(self):
92 92
 class Templates(unittest.TestCase):
93 93
     def test_loaders_security(self):
94 94
         def test_template_sources(path, template_dirs, expected_sources):
95  
-            # Fix expected sources so they are normcased and abspathed
96  
-            expected_sources = [os.path.normcase(os.path.abspath(s)) for s in expected_sources]
97  
-            # Test app_directories loader
98  
-            sources = app_directories.get_template_sources(path, template_dirs)
99  
-            self.assertEqual(list(sources), expected_sources)
100  
-            # Test filesystem loader
101  
-            sources = filesystem.get_template_sources(path, template_dirs)
102  
-            self.assertEqual(list(sources), expected_sources)
  95
+            if isinstance(expected_sources, list):
  96
+                # Fix expected sources so they are normcased and abspathed
  97
+                expected_sources = [os.path.normcase(os.path.abspath(s)) for s in expected_sources]
  98
+            # Test the two loaders (app_directores and filesystem).
  99
+            func1 = lambda p, t: list(app_directories.get_template_sources(p, t))
  100
+            func2 = lambda p, t: list(filesystem.get_template_sources(p, t))
  101
+            for func in (func1, func2):
  102
+                if isinstance(expected_sources, list):
  103
+                    self.assertEqual(func(path, template_dirs), expected_sources)
  104
+                else:
  105
+                    self.assertRaises(expected_sources, func, path, template_dirs)
103 106
 
104 107
         template_dirs = ['/dir1', '/dir2']
105 108
         test_template_sources('index.html', template_dirs,
106 109
                               ['/dir1/index.html', '/dir2/index.html'])
107  
-        test_template_sources('/etc/passwd', template_dirs,
108  
-                              [])
  110
+        test_template_sources('/etc/passwd', template_dirs, [])
109 111
         test_template_sources('etc/passwd', template_dirs,
110 112
                               ['/dir1/etc/passwd', '/dir2/etc/passwd'])
111  
-        test_template_sources('../etc/passwd', template_dirs,
112  
-                              [])
113  
-        test_template_sources('../../../etc/passwd', template_dirs,
114  
-                              [])
  113
+        test_template_sources('../etc/passwd', template_dirs, [])
  114
+        test_template_sources('../../../etc/passwd', template_dirs, [])
115 115
         test_template_sources('/dir1/index.html', template_dirs,
116 116
                               ['/dir1/index.html'])
117 117
         test_template_sources('../dir2/index.html', template_dirs,
118 118
                               ['/dir2/index.html'])
119  
-        test_template_sources('/dir1blah', template_dirs,
120  
-                              [])
121  
-        test_template_sources('../dir1blah', template_dirs,
122  
-                              [])
  119
+        test_template_sources('/dir1blah', template_dirs, [])
  120
+        test_template_sources('../dir1blah', template_dirs, [])
  121
+
  122
+        # UTF-8 bytestrings are permitted.
  123
+        test_template_sources('\xc3\x85ngstr\xc3\xb6m', template_dirs,
  124
+                              [u'/dir1/Ångström', u'/dir2/Ångström'])
  125
+        # Unicode strings are permitted.
  126
+        test_template_sources(u'Ångström', template_dirs,
  127
+                              [u'/dir1/Ångström', u'/dir2/Ångström'])
  128
+        test_template_sources(u'Ångström', ['/Straße'], [u'/Straße/Ångström'])
  129
+        test_template_sources('\xc3\x85ngstr\xc3\xb6m', ['/Straße'],
  130
+                              [u'/Straße/Ångström'])
  131
+        # Invalid UTF-8 encoding in bytestrings is not. Should raise a
  132
+        # semi-useful error message.
  133
+        test_template_sources('\xc3\xc3', template_dirs, UnicodeDecodeError)
123 134
 
124 135
         # Case insensitive tests (for win32). Not run unless we're on
125 136
         # a case insensitive operating system.
@@ -372,7 +383,7 @@ def get_template_tests(self):
372 383
 
373 384
             # Numbers as filter arguments should work
374 385
             'filter-syntax19': ('{{ var|truncatewords:1 }}', {"var": "hello world"}, "hello ..."),
375  
-            
  386
+
376 387
             #filters should accept empty string constants
377 388
             'filter-syntax20': ('{{ ""|default_if_none:"was none" }}', {}, ""),
378 389
 

0 notes on commit 8fb1459

Please sign in to comment.
Something went wrong with that request. Please try again.