title besides title

 
Showing posts with label PHP - Web Basics. Show all posts
Showing posts with label PHP - Web Basics. Show all posts

Saturday, December 1, 2012

PHP : Web Basics - [8.1] Introduction


Web programming is probably why you're reading this book. It's why the first version of PHP was written and what continues to make it so popular today. With PHP, it's easy to write dynamic web programs that do almost anything. Other chapters cover various PHP capabilities, like graphics, regular expressions, database access, and file I/O. These capabilities are all part of web programming, but this chapter focuses on some web-specific concepts and organizational topics that will make your web programming stronger.

Section 8.2, Section 8.3, and Section 8.4 show how to set, read, and delete cookies. A cookie is a small text string that the server instructs the browser to send along with requests the browser makes. Normally, HTTP requests aren't "stateful"; each request can't be connected to a previous one. A cookie, however, can link different requests by the same user. This makes it easier to build features such as shopping carts or to keep track of a user's search history.

Section 8.5 shows how to redirect users to a different web page than the one they requested. Section 8.6 explains the session module, which lets you easily associate persistent data with a user as he moves through your site. Section 8.7 demonstrates how to store session information in a database, which increases the scalability and flexibility of your web site. Discovering the features of a user's browser is shown in Section 8.8. Section 8.9 shows the details of constructing a URL that includes a GET query string, including proper encoding of special characters and handling of HTML entities.

The next two Sections demonstrate how to use authentication, which lets you protect your web pages with passwords. PHP's special features for dealing with HTTP Basic authentication are explained in Section 8.10. Sometimes it's a better idea to roll your own authentication method using cookies, as shown in Section 8.11.

The three following Sections deal with output control. Section 8.12 shows how to force output to be sent to the browser. Section 8.13 explains the output buffering functions. Output buffers enable you to capture output that would otherwise be printed or delay output until an entire page is processed. Automatic compression of output is shown in Section 8.14.

Section 8.15 to Section 8.20 cover error handling topics, including controlling where errors are printed, writing custom functions to handle error processing, and adding debugging assistance information to your programs. Section 8.19 includes strategies for avoiding the common "headers already sent" error message, such as using the output buffering discussed in Section 8.13.

The next four Sections show how to interact with external variables: environment variables and PHP configuration settings. Section 8.21 and Section 8.22 discuss environment variables, while Section 8.23 and Section 8.24 discuss reading and changing PHP configuration settings. If Apache is your web server, you can use the techniques in Section 8.25 to communicate with other Apache modules from within your PHP programs.

Section 8.26 demonstrates a few methods for profiling and benchmarking your code. By finding where your programs spend most of their time, you can focus your development efforts on improving the code that has the most noticeable speed-up effect to your users.

This chapter also includes two programs that assist in web site maintenance. Program Section 8.27 validates user accounts by sending an email message with a customized link to each new user. If the user doesn't visit the link within a week of receiving the message, the account is deleted. Program Section 8.28 monitors requests in real time on a per-user basis and blocks requests from users that flood your site with traffic.

PHP : Web Basics - [8.2] Setting Cookies


8.2.1 Problem

You want to set a cookie.

8.2.2 Solution

Use setcookie( ):
setcookie('flavor','chocolate chip');

8.2.3 Discussion

Cookies are sent with the HTTP headers, so setcookie( ) must be called before any output is generated.
You can pass additional arguments to setcookie( ) to control cookie behavior. The third argument to setcookie( ) is an expiration time, expressed as an epoch timestamp. For example, this cookie expires at noon GMT on December 3, 2004:
setcookie('flavor','chocolate chip',1102075200);
If the third argument to setcookie( ) is missing (or empty), the cookie expires when the browser is closed. Also, many systems can't handle a cookie expiration time greater than 2147483647, because that's the largest epoch timestamp that fits in a 32-bit integer, as discussed in the introduction to Chapter 3.
The fourth argument to setcookie( ) is a path. The cookie is sent back to the server only when pages whose path begin with the specified string are requested. For example, the following cookie is sent back only to pages whose path begins with /products/:
setcookie('flavor','chocolate chip','','/products/');
The page that's setting this cookie doesn't have to have a URL that begins with /products/, but the following cookie is sent back only to pages that do.
The fifth argument to setcookie( ) is a domain. The cookie is sent back to the server only when pages whose hostname ends with the specified domain are requested. For example, the first cookie in the following code is sent back to all hosts in the example.com domain, but the second cookie is sent only with requests to the host jeannie.example.com:
setcookie('flavor','chocolate chip','','','.example.com');
setcookie('flavor','chocolate chip','','','jeannie.example.com');
If the first cookie's domain was just example.com instead of .example.com, it would be sent only to the single host example.com (and not www.example.com or jeannie.example.com).
The last optional argument to setcookie( ) is a flag that if set to 1, instructs the browser only to send the cookie over an SSL connection. This can be useful if the cookie contains sensitive information, but remember that the data in the cookie is stored in the clear on the user's computer.
Different browsers handle cookies in slightly different ways, especially with regard to how strictly they match path and domain strings and how they determine priority between different cookies of the same name. The setcookie( ) page of the online manual has helpful clarifications of these differences. 

