Chapter 9. Working with Files

The data storage destination of choice for a web application is a database. That doesn’t mean that you’re completely off the hook from dealing with regular old files, though. Plain text files are still a handy, universal way to exchange some kinds of information.

You can do easy customization of your website by storing HTML templates in text files. When it’s time to generate a specialized page, load the text file, substitute real data for the template elements, and print it. Example 9-2 shows you how to do this.

Files are also good for exchanging tabular data between your program and a spreadsheet. In your PHP programs, you can easily read and write the CSV (comma-separated value) files with which spreadsheet programs work.

This chapter shows you how to work with files from your PHP programs: dealing with file permissions, which your computer uses to enforces rules about which files your programs can read and write; reading data from and writing data to files; and handling errors that may occur with file-related operations.

Understanding File Permissions

To read or write a file with any of the functions you’ll learn about in this chapter, the PHP engine must have permission from the operating system to do so. Every program that runs on a computer, including the PHP engine, runs with the privileges of a particular user account. Most of the user accounts correspond to people. When you log in to your computer and start up your word processor, that word processor runs with the privileges that correspond to your account: it can read files that you are allowed to see and write files that you are allowed to change.

Some user accounts on a computer, however, aren’t for people but for system processes such as web servers. When the PHP interpreter runs inside of a web server, it has the privileges that the web server’s “account” has. So if the web server is allowed to read a certain file or directory, then the PHP engine (and therefore your PHP program) can read that file or directory. If the web server is allowed to change a certain file or write new files in a particular directory, then so can the PHP engine and your PHP program.

Usually, the privileges extended to a web server’s account are more limited than the privileges that go along with a real person’s account. The web server (and the PHP engine) need to be able to read all of the PHP program files that make up your website, but they shouldn’t be able to change them. If a bug in the web server or an insecure PHP program lets an attacker break in, the PHP program files should be protected against being changed by that attacker.

In practice, what this means is that your PHP programs shouldn’t have too much trouble reading most files that you need to read. (Of course, if you try to read another user’s private files, you may run into a problem—but that’s as it should be!) However, the files that your PHP program can change and the directories into which your program can write new files are limited. If you need to create lots of new files in your PHP programs, work with your system administrator to make a special directory that you can write to but that doesn’t compromise system security. “Inspecting File Permissions” shows you how to determine which files and directories your programs are allowed to read and write.

Reading and Writing Entire Files

This section shows you how to work with an entire file at once, as opposed to manipulating just a few lines of a file. PHP provides special functions for reading or writing a whole file in a single step.

Reading a File

To read the contents of a file into a string, use file_get_contents(). Pass it a filename, and it returns a string containing everything in the file. Example 9-2 reads the file in Example 9-1 with file_get_contents(), modifies it with str_replace(), and then prints the result.

Example 9-1. page-template.html for Example 9-2
<html>
<head><title>{page_title}</title></head>
<body bgcolor="{color}">

<h1>Hello, {name}</h1>

</body>
</html>
Example 9-2. Using file_get_contents() with a page template
// Load the template file from the previous example
$page = file_get_contents('page-template.html');

// Insert the title of the page
$page = str_replace('{page_title}', 'Welcome', $page);

// Make the page blue in the afternoon and
// green in the morning
if (date('H' >= 12)) {
    $page = str_replace('{color}', 'blue', $page);
} else {
    $page = str_replace('{color}', 'green', $page);
}

// Take the username from a previously saved session
// variable
$page = str_replace('{name}', $_SESSION['username'], $page);

// Print the results
print $page;
Warning

Every time you use a file access function, you need to check that it didn’t encounter an error because of a lack of disk space, permission problem, or other failure. Error checking is discussed in detail in “Checking for Errors”. The examples in the next few sections don’t have error-checking code, so you can see the actual file access function at work without other new material getting in the way. Real programs that you write always need to check for errors after calling a file access function.

