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.
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.
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.
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.
<html><head><title>{page_title}</title></head><bodybgcolor="{color}"><h1>Hello, {name}</h1></body></html>
// 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 morningif(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$page;
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>
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.
// 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 morningif(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.htmlfile_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.
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.
foreach(file('people.txt')as$line){$line=trim($line);$info=explode('|',$line);'<li><a href="mailto:'.$info[0].'">'.$info[1]."</li>\n";}
Suppose people.txt contains what’s listed in Example 9-5.
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.
$fh=fopen('people.txt','rb');while((!feof($fh))&&($line=fgets($fh))){$line=trim($line);$info=explode('|',$line);'<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:
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.)fgets() function reads a line from the file and returns it as a string.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.feof() function returns true if the bookmark is past the end of the file (“eof” stands for “end of file”).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.$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.
$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.
| 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.
try{$db=newPDO('sqlite:/tmp/restaurant.db');}catch(Exception$e){"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.txtfwrite($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().
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.
"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
try{$db=newPDO('sqlite:/tmp/restaurant.db');}catch(Exception$e){"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);"Inserted$info[0]\n";}// Close the filefclose($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.
try{$db=newPDO('sqlite:/tmp/restaurant.db');}catch(Exception$e){"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.
// Tell the web client to expect a CSV fileheader('Content-Type: text/csv');// Tell the web client to view the CSV file in a separate programheader('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.
try{$db=newPDO('sqlite:/tmp/restaurant.db');}catch(Exception$e){"Couldn't connect to database: ".$e->getMessage();exit();}// Tell the web client that a CSV file called "dishes.csv" is comingheader('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.
See Chapter 16 for details on how to install packages.
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.
if(file_exists('/usr/local/htdocs/index.html')){"Index file is there.";}else{"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().
$template_file='page-template.html';if(is_readable($template_file)){$template=file_get_contents($template_file);}else{"Can't read template file.";}
Example 9-16 verifies that a file is writeable before appending a line to it with fopen() and fwrite().
$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{"Cant write to log file.";}
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.
try{$db=newPDO('sqlite:/tmp/restaurant.db');}catch(Exception$e){"Couldn't connect to database: ".$e->getMessage();exit();}// Open dishes.txt for writing$fh=fopen('/usr/local/dishes.txt','wb');if(!$fh){"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.txtfwrite($fh,"The price of$row[0]is$row[1]\n");}if(!fclose($fh)){"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().
$page=file_get_contents('page-template.html');// Note the three equals signs in the test expressionif($page===false){"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().
$fh=fopen('people.txt','rb');if(!$fh){"Error opening people.txt:$php_errormsg";}else{while(!feof($fh)){$line=fgets($fh);if($line!==false){$line=trim($line);$info=explode('|',$line);'<li><a href="mailto:'.$info[0].'">'.$info[1]."</li>\n";}}if(!fclose($fh)){"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().
// 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 morningif(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 -1if(($result===false)||($result==-1)){"Couldn't save HTML to page.html";}
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.
// Remove slashes from user$user=str_replace('/','',$_POST['user']);// Remove .. from user$user=str_replace('..','',$user);if(is_readable("/usr/local/data/$user")){'User profile for '.htmlentities($user).': <br/>';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.
$filename=realpath("/usr/local/data/$_POST[user]");// Make sure that $filename is under /usr/local/dataif(('/usr/local/data/'==substr($filename,0,16))&&is_readable($filename)){'User profile for '.htmlentities($_POST['user']).': <br/>';file_get_contents($filename);}else{"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.
This chapter covered:
file_get_contents()file_put_contents()file()fopen() and fclose()fgets()feof() and a while() loop to read each line in a filefopen()fwrite()fgetcsv()fputcsv()php://output stream to display outputfile_exists()is_readable() and is_writeable()===)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.