PHP : Web Basics - [8.3] Reading Cookie Values


8.3.1 Problem

You want to read the value of a cookie that's been previously set.

8.3.2 Solution

Look in the $_COOKIE superglobal array:
if (isset($_COOKIE['flavor'])) {
    print "You ate a $_COOKIE[flavor] cookie.";
}

8.3.3 Discussion

A cookie's value isn't available in $_COOKIE during the request in which the cookie is set. In other words, the setcookie( ) function doesn't alter the value of $_COOKIE. On subsequent requests, however, each cookie is stored in $_COOKIE. If register_globals is on, cookie values are also assigned to global variables.
When a browser sends a cookie back to the server, it sends only the value. You can't access the cookie's domain, path, expiration time, or secure status through $_COOKIE because the browser doesn't send that to the server.
To print the names and values of all cookies sent in a particular request, loop through the $_COOKIE array:
foreach ($_COOKIE as $cookie_name => $cookie_value) {
    print "$cookie_name = $cookie_value<br>";
}

PHP : Web Basics - [8.4] Deleting Cookies


8.4.1 Problem

You want to delete a cookie so a browser doesn't send it back to the server.

8.4.2 Solution

Call setcookie( ) with no value for the cookie and an expiration time in the past:
setcookie('flavor','',time()-86400);

8.4.3 Discussion

It's a good idea to make the expiration time a few hours or an entire day in the past, in case your server and the user's computer have unsynchronized clocks. For example, if your server thinks it's 3:06 P.M. and a user's computer thinks it's 3:02 P.M., a cookie with an expiration time of 3:05 P.M. isn't deleted by that user's computer even though the time is in the past for the server.
The call to setcookie( ) that deletes a cookie has to have the same arguments (except for value and time) that the call to setcookie( ) that set the cookie did, so include the path, domain, and secure flag if necessary. 

PHP : Web Basics - [8.5] Redirecting to a Different Location


8.5.1 Problem

You want to automatically send a user to a new URL. For example, after successfully saving form data, you want to redirect a user to a page that confirms the data.

8.5.2 Solution

Before any output is printed, use header( ) to send a Location header with the new URL:
header('Location: http://www.example.com/');

8.5.3 Discussion

If you want to pass variables to the new page, you can include them in the query string of the URL:
header('Location: http://www.example.com/?monkey=turtle');
The URL that you are redirecting a user to is retrieved with GET. You can't redirect someone to retrieve a URL via POST. You can, however, send other headers along with the Location header. This is especially useful with the Window-target header, which indicates a particular named frame or window in which to load the new URL:
header('Window-target: main');
header('Location: http://www.example.com/');
The redirect URL must include the protocol and hostname; it can't just be a pathname:
// Good Redirect
header('Location: http://www.example.com/catalog/food/pemmican.php');

// Bad Redirect
header('Location: /catalog/food/pemmican.php');

PHP : Web Basics - [8.6] Using Session Tracking


8.6.1 Problem

You want to maintain information about a user as she moves through your site.

8.6.2 Solution

Use the session module. The session_start( ) function initializes a session, and accessing an element in the global $_SESSION array tells PHP to keep track of the corresponding variable.
session_start();
$_SESSION['visits']++;
print 'You have visited here '.$_SESSION['visits'].' times.';

8.6.3 Discussion