With $_SESSION['username'] set to Jacob, Example 9-2 prints:

<html>
<head><title>Welcome</title></head>
<body bgcolor="green">

<h1>Hello, Jacob</h1>

</body>
</html>

Writing a File

The counterpart to reading the contents of a file into a string is writing a string to a file. And the counterpart to file_get_contents() is file_put_contents(). Example 9-3 extends Example 9-2 by saving the HTML to a file instead of printing it.

Example 9-3. Saving a file with file_put_contents()
// Load the template file we used earlier
$page = file_get_contents('page-template.html');

// Insert the title of the page
$page = str_replace('{page_title}', 'Welcome', $page);

// Make the page blue in the afternoon and
// green in the morning
if (date('H' >= 12)) {
    $page = str_replace('{color}', 'blue', $page);
} else {
    $page = str_replace('{color}', 'green', $page);
}

// Take the username from a previously saved session
// variable
$page = str_replace('{name}', $_SESSION['username'], $page);

// Write the results to page.html
file_put_contents('page.html', $page);

Example 9-3 writes the value of $page (the HTML) to the file page.html. The first argument to file_put_contents() is the filename to write to, and the second argument is what to write to the file.

Reading and Writing Parts of Files

The file_get_contents() and file_put_contents() functions are fine when you want to work with an entire file at once. But when it’s time for precision work, use the file() function to access each line of a file. Example 9-4 reads a file in which each line contains a name and an email address and then prints an HTML-formatted list of that information.

Example 9-4. Accessing each line of a file
foreach (file('people.txt') as $line) {
    $line = trim($line);
    $info = explode('|', $line);
    print '<li><a href="mailto:' . $info[0] . '">' . $info[1] ."</li>\n";
}

Suppose people.txt contains what’s listed in Example 9-5.

Example 9-5. people.txt for Example 9-4
alice@example.com|Alice Liddell
bandersnatch@example.org|Bandersnatch Gardner
charles@milk.example.com|Charlie Tenniel
dodgson@turtle.example.com|Lewis Humbert

Then, Example 9-4 prints:

<li><a href="mailto:alice@example.com">Alice Liddell</li>
<li><a href="mailto:bandersnatch@example.org">Bandersnatch Gardner</li>
<li><a href="mailto:charles@milk.example.com">Charlie Tenniel</li>
<li><a href="mailto:dodgson@turtle.example.com">Lewis Humbert</li>

The file() function returns an array. Each element of that array is a string containing one line of the file, newline included. So, the foreach() loop in Example 9-4 visits each element of the array, putting the string in $line. The trim() function removes the trailing newline, explode() breaks apart the line into what’s before the | and what’s after it, and then print outputs the HTML list elements.

Although file() is very convenient, it can be problematic with very large files. It reads the whole file to build the array of lines—and with a file that contains lots of lines, that may use up too much memory. In that case, you need to read the file line-by-line, as shown in Example 9-6.

Example 9-6. Reading a file one line at a time
$fh = fopen('people.txt','rb');
while ((! feof($fh)) && ($line = fgets($fh))) {
    $line = trim($line);
    $info = explode('|', $line);
    print '<li><a href="mailto:' . $info[0] . '">' . $info[1] ."</li>\n";
}
fclose($fh);

The four file access functions in Example 9-6 are fopen(),fgets(), feof(), and fclose(). They work together as follows:

  • The fopen() function opens a connection to the file and returns a variable that’s used for subsequent access to the file in the program. (This is conceptually similar to the database connection variable returned by new PDO() that you saw in Chapter 8.)
  • The fgets() function reads a line from the file and returns it as a string.
  • The PHP engine keeps a bookmark of where its current position in the file is. The bookmark starts at the beginning of the file, so the first time that fgets() is called, the first line of the file is read. After that line is read, the bookmark is updated to the beginning of the next line.
  • The feof() function returns true if the bookmark is past the end of the file (“eof” stands for “end of file”).
  • The fclose() function closes the connection to the file.

