PHP

Reloading Data and Relocation Techniques

Simple updates using the approach shown in Example 6-1 are susceptible to a common problem of the stateless HTTP protocol that we call the reload problem. Consider what happens when a user successfully enters a new region name and description, and clicks the Submit button. Since the script is a combined script, the same code is executed for a second time, the HTTP encoded variables and values are passed through with the GET method request, a new row is inserted in the region table, and a success message is displayed. So far, everything is going according to plan.

Consider now what happens if the user reloads the success message page with the Reload or Refresh button in the browser. Unfortunately, the variables and values are resubmitted to the same script, and another region row-with the same name and description-is added to the region table. There is no way in this example that the first click of the Submit button to add the first row can be distinguished from a second action that sends the same variables and values to the script. A representation of the reload problem is shown in Figure 6-1.

Figure 6-1. The reload problem
figs/wda_0601.gif

The same reload problem occurs when the user stores the URL as a bookmark or favorite location in her browser and then later requests the URL. Other actions that return to the success page, such as using the Back button, have the same undesirable effect. Perhaps surprisingly, resizing the browser window or printing the page also creates a new HTTP request and causes the reload problem. In our case, each request for the URL adds another identical region to the winestore!

The reload problem occurs in many situations. Actions that rerequest a document from the server include pressing the Reload or Refresh buttons, printing, saving the URL in the browser and returning to the page using a bookmark or favorite, using the Back or Forward buttons, pressing the Enter key in the URL Location entry box, and resizing the browser window.

The reload problem isn't always a significant problem. For example, if you use the SQL UPDATE statement to update customer details, and the values are amended with the same correct values repeatedly, there is no data duplication. Indeed, if a row is deleted and the user repeats the operation, the user, at worst, sees a MySQL DBMS error message. However, while some UPDATE and DELETE operations are less susceptible to the reload problem, a well-designed system avoids the problem altogether. Avoidance prevents user confusion and unnecessary DBMS activity. We discuss a solution in a moment.

The HTTP POST method is a little less susceptible to the reload problem than the GET method. If a user reretrieves the script after the first database change, the browser should ask the user whether or not to repost form data as per the HTTP specification. If the user answers OK, the database operation is repeated causing the problem. However, if the user bookmarks the page or reenters the URL at a later time, the <form> is redisplayed because the POST variables and values aren't part of the URL and are lost.

A solution to the reload problem is shown in Figure 6-2, based on the HTTP Location: header, the same header used for one-component querying in Chapter 5.

Figure 6-2. Solving the reload problem with a redirection to a receipt page
figs/wda_0602.gif

The reload solution works as follows:

  1. The user submits the <form> with the variables and values for a database write operation (an SQL INSERT, UPDATE, or DELETE).

  2. The SQL write operation is attempted.

  3. Whether or not the modification is successful, an HTTP Location: header is sent to the browser to redirect the browser to a new, receipt page.

    HTTP GET encoded variables and values are usually included with the Location: header to indicate whether the action was successful or not. Additionally, text to display might be sent as part of the redirection URL.

  4. An informative-but harmless-receipt page is displayed to the user, including a success or failure message, and other appropriate text.

The HTTP redirection solves the reload problem. If the user reloads the receipt page the browser has been redirected to, he sees the receipt again, and no database write operations occur. Moreover, since the receipt page receives information about the success or failure of the operation-and any other information identifying the action-encoded in the URL, the receipt page URL can be saved and reloaded in the future without any undesirable effect.

Solving the reload problem in practice

A modified version of Example 6-1 with the redirect functionality is shown in Example 6-2. The code is almost identical to that of Example 6-1, with two exceptions.

The first difference in the script in Example 6-2 is that regardless of whether the database insert succeeds or fails, the header( ) function is called. This redirects the browser to the script shown in Example 6-3 by sending a Location: example.6-3.php HTTP header. The difference between the success and failure cases is what is appended to the URL as a query string. In the case of success, status=T and the value of the added region_id attribute are sent. A value of status=F is sent on failure.

The second difference is that the script allows the user to upload a map of the wine region in GIF format for storage in the database. We discuss this functionality in the next section. The script also uses the function mysql_insert_id( ); look for this function in the later section Section 6.1.3.

