<

How to prevent a browser refresh from ruining your day

Published on
9,376 Points
3,276 Views
1 Endorsement
Last Modified:
Approved
Consider the following scenario: You are working on a website and make something great - something that lets the server work with information submitted by your users.

This could be anything, from a simple guestbook to a e-Money solution. But what happens when a user refreshes the website, and the information gets re-sent ? At best the user will create 2 identical entries in a guestbook, at worst your client will send his payment twice.

The information can be re-sent due to a multitude of reasons. The most common one is that the website does not load fast enough and the user gets antsy - and inevitably decides hitting F5 is a grand idea. The warning dialogue that may come up about re-sending data is blissfully ignored, and the deed is done.

Consider the following code:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" dir="ltr">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <title>Refresh woes</title>
    </head>
    <body>
        <?PHP
        if ($_GET['submit'] == 1) {
            echo 'Sending money - lets hope the client does not refresh and accidentially sends money twice!';
        } else {
            ?>
            <form action="<?= $_SERVER['PHP_SELF'] ?>?submit=1" method="post">
                <input type="submit" value="Send Money" />
            </form>
            <?PHP
        }
        ?>
    </body>
</html>

Open in new window


This code creates a simplified site that consists of a single Submit-button that, when clicked, leads to a different part of the same page that we DON'T want to have executed twice.

My preferred solution for this problem is a session with a transaction token. When stowed away in a function, it can be easily employed and adapted to any situation.

First, I will show the function that we will be using, it has comments for every part of the code, so you should be able to follow what's going on fairly easily:
/* createtoken()
 * Function to create a new token 
 * Returns the new token
 */

function createtoken() {
    /* First we need to check if a session token exists already */
    if (!count($_SESSION['token'])) {
        /* There is no previous session token for this client, so we need to create a new token */
        $txid = rand(1, 99999);
        /* Here we specify the amount of uses this token should have. 1 is recommended */
        $_SESSION['token']['_' . $txid] = 1;
    }

    /* Next we check if the current token is out of uses */
    if (current($_SESSION['token']) == 0) {
        /* We preserve the old token to make sure it does not randomly roll the same number again */
        $oldtx = key($_SESSION['token']);
        unset($_SESSION['token']);

        /* This loop prevents the same transaction ID from re-appearing. */
        do {
            $txid = rand(1, 99999);
        } while ('_' . $txid == $oldtx);

        $_SESSION['token']['_' . $txid] = 1;
    }

    /* And finally, we return the new transaction ID */
    return $curtx = key($_SESSION['token']);
}

Open in new window


After you have written the function, you can very easily apply it to all your forms and pages. You only need to follow these guidelines:
1) Make sure you call session_start(); at the top of each page
2) In forms, pass along the transaction ID ( e.g. <input type="hidden" name="tx" value="<?=$curtx?>"/> )
3) Use $_SESSION['token'][$_POST['tx']]--; on pages which should use up a token.
4) Call $curtx = createtoken(); whenever you want to refresh the token.

If this sounds confusing, take a look at the final code below and I am sure you will understand:
<?PHP
/* We have to start the session in order to use the session functionality */
session_start();

/* createtoken()
 * Function to create a new token 
 * Returns the new token
 */

function createtoken() {
    /* First we need to check if a session token exists already */
    if (!count($_SESSION['token'])) {
        /* There is no previous session token for this client, so we need to create a new token */
        $txid = rand(1, 99999);
        /* Here we specify the amount of uses this token should have. 1 is recommended */
        $_SESSION['token']['_' . $txid] = 1;
    }

    /* Next we check if the current token is out of uses */
    if (current($_SESSION['token']) == 0) {
        /* We preserve the old token to make sure it does not randomly roll the same number again */
        $oldtx = key($_SESSION['token']);
        unset($_SESSION['token']);

        /* This loop prevents the same transaction ID from re-appearing. */
        do {
            $txid = rand(1, 99999);
        } while ('_' . $txid == $oldtx);

        $_SESSION['token']['_' . $txid] = 1;
    }

    /* And finally, we return the new transaction ID */
    return $curtx = key($_SESSION['token']);
}

/* start the token system */
$curtx = createtoken();
?>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" dir="ltr">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <title>Refresh woes</title>
    </head>

    <body>
        <?PHP
        if ($_GET['submit'] == 1) {
            /* This information needs to be added to the part of the page that you DON'T want to execute twice
             * First we need to test if we currently have a valid token, and if we do not, we output a error. */
            if ((!isset($_SESSION['token'][$_POST['tx']]) || $_SESSION['token'][$_POST['tx']] <= 0)) {
                echo '<p>Prevented page reload from re-sending money.</p>';
            } else {
                /* Removing one token use */
                $_SESSION['token'][$_POST['tx']]--;
                echo '<p>Sending money - now refresh-protected!</p>';
            }
        } else {
            ?>
            <form action="<?= $_SERVER['PHP_SELF'] ?>?submit=1" method="post">
                <!-- We need to pass along the current transaction ID -->
                <input type="hidden" name="tx" value="<?= $curtx ?>"/>
                <input type="submit" value="Send Money" />
            </form>
            <?PHP
        }
        ?>
    </body>
</html>

Open in new window


This solution blocks the following:
Clients revisiting that particular part of the site (e.g. just typing in their address bar: sendmoney.php?submit=1 )
Clients reloading the critical stage of the page
Clients using their browsers back / forward history

It is also completely serverside and as such will work with any browser your clients may use.

I hope this solution helps you and thank you for reading!
1
Author:Midboss
Ask questions about what you read
If you have a question about something within an article, you can receive help directly from the article author. Experts Exchange article authors are available to answer questions and further the discussion.
Get 7 days free