The while() loop in Example 9-6 keeps executing as long as two things are true:

  • feof($fh) returns false.
  • The $line value that fgets($fh) returns evaluates to true.

Each time fgets($fh) runs, the PHP engine grabs a line from the file, advances its bookmark, and returns the line. When the bookmark is pointing at the very last spot in the file, feof($fh) still returns false. At that point, however, fgets($fh) returns false because it tries to read a line and can’t. So, both of those checks are necessary to make the loop end properly.

Example 9-6 uses trim() on $line because the string that fgets() returns includes the trailing newline at the end of the line. The trim() function removes the newline, which makes the output look better.

The first argument to fopen() is the name of the file that you want to access. As with other PHP file access functions, use forward slashes (/) instead of backslashes (\) here, even on Windows. Example 9-7 opens a file in the Windows system directory.

Example 9-7. Opening a file on Windows
$fh = fopen('c:/windows/system32/settings.txt','rb');

Because backslashes have a special meaning (escaping, which you saw in “Defining Text Strings”) inside strings, it’s easier to use forward slashes in filenames. The PHP engine does the right thing in Windows and loads the correct file.

The second argument to fopen() is the file mode. This controls what you’re allowed to do with the file once it’s opened: reading, writing, or both. The file mode also affects where the PHP engine’s file position bookmark starts, whether the file’s contents are cleared out when it’s opened, and how the PHP engine should react if the file doesn’t exist. Table 9-1 lists the different modes that fopen() understands.

Table 9-1. File modes for fopen()
Mode Allowable actions Position bookmark starting point Clear contents? If the file doesn’t exist?
rb Reading Beginning of file No Issue a warning, return false.
rb+ Reading, Writing Beginning of file No Issue a warning, return false.
wb Writing Beginning of file Yes Try to create it.
wb+ Reading, writing Beginning of file Yes Try to create it.
ab Writing End of file No Try to create it.
ab+ Reading, writing End of file No Try to create it.
xb Writing Beginning of file No Try to create it; if the file does exist, issue a warning and return false.
xb+ Reading, writing Beginning of file No Try to create it; if the file does exist, issue a warning and return false.
cb Writing Beginning of file No Try to create it.
cb+ Reading, writing Beginning of file No Try to create it.

Once you’ve opened a file in a mode that allows writing, use the fwrite() function to write something to the file. Example 9-8 uses the wb mode with fopen() and uses fwrite() to write information retrieved from a database table to the file dishes.txt.

Example 9-8. Writing data to a file
try {
    $db = new PDO('sqlite:/tmp/restaurant.db');
} catch (Exception $e) {
    print "Couldn't connect to database: " . $e->getMessage();
    exit();
}

// Open dishes.txt for writing
$fh = fopen('dishes.txt','wb');

$q = $db->query("SELECT dish_name, price FROM dishes");
while($row = $q->fetch()) {
    // Write each line (with a newline on the end) to
    // dishes.txt
    fwrite($fh, "The price of $row[0] is $row[1] \n");
}
fclose($fh);

The fwrite() function doesn’t automatically add a newline to the end of the string you write. It just writes exactly what you pass to it. If you want to write a line at a time (such as in Example 9-8), be sure to add a newline (\n) to the end of the string that you pass to fwrite().

Working with CSV Files

One type of text file gets special treatment in PHP: the CSV file. It can’t handle graphs or charts, but excels at sharing tables of data among different programs. To read a line of a CSV file, use fgetcsv() instead of fgets(). It reads a line from the CSV file and returns an array containing each field in the line. Example 9-9 is a CSV file of information about restaurant dishes. Example 9-10 uses fgetcsv() to read the file and insert the information in it into the dishes database table from Chapter 8.

