<? if ($userfile==”none”) { echo “Problem: no file uploaded”; exit; } if ($userfile_size==0) { echo “Problem: uploaded file is zero length”; exit; } if ($userfile_type != “text/plain”) { echo “Problem: file is not plain text”; exit; } if (!is_uploaded_file($userfile)) { echo “Problem: possible file upload attack”; exit; } $upfile = “/home/book/uploads/”.$userfile_name; if ( !copy($userfile, $upfile)) { echo “Problem: Could not move file into directory”; exit; } echo “File uploaded successfully<br><br>”; $fp = fopen($upfile, “r”); $contents = fread ($fp, filesize ($upfile)); fclose ($fp); $contents = strip_tags($contents); $fp = fopen($upfile, “w”); fwrite($fp, $contents); fclose($fp); Interacting with the File System and the Server C HAPTER 16 16 INTERACTING WITH THE F ILE SYSTEM AND THE SERVER 355 LISTING 16.2 Continued 21 7842 CH16 3/6/01 3:40 PM Page 355 echo “Preview of uploaded file contents:<br><hr>”; echo $contents; echo “<br><hr>”; ?> </body> </html> <? // This function is from the PHP manual. // is_uploaded_file is built into PHP4.0.3. // Prior to that, we can use this code. function is_uploaded_file($filename) { if (!$tmp_file = get_cfg_var(‘upload_tmp_dir’)) { $tmp_file = dirname(tempnam(‘’, ‘’)); } $tmp_file .= ‘/’ . basename($filename); /* User might have trailing slash in php.ini */ return (ereg_replace(‘/+’, ‘/’, $tmp_file) == $filename); } ?> Interestingly enough, most of this script is error checking. File upload involves potential secu- rity risks, and we need to mitigate these where possible. We need to validate the uploaded file as carefully as possible to make sure it is safe to echo to our visitors. Let’s go through the main parts of the script. First, we check whether $userfile is “none”. This is the value set by PHP if no file was uploaded. We also test that the file has some content (by testing that $userfile_size is greater than 0), and that the content is of the right type (by testing $userfile_type). We then check that the file we are trying to open has actually been uploaded and is not a local file such as /etc/passwd. We’ll come back to this in a moment. If that all works out okay, we then copy the file into our include directory. We use /home/book/uploads/ in this example—it’s outside the Web document tree, and therefore a good place to put files that are to be included elsewhere. We then open up the file, clean out any stray HTML or PHP tags that might be in the file using the strip_tags() function, and write the file back. Advanced PHP Techniques P ART IV 356 LISTING 16.2 Continued 21 7842 CH16 3/6/01 3:40 PM Page 356 Finally we display the contents of the file so the user can see that their file uploaded successfully. The results of one (successful) run of this script are shown in Figure 16.2. Interacting with the File System and the Server C HAPTER 16 16 INTERACTING WITH THE F ILE SYSTEM AND THE SERVER 357 FIGURE 16.2 After the file is copied and reformatted, the uploaded file is displayed as confirmation to the user that the upload was successful. In September 2000, an exploit was announced that could allow a cracker to fool your file upload script into processing a local file as if it had been uploaded. This exploit was docu- mented on the BUGTRAQ mailing list. You can read the official security advisory at one of the many BUGTRAQ archives, such as http://lists.insecure.org/bugtraq/2000/Sep/0237.html We have used the is_uploaded_file() function to make sure that the file we are processing has actually been uploaded and is not a local file such as /etc/passwd. This function will be in PHP version 4.0.3. At the time of writing the current release was 4.0.2, so we have used the sample code for this function from the PHP manual. Unless you write your upload handling script carefully, a malicious visitor could provide his own temporary filename and convince your script to handle that file as though it were the uploaded file. As many file upload scripts echo the uploaded data back to the user, or store it somewhere that it can be loaded, this could lead to people being able to access any file that the Web server can read. This could include sensitive files such as /etc/passwd and PHP source code including your database passwords. 21 7842 CH16 3/6/01 3:40 PM Page 357 Common Problems There are a few things to keep in mind when performing file uploads. • The previous example assumes that users have been authenticated elsewhere. You shouldn’t allow just anybody to upload files on to your site. • If you are allowing untrusted or unauthenticated users to upload files, it’s a good idea to be pretty paranoid about the contents of them. The last thing you want is a malicious script being uploaded and run. You should be careful, not just of the type and contents of the file as we are here, but of the filename itself. It’s a pretty good idea to rename uploaded files to something you know to be “safe.” • If you are using an NT or other Windows-based machines, be sure to use \\ instead of \ in file paths as usual. • If you are having problems getting this to work, check out your php.ini file. You will need to have set the upload_tmp_dir directive to point to some directory that you have access to. You might also need to adjust the memory_limit directive if you want to upload large files—this will determine the maximum file size in bytes that you can upload. • If PHP is running in safe mode, you will get an error message about being unable to access the temporary file. This can only be fixed either by not running in safe mode or by writing a non-PHP script that copies the file to an accessible location. You can then execute this script from your PHP script. We’ll look at how to execute programs on the server from PHP toward the end of this chapter. Using Directory Functions After the users have uploaded some files, it will be useful for them to be able to see what’s been uploaded and manipulate the content files. PHP has a set of directory and file system functions that are useful for this purpose. Reading from Directories First, we’ll implement a script to allow directory browsing of the uploaded content. Browsing directories is actually very straightforward in PHP. In Listing 16.3, we show a simple script that can be used for this purpose. Advanced PHP Techniques P ART IV 358 21 7842 CH16 3/6/01 3:40 PM Page 358 LISTING 16.3 browsedir.php—A Directory Listing of the Uploaded Files <html> <head> <title>Browse Directories</title> </head> <body> <h1>Browsing</h1> <? $current_dir = “/home/book/uploads/”; $dir = opendir($current_dir); echo “Upload directory is $current_dir<br>”; echo “Directory Listing:<br><hr><br>”; while ($file = readdir($dir)) { echo “$file<br>”; } echo “<hr><br>”; closedir($dir); ?> </body> </html> This script makes use of the opendir(), closedir(), and readdir() functions. The function opendir()is used to open a directory for reading. Its use is very similar to the use of fopen() for reading from files. Instead of passing it a filename, you should pass it a directory name: $dir = opendir($current_dir); The function returns a directory handle, again in much the same way as fopen() returns a file handle. When the directory is open, you can read a filename from it by calling readdir($dir),as shown in the example. This returns false when there are no more files to be read. (Note that it will also return false if it reads a file called “0”—you could, of course, test for this if it is likely to occur.) Files aren’t sorted in any particular order, so if you require a sorted list, you should read them into an array and sort that. When you are finished reading from a directory, you call closedir($dir) to finish. This is again similar to calling fclose() for a file. Sample output of the directory browsing script is shown in Figure 16.3. Interacting with the File System and the Server C HAPTER 16 16 INTERACTING WITH THE F ILE SYSTEM AND THE SERVER 359 21 7842 CH16 3/6/01 3:40 PM Page 359 FIGURE 16.3 The directory listing shows all the files in the chosen directory, including the . (the current directory) and (one level up) directories. You can choose to filter these out. If you are making directory browsing available via this mechanism, it is sensible to limit the directories that can be browsed so that a user cannot browse directory listings in areas not nor- mally available to him. An associated and sometimes useful function is rewinddir($dir), which resets the reading of filenames to the beginning of the directory. As an alternative to these functions, you can use the dir class provided by PHP. This has the properties handle and path, and the methods read(), close(), and rewind(), which perform identically to the non-class alternatives. Getting Info About the Current Directory We can obtain some additional information given a path to a file. The dirname($path) and basename($path) functions return the directory part of the path and the filename part of the path, respectively. This could be useful for our directory browser, par- ticularly if we began to build up a complex directory structure of content based on meaningful directory names and filenames. We could also add to our directory listing an indication of how much space is left for uploads by using the diskfreespace($path) function. If you pass this function a path to a directory, it will return the number of bytes free on the disk (Windows) or the file system (UNIX) that the directory is on. Advanced PHP Techniques P ART IV 360 21 7842 CH16 3/6/01 3:40 PM Page 360 Creating and Deleting Directories In addition to passively reading information about directories, you can use the PHP functions mkdir() and rmdir() to create and delete directories. You will only be able to create or delete directories in paths that the user the script runs as has access to. Using mkdir() is more complicated than you might think. It takes two parameters, the path to the desired directory (including the new directory name), and the permissions you would like that directory to have, for example, mkdir(“/tmp/testing”, 0777); However, the permissions you list are not necessarily the permissions you are going to get. The current umask will be ANDed (like subtraction) with this value to get the actual permissions. For example, if the umask is 022, you will get permissions of 0755. You might like to reset the umask before creating a directory to counter this effect, by entering $oldumask = umask(0); mkdir(“/tmp/testing”, 0777); umask($oldumask); This code uses the umask() function, which can be used to check and change the current umask. It will change the current umask to whatever it is passed and return the old umask, or if called without parameters, it will just return the current umask. The rmdir() function deletes a directory, as follows: rmdir(“/tmp/testing”); or rmdir(“c:\\tmp\\testing”); The directory you are trying to delete must be empty. Interacting with the File System In addition to viewing and getting information about directories, we can interact with and get information about files on the Web server. We’ve previously looked at writing to and reading from files. A large number of other file functions are available. Get File Info We can alter the part of our directory browsing script that reads files as follows: while ($file = $dir->read()) { Interacting with the File System and the Server C HAPTER 16 16 INTERACTING WITH THE F ILE SYSTEM AND THE SERVER 361 21 7842 CH16 3/6/01 3:40 PM Page 361 echo “<a href=\”filedetails.php?file=”.$file.”\”>”.$file.”</a><br>”; } We can then create the script filedetails.php to provide further information about a file. The contents of this file are shown in Listing 16.4. One warning about this script: Some of the functions used here are not supported under Windows, including fileowner() and filegroup(), or are not supported reliably. LISTING 16.4 filedetails.php—File Status Functions and Their Results <html> <head> <title>File Details</title> </head> <body> <? $current_dir = “/home/book/uploads/”; $file = basename($file); // strip off directory information for security echo “<h1>Details of file: “.$file.”</h1>”; $file = $current_dir.$file; echo “<h2>File data</h2>”; echo “File last accessed: “.date(“j F Y H:i”, fileatime($file)).”<br>”; echo “File last modified: “.date(“j F Y H:i”, filemtime($file)).”<br>”; $user = posix_getpwuid(fileowner($file)); echo “File owner: “.$user[“name”].”<br>”; $group = posix_getgrgid(filegroup($file)); echo “File group: “.$group[“name”].”<br>”; echo “File permissions: “.decoct(fileperms($file)).”<br>”; echo “File type: “.filetype($file).”<br>”; echo “File size: “.filesize($file).” bytes<br>”; echo “<h2>File tests</h2>”; echo “is_dir: “.(is_dir($file)? “true” : “false”).”<br>”; echo “is_executable: “.(is_executable($file)? “true” : “false”).”<br>”; echo “is_file: “.(is_file($file)? “true” : “false”).”<br>”; echo “is_link: “.(is_link($file)? “true” : “false”).”<br>”; echo “is_readable: “.(is_readable($file)? “true” : “false”).”<br>”; echo “is_writable: “.(is_writable($file)? “true” : ”false”).”<br>”; Advanced PHP Techniques P ART IV 362 21 7842 CH16 3/6/01 3:40 PM Page 362 ?> </body> </html> The results of one sample run of Listing 16.4 are shown in Figure 16.4. Interacting with the File System and the Server C HAPTER 16 16 INTERACTING WITH THE F ILE SYSTEM AND THE SERVER 363 LISTING 16.4 Continued FIGURE 16.4 The File Details view shows file system information about a file. Note that permissions are shown in an octal format. Let’s talk about what each of the functions used in Listing 16.4 does. As mentioned previously, the basename() function gets the name of the file without the direc- tory. (You can also use the dirname() function to get the directory name without the filename.) The fileatime() and filemtime() functions return the time stamp of the time the file was last accessed and last modified, respectively. We’ve reformatted the time stamp using the date() function to make it more human-readable. These functions will return the same value on some operating systems (as in the example) depending on what information the system stores. The fileowner() and filegroup() functions return the user ID (uid) and group ID (gid) of the file. These can be converted to names using the functions posix_getpwuid() and posix_getgrgid(), respectively, which makes them a bit easier to read. These functions take the uid or gid as a parameter and return an associative array of information about the user or group, including the name of the user or group, as we have used in this script. 21 7842 CH16 3/6/01 3:40 PM Page 363 The fileperms() function returns the permissions on the file. We have reformatted them as an octal number using the decoct() function to put them into a format more familiar to UNIX users. The filetype() function returns some information about the type of file being examined. The possible results are fifo, char, dir, block, link, file, and unknown. The filesize() function returns the size of the file in bytes. The second set of functions—is_dir(), is_executable(), is_file(), is_link(), is_ readable(), and is_writable()—all test the named attribute of a file and return true or false. We could alternatively have used the function stat() to gather a lot of the same information. When passed a file, this returns an array containing similar data to these functions. The lstat() function is similar, but for use with symbolic links. All the file status functions are quite expensive to run in terms of time. Their results are therefore cached. If you want to check some file information before and after a change, you need to call clearstatcache(); in order to clear the previous results. If you wanted to use the previous script before and after changing some of the file data, you should begin by calling this function to make sure the data produced is up-to-date. Changing File Properties In addition to viewing file properties, we can alter them. Each of the chgrp(file, group), chmod(file, permissions), and chown(file, user) functions behaves similarly to its UNIX equivalent. None of these will work in Windows-based systems, although chown() will execute and always return true. The chgrp() function is used to change the group of a file. It can only be used to change the group to groups of which the user is a member unless the user is root. The chmod() function is used to change the permissions on a file. The permissions you pass to it are in the usual UNIX chmod form—you should prefix them with a “0” to show that they are in octal, for example, chmod(“somefile.txt”, 0777); The chown() function is used to change the owner of a file. It can only be used if the script is running as root, which should never happen. Advanced PHP Techniques P ART IV 364 21 7842 CH16 3/6/01 3:40 PM Page 364 . writing a non -PHP script that copies the file to an accessible location. You can then execute this script from your PHP script. We’ll look at how to execute programs on the server from PHP toward. show a simple script that can be used for this purpose. Advanced PHP Techniques P ART IV 358 21 7842 CH16 3/6/01 3:40 PM Page 358 LISTING 16.3 browsedir .php A Directory Listing of the Uploaded. /etc/passwd and PHP source code including your database passwords. 21 7842 CH16 3/6/01 3:40 PM Page 357 Common Problems There are a few things to keep in mind when performing file uploads. • The previous