diff --git a/django/contrib/formtools/tests/__init__.py b/django/contrib/formtools/tests/__init__.py index b88f565d6b6..1ec71d3e338 100644 --- a/django/contrib/formtools/tests/__init__.py +++ b/django/contrib/formtools/tests/__init__.py @@ -233,6 +233,8 @@ class WizardPageOneForm(forms.Form): class WizardPageTwoForm(forms.Form): field = forms.CharField() +class WizardPageTwoAlternativeForm(forms.Form): + field = forms.CharField() class WizardPageThreeForm(forms.Form): field = forms.CharField() @@ -351,7 +353,8 @@ class WizardTests(TestCase): def test_14498(self): """ - Regression test for ticket #14498. + Regression test for ticket #14498. All previous steps' forms should be + validated. """ that = self @@ -391,3 +394,26 @@ class WizardTests(TestCase): "wizard_step": "1"} wizard(DummyRequest(POST=data)) self.assertTrue(reached[0]) + + def test_15075(self): + """ + Regression test for ticket #15075. Allow modifying wizard's form_list + in process_step. + """ + that = self + + class WizardWithProcessStep(WizardClass): + def process_step(self, request, form, step): + if step == 0: + self.form_list[1] = WizardPageTwoAlternativeForm + if step == 1: + that.assertTrue(isinstance(form, WizardPageTwoAlternativeForm)) + + wizard = WizardWithProcessStep([WizardPageOneForm, + WizardPageTwoForm, + WizardPageThreeForm]) + data = {"0-field": "test", + "1-field": "test2", + "hash_0": "7e9cea465f6a10a6fb47fcea65cb9a76350c9a5c", + "wizard_step": "1"} + wizard(DummyRequest(POST=data)) diff --git a/django/contrib/formtools/wizard.py b/django/contrib/formtools/wizard.py index e0f5e2c64a6..6f3660dd996 100644 --- a/django/contrib/formtools/wizard.py +++ b/django/contrib/formtools/wizard.py @@ -90,6 +90,40 @@ class FormWizard(object): if current_step >= self.num_steps(): raise Http404('Step %s does not exist' % current_step) + # Validate and process all the previous forms before instantiating the + # current step's form in case self.process_step makes changes to + # self.form_list. + + # If any of them fails validation, that must mean the validator relied + # on some other input, such as an external Web site. + + # It is also possible that alidation might fail under certain attack + # situations: an attacker might be able to bypass previous stages, and + # generate correct security hashes for all the skipped stages by virtue + # of: + # 1) having filled out an identical form which doesn't have the + # validation (and does something different at the end), + # 2) or having filled out a previous version of the same form which + # had some validation missing, + # 3) or previously having filled out the form when they had more + # privileges than they do now. + # + # Since the hashes only take into account values, and not other other + # validation the form might do, we must re-do validation now for + # security reasons. + previous_form_list = [] + for i in range(current_step): + f = self.get_form(i, request.POST) + if not self._check_security_hash(request.POST.get("hash_%d" % i, ''), + request, f): + return self.render_hash_failure(request, i) + + if not f.is_valid(): + return self.render_revalidation_failure(request, i, f) + else: + self.process_step(request, f, i) + previous_form_list.append(f) + # Process the current step. If it's valid, go to the next step or call # done(), depending on whether any steps remain. if request.method == 'POST': @@ -98,36 +132,6 @@ class FormWizard(object): form = self.get_form(current_step) if form.is_valid(): - # Validate all the forms. If any of them fail validation, that - # must mean the validator relied on some other input, such as - # an external Web site. - - # It is also possible that validation might fail under certain - # attack situations: an attacker might be able to bypass previous - # stages, and generate correct security hashes for all the - # skipped stages by virtue of: - # 1) having filled out an identical form which doesn't have the - # validation (and does something different at the end), - # 2) or having filled out a previous version of the same form - # which had some validation missing, - # 3) or previously having filled out the form when they had - # more privileges than they do now. - # - # Since the hashes only take into account values, and not other - # other validation the form might do, we must re-do validation - # now for security reasons. - previous_form_list = [self.get_form(i, request.POST) for i in range(current_step)] - - for i, f in enumerate(previous_form_list): - if not self._check_security_hash(request.POST.get("hash_%d" % i, ''), request, f): - return self.render_hash_failure(request, i) - - if not f.is_valid(): - return self.render_revalidation_failure(request, i, f) - else: - self.process_step(request, f, i) - - # Now progress to processing this step: self.process_step(request, form, current_step) next_step = current_step + 1