Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixes #8593 -- better handling of safe_join case sensitivity on windo…

…ws. Thanks for the initial patch, ramiro.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@16267 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit fcf7fbc68cadb7efd8bc779c0e189389d6475463 1 parent dab90dd
Chris Beaven authored May 22, 2011
15  django/utils/_os.py
@@ -30,17 +30,16 @@ def safe_join(base, *paths):
30 30
     The final path must be located inside of the base path component (otherwise
31 31
     a ValueError is raised).
32 32
     """
33  
-    # We need to use normcase to ensure we don't false-negative on case
34  
-    # insensitive operating systems (like Windows).
35 33
     base = force_unicode(base)
36 34
     paths = [force_unicode(p) for p in paths]
37  
-    final_path = normcase(abspathu(join(base, *paths)))
38  
-    base_path = normcase(abspathu(base))
  35
+    final_path = abspathu(join(base, *paths))
  36
+    base_path = abspathu(base)
39 37
     base_path_len = len(base_path)
40  
-    # Ensure final_path starts with base_path and that the next character after
41  
-    # the final path is os.sep (or nothing, in which case final_path must be
42  
-    # equal to base_path).
43  
-    if not final_path.startswith(base_path) \
  38
+    # Ensure final_path starts with base_path (using normcase to ensure we
  39
+    # don't false-negative on case insensitive operating systems like Windows)
  40
+    # and that the next character after the final path is os.sep (or nothing,
  41
+    # in which case final_path must be equal to base_path).
  42
+    if not normcase(final_path).startswith(normcase(base_path)) \
44 43
        or final_path[base_path_len:base_path_len+1] not in ('', sep):
45 44
         raise ValueError('The joined path (%s) is located outside of the base '
46 45
                          'path component (%s)' % (final_path, base_path))
21  tests/regressiontests/file_storage/tests.py
@@ -90,13 +90,16 @@ class FileStorageTests(unittest.TestCase):
90 90
     storage_class = FileSystemStorage
91 91
 
92 92
     def setUp(self):
93  
-        self.temp_dir = tempfile.mktemp()
94  
-        os.makedirs(self.temp_dir)
  93
+        self.temp_dir = tempfile.mkdtemp()
95 94
         self.storage = self.storage_class(location=self.temp_dir,
96 95
             base_url='/test_media_url/')
  96
+        # Set up a second temporary directory which is ensured to have a mixed
  97
+        # case name.
  98
+        self.temp_dir2 = tempfile.mkdtemp(suffix='aBc')
97 99
 
98 100
     def tearDown(self):
99 101
         shutil.rmtree(self.temp_dir)
  102
+        shutil.rmtree(self.temp_dir2)
100 103
 
101 104
     def test_file_access_options(self):
102 105
         """
@@ -265,6 +268,20 @@ def test_file_storage_prevents_directory_traversal(self):
265 268
         self.assertRaises(SuspiciousOperation, self.storage.exists, '..')
266 269
         self.assertRaises(SuspiciousOperation, self.storage.exists, '/etc/passwd')
267 270
 
  271
+    def test_file_storage_preserves_filename_case(self):
  272
+        """The storage backend should preserve case of filenames."""
  273
+        # Create a storage backend associated with the mixed case name
  274
+        # directory.
  275
+        temp_storage = self.storage_class(location=self.temp_dir2)
  276
+        # Ask that storage backend to store a file with a mixed case filename.
  277
+        mixed_case = 'CaSe_SeNsItIvE'
  278
+        file = temp_storage.open(mixed_case, 'w')
  279
+        file.write('storage contents')
  280
+        file.close()
  281
+        self.assertEqual(os.path.join(self.temp_dir2, mixed_case),
  282
+                         temp_storage.path(mixed_case))
  283
+        temp_storage.delete(mixed_case)
  284
+
268 285
 class CustomStorage(FileSystemStorage):
269 286
     def get_available_name(self, name):
270 287
         """
31  tests/regressiontests/file_uploads/tests.py
@@ -278,6 +278,37 @@ def handle_uncaught_exception(self, request, resolver, exc_info):
278 278
             # CustomUploadError is the error that should have been raised
279 279
             self.assertEqual(err.__class__, uploadhandler.CustomUploadError)
280 280
 
  281
+    def test_filename_case_preservation(self):
  282
+        """
  283
