How to prevent a browser refresh from ruining your day

Published:
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
3,543 Views

Comments (0)

Have a question about something in this article? You can receive help directly from the article author. Sign up for a free trial to get started.