Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Upgrade Django 1.4.5

  • Loading branch information...
commit 3c69523c12766ab714e2bfc7fb7308dfa0a6e841 1 parent 9f29294
@camd camd authored
Showing with 386 additions and 118 deletions.
  1. +1 −1  lib/python/django/__init__.py
  2. +4 −0 lib/python/django/conf/global_settings.py
  3. +4 −0 lib/python/django/conf/project_template/project_name/settings.py
  4. +2 −2 lib/python/django/contrib/admin/filters.py
  5. +8 −2 lib/python/django/contrib/admin/options.py
  6. +4 −1 lib/python/django/contrib/auth/hashers.py
  7. +5 −0 lib/python/django/contrib/auth/tests/hashers.py
  8. +5 −0 lib/python/django/contrib/auth/tests/views.py
  9. +22 −29 lib/python/django/contrib/auth/views.py
  10. +4 −7 lib/python/django/contrib/comments/views/comments.py
  11. +3 −4 lib/python/django/contrib/comments/views/moderation.py
  12. +6 −4 lib/python/django/contrib/comments/views/utils.py
  13. +2 −0  lib/python/django/contrib/contenttypes/tests.py
  14. +1 −23 lib/python/django/contrib/gis/db/backends/oracle/compiler.py
  15. +10 −0 lib/python/django/contrib/gis/db/backends/oracle/operations.py
  16. +20 −16 lib/python/django/contrib/gis/tests/inspectapp/tests.py
  17. +2 −0  lib/python/django/contrib/sites/tests.py
  18. +94 −1 lib/python/django/core/serializers/xml_serializer.py
  19. +8 −2 lib/python/django/db/__init__.py
  20. +17 −0 lib/python/django/db/backends/__init__.py
  21. +1 −1  lib/python/django/db/backends/mysql/base.py
  22. +1 −1  lib/python/django/db/backends/sqlite3/base.py
  23. +38 −9 lib/python/django/db/models/fields/related.py
  24. +2 −0  lib/python/django/db/models/sql/compiler.py
  25. +15 −0 lib/python/django/db/transaction.py
  26. +10 −2 lib/python/django/forms/formsets.py
  27. +48 −5 lib/python/django/http/__init__.py
  28. +20 −1 lib/python/django/middleware/transaction.py
  29. +3 −0  lib/python/django/test/testcases.py
  30. +6 −0 lib/python/django/test/utils.py
  31. +1 −2  lib/python/django/utils/html_parser.py
  32. +12 −0 lib/python/django/utils/http.py
  33. +7 −5 lib/python/django/views/i18n.py