Example 9-9. dishes.csv
"Fish Ball with Vegetables",4.25,0
"Spicy Salt Baked Prawns",5.50,1
"Steamed Rock Cod",11.95,0
"Sauteed String Beans",3.15,1
"Confucius ""Chicken""",4.75,0
Example 9-10. Inserting CSV data into a database table
try {
    $db = new PDO('sqlite:/tmp/restaurant.db');
} catch (Exception $e) {
    print "Couldn't connect to database: " . $e->getMessage();
    exit();
}
$fh = fopen('dishes.csv','rb');
$stmt = $db->prepare('INSERT INTO dishes (dish_name, price, is_spicy) 
                                  VALUES (?,?,?)');
while ((! feof($fh)) && ($info = fgetcsv($fh))) {
    // $info[0] is the dish name    (the  first field in a line of dishes.csv)
    // $info[1] is the price        (the second field)
    // $info[2] is the spicy status (the  third field)
    // Insert a row into the database table
    $stmt->execute($info);
    print "Inserted $info[0]\n";
}
// Close the file
fclose($fh);

Example 9-10 prints:

Inserted Fish Ball with Vegetables
Inserted Spicy Salt Baked Prawns
Inserted Steamed Rock Cod
Inserted Sauteed String Beans
Inserted Confucius "Chicken"

Writing a CSV-formatted line is similar to reading one. The fputcsv() function takes a file handle and an array of values as arguments and writes those values, formatted as proper CSV, to the file. Example 9-11 uses fputcsv() along with fopen() and fclose() to retrieve information from a database table and write it to a CSV file.

Example 9-11. Writing CSV-formatted data to a file
try {
    $db = new PDO('sqlite:/tmp/restaurant.db');
} catch (Exception $e) {
    print "Couldn't connect to database: " . $e->getMessage();
    exit();
}

// Open the CSV file for writing
$fh = fopen('dish-list.csv','wb');

$dishes = $db->query('SELECT dish_name, price, is_spicy FROM dishes');
while ($row = $dishes->fetch(PDO::FETCH_NUM)) {
    // Write the data in $row as a CSV-formatted string. fputcsv()
    // adds a newline at the end.
    fputcsv($fh, $row);
}
fclose($fh);

To send a page that consists only of CSV-formatted data back to a web client, you need to tell fputcsv() to write the data to the regular PHP output stream (instead of a file). You also have to use PHP’s header() function to tell the web client to expect a CSV document instead of an HTML document. Example 9-12 shows how to call the header() function with the appropriate arguments.

Example 9-12. Changing the page type to CSV
// Tell the web client to expect a CSV file
header('Content-Type: text/csv');
// Tell the web client to view the CSV file in a separate program
header('Content-Disposition: attachment; filename="dishes.csv"');

Example 9-13 contains a complete program that sends the correct CSV header, retrieves rows from a database table, and prints them. Its output can be loaded directly into a spreadsheet program from a user’s web browser.

Example 9-13. Sending a CSV file to the browser
try {
    $db = new PDO('sqlite:/tmp/restaurant.db');
} catch (Exception $e) {
    print "Couldn't connect to database: " . $e->getMessage();
    exit();
}

// Tell the web client that a CSV file called "dishes.csv" is coming
header('Content-Type: text/csv');
header('Content-Disposition: attachment; filename="dishes.csv"');

// Open a file handle to the output stream
$fh = fopen('php://output','wb');

// Retrieve the info from the database table and print it
$dishes = $db->query('SELECT dish_name, price, is_spicy FROM dishes');
while ($row = $dishes->fetch(PDO::FETCH_NUM)) {
    fputcsv($fh, $row);
}

In Example 9-13, the first argument to fputcsv() is php://output. This is a special built-in file handle which sends data to the same place that print sends it to.

To generate more complicated spreadsheets that include formulas, formatting, and images, use the PHPOffice PHPExcel package.

Tip

See Chapter 16 for details on how to install packages.

Inspecting File Permissions

As mentioned at the beginning of the chapter, your programs can only read and write files when the PHP engine has permission to do so. You don’t have to cast about blindly and rely on error messages to figure out what those permissions are, however. PHP gives you functions with which you can determine what your program is allowed to do.

To check whether a file or directory exists, use file_exists(). Example 9-14 uses this function to report whether a directory’s index file has been created.

Example 9-14. Checking the existence of a file
if (file_exists('/usr/local/htdocs/index.html')) {
    print "Index file is there.";
} else {
    print "No index file in /usr/local/htdocs.";
}

To determine whether your program has permission to read or write a particular file, use is_readable() or is_writeable(). Example 9-15 checks that a file is readable before retrieving its contents with file_get_contents().

Example 9-15. Testing for read permission
$template_file = 'page-template.html';
if (is_readable($template_file)) {
    $template = file_get_contents($template_file);
} else {
    print "Can't read template file.";
}

Example 9-16 verifies that a file is writeable before appending a line to it with fopen() and fwrite().

Example 9-16. Testing for write permission
$log_file = '/var/log/users.log';
if (is_writeable($log_file)) {
    $fh = fopen($log_file,'ab');
    fwrite($fh, $_SESSION['username'] . ' at ' . strftime('%c') . "\n");
    fclose($fh);
} else {
    print "Cant write to log file.";
}

Checking for Errors

So far, the examples in this chapter have been shown without any error checking in them. This keeps them shorter, so you can focus on the file manipulation functions such as file_get_contents(), fopen(), and fgetcsv(). It also makes them somewhat incomplete. Just like talking to a database program, working with files means interacting with resources external to your program. This means you have to worry about all sorts of things that can cause problems, such as operating system file permissions or a disk running out of free space.

In practice, to write robust file-handling code, you should check the return value of each file-related function. They each generate a warning message and return false if there is a problem. If the configuration directive track_errors is on, the text of the error message is available in the global variable $php_errormsg.

Example 9-17 shows how to check whether fopen() or fclose() encounters an error.

Example 9-17. Checking for an error from fopen() or fclose()
try {
    $db = new PDO('sqlite:/tmp/restaurant.db');
} catch (Exception $e) {
    print "Couldn't connect to database: " . $e->getMessage();
    exit();
}

// Open dishes.txt for writing
$fh = fopen('/usr/local/dishes.txt','wb');
if (! $fh) {
    print "Error opening dishes.txt: $php_errormsg";
} else {
    $q = $db->query("SELECT dish_name, price FROM dishes");
    while($row = $q->fetch()) {
        // Write each line (with a newline on the end) to
        // dishes.txt
        fwrite($fh, "The price of $row[0] is $row[1] \n");
    }
    if (! fclose($fh)) {
        print "Error closing dishes.txt: $php_errormsg";
    }
}

If your program doesn’t have permission to write into the /usr/local directory, then fopen() returns false, and Example 9-17 prints:

Error opening dishes.txt: failed to open stream: Permission denied

It also generates a warning message that looks like this:

Warning: fopen(/usr/local/dishes.txt): failed to open stream: Permission denied 
in dishes.php on line 5

“Controlling Where Errors Appear” talks about how to control where the warning message is shown.

The same thing happens with fclose(). If it returns false, then the Error closing dishes.txt message is printed. Sometimes operating systems buffer data written with fwrite() and don’t actually save the data to the file until you call fclose(). If there’s no space on the disk for the data you’re writing, the error might show up when you call fclose(), not when you call fwrite().

Checking for errors from the other file-handling functions (such as fgets(), fwrite(), fgetcsv(), file_get_contents(), and file_put_contents()) is a little trickier. This is because you have to do something special to distinguish the value they return when an error happens from the data they return when everything goes OK.

If something goes wrong with fgets(), file_get_contents(), or fgetcsv(), they each return false. However, it’s possible that these functions could succeed and still return a value that evaluates to false in a comparison. If file_get_contents() reads a file that just consists of the one character 0, then it returns a one-character string, 0. Remember from “Understanding true and false”, though, that such a string is considered false.

To get around this, be sure to use the identity operator to check the function’s return value. That way, you can compare the value with false and know that an error has happened only if the function actually returns false, not a string that evaluates to false.

Example 9-18 shows how to use the identity operator to check for an error from file_get_contents().

Example 9-18. Checking for an error from file_get_contents()
$page = file_get_contents('page-template.html');
// Note the three equals signs in the test expression
if ($page === false) {
   print "Couldn't load template: $php_errormsg";
} else {
   // ... process template here
}

Use the same technique with fgets() or fgetcsv(). Example 9-19 correctly checks for errors from fopen(), fgets(), and fclose().

Example 9-19. Checking for an error from fopen(), fgets(), or fclose()
$fh = fopen('people.txt','rb');
if (! $fh) {
    print "Error opening people.txt: $php_errormsg";
} else {
    while (! feof($fh)) {
        $line = fgets($fh);
        if ($line !== false) {
            $line = trim($line);
            $info = explode('|', $line);
            print '<li><a href="mailto:' . $info[0] . '">' . $info[1] ."</li>\n";
        }
    }
    if (! fclose($fh)) {
        print "Error closing people.txt: $php_errormsg";
    }
}

When fwrite(), fputcsv(), and file_put_contents() succeed, they return the number of bytes they’ve written. When fwrite() or fputcsv() fails, it returns false, so you can use the identity operator with it just like with fgets(). The file_put_contents() function is a little different. Depending on what goes wrong, it either returns false or -1, so you need to check for both possibilities. Example 9-20 shows how to check for errors from file_put_contents().

Example 9-20. Checking for an error from file_put_contents()
// Load the file from Example 9-1
$page = file_get_contents('page-template.html');

// Insert the title of the page
$page = str_replace('{page_title}', 'Welcome', $page);

// Make the page blue in the afternoon and
// green in the morning
if (date('H' >= 12)) {
    $page = str_replace('{color}', 'blue', $page);
} else {
    $page = str_replace('{color}', 'green', $page);
}

// Take the username from a previously saved session
// variable
$page = str_replace('{name}', $_SESSION['username'], $page);

$result = file_put_contents('page.html', $page);
// Need to check if file_put_contents() returns false or -1
if (($result === false) || ($result == -1)) {
    print "Couldn't save HTML to page.html";
}

Sanitizing Externally Supplied Filenames

Just as data submitted in a form or URL can cause problems when it is displayed (cross-site scripting attack) or put in an SQL query (SQL injection attack), it can also cause problems when it is used as a filename or as part of a filename. This problem doesn’t have a fancy name like those other attacks, but it can be just as devastating.

The cause of the problem is the same: there are special characters that must be escaped so they lose their special meaning. In filenames, the special characters are / (which separates parts of filenames), and the two-character sequence .. (which means “go up one directory” in a filename).

For example, the funny-looking filename /usr/local/data/../../../etc/passwd doesn’t point to a file in the /usr/local/data directory but instead to the location of the file /etc/passwd, which, on most Unix systems, contains a list of user accounts. The filename /usr/local/data/../../../etc/passwd means “from the directory /usr/local/data, go up one level (to /usr/local), then go up another level (to /usr), then go up another level (to /, the top level of the filesystem), then down into /etc, then stop at the file passwd.”

How could this be a problem in your PHP programs? When you use data from a form in a filename, you are vulnerable to an attack that enables a user to gain access to areas of your filesystem that you may not have intended, unless you sanitize the submitted form data. Example 9-21 takes the approach of removing all forward slashes and .. sequences from a submitted form parameter before incorporating the parameter into a filename.

Example 9-21. Cleaning up a form parameter that goes in a filename
// Remove slashes from user
$user = str_replace('/', '', $_POST['user']);
// Remove .. from user
$user = str_replace('..', '', $user);

if (is_readable("/usr/local/data/$user")) {
    print 'User profile for ' . htmlentities($user) .': <br/>';
    print file_get_contents("/usr/local/data/$user");
}

If a malicious user supplies ../../../etc/passwd as the user form parameter in Example 9-21, that is translated into etcpasswd before being interpolated into the filename used with file_get_contents().

Another helpful technique for getting rid of user-entered nastiness is to use realpath(). It translates an obfuscated filename that contains .. sequences into the ..-less version of the filename that more directly indicates where the file is. For example, realpath('/usr/local/data/../../../etc/passwd') returns the string /etc/passwd. You can use realpath() as in Example 9-22 to see whether filenames, after incorporating form data, are acceptable.

Example 9-22. Cleaning up a filename with realpath()
$filename = realpath("/usr/local/data/$_POST[user]");

// Make sure that $filename is under /usr/local/data
if (('/usr/local/data/' == substr($filename, 0, 16)) &&
    is_readable($filename)) {
    print 'User profile for ' . htmlentities($_POST['user']) .': <br/>';
    print file_get_contents($filename);
} else {
    print "Invalid user entered.";
}

In Example 9-22, if $_POST['user'] is james, then $filename is set to /usr/local/data/james and the if() code block runs. However, if $_POST['user'] is something suspicious such as ../secrets.txt, then $filename is set to /usr/local/secrets.txt, and the if() test fails, so Invalid user entered. is printed.

Chapter Summary

This chapter covered:

  • Understanding where the PHP engine’s file access permissions come from
  • Reading entire files with file_get_contents()
  • Writing entire files with file_put_contents()
  • Reading each line of a file with file()
  • Opening and closing files with fopen() and fclose()
  • Reading a line of a file with fgets()
  • Using feof() and a while() loop to read each line in a file
  • Using forward slashes in filenames with all operating systems
  • Providing different file modes to fopen()
  • Writing data to a file with fwrite()
  • Reading a line of a CSV file with fgetcsv()
  • Writing a line of a CSV file with fputcsv()
  • Using the php://output stream to display output
  • Determining whether a file exists with file_exists()
  • Inspecting file permissions with is_readable() and is_writeable()
  • Checking for errors returned from file access functions
  • Understanding when to check a return value with the identity operator (===)
  • Removing potentially dangerous parts of externally supplied filenames

Exercises

  1. Outside of the PHP engine, create a new template file in the style of Example 9-1. Write a program that uses file_get_contents() and file_put_contents() to read the HTML template file, substitute values for the template variables, and save the new page to a separate file.
  2. Outside of the PHP engine, create a file that contains some email addresses, one per line. Make sure a few of the addresses appear more than once in the file. Call that file addresses.txt. Then, write a PHP program that reads each line in addresses.txt and counts how many times each address appears. For each distinct address in addresses.txt, your program should write a line to another file, addresses-count.txt. Each line in addresses-count.txt should consist of the number of times an address appears in addresses.txt, a comma, and the email address. Write the lines to addresses-count.txt in sorted order from the address that occurs the most times in addresses.txt to the address that occurs the fewest times in addresses.txt.
  3. Display a CSV file as an HTML table. If you don’t have a CSV file (or spreadsheet program) handy, use the data from Example 9-9.
  4. Write a PHP program that displays a form that asks a user for the name of a file underneath the web server’s document root directory. If that file exists on the server, is readable, and is underneath the web server’s document root directory, then display the contents of the file. For example, if the user enters article.html, display the file article.html in the document root directory. If the user enters catalog/show.php, display the file show.php in the directory catalog under the document root directory. Table 7-1 tells you how to find the web server’s document root directory.
  5. Modify your solution to the previous exercise so that the program displays only files whose names end in .html. Letting users look at the PHP source code of any page on your site can be dangerous if those pages have sensitive information in them such as database usernames and passwords.