views:

528

answers:

5

I am trying to find a way to prevent users from double-submitting my forms. I have javascript that disables the submit button, but there is still an occasional user who finds a way to double-submit.

I have a vision of a re-usable library that I could create to protect from this.

In my ideal library, the code block would look something like this:

try:
    with acquire_lock({'field1':'abc', 'field2':'def'}) as lock:
        response = #do some credit card processing
        lock.response = response
except SubmissionWasDuplicate, e:
    response = e.response

The lock table would look something like this:

duplicate_submission_locks

  • submission_hash # a MD5 of the submitted arguments
  • response # pickled data
  • created_at # used for sweeping this table
  • lock_expired # boolean signifying if the lock has expired

Does anyone know if this already exists? It doesn't seem to difficult to write, so if it doesn't exist I may write it myself.

+4  A: 

To be honest, your best bet (easy and good practice) is to issue a HTTPRedirect() to the thank you page, and if the thank you page is the same one as the form, that's OK. You can still do this.

orokusaki
Thanks for answering. Doing a HTTPRedirect to display the thank you page is what I had in mind. This question is more about how to detect that the post is duplicate.
Gattster
+3  A: 

One easy solution to this problem is to add a unique hash to each form. Then you can have a rolling table of current forms. When a form is submitted, or the hash gets too old, you can expire it out of your table, and reject any form which does not have a matching hash in your table.

The HTTPRedirect is the correct way to do it, as previously mentioned.

Unfortunately, even Django's own built in admin is prone to problems related to this issue. In some cases, the cross-site scripting framework can assist to prevent some of this, but I'm afraid the current production versions just don't have this built in.

Paul McMillan
I wonder if this would hit glitches if the user uses his back-button and resubmits the same form with different fields? We would need to refresh the unique hash.
Gattster
Your request was to prevent resubmission. Removing the hash from the DB should do that handily. A form submitted with slightly different values is still a re-submission in most cases, though you might use the hash to store a temporary forward reference to the submission, allowing editing a previous submission for some finite period of time.
Paul McMillan
+2  A: 

It is always good to use the redirect-after-post method. This prevents user from accidently resubmitting the form using refresh function from the browser. It is also helpful even when you use the hash method. It's because without redirect after a POST, in case of hitting Back/Refresh button, user will see a question message about resubmitting the form, which can confuse her.

If you do a GET redirect after every POST, then hitting Back/Refresh won't display this wierd (for usual user) message. So for full protection use Hash+redirect-after-post.

Łukasz Korzybski
+5  A: 

You can use a session to store the hash

import hashlib

def contact(request):
    if request.method == 'POST':
        form = MyForm(request.POST)
        #join all the fields in one string
        hashstring=hashlib.sha1(fieldsstring)
        if request.session.get('sesionform')!=hashstring:
            if form.is_valid() :                                         
                request.session['sesionform'] = hashstring
                #do some stuff...
                return HttpResponseRedirect('/thanks/') # Redirect after POST  
        else
           raise SubmissionWasDuplicate("duplicate")
    else:
        form = MyForm() 

With this approach (not deleting the session cookie) the user can't re-store the data util the session expires, by the way, i'm assuming that exist something who identify the user who send the data

Kristian Damian
What happens if the user hits the /thanks/ page and the transaction hasn't finished yet? Also, what if the transaction failed?
Gattster
if the transaction fails, you must end the session and redirect to some error page.If you wanna assure that only can get to the "/thanks" page when the transaction is complete and valid, you must add a token when the transactions completes and send it to the "/thanks" page and validated there (just like paypal does)
Kristian Damian
+2  A: 

Kristian Damian's answer is really a great suggestion. I just thought of a slight variation on that theme, but it might have more overhead.

You could try implementing something that is used in django-piston for BaseHandler objects, which is a method called exists() that checks to see if what you are submitting is already in the database.

From handler.py (BaseHandler):

def exists(self, **kwargs):
    if not self.has_model():
        raise NotImplementedError

    try:
        self.model.objects.get(**kwargs)
        return True
    except self.model.DoesNotExist:
        return False

So let's say make that a function called request_exists(), instead of a method:

if form.is_valid()
    if request_exists(request):
        # gracefully reject dupe submission
    else:
        # do stuff to save the request
        ...
        # and ALWAYS redirect after a POST!!
        return HttpResponseRedirect('/thanks/') 
jathanism
This will work. The main question this raises for me is how to gracefully handle the dupe submission. Since the first submission is probably not finished yet, I need to delay until the first submission finishes. Then I need to send the user to the thanks page and find the result of the submission, which could have succeeded or failed with an error message.
Gattster
Well you don't have to send them to a naked "/thanks/" page. You could send it along with the request data so that the template is able to process the results of the submission and display them accordingly. Perhaps an interstitial "/add/confirm/" page would be good before you get to "/thanks/"?
jathanism