Don't leak into current language from .clean_fields()

Django uses .clean_fields() to verify the values of all fields of an
instance, e.g. after the instance has been updated from a form view.

.clean_fields() essentially walks over the fields in the model, reads
the current values, verifies and normalizes them, and writes them
back [1].

With model-translations activated, this can leak values from a fallback
language into the current language of the session, if the value for the
current language has been set to an empty value from a translation aware
application or form.

Example:

- current language is "en"
- fallback languages are "en", "de"
- web form wants to set "title_de" to "Schnapsidee" and leave
    "title_en" empty

After the form is submitted, Django calls .clean_fields() which
processes the field "title" first

- reads value of "title" - which gives "Schnapsidee" from
    "title_de" by the fallback mechanism of Modeltranslations
- writes this value back to "title", which sets "title_en"
    as a side effect.

Bugs have been reported at [2], [3] and [4]

Disable the implicit setting of the translation field from the
translated field ( Rule 2 of [5] ) during .clean_field() processing.

[1] bf77669453/django/db/models/base.py (L1229)
[2] https://github.com/deschler/django-modeltranslation/issues/382
[3] https://github.com/infoportugal/wagtail-modeltranslation/issues/247
[4] https://github.molgen.mpg.de/molgen/mpicms/issues/15
[5] https://django-modeltranslation.readthedocs.io/en/latest/usage.html#rules-for-translated-field-access
This commit is contained in:
Donald Buczek 2020-01-16 08:05:32 +01:00
parent 0f79d852a5
commit 2c9669efd8
2 changed files with 7 additions and 2 deletions

View file

@ -314,9 +314,10 @@ class TranslationFieldDescriptor(object):
instance.__dict__[self.field.name] = value
if isinstance(self.field, fields.related.ForeignKey):
instance.__dict__[self.field.get_attname()] = None if value is None else value.pk
if getattr(instance, '_mt_init', False):
if getattr(instance, '_mt_init', False) or getattr(instance, '_mt_disable', False):
# When assignment takes place in model instance constructor, don't set value.
# This is essential for only/defer to work, but I think it's sensible anyway.
# Setting the localized field may also be disabled by setting _mt_disable.
return
loc_field_name = build_localized_fieldname(self.field.name, get_language())
setattr(instance, loc_field_name, value)

View file

@ -267,7 +267,11 @@ def patch_clean_fields(model):
if orig_field_name in exclude:
field.save_form_data(self, value, check=False)
delattr(self, '_mt_form_pending_clear')
old_clean_fields(self, exclude)
try:
setattr(self, '_mt_disable', True)
old_clean_fields(self, exclude)
finally:
setattr(self, '_mt_disable', False)
model.clean_fields = new_clean_fields