stymiee

Handling Online Payments Part 6 - Preventing Duplicate Submissions with POST/REDIRECT/GET

by All Star ‎03-16-2011 10:59 AM - edited ‎04-24-2012 04:41 AM (17,182 Views)

This is part six of a multi-part series on handling online payments.

 

In Part 1 of this series we identified our goals (creating a payment form that was usable, accessible, and secure) and began by creating the form we will use to capture payment information. In Part 2 of this series we continued this process by exploring how we will handle the data submitted by that form. In Part 3 of this series we took the data received and sanitized in Part 2 and validated that it was in a format we required. In Part 4 of this series we took the errors we found in Part 3 and displayed them in a user-friendly format to minimize cart abandonment. In Part 5 of this series we processed the payment and handled the response returned to us whether it be approved, declined, or an error completing the transaction process.

 

The remaining installments of this series are going to improve upon our form to make it more user-friendly, secure, and easier to maintain. In this installment we will look at how we can prevent duplicate submissions when refreshing the page or using the back button.

 

The Problem

 

As web designers and developers we can do our best to make our website as easy to use as possible. But any experienced developer can tell you that users will still do silly things like use the back button although you have provided navigation right in your web page. The result? Duplicate submissions. This is bad enough in-and-of-itself, but it's twice as bad when the form is processing payments. At best your users get confused when they see error message on their screen. At worst you just processed their payments twice. Queue the customer complaints and chargebacks.

 

The Solution - Post/Redirect/Get

 