View
2  lib/python/django/__init__.py
@@ -1,4 +1,4 @@
-VERSION = (1, 4, 2, 'final', 0)
+VERSION = (1, 4, 5, 'final', 0)
def get_version(version=None):
"""Derives a PEP386-compliant version number from VERSION."""
View
4 lib/python/django/conf/global_settings.py
@@ -29,6 +29,10 @@
# * Receive x-headers
INTERNAL_IPS = ()
+# Hosts/domain names that are valid for this site.
+# "*" matches anything, ".example.com" matches example.com and all subdomains
+ALLOWED_HOSTS = ['*']
+
# Local time zone for this installation. All choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name (although not all
# systems may support all possibilities). When USE_TZ is True, this is
View
4 lib/python/django/conf/project_template/project_name/settings.py
@@ -20,6 +20,10 @@
}
}
+# Hosts/domain names that are valid for this site; required if DEBUG is False
+# See https://docs.djangoproject.com/en/1.4/ref/settings/#allowed-hosts
+ALLOWED_HOSTS = []
+
# Local time zone for this installation. Choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# although not all choices may be available on all operating systems.
View
4 lib/python/django/contrib/admin/filters.py
@@ -9,7 +9,7 @@
from django.db import models
from django.core.exceptions import ImproperlyConfigured, ValidationError
-from django.utils.encoding import smart_unicode
+from django.utils.encoding import smart_unicode, force_unicode
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from django.contrib.admin.util import (get_model_from_relation,
@@ -102,7 +102,7 @@ def choices(self, cl):
}
for lookup, title in self.lookup_choices:
yield {
- 'selected': self.value() == lookup,
+ 'selected': self.value() == force_unicode(lookup),
'query_string': cl.get_query_string({
self.parameter_name: lookup,
}, []),
View
10 lib/python/django/contrib/admin/options.py
@@ -1317,15 +1317,21 @@ def delete_view(self, request, object_id, extra_context=None):
def history_view(self, request, object_id, extra_context=None):
"The 'history' admin view for this model."
from django.contrib.admin.models import LogEntry
+ # First check if the user can see this history.
model = self.model
+ obj = get_object_or_404(model, pk=unquote(object_id))
+
+ if not self.has_change_permission(request, obj):
+ raise PermissionDenied
+
+ # Then get the history for this object.
opts = model._meta
app_label = opts.app_label
action_list = LogEntry.objects.filter(
object_id = object_id,
content_type__id__exact = ContentType.objects.get_for_model(model).id
).select_related().order_by('action_time')
- # If no history was found, see whether this object even exists.
- obj = get_object_or_404(model, pk=unquote(object_id))
+
context = {
'title': _('Change history: %s') % force_unicode(obj),
'action_list': action_list,
View
5 lib/python/django/contrib/auth/hashers.py
@@ -35,7 +35,8 @@ def check_password(password, encoded, setter=None, preferred='default'):
password = smart_str(password)
encoded = smart_str(encoded)
- if len(encoded) == 32 and '$' not in encoded:
+ if ((len(encoded) == 32 and '$' not in encoded) or
+ (len(encoded) == 37 and encoded.startswith('md5$$'))):
hasher = get_hasher('unsalted_md5')
else:
algorithm = encoded.split('$', 1)[0]
@@ -347,6 +348,8 @@ def encode(self, password, salt):
return hashlib.md5(password).hexdigest()
def verify(self, password, encoded):
+ if len(encoded) == 37 and encoded.startswith('md5$$'):
+ encoded = encoded[5:]
encoded_2 = self.encode(password, '')
return constant_time_compare(encoded, encoded_2)
View
5 lib/python/django/contrib/auth/tests/hashers.py
@@ -59,6 +59,11 @@ def test_unsalted_md5(self):
self.assertTrue(is_password_usable(encoded))
self.assertTrue(check_password(u'letmein', encoded))
self.assertFalse(check_password('letmeinz', encoded))
+ # Alternate unsalted syntax
+ alt_encoded = "md5$$%s" % encoded
+ self.assertTrue(is_password_usable(alt_encoded))
+ self.assertTrue(check_password(u'letmein', alt_encoded))
+ self.assertFalse(check_password('letmeinz', alt_encoded))
@skipUnless(crypt, "no crypt module to generate password.")
def test_crypt(self):
View
5 lib/python/django/contrib/auth/tests/views.py
@@ -107,6 +107,7 @@ def test_email_found_custom_from(self):
self.assertEqual(len(mail.outbox), 1)
self.assertEqual("staffmember@example.com", mail.outbox[0].from_email)
+ @override_settings(ALLOWED_HOSTS=['adminsite.com'])
def test_admin_reset(self):
"If the reset view is marked as being for admin, the HTTP_HOST header is used for a domain override."
response = self.client.post('/admin_password_reset/',
@@ -118,6 +119,8 @@ def test_admin_reset(self):
self.assertTrue("http://adminsite.com" in mail.outbox[0].body)
self.assertEqual(settings.DEFAULT_FROM_EMAIL, mail.outbox[0].from_email)
+ # Skip any 500 handler action (like sending more mail...)
+ @override_settings(DEBUG_PROPAGATE_EXCEPTIONS=True)
def test_poisoned_http_host(self):
"Poisoned HTTP_HOST headers can't be used for reset emails"
# This attack is based on the way browsers handle URLs. The colon
@@ -134,6 +137,8 @@ def test_poisoned_http_host(self):
)
self.assertEqual(len(mail.outbox), 0)
+ # Skip any 500 handler action (like sending more mail...)
+ @override_settings(DEBUG_PROPAGATE_EXCEPTIONS=True)
def test_poisoned_http_host_admin_site(self):
"Poisoned HTTP_HOST headers can't be used for reset emails on admin views"
with self.assertRaises(SuspiciousOperation):
View
51 lib/python/django/contrib/auth/views.py
@@ -4,7 +4,7 @@
from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect, QueryDict
from django.template.response import TemplateResponse
-from django.utils.http import base36_to_int
+from django.utils.http import base36_to_int, is_safe_url
from django.utils.translation import ugettext as _
from django.views.decorators.debug import sensitive_post_parameters
from django.views.decorators.cache import never_cache
@@ -34,18 +34,11 @@ def login(request, template_name='registration/login.html',
if request.method == "POST":
form = authentication_form(data=request.POST)
if form.is_valid():
- netloc = urlparse.urlparse(redirect_to)[1]
-
- # Use default setting if redirect_to is empty
- if not redirect_to:
- redirect_to = settings.LOGIN_REDIRECT_URL
-
- # Heavier security check -- don't allow redirection to a different
- # host.
- elif netloc and netloc != request.get_host():
+ # Ensure the user-originating redirection url is safe.
+ if not is_safe_url(url=redirect_to, host=request.get_host()):
redirect_to = settings.LOGIN_REDIRECT_URL
- # Okay, security checks complete. Log the user in.
+ # Okay, security check complete. Log the user in.
auth_login(request, form.get_user())
if request.session.test_cookie_worked():
@@ -78,27 +71,27 @@ def logout(request, next_page=None,
Logs out the user and displays 'You are logged out' message.
"""
auth_logout(request)
- redirect_to = request.REQUEST.get(redirect_field_name, '')
- if redirect_to:
- netloc = urlparse.urlparse(redirect_to)[1]
+
+ if redirect_field_name in request.REQUEST:
+ next_page = request.REQUEST[redirect_field_name]
# Security check -- don't allow redirection to a different host.
- if not (netloc and netloc != request.get_host()):
- return HttpResponseRedirect(redirect_to)
+ if not is_safe_url(url=next_page, host=request.get_host()):
+ next_page = request.path
- if next_page is None:
- current_site = get_current_site(request)
- context = {
- 'site': current_site,
- 'site_name': current_site.name,
- 'title': _('Logged out')
- }
- if extra_context is not None:
- context.update(extra_context)
- return TemplateResponse(request, template_name, context,
- current_app=current_app)
- else:
+ if next_page:
# Redirect to this page until the session has been cleared.
- return HttpResponseRedirect(next_page or request.path)
+ return HttpResponseRedirect(next_page)
+
+ current_site = get_current_site(request)
+ context = {
+ 'site': current_site,
+ 'site_name': current_site.name,
+ 'title': _('Logged out')
+ }
+ if extra_context is not None:
+ context.update(extra_context)
+ return TemplateResponse(request, template_name, context,
+ current_app=current_app)
def logout_then_login(request, login_url=None, current_app=None, extra_context=None):
"""
View
11 lib/python/django/contrib/comments/views/comments.py
@@ -44,9 +44,6 @@ def post_comment(request, next=None, using=None):
if not data.get('email', ''):
data["email"] = request.user.email
- # Check to see if the POST data overrides the view's next argument.
- next = data.get("next", next)
-
# Look up the object we're trying to comment about
ctype = data.get("content_type")
object_pk = data.get("object_pk")
@@ -98,9 +95,9 @@ def post_comment(request, next=None, using=None):
]
return render_to_response(
template_list, {
- "comment" : form.data.get("comment", ""),
- "form" : form,
- "next": next,
+ "comment": form.data.get("comment", ""),
+ "form": form,
+ "next": data.get("next", next),
},
RequestContext(request, {})
)
@@ -131,7 +128,7 @@ def post_comment(request, next=None, using=None):
request = request
)
- return next_redirect(data, next, comment_done, c=comment._get_pk_val())
+ return next_redirect(request, next, comment_done, c=comment._get_pk_val())
comment_done = confirmation_view(
template = "comments/posted.html",
View
7 lib/python/django/contrib/comments/views/moderation.py
@@ -10,7 +10,6 @@
from django.views.decorators.csrf import csrf_protect
-
@csrf_protect
@login_required
def flag(request, comment_id, next=None):
@@ -27,7 +26,7 @@ def flag(request, comment_id, next=None):
# Flag on POST
if request.method == 'POST':
perform_flag(request, comment)
- return next_redirect(request.POST.copy(), next, flag_done, c=comment.pk)
+ return next_redirect(request, next, flag_done, c=comment.pk)
# Render a form on GET
else:
@@ -54,7 +53,7 @@ def delete(request, comment_id, next=None):
if request.method == 'POST':
# Flag the comment as deleted instead of actually deleting it.
perform_delete(request, comment)
- return next_redirect(request.POST.copy(), next, delete_done, c=comment.pk)
+ return next_redirect(request, next, delete_done, c=comment.pk)
# Render a form on GET
else:
@@ -81,7 +80,7 @@ def approve(request, comment_id, next=None):
if request.method == 'POST':
# Flag the comment as approved.
perform_approve(request, comment)
- return next_redirect(request.POST.copy(), next, approve_done, c=comment.pk)
+ return next_redirect(request, next, approve_done, c=comment.pk)
# Render a form on GET
else:
View
10 lib/python/django/contrib/comments/views/utils.py
@@ -4,14 +4,15 @@
import urllib
import textwrap
-from django.http import HttpResponseRedirect
from django.core import urlresolvers
+from django.http import HttpResponseRedirect
from django.shortcuts import render_to_response
from django.template import RequestContext
from django.core.exceptions import ObjectDoesNotExist
from django.contrib import comments
+from django.utils.http import is_safe_url
-def next_redirect(data, default, default_view, **get_kwargs):
+def next_redirect(request, default, default_view, **get_kwargs):
"""
Handle the "where should I go next?" part of comment views.
@@ -21,9 +22,10 @@ def next_redirect(data, default, default_view, **get_kwargs):
Returns an ``HttpResponseRedirect``.
"""
- next = data.get("next", default)
- if next is None:
+ next = request.POST.get('next', default)
+ if not is_safe_url(url=next, host=request.get_host()):
next = urlresolvers.reverse(default_view)
+
if get_kwargs:
if '#' in next:
tmp = next.rsplit('#', 1)
View
2  lib/python/django/contrib/contenttypes/tests.py
@@ -9,6 +9,7 @@
from django.http import HttpRequest, Http404
from django.test import TestCase
from django.utils.encoding import smart_str
+from django.test.utils import override_settings
class FooWithoutUrl(models.Model):
@@ -114,6 +115,7 @@ def test_get_for_models_full_cache(self):
FooWithUrl: ContentType.objects.get_for_model(FooWithUrl),
})
+ @override_settings(ALLOWED_HOSTS=['example.com'])
def test_shortcut_view(self):
"""
Check that the shortcut view (used for the admin "view on site"
View
24 lib/python/django/contrib/gis/db/backends/oracle/compiler.py
@@ -7,29 +7,7 @@ class GeoSQLCompiler(BaseGeoSQLCompiler, SQLCompiler):
pass
class SQLInsertCompiler(compiler.SQLInsertCompiler, GeoSQLCompiler):
- def placeholder(self, field, val):
- if field is None:
- # A field value of None means the value is raw.
- return val
- elif hasattr(field, 'get_placeholder'):
- # Some fields (e.g. geo fields) need special munging before
- # they can be inserted.
- ph = field.get_placeholder(val, self.connection)
- if ph == 'NULL':
- # If the placeholder returned is 'NULL', then we need to
- # to remove None from the Query parameters. Specifically,
- # cx_Oracle will assume a CHAR type when a placeholder ('%s')
- # is used for columns of MDSYS.SDO_GEOMETRY. Thus, we use
- # 'NULL' for the value, and remove None from the query params.
- # See also #10888.
- param_idx = self.query.columns.index(field.column)
- params = list(self.query.params)
- params.pop(param_idx)
- self.query.params = tuple(params)
- return ph
- else:
- # Return the common case for the placeholder
- return '%s'
+ pass
class SQLDeleteCompiler(compiler.SQLDeleteCompiler, GeoSQLCompiler):
pass
View
10 lib/python/django/contrib/gis/db/backends/oracle/operations.py
@@ -9,6 +9,7 @@
"""
import re
from decimal import Decimal
+from itertools import izip
from django.db.backends.oracle.base import DatabaseOperations
from django.contrib.gis.db.backends.base import BaseSpatialOperations
@@ -287,3 +288,12 @@ def geometry_columns(self):
def spatial_ref_sys(self):
from django.contrib.gis.db.backends.oracle.models import SpatialRefSys
return SpatialRefSys
+
+ def modify_insert_params(self, placeholders, params):
+ """Drop out insert parameters for NULL placeholder. Needed for Oracle Spatial
+ backend due to #10888
+ """
+ # This code doesn't work for bulk insert cases.
+ assert len(placeholders) == 1
+ return [[param for pholder,param
+ in izip(placeholders[0], params[0]) if pholder != 'NULL'], ]
View
36 lib/python/django/contrib/gis/tests/inspectapp/tests.py
@@ -68,23 +68,27 @@ def test_time_field(self):
layer_key=AllOGRFields._meta.db_table,
decimal=['f_decimal'])
- expected = [
- '# This is an auto-generated Django model module created by ogrinspect.',
- 'from django.contrib.gis.db import models',
- '',
- 'class Measurement(models.Model):',
- ' f_decimal = models.DecimalField(max_digits=0, decimal_places=0)',
- ' f_int = models.IntegerField()',
- ' f_datetime = models.DateTimeField()',
- ' f_time = models.TimeField()',
- ' f_float = models.FloatField()',
- ' f_char = models.CharField(max_length=10)',
- ' f_date = models.DateField()',
- ' geom = models.PolygonField()',
- ' objects = models.GeoManager()',
- ]
+ self.assertTrue(model_def.startswith(
+ '# This is an auto-generated Django model module created by ogrinspect.\n'
+ 'from django.contrib.gis.db import models\n'
+ '\n'
+ 'class Measurement(models.Model):\n'
+ ))
+
+ # The ordering of model fields might vary depending on several factors (version of GDAL, etc.)
+ self.assertIn(' f_decimal = models.DecimalField(max_digits=0, decimal_places=0)', model_def)
+ self.assertIn(' f_int = models.IntegerField()', model_def)
+ self.assertIn(' f_datetime = models.DateTimeField()', model_def)
+ self.assertIn(' f_time = models.TimeField()', model_def)
+ self.assertIn(' f_float = models.FloatField()', model_def)
+ self.assertIn(' f_char = models.CharField(max_length=10)', model_def)
+ self.assertIn(' f_date = models.DateField()', model_def)
+
+ self.assertTrue(model_def.endswith(
+ ' geom = models.PolygonField()\n'
+ ' objects = models.GeoManager()'
+ ))
- self.assertEqual(model_def, '\n'.join(expected))
def get_ogr_db_string():
# Construct the DB string that GDAL will use to inspect the database.
View
2  lib/python/django/contrib/sites/tests.py
@@ -3,6 +3,7 @@
from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpRequest
from django.test import TestCase
+from django.test.utils import override_settings
class SitesFrameworkTests(TestCase):
@@ -39,6 +40,7 @@ def test_site_cache(self):
site = Site.objects.get_current()
self.assertEqual(u"Example site", site.name)
+ @override_settings(ALLOWED_HOSTS=['example.com'])
def test_get_current_site(self):
# Test that the correct Site object is returned
request = HttpRequest()
View
95 lib/python/django/core/serializers/xml_serializer.py
@@ -8,6 +8,8 @@
from django.utils.xmlutils import SimplerXMLGenerator
from django.utils.encoding import smart_unicode
from xml.dom import pulldom
+from xml.sax import handler
+from xml.sax.expatreader import ExpatParser as _ExpatParser
class Serializer(base.Serializer):
"""
@@ -149,9 +151,13 @@ class Deserializer(base.Deserializer):
def __init__(self, stream_or_string, **options):
super(Deserializer, self).__init__(stream_or_string, **options)
- self.event_stream = pulldom.parse(self.stream)
+ self.event_stream = pulldom.parse(self.stream, self._make_parser())
self.db = options.pop('using', DEFAULT_DB_ALIAS)
+ def _make_parser(self):
+ """Create a hardened XML parser (no custom/external entities)."""
+ return DefusedExpatParser()
+
def next(self):
for event, node in self.event_stream:
if event == "START_ELEMENT" and node.nodeName == "object":
@@ -290,3 +296,90 @@ def getInnerText(node):
else:
pass
return u"".join(inner_text)
+
+
+# Below code based on Christian Heimes' defusedxml
+
+
+class DefusedExpatParser(_ExpatParser):
+ """
+ An expat parser hardened against XML bomb attacks.
+
+ Forbids DTDs, external entity references
+
+ """
+ def __init__(self, *args, **kwargs):
+ _ExpatParser.__init__(self, *args, **kwargs)
+ self.setFeature(handler.feature_external_ges, False)
+ self.setFeature(handler.feature_external_pes, False)
+
+ def start_doctype_decl(self, name, sysid, pubid, has_internal_subset):
+ raise DTDForbidden(name, sysid, pubid)
+
+ def entity_decl(self, name, is_parameter_entity, value, base,
+ sysid, pubid, notation_name):
+ raise EntitiesForbidden(name, value, base, sysid, pubid, notation_name)
+
+ def unparsed_entity_decl(self, name, base, sysid, pubid, notation_name):
+ # expat 1.2
+ raise EntitiesForbidden(name, None, base, sysid, pubid, notation_name)
+
+ def external_entity_ref_handler(self, context, base, sysid, pubid):
+ raise ExternalReferenceForbidden(context, base, sysid, pubid)
+
+ def reset(self):
+ _ExpatParser.reset(self)
+ parser = self._parser
+ parser.StartDoctypeDeclHandler = self.start_doctype_decl
+ parser.EntityDeclHandler = self.entity_decl
+ parser.UnparsedEntityDeclHandler = self.unparsed_entity_decl
+ parser.ExternalEntityRefHandler = self.external_entity_ref_handler
+
+
+class DefusedXmlException(ValueError):
+ """Base exception."""
+ def __repr__(self):
+ return str(self)
+
+
+class DTDForbidden(DefusedXmlException):
+ """Document type definition is forbidden."""
+ def __init__(self, name, sysid, pubid):
+ super(DTDForbidden, self).__init__()
+ self.name = name
+ self.sysid = sysid
+ self.pubid = pubid
+
+ def __str__(self):
+ tpl = "DTDForbidden(name='{}', system_id={!r}, public_id={!r})"
+ return tpl.format(self.name, self.sysid, self.pubid)
+
+
+class EntitiesForbidden(DefusedXmlException):
+ """Entity definition is forbidden."""
+ def __init__(self, name, value, base, sysid, pubid, notation_name):
+ super(EntitiesForbidden, self).__init__()
+ self.name = name
+ self.value = value
+ self.base = base
+ self.sysid = sysid
+ self.pubid = pubid
+ self.notation_name = notation_name
+
+ def __str__(self):
+ tpl = "EntitiesForbidden(name='{}', system_id={!r}, public_id={!r})"
+ return tpl.format(self.name, self.sysid, self.pubid)
+
+
+class ExternalReferenceForbidden(DefusedXmlException):
+ """Resolving an external reference is forbidden."""
+ def __init__(self, context, base, sysid, pubid):
+ super(ExternalReferenceForbidden, self).__init__()
+ self.context = context
+ self.base = base
+ self.sysid = sysid
+ self.pubid = pubid
+
+ def __str__(self):
+ tpl = "ExternalReferenceForbidden(system_id='{}', public_id={})"
+ return tpl.format(self.sysid, self.pubid)
View
10 lib/python/django/db/__init__.py
@@ -42,8 +42,14 @@ def __setattr__(self, name, value):
# Register an event that closes the database connection
# when a Django request is finished.
def close_connection(**kwargs):
- for conn in connections.all():
- conn.close()
+ # Avoid circular imports
+ from django.db import transaction
+ for conn in connections:
+ # If an error happens here the connection will be left in broken
+ # state. Once a good db connection is again available, the
+ # connection state will be cleaned up.
+ transaction.abort(conn)
+ connections[conn].close()
signals.request_finished.connect(close_connection)
# Register an event that resets connection.queries
View
17 lib/python/django/db/backends/__init__.py
@@ -83,6 +83,17 @@ def _savepoint_commit(self, sid):
return
self.cursor().execute(self.ops.savepoint_commit_sql(sid))
+ def abort(self):
+ """
+ Roll back any ongoing transaction and clean the transaction state
+ stack.
+ """
+ if self._dirty:
+ self._rollback()
+ self._dirty = False
+ while self.transaction_state:
+ self.leave_transaction_management()
+
def enter_transaction_management(self, managed=True):
"""
Enters transaction management for a running thread. It must be balanced with
@@ -874,6 +885,12 @@ def combine_expression(self, connector, sub_expressions):
conn = ' %s ' % connector
return conn.join(sub_expressions)
+ def modify_insert_params(self, placeholders, params):
+ """Allow modification of insert parameters. Needed for Oracle Spatial
+ backend due to #10888.
+ """
+ return params
+
class BaseDatabaseIntrospection(object):
"""
This class encapsulates all backend-specific introspection utilities
View
2  lib/python/django/db/backends/mysql/base.py
@@ -177,7 +177,7 @@ def _mysql_storage_engine(self):
# will tell you the default table type of the created
# table. Since all Django's test tables will have the same
# table type, that's enough to evaluate the feature.
- cursor.execute("SHOW TABLE STATUS WHERE Name='INTROSPECT_TEST'")
+ cursor.execute("SHOW TABLE STATUS LIKE 'INTROSPECT_TEST'")
result = cursor.fetchone()
cursor.execute('DROP TABLE INTROSPECT_TEST')
self._storage_engine = result[1]
View
2  lib/python/django/db/backends/sqlite3/base.py
@@ -215,7 +215,7 @@ def bulk_insert_sql(self, fields, num_values):
res.append("SELECT %s" % ", ".join(
"%%s AS %s" % self.quote_name(f.column) for f in fields
))
- res.extend(["UNION SELECT %s" % ", ".join(["%s"] * len(fields))] * (num_values - 1))
+ res.extend(["UNION ALL SELECT %s" % ", ".join(["%s"] * len(fields))] * (num_values - 1))
return " ".join(res)
class DatabaseWrapper(BaseDatabaseWrapper):
View
47 lib/python/django/db/models/fields/related.py
@@ -531,9 +531,33 @@ def __init__(self, model=None, query_field_name=None, instance=None, symmetrical
self.reverse = reverse
self.through = through
self.prefetch_cache_name = prefetch_cache_name
- self._pk_val = self.instance.pk
- if self._pk_val is None:
- raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % instance.__class__.__name__)
+ self._fk_val = self._get_fk_val(instance, source_field_name)
+ if self._fk_val is None:
+ raise ValueError('"%r" needs to have a value for field "%s" before '
+ 'this many-to-many relationship can be used.' %
+ (instance, source_field_name))
+ # Even if this relation is not to pk, we require still pk value.
+ # The wish is that the instance has been already saved to DB,
+ # although having a pk value isn't a guarantee of that.
+ if instance.pk is None:
+ raise ValueError("%r instance needs to have a primary key value before "
+ "a many-to-many relationship can be used." %
+ instance.__class__.__name__)
+
+ def _get_fk_val(self, obj, field_name):
+ """
+ Returns the correct value for this relationship's foreign key. This
+ might be something else than pk value when to_field is used.
+ """
+ if not self.through:
+ # Make custom m2m fields with no through model defined usable.
+ return obj.pk
+ fk = self.through._meta.get_field(field_name)
+ if fk.rel.field_name and fk.rel.field_name != fk.rel.to._meta.pk.attname:
+ attname = fk.rel.get_related_field().get_attname()
+ return fk.get_prep_lookup('exact', getattr(obj, attname))
+ else:
+ return obj.pk
def get_query_set(self):
try:
@@ -635,7 +659,11 @@ def _add_items(self, source_field_name, target_field_name, *objs):
if not router.allow_relation(obj, self.instance):
raise ValueError('Cannot add "%r": instance is on database "%s", value is on database "%s"' %
(obj, self.instance._state.db, obj._state.db))
- new_ids.add(obj.pk)
+ fk_val = self._get_fk_val(obj, target_field_name)
+ if fk_val is None:
+ raise ValueError('Cannot add "%r": the value for field "%s" is None' %
+ (obj, target_field_name))
+ new_ids.add(self._get_fk_val(obj, target_field_name))
elif isinstance(obj, Model):
raise TypeError("'%s' instance expected, got %r" % (self.model._meta.object_name, obj))
else:
@@ -643,7 +671,7 @@ def _add_items(self, source_field_name, target_field_name, *objs):
db = router.db_for_write(self.through, instance=self.instance)
vals = self.through._default_manager.using(db).values_list(target_field_name, flat=True)
vals = vals.filter(**{
- source_field_name: self._pk_val,
+ source_field_name: self._fk_val,
'%s__in' % target_field_name: new_ids,
})
new_ids = new_ids - set(vals)
@@ -657,11 +685,12 @@ def _add_items(self, source_field_name, target_field_name, *objs):
# Add the ones that aren't there already
self.through._default_manager.using(db).bulk_create([
self.through(**{
- '%s_id' % source_field_name: self._pk_val,
+ '%s_id' % source_field_name: self._fk_val,
'%s_id' % target_field_name: obj_id,
})
for obj_id in new_ids
])
+
if self.reverse or source_field_name == self.source_field_name:
# Don't send the signal when we are inserting the
# duplicate data row for symmetrical reverse entries.
@@ -680,7 +709,7 @@ def _remove_items(self, source_field_name, target_field_name, *objs):
old_ids = set()
for obj in objs:
if isinstance(obj, self.model):
- old_ids.add(obj.pk)
+ old_ids.add(self._get_fk_val(obj, target_field_name))
else:
old_ids.add(obj)
# Work out what DB we're operating on
@@ -694,7 +723,7 @@ def _remove_items(self, source_field_name, target_field_name, *objs):
model=self.model, pk_set=old_ids, using=db)
# Remove the specified objects from the join table
self.through._default_manager.using(db).filter(**{
- source_field_name: self._pk_val,
+ source_field_name: self._fk_val,
'%s__in' % target_field_name: old_ids
}).delete()
if self.reverse or source_field_name == self.source_field_name:
@@ -714,7 +743,7 @@ def _clear_items(self, source_field_name):
instance=self.instance, reverse=self.reverse,
model=self.model, pk_set=None, using=db)
self.through._default_manager.using(db).filter(**{
- source_field_name: self._pk_val
+ source_field_name: self._fk_val
}).delete()
if self.reverse or source_field_name == self.source_field_name:
# Don't send the signal when we are clearing the
View
2  lib/python/django/db/models/sql/compiler.py
@@ -885,6 +885,8 @@ def as_sql(self):
[self.placeholder(field, v) for field, v in izip(fields, val)]
for val in values
]
+ # Oracle Spatial needs to remove some values due to #10888
+ params = self.connection.ops.modify_insert_params(placeholders, params)
if self.return_id and self.connection.features.can_return_id_from_insert:
params = params[0]
col = "%s.%s" % (qn(opts.db_table), qn(opts.pk.column))
View
15 lib/python/django/db/transaction.py
@@ -25,6 +25,21 @@ class TransactionManagementError(Exception):
"""
pass
+def abort(using=None):
+ """
+ Roll back any ongoing transactions and clean the transaction management
+ state of the connection.
+
+ This method is to be used only in cases where using balanced
+ leave_transaction_management() calls isn't possible. For example after a
+ request has finished, the transaction state isn't known, yet the connection
+ must be cleaned up for the next request.
+ """
+ if using is None:
+ using = DEFAULT_DB_ALIAS
+ connection = connections[using]
+ connection.abort()
+
def enter_transaction_management(managed=True, using=None):
"""
Enters transaction management for a running thread. It must be balanced with
View
12 lib/python/django/forms/formsets.py
@@ -19,6 +19,9 @@
ORDERING_FIELD_NAME = 'ORDER'
DELETION_FIELD_NAME = 'DELETE'
+# default maximum number of forms in a formset, to prevent memory exhaustion
+DEFAULT_MAX_NUM = 1000
+
class ManagementForm(Form):
"""
``ManagementForm`` is used to keep track of how many form instances
@@ -111,7 +114,7 @@ def initial_form_count(self):
def _construct_forms(self):
# instantiate all the forms and put them in self.forms
self.forms = []
- for i in xrange(self.total_form_count()):
+ for i in xrange(min(self.total_form_count(), self.absolute_max)):
self.forms.append(self._construct_form(i))
def _construct_form(self, i, **kwargs):
@@ -360,9 +363,14 @@ def as_ul(self):
def formset_factory(form, formset=BaseFormSet, extra=1, can_order=False,
can_delete=False, max_num=None):
"""Return a FormSet for the given form class."""
+ if max_num is None:
+ max_num = DEFAULT_MAX_NUM
+ # hard limit on forms instantiated, to prevent memory-exhaustion attacks
+ # limit defaults to DEFAULT_MAX_NUM, but developer can increase it via max_num
+ absolute_max = max(DEFAULT_MAX_NUM, max_num)
attrs = {'form': form, 'extra': extra,
'can_order': can_order, 'can_delete': can_delete,
- 'max_num': max_num}
+ 'max_num': max_num, 'absolute_max': absolute_max}
return type(form.__name__ + 'FormSet', (formset,), attrs)
def all_valid(formsets):
View
53 lib/python/django/http/__init__.py
@@ -126,6 +126,8 @@ def __init__(self, *args, **kwargs):
RESERVED_CHARS="!*'();:@&=+$,/?%#[]"
absolute_http_url_re = re.compile(r"^https?://", re.I)
+host_validation_re = re.compile(r"^([a-z0-9.-]+|\[[a-f0-9]*:[a-f0-9:]+\])(:\d+)?$")
+
class Http404(Exception):
pass
@@ -213,11 +215,12 @@ def get_host(self):
if server_port != (self.is_secure() and '443' or '80'):
host = '%s:%s' % (host, server_port)
- # Disallow potentially poisoned hostnames.
- if set(';/?@&=+$,').intersection(host):
- raise SuspiciousOperation('Invalid HTTP_HOST header: %s' % host)
-
- return host
+ allowed_hosts = ['*'] if settings.DEBUG else settings.ALLOWED_HOSTS
+ if validate_host(host, allowed_hosts):
+ return host
+ else:
+ raise SuspiciousOperation(
+ "Invalid HTTP_HOST header (you may need to set ALLOWED_HOSTS): %s" % host)
def get_full_path(self):
# RFC 3986 requires query string arguments to be in the ASCII range.
@@ -797,3 +800,43 @@ def str_to_unicode(s, encoding):
else:
return s
+def validate_host(host, allowed_hosts):
+ """
+ Validate the given host header value for this site.
+
+ Check that the host looks valid and matches a host or host pattern in the
+ given list of ``allowed_hosts``. Any pattern beginning with a period
+ matches a domain and all its subdomains (e.g. ``.example.com`` matches
+ ``example.com`` and any subdomain), ``*`` matches anything, and anything
+ else must match exactly.
+
+ Return ``True`` for a valid host, ``False`` otherwise.
+
+ """
+ # All validation is case-insensitive
+ host = host.lower()
+
+ # Basic sanity check
+ if not host_validation_re.match(host):
+ return False
+
+ # Validate only the domain part.
+ if host[-1] == ']':
+ # It's an IPv6 address without a port.
+ domain = host
+ else:
+ domain = host.rsplit(':', 1)[0]
+
+ for pattern in allowed_hosts:
+ pattern = pattern.lower()
+ match = (
+ pattern == '*' or
+ pattern.startswith('.') and (
+ domain.endswith(pattern) or domain == pattern[1:]
+ ) or
+ pattern == domain
+ )
+ if match:
+ return True
+
+ return False
View
21 lib/python/django/middleware/transaction.py
@@ -15,6 +15,10 @@ def process_request(self, request):
def process_exception(self, request, exception):
"""Rolls back the database and leaves transaction management"""
if transaction.is_dirty():
+ # This rollback might fail because of network failure for example.
+ # If rollback isn't possible it is impossible to clean the
+ # connection's state. So leave the connection in dirty state and
+ # let request_finished signal deal with cleaning the connection.
transaction.rollback()
transaction.leave_transaction_management()
@@ -22,6 +26,21 @@ def process_response(self, request, response):
"""Commits and leaves transaction management."""
if transaction.is_managed():
if transaction.is_dirty():
- transaction.commit()
+ # Note: it is possible that the commit fails. If the reason is
+ # closed connection or some similar reason, then there is
+ # little hope to proceed nicely. However, in some cases (
+ # deferred foreign key checks for exampl) it is still possible
+ # to rollback().
+ try:
+ transaction.commit()
+ except Exception:
+ # If the rollback fails, the transaction state will be
+ # messed up. It doesn't matter, the connection will be set
+ # to clean state after the request finishes. And, we can't
+ # clean the state here properly even if we wanted to, the
+ # connection is in transaction but we can't rollback...
+ transaction.rollback()
+ transaction.leave_transaction_management()
+ raise
transaction.leave_transaction_management()
return response
View
3  lib/python/django/test/testcases.py
@@ -63,6 +63,7 @@ def to_list(value):
real_enter_transaction_management = transaction.enter_transaction_management
real_leave_transaction_management = transaction.leave_transaction_management
real_managed = transaction.managed
+real_abort = transaction.abort
def nop(*args, **kwargs):
return
@@ -73,6 +74,7 @@ def disable_transaction_methods():
transaction.enter_transaction_management = nop
transaction.leave_transaction_management = nop
transaction.managed = nop
+ transaction.abort = nop
def restore_transaction_methods():
transaction.commit = real_commit
@@ -80,6 +82,7 @@ def restore_transaction_methods():
transaction.enter_transaction_management = real_enter_transaction_management
transaction.leave_transaction_management = real_leave_transaction_management
transaction.managed = real_managed
+ transaction.abort = real_abort
def assert_and_parse_html(self, html, user_msg, msg):
View
6 lib/python/django/test/utils.py
@@ -75,6 +75,9 @@ def setup_test_environment():
mail.original_email_backend = settings.EMAIL_BACKEND
settings.EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'
+ settings._original_allowed_hosts = settings.ALLOWED_HOSTS
+ settings.ALLOWED_HOSTS = ['*']
+
mail.outbox = []
deactivate()
@@ -93,6 +96,9 @@ def teardown_test_environment():
settings.EMAIL_BACKEND = mail.original_email_backend
del mail.original_email_backend
+ settings.ALLOWED_HOSTS = settings._original_allowed_hosts
+ del settings._original_allowed_hosts
+
del mail.outbox
View
3  lib/python/django/utils/html_parser.py
@@ -5,8 +5,7 @@
current_version = sys.version_info
use_workaround = (
- (current_version < (2, 6, 8)) or
- (current_version >= (2, 7) and current_version < (2, 7, 3)) or
+ (current_version < (2, 7, 3)) or
(current_version >= (3, 0) and current_version < (3, 2, 3))
)
View
12 lib/python/django/utils/http.py
@@ -224,3 +224,15 @@ def same_origin(url1, url2):
"""
p1, p2 = urlparse.urlparse(url1), urlparse.urlparse(url2)
return p1[0:2] == p2[0:2]
+
+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).
+
+ Always returns ``False`` on an empty url.
+ """
+ if not url:
+ return False
+ netloc = urlparse.urlparse(url)[1]
+ return not netloc or netloc == host
View
12 lib/python/django/views/i18n.py
@@ -8,6 +8,8 @@
from django.utils.text import javascript_quote
from django.utils.encoding import smart_unicode
from django.utils.formats import get_format_modules, get_format
+from django.utils.http import is_safe_url
+
def set_language(request):
"""
@@ -20,11 +22,11 @@ def set_language(request):
redirect to the page in the request (the 'next' parameter) without changing
any state.
"""
- next = request.REQUEST.get('next', None)
- if not next:
- next = request.META.get('HTTP_REFERER', None)
- if not next:
- next = '/'
+ next = request.REQUEST.get('next')
+ if not is_safe_url(url=next, host=request.get_host()):
+ next = request.META.get('HTTP_REFERER')
+ if not is_safe_url(url=next, host=request.get_host()):
+ next = '/'
response = http.HttpResponseRedirect(next)
if request.method == 'POST':
lang_code = request.POST.get('language', None)
Please sign in to comment.
Something went wrong with that request. Please try again.