Example 6-2. An insertion script
<?php
  include 'db.inc';
  include 'error.inc';
  if (empty($regionName) || empty($description))
  {
?>
    <!DOCTYPE HTML PUBLIC
               "-//W3C//DTD HTML 4.0 Transitional//EN"
               "http://www.w3.org/TR/html4/loose.dtd">
    <html>
    <head>
      <title>Insert a Region</title>
    </head>
    <body>
    <form enctype="multipart/form-data"
     action="example.6-2.php" method="post">
    Region_name:
    <br><input type="text" name="regionName" size=80>
    <br>Description:
    <br><textarea name="description" rows=4 cols=80>
    </textarea>
    <input type="hidden"
      name="MAX_FILE_SIZE" value="100000">
    <br>Region map (GIF format):
    <input name="userfile" type="file">
    <br><input type="submit">
    </form>
    </body>
    </html>
<?php
  }
  else
  {
     $regionName = clean($regionName, 50);
     $description = clean($description, 2048);
     if (!($connection = @ mysql_connect($hostName,
                                       $username,
                                       $password)))
        die("Could not connect to database");
     if (!mysql_select_db($databaseName, $connection))
        showerror(  );
     // Was an image file uploaded?
     if (is_uploaded_file($userfile))
     {
        // Open the uploaded file
        $file = fopen($userfile, "r");
        // Read in the uploaded file
        $fileContents =
          fread($file, filesize($userfile));
        // Escape special characters in the file
        $fileContents = AddSlashes($fileContents);
     }
     else
       $fileContents = NULL;
    // Insert region data, including the image file
     $insertQuery = "INSERT INTO region VALUES
                    (NULL, " .
                    "\"" . $regionName . "\", " .
                    "\"" . $description . "\", " .
                    "\"" . $fileContents . "\")";
     if ((@ mysql_query ($insertQuery,
                         $connection))
        && @ mysql_affected_rows(  ) == 1)
           header("Location: example.6-3.php?" .
                 "regionId=". mysql_insert_id($connection) .
                 "&status=T");
        else
           header("Location: example.6-3.php?" .
                  "status=F");
  }
?>

The script in Example 6-3 is a receipt page. When requested with a parameter status=T, it queries the database and displays the details of the newly inserted region. The region is identified by the value of the query string variable regionId. The script also uses another script to display the image of the map inserted by the user; this approach is discussed next. On failure, where status=F, the script displays a database insertion failure message. If the script is unexpectedly called without a status parameter, an error message is displayed.

Example 6-3. The redirection receipt page
<!DOCTYPE HTML PUBLIC
               "-//W3C//DTD HTML 4.0 Transitional//EN"
               "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
  <title>Region Receipt</title>
</head>
<body bgcolor="white">
<?php
  include 'db.inc';
  include 'error.inc';
  $regionId = clean($regionId, 3);
  $status = clean($status, 1);
  // did the insert operation succeed?
  switch ($status)
  {
  case "T":
     // Yes, insert operation succeeded.
     // Show details of the new region as
     // a receipt page. The new region_id
     // is in the variable $regionId
     $query = "SELECT * FROM region WHERE " .
              "region_id = $regionId";
     // Connect to the MySQL DBMS
     if (!($connection = @ mysql_connect($hostName,
                                       $username,
                                       $password)))
        die("Could not connect to database");
     if (!mysql_select_db($databaseName, $connection))
        showerror(  );
     // Run the query on the DBMS
     if (!($result = @ mysql_query ($query, $connection)))
        showerror(  );
     if ($row = @ mysql_fetch_array($result))
     {
        echo "The following region was added";
        echo "\n<br>Region number: " . $row["region_id"];
        echo "\n<br>Region name: " . $row["region_name"];
        echo "\n<br>Region description: " .
             $row["description"];
        // Use the script example.6-4.php to display
        // the map GIF
        echo "\n<br>Region map : " .
             "\n<br><img src=\"example.6-4.php?region_id=" .
             $regionId . "\">";
     } // if mysql_fetch_array(  )
     // leave the switch statement
     break;
  case "F":
     // No, insert operation failed
     // Show an error message
     echo "The region insert operation failed.";
     echo "<br>Contact the winestore administrator.";
     // leave the switch statement
     break;
  default:
     // User did not provide a status parameter
     echo "You arrived unexpectedly at this page.";
  } // end of switch
?>
</body>
</html>

Several different receipt pages would be developed for an application to informatively display enough information for each different insert, update, and delete operation.