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 |