Hi,
I mentioned on the Django User Mailing List that the interaction of 
ValidationError, mark_safe, and ugettext was not what I expected.

    https://groups.google.com/d/topic/django-users/Soik095MTaI/discussion
    
Succinctly, the two versions of clean_slug below behave differently, when they 
should not.

    # results in <code> being printed for user
    def clean_slug(self):
        new_slug = self.cleaned_data['slug'].lower()
        if new_slug == 'invalid_value':
            raise ValidationError(
                # _ is ugettext
                mark_safe(_('SlugField may not be '
                            '"%(slug_value)s" '
                            'for URL reasons.')),
                params={
                    'slug_value':
                        mark_safe('<code>invalid_value</code>')})
        return new_slug

    # results in <code> being valid HTML elements
    def clean_slug(self):
        new_slug = self.cleaned_data['slug'].lower()
        if new_slug == 'invalid_value':
            raise ValidationError(
                # _ is ugettext
                mark_safe(_('SlugField may not be '
                            '"<code>invalid_value</code>" '
                            'for URL reasons.')))
        return new_slug 

The problem behavior is caused by the way ValidationError outputs its text; 
ValidationError behaves differently depending on whether params are used, which 
seems undesirable. We can demonstrate this in the Django shell (1.7.4).

    >>> from django.core.exceptions import ValidationError
    >>> from django.utils.safestring import mark_safe, SafeData
    >>> from django.utils.six import text_type
    >>> isinstance(mark_safe('hello'), SafeData)
    True
    
When the ValidationError message is SafeData, the output of the ValidationError 
is also SafeData.

    >>> ve = ValidationError('this is static')
    >>> isinstance(list(ve)[0], text_type)
    True
    >>> ve = ValidationError(mark_safe('this is safe'))
    >>> isinstance(list(ve)[0], SafeData)
    True

... unless we use the params feature of the exception, as the documentation 
tells developers to.

    >>> ve = ValidationError(mark_safe('%(r)s'), params={'r': 'replaced'})
    >>> isinstance(list(ve)[0], SafeData)
    False
    >>> ve = ValidationError(mark_safe('%(r)s'), params={'r': 
mark_safe('replaced')})
    >>> isinstance(list(ve)[0], SafeData)
    False

The difference in behavior depending on whether params is passed should not 
happen.

I can see how in the first instance above---where the message is SafeData but 
the params values aren't---it is desirable to return text instead of SafeData 
from a security standpoint. However, in the second case, when both the message 
and the params are SafeData, it seems counter-intuitive to return text.

| message | params | output |
|---------|--------|--------|
| text    | none   | text   |
| safe    | none   | safe   |
| text    | text   | text   |
| safe    | text   | text   |
| text    | safe   | text   |
| safe    | safe   | safe   | <-- the problem: currently outputs text

The issues stems from ValidationError.__iter__(). In `add_error` of form 
validation, ValidationErrors are added on with the following:

    # django/forms/forms.py: line 353
    self._errors[field].extend(error_list)

This code results in a call to ValidationError.__iter__(),

    # django/core/exceptions.py: line 151
    def __iter__(self):
        if hasattr(self, 'error_dict'):
            for field, errors in self.error_dict.items():
                yield field, list(ValidationError(errors))
        else:
            for error in self.error_list:
                message = error.message
                if error.params:
                    message %= error.params
                yield force_text(message)

Turns out, __mod__ on SafeText is not defined, meaning the code `message %= 
error.params` always transforms SafeText to text.

>>> isinstance(mark_safe('hello %(r)s'), SafeData)
True
>>> isinstance(mark_safe('hello %(r)s') % {'r': 'boo'}, SafeData)
False
>>> isinstance(mark_safe('hello %(r)s') % {'r': mark_safe('boo')}, SafeData)
False

I can thus think of two ways to solve this:

1. Modify ValidationError.__iter__() to account for SafeText with params.
2. add __mod__() to SafeText

Given that ugettext (or rather, do_translate() in 
django/utils/translation/trans_real.py) takes the first approach, I've created 
a branch in my fork of Django to do just that.

https://github.com/jambonrose/django/commit/61612f38aa68efcb5038f51305359b25485e2f48

My djangocore-box isn't working quite the way it should (postgres tests will 
not run on master), but all the tests appear to run correctly on 
runtests2.7-sqlite and runtests3.3-sqlite.

With that said: while I think there is a problem in the difference in behavior, 
I'm not sure my solution is the best. More simply: is the following behavior 
table actually what we want?

| message | params | output |
|---------|--------|--------|
| text    | none   | text   |
| safe    | none   | safe   |
| text    | text   | text   |
| safe    | text   | text   | <-- this in particular makes me wonder
| text    | safe   | text   |
| safe    | safe   | safe   |

If this is what we want, what next step should I be taking for this? Should I 
be filing a bug report, or issuing a PR directly?

If the addition of __mod__ to SafeText is a better change, I'm happy to code 
that too.

Andrew

-- 
You received this message because you are subscribed to the Google Groups 
"Django developers  (Contributions to Django itself)" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To post to this group, send email to [email protected].
Visit this group at http://groups.google.com/group/django-developers.
To view this discussion on the web visit 
https://groups.google.com/d/msgid/django-developers/4761844C-6BFE-465A-9108-AFDA693A9E06%40andrewsforge.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to