Fortunately we can program around this using a programming pattern known as Post/Redirect/Get. What this pattern is doing is receiving the POSTed form data, saving it, and REDIRECTing the user back to the same page (which uses the GET method to retrieve it. Let's break it down into pieces and throw in some technical information to help make this clearer for us.

 

  1. The user submits the form

     

    This is pretty straight forward. The user completes the form and submits it by pressing the submit button or enter on their keyboard.

     

  2. We store the form data in a session

     

    After processing the data we discover an error so we need to redisplay the form with an error message but we also want to populate it with their data so they don't have to refill the entire form just to fix potentially one little mistake. So we store their data in a session ($_SESSION). Session variables carry over from page-to-page for as long as the session is valid or until they are deleted. This is an ideal place to put their information since redirecting will cause their information to be immediately discarded by the server.

     

  3. We redirect the user back to the same page using a 303 redirect

     

    Once we have saved the user's information in their session we need to redirect them back to the same page. In order for this to work properly we need to use a 303 redirect. This means we need to send a 303 header with our redirect. A 303 redirect will cause the browser to reload the page without the initial HTTP POST request to be resubmitted. This includes when the user uses the back or refresh buttons.

     

  4. We re-populate the form using the data stored in the session

     

    When the page is sent to the user we re-populate it with their information we saved in their session.

     

 

Let's See How That Works With Some Code

 

Before we do anything we need to start our session using the session_start() function. This should be the first piece of code on every page we wish to use session variables.

 

<?php
    session_start();

    $errors = array();
    /* More code below */

 

That's easy enough. Now when there is an error we need to place the user's information into their session for retrieval after the redirect. If you remember we use an if statement to determine if there were any errors validating their information and if not we go ahead and process their payment. Otherwise we reloaded the page with the form and an error message. We're going to amend this code a bit and place their information in the session by adding an else statement. This is also where we will place our redirect.

 

if (count($errors) === 0)
{
    // This code doesn't change
}
else
{
    // This is new!

    // Create an array in our session for use to store their variables
    $_SESSION['prg'] = array();

    // Put their information into the array
    //$_SESSION['prg']['credit_card']           = $credit_card;
    //$_SESSION['prg']['expiration_month']      = $expiration_month;
    //$_SESSION['prg']['expiration_year']       = $expiration_year;
    //$_SESSION['prg']['cvv']                   = $cvv;
    $_SESSION['prg']['cardholder_first_name'] = $cardholder_first_name;
    $_SESSION['prg']['cardholder_last_name']  = $cardholder_last_name;
    $_SESSION['prg']['billing_address']       = $billing_address;
    $_SESSION['prg']['billing_address2']      = $billing_address2;
    $_SESSION['prg']['billing_city']          = $billing_city;
    $_SESSION['prg']['billing_state']         = $billing_state;
    $_SESSION['prg']['billing_zip']           = $billing_zip;
    $_SESSION['prg']['telephone']             = $telephone;
    $_SESSION['prg']['email']                 = $email;
    $_SESSION['prg']['recipient_first_name']  = $recipient_first_name;
    $_SESSION['prg']['recipient_last_name']   = $recipient_last_name;
    $_SESSION['prg']['shipping_address']      = $shipping_address;
    $_SESSION['prg']['shipping_address2']     = $shipping_address2;
    $_SESSION['prg']['shipping_city']         = $shipping_city;
    $_SESSION['prg']['shipping_state']        = $shipping_state;
    $_SESSION['prg']['shipping_zip']          = $shipping_zip;

    // Don't forget the $errors array!
    $_SESSION['prg']['errors']                = $errors;

    // Do our redirect. Make sure it sends the 303 header
    header('Location: /payment-form.php', true, 303);
    exit;
}

 

The first thing we do in our new else statement is create an array in our session to store the user's information. We put this in an array instead of individual variables so it will be easier for us to remove their information from the session later on. After all, it's easier to unset one variable then twenty! We then populate our array with the user's data. You'll notice we use the variables names as the keys in the array. Consistency makes it easier for us to maintain this code especially if we revisit it a year down the road and don't remember exactly what we did. We also need to store the $errors array so we know what errors occurred and can display them to the user. Once the array is populated with the user's information we do our redirect.

 

At this point if we refresh the page you'll notice we are not prompted to resend the form data. You can thank the 303 redirect for that. But now we need to populate the form so our users can make their corrections. We're going to follow a very similar approach to the one we took to save the user's information in the session. We used an if statement to determine of the form was submitted or not. We're going to append an else if statement to it looking for our PRG session array. If we find it we'll grab the contents for display on the page.

 

if ('POST' === $_SERVER['REQUEST_METHOD'])
{
    // This code doesn't change
}
else if (isset($_SESSION['prg']) && is_array($_SESSION['prg']))
{
    // This is new!

    // Retreive the user's information and our error messages
    $credit_card           = $_SESSION['prg']['credit_card'];
    $expiration_month      = $_SESSION['prg']['expiration_month'];
    $expiration_year       = $_SESSION['prg']['expiration_year'];
    $cvv                   = $_SESSION['prg']['cvv'];
    $cardholder_first_name = $_SESSION['prg']['cardholder_first_name'];
    $cardholder_last_name  = $_SESSION['prg']['cardholder_last_name'];
    $billing_address       = $_SESSION['prg']['billing_address'];
    $billing_address2      = $_SESSION['prg']['billing_address2'];
    $billing_city          = $_SESSION['prg']['billing_city'];
    $billing_state         = $_SESSION['prg']['billing_state'];
    $billing_zip           = $_SESSION['prg']['billing_zip'];
    $telephone             = $_SESSION['prg']['telephone'];
    $email                 = $_SESSION['prg']['email'];
    $recipient_first_name  = $_SESSION['prg']['recipient_first_name'];
    $recipient_last_name   = $_SESSION['prg']['recipient_last_name'];
    $shipping_address      = $_SESSION['prg']['shipping_address'];
    $shipping_address2     = $_SESSION['prg']['shipping_address2'];
    $shipping_city         = $_SESSION['prg']['shipping_city'];
    $shipping_state        = $_SESSION['prg']['shipping_state'];
    $shipping_zip          = $_SESSION['prg']['shipping_zip'];
    $errors                = $_SESSION['prg']['errors'];
}

 

This part is pretty straight forward. After checking to see if the form was submitted, and it is not when we do a redirect, we check to see if our session exists and we verify it is an array. That last part isn't really necessary but it helps us catch programming errors that may overwrite this variable with scalar variable. Better safe than sorry. Once we're sure we have this information we place it into the same variables we use to both collect and re-display the user's information. That means we don't have to do any extra work to display the information to the user!

 

One Loose End

 

So what happens when the user submits the form and everything is okay? We need to remove our session data or else if they come back to this page they'll see whatever error message and information the saw previously and that is exactly what we're trying to prevent. Fortunately that's easy to remedy. Once we verify the payment is made but before we redirect the user to the thank you page we need to unset the session variable. This way if they come back to this page our new else if statement won't be triggered and a harmless blank form will be presented.

 

Here's what that would look like:

 

if ($response->approved)
{
    // Nothing changes except we add this line here to
    // remove our session variable
    if (isset($_SESSION['prg']))
    {
        unset($_SESSION['prg']);
    }

    header('Location: thank-you-page.php');
    exit;
}

 

Now we can have our users use the back button without breaking our form or confusing themselves!

 

Our Form Page So Far

 

Now that we have some more code written, let's see our how our page looks now: (Update, the code is so long it is now an attachment).

 

What's Next?

 

At this point we have a fully functional checkout form that's usable and secure. But that doesn't mean we can't improve upon it. In the remaining posts in this series we are going to make incremental improvements to our form and payment process. We'll begin by preventing a page refresh from resubmitting the form again.

 

The Handling Online Payments Series

 

  1. Part 1 - Basic Information and Our Form
  2. Part 2 - Reading In And Sanitizing Submitted Data
  3. Part 3 - Data Validation
  4. Part 4 - Handling Validation Errors
  5. Part 5 - Processing Payment and Handling the Response
  6. Part 6 - Preventing Duplicate Submissions with POST/REDIRECT/GET
  7. Part 7 - Preventing Automated Form Submissions
  8. Part 8 - Using JavaScript To Increase Usability
  9. Part 9 - HTML and CSS Enhancements
  10. Part 10 - A Little Bit More PHP

---------------------------------------------------------------------------------------------------


John Conde is a certified Authorize.Net developer

Comments
by jmorrison on ‎03-16-2011 11:51 AM

Is storing the card data in session variables a violation of PCI-DSS?

by All Star ‎03-16-2011 01:15 PM - edited ‎03-16-2011 01:16 PM

@jmorrison It only is a violation if it is not encrypted (which is the case in this article). A future post in this series will show how to do that. An alternative is to not re-populate the credit card fields and have the user re-enter them if there is an error (anyone on a shared hosting platform and/or server that is not PCI compliant will probably need to do this).

by LesMizzell ‎05-31-2011 05:31 PM - edited ‎05-31-2011 05:32 PM

I'm coming into this form from another form post on another page. Which, of course, immediately triggers:
 if ('POST' === $_SERVER['REQUEST_METHOD']) and it thinks the payment form was submitted empty.

What would you consider the best way around this, but still leaving all the validation and error checking in place?

by All Star on ‎06-01-2011 06:34 AM

@LesMizzell You can add an additional check to the IF statement to look for a certain variable or exclude a certain variable.

 

For example, if your other form contains a submitted field called "cart". You can do this to ignore any submissions containing that value:

 

if ('POST' === $_SERVER['REQUEST_METHOD'] && !isset($_POST['cart'])) 

That way only form submissions from this form will pass the conditions in the IF statement.

by LesMizzell on ‎06-01-2011 05:04 PM

@stymiee That did the trick. Thanks

About the Author
  • Authorize.Net Developer Community Manager
Announcements
Labels