diff --git a/django/contrib/admin/views/decorators.py b/django/contrib/admin/views/decorators.py
index fce50909f095..051f0ff7e385 100644
--- a/django/contrib/admin/views/decorators.py
+++ b/django/contrib/admin/views/decorators.py
@@ -3,43 +3,21 @@
from django.contrib.auth.models import User
from django.contrib.auth import authenticate, login
from django.shortcuts import render_to_response
+from django.utils.html import escape
from django.utils.translation import gettext_lazy
-import base64, datetime, md5
-import cPickle as pickle
+import base64, datetime
ERROR_MESSAGE = gettext_lazy("Please enter a correct username and password. Note that both fields are case-sensitive.")
LOGIN_FORM_KEY = 'this_is_the_login_form'
def _display_login_form(request, error_message=''):
request.session.set_test_cookie()
- if request.POST and request.POST.has_key('post_data'):
- # User has failed login BUT has previously saved post data.
- post_data = request.POST['post_data']
- elif request.POST:
- # User's session must have expired; save their post data.
- post_data = _encode_post_data(request.POST)
- else:
- post_data = _encode_post_data({})
return render_to_response('admin/login.html', {
'title': _('Log in'),
- 'app_path': request.path,
- 'post_data': post_data,
+ 'app_path': escape(request.path),
'error_message': error_message
}, context_instance=template.RequestContext(request))
-def _encode_post_data(post_data):
- pickled = pickle.dumps(post_data)
- pickled_md5 = md5.new(pickled + settings.SECRET_KEY).hexdigest()
- return base64.encodestring(pickled + pickled_md5)
-
-def _decode_post_data(encoded_data):
- encoded_data = base64.decodestring(encoded_data)
- pickled, tamper_check = encoded_data[:-32], encoded_data[-32:]
- if md5.new(pickled + settings.SECRET_KEY).hexdigest() != tamper_check:
- from django.core.exceptions import SuspiciousOperation
- raise SuspiciousOperation, "User may have tampered with session cookie."
- return pickle.loads(pickled)
-
def staff_member_required(view_func):
"""
Decorator for views that checks that the user is logged in and is a staff
@@ -48,10 +26,6 @@ def staff_member_required(view_func):
def _checklogin(request, *args, **kwargs):
if request.user.is_authenticated() and request.user.is_staff:
# The user is valid. Continue to the admin page.
- if request.POST.has_key('post_data'):
- # User must have re-authenticated through a different window
- # or tab.
- request.POST = _decode_post_data(request.POST['post_data'])
return view_func(request, *args, **kwargs)
assert hasattr(request, 'session'), "The Django admin requires session middleware to be installed. Edit your MIDDLEWARE_CLASSES setting to insert 'django.contrib.sessions.middleware.SessionMiddleware'."
@@ -59,7 +33,7 @@ def _checklogin(request, *args, **kwargs):
# If this isn't already the login page, display it.
if not request.POST.has_key(LOGIN_FORM_KEY):
if request.POST:
- message = _("Please log in again, because your session has expired. Don't worry: Your submission has been saved.")
+ message = _("Please log in again, because your session has expired.")
else:
message = ""
return _display_login_form(request, message)
@@ -92,16 +66,7 @@ def _checklogin(request, *args, **kwargs):
# TODO: set last_login with an event.
user.last_login = datetime.datetime.now()
user.save()
- if request.POST.has_key('post_data'):
- post_data = _decode_post_data(request.POST['post_data'])
- if post_data and not post_data.has_key(LOGIN_FORM_KEY):
- # overwrite request.POST with the saved post_data, and continue
- request.POST = post_data
- request.user = user
- return view_func(request, *args, **kwargs)
- else:
- request.session.delete_test_cookie()
- return http.HttpResponseRedirect(request.path)
+ return http.HttpResponseRedirect(request.path)
else:
return _display_login_form(request, ERROR_MESSAGE)
diff --git a/django/contrib/auth/middleware.py b/django/contrib/auth/middleware.py
index a6a60780a779..42dc15a36661 100644
--- a/django/contrib/auth/middleware.py
+++ b/django/contrib/auth/middleware.py
@@ -1,12 +1,9 @@
class LazyUser(object):
- def __init__(self):
- self._user = None
-
def __get__(self, request, obj_type=None):
- if self._user is None:
+ if not hasattr(request, '_cached_user'):
from django.contrib.auth import get_user
- self._user = get_user(request)
- return self._user
+ request._cached_user = get_user(request)
+ return request._cached_user
class AuthenticationMiddleware(object):
def process_request(self, request):
diff --git a/django/core/servers/fastcgi.py b/django/core/servers/fastcgi.py
index dedc1f8ba1c0..e3cc24de9d57 100644
--- a/django/core/servers/fastcgi.py
+++ b/django/core/servers/fastcgi.py
@@ -108,7 +108,9 @@ def runfastcgi(argset):
wsgi_opts = {}
else:
return fastcgi_help("ERROR: Implementation must be one of prefork or thread.")
-
+
+ wsgi_opts['debug'] = False # Turn off flup tracebacks
+
# Prep up and go
from django.core.handlers.wsgi import WSGIHandler
diff --git a/django/db/models/query.py b/django/db/models/query.py
index 0b85c3f515c1..aebc08a61632 100644
--- a/django/db/models/query.py
+++ b/django/db/models/query.py
@@ -1,5 +1,6 @@
from django.db import backend, connection, transaction
from django.db.models.fields import DateField, FieldDoesNotExist
+from django.db.models.fields.generic import GenericRelation
from django.db.models import signals
from django.dispatch import dispatcher
from django.utils.datastructures import SortedDict
@@ -925,18 +926,26 @@ def delete_objects(seen_objs):
pk_list = [pk for pk,instance in seen_objs[cls]]
for related in cls._meta.get_all_related_many_to_many_objects():
- for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE):
- cursor.execute("DELETE FROM %s WHERE %s IN (%s)" % \
- (qn(related.field.m2m_db_table()),
- qn(related.field.m2m_reverse_name()),
- ','.join(['%s' for pk in pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE]])),
- pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE])
+ if not isinstance(related.field, GenericRelation):
+ for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE):
+ cursor.execute("DELETE FROM %s WHERE %s IN (%s)" % \
+ (qn(related.field.m2m_db_table()),
+ qn(related.field.m2m_reverse_name()),
+ ','.join(['%s' for pk in pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE]])),
+ pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE])
for f in cls._meta.many_to_many:
+ if isinstance(f, GenericRelation):
+ from django.contrib.contenttypes.models import ContentType
+ query_extra = 'AND %s=%%s' % f.rel.to._meta.get_field(f.content_type_field_name).column
+ args_extra = [ContentType.objects.get_for_model(cls).id]
+ else:
+ query_extra = ''
+ args_extra = []
for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE):
cursor.execute("DELETE FROM %s WHERE %s IN (%s)" % \
- (qn(f.m2m_db_table()), qn(f.m2m_column_name()),
- ','.join(['%s' for pk in pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE]])),
- pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE])
+ (qn(f.m2m_db_table()), qn(f.m2m_column_name()),
+ ','.join(['%s' for pk in pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE]])) + query_extra,
+ pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE] + args_extra)
for field in cls._meta.fields:
if field.rel and field.null and field.rel.to in seen_objs:
for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE):
diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py
index 94df23a8e93c..e576c570e3ca 100644
--- a/django/utils/translation/trans_real.py
+++ b/django/utils/translation/trans_real.py
@@ -1,6 +1,9 @@
"Translation helper functions"
-import os, re, sys
+import locale
+import os
+import re
+import sys
import gettext as gettext_module
from cStringIO import StringIO
from django.utils.functional import lazy
@@ -25,15 +28,25 @@ def currentThread():
# The default translation is based on the settings file.
_default = None
-# This is a cache for accept-header to translation object mappings to prevent
-# the accept parser to run multiple times for one user.
+# This is a cache for normalised accept-header languages to prevent multiple
+# file lookups when checking the same locale on repeated requests.
_accepted = {}
-def to_locale(language):
+# Format of Accept-Language header values. From RFC 2616, section 14.4 and 3.9.
+accept_language_re = re.compile(r'''
+ ([A-Za-z]{1,8}(?:-[A-Za-z]{1,8})*|\*) # "en", "en-au", "x-y-z", "*"
+ (?:;q=(0(?:\.\d{,3})?|1(?:.0{,3})?))? # Optional "q=1.00", "q=0.8"
+ (?:\s*,\s*|$) # Multiple accepts per header.
+ ''', re.VERBOSE)
+
+def to_locale(language, to_lower=False):
"Turns a language name (en-us) into a locale name (en_US)."
p = language.find('-')
if p >= 0:
- return language[:p].lower()+'_'+language[p+1:].upper()
+ if to_lower:
+ return language[:p].lower()+'_'+language[p+1:].lower()
+ else:
+ return language[:p].lower()+'_'+language[p+1:].upper()
else:
return language.lower()
@@ -309,46 +322,40 @@ def get_language_from_request(request):
if lang_code in supported and lang_code is not None and check_for_language(lang_code):
return lang_code
- lang_code = request.COOKIES.get('django_language', None)
- if lang_code in supported and lang_code is not None and check_for_language(lang_code):
+ lang_code = request.COOKIES.get('django_language')
+ if lang_code and lang_code in supported and check_for_language(lang_code):
return lang_code
- accept = request.META.get('HTTP_ACCEPT_LANGUAGE', None)
- if accept is not None:
-
- t = _accepted.get(accept, None)
- if t is not None:
- return t
-
- def _parsed(el):
- p = el.find(';q=')
- if p >= 0:
- lang = el[:p].strip()
- order = int(float(el[p+3:].strip())*100)
- else:
- lang = el
- order = 100
- p = lang.find('-')
- if p >= 0:
- mainlang = lang[:p]
- else:
- mainlang = lang
- return (lang, mainlang, order)
-
- langs = [_parsed(el) for el in accept.split(',')]
- langs.sort(lambda a,b: -1*cmp(a[2], b[2]))
-
- for lang, mainlang, order in langs:
- if lang in supported or mainlang in supported:
- langfile = gettext_module.find('django', globalpath, [to_locale(lang)])
- if langfile:
- # reconstruct the actual language from the language
- # filename, because otherwise we might incorrectly
- # report de_DE if we only have de available, but
- # did find de_DE because of language normalization
- lang = langfile[len(globalpath):].split(os.path.sep)[1]
- _accepted[accept] = lang
- return lang
+ accept = request.META.get('HTTP_ACCEPT_LANGUAGE', '')
+ for lang, unused in parse_accept_lang_header(accept):
+ if lang == '*':
+ break
+
+ # We have a very restricted form for our language files (no encoding
+ # specifier, since they all must be UTF-8 and only one possible
+ # language each time. So we avoid the overhead of gettext.find() and
+ # look up the MO file manually.
+
+ normalized = locale.locale_alias.get(to_locale(lang, True))
+ if not normalized:
+ continue
+
+ # Remove the default encoding from locale_alias
+ normalized = normalized.split('.')[0]
+
+ if normalized in _accepted:
+ # We've seen this locale before and have an MO file for it, so no
+ # need to check again.
+ return _accepted[normalized]
+
+ for lang in (normalized, normalized.split('_')[0]):
+ if lang not in supported:
+ continue
+ langfile = os.path.join(globalpath, lang, 'LC_MESSAGES',
+ 'django.mo')
+ if os.path.exists(langfile):
+ _accepted[normalized] = lang
+ return lang
return settings.LANGUAGE_CODE
@@ -494,3 +501,24 @@ def string_concat(*strings):
return ''.join([str(el) for el in strings])
string_concat = lazy(string_concat, str)
+
+def parse_accept_lang_header(lang_string):
+ """
+ Parses the lang_string, which is the body of an HTTP Accept-Language
+ header, and returns a list of (lang, q-value), ordered by 'q' values.
+
+ Any format errors in lang_string results in an empty list being returned.
+ """
+ result = []
+ pieces = accept_language_re.split(lang_string)
+ if pieces[-1]:
+ return []
+ for i in range(0, len(pieces) - 1, 3):
+ first, lang, priority = pieces[i : i + 3]
+ if first:
+ return []
+ priority = priority and float(priority) or 1.0
+ result.append((lang, priority))
+ result.sort(lambda x, y: -cmp(x[1], y[1]))
+ return result
+
diff --git a/docs/release_notes_0.95.txt b/docs/release_notes_0.95.txt
index 029d65b131b5..bf6a54b47c8a 100644
--- a/docs/release_notes_0.95.txt
+++ b/docs/release_notes_0.95.txt
@@ -1,9 +1,8 @@
-=================================
-Django version 0.95 release notes
-=================================
+===================================
+Django version 0.95.2 release notes
+===================================
-
-Welcome to the Django 0.95 release.
+Welcome to the Django 0.95.2 release.
This represents a significant advance in Django development since the 0.91
release in January 2006. The details of every change in this release would be
@@ -91,6 +90,31 @@ easy checklist_ for reference when undertaking the porting operation.
.. _Removing The Magic: http://code.djangoproject.com/wiki/RemovingTheMagic
.. _checklist: http://code.djangoproject.com/wiki/MagicRemovalCheatSheet1
+Changes since the 0.95 release
+==============================
+
+This release contains fixes for several bugs discovered after the
+initial release of Django 0.95; these include:
+
+ * A patch for a small security vulnerability in the script
+ Django's internationalization system uses to compile translation
+ files.
+
+ * A fix for a bug in Django's authentication middleware which
+ could cause apparent "caching" of a logged-in user.
+
+ * A patch which disables debugging mode in the flup FastCGI
+ package Django uses to launch its FastCGI server, which prevents
+ tracebacks from bubbling up during production use.
+
+ * A security fix to the i18n framework which could allow an
+ attacker to send extremely large strings in the Accept-Language
+ header and cause a denial of service by filling available memory.
+
+Because these problems weren't discovered and fixed until after the
+0.95 release, it's recommended that you use this release rather than
+the original 0.95.
+
Problem reports and getting help
================================
@@ -118,5 +142,5 @@ available at any hour of the day -- to help, or just to chat.
Thanks for using Django!
The Django Team
-July 2006
+January 2007
diff --git a/setup.py b/setup.py
index 845e5da3b560..17380f103c55 100644
--- a/setup.py
+++ b/setup.py
@@ -5,7 +5,7 @@
setup(
name = "Django",
- version = "0.95",
+ version = "0.95.4",
url = 'http://www.djangoproject.com/',
author = 'Lawrence Journal-World',
author_email = 'holovaty@gmail.com',
diff --git a/tests/modeltests/generic_relations/models.py b/tests/modeltests/generic_relations/models.py
index e9a81a19e8b2..b73d9da23a43 100644
--- a/tests/modeltests/generic_relations/models.py
+++ b/tests/modeltests/generic_relations/models.py
@@ -65,14 +65,14 @@ def __str__(self):
# Objects with declared GenericRelations can be tagged directly -- the API
# mimics the many-to-many API.
->>> lion.tags.create(tag="yellow")
-
->>> lion.tags.create(tag="hairy")
-
>>> bacon.tags.create(tag="fatty")
>>> bacon.tags.create(tag="salty")
+>>> lion.tags.create(tag="yellow")
+
+>>> lion.tags.create(tag="hairy")
+
>>> lion.tags.all()
[, ]
@@ -105,4 +105,31 @@ def __str__(self):
[]
>>> TaggedItem.objects.filter(content_type__pk=ctype.id, object_id=quartz.id)
[]
+
+
+# If you delete an object with an explicit Generic relation, the related
+# objects are deleted when the source object is deleted.
+# Original list of tags:
+>>> [(t.tag, t.content_type, t.object_id) for t in TaggedItem.objects.all()]
+[('clearish', , 1), ('fatty', , 2), ('hairy', , 1), ('salty', , 2), ('shiny', , 2), ('yellow', , 1)]
+
+>>> lion.delete()
+>>> [(t.tag, t.content_type, t.object_id) for t in TaggedItem.objects.all()]
+[('clearish', , 1), ('fatty', , 2), ('salty', , 2), ('shiny', , 2)]
+
+# If Generic Relation is not explicitly defined, any related objects
+# remain after deletion of the source object.
+>>> quartz.delete()
+>>> [(t.tag, t.content_type, t.object_id) for t in TaggedItem.objects.all()]
+[('clearish', , 1), ('fatty', , 2), ('salty', , 2), ('shiny', , 2)]
+
+# If you delete a tag, the objects using the tag are unaffected
+# (other than losing a tag)
+>>> tag = TaggedItem.objects.get(id=1)
+>>> tag.delete()
+>>> bacon.tags.all()
+[]
+>>> [(t.tag, t.content_type, t.object_id) for t in TaggedItem.objects.all()]
+[('clearish', , 1), ('salty', , 2), ('shiny', , 2)]
+
"""