|
Custom Error Pages with PHP and Apacheby David Sklar, coauthor of PHP Cookbook02/13/2003 |
Using PHP and Apache, you can turn your "Page Not Found" messages into more than bland error reports. You can serve an alternate page based on the name of the page that was not found, create a page on the fly from a database, or send an email about the missing page to a webmaster.
Building a custom error page with PHP and Apache requires two steps. You need to tell Apache to run a PHP program when it encounters a 404 ("Page Not Found") error. And you need to write the corresponding program that takes the appropriate action.
Configuring Apache
To tell Apache what to do on a 404 error, use the
ErrorDocument directive:
ErrorDocument 404 /error-404.php
This tells Apache to serve up error-404.php in the
document root directory when it encounters a 404 error. The
ErrorDocument directive can go in Apache's
httpd.conf file, but it also works in .htaccess files in
individual directories. You can have a site-wide error-handling page or
different error-handling pages for different parts of your site. Apache
also sets some server variables that the error-handling page can
access:
|
Related Reading
PHP Cookbook |
REDIRECT_URL: the URL-path that was not found. If a user asks for the nonexistent page http://www.example.com/lunch/pastrami.html, for example, this variable is set to/lunch/pastrami.html.REDIRECT_STATUS: the HTTP response status resulting from the request for the original page. In our case, this is always "404". You can useErrorDocumentwith other status codes, though, so if you have one error-handling page for multiple statuses, you can use this variable to determine which error status caused the error-handling page to be loaded.REDIRECT_ERROR_NOTES: a brief description of what went wrong, for example, "File does not exist: /usr/local/apache/docroot/lunch/pastrami.html".REDIRECT_REQUEST_METHOD: the method of the request for the original page, such asGETorPOST.
If there is a query string in the original request, it is stored in
REDIRECT_QUERY_STRING. The error page does not have access to
the GET or POST variables via
$_GET, $_POST, or $_REQUEST, but
cookie variables are still available in $_COOKIE.
These REDIRECT variables are available in the PHP
superglobal array $_SERVER:
$_SERVER['REDIRECT_URL'],
$_SERVER['REDIRECT_STATUS'], and so forth.
Taking Action
The information in the REDIRECT variables can be used to
do many different things in response to a request for a nonexistent
page. If your site has been recently reorganized, you can transparently
redirect users to the new URL that corresponds to a particular old
URL:
<?php
$map = array('/old/1' => '/new/2.html',
'/old/2' => '/new/3.html');
if (isset($map[$_SERVER['REDIRECT_URL']])) {
$new_loc = 'http://' .
$_SERVER['HTTP_HOST'] .
$map[$_SERVER['REDIRECT_URL']];
if (isset($_SERVER['REDIRECT_QUERY_STRING'])) {
$new_loc .= '?' .
$_SERVER['REDIRECT_QUERY_STRING'];
}
header("Location: $new_loc");
} else {
print "This page is really not found.";
}
?>
A redirect response needs to include the query string in the redirect
URL if the query string was present in the original request. Redirects
always use the GET method. Including the query string
preserves any GET variables from the original request, but
POST data is lost.
Additionally, the protocol and host name need to be at the beginning of
the redirect URL sent with the Location header. This example hardcodes
"http" as the protocol and gets the host name from the
HTTP_HOST server variable. To work transparently under https
as well as http, your code should test for the presence of
$_SERVER['HTTPS']. If this variable is set to "on", then the
protocol should be "https" instead of "http".
Basic redirection could also be accomplished with a list of Apache
Redirect or RedirectMatch directives, but you
can construct more complicated expressions in PHP. You can easily redirect
multiple old URLs to the same new URL:
<?php
$rev_map = array('new.html' =>
array('/old-1.html',
'/old-2.html',
'/old-3.html'));
foreach ($rev_map as $new => $ar) {
foreach ($ar as $old) {
$map[$old] = $new;
}
}
if (isset($map[$_SERVER['REDIRECT_URL']])) {
$new_loc = 'http://' .
$_SERVER['HTTP_HOST'] .
$map[$_SERVER['REDIRECT_URL']];
if (isset($_SERVER['REDIRECT_QUERY_STRING'])) {
$new_loc .= '?' .
$_SERVER['REDIRECT_QUERY_STRING'];
}
header("Location: $new_loc");
} else {
print "This page is really not found.";
}
?>
You can look up the new URLs to which the old ones map in a database:
<?php
mysql_connect('localhost','user','password');
mysql_select_db('pages');
// escape quotes and SQL wildcards from the old URL
$old_page = mysql_real_escape_string($_SERVER['REDIRECT_URL']);
$old_page = strtr($old_page,array('_' => '\_',
'%' => '\%'));
$r = mysql_query("SELECT new FROM pages
WHERE old LIKE '$old_page'");
if (mysql_numrows($r) == 1) {
$ob = mysql_fetch_object($r);
$new_loc = 'http://' .
$_SERVER['HTTP_HOST'] . $ob->new;
if (isset($_SERVER['REDIRECT_QUERY_STRING'])) {
$new_loc .= '?' .
$_SERVER['REDIRECT_QUERY_STRING'];
}
header("Location: $new_loc");
} else {
print "This page is really not found.";
}
?>
If you need to use values from
$_SERVER['REDIRECT_QUERY_STRING'] into variables to determine
the new URL, parse the query string with parse_str(). If
$_SERVER['REDIRECT_QUERY_STRING'] is
artist=weird+al&album=dare+to+be+stupid, then
parse_str($_SERVER['REDIRECT_QUERY_STRING'],$vars) sets
$vars['artist'] to "weird al" and $vars['album']
to "dare to be stupid".
You can even use the error document to make a simple caching system. If a page isn't found, get its contents from your database and write them to disk. Then, redirect the user to the same URL they just asked for. Since the page now exists, they'll get it, and not the error page:
<?php
mysql_connect('localhost','user','password');
mysql_select_db('pages');
// escape quotes and SQL wildcards from the old URL
$url = mysql_real_escape_string($_SERVER['REDIRECT_URL']);
$url = strtr($url,array('_' => '\_',
'%' => '\%'));
// look for the page in the database
$r = mysql_query("SELECT page FROM pages
WHERE url LIKE '$url'");
if (mysql_numrows($r) == 1) {
$ob = mysql_fetch_object($r);
if ($fp = fopen($_SERVER['DOCUMENT_ROOT'] .
$_SERVER['REDIRECT_URL'],'w')) {
// write the page to disk
fwrite($fp,$ob->page);
fclose($fp);
// send the user back to the same URL
$new_loc = 'http://' .
$_SERVER['HTTP_HOST'] .
$_SERVER['REDIRECT_URL'];
if (isset($_SERVER['REDIRECT_QUERY_STRING'])) {
$new_loc .= '?' .
$_SERVER['REDIRECT_QUERY_STRING'];
}
header("Location: $new_loc");
} else {
// couldn't generate the page
print "This page is really not found.";
}
} else {
// couldn't find the page in the database
print "This page is really not found.";
}
?>
In this example, the entire contents of a page are stored in the page
column of the pages table and are written to a file with
fwrite(). You could do more interesting or complicated things
when generating a page, like pull multiple pieces of the page from
different places or populate a template with dynamic data. However you
generate the page, publishing a new version of it is easy. Just update the
database and delete the file from disk. The next time a user asks for that
page, it won't be found. The error-handling page will load the updated
page (or its components) from the database and write the new version to a
file.
If you're sending a user to a new PHP page, it's important to use a
redirect instead of just loading the page with include(). The
error page doesn't have GET or POST variables
set, and some server variables are different (for example,
$_SERVER['PHP_SELF'] points to the error page, not the
original URL.) If you're sending the user to a static page, however,
including content without a redirect can be useful. You can use an
error-handling page to provide access to a library of files without
keeping the files under the web server document root, for example:
<?php
$file_root = '/usr/local/songs/';
$song = strtolower($_SERVER['REDIRECT_URL']);
$song_file = realpath($file_root .
substr($song,1,1) .
"/$song.mp3");
if (preg_match("{^$file_root}",$song_file) &&
is_readable($song_file)) {
header('Status: 200 Found');
header('Content-type: audio/mpeg');
header('Content-disposition: attachment; filename=' .
$song . '.mp3');
readfile($song_file);
} else {
print "Unknown song.";
}
?>
If this error-handling page is set up for the root directory of
http://www.example.com/, asking for
http://www.example.com/EatIt sends you the file
/usr/local/songs/e/eatit.mp3, if that file exists. Checking to
see whether the output of realpath() begins with
$file_root prevents a user from passing directory-changing
strings like "/../" in the URL. If a file is found, the page
sends the right status code and headers to tell the user that they're
getting an MP3 file and then sends the contents of the song file.
The error-handling page doesn't just have to find a new page to send to users. It can notify the webmaster that a page is missing. You can use this to find out if your own site has bad links to itself:
if (preg_match('{^http(s)?://'.$_SERVER['HTTP_HOST'].'}',
$_SERVER['HTTP_REFERER'])) {
ob_start();
print_r($_SERVER);
$data = ob_get_contents();
ob_end_clean();
mail($_SERVER['SERVER_ADMIN'],
'Page Not Found: '.$_SERVER['REDIRECT_URL'],
$data);
}
The preg_match() statement finds referrer URLs that are on
the same host as the current request by comparing the beginning of the
referring URL to the $_SERVER['HTTP_HOST']. If they match,
the output of print_r($_SERVER) is stored in
$data using output buffering:
ob_start()tells PHP to capture output in a buffer instead of printing it.ob_get_contents()returns the contents of that buffer.ob_end_clean()turns off output buffering without printing the buffer.
The mail() function sends a message to the server
administrator. The body of the message (all the $_SERVER
variables in $data) contains the referring URL and other
information that you can use to fix the page with the bad link.
More Information
Documentation for Apache custom error responses is at http://httpd.apache.org/docs/custom-error.html. The www.php.net site uses a custom error response to turn handy shortcut URLs like http://www.php.net/xml into the correct URL for the XML section of the manual. You can see the source code to it at http://cvs.php.net/co.php/phpweb/error/index.php?r=HEAD.
O'Reilly & Associates recently released (November 2002) PHP Cookbook .
Sample Chapter 8, Web Basics, is available free online.
You can also look at the Table of Contents, the Index, and the Full Description of the book.
For more information, or to order the book, click here.
David Sklar is an independent consultant in New York City, the author of O'Reilly's Learning PHP 5, and a coauthor of PHP Cookbook.
Return to ONLamp.com.
You must be logged in to the O'Reilly Network to post a talkback.
Showing messages 1 through 6 of 6.
-
Redirect Class
2003-04-26 23:11:52 anonymous2 [Reply | View]
This might be useful to people getting started with this.
<?php
define('DIR_SLASH', '/');
/**
* Redirect Class
* Author : Cameron Green
*
* History :
* 20030427 camerongreen at wildmail dot com
* started work
*
* Simple class to work with custom error pages
*
* To set up redirects in Apache :
* ErrorDocument 404 /error.php
*
* Where error.php is a file in your documentroot which includes and
* instantiates this class and does something useful with it
*/
class Redirect {
/**
* query_string
*
* Holds redirect query string, parsed into
* key valu pairs
*
* @var array $query_string
* @access private
*/
var $query_string;
/**
* url_parts
*
* Hash of 'file' - requested file, and 'dirs' - array of requested directories
*
* @var array $url_parts
* @access private
*/
var $url_parts;
/**
* Constructor
*
* @access public
*/
function Redirect() {
}
/**
* get status
*
* returns REDIRECT_STATUS
*
* @access public
* @return mixed $status string or false if not set
*/
function get_status() {
return (isset($_SERVER['REDIRECT_STATUS']) ? $_SERVER['REDIRECT_STATUS'] : FALSE);
}
/**
* get url
*
* returns REDIRECT_URL
*
* @access public
* @return mixed $url string or false if not set
*/
function get_url() {
return (isset($_SERVER['REDIRECT_URL']) ? $_SERVER['REDIRECT_URL'] : FALSE);
}
/**
* get request method
*
* returns REDIRECT_REQUEST_METHOD
*
* @access public
* @return mixed $request_method string or false if not set
*/
function get_request_method() {
return (isset($_SERVER['REDIRECT_REQUEST_METHOD']) ? $_SERVER['REDIRECT_REQUEST_METHOD'] : FALSE);
}
/**
* get error notes
*
* returns REDIRECT_ERROR_NOTES
*
* @access public
* @return mixed $error_notes string or false if not set
*/
function get_error_notes() {
return (isset($_SERVER['REDIRECT_ERROR_NOTES']) ? $_SERVER['REDIRECT_ERROR_NOTES'] : FALSE);
}
/**
* get raw query string
*
* returns REDIRECT_QUERY_STRING
*
* @access public
* @return mixed $query_string string or false if not set
*/
function get_raw_query_string() {
return (isset($_SERVER['REDIRECT_QUERY_STRING']) ? $_SERVER['REDIRECT_QUERY_STRING'] : FALSE);
}
/**
* Get query string
*
* @access private
* @return mixed $query_string array or false if not set
*/
function get_query_string() {
// if its already set, don't do it again
if (!is_array($this->query_string)) {
if ($query_string = $this->get_raw_query_string()) {
$this->query_string = $this->parse_query_string($query_string);
}
else {
return (FALSE);
}
}
return ($this->query_string);
}
/**
* Get url
*
* @access private
* @return array $url
*/
function get_url_parts() {
// make sure var exists
if (isset($_SERVER['REDIRECT_URL'])) {
// if its already set, don't do it again
if (!is_array($this->url_parts)) {
$this->parse_url($_SERVER['REDIRECT_URL']);
}
}
return ($this->url_parts);
}
/**
* To String
*
* Returns all parameters in string suitable for output to pre
*
* @access public
* @return string $values array as nested string
*/
function to_string() {
$url = $this->get_url_parts();
$output =
"REDIRECT_URL =" . implode($url['dirs'], DIR_SLASH) . (empty($url['file']) ? '' : DIR_SLASH . $url['file']) . "\n" .
"REDIRECT_STATUS =" . $this->get_status() . "\n" .
"REDIRECT_ERROR_NOTES =" . $this->get_error_notes() . "\n" .
"REDIRECT_REQUEST_METHOD =" . $this->get_request_method() . "\n" .
"REDIRECT_QUERY_STRING =" . $this->make_query_string($this->get_query_string()) . "\n";
return ($output);
}
/**
* Make Query String
*
* Takes an associative array and makes a query
* string out of it, encoding values
*
* @param array $assoc_array
* @access public
* @return string $query
*/
function make_query_string($assoc_array) {
if (is_array($assoc_array)) {
$query_array = array();
foreach ($assoc_array as $key => $value) {
$query_array[] = $key . "=" . urlencode($value);
}
return (implode($query_array, "&"));
}
return (FALSE);
}
/**
* parse_query_string
*
* I know there is parse_str, but that puts things into
* global scope which is insane for most things
*
* @param string $query
* @access private
* @return array $query_elements
*/
function parse_query_string($query) {
$return_val = array();
foreach (explode('&', $query) as $key => $value) {
list($attribute, $attribute_value) = explode('=', $value);
$return_val[$attribute] = $attribute_value;
}
return($return_val);
}
/**
* parse_url
*
* takes the passed in url and sets the url_parts
* instance var to the directories and file_name requested
*
* @param string $url
* @access private
*/
function parse_url($url) {
$this->url_parts = array();
$parts = explode(DIR_SLASH, $url);
$potential_file = array_pop($parts);
// the only way we can presume the user has
// requested a file, is if there is a dot in this field
if (strpos($potential_file, '.') === FALSE) {
// put it back on
array_push($parts, $potential_file);
}
else {
$this->url_parts['file'] = $potential_file;
}
$this->url_parts['dirs'] = array_filter($parts, 'not_empty');
}
}
/**
* not empty
*
* call back function to remove empty values from array
*
* @param string $var
* @access public
* @return bool $not_empty
*/
function not_empty($var) {
return (!empty($var));
}
?>
-
Keep it simple and stupid
2006-09-01 12:05:39 CagdasCubukcu [Reply | View]
Very simple error page to help everyone get started.
Open apache configuration file httpd.conf probably at your "conf" directory in your Apache folder.
Search the line "ErrorDocument 404 /missing.html"
Uncomment it.
Change it as "ErrorDocument 404 /missing.php"
Create a new php page in your "htdocs" folder. Name it missing.php.
Put the code inside:
<?php
ob_start();
echo "<p>Page you are looking is not found :)";
header("Refresh: 2; javascript:history.back()");
?>
You are all set! When a page is not found, you will be directed to this page which will automatically take you back in 2 seconds.
-
Search and find
2007-07-03 00:35:43 ageertsma [Reply | View]
Remember that a lot of 404's will be generated from old links. Links on blogs, links from banners or links from search engines.
Search engines might match the content they expect with the content displayed. So, if you have a site update that changes URLs, try to catch the known old URLs and display the old content. You might lose your search engine ranking if you don't.
Catching the 404 and redirecting to the expected content might not be the cleanest way to handle things but it is quite easy. Especially when you change languages (from asp to php for example).
-
I can't get it to work!
2008-01-01 02:23:52 dawmail333 [Reply | View]
I can't get it to work?
Here's the code that I use on the error page:
$loc = isset($_SERVER['REDIRECT_URL']) ? $_SERVER['REDIRECT_URL'] : FALSE;
$cod = isset($_SERVER['REDIRECT_STATUS']) ? $_SERVER['REDIRECT_STATUS'] : FALSE;
print($loc);
print(" ");
print($cod);
However, when the page is displayed, it shows the error code correctly, but it prints the url to the error page, not the page they were trying to go to.
This is my .htaccess:
ErrorDocument 404 /err404.php
ErrorDocument 403 /err403.php
ErrorDocument 500 /err500.php
(Linebreak intentional)
Can anyone tell me what is going wrong? Thanks in advance.








PHP.net also does db queries and searches, which are both awesome ways to help users get where they originally intended to be.