diff --git a/django/__init__.py b/django/__init__.py index 544df522632e..5e04a072bc60 100644 --- a/django/__init__.py +++ b/django/__init__.py @@ -1 +1 @@ -VERSION = (0, 95, None) +VERSION = (0, 95.4, None) diff --git a/django/bin/compile-messages.py b/django/bin/compile-messages.py index 5f653df95d9f..f04bcea1a10a 100755 --- a/django/bin/compile-messages.py +++ b/django/bin/compile-messages.py @@ -19,7 +19,17 @@ def compile_messages(): if f.endswith('.po'): sys.stderr.write('processing file %s in %s\n' % (f, dirpath)) pf = os.path.splitext(os.path.join(dirpath, f))[0] - cmd = 'msgfmt -o "%s.mo" "%s.po"' % (pf, pf) + # Store the names of the .mo and .po files in an environment + # variable, rather than doing a string replacement into the + # command, so that we can take advantage of shell quoting, to + # quote any malicious characters/escaping. + # See http://cyberelk.net/tim/articles/cmdline/ar01s02.html + os.environ['djangocompilemo'] = pf + '.mo' + os.environ['djangocompilepo'] = pf + '.po' + if sys.platform == 'win32': # Different shell-variable syntax + cmd = 'msgfmt -o "%djangocompilemo%" "%djangocompilepo%"' + else: + cmd = 'msgfmt -o "$djangocompilemo" "$djangocompilepo"' os.system(cmd) if __name__ == "__main__": diff --git a/django/contrib/admin/templates/admin/login.html b/django/contrib/admin/templates/admin/login.html index e5bda0f0e60b..eab80017fe04 100644 --- a/django/contrib/admin/templates/admin/login.html +++ b/django/contrib/admin/templates/admin/login.html @@ -19,7 +19,6 @@
- {% comment %}{% trans 'Have you forgotten your password?' %}{% endcomment %}
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)] + """