How to handle browser redirections in PHP and preventing loops

When programming web applications it happens that I need to redirect a user from one url to another. One example is when the user has made a payment and is redirected back from the payment provider’s portal to my summary page. In that situation I don’t want the user to stay on that page with that URL, not because anything secret is shown in the URL, but just because users far too often tend to bookmark that URL. Everytime they visit that bookmark the system will then get an “invalid payment response” with a lot of POST fields missing, leading the system to create “hacking attempt warnings”. But by redirecting the user, immediately after they come from the payment provider’s portal, I can choose what URL that will be visible for the user and the bookmarking is no longer any problem.

The code below shows a simple function solving this situation in a simple manner. To use the function simply make any changes you want to the $_GET array and then call the browserRedirect() function to make the redirection. Preferable you should end the code execution after the redirection but you might have some cleaning up left to do.

// Change or add the show property from the query string
$_GET["show"] = "summary";  

// Delete the tag property from the query string
unset($_GET["tag"]); 

browserRedirect();
exit();

The code for the function is hopefully quite self explanatory with all the comments added.

function browserRedirect()
{
    // Extract previous hash in case we're trying to make two 
    // redirects in a row
    $previousHash = "";
    if (isset($_GET["redirection_loop"])) {
        $previousHash = $_GET["redirection_loop"];

        // Remove this variable from $_GET so it won't be included in 
        // the new hash
        unset($_GET["redirection_loop"]);
    }

    // Calculate hash for new redirection. 
    $incomingHash = md5(serialize($_GET));

    // Compare new hash to previous hash to detect and prevent 
    // redirection loops
    if ($previousHash != $incomingHash) {
        // Set new redirection hash
        $_GET["redirection_loop"] = $incomingHash;

        // Construct new URL
        $qs = http_build_query($_GET);
        $newUrl = "http" . (($_SERVER['SERVER_PORT']==443) ? "s://" : "://").
                  $_SERVER["HTTP_HOST"].$_SERVER['PHP_SELF']."?$qs";

        // For the redirection header to work, remember to turn on 
        // output buffering!
        header("Location: $newUrl");

        // Finally we display a redirection link in case the browser 
        // won't redirect the user. We use the same URL as we're giving 
        // to the redirection header. This is just for backup.
        echo "
Redirection
Your payment has been registered and you're being redirected to the summary page! If your browser isn't redirecting you within 10 seconds then please click here to manually go there.
"; } else { // Detected a redirection loop so we won't be doing a new redirection echo "Detected a redirection loop so we won't be redirecting any more"; } }

Output buffer
The redirection header needs to be sent first back to the user. Most likely you’ve already started to produce some data to be displayed. To come around this we need to use the output buffer. This is easily done by adding a call to ob_start() in the beginning of your script.

ob_start();

When using the output buffer you can send header commands anywhere in the code and they will be sent first to the user even if you’ve started to send other kind of data.

At the end of your script you also need to make a flush call to send the output buffer to the browser.

ob_end_flush();

Backup message
In case the output buffer fails so the header can’t be sent back to the browser, or if the browser ignores the redirection, we need a backup. This is made by adding a redirection instruction text for the user directly after the redirection header. If the header fails then this code will be shown to the user who then can click themselves to the next summary page.

The code for this message is simply:

// Finally we display a redirection link in case the browser 
// won't redirect the user. We use the same URL as we're giving 
// to the redirection header. This is just for backup.
echo "
Redirection
Your payment has been registered and you're being redirected to the summary page! If your browser isn't redirecting you within 10 seconds then please click here to manually go there.
";

This is what the message would look like with some CSS tags added as well.
Redirection in PHP

Redirection loop
The nifty thing about this function is that it also can detect redirection loops, meaning loops where you redirect the user to the same address as the current one and thus ending up in an infinite redirection loop. This is done by hashing the query string and attaching this hash to the query string. Redirections can now be made, if needed, one after another as long as they are not leading to the same URL.

Here is a simple code snippet to test the redirection is a serie of three redirections, starting at 1 redirecting all the way up to 4. If you change the code below and comment out one of the lines setting $_GET to the next number, the loop detection kicks in and breaks the redirection.

$step = 1;
if (isset($_GET["step"]))
    $step = $_GET["step"];

if ($step == 1) 
{
    $_GET["step"] = 2;
    browserRedirect();
} 
elseif ($step == 2) 
{
    // Comment out the following line to test redirection loop detection
    $_GET["step"] = 3;
    browserRedirect();
} 
elseif ($step == 3) 
{
    $_GET["step"] = 4;
    browserRedirect();
} 
else 
{
    echo "You have reached step $step";
}

Caveats
Redirections are a great feature but there are a few things to keep in mind when using browser redirections.

  • The code doesn’t take $_POST variables into consideration. Most of the time though, as in the example above with a payment response redirection, this is not needed at all.
  • Infinite loops where 1 is pointing to 2 pointing to 1 pointing to 2 etc won’t be detected. This could be detected by having a redirection counter that limits the number of direct following redirects.
  • The code assumes HTTPS using port 443 but this can differ on internal test systems.
  • This functionality, with the ability to redirect a user from anywhere in your code, is difficult to implement without the output buffering turned on. Output buffering is costly on the server since everything is stored in memory, but the benefits might be greater.