To start a session automatically on each request, set session.auto_start to 1 in php.ini. With session.auto_start, there's no need to call session_start( ).
The session functions keep track of users by issuing them cookies with a randomly generated session IDs. If PHP detects that a user doesn't accept the session ID cookie, it automatically adds the session ID to URLs and forms.[1] For example, consider this code that prints a URL:
[1] Before PHP 4.2.0, this behavior had to be explicitly enabled by building PHP with the --enable-trans-sid configuration setting.
print '<a href="train.php">Take the A Train</a>';
If sessions are enabled, but a user doesn't accept cookies, what's sent to the browser is something like:
<a href="train.php?PHPSESSID=2eb89f3344520d11969a79aea6bd2fdd">Take the A Train</a>
In this example, the session name is PHPSESSID and the session ID is 2eb89f3344520d11969a79aea6bd2fdd. PHP adds those to the URL so they are passed along to the next page. Forms are modified to include a hidden element that passes the session ID. Redirects with the Location header aren't automatically modified, so you have to add a session ID to them yourself using the SID constant:
$redirect_url = 'http://www.example.com/airplane.php';
if (defined('SID') && (! isset($_COOKIE[session_name()]))) {
    $redirect_url .= '?' . SID;
}

header("Location: $redirect_url");
The session_name( ) function returns the name of the cookie that the session ID is stored in, so this code appends the SID constant only to $redirect_url if the constant is defined, and the session cookie isn't set.
By default, PHP stores session data in files in the /tmp directory on your server. Each session is stored in its own file. To change the directory in which the files are saved, set the session.save_path configuration directive in php.ini to the new directory. You can also call session_save_path( ) with the new directory to change directories, but you need to do this before accessing any session variables. 

PHP : Web Basics - [8.7] Storing Sessions in a Database


8.7.1 Problem

You want to store session data in a database instead of in files. If multiple web servers all have access to the same database, the session data is then mirrored across all the web servers.

8.7.2 Solution

Set session.save_handler to user in php.ini and use the pc_DB_Session class shown in Example 8-1. For example:
$s = new pc_DB_Session('mysql://user:password@localhost/db');
ini_get('session.auto_start') or session_start();

8.7.3 Discussion

One of the most powerful aspects of the session module is its abstraction of how sessions get saved. The session_set_save_handler( ) function tells PHP to use different functions for the various session operations such as saving a session and reading session data. The pc_DB_Session class stores the session data in a database. If this database is shared between multiple web servers, users' session information is portable across all those web servers. So, if you have a bunch of web servers behind a load balancer, you don't need any fancy tricks to ensure that a user's session data is accurate no matter which web server they get sent to.
To use pc_DB_Session, pass a data source name (DSN) to the class when you instantiate it. The session data is stored in a table called php_session whose structure is:
CREATE TABLE php_session (
  id CHAR(32) NOT NULL,
  data MEDIUMBLOB,
  last_access INT UNSIGNED NOT NULL,
  PRIMARY KEY(id)
)
If you want the table name to be different than php_session, set session.save_path in php.ini to your new table name. Example 8-1 shows the pc_DB_Session class.
Example 8-1. pc_DB_Session class
require 'PEAR.php';
require 'DB.php';

class pc_DB_Session extends PEAR {

    var $_dbh;
    var $_table;
    var $_connected = false;
    var $_gc_maxlifetime;
    var $_prh_read;
    var $error = null;

