48
}
Here's an example of how to use
pc_validate()
:
if (! pc_validate($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'])) {
header('WWW-Authenticate: Basic realm="My Website"');
header('HTTP/1.0 401 Unauthorized');
echo "You need to enter a valid username and password.";
exit;
}
Replace the contents of the
pc_validate( )
function with appropriate logic to determine if a
user entered the correct password. You can also change the realm string from "My Website"
and the message that gets printed if a user hits "cancel" in their browser's authentication box
from "You need to enter a valid username and password."
HTTP Basic authentication can't be used if you're running PHP as a CGI. If you can't run PHP
as a server module, you can use cookie authentication, discussed in Recipe 8.11
.
Another issue with HTTP Basic authentication is that it provides no simple way for a user to log
out, other then to exit his browser. The PHP online manual has a few suggestions for log out
methods that work with varying degrees of success with different server and browser
combinations at http://www.php.net/features.http-auth
.
There is a straightforward way, however, to force a user to log out after a fixed time interval:
include a time calculation in the realm string. Browsers use the same username and password
combination every time they're asked for credentials in the same realm. By changing the
realm name, the browser is forced to ask the user for new credentials. For example, this
forces a log out every night at midnight:
if (! pc_validate($_SERVER['PHP_AUTH_USER'],$_SERVER['PHP_AUTH_PW'])) {
$realm = 'My Website for '.date('Y-m-d');
header('WWW-Authenticate: Basic realm="'.$realm.'"');
header('HTTP/1.0 401 Unauthorized');
echo "You need to enter a valid username and password.";
exit;
}
You can also have a user-specific timeout without changing the realm name by storing the
time that a user logs in or accesses a protected page. The
pc_validate()
function in
Example 8-3
stores login time in a database and forces a log out if it's been more than 15
minutes since the user last requested a protected page.
Example 8-3. pc_validate2( )
function pc_validate2($user,$pass) {
$safe_user = strtr(addslashes($user),array('_' => '\_', '%' => '\%'));
$r = mysql_query("SELECT password,last_access
FROM users WHERE user LIKE '$safe_user'");
43
if (mysql_numrows($r) == 1) {
$ob = mysql_fetch_object($r);
if ($ob->password == $pass) {
$now = time();
if (($now - $ob->last_access) > (15 * 60)) {
return false;
} else {
// update the last access time
mysql_query("UPDATE users SET last_access = NOW()
WHERE user LIKE '$safe_user'");
return true;
}
}
} else {
return false;
}
}
For example:
if (! pc_validate($_SERVER['PHP_AUTH_USER'],$_SERVER['PHP_AUTH_PW'])) {
header('WWW-Authenticate: Basic realm="My Website"');
header('HTTP/1.0 401 Unauthorized');
echo "You need to enter a valid username and password.";
exit;
}
8.10.4 See Also
Recipe 8.11
; the HTTP Authentication section of the PHP online manual at
http://www.php.net/features.http-auth
.
Recipe 8.11 Using Cookie Authentication
8.11.1 Problem
You want more control over the user login procedure, such as presenting your own login form.
8.11.2 Solution
Store authentication status in a cookie or as part of a session. When a user logs in
successfully, put their username in a cookie. Also include a hash of the username and a secret
word so a user can't just make up an authentication cookie with a username in it:
$secret_word = 'if i ate spinach';
if (pc_validate($_REQUEST['username'],$_REQUEST['password'])) {
setcookie('login',
$_REQUEST['username'].','.md5($_REQUEST['username'].$secret_word));
}
8.11.3 Discussion
When using cookie authentication, you have to display your own login form:
57
<form method="post" action="login.php">
Username: <input type="text" name="username"> <br>
Password: <input type="password" name="password"> <br>
<input type="submit" value="Log In">
</form>
You can use the same
pc_validate( )
function from the Recipe 8.10
to verify the username
and password. The only difference is that you pass it
$_REQUEST['username']
and
$_REQUEST['password']
as the credentials instead of
$_SERVER['PHP_AUTH_USER']
and
$_SERVER['PHP_AUTH_PW']
. If the password checks out, send back a cookie that contains a
username and a hash of the username, and a secret word. The hash prevents a user from
faking a login just by sending a cookie with a username in it.
Once the user has logged in, a page just needs to verify that a valid login cookie was sent in
order to do special things for that logged-in user:
unset($username);
if ($_COOKIE['login']) {
list($c_username,$cookie_hash) = split(',',$_COOKIE['login']);
if (md5($c_username.$secret_word) == $cookie_hash) {
$username = $c_username;
} else {
print "You have sent a bad cookie.";
}
}
if ($username) {
print "Welcome, $username.";
} else {
print "Welcome, anonymous user.";
}
If you use the built-in session support, you can add the username and hash to the session and
avoid sending a separate cookie. When someone logs in, set an additional variable in the
session instead of sending a cookie:
if (pc_validate($_REQUEST['username'],$_REQUEST['password'])) {
$_SESSION['login'] =
$_REQUEST['username'].','.md5($_REQUEST['username'].$secret_word));
}
The verification code is almost the same; it just uses
$_SESSION
instead of
$_COOKIE
:
unset($username);
if ($_SESSION['login']) {
list($c_username,$cookie_hash) = explode(',',$_SESSION['login']);
if (md5($c_username.$secret_word) == $cookie_hash) {
$username = $c_username;
} else {
print "You have tampered with your session.";
}
}
47
Using cookie or session authentication instead of HTTP Basic authentication makes it much
easier for users to log out: you just delete their login cookie or remove the login variable from
their session. Another advantage of storing authentication information in a session is that you
can link users' browsing activities while logged in to their browsing activities before they log in
or after they log out. With HTTP Basic authentication, you have no way of tying the requests
with a username to the requests that the same user made before they supplied a username.
Looking for requests from the same IP address is error-prone, especially if the user is behind a
firewall or proxy server. If you are using sessions, you can modify the login procedure to log
the connection between session ID and username:
if (pc_validate($_REQUEST['username'],$_REQUEST['password'])) {
$_SESSION['login'] =
$_REQUEST['username'].','.md5($_REQUEST['username'].$secret_word));
error_log('Session id '.session_id().' log in as
'.$_REQUEST['username']);
}
This example writes a message to the error log, but it could just as easily record the
information in a database that you could use in your analysis of site usage and traffic.
One danger of using session IDs is that sessions are hijackable. If Alice guesses Bob's session
ID, she can masquerade as Bob to the web server. The session module has two optional
configuration directives that help you make session IDs harder to guess. The
session.entropy_file
directive contains a path to a device or file that generates
randomness, such as /dev/random or /dev/urandom. The
session.entropy_length
directive holds the number of bytes to be read from the entropy file when creating session
IDs.
No matter how hard session IDs are to guess, they can also be stolen if they are sent in clear
text between your server and a user's browser. HTTP Basic authentication also has this
problem. Use SSL to guard against network sniffing, as described in Recipe 14.11
.
8.11.4 See Also
Recipe 8.10
; Recipe 8.18
discusses logging errors; Recipe 14.4
discusses verifying data with
hashes; documentation on
setcookie( )
at http://www.php.net/setcookie
and on
md5( )
at http://www.php.net/md5
.
Recipe 8.12 Flushing Output to the Browser
8.12.1 Problem
You want to force output to be sent to the browser. For example, before doing a slow database
query, you want to give the user a status update.
8.12.2 Solution
47
Use
flush( )
:
print 'Finding identical snowflakes...';
flush();
$sth = $dbh->query(
'SELECT shape,COUNT(*) AS c FROM snowflakes GROUP BY shape HAVING c >
1');
8.12.3 Discussion
The
flush( )
function sends all output that PHP has internally buffered to the web server,
but the web server may have internal buffering of its own that delays when the data reaches
the browser. Additionally, some browsers don't display data immediately upon receiving it,
and some versions of Internet Explorer don't display a page until they've received at least 256
bytes. To force IE to display content, print blank spaces at the beginning of the page:
print str_repeat(' ',300);
print 'Finding identical snowflakes...';
flush();
$sth = $dbh->query(
'SELECT shape,COUNT(*) AS c FROM snowflakes GROUP BY shape HAVING c >
1');
8.12.4 See Also
Recipe 18.18
; documentation on
flush( )
at http://www.php.net/flush
.
Recipe 8.13 Buffering Output to the Browser
8.13.1 Problem
You want to start generating output before you're finished sending headers or cookies.
8.13.2 Solution
Call
ob_start( )
at the top of your page and
ob_end_flush( )
at the bottom. You can
then intermix commands that generate output and commands that send headers. The output
won't be sent until
ob_end_flush( )
is called:
<?php ob_start(); ?>
I haven't decided if I want to send a cookie yet.
<?php setcookie('heron','great blue'); ?>
Yes, sending that cookie was the right decision.
<?php ob_end_flush(); ?>
8.13.3 Discussion
51
You can pass
ob_start( )
the name of a callback function to process the output buffer with
that function. This is useful for postprocessing all the content in a page, such as hiding email
addresses from address-harvesting robots:
<?php
function mangle_email($s) {
return preg_replace('/([^@\s]+)@([-a-z0-9]+\.)+[a-z]{2,}/is',
'<$1@...>',
$s);
}
ob_start('mangle_email');
?>
I would not like spam sent to ronald@example.com!
<?php ob_end_flush(); ?>
The
mangle_email( )
function transforms the output to:
I would not like spam sent to <ronald@...>!
The
output_buffering
configuration directive turns output buffering on for all pages:
output_buffering = On
Similarly,
output_handler
sets an output buffer processing callback to be used on all pages:
output_handler=mangle_email
Setting an
output_handler
automatically sets
output_buffering
to
on
.
8.13.4 See Also
Recipe 10.11
uses output buffering in a database error logging function; documentation on
ob_start( )
at http://www.php.net/ob-start
,
ob_end_flush( )
at
http://www.php.net/ob-end-flush
, and output buffering at http://www.php.net/outcontrol
.
Recipe 8.14 Compressing Web Output with gzip
8.14.1 Problem
You want to send compressed content to browsers that support automatic decompression.
8.14.2 Solution
Add this setting to your php.ini file:
zlib.output_compression=1
45
8.14.3 Discussion
Browsers tell the server that they can accept compressed responses with the
Accept-
Encoding
header. If a browser sends
Accept-Encoding: gzip
or
Accept-Encoding:
deflate
, and PHP is built with the zlib extension, the
zlib.output_compression
configuration directive tells PHP to compress the output with the appropriate algorithm before
sending it back to the browser. The browser uncompresses the data before displaying it.
You can adjust the compression level with the
zlib.output_compression_level
configuration directive:
; minimal compression
zlib.output_compression_level=1
; maximal compression
zlib.output_compression_level=9
At higher compression levels, less data needs to be sent from the server to the browser, but
more server CPU time must be used to compress the data.
8.14.4 See Also
Documentation on the zlib extension at http://www.php.net/zlib
.
Recipe 8.15 Hiding Error Messages from Users
8.15.1 Problem
You don't want PHP error messages visible to users.
8.15.2 Solution
Set the following values in your php.ini or web server configuration file:
display_errors =off
log_errors =on
These settings tell PHP not to display errors as HTML to the browser but to put them in the
server's error log.
8.15.3 Discussion
When
log_errors
is set to
on
, error messages are written to the server's error log. If you
want PHP errors to be written to a separate file, set the
error_log
configuration directive
with the name of that file:
error_log = /var/log/php.error.log
42
If
error_log
is set to
syslog
, PHP error messages are sent to the system logger using
syslog(3) on Unix and to the Event Log on Windows NT.
There are lots of error messages you want to show your users, such as telling them they've
filled in a form incorrectly, but you should shield your users from internal errors that may
reflect a problem with your code. There are two reasons for this. First, these errors appear
unprofessional (to expert users) and confusing (to novice users). If something goes wrong
when saving form input to a database, check the return code from the database query and
display a message to your users apologizing and asking them to come back later. Showing
them a cryptic error message straight from PHP doesn't inspire confidence in your web site.
Second, displaying these errors to users is a security risk. Depending on your database and
the type of error, the error message may contain information about how to log in to your
database or server and how it is structured. Malicious users can use this information to mount
an attack on your web site.
For example, if your database server is down, and you attempt to connect to it with
mysql_connect( )
, PHP generates the following warning:
<br>
<b>Warning</b>: Can't connect to MySQL server on 'db.example.com' (111) in
<b>/www/docroot/example.php</b> on line <b>3</b><br>
If this warning message is sent to a user's browser, he learns that your database server is
called db.example.com and can mount an attack on it.
8.15.4 See Also
Recipe 8.18
for how to log errors; documentation on PHP configuration directives at
http://www.php.net/configuration
.
Recipe 8.16 Tuning Error Handling
8.16.1 Problem
You want to alter the error-logging sensitivity on a particular page. This lets you control what
types of errors are reported.
8.16.2 Solution
To adjust the types of errors PHP complains about, use
error_reporting( )
:
error_reporting(E_ALL); // everything
error_reporting(E_ERROR | E_PARSE); // only major problems
error_reporting(E_ALL & ~E_NOTICE); // everything but notices
8.16.3 Discussion
72
Every error generated has an error type associated with it. For example, if you try to
array_pop( )
a string, PHP complains that "This argument needs to be an array," since you
can only pop arrays. The error type associated with this message is
E_NOTICE
, a nonfatal
runtime problem.
By default, the error reporting level is
E_ALL & ~E_NOTICE
, which means all error types
except notices. The
&
is a logical
AND
, and the
~
is a logical
NOT
. However, the php.ini-
recommended configuration file sets the error reporting level to
E_ALL
, which is all error
types.
Error messages flagged as notices are runtime problems that are less serious than warnings.
They're not necessarily wrong, but they indicate a potential problem. One example of an
E_NOTICE
is "Undefined variable," which occurs if you try to use a variable without previously
assigning it a value:
// Generates an E_NOTICE
foreach ($array as $value) {
$html .= $value;
}
// Doesn't generate any error message
$html = '';
foreach ($array as $value) {
$html .= $value;
}
In the first case, the first time though the
foreach
,
$html
is undefined. So, when you
append to it, PHP lets you know you're appending to an undefined variable. In the second
case, the empty string is assigned to
$html
above the loop to avoid the
E_NOTICE
. The
previous two code snippets generate identical code because the default value of a variable is
the empty string. The
E_NOTICE
can be helpful because, for example, you may have
misspelled a variable name:
foreach ($array as $value) {
$hmtl .= $value; // oops! that should be $html
}
$html = ''
foreach ($array as $value) {
$hmtl .= $value; // oops! that should be $html
}
A custom error-handling function can parse errors based on their type and take an appropriate
action. A complete list of error types is shown in Table 8-2
.
Table 8-2. Error types
Value
Constant
Description
Catchable
1
E_ERROR
Nonrecoverable error
No
88
2
E_WARNING
Recoverable error
Yes
4
E_PARSE
Parser error
No
8
E_NOTICE
Possible error
Yes
16
E_CORE_ERROR
Like
E_ERROR
but generated by the PHP core
No
32
E_CORE_WARNING
Like
E_WARNING
but generated by the PHP core
No
64
E_COMPILE_ERROR
Like
E_ERROR
but generated by the Zend Engine
No
128
E_COMPILE_WARNING
Like
E_WARNING
but generated by the Zend Engine No
256
E_USER_ERROR
Like
E_ERROR
but triggered by calling
trigger_error( )
Yes
512
E_USER_WARNING
Like
E_WARNING
but triggered by calling
trigger_error( )
Yes
1024
E_USER_NOTICE
Like
E_NOTICE
but triggered by calling
trigger_error( )
Yes
2047
E_ALL
Everything
n/a
Errors labeled catchable can be processed by the function registered using
set_error_handler( )
. The others indicate such a serious problem that they're not safe
to be handled by users, and PHP must take care of them.
8.16.4 See Also
Recipe 8.17
shows how to set up a custom error handler; documentation on
error_reporting( )
at http://www.php.net/error-reporting
and
set_error_handler( )
at http://www.php.net/set-error-handler
; for more information about errors, see
http://www.php.net/ref.errorfunc.php
.
Recipe 8.17 Using a Custom Error Handler
8.17.1 Problem
You want to create a custom error handler that lets you control how PHP reports errors.
8.17.2 Solution
To set up your own error function, use
set_error_handler( )
:
set_error_handler('pc_error_handler');
function pc_error_handler($errno, $error, $file, $line) {
$message = "[ERROR][$errno][$error][$file:$line]";
error_log($message);
}
8.17.3 Discussion
Documents you may be interested
Documents you may be interested