+        The storage backend shouldn't mess with the case of the filenames
  284
+        uploaded.
  285
+        """
  286
+        # Synthesize the contents of a file upload with a mixed case filename
  287
+        # so we don't have to carry such a file in the Django tests source code
  288
+        # tree.
  289
+        vars = {'boundary': 'oUrBoUnDaRyStRiNg'}
  290
+        post_data = [
  291
+            '--%(boundary)s',
  292
+            'Content-Disposition: form-data; name="file_field"; '
  293
+                'filename="MiXeD_cAsE.txt"',
  294
+            'Content-Type: application/octet-stream',
  295
+            '',
  296
+            'file contents\n'
  297
+            '',
  298
+            '--%(boundary)s--\r\n',
  299
+        ]
  300
+        response = self.client.post(
  301
+            '/file_uploads/filename_case/',
  302
+            '\r\n'.join(post_data) % vars,
  303
+            'multipart/form-data; boundary=%(boundary)s' % vars
  304
+        )
  305
+        self.assertEqual(response.status_code, 200)
  306
+        id = int(response.content)
  307
+        obj = FileModel.objects.get(pk=id)
  308
+        # The name of the file uploaded and the file stored in the server-side
  309
+        # shouldn't differ.
  310
+        self.assertEqual(os.path.basename(obj.testfile.path), 'MiXeD_cAsE.txt')
  311
+
281 312
 class DirectoryCreationTests(unittest.TestCase):
282 313
     """
283 314
     Tests for error handling during directory creation
1  tests/regressiontests/file_uploads/urls.py
@@ -11,4 +11,5 @@
11 11
     (r'^quota/broken/$',    views.file_upload_quota_broken),
12 12
     (r'^getlist_count/$',   views.file_upload_getlist_count),
13 13
     (r'^upload_errors/$',   views.file_upload_errors),
  14
+    (r'^filename_case/$',   views.file_upload_filename_case_view),
14 15
 )
9  tests/regressiontests/file_uploads/views.py
@@ -120,3 +120,12 @@ def file_upload_getlist_count(request):
120 120
 def file_upload_errors(request):
121 121
     request.upload_handlers.insert(0, ErroringUploadHandler())
122 122
     return file_upload_echo(request)
  123
+
  124
+def file_upload_filename_case_view(request):
  125
+    """
  126
+    Check adding the file to the database will preserve the filename case.
  127
+    """
  128
+    file = request.FILES['file_field']
  129
+    obj = FileModel()
  130
+    obj.testfile.save(file.name, file)
  131
+    return HttpResponse('%d' % obj.pk)
8  tests/regressiontests/templates/tests.py
@@ -161,8 +161,8 @@ def test_loaders_security(self):
161 161
         fs_loader = filesystem.Loader()
162 162
         def test_template_sources(path, template_dirs, expected_sources):
163 163
             if isinstance(expected_sources, list):
164  
-                # Fix expected sources so they are normcased and abspathed
165  
-                expected_sources = [os.path.normcase(os.path.abspath(s)) for s in expected_sources]
  164
+                # Fix expected sources so they are abspathed
  165
+                expected_sources = [os.path.abspath(s) for s in expected_sources]
166 166
             # Test the two loaders (app_directores and filesystem).
167 167
             func1 = lambda p, t: list(ad_loader.get_template_sources(p, t))
168 168
             func2 = lambda p, t: list(fs_loader.get_template_sources(p, t))
@@ -205,9 +205,9 @@ def test_template_sources(path, template_dirs, expected_sources):
205 205
         if os.path.normcase('/TEST') == os.path.normpath('/test'):
206 206
             template_dirs = ['/dir1', '/DIR2']
207 207
             test_template_sources('index.html', template_dirs,
208  
-                                  ['/dir1/index.html', '/dir2/index.html'])
  208
+                                  ['/dir1/index.html', '/DIR2/index.html'])
209 209
             test_template_sources('/DIR1/index.HTML', template_dirs,
210  
-                                  ['/dir1/index.html'])
  210
+                                  ['/DIR1/index.HTML'])
211 211
 
212 212
     def test_loader_debug_origin(self):
213 213
         # Turn TEMPLATE_DEBUG on, so that the origin file name will be kept with

0 notes on commit fcf7fbc

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