    /**
     * Constructor
     */
    function pc_DB_Session($dsn = null) {

PHP : Web Basics - [8.8] Detecting Different Browsers


8.8.1 Problem

You want to generate content based on the capabilities of a user's browser.

8.8.2 Solution

Use the object returned by get_browser( ) to determine a browser's capabilities:
$browser = get_browser( );

if ($browser->frames) {
    // print out a frame-based layout
} elseif ($browser->tables) {
    // print out a table-based layout
} else {
    // print out a boring layout
}

8.8.3 Discussion

The get_browser( ) function examines the environment variable $_ENV['HTTP_USER_AGENT'] (set by the web server) and compares it to browsers listed in an external browser capability file. Due to licensing issues, PHP isn't distributed with a browser capability file. The "Obtaining PHP" section of the PHP FAQ (http://www.php.net/faq.obtaining) lists http://www.cyscape.com/asp/browscap/ and http://www.amrein. com/apps/page.asp?Q=InowDownload as sources for a browser capabilities file, and there is also one at http://asp.net.do/browscap.zip.
Once you download a browser capability file, you need to tell PHP where to find it by setting the browscap configuration directive to the pathname of the file. If you use PHP as a CGI, set the directive in the php.ini file:
browscap=/usr/local/lib/browscap.txt
If you use Apache, you need to set the directive in your Apache configuration file:
php_value browscap "/usr/local/lib/browscap.txt"
Many of the capabilities get_browser( ) finds are shown in Table 8-1. For user-configurable capabilities such as javascript or cookies though, get_browser( ) just tells you if the browser can support those functions. It doesn't tell you if the user has disabled the functions. If JavaScript is turned off in a JavaScript-capable browser or a user refuses to accept cookies when the browser prompts him, get_browser( ) still indicates that the browser supports those functions.

Table 8-1. Browser capability object properties
Property
Description
platform
Operating system the browser is running on (e.g., Windows, Macintosh, UNIX, Win32, Linux, MacPPC)
version
Full browser version (e.g., 5.0, 3.5, 6.0b2)
majorver
Major browser version (e.g., 5, 3, 6)
minorver
Minor browser version (e.g., 0, 5, 02)
frames
1 if the browser supports frames
tables
1 if the browser supports tables
cookies
1 if the browser supports cookies
backgroundsounds
1 if the browser supports background sounds with <embed> or <bgsound>
vbscript
1 if the browser supports VBScript
javascript
1 if the browser supports JavaScript
javaapplets
1 if the browser can run Java applets
activexcontrols
1 if the browser can run ActiveX controls

PHP : Web Basics - [8.9] Building a GET Query String


8.9.1 Problem

You need to construct a link that includes name/value pairs in a query string.

8.9.2 Solution

Encode the names and values with urlencode( ) and use join( ) to create the query string:
$vars = array('name' => 'Oscar the Grouch',
              'color' => 'green',
              'favorite_punctuation' => '#');
$safe_vars = array( );
foreach ($vars as $name => $value) {
    $safe_vars[ ] = urlencode($name).'='.urlencode($value);
}

$url = '/muppet/select.php?' . join('&',$safe_vars);

8.9.3 Discussion

The URL built in the solution is:
/muppet/select.php?name=Oscar+the+Grouch&color=green&favorite_punctuation=%23
The query string has spaces encoded as +. Special characters such as # are hex-encoded as %23 because the ASCII value of # is 35, which is 23 in hexadecimal.
Although urlencode( ) prevents any special characters in the variable names or values from disrupting the constructed URL, you may have problems if your variable names begin with the names of HTML entities. Consider this partial URL for retrieving information about a stereo system:
/stereo.php?speakers=12&cdplayer=52&amp=10
The HTML entity for ampersand (&) is &amp; so a browser may interpret that URL as:
/stereo.php?speakers=12&cdplayer=52&=10
To prevent embedded entities from corrupting your URLs, you have three choices. The first is to choose variable names that can't be confused with entities, such as _amp instead of amp. The second is to convert characters with HTML entity equivalents to those entities before printing out the URL. Use htmlentities( ) :
$url = '/muppet/select.php?' . htmlentities(join('&',$safe_vars));
The resulting URL is:
/muppet/select.php?name=Oscar+the+Grouch&color=green&favorite_punctuation=%23
Your third choice is to change the argument separator from & to ; by setting the configuration directive arg_separator.input to ;. You then join name-value pairs with ; to produce a query string:
/muppet/select.php?name=Oscar+the+Grouch;color=green;favorite_punctuation=%23
You may run into trouble with any GET method URLs that you can't explicitly construct with semicolons, such as a form with its method set to GET, because your users' browsers use & as the argument separator.
Because many browsers don't support using ; as an argument separator, the easiest way to avoid problems with entities in URLs is to choose variable names that don't overlap with entity names. If you don't have complete control over variable names, however, use htmlentities( ) to protect your URLs from entity decoding. 

PHP : Web Basics - [8.10] Using HTTP Basic Authentication


8.10.1 Problem

You want to use PHP to protect parts of your web site with passwords. Instead of storing the passwords in an external file and letting the web server handle the authentication, you want the password verification logic to be in a PHP program.

8.10.2 Solution

The $_SERVER['PHP_AUTH_USER'] and $_SERVER['PHP_AUTH_PW'] global variables contain the username and password supplied by the user, if any. To deny access to a page, send a WWW-Authenticate header identifying the authentication realm as part of a response with status code 401:
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.3 Discussion

When a browser sees a 401 header, it pops up a dialog box for a username and password. Those authentication credentials (the username and password), if accepted by the server, are associated with the realm in the WWW-Authenticate header. Code that checks authentication credentials needs to be executed before any output is sent to the browser, since it might send headers. For example, you can use a function such as pc_validate( ), shown in Example 8-2.
Example 8-2. pc_validate( )
function pc_validate($user,$pass) {
    /* replace with appropriate username and password checking,
       such as checking a database */
    $users = array('david' => 'fadj&32',
                   'adam'  => '8HEj838');

    if (isset($users[$user]) && ($users[$user] == $pass)) {
        return true;
    } else {
        return false;
    }
}
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 Section 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'");
    
    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;
}

PHP : Web Basics - [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:
<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 Section  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.";
    }
}
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 Section 14.11. 

