diff --git a/django/utils/_os.py b/django/utils/_os.py
a
|
b
|
|
21 | 21 | path = join(os.getcwdu(), path) |
22 | 22 | return normpath(path) |
23 | 23 | |
| 24 | if os.name == 'nt': |
| 25 | _normcase = lambda s: s.replace("/", "\\") |
| 26 | else: |
| 27 | _normcase = normcase |
| 28 | |
24 | 29 | def safe_join(base, *paths): |
25 | 30 | """ |
26 | 31 | Joins one or more path components to the base path component intelligently. |
… |
… |
|
29 | 34 | The final path must be located inside of the base path component (otherwise |
30 | 35 | a ValueError is raised). |
31 | 36 | """ |
32 | | # We need to use normcase to ensure we don't false-negative on case |
33 | | # insensitive operating systems (like Windows). |
34 | 37 | base = force_unicode(base) |
35 | 38 | paths = [force_unicode(p) for p in paths] |
36 | | final_path = normcase(abspathu(join(base, *paths))) |
37 | | base_path = normcase(abspathu(base)) |
| 39 | final_path = abspathu(join(base, *paths)) |
| 40 | fp = normcase(final_path) |
| 41 | final_path = _normcase(final_path) |
| 42 | |
| 43 | base_path = abspathu(base) |
| 44 | bp = normcase(base_path) |
| 45 | base_path = _normcase(base_path) |
| 46 | |
38 | 47 | base_path_len = len(base_path) |
39 | 48 | # Ensure final_path starts with base_path and that the next character after |
40 | | # the final path is os.sep (or nothing, in which case final_path must be |
41 | | # equal to base_path). |
42 | | if not final_path.startswith(base_path) \ |
43 | | or final_path[base_path_len:base_path_len+1] not in ('', sep): |
| 49 | # the base path portion of final_path is os.sep (or nothing, in which case |
| 50 | # final_path must be equal to base_path). |
| 51 | # We need to use normcase in these checks to ensure we don't false-negative |
| 52 | # on case insensitive operating systems (like Windows). |
| 53 | if not fp.startswith(bp) \ |
| 54 | or fp[base_path_len:base_path_len+1] not in ('', sep): |
44 | 55 | raise ValueError('the joined path is located outside of the base path' |
45 | 56 | ' component') |
46 | 57 | return final_path |
diff --git a/tests/regressiontests/file_storage/tests.py b/tests/regressiontests/file_storage/tests.py
a
|
b
|
|
5 | 5 | >>> import tempfile |
6 | 6 | >>> from django.core.files.storage import FileSystemStorage |
7 | 7 | >>> from django.core.files.base import ContentFile |
| 8 | >>> import os.path |
8 | 9 | |
9 | 10 | # Set up a unique temporary directory |
10 | 11 | >>> import os |
… |
… |
|
43 | 44 | ... |
44 | 45 | SuspiciousOperation: Attempted access to '/etc/passwd' denied. |
45 | 46 | |
| 47 | # Case should be preserved by the storage backend |
| 48 | |
| 49 | # Set up a unique temporary directory with a mixed case name |
| 50 | >>> temp_dir2 = tempfile.mktemp() |
| 51 | >>> dn, bn = os.path.split(temp_dir2) |
| 52 | >>> bn = bn[0].swapcase() + bn[1:-1] + bn[-1].swapcase() |
| 53 | >>> temp_dir2 = os.path.join(dn, bn) |
| 54 | >>> os.makedirs(temp_dir2) |
| 55 | |
| 56 | >>> temp_storage2 = FileSystemStorage(location=temp_dir2) |
| 57 | |
| 58 | # Ask the storage backend to store a file with a mixed case filename |
| 59 | >>> mixed_case = 'CaSe_SeNsItIvE' |
| 60 | >>> file = temp_storage2.open(mixed_case, 'w') |
| 61 | >>> file.write('storage contents') |
| 62 | >>> file.close() |
| 63 | >>> os.path.join(temp_dir2, mixed_case) == temp_storage2.path(mixed_case) |
| 64 | True |
| 65 | >>> temp_storage2.delete(mixed_case) |
| 66 | |
46 | 67 | # Custom storage systems can be created to customize behavior |
47 | 68 | |
48 | 69 | >>> class CustomStorage(FileSystemStorage): |
… |
… |
|
70 | 91 | >>> custom_storage.delete(first) |
71 | 92 | >>> custom_storage.delete(second) |
72 | 93 | |
73 | | # Cleanup the temp dir |
| 94 | # Cleanup the temp dirs |
74 | 95 | >>> os.rmdir(temp_dir) |
| 96 | >>> os.rmdir(temp_dir2) |
75 | 97 | |
76 | 98 | |
77 | 99 | # Regression test for #8156: files with unicode names I can't quite figure out the |
diff --git a/tests/regressiontests/file_uploads/tests.py b/tests/regressiontests/file_uploads/tests.py
a
|
b
|
|
1 | 1 | import os |
| 2 | import os.path |
2 | 3 | import errno |
3 | 4 | import shutil |
4 | 5 | import unittest |
… |
… |
|
229 | 230 | # CustomUploadError is the error that should have been raised |
230 | 231 | self.assertEqual(err.__class__, uploadhandler.CustomUploadError) |
231 | 232 | |
| 233 | def test_filename_case_preservation(self): |
| 234 | """ |
| 235 | The storage backend shouldn't mess with the case of the filenames |
| 236 | uploaded. |
| 237 | """ |
| 238 | # Synthetize the contents of a file upload with a mixed |
| 239 | # case filename so we don't have to carry such a file |
| 240 | # in the Django tests source code tree |
| 241 | OUR_BOUNDARY='oUrBoUnDaRyStRiNg' |
| 242 | post_data = [ |
| 243 | '--%s' % OUR_BOUNDARY, |
| 244 | 'Content-Disposition: form-data; name="file_field"; filename="MiXeD_cAsE.txt"', |
| 245 | 'Content-Type: application/octet-stream', |
| 246 | '', |
| 247 | 'file contents\n' |
| 248 | '', |
| 249 | '--%s--\r\n' % OUR_BOUNDARY, |
| 250 | ] |
| 251 | response = self.client.post('/file_uploads/filename_case/', '\r\n'.join(post_data), |
| 252 | 'multipart/form-data; boundary=%s' % OUR_BOUNDARY) |
| 253 | self.assertEqual(response.status_code, 200) |
| 254 | id = int(response.content) |
| 255 | obj = FileModel.objects.get(pk=id) |
| 256 | # The name of the file uploaded and the file stored in the server-side |
| 257 | # shouldn't differ |
| 258 | self.assertEqual(os.path.basename(obj.testfile.path), 'MiXeD_cAsE.txt') |
| 259 | |
232 | 260 | class DirectoryCreationTests(unittest.TestCase): |
233 | 261 | """ |
234 | 262 | Tests for error handling during directory creation |
diff --git a/tests/regressiontests/file_uploads/urls.py b/tests/regressiontests/file_uploads/urls.py
a
|
b
|
|
9 | 9 | (r'^quota/broken/$', views.file_upload_quota_broken), |
10 | 10 | (r'^getlist_count/$', views.file_upload_getlist_count), |
11 | 11 | (r'^upload_errors/$', views.file_upload_errors), |
| 12 | (r'^filename_case/$', views.file_upload_filename_case_view), |
12 | 13 | ) |
diff --git a/tests/regressiontests/file_uploads/views.py b/tests/regressiontests/file_uploads/views.py
a
|
b
|
|
88 | 88 | def file_upload_errors(request): |
89 | 89 | request.upload_handlers.insert(0, ErroringUploadHandler()) |
90 | 90 | return file_upload_echo(request) |
| 91 | |
| 92 | def file_upload_filename_case_view(request): |
| 93 | # Adding the file to the database should succeed |
| 94 | file = request.FILES['file_field'] |
| 95 | obj = FileModel() |
| 96 | obj.testfile.save(file.name, file) |
| 97 | |
| 98 | return HttpResponse('%d' % obj.pk) |
| 99 | |
diff --git a/tests/regressiontests/templates/tests.py b/tests/regressiontests/templates/tests.py
a
|
b
|
|
137 | 137 | if os.path.normcase('/TEST') == os.path.normpath('/test'): |
138 | 138 | template_dirs = ['/dir1', '/DIR2'] |
139 | 139 | test_template_sources('index.html', template_dirs, |
140 | | ['/dir1/index.html', '/dir2/index.html']) |
| 140 | ['/dir1/index.html', '/DIR2/index.html']) |
141 | 141 | test_template_sources('/DIR1/index.HTML', template_dirs, |
142 | | ['/dir1/index.html']) |
| 142 | ['/DIR1/index.HTML']) |
143 | 143 | |
144 | 144 | def test_token_smart_split(self): |
145 | 145 | # Regression test for #7027 |