avoid form resubmission


(Chekhiv) #1

Hello everyone.

To avoid form resubmission and thus duplicating record in the database when a user reloads the webpage I use the following method:

In my view I insert


<?php $_SESSION['nonce'] = $nonce = md5('salt'.microtime()); ?>

<form>

<input type="hidden" name="nonce" value="<?php echo $nonce; ?>" />

...

</form>

and in controller


if ($_SESSION['nonce'] == $_POST['nonce'])

{

        $model->save();

	$_SESSION['nonce'] = null;

}

It works, but is it good? And is there any yii specific way to do this?


(Jacob Moen) #2

From what I’ve gathered, it’s a good approach.

Don’t think there’s anything built-in. :)


(Imehesz) #3

that’s pretty cool - i don’t think there is a built-in solution, but don’t quote me on that.

sometimes I attach an [i]onClick[/i] event on the submit button(s) (with javascript) to disable the button. it’s definitely not the most efficient way, but worked so far.

–iM


(Mike) #4

Note, that this will only protect you from saving the same POST data again after a successful save operation. Usually this is prevented by a redirect to some other page immediately after save().

Anyway this could still be useful in some situations. I even tried something similar once, but your approach looks better. I’d only wrap it into some helper methods. A good name for the 'nonce 'part could be save token. A token in this sense is something you give to the user that allows him to perform one (and only one) successful save operation. After that the user needs to obtain a new token by reloading the form.

Again a nice exercise for creating a controller behavior. Just some quick brainstorming of an interface:




// view:

<?php echo $this->getSaveTokenField(); // creates the hidden input, generates new token if none available ?>


// controller:

if (isset($_POST['Form'])) 

{  

    // ... (assign attributes, etc. here)...


    // Refresh form if token is not valid

    if (!$this->getIsSaveTokenValid())  

        $this->refresh();


    // Ohterwhise we can save

    if ($model->save()) 

    {

        $this->clearSaveToken();  // token has been consumed

        // ... save flash message, etc.  here ...

        $this->refresh();

    }

}

Using the getters, we could write even shorter: $this->saveTokenField, $this->isSaveTokenValid.

Creating a behavior from that should be straightforward … :)


(Tomvdp) #5

Thank you people. I have put together the snippets from this thread into something that I think is a solid solution.

When a user hits the back button (or forward in some cases) and the browser re-submits the data, this solution blocks that attempt. What is elegant is that next time the user backs/forwards, the submit has magically disappeared from the browser’s history.

Even if this is an old thread, this code still might help others, so here it is. Feel free to comment.

The extended controller:


class Controller extends CController {

    

    //this is the name of the hidden field holding the token and the name of the session variable

    const PreventReSubmitFieldName = '__r__';

 

    

    /**

     * Stores a token in the user's session and generates html for it.  This function is generally called from a view, to inject the token in a form.

     * @return string The html for a hidden field with the token.

     */

    public function getPreventReSubmitToken() {

        $preventReSubmitToken = (string) microtime(true);

        $_SESSION[self::PreventReSubmitFieldName] = $preventReSubmitToken;

        return '<input type="hidden" name="' . self::PreventReSubmitFieldName . '" value="' . $preventReSubmitToken . '">';

    }

    

    /**

     * Consumes the token (clears it).

     * @return boolean Returns true if the token in the POST matches the token in the session.

     */

    public function consumePreventReSubmitToken() {

        $preventReSubmitToken = $_SESSION[self::PreventReSubmitFieldName];

        

        //clear the session variable

        $_SESSION[self::PreventReSubmitFieldName] = null;

        

        //did we get the token we expected

        if ($preventReSubmitToken == $_POST[self::PreventReSubmitFieldName])

            return true;

        

        return false;

    }

}

In a view, somewhere inside the form:


	echo $this->getPreventReSubmitToken();

In your controller, the action handling the submit you are protecting:


    public function actionRegister() {

  

        //first: ajax validation

	//...

		

	//next: classic Yii

        $model = new RegistrationForm();


        if (isset($_POST['RegistrationForm'])) {

            

		//safeguard against double submits from back-button

		if (!$this->consumePreventReSubmitToken()) $this->refresh();

            

		//continue...

            	$model->attributes = $_POST['RegistrationForm'];

		//...



I left out the md5-ing of the microtime, since in my opinion this is a waste of resources.

Also, the consumation of the token in the controller should happen as soon as possible, and not after validating the _POST. You want to minimize the impact of the re-submit, to avoid side effects that are sometimes programmed inside model validations.


(nolanchrist007) #6

Thanks I had to avoid form resubmission on my site and was looking for something like this. This thread helped me a lot. Thanks