PHP : Web Basics - [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

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');

PHP : Web Basics - [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

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

PHP : Web Basics - [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

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. 

PHP : Web Basics - [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
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. 

PHP : Web Basics - [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

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
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.

PHP : Web Basics - [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

A custom error handling function can parse errors based on their type and take the appropriate action. See Table 8-2 in Section 8.16 for a list of error types.
Pass set_error_handler( ) the name of a function, and PHP forwards all errors to that function. The error handling function can take up to five parameters. The first parameter is the error type, such as 8 for E_NOTICE. The second is the message thrown by the error, such as "Undefined variable: html". The third and fourth arguments are the name of the file and the line number in which PHP detected the error. The final parameter is an array holding all the variables defined in the current scope and their values.
For example, in this code $html is appended to without first being assigned an initial value:
error_reporting(E_ALL);
set_error_handler('pc_error_handler');

function pc_error_handler($errno, $error, $file, $line, $context) {
    $message = "[ERROR][$errno][$error][$file:$line]";
    print "$message";
    print_r($context);
}

$form = array('one','two');

foreach ($form as $line) {
    $html .= "<b>$line</b>";
}
When the "Undefined variable" error is generated, pc_error_handler( ) prints:
[ERROR][8][Undefined variable:  html][err-all.php:16]
After the initial error message, pc_error_handler( ) also prints a large array containing all the globals, environment, request, and session variables.
Errors labeled catchable in Table 8-2 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.

PHP : Web Basics - [8.18] Logging Errors

8.18.1 Problem

You want to write program errors to a log. These errors can include everything from parser errors and files not being found to bad database queries and dropped connections.

8.18.2 Solution

Use error_log( ) to write to the error log:
// LDAP error
if (ldap_errno($ldap)) {
    error_log("LDAP Error #" . ldap_errno($ldap) . ": " . ldap_error($ldap));
}

8.18.3 Discussion

Logging errors facilitates debugging. Smart error logging makes it easier to fix bugs. Always log information about what caused the error:
$r = mysql_query($sql);
if (! $r) {
    $error = mysql_error( );
    error_log('[DB: query @'.$_SERVER['REQUEST_URI']."][$sql]: $error");
} else {
    // process results
}
You're not getting all the debugging help you could be if you simply log that an error occurred without any supporting information:
$r = mysql_query($sql);
if (! $r) {
    error_log("bad query");
} else {
    // process result
}
Another useful technique is to include the _ _FILE_ _ and _ _LINE_ _ constants in your error messages:
error_log('['._ _FILE_ _.']['._ _LINE_ _."]: $error");
The _ _FILE_ _ constant is the current filename, and _ _LINE_ _ is the current line number.

PHP : Web Basics - [8.19] Eliminating "headers already sent" Errors

8.19.1 Problem

You are trying to send a HTTP header or cookie using header( ) or setcookie( ), but PHP reports a "headers already sent" error message.

8.19.2 Solution

This error happens when you send nonheader output before calling header( ) or setcookie( ).
Rewrite your code so any output happens after sending headers:
// good
setcookie("name", $name);
print "Hello $name!";

// bad
print "Hello $name!";
setcookie("name", $name);

// good
<?php setcookie("name",$name); ?>
<html><title>Hello</title>

8.19.3 Discussion

An HTTP message has a header and a body, which are sent to the client in that order. Once you begin sending the body, you can't send any more headers. So, if you call setcookie( ) after printing some HTML, PHP can't send the appropriate Cookie header.
Also, remove trailing whitespace in any include files. When you include a file with blank lines outside <?php ?> tags, the blank lines are sent to the browser. Use trim( ) to remove leading and trailing blank lines from files:
$file = '/path/to/file.php';

// backup
copy($file, "$file.bak") or die("Can't copy $file: $php_errormsg);

// read and trim
$contents = trim(join('',file($file)));

// write
$fh = fopen($file, 'w')  or die("Can't open $file for writing: $php_errormsg);
if (-1 == fwrite($fh, $contents)) { die("Can't write to $file: $php_errormsg); }
fclose($fh)              or die("Can't close $file: $php_errormsg);
Instead of processing files on a one-by-one basis, it may be more convenient to do so on a directory-by-directory basis. Section 19.8 describes how to process all the files in a directory.
If you don't want to worry about blank lines disrupting the sending of headers, turn on output buffering. Output buffering prevents PHP from immediately sending all output to the client. If you buffer your output, you can intermix headers and body text with abandon. However, it may seem to users that your server takes longer to fulfill their requests since they have to wait slightly longer before the browser displays any output.

PHP : Web Basics - [8.20] Logging Debugging Information

8.20.1 Problem

You want to make debugging easier by adding statements to print out variables. But, you want to easily be able to switch back and forth from production and debug modes.

8.20.2 Solution

Put a function that conditionally prints out messages based on a defined constant in a page included using the auto_prepend_file configuration setting. Save the following code to debug.php:
// turn debugging on
define('DEBUG',true);

// generic debugging function
function pc_debug($message) {
    if (defined(DEBUG) && DEBUG) {
        error_log($message);
    }
}
Set the auto_prepend_file directive in php.ini:
auto_prepend_file=debug.php
Now call pc_debug( ) from your code to print out debugging information:
$sql = 'SELECT color, shape, smell FROM vegetables';
pc_debug("[sql: $sql]"); // only printed if DEBUG is true
$r = mysql_query($sql);

8.20.3 Discussion

Debugging code is a necessary side-effect of writing code. There are a variety of techniques to help you quickly locate and squash your bugs. Many of these involve including scaffolding that helps ensure the correctness of your code. The more complicated the program, the more scaffolding needed. Fred Brooks, in The Mythical Man-Month, guesses that there's "half as much code in scaffolding as there is in product." Proper planning ahead of time allows you to integrate the scaffolding into your programming logic in a clean and efficient fashion. This requires you to think out beforehand what you want to measure and record and how you plan on sorting through the data gathered by your scaffolding.
One technique for sifting through the information is to assign different priority levels to different types of debugging comments. Then the debug function prints information only if it's higher than the current priority level.
define('DEBUG',2);

function pc_debug($message, $level = 0) {
    if (defined(DEBUG) && ($level > DEBUG) {
        error_log($message);
    }
}

$sql = 'SELECT color, shape, smell FROM vegetables';
pc_debug("[sql: $sql]", 1); // not printed, since 1 < 2
pc_debug("[sql: $sql]", 3); // printed, since 3 > 2
Another technique is to write wrapper functions to include additional information to help with performance tuning, such as the time it takes to execute a database query.
function getmicrotime(){
    $mtime = microtime();
    $mtime = explode(' ',$mtime);
    return ($mtime[1] + $mtime[0]);
}
 
function db_query($sql) {
    if (defined(DEBUG) && DEBUG) {
         // start timing the query if DEBUG is on
         $DEBUG_STRING = "[sql: $sql]<br>\n";
         $starttime = getmicrotime();
    }

    $r = mysql_query($sql);

    if (! $r) {
        $error = mysql_error();
        error_log('[DB: query @'.$_SERVER['REQUEST_URI']."][$sql]: $error");
    } elseif (defined(DEBUG) && DEBUG) {
        // the query didn't fail and DEBUG is turned on, so finish timing it
        $endtime = getmicrotime();
        $elapsedtime = $endtime - $starttime;
        $DEBUG_STRING .= "[time: $elapsedtime]<br>\n";
        error_log($DEBUG_STRING);
    }

    return $r;
}
Here, instead of just printing out the SQL to the error log, you also record the number of seconds it takes MySQL to perform the request. This lets you see if certain queries are taking too long.
The getmicrotime( ) function converts the output of microtime( ) into a format that allows you to easily perform addition and subtraction upon the numbers.