From 8a160d5de1763614ab1a86107e779b0afd151ade Mon Sep 17 00:00:00 2001 From: David Evans Date: Wed, 7 Aug 2013 12:00:39 +0100 Subject: [PATCH 01/47] Use `usegmt` flag in formatdate Slightly cleaner and faster than string manipulation. This flag has been available since Python 2.4: http://docs.python.org/2/library/email.util.html#email.utils.formatdate --- django/utils/http.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/django/utils/http.py b/django/utils/http.py index 4647d89847..e397acad5b 100644 --- a/django/utils/http.py +++ b/django/utils/http.py @@ -109,8 +109,7 @@ def http_date(epoch_seconds=None): Outputs a string in the format 'Wdy, DD Mon YYYY HH:MM:SS GMT'. """ - rfcdate = formatdate(epoch_seconds) - return '%s GMT' % rfcdate[:25] + return formatdate(epoch_seconds, usegmt=True) def parse_http_date(date): """ From 453915bb1272c9a9189a741e6a9b9246edfcbd03 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Fri, 9 Aug 2013 10:57:25 -0400 Subject: [PATCH 02/47] SQLite test fix -- refs #9057 --- django/contrib/auth/tests/test_management.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/django/contrib/auth/tests/test_management.py b/django/contrib/auth/tests/test_management.py index 3711f52dea..91a6a589c1 100644 --- a/django/contrib/auth/tests/test_management.py +++ b/django/contrib/auth/tests/test_management.py @@ -239,21 +239,22 @@ class PermissionTestCase(TestCase): create_permissions(models, [], verbosity=0) def test_default_permissions(self): + permission_content_type = ContentType.objects.get_by_natural_key('auth', 'permission') models.Permission._meta.permissions = [ ('my_custom_permission', 'Some permission'), ] create_permissions(models, [], verbosity=0) # add/change/delete permission by default + custom permission - self.assertEqual(models.Permission.objects.filter(content_type= - ContentType.objects.get_by_natural_key('auth', 'permission') + self.assertEqual(models.Permission.objects.filter( + content_type=permission_content_type, ).count(), 4) - models.Permission.objects.all().delete() + models.Permission.objects.filter(content_type=permission_content_type).delete() models.Permission._meta.default_permissions = [] create_permissions(models, [], verbosity=0) # custom permission only since default permissions is empty - self.assertEqual(models.Permission.objects.filter(content_type= - ContentType.objects.get_by_natural_key('auth', 'permission') + self.assertEqual(models.Permission.objects.filter( + content_type=permission_content_type, ).count(), 1) From 0cac4fbf699bb6a3de5f4a48c6e047a4dc6c2df7 Mon Sep 17 00:00:00 2001 From: Bojan Mihelac Date: Mon, 4 Feb 2013 16:50:15 +0100 Subject: [PATCH 03/47] Fixed #18356 -- Gave the test client signals.template_rendered call a unique dispatch_uid This prevents the test client context from being lost when the client is used in a nested fashion. --- AUTHORS | 1 + django/test/client.py | 5 +++-- tests/test_client_regress/tests.py | 8 ++++++++ tests/test_client_regress/urls.py | 1 + tests/test_client_regress/views.py | 13 ++++++++++++- 5 files changed, 25 insertions(+), 3 deletions(-) diff --git a/AUTHORS b/AUTHORS index 15f3e8dbf4..9300c07a4f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -418,6 +418,7 @@ answer newbie questions, and generally made Django that much better: Christian Metts michal@plovarna.cz Justin Michalicek + Bojan Mihelac Slawek Mikula Katie Miller Shawn Milochik diff --git a/django/test/client.py b/django/test/client.py index 754d4d73f8..ae7d7a5fb7 100644 --- a/django/test/client.py +++ b/django/test/client.py @@ -406,7 +406,8 @@ class Client(RequestFactory): # callback function. data = {} on_template_render = curry(store_rendered_templates, data) - signals.template_rendered.connect(on_template_render, dispatch_uid="template-render") + signal_uid = "template-render-%s" % id(request) + signals.template_rendered.connect(on_template_render, dispatch_uid=signal_uid) # Capture exceptions created by the handler. got_request_exception.connect(self.store_exc_info, dispatch_uid="request-exception") try: @@ -452,7 +453,7 @@ class Client(RequestFactory): return response finally: - signals.template_rendered.disconnect(dispatch_uid="template-render") + signals.template_rendered.disconnect(dispatch_uid=signal_uid) got_request_exception.disconnect(dispatch_uid="request-exception") def get(self, path, data={}, follow=False, **extra): diff --git a/tests/test_client_regress/tests.py b/tests/test_client_regress/tests.py index 67e66fa52d..857d6d83c0 100644 --- a/tests/test_client_regress/tests.py +++ b/tests/test_client_regress/tests.py @@ -925,6 +925,14 @@ class ContextTests(TestCase): finally: django.template.context._standard_context_processors = None + def test_nested_requests(self): + """ + response.context is not lost when view call another view. + """ + response = self.client.get("/test_client_regress/nested_view/") + self.assertEqual(response.context.__class__, Context) + self.assertEqual(response.context['nested'], 'yes') + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class SessionTests(TestCase): diff --git a/tests/test_client_regress/urls.py b/tests/test_client_regress/urls.py index 6a0b330e02..1bde10315d 100644 --- a/tests/test_client_regress/urls.py +++ b/tests/test_client_regress/urls.py @@ -11,6 +11,7 @@ urlpatterns = patterns('', (r'^request_data/$', views.request_data), (r'^request_data_extended/$', views.request_data, {'template':'extended.html', 'data':'bacon'}), url(r'^arg_view/(?P.+)/$', views.view_with_argument, name='arg_view'), + url(r'^nested_view/$', views.nested_view, name='nested_view'), (r'^login_protected_redirect_view/$', views.login_protected_redirect_view), (r'^redirects/$', RedirectView.as_view(url='/test_client_regress/redirects/further/')), (r'^redirects/further/$', RedirectView.as_view(url='/test_client_regress/redirects/further/more/')), diff --git a/tests/test_client_regress/views.py b/tests/test_client_regress/views.py index 71e5b526e5..d7c54cf2f7 100644 --- a/tests/test_client_regress/views.py +++ b/tests/test_client_regress/views.py @@ -5,8 +5,10 @@ from django.contrib.auth.decorators import login_required from django.http import HttpResponse, HttpResponseRedirect from django.shortcuts import render_to_response from django.core.serializers.json import DjangoJSONEncoder -from django.test.client import CONTENT_TYPE_RE from django.template import RequestContext +from django.test import Client +from django.test.client import CONTENT_TYPE_RE +from django.test.utils import setup_test_environment class CustomTestException(Exception): @@ -52,6 +54,15 @@ def view_with_argument(request, name): else: return HttpResponse('Howdy, %s' % name) +def nested_view(request): + """ + A view that uses test client to call another view. + """ + setup_test_environment() + c = Client() + c.get("/test_client_regress/no_template_view") + return render_to_response('base.html', {'nested':'yes'}) + def login_protected_redirect_view(request): "A view that redirects all requests to the GET view" return HttpResponseRedirect('/test_client_regress/get_view/') From 5737c57d95cc8c17b1aa2da4809f70ad4c212716 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Fri, 9 Aug 2013 16:02:05 -0400 Subject: [PATCH 04/47] Fixed #20868 -- Added an email to django-announce as a security step. Thanks garrison for the report. --- docs/internals/security.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/internals/security.txt b/docs/internals/security.txt index 486b2c9968..327a6a5f60 100644 --- a/docs/internals/security.txt +++ b/docs/internals/security.txt @@ -108,8 +108,12 @@ On the day of disclosure, we will take the following steps: relevant patches and new releases, and crediting the reporter of the issue (if the reporter wishes to be publicly identified). +4. Post a notice to the `django-announce`_ mailing list that links to the blog + post. + .. _the Python Package Index: http://pypi.python.org/pypi .. _the official Django development blog: https://www.djangoproject.com/weblog/ +.. _django-announce: http://groups.google.com/group/django-announce If a reported issue is believed to be particularly time-sensitive -- due to a known exploit in the wild, for example -- the time between @@ -214,4 +218,4 @@ If you are added to the notification list, security-related emails will be sent to you by Django's release manager, and all notification emails will be signed with the same key used to sign Django releases; that key has the ID ``0x3684C0C08C8B2AE1``, and is available from most -commonly-used keyservers. \ No newline at end of file +commonly-used keyservers. From 00d23a13ebaf6057d1428e798bfb6cf47bb5ef7c Mon Sep 17 00:00:00 2001 From: ersran9 Date: Wed, 7 Aug 2013 21:33:31 +0530 Subject: [PATCH 05/47] Fixed #20828 -- Allowed @permission_required to take a list of permissions Thanks Giggaflop for the suggestion. --- django/contrib/auth/decorators.py | 6 +- django/contrib/auth/tests/test_decorators.py | 58 +++++++++++++++++++- docs/releases/1.7.txt | 3 + docs/topics/auth/default.txt | 5 ++ 4 files changed, 70 insertions(+), 2 deletions(-) diff --git a/django/contrib/auth/decorators.py b/django/contrib/auth/decorators.py index 11518193e7..24e31144b1 100644 --- a/django/contrib/auth/decorators.py +++ b/django/contrib/auth/decorators.py @@ -64,8 +64,12 @@ def permission_required(perm, login_url=None, raise_exception=False): is raised. """ def check_perms(user): + if not isinstance(perm, (list, tuple)): + perms = (perm, ) + else: + perms = perm # First check if the user has the permission (even anon users) - if user.has_perm(perm): + if user.has_perms(perms): return True # In case the 403 handler should be called raise the exception if raise_exception: diff --git a/django/contrib/auth/tests/test_decorators.py b/django/contrib/auth/tests/test_decorators.py index 6d6d335354..22ad933644 100644 --- a/django/contrib/auth/tests/test_decorators.py +++ b/django/contrib/auth/tests/test_decorators.py @@ -1,7 +1,12 @@ from django.conf import settings -from django.contrib.auth.decorators import login_required +from django.contrib.auth import models +from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.tests.test_views import AuthViewsTestCase from django.contrib.auth.tests.utils import skipIfCustomUser +from django.core.exceptions import PermissionDenied +from django.http import HttpResponse +from django.test import TestCase +from django.test.client import RequestFactory @skipIfCustomUser @@ -49,3 +54,54 @@ class LoginRequiredTestCase(AuthViewsTestCase): """ self.testLoginRequired(view_url='/login_required_login_url/', login_url='/somewhere/') + + +class PermissionsRequiredDecoratorTest(TestCase): + """ + Tests for the permission_required decorator + """ + def setUp(self): + self.user = models.User.objects.create(username='joe', password='qwerty') + self.factory = RequestFactory() + # Add permissions auth.add_customuser and auth.change_customuser + perms = models.Permission.objects.filter(codename__in=('add_customuser', 'change_customuser')) + self.user.user_permissions.add(*perms) + + def test_many_permissions_pass(self): + + @permission_required(['auth.add_customuser', 'auth.change_customuser']) + def a_view(request): + return HttpResponse() + request = self.factory.get('/rand') + request.user = self.user + resp = a_view(request) + self.assertEqual(resp.status_code, 200) + + def test_single_permission_pass(self): + + @permission_required('auth.add_customuser') + def a_view(request): + return HttpResponse() + request = self.factory.get('/rand') + request.user = self.user + resp = a_view(request) + self.assertEqual(resp.status_code, 200) + + def test_permissioned_denied_redirect(self): + + @permission_required(['auth.add_customuser', 'auth.change_customuser', 'non-existant-permission']) + def a_view(request): + return HttpResponse() + request = self.factory.get('/rand') + request.user = self.user + resp = a_view(request) + self.assertEqual(resp.status_code, 302) + + def test_permissioned_denied_exception_raised(self): + + @permission_required(['auth.add_customuser', 'auth.change_customuser', 'non-existant-permission'], raise_exception=True) + def a_view(request): + return HttpResponse() + request = self.factory.get('/rand') + request.user = self.user + self.assertRaises(PermissionDenied, a_view, request) diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index 6c28d6e1d0..ec37b382d4 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -135,6 +135,9 @@ Minor features ``Meta`` option allows you to customize (or disable) creation of the default add, change, and delete permissions. +* The :func:`~django.contrib.auth.decorators.permission_required` decorator can + take a list of permissions as well as a single permission. + Backwards incompatible changes in 1.7 ===================================== diff --git a/docs/topics/auth/default.txt b/docs/topics/auth/default.txt index 7dff9cdca7..78bf820e89 100644 --- a/docs/topics/auth/default.txt +++ b/docs/topics/auth/default.txt @@ -528,6 +528,11 @@ The permission_required decorator (HTTP Forbidden) view` instead of redirecting to the login page. + .. versionchanged:: 1.7 + + The :func:`~django.contrib.auth.decorators.permission_required` + decorator can take a list of permissions as well as a single permission. + Applying permissions to generic views ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From cb92e3391b0560bc3d519f066185522316b4533f Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Sat, 10 Aug 2013 15:54:22 -0300 Subject: [PATCH 06/47] Test that django.views.static.serve() generates 404 status codes. Also, change tests to be based on SimpleTestCase. --- tests/view_tests/tests/test_static.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/view_tests/tests/test_static.py b/tests/view_tests/tests/test_static.py index d2f3f47fa7..8f67890c11 100644 --- a/tests/view_tests/tests/test_static.py +++ b/tests/view_tests/tests/test_static.py @@ -6,7 +6,7 @@ import unittest from django.conf.urls.static import static from django.http import HttpResponseNotModified -from django.test import TestCase +from django.test import SimpleTestCase from django.test.utils import override_settings from django.utils.http import http_date from django.views.static import was_modified_since @@ -16,7 +16,7 @@ from ..urls import media_dir @override_settings(DEBUG=True) -class StaticTests(TestCase): +class StaticTests(SimpleTestCase): """Tests django views in django/views/static.py""" prefix = 'site_media' @@ -94,6 +94,10 @@ class StaticTests(TestCase): self.assertEqual(len(response_content), int(response['Content-Length'])) + def test_404(self): + response = self.client.get('/views/%s/non_existing_resource' % self.prefix) + self.assertEqual(404, response.status_code) + class StaticHelperTest(StaticTests): """ From 22af1394c64b6687e45747ae9f643068b340a867 Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Sat, 10 Aug 2013 16:32:07 -0300 Subject: [PATCH 07/47] Expand testing of Test LiveServerTestCase static files serving. --- tests/runtests.py | 1 + tests/servers/another_app/__init__.py | 0 tests/servers/another_app/models.py | 0 .../static/another_app/another_app_static_file.txt | 1 + tests/servers/tests.py | 8 ++++++++ 5 files changed, 10 insertions(+) create mode 100644 tests/servers/another_app/__init__.py create mode 100644 tests/servers/another_app/models.py create mode 100644 tests/servers/another_app/static/another_app/another_app_static_file.txt diff --git a/tests/runtests.py b/tests/runtests.py index 53318a7461..adfc77b13b 100755 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -60,6 +60,7 @@ ALWAYS_INSTALLED_APPS = [ 'staticfiles_tests', 'staticfiles_tests.apps.test', 'staticfiles_tests.apps.no_label', + 'servers.another_app', ] diff --git a/tests/servers/another_app/__init__.py b/tests/servers/another_app/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/servers/another_app/models.py b/tests/servers/another_app/models.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/servers/another_app/static/another_app/another_app_static_file.txt b/tests/servers/another_app/static/another_app/another_app_static_file.txt new file mode 100644 index 0000000000..a2784fa8e2 --- /dev/null +++ b/tests/servers/another_app/static/another_app/another_app_static_file.txt @@ -0,0 +1 @@ +static file from another_app diff --git a/tests/servers/tests.py b/tests/servers/tests.py index 1f02a88d5a..0340873013 100644 --- a/tests/servers/tests.py +++ b/tests/servers/tests.py @@ -145,6 +145,14 @@ class LiveServerViews(LiveServerBase): f = self.urlopen('/static/example_static_file.txt') self.assertEqual(f.read().rstrip(b'\r\n'), b'example static file') + def test_collectstatic_emulation(self): + """ + Test LiveServerTestCase use of staticfiles' serve() allows it to + discover app's static assets without having to collectstatic first. + """ + f = self.urlopen('/static/another_app/another_app_static_file.txt') + self.assertEqual(f.read().rstrip(b'\r\n'), b'static file from another_app') + def test_media_files(self): """ Ensure that the LiveServerTestCase serves media files. From e868eaf680a3d7acfcd3c76743bb248d29ac7b60 Mon Sep 17 00:00:00 2001 From: Daniele Procida Date: Sat, 10 Aug 2013 22:24:24 +0100 Subject: [PATCH 08/47] clarified misleading wording about squashing commits --- docs/internals/contributing/writing-code/working-with-git.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/internals/contributing/writing-code/working-with-git.txt b/docs/internals/contributing/writing-code/working-with-git.txt index dcfdd9e85b..32fc459e70 100644 --- a/docs/internals/contributing/writing-code/working-with-git.txt +++ b/docs/internals/contributing/writing-code/working-with-git.txt @@ -157,7 +157,7 @@ using interactive rebase:: The HEAD~2 above is shorthand for two latest commits. The above command will open an editor showing the two commits, prefixed with the word "pick". -Change the second line to "squash" instead. This will keep the +Change "pick" on the second line to "squash" instead. This will keep the first commit, and squash the second commit into the first one. Save and quit the editor. A second editor window should open, so you can reword the commit message for the commit now that it includes both your steps. From ab680725bfb2f0d79cff26331b30a3d583c55a80 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sat, 10 Aug 2013 18:08:05 -0400 Subject: [PATCH 09/47] Fixed #20890 -- Added missing import in class-based view docs. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thanks André Augusto. --- docs/topics/class-based-views/intro.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/class-based-views/intro.txt b/docs/topics/class-based-views/intro.txt index a65b887921..5986ff2ea7 100644 --- a/docs/topics/class-based-views/intro.txt +++ b/docs/topics/class-based-views/intro.txt @@ -198,6 +198,7 @@ A similar class-based view might look like:: from django.http import HttpResponseRedirect from django.shortcuts import render + from django.views.generic.base import View from .forms import MyForm From 6bdb3b1135d1bd7b2dc24131b9d26ac19ebdba67 Mon Sep 17 00:00:00 2001 From: Mel Collins Date: Mon, 13 May 2013 13:38:53 +0200 Subject: [PATCH 10/47] Fixed #13518 -- Added FILE_UPLOAD_DIRECTORY_PERMISSIONS setting This setting does for new directories what FILE_UPLOAD_PERMISSIONS does for new files. Thanks jacob@ for the suggestion. --- django/conf/global_settings.py | 5 +++++ django/core/files/storage.py | 11 ++++++++++- docs/ref/settings.txt | 13 +++++++++++++ docs/releases/1.7.txt | 4 ++++ docs/topics/http/file-uploads.txt | 7 ++++++- tests/file_storage/tests.py | 12 ++++++++++++ 6 files changed, 50 insertions(+), 2 deletions(-) diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 19258fbcd4..95deaa8d87 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -313,6 +313,11 @@ FILE_UPLOAD_TEMP_DIR = None # you'd pass directly to os.chmod; see http://docs.python.org/lib/os-file-dir.html. FILE_UPLOAD_PERMISSIONS = None +# The numeric mode to assign to newly-created directories, when uploading files. +# The value should be a mode as you'd pass to os.chmod; +# see http://docs.python.org/lib/os-file-dir.html. +FILE_UPLOAD_DIRECTORY_PERMISSIONS = None + # Python module path where user will place custom format definition. # The directory where this setting is pointing should contain subdirectories # named as the locales, containing a formats.py file diff --git a/django/core/files/storage.py b/django/core/files/storage.py index 5d301a317c..5e587da2da 100644 --- a/django/core/files/storage.py +++ b/django/core/files/storage.py @@ -172,7 +172,16 @@ class FileSystemStorage(Storage): directory = os.path.dirname(full_path) if not os.path.exists(directory): try: - os.makedirs(directory) + if settings.FILE_UPLOAD_DIRECTORY_PERMISSIONS is not None: + # os.makedirs applies the global umask, so we reset it, + # for consistency with FILE_UPLOAD_PERMISSIONS behavior. + old_umask = os.umask(0) + try: + os.makedirs(directory, settings.FILE_UPLOAD_DIRECTORY_PERMISSIONS) + finally: + os.umask(old_umask) + else: + os.makedirs(directory) except OSError as e: if e.errno != errno.EEXIST: raise diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 38d7275aed..90545d96c5 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -1108,6 +1108,19 @@ Default: ``2621440`` (i.e. 2.5 MB). The maximum size (in bytes) that an upload will be before it gets streamed to the file system. See :doc:`/topics/files` for details. +.. setting:: FILE_UPLOAD_DIRECTORY_PERMISSIONS + +FILE_UPLOAD_DIRECTORY_PERMISSIONS +--------------------------------- + +.. versionadded:: 1.7 + +Default: ``None`` + +The numeric mode to apply to directories created in the process of +uploading files. This value mirrors the functionality and caveats of +the :setting:`FILE_UPLOAD_PERMISSIONS` setting. + .. setting:: FILE_UPLOAD_PERMISSIONS FILE_UPLOAD_PERMISSIONS diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index ec37b382d4..6fda83ebc7 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -138,6 +138,10 @@ Minor features * The :func:`~django.contrib.auth.decorators.permission_required` decorator can take a list of permissions as well as a single permission. +* The new :setting:`FILE_UPLOAD_DIRECTORY_PERMISSIONS` setting controls + the file system permissions of directories created during file upload, like + :setting:`FILE_UPLOAD_PERMISSIONS` does for the files themselves. + Backwards incompatible changes in 1.7 ===================================== diff --git a/docs/topics/http/file-uploads.txt b/docs/topics/http/file-uploads.txt index 2cdab9ea9b..d88524ee20 100644 --- a/docs/topics/http/file-uploads.txt +++ b/docs/topics/http/file-uploads.txt @@ -132,7 +132,7 @@ upload behavior. Changing upload handler behavior -------------------------------- -Three settings control Django's file upload behavior: +There are a few settings which control Django's file upload behavior: :setting:`FILE_UPLOAD_MAX_MEMORY_SIZE` The maximum size, in bytes, for files that will be uploaded into memory. @@ -167,6 +167,11 @@ Three settings control Django's file upload behavior: **Always prefix the mode with a 0.** +:setting:`FILE_UPLOAD_DIRECTORY_PERMISSIONS` + The numeric mode to apply to directories created in the process of + uploading files. This value mirrors the functionality and caveats of + the :setting:`FILE_UPLOAD_PERMISSIONS` setting. + :setting:`FILE_UPLOAD_HANDLERS` The actual handlers for uploaded files. Changing this setting allows complete customization -- even replacement -- of Django's upload diff --git a/tests/file_storage/tests.py b/tests/file_storage/tests.py index cdd9720374..2a6f602e3e 100644 --- a/tests/file_storage/tests.py +++ b/tests/file_storage/tests.py @@ -462,6 +462,18 @@ class FileStoragePermissions(unittest.TestCase): mode = os.stat(self.storage.path(fname))[0] & 0o777 self.assertEqual(mode, 0o666 & ~self.umask) + @override_settings(FILE_UPLOAD_DIRECTORY_PERMISSIONS=0o765) + def test_file_upload_directory_permissions(self): + name = self.storage.save("the_directory/the_file", ContentFile("data")) + dir_mode = os.stat(os.path.dirname(self.storage.path(name)))[0] & 0o777 + self.assertEqual(dir_mode, 0o765) + + @override_settings(FILE_UPLOAD_DIRECTORY_PERMISSIONS=None) + def test_file_upload_directory_default_permissions(self): + name = self.storage.save("the_directory/the_file", ContentFile("data")) + dir_mode = os.stat(os.path.dirname(self.storage.path(name)))[0] & 0o777 + self.assertEqual(dir_mode, 0o777 & ~self.umask) + class FileStoragePathParsing(unittest.TestCase): def setUp(self): self.storage_dir = tempfile.mkdtemp() From 71b5617c24bb997db294480f07611233069e3359 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Mon, 12 Aug 2013 12:41:39 -0400 Subject: [PATCH 11/47] Fixed #17778 -- Prevented class attributes on context from resolving as template variables. Thanks KyleMac for the report, regebro for the patch, and Aymeric for the test. --- django/template/base.py | 5 ++++- tests/template_tests/test_context.py | 11 ++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/django/template/base.py b/django/template/base.py index ed4196012a..382b85aefd 100644 --- a/django/template/base.py +++ b/django/template/base.py @@ -6,7 +6,7 @@ from importlib import import_module from inspect import getargspec from django.conf import settings -from django.template.context import (Context, RequestContext, +from django.template.context import (BaseContext, Context, RequestContext, ContextPopException) from django.utils.itercompat import is_iterable from django.utils.text import (smart_split, unescape_string_literal, @@ -765,6 +765,9 @@ class Variable(object): current = current[bit] except (TypeError, AttributeError, KeyError, ValueError): try: # attribute lookup + # Don't return class attributes if the class is the context: + if isinstance(current, BaseContext) and getattr(type(current), bit): + raise AttributeError current = getattr(current, bit) except (TypeError, AttributeError): try: # list-index lookup diff --git a/tests/template_tests/test_context.py b/tests/template_tests/test_context.py index ca167a73f3..7bcfb7f9f2 100644 --- a/tests/template_tests/test_context.py +++ b/tests/template_tests/test_context.py @@ -2,7 +2,7 @@ from unittest import TestCase -from django.template import Context +from django.template import Context, Variable, VariableDoesNotExist class ContextTests(TestCase): @@ -25,3 +25,12 @@ class ContextTests(TestCase): with c.push(a=3): self.assertEqual(c['a'], 3) self.assertEqual(c['a'], 1) + + def test_resolve_on_context_method(self): + # Regression test for #17778 + empty_context = Context() + self.assertRaises(VariableDoesNotExist, + Variable('no_such_variable').resolve, empty_context) + self.assertRaises(VariableDoesNotExist, + Variable('new').resolve, empty_context) + self.assertEqual(Variable('new').resolve(Context({'new': 'foo'})), 'foo') From 3f6cc33cffa5774beedf7997354fc269497a93dd Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Mon, 12 Aug 2013 13:20:58 -0400 Subject: [PATCH 12/47] Added missing release notes for older versions of Django --- docs/releases/1.3.3.txt | 11 ++++++ docs/releases/1.3.4.txt | 37 +++++++++++++++++ docs/releases/1.3.5.txt | 60 ++++++++++++++++++++++++++++ docs/releases/1.3.6.txt | 78 ++++++++++++++++++++++++++++++++++++ docs/releases/1.3.7.txt | 13 ++++++ docs/releases/1.4.2.txt | 9 +++-- docs/releases/1.4.3.txt | 60 ++++++++++++++++++++++++++++ docs/releases/1.4.4.txt | 88 +++++++++++++++++++++++++++++++++++++++++ docs/releases/1.4.5.txt | 13 ++++++ docs/releases/index.txt | 8 ++++ 10 files changed, 373 insertions(+), 4 deletions(-) create mode 100644 docs/releases/1.3.3.txt create mode 100644 docs/releases/1.3.4.txt create mode 100644 docs/releases/1.3.5.txt create mode 100644 docs/releases/1.3.6.txt create mode 100644 docs/releases/1.3.7.txt create mode 100644 docs/releases/1.4.3.txt create mode 100644 docs/releases/1.4.4.txt create mode 100644 docs/releases/1.4.5.txt diff --git a/docs/releases/1.3.3.txt b/docs/releases/1.3.3.txt new file mode 100644 index 0000000000..437cbfb412 --- /dev/null +++ b/docs/releases/1.3.3.txt @@ -0,0 +1,11 @@ +========================== +Django 1.3.3 release notes +========================== + +*August 1, 2012* + +Following Monday's security release of :doc:`Django 1.3.2 `, +we began receiving reports that one of the fixes applied was breaking Python +2.4 compatibility for Django 1.3. Since Python 2.4 is a supported Python +version for that release series, this release fixes compatibility with +Python 2.4. diff --git a/docs/releases/1.3.4.txt b/docs/releases/1.3.4.txt new file mode 100644 index 0000000000..3a174b3d44 --- /dev/null +++ b/docs/releases/1.3.4.txt @@ -0,0 +1,37 @@ +========================== +Django 1.3.4 release notes +========================== + +*October 17, 2012* + +This is the fourth release in the Django 1.3 series. + +Host header poisoning +--------------------- + +Some parts of Django -- independent of end-user-written applications -- make +use of full URLs, including domain name, which are generated from the HTTP Host +header. Some attacks against this are beyond Django's ability to control, and +require the web server to be properly configured; Django's documentation has +for some time contained notes advising users on such configuration. + +Django's own built-in parsing of the Host header is, however, still vulnerable, +as was reported to us recently. The Host header parsing in Django 1.3.3 and +Django 1.4.1 -- specifically, ``django.http.HttpRequest.get_host()`` -- was +incorrectly handling username/password information in the header. Thus, for +example, the following Host header would be accepted by Django when running on +"validsite.com":: + + Host: validsite.com:random@evilsite.com + +Using this, an attacker can cause parts of Django -- particularly the +password-reset mechanism -- to generate and display arbitrary URLs to users. + +To remedy this, the parsing in ``HttpRequest.get_host()`` is being modified; +Host headers which contain potentially dangerous content (such as +username/password pairs) now raise the exception +:exc:`django.core.exceptions.SuspiciousOperation`. + +Details of this issue were initially posted online as a `security advisory`_. + +.. _security advisory: https://www.djangoproject.com/weblog/2012/oct/17/security/ diff --git a/docs/releases/1.3.5.txt b/docs/releases/1.3.5.txt new file mode 100644 index 0000000000..65c403209d --- /dev/null +++ b/docs/releases/1.3.5.txt @@ -0,0 +1,60 @@ +========================== +Django 1.3.5 release notes +========================== + +*December 10, 2012* + +Django 1.3.5 addresses two security issues present in previous Django releases +in the 1.3 series. + +Please be aware that this security release is slightly different from previous +ones. Both issues addressed here have been dealt with in prior security updates +to Django. In one case, we have received ongoing reports of problems, and in +the other we've chosen to take further steps to tighten up Django's code in +response to independent discovery of potential problems from multiple sources. + +Host header poisoning +--------------------- + +Several earlier Django security releases focused on the issue of poisoning the +HTTP Host header, causing Django to generate URLs pointing to arbitrary, +potentially-malicious domains. + +In response to further input received and reports of continuing issues +following the previous release, we're taking additional steps to tighten Host +header validation. Rather than attempt to accommodate all features HTTP +supports here, Django's Host header validation attempts to support a smaller, +but far more common, subset: + +* Hostnames must consist of characters [A-Za-z0-9] plus hyphen ('-') or dot + ('.'). +* IP addresses -- both IPv4 and IPv6 -- are permitted. +* Port, if specified, is numeric. + +Any deviation from this will now be rejected, raising the exception +:exc:`django.core.exceptions.SuspiciousOperation`. + +Redirect poisoning +------------------ + +Also following up on a previous issue: in July of this year, we made changes to +Django's HTTP redirect classes, performing additional validation of the scheme +of the URL to redirect to (since, both within Django's own supplied +applications and many third-party applications, accepting a user-supplied +redirect target is a common pattern). + +Since then, two independent audits of the code turned up further potential +problems. So, similar to the Host-header issue, we are taking steps to provide +tighter validation in response to reported problems (primarily with third-party +applications, but to a certain extent also within Django itself). This comes in +two parts: + +1. A new utility function, ``django.utils.http.is_safe_url``, is added; this +function takes a URL and a hostname, and checks that the URL is either +relative, or if absolute matches the supplied hostname. This function is +intended for use whenever user-supplied redirect targets are accepted, to +ensure that such redirects cannot lead to arbitrary third-party sites. + +2. All of Django's own built-in views -- primarily in the authentication system +-- which allow user-supplied redirect targets now use ``is_safe_url`` to +validate the supplied URL. diff --git a/docs/releases/1.3.6.txt b/docs/releases/1.3.6.txt new file mode 100644 index 0000000000..d55199a882 --- /dev/null +++ b/docs/releases/1.3.6.txt @@ -0,0 +1,78 @@ +========================== +Django 1.3.6 release notes +========================== + +*February 19, 2013* + +Django 1.3.6 fixes four security issues present in previous Django releases in +the 1.3 series. + +This is the sixth bugfix/security release in the Django 1.3 series. + + +Host header poisoning +--------------------- + +Some parts of Django -- independent of end-user-written applications -- make +use of full URLs, including domain name, which are generated from the HTTP Host +header. Django's documentation has for some time contained notes advising users +on how to configure webservers to ensure that only valid Host headers can reach +the Django application. However, it has been reported to us that even with the +recommended webserver configurations there are still techniques available for +tricking many common webservers into supplying the application with an +incorrect and possibly malicious Host header. + +For this reason, Django 1.3.6 adds a new setting, ``ALLOWED_HOSTS``, which +should contain an explicit list of valid host/domain names for this site. A +request with a Host header not matching an entry in this list will raise +``SuspiciousOperation`` if ``request.get_host()`` is called. For full details +see the documentation for the :setting:`ALLOWED_HOSTS` setting. + +The default value for this setting in Django 1.3.6 is ``['*']`` (matching any +host), for backwards-compatibility, but we strongly encourage all sites to set +a more restrictive value. + +This host validation is disabled when ``DEBUG`` is ``True`` or when running tests. + + +XML deserialization +------------------- + +The XML parser in the Python standard library is vulnerable to a number of +attacks via external entities and entity expansion. Django uses this parser for +deserializing XML-formatted database fixtures. The fixture deserializer is not +intended for use with untrusted data, but in order to err on the side of safety +in Django 1.3.6 the XML deserializer refuses to parse an XML document with a +DTD (DOCTYPE definition), which closes off these attack avenues. + +These issues in the Python standard library are CVE-2013-1664 and +CVE-2013-1665. More information available `from the Python security team`_. + +Django's XML serializer does not create documents with a DTD, so this should +not cause any issues with the typical round-trip from ``dumpdata`` to +``loaddata``, but if you feed your own XML documents to the ``loaddata`` +management command, you will need to ensure they do not contain a DTD. + +.. _from the Python security team: http://blog.python.org/2013/02/announcing-defusedxml-fixes-for-xml.html + + +Formset memory exhaustion +------------------------- + +Previous versions of Django did not validate or limit the form-count data +provided by the client in a formset's management form, making it possible to +exhaust a server's available memory by forcing it to create very large numbers +of forms. + +In Django 1.3.6, all formsets have a strictly-enforced maximum number of forms +(1000 by default, though it can be set higher via the ``max_num`` formset +factory argument). + + +Admin history view information leakage +-------------------------------------- + +In previous versions of Django, an admin user without change permission on a +model could still view the unicode representation of instances via their admin +history log. Django 1.3.6 now limits the admin history log view for an object +to users with change permission for that model. diff --git a/docs/releases/1.3.7.txt b/docs/releases/1.3.7.txt new file mode 100644 index 0000000000..3cccfcfb1c --- /dev/null +++ b/docs/releases/1.3.7.txt @@ -0,0 +1,13 @@ +========================== +Django 1.3.7 release notes +========================== + +*February 20, 2013* + +Django 1.3.7 corrects a packaging problem with yesterday's :doc:`1.3.6 release +`. + +The release contained stray ``.pyc`` files that caused "bad magic number" +errors when running with some versions of Python. This releases corrects this, +and also fixes a bad documentation link in the project template ``settings.py`` +file generated by ``manage.py startproject``. diff --git a/docs/releases/1.4.2.txt b/docs/releases/1.4.2.txt index 07eec39764..a6150f56c3 100644 --- a/docs/releases/1.4.2.txt +++ b/docs/releases/1.4.2.txt @@ -17,7 +17,7 @@ for some time contained notes advising users on such configuration. Django's own built-in parsing of the Host header is, however, still vulnerable, as was reported to us recently. The Host header parsing in Django 1.3.3 and -Django 1.4.1 -- specifically, django.http.HttpRequest.get_host() -- was +Django 1.4.1 -- specifically, ``django.http.HttpRequest.get_host()`` -- was incorrectly handling username/password information in the header. Thus, for example, the following Host header would be accepted by Django when running on "validsite.com":: @@ -27,9 +27,10 @@ example, the following Host header would be accepted by Django when running on Using this, an attacker can cause parts of Django -- particularly the password-reset mechanism -- to generate and display arbitrary URLs to users. -To remedy this, the parsing in HttpRequest.get_host() is being modified; Host -headers which contain potentially dangerous content (such as username/password -pairs) now raise the exception django.core.exceptions.SuspiciousOperation +To remedy this, the parsing in ``HttpRequest.get_host()`` is being modified; +Host headers which contain potentially dangerous content (such as +username/password pairs) now raise the exception +:exc:`django.core.exceptions.SuspiciousOperation`. Details of this issue were initially posted online as a `security advisory`_. diff --git a/docs/releases/1.4.3.txt b/docs/releases/1.4.3.txt new file mode 100644 index 0000000000..aadf623c3c --- /dev/null +++ b/docs/releases/1.4.3.txt @@ -0,0 +1,60 @@ +========================== +Django 1.4.3 release notes +========================== + +*December 10, 2012* + +Django 1.4.3 addresses two security issues present in previous Django releases +in the 1.4 series. + +Please be aware that this security release is slightly different from previous +ones. Both issues addressed here have been dealt with in prior security updates +to Django. In one case, we have received ongoing reports of problems, and in +the other we've chosen to take further steps to tighten up Django's code in +response to independent discovery of potential problems from multiple sources. + +Host header poisoning +--------------------- + +Several earlier Django security releases focused on the issue of poisoning the +HTTP Host header, causing Django to generate URLs pointing to arbitrary, +potentially-malicious domains. + +In response to further input received and reports of continuing issues +following the previous release, we're taking additional steps to tighten Host +header validation. Rather than attempt to accommodate all features HTTP +supports here, Django's Host header validation attempts to support a smaller, +but far more common, subset: + +* Hostnames must consist of characters [A-Za-z0-9] plus hyphen ('-') or dot + ('.'). +* IP addresses -- both IPv4 and IPv6 -- are permitted. +* Port, if specified, is numeric. + +Any deviation from this will now be rejected, raising the exception +:exc:`django.core.exceptions.SuspiciousOperation`. + +Redirect poisoning +------------------ + +Also following up on a previous issue: in July of this year, we made changes to +Django's HTTP redirect classes, performing additional validation of the scheme +of the URL to redirect to (since, both within Django's own supplied +applications and many third-party applications, accepting a user-supplied +redirect target is a common pattern). + +Since then, two independent audits of the code turned up further potential +problems. So, similar to the Host-header issue, we are taking steps to provide +tighter validation in response to reported problems (primarily with third-party +applications, but to a certain extent also within Django itself). This comes in +two parts: + +1. A new utility function, ``django.utils.http.is_safe_url``, is added; this +function takes a URL and a hostname, and checks that the URL is either +relative, or if absolute matches the supplied hostname. This function is +intended for use whenever user-supplied redirect targets are accepted, to +ensure that such redirects cannot lead to arbitrary third-party sites. + +2. All of Django's own built-in views -- primarily in the authentication system +-- which allow user-supplied redirect targets now use ``is_safe_url`` to +validate the supplied URL. diff --git a/docs/releases/1.4.4.txt b/docs/releases/1.4.4.txt new file mode 100644 index 0000000000..c5fcbc3e39 --- /dev/null +++ b/docs/releases/1.4.4.txt @@ -0,0 +1,88 @@ +========================== +Django 1.4.4 release notes +========================== + +*February 19, 2013* + +Django 1.4.4 fixes four security issues present in previous Django releases in +the 1.4 series, as well as several other bugs and numerous documentation +improvements. + +This is the fourth bugfix/security release in the Django 1.4 series. + + +Host header poisoning +--------------------- + +Some parts of Django -- independent of end-user-written applications -- make +use of full URLs, including domain name, which are generated from the HTTP Host +header. Django's documentation has for some time contained notes advising users +on how to configure webservers to ensure that only valid Host headers can reach +the Django application. However, it has been reported to us that even with the +recommended webserver configurations there are still techniques available for +tricking many common webservers into supplying the application with an +incorrect and possibly malicious Host header. + +For this reason, Django 1.4.4 adds a new setting, ``ALLOWED_HOSTS``, containing +an explicit list of valid host/domain names for this site. A request with a +Host header not matching an entry in this list will raise +``SuspiciousOperation`` if ``request.get_host()`` is called. For full details +see the documentation for the :setting:`ALLOWED_HOSTS` setting. + +The default value for this setting in Django 1.4.4 is ``['*']`` (matching any +host), for backwards-compatibility, but we strongly encourage all sites to set +a more restrictive value. + +This host validation is disabled when ``DEBUG`` is ``True`` or when running tests. + + +XML deserialization +------------------- + +The XML parser in the Python standard library is vulnerable to a number of +attacks via external entities and entity expansion. Django uses this parser for +deserializing XML-formatted database fixtures. This deserializer is not +intended for use with untrusted data, but in order to err on the side of safety +in Django 1.4.4 the XML deserializer refuses to parse an XML document with a +DTD (DOCTYPE definition), which closes off these attack avenues. + +These issues in the Python standard library are CVE-2013-1664 and +CVE-2013-1665. More information available `from the Python security team`_. + +Django's XML serializer does not create documents with a DTD, so this should +not cause any issues with the typical round-trip from ``dumpdata`` to +``loaddata``, but if you feed your own XML documents to the ``loaddata`` +management command, you will need to ensure they do not contain a DTD. + +.. _from the Python security team: http://blog.python.org/2013/02/announcing-defusedxml-fixes-for-xml.html + + +Formset memory exhaustion +------------------------- + +Previous versions of Django did not validate or limit the form-count data +provided by the client in a formset's management form, making it possible to +exhaust a server's available memory by forcing it to create very large numbers +of forms. + +In Django 1.4.4, all formsets have a strictly-enforced maximum number of forms +(1000 by default, though it can be set higher via the ``max_num`` formset +factory argument). + + +Admin history view information leakage +-------------------------------------- + +In previous versions of Django, an admin user without change permission on a +model could still view the unicode representation of instances via their admin +history log. Django 1.4.4 now limits the admin history log view for an object +to users with change permission for that model. + + +Other bugfixes and changes +========================== + +* Prevented transaction state from leaking from one request to the next (#19707). +* Changed a SQL command syntax to be MySQL 4 compatible (#19702). +* Added backwards-compatibility with old unsalted MD5 passwords (#18144). +* Numerous documentation improvements and fixes. diff --git a/docs/releases/1.4.5.txt b/docs/releases/1.4.5.txt new file mode 100644 index 0000000000..9ba5235f79 --- /dev/null +++ b/docs/releases/1.4.5.txt @@ -0,0 +1,13 @@ +========================== +Django 1.4.5 release notes +========================== + +*February 20, 2013* + +Django 1.4.5 corrects a packaging problem with yesterday's :doc:`1.4.4 release +`. + +The release contained stray ``.pyc`` files that caused "bad magic number" +errors when running with some versions of Python. This releases corrects this, +and also fixes a bad documentation link in the project template ``settings.py`` +file generated by ``manage.py startproject``. diff --git a/docs/releases/index.txt b/docs/releases/index.txt index 39439ff9aa..98598209cf 100644 --- a/docs/releases/index.txt +++ b/docs/releases/index.txt @@ -44,6 +44,9 @@ Final releases .. toctree:: :maxdepth: 1 + 1.4.5 + 1.4.4 + 1.4.3 1.4.2 1.4.1 1.4 @@ -53,6 +56,11 @@ Final releases .. toctree:: :maxdepth: 1 + 1.3.7 + 1.3.6 + 1.3.5 + 1.3.4 + 1.3.3 1.3.2 1.3.1 1.3 From 6c12cd15e990b0ff5a5e85328f0a092f4bfe8080 Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Sat, 20 Oct 2012 07:50:44 -0500 Subject: [PATCH 13/47] Unlocalize line numbers and ids in debug 500 view. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit While using USE_L10N, line numbers and IDs were printed as comma (or locale equivalent) separated values. Thanks Kronuz for the report and intial patch. Fixes #20861. --- django/views/debug.py | 6 +++--- tests/view_tests/tests/test_debug.py | 16 ++++++++++++++++ tests/view_tests/urls.py | 1 + tests/view_tests/views.py | 8 ++++++++ 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/django/views/debug.py b/django/views/debug.py index 2129a83d67..16f75df8c3 100644 --- a/django/views/debug.py +++ b/django/views/debug.py @@ -227,7 +227,7 @@ class ExceptionReporter(object): return "File exists" def get_traceback_data(self): - "Return a Context instance containing traceback information." + """Return a dictionary containing traceback information.""" if self.exc_type and issubclass(self.exc_type, TemplateDoesNotExist): from django.template.loader import template_source_loaders @@ -295,13 +295,13 @@ class ExceptionReporter(object): def get_traceback_html(self): "Return HTML version of debug 500 HTTP error page." t = Template(TECHNICAL_500_TEMPLATE, name='Technical 500 template') - c = Context(self.get_traceback_data()) + c = Context(self.get_traceback_data(), use_l10n=False) return t.render(c) def get_traceback_text(self): "Return plain text version of debug 500 HTTP error page." t = Template(TECHNICAL_500_TEXT_TEMPLATE, name='Technical 500 template') - c = Context(self.get_traceback_data(), autoescape=False) + c = Context(self.get_traceback_data(), autoescape=False, use_l10n=False) return t.render(c) def get_template_exception_info(self): diff --git a/tests/view_tests/tests/test_debug.py b/tests/view_tests/tests/test_debug.py index 0159886918..e941f4ae0e 100644 --- a/tests/view_tests/tests/test_debug.py +++ b/tests/view_tests/tests/test_debug.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals import inspect import os +import re import shutil import sys from tempfile import NamedTemporaryFile, mkdtemp, mkstemp @@ -69,6 +70,21 @@ class DebugViewTests(TestCase): self.assertRaises(BrokenException, self.client.get, reverse('view_exception', args=(n,))) + def test_non_l10ned_numeric_ids(self): + """ + Numeric IDs and fancy traceback context blocks line numbers shouldn't be localized. + """ + with self.settings(DEBUG=True, USE_L10N=True): + response = self.client.get('/views/raises500/') + # We look for a HTML fragment of the form + # '
', not '
', response.content) + self.assertFalse(match is None) + id_repr = match.group('id') + self.assertFalse(re.search(b'[^c\d]', id_repr), + "Numeric IDs in debug response HTML page shouldn't be localized (value: %s)." % id_repr) + def test_template_exceptions(self): for n in range(len(except_args)): try: diff --git a/tests/view_tests/urls.py b/tests/view_tests/urls.py index f2c910368d..4db3e262ed 100644 --- a/tests/view_tests/urls.py +++ b/tests/view_tests/urls.py @@ -49,6 +49,7 @@ urlpatterns = patterns('', (r'raises400/$', views.raises400), (r'raises403/$', views.raises403), (r'raises404/$', views.raises404), + (r'raises500/$', views.raises500), # i18n views (r'^i18n/', include('django.conf.urls.i18n')), diff --git a/tests/view_tests/views.py b/tests/view_tests/views.py index 0bac7d9321..04924bc9f7 100644 --- a/tests/view_tests/views.py +++ b/tests/view_tests/views.py @@ -31,6 +31,14 @@ def raises(request): except Exception: return technical_500_response(request, *sys.exc_info()) +def raises500(request): + # We need to inspect the HTML generated by the fancy 500 debug view but + # the test client ignores it, so we send it explicitly. + try: + raise Exception + except Exception: + return technical_500_response(request, *sys.exc_info()) + def raises400(request): raise SuspiciousOperation From dcdc579d162b750ee3449e34efd772703592faca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Tue, 13 Aug 2013 14:11:52 +0300 Subject: [PATCH 14/47] Fixed #20874 -- bump_prefix() in nested subqueries Also made some cleanup to build_filter() code by introducing submethods solve_lookup_type() and prepare_lookup_value(). --- django/db/models/sql/compiler.py | 5 +- django/db/models/sql/query.py | 144 +++++++++++++++++-------------- tests/foreign_object/tests.py | 19 +++- tests/queries/tests.py | 16 +++- 4 files changed, 113 insertions(+), 71 deletions(-) diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index e17cb3f616..54b4e86245 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -167,7 +167,6 @@ class SQLCompiler(object): if obj.low_mark == 0 and obj.high_mark is None: # If there is no slicing in use, then we can safely drop all ordering obj.clear_ordering(True) - obj.bump_prefix() return obj.get_compiler(connection=self.connection).as_sql() def get_columns(self, with_aliases=False): @@ -808,13 +807,14 @@ class SQLCompiler(object): return result def as_subquery_condition(self, alias, columns, qn): + inner_qn = self.quote_name_unless_alias qn2 = self.connection.ops.quote_name if len(columns) == 1: sql, params = self.as_sql() return '%s.%s IN (%s)' % (qn(alias), qn2(columns[0]), sql), params for index, select_col in enumerate(self.query.select): - lhs = '%s.%s' % (qn(select_col.col[0]), qn2(select_col.col[1])) + lhs = '%s.%s' % (inner_qn(select_col.col[0]), qn2(select_col.col[1])) rhs = '%s.%s' % (qn(alias), qn2(columns[index])) self.query.where.add( QueryWrapper('%s = %s' % (lhs, rhs), []), 'AND') @@ -1010,7 +1010,6 @@ class SQLUpdateCompiler(SQLCompiler): # We need to use a sub-select in the where clause to filter on things # from other tables. query = self.query.clone(klass=Query) - query.bump_prefix() query.extra = {} query.select = [] query.add_fields([query.get_meta().pk.name]) diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index 15f2643495..bb7d071f38 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -97,6 +97,7 @@ class Query(object): LOUTER = 'LEFT OUTER JOIN' alias_prefix = 'T' + subq_aliases = frozenset([alias_prefix]) query_terms = QUERY_TERMS aggregates_module = base_aggregates_module @@ -273,6 +274,10 @@ class Query(object): else: obj.used_aliases = set() obj.filter_is_sticky = False + if 'alias_prefix' in self.__dict__: + obj.alias_prefix = self.alias_prefix + if 'subq_aliases' in self.__dict__: + obj.subq_aliases = self.subq_aliases.copy() obj.__dict__.update(kwargs) if hasattr(obj, '_setup_query'): @@ -780,28 +785,22 @@ class Query(object): data = data._replace(lhs_alias=change_map[lhs]) self.alias_map[alias] = data - def bump_prefix(self, exceptions=()): + def bump_prefix(self, outer_query): """ - Changes the alias prefix to the next letter in the alphabet and - relabels all the aliases. Even tables that previously had no alias will - get an alias after this call (it's mostly used for nested queries and - the outer query will already be using the non-aliased table name). - - Subclasses who create their own prefix should override this method to - produce a similar result (a new prefix and relabelled aliases). - - The 'exceptions' parameter is a container that holds alias names which - should not be changed. + Changes the alias prefix to the next letter in the alphabet in a way + that the outer query's aliases and this query's aliases will not + conflict. Even tables that previously had no alias will get an alias + after this call. """ - current = ord(self.alias_prefix) - assert current < ord('Z') - prefix = chr(current + 1) - self.alias_prefix = prefix + self.alias_prefix = chr(ord(self.alias_prefix) + 1) + while self.alias_prefix in self.subq_aliases: + self.alias_prefix = chr(ord(self.alias_prefix) + 1) + assert self.alias_prefix < 'Z' + self.subq_aliases = self.subq_aliases.union([self.alias_prefix]) + outer_query.subq_aliases = outer_query.subq_aliases.union(self.subq_aliases) change_map = OrderedDict() for pos, alias in enumerate(self.tables): - if alias in exceptions: - continue - new_alias = '%s%d' % (prefix, pos) + new_alias = '%s%d' % (self.alias_prefix, pos) change_map[alias] = new_alias self.tables[pos] = new_alias self.change_aliases(change_map) @@ -1005,6 +1004,65 @@ class Query(object): # Add the aggregate to the query aggregate.add_to_query(self, alias, col=col, source=source, is_summary=is_summary) + def prepare_lookup_value(self, value, lookup_type, can_reuse): + # Interpret '__exact=None' as the sql 'is NULL'; otherwise, reject all + # uses of None as a query value. + if value is None: + if lookup_type != 'exact': + raise ValueError("Cannot use None as a query value") + lookup_type = 'isnull' + value = True + elif callable(value): + value = value() + elif isinstance(value, ExpressionNode): + # If value is a query expression, evaluate it + value = SQLEvaluator(value, self, reuse=can_reuse) + if hasattr(value, 'query') and hasattr(value.query, 'bump_prefix'): + value = value._clone() + value.query.bump_prefix(self) + if hasattr(value, 'bump_prefix'): + value = value.clone() + value.bump_prefix(self) + # For Oracle '' is equivalent to null. The check needs to be done + # at this stage because join promotion can't be done at compiler + # stage. Using DEFAULT_DB_ALIAS isn't nice, but it is the best we + # can do here. Similar thing is done in is_nullable(), too. + if (connections[DEFAULT_DB_ALIAS].features.interprets_empty_strings_as_nulls and + lookup_type == 'exact' and value == ''): + value = True + lookup_type = 'isnull' + return value, lookup_type + + def solve_lookup_type(self, lookup): + """ + Solve the lookup type from the lookup (eg: 'foobar__id__icontains') + """ + lookup_type = 'exact' # Default lookup type + lookup_parts = lookup.split(LOOKUP_SEP) + num_parts = len(lookup_parts) + if (len(lookup_parts) > 1 and lookup_parts[-1] in self.query_terms + and lookup not in self.aggregates): + # Traverse the lookup query to distinguish related fields from + # lookup types. + lookup_model = self.model + for counter, field_name in enumerate(lookup_parts): + try: + lookup_field = lookup_model._meta.get_field(field_name) + except FieldDoesNotExist: + # Not a field. Bail out. + lookup_type = lookup_parts.pop() + break + # Unless we're at the end of the list of lookups, let's attempt + # to continue traversing relations. + if (counter + 1) < num_parts: + try: + lookup_model = lookup_field.rel.to + except AttributeError: + # Not a related field. Bail out. + lookup_type = lookup_parts.pop() + break + return lookup_type, lookup_parts + def build_filter(self, filter_expr, branch_negated=False, current_negated=False, can_reuse=None): """ @@ -1033,58 +1091,15 @@ class Query(object): is responsible for unreffing the joins used. """ arg, value = filter_expr - parts = arg.split(LOOKUP_SEP) + lookup_type, parts = self.solve_lookup_type(arg) if not parts: raise FieldError("Cannot parse keyword query %r" % arg) # Work out the lookup type and remove it from the end of 'parts', # if necessary. - lookup_type = 'exact' # Default lookup type - num_parts = len(parts) - if (len(parts) > 1 and parts[-1] in self.query_terms - and arg not in self.aggregates): - # Traverse the lookup query to distinguish related fields from - # lookup types. - lookup_model = self.model - for counter, field_name in enumerate(parts): - try: - lookup_field = lookup_model._meta.get_field(field_name) - except FieldDoesNotExist: - # Not a field. Bail out. - lookup_type = parts.pop() - break - # Unless we're at the end of the list of lookups, let's attempt - # to continue traversing relations. - if (counter + 1) < num_parts: - try: - lookup_model = lookup_field.rel.to - except AttributeError: - # Not a related field. Bail out. - lookup_type = parts.pop() - break + value, lookup_type = self.prepare_lookup_value(value, lookup_type, can_reuse) clause = self.where_class() - # Interpret '__exact=None' as the sql 'is NULL'; otherwise, reject all - # uses of None as a query value. - if value is None: - if lookup_type != 'exact': - raise ValueError("Cannot use None as a query value") - lookup_type = 'isnull' - value = True - elif callable(value): - value = value() - elif isinstance(value, ExpressionNode): - # If value is a query expression, evaluate it - value = SQLEvaluator(value, self, reuse=can_reuse) - # For Oracle '' is equivalent to null. The check needs to be done - # at this stage because join promotion can't be done at compiler - # stage. Using DEFAULT_DB_ALIAS isn't nice, but it is the best we - # can do here. Similar thing is done in is_nullable(), too. - if (connections[DEFAULT_DB_ALIAS].features.interprets_empty_strings_as_nulls and - lookup_type == 'exact' and value == ''): - value = True - lookup_type = 'isnull' - for alias, aggregate in self.aggregates.items(): if alias in (parts[0], LOOKUP_SEP.join(parts)): clause.add((aggregate, lookup_type, value), AND) @@ -1096,7 +1111,7 @@ class Query(object): try: field, sources, opts, join_list, path = self.setup_joins( - parts, opts, alias, can_reuse, allow_many,) + parts, opts, alias, can_reuse, allow_many,) if can_reuse is not None: can_reuse.update(join_list) except MultiJoin as e: @@ -1404,7 +1419,6 @@ class Query(object): # Generate the inner query. query = Query(self.model) query.where.add(query.build_filter(filter_expr), AND) - query.bump_prefix() query.clear_ordering(True) # Try to have as simple as possible subquery -> trim leading joins from # the subquery. diff --git a/tests/foreign_object/tests.py b/tests/foreign_object/tests.py index cd81cc68a2..77582162a8 100644 --- a/tests/foreign_object/tests.py +++ b/tests/foreign_object/tests.py @@ -132,7 +132,6 @@ class MultiColumnFKTests(TestCase): ], attrgetter('person_id') ) - self.assertQuerysetEqual( Membership.objects.filter(person__in=Person.objects.filter(name='Jim')), [ self.jim.id, @@ -140,6 +139,24 @@ class MultiColumnFKTests(TestCase): attrgetter('person_id') ) + def test_double_nested_query(self): + m1 = Membership.objects.create(membership_country_id=self.usa.id, person_id=self.bob.id, + group_id=self.cia.id) + m2 = Membership.objects.create(membership_country_id=self.usa.id, person_id=self.jim.id, + group_id=self.cia.id) + Friendship.objects.create(from_friend_country_id=self.usa.id, from_friend_id=self.bob.id, + to_friend_country_id=self.usa.id, to_friend_id=self.jim.id) + self.assertQuerysetEqual(Membership.objects.filter( + person__in=Person.objects.filter( + from_friend__in=Friendship.objects.filter( + to_friend__in=Person.objects.all()))), + [m1], lambda x: x) + self.assertQuerysetEqual(Membership.objects.exclude( + person__in=Person.objects.filter( + from_friend__in=Friendship.objects.filter( + to_friend__in=Person.objects.all()))), + [m2], lambda x: x) + def test_select_related_foreignkey_forward_works(self): Membership.objects.create(membership_country=self.usa, person=self.bob, group=self.cia) Membership.objects.create(membership_country=self.usa, person=self.jim, group=self.democrat) diff --git a/tests/queries/tests.py b/tests/queries/tests.py index 6e03b0d7f6..cd47f0ccd5 100644 --- a/tests/queries/tests.py +++ b/tests/queries/tests.py @@ -27,7 +27,6 @@ from .models import ( BaseA, FK1, Identifier, Program, Channel, Page, Paragraph, Chapter, Book, MyObject, Order, OrderItem) - class BaseQuerysetTest(TestCase): def assertValueQuerysetEqual(self, qs, values): return self.assertQuerysetEqual(qs, values, transform=lambda x: x) @@ -84,6 +83,19 @@ class Queries1Tests(BaseQuerysetTest): Cover.objects.create(title="first", item=i4) Cover.objects.create(title="second", item=self.i2) + def test_subquery_condition(self): + qs1 = Tag.objects.filter(pk__lte=0) + qs2 = Tag.objects.filter(parent__in=qs1) + qs3 = Tag.objects.filter(parent__in=qs2) + self.assertEqual(qs3.query.subq_aliases, set(['T', 'U', 'V'])) + self.assertIn('V0', str(qs3.query)) + qs4 = qs3.filter(parent__in=qs1) + self.assertEqual(qs4.query.subq_aliases, set(['T', 'U', 'V'])) + # It is possible to reuse U for the second subquery, no need to use W. + self.assertNotIn('W0', str(qs4.query)) + # So, 'U0."id"' is referenced twice. + self.assertTrue(str(qs4.query).count('U0."id"'), 2) + def test_ticket1050(self): self.assertQuerysetEqual( Item.objects.filter(tags__isnull=True), @@ -810,7 +822,7 @@ class Queries1Tests(BaseQuerysetTest): # Make sure bump_prefix() (an internal Query method) doesn't (re-)break. It's # sufficient that this query runs without error. qs = Tag.objects.values_list('id', flat=True).order_by('id') - qs.query.bump_prefix() + qs.query.bump_prefix(qs.query) first = qs[0] self.assertEqual(list(qs), list(range(first, first+5))) From 33fc083b0db392bd35bc9d5d9a141adb25537253 Mon Sep 17 00:00:00 2001 From: Loic Bistuer Date: Tue, 13 Aug 2013 17:38:29 +0700 Subject: [PATCH 15/47] Fixed overflow for the "Recent Actions" widget on the admin index. Previously the CSS targeted "li.changelink" and therefore didn't work for the "add" and "delete" actions. Refs #14868. --- django/contrib/admin/static/admin/css/dashboard.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/django/contrib/admin/static/admin/css/dashboard.css b/django/contrib/admin/static/admin/css/dashboard.css index ceefe1525f..05808bcb0a 100644 --- a/django/contrib/admin/static/admin/css/dashboard.css +++ b/django/contrib/admin/static/admin/css/dashboard.css @@ -23,8 +23,8 @@ ul.actionlist li { list-style-type: none; } -ul.actionlist li.changelink { +ul.actionlist li { overflow: hidden; text-overflow: ellipsis; -o-text-overflow: ellipsis; -} \ No newline at end of file +} From 163a34ce4bc1086b346a52c7271f48d2c207f710 Mon Sep 17 00:00:00 2001 From: Loic Bistuer Date: Mon, 12 Aug 2013 18:30:38 +0700 Subject: [PATCH 16/47] Fixed #20883 -- Made model inheritance find parent links in abstract parents --- django/db/models/base.py | 21 ++++++++++++++++----- docs/releases/1.7.txt | 3 +++ tests/model_inheritance_regress/models.py | 13 +++++++++++++ tests/model_inheritance_regress/tests.py | 16 +++++++++++++++- 4 files changed, 47 insertions(+), 6 deletions(-) diff --git a/django/db/models/base.py b/django/db/models/base.py index 01ba559e08..cd3768bccb 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -184,10 +184,21 @@ class ModelBase(type): else: new_class._meta.concrete_model = new_class - # Do the appropriate setup for any model parents. - o2o_map = dict([(f.rel.to, f) for f in new_class._meta.local_fields - if isinstance(f, OneToOneField)]) + # Collect the parent links for multi-table inheritance. + parent_links = {} + for base in reversed([new_class] + parents): + # Conceptually equivalent to `if base is Model`. + if not hasattr(base, '_meta'): + continue + # Skip concrete parent classes. + if base != new_class and not base._meta.abstract: + continue + # Locate OneToOneField instances. + for field in base._meta.local_fields: + if isinstance(field, OneToOneField): + parent_links[field.rel.to] = field + # Do the appropriate setup for any model parents. for base in parents: original_base = base if not hasattr(base, '_meta'): @@ -208,8 +219,8 @@ class ModelBase(type): if not base._meta.abstract: # Concrete classes... base = base._meta.concrete_model - if base in o2o_map: - field = o2o_map[base] + if base in parent_links: + field = parent_links[base] elif not is_proxy: attr_name = '%s_ptr' % base._meta.model_name field = OneToOneField(base, name=attr_name, diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index 6fda83ebc7..6ea957d959 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -142,6 +142,9 @@ Minor features the file system permissions of directories created during file upload, like :setting:`FILE_UPLOAD_PERMISSIONS` does for the files themselves. +* Explicit :class:`~django.db.models.OneToOneField` for + :ref:`multi-table-inheritance` are now discovered in abstract classes. + Backwards incompatible changes in 1.7 ===================================== diff --git a/tests/model_inheritance_regress/models.py b/tests/model_inheritance_regress/models.py index 811c8175bb..0f45a2cb3f 100644 --- a/tests/model_inheritance_regress/models.py +++ b/tests/model_inheritance_regress/models.py @@ -50,6 +50,19 @@ class ParkingLot3(Place): primary_key = models.AutoField(primary_key=True) parent = models.OneToOneField(Place, parent_link=True) +class ParkingLot4(models.Model): + # Test parent_link connector can be discovered in abstract classes. + parent = models.OneToOneField(Place, parent_link=True) + + class Meta: + abstract = True + +class ParkingLot4A(ParkingLot4, Place): + pass + +class ParkingLot4B(Place, ParkingLot4): + pass + class Supplier(models.Model): restaurant = models.ForeignKey(Restaurant) diff --git a/tests/model_inheritance_regress/tests.py b/tests/model_inheritance_regress/tests.py index 10a1230685..7f78fc7a98 100644 --- a/tests/model_inheritance_regress/tests.py +++ b/tests/model_inheritance_regress/tests.py @@ -14,7 +14,8 @@ from .models import (Place, Restaurant, ItalianRestaurant, ParkingLot, ParkingLot2, ParkingLot3, Supplier, Wholesaler, Child, SelfRefParent, SelfRefChild, ArticleWithAuthor, M2MChild, QualityControl, DerivedM, Person, BirthdayParty, BachelorParty, MessyBachelorParty, - InternalCertificationAudit, BusStation, TrainStation, User, Profile) + InternalCertificationAudit, BusStation, TrainStation, User, Profile, + ParkingLot4A, ParkingLot4B) class ModelInheritanceTest(TestCase): @@ -311,6 +312,19 @@ class ModelInheritanceTest(TestCase): ParkingLot3._meta.get_ancestor_link(Place).name, "parent") + def test_use_explicit_o2o_to_parent_from_abstract_model(self): + self.assertEqual(ParkingLot4A._meta.pk.name, "parent") + ParkingLot4A.objects.create( + name="Parking4A", + address='21 Jump Street', + ) + + self.assertEqual(ParkingLot4B._meta.pk.name, "parent") + ParkingLot4A.objects.create( + name="Parking4B", + address='21 Jump Street', + ) + def test_all_fields_from_abstract_base_class(self): """ Regression tests for #7588 From 09a5f5aabe27f63ec8d8982efa6cef9bf7b86022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Tue, 13 Aug 2013 15:30:02 +0300 Subject: [PATCH 17/47] Fixed test failure on MySQL The fix for #20874 caused a MySQL specific failure. --- tests/queries/tests.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/queries/tests.py b/tests/queries/tests.py index cd47f0ccd5..a3558a06d4 100644 --- a/tests/queries/tests.py +++ b/tests/queries/tests.py @@ -88,13 +88,13 @@ class Queries1Tests(BaseQuerysetTest): qs2 = Tag.objects.filter(parent__in=qs1) qs3 = Tag.objects.filter(parent__in=qs2) self.assertEqual(qs3.query.subq_aliases, set(['T', 'U', 'V'])) - self.assertIn('V0', str(qs3.query)) + self.assertIn('v0', str(qs3.query).lower()) qs4 = qs3.filter(parent__in=qs1) self.assertEqual(qs4.query.subq_aliases, set(['T', 'U', 'V'])) # It is possible to reuse U for the second subquery, no need to use W. - self.assertNotIn('W0', str(qs4.query)) + self.assertNotIn('w0', str(qs4.query).lower()) # So, 'U0."id"' is referenced twice. - self.assertTrue(str(qs4.query).count('U0."id"'), 2) + self.assertTrue(str(qs4.query).lower().count('u0'), 2) def test_ticket1050(self): self.assertQuerysetEqual( From ae3535169af804352517b7fea94a42a1c9c4b762 Mon Sep 17 00:00:00 2001 From: Jacob Kaplan-Moss Date: Tue, 13 Aug 2013 11:06:22 -0500 Subject: [PATCH 18/47] Fixed is_safe_url() to reject URLs that use a scheme other than HTTP/S. This is a security fix; disclosure to follow shortly. --- django/contrib/auth/tests/test_views.py | 8 ++++++-- django/utils/http.py | 7 ++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/django/contrib/auth/tests/test_views.py b/django/contrib/auth/tests/test_views.py index 22ccbfd225..7839b0b9f9 100644 --- a/django/contrib/auth/tests/test_views.py +++ b/django/contrib/auth/tests/test_views.py @@ -446,7 +446,8 @@ class LoginTest(AuthViewsTestCase): for bad_url in ('http://example.com', 'https://example.com', 'ftp://exampel.com', - '//example.com'): + '//example.com', + 'javascript:alert("XSS")'): nasty_url = '%(url)s?%(next)s=%(bad_url)s' % { 'url': login_url, @@ -467,6 +468,7 @@ class LoginTest(AuthViewsTestCase): '/view?param=ftp://exampel.com', 'view/?param=//example.com', 'https:///', + 'HTTPS:///', '//testserver/', '/url%20with%20spaces/'): # see ticket #12534 safe_url = '%(url)s?%(next)s=%(good_url)s' % { @@ -661,7 +663,8 @@ class LogoutTest(AuthViewsTestCase): for bad_url in ('http://example.com', 'https://example.com', 'ftp://exampel.com', - '//example.com'): + '//example.com', + 'javascript:alert("XSS")'): nasty_url = '%(url)s?%(next)s=%(bad_url)s' % { 'url': logout_url, 'next': REDIRECT_FIELD_NAME, @@ -680,6 +683,7 @@ class LogoutTest(AuthViewsTestCase): '/view?param=ftp://exampel.com', 'view/?param=//example.com', 'https:///', + 'HTTPS:///', '//testserver/', '/url%20with%20spaces/'): # see ticket #12534 safe_url = '%(url)s?%(next)s=%(good_url)s' % { diff --git a/django/utils/http.py b/django/utils/http.py index e397acad5b..9b36ab91d7 100644 --- a/django/utils/http.py +++ b/django/utils/http.py @@ -252,11 +252,12 @@ def same_origin(url1, url2): def is_safe_url(url, host=None): """ Return ``True`` if the url is a safe redirection (i.e. it doesn't point to - a different host). + a different host and uses a safe scheme). Always returns ``False`` on an empty url. """ if not url: return False - netloc = urllib_parse.urlparse(url)[1] - return not netloc or netloc == host + url_info = urllib_parse.urlparse(url) + return (not url_info.netloc or url_info.netloc == host) and \ + (not url_info.scheme or url_info.scheme in ['http', 'https']) From cbe6d5568f4f5053ed7228ca3c3d0cce77cf9560 Mon Sep 17 00:00:00 2001 From: Jacob Kaplan-Moss Date: Tue, 13 Aug 2013 11:06:41 -0500 Subject: [PATCH 19/47] Apply autoescaping to AdminURLFieldWidget. This is a security fix; disclosure to follow shortly. --- django/contrib/admin/widgets.py | 4 ++-- tests/admin_widgets/tests.py | 20 +++++++++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py index c4b15cdd6a..5773db6394 100644 --- a/django/contrib/admin/widgets.py +++ b/django/contrib/admin/widgets.py @@ -305,9 +305,9 @@ class AdminURLFieldWidget(forms.URLInput): html = super(AdminURLFieldWidget, self).render(name, value, attrs) if value: value = force_text(self._format_value(value)) - final_attrs = {'href': mark_safe(smart_urlquote(value))} + final_attrs = {'href': smart_urlquote(value)} html = format_html( - '

{0} {2}
{3} {4}

', + '

{0} {2}
{3} {4}

', _('Currently:'), flatatt(final_attrs), value, _('Change:'), html ) diff --git a/tests/admin_widgets/tests.py b/tests/admin_widgets/tests.py index 5a88df1e57..9f5abe0684 100644 --- a/tests/admin_widgets/tests.py +++ b/tests/admin_widgets/tests.py @@ -321,18 +321,24 @@ class AdminURLWidgetTest(DjangoTestCase): w = widgets.AdminURLFieldWidget() self.assertHTMLEqual( conditional_escape(w.render('test', 'http://example-äüö.com')), - '

Currently:http://example-äüö.com
Change:

' + '

Currently: http://example-äüö.com
Change:

' ) def test_render_quoting(self): + # WARNING: Don't use assertHTMLEqual in that testcase! + # assertHTMLEqual will get rid of some escapes which are tested here! w = widgets.AdminURLFieldWidget() - self.assertHTMLEqual( - conditional_escape(w.render('test', 'http://example.com/some text')), - '

Currently:http://example.com/<sometag>some text</sometag>
Change:

' + self.assertEqual( + w.render('test', 'http://example.com/some text'), + '

Currently: http://example.com/<sometag>some text</sometag>
Change:

' ) - self.assertHTMLEqual( - conditional_escape(w.render('test', 'http://example-äüö.com/some text')), - '

Currently:http://example-äüö.com/<sometag>some text</sometag>
Change:

' + self.assertEqual( + w.render('test', 'http://example-äüö.com/some text'), + '

Currently: http://example-äüö.com/<sometag>some text</sometag>
Change:

' + ) + self.assertEqual( + w.render('test', 'http://www.example.com/%C3%A4">"'), + '

Currently: http://www.example.com/%C3%A4"><script>alert("XSS!")</script>"
Change:

' ) From db682dcc9e028fa40bb4d3efb322fd3191ed1bd2 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Tue, 13 Aug 2013 11:16:30 -0500 Subject: [PATCH 20/47] Added 1.4.6/1.5.2 release notes. --- docs/releases/1.4.6.txt | 31 +++++++++++++++++++++ docs/releases/1.5.2.txt | 62 +++++++++++++++++++++++++++++++++++++++++ docs/releases/index.txt | 2 ++ 3 files changed, 95 insertions(+) create mode 100644 docs/releases/1.4.6.txt create mode 100644 docs/releases/1.5.2.txt diff --git a/docs/releases/1.4.6.txt b/docs/releases/1.4.6.txt new file mode 100644 index 0000000000..575e9fa75a --- /dev/null +++ b/docs/releases/1.4.6.txt @@ -0,0 +1,31 @@ +========================== +Django 1.4.6 release notes +========================== + +*August 13, 2013* + +Django 1.4.6 fixes one security issue present in previous Django releases in +the 1.4 series, as well as one other bug. + +This is the sixth bugfix/security release in the Django 1.4 series. + +Mitigated possible XSS attack via user-supplied redirect URLs +------------------------------------------------------------- + +Django relies on user input in some cases (e.g. +:func:`django.contrib.auth.views.login`, :mod:`django.contrib.comments`, and +:doc:`i18n `) to redirect the user to an "on success" URL. +The security checks for these redirects (namely +``django.util.http.is_safe_url()``) didn't check if the scheme is ``http(s)`` +and as such allowed ``javascript:...`` URLs to be entered. If a developer +relied on ``is_safe_url()`` to provide safe redirect targets and put such a +URL into a link, he could suffer from a XSS attack. This bug doesn't affect +Django currently, since we only put this URL into the ``Location`` response +header and browsers seem to ignore JavaScript there. + +Bugfixes +======== + +* Fixed an obscure bug with the :func:`~django.test.utils.override_settings` + decorator. If you hit an ``AttributeError: 'Settings' object has no attribute + '_original_allowed_hosts'`` exception, it's probably fixed (#20636). diff --git a/docs/releases/1.5.2.txt b/docs/releases/1.5.2.txt new file mode 100644 index 0000000000..710f16555c --- /dev/null +++ b/docs/releases/1.5.2.txt @@ -0,0 +1,62 @@ +========================== +Django 1.5.2 release notes +========================== + +*August 13, 2013* + +This is Django 1.5.2, a bugfix and security release for Django 1.5. + +Mitigated possible XSS attack via user-supplied redirect URLs +------------------------------------------------------------- + +Django relies on user input in some cases (e.g. +:func:`django.contrib.auth.views.login`, :mod:`django.contrib.comments`, and +:doc:`i18n `) to redirect the user to an "on success" URL. +The security checks for these redirects (namely +``django.util.http.is_safe_url()``) didn't check if the scheme is ``http(s)`` +and as such allowed ``javascript:...`` URLs to be entered. If a developer +relied on ``is_safe_url()`` to provide safe redirect targets and put such a +URL into a link, he could suffer from a XSS attack. This bug doesn't affect +Django currently, since we only put this URL into the ``Location`` response +header and browsers seem to ignore JavaScript there. + +XSS vulnerability in :mod:`django.contrib.admin` +------------------------------------------------ + +If a :class:`~django.db.models.URLField` is used in Django 1.5, it displays the +current value of the field and a link to the target on the admin change page. +The display routine of this widget was flawed and allowed for XSS. + +Bugfixes +======== + +* Fixed a crash with :meth:`~django.db.models.query.QuerySet.prefetch_related` + (#19607) as well as some ``pickle`` regressions with ``prefetch_related`` + (#20157 and #20257). +* Fixed a regression in :mod:`django.contrib.gis` in the Google Map output on + Python 3 (#20773). +* Made ``DjangoTestSuiteRunner.setup_databases`` properly handle aliases for + the default database (#19940) and prevented ``teardown_databases`` from + attempting to tear down aliases (#20681). +* Fixed the ``django.core.cache.backends.memcached.MemcachedCache`` backend's + ``get_many()`` method on Python 3 (#20722). +* Fixed :mod:`django.contrib.humanize` translation syntax errors. Affected + languages: Mexican Spanish, Mongolian, Romanian, Turkish (#20695). +* Added support for wheel packages (#19252). +* The CSRF token now rotates when a user logs in. +* Some Python 3 compatibility fixes including #20212 and #20025. +* Fixed some rare cases where :meth:`~django.db.models.query.QuerySet.get` + exceptions recursed infinitely (#20278). +* :djadmin:`makemessages` no longer crashes with ``UnicodeDecodeError`` + (#20354). +* Fixed ``geojson`` detection with Spatialite. +* :meth:`~django.test.SimpleTestCase.assertContains` once again works with + binary content (#20237). +* Fixed :class:`~django.db.models.ManyToManyField` if it has a unicode ``name`` + parameter (#20207). +* Ensured that the WSGI request's path is correctly based on the + ``SCRIPT_NAME`` environment variable or the :setting:`FORCE_SCRIPT_NAME` + setting, regardless of whether or not either has a trailing slash (#20169). +* Fixed an obscure bug with the :func:`~django.test.utils.override_settings` + decorator. If you hit an ``AttributeError: 'Settings' object has no attribute + '_original_allowed_hosts'`` exception, it's probably fixed (#20636). diff --git a/docs/releases/index.txt b/docs/releases/index.txt index 98598209cf..c3ba5cf478 100644 --- a/docs/releases/index.txt +++ b/docs/releases/index.txt @@ -36,6 +36,7 @@ Final releases .. toctree:: :maxdepth: 1 + 1.5.2 1.5.1 1.5 @@ -44,6 +45,7 @@ Final releases .. toctree:: :maxdepth: 1 + 1.4.6 1.4.5 1.4.4 1.4.3 From 907ef9d0d157c47c66bf265dca93a0bee8664ea3 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Tue, 4 Jun 2013 22:41:49 +0200 Subject: [PATCH 21/47] Fixed #20555 -- Make subwidget id attribute available In `BoundField.__iter__`, the widget's id attribute is now passed to each subwidget. A new id_for_label property was added to ChoiceInput. --- django/forms/forms.py | 4 +- django/forms/widgets.py | 15 +++--- docs/ref/forms/widgets.txt | 65 ++++++++++++++++--------- docs/releases/1.7.txt | 7 +++ tests/forms_tests/tests/test_widgets.py | 16 ++++++ 5 files changed, 75 insertions(+), 32 deletions(-) diff --git a/django/forms/forms.py b/django/forms/forms.py index c2b700ce77..ec51507981 100644 --- a/django/forms/forms.py +++ b/django/forms/forms.py @@ -434,7 +434,9 @@ class BoundField(object): This really is only useful for RadioSelect widgets, so that you can iterate over individual radio buttons in a template. """ - for subwidget in self.field.widget.subwidgets(self.html_name, self.value()): + id_ = self.field.widget.attrs.get('id') or self.auto_id + attrs = {'id': id_} if id_ else {} + for subwidget in self.field.widget.subwidgets(self.html_name, self.value(), attrs): yield subwidget def __len__(self): diff --git a/django/forms/widgets.py b/django/forms/widgets.py index 0a5059a9c2..98d47d0b00 100644 --- a/django/forms/widgets.py +++ b/django/forms/widgets.py @@ -601,16 +601,15 @@ class ChoiceInput(SubWidget): self.choice_value = force_text(choice[0]) self.choice_label = force_text(choice[1]) self.index = index + if 'id' in self.attrs: + self.attrs['id'] += "_%d" % self.index def __str__(self): return self.render() def render(self, name=None, value=None, attrs=None, choices=()): - name = name or self.name - value = value or self.value - attrs = attrs or self.attrs - if 'id' in self.attrs: - label_for = format_html(' for="{0}_{1}"', self.attrs['id'], self.index) + if self.id_for_label: + label_for = format_html(' for="{0}"', self.id_for_label) else: label_for = '' return format_html('{1} {2}', label_for, self.tag(), self.choice_label) @@ -619,13 +618,15 @@ class ChoiceInput(SubWidget): return self.value == self.choice_value def tag(self): - if 'id' in self.attrs: - self.attrs['id'] = '%s_%s' % (self.attrs['id'], self.index) final_attrs = dict(self.attrs, type=self.input_type, name=self.name, value=self.choice_value) if self.is_checked(): final_attrs['checked'] = 'checked' return format_html('', flatatt(final_attrs)) + @property + def id_for_label(self): + return self.attrs.get('id', '') + class RadioChoiceInput(ChoiceInput): input_type = 'radio' diff --git a/docs/ref/forms/widgets.txt b/docs/ref/forms/widgets.txt index 080d1fea86..951fbfa310 100644 --- a/docs/ref/forms/widgets.txt +++ b/docs/ref/forms/widgets.txt @@ -590,25 +590,26 @@ Selector and checkbox widgets .. code-block:: html
- +
- +
- +
- +
That included the ``