Some web applications have the real and valid need for users to upload files to the server. For example, web-based e-mail clients want to let the user upload files to attach to e-mails being sent, whereas image-hosting web sites want to let users upload the image files they want hosted on their behalf. PHP includes full support for file uploads, and we cover this now.
How File Uploading WorksHTML and the HTTP protocol, as they first existed, had no real support for file uploads. A number of people, recognizing this, began an attempt to solve this problem. The end result of these efforts was an open specification, RFC 1867. (RFC stands for Request For Comments. It is a standard way to publish technical specifications on the Internet.) RFC 1867 specified the following:
The section "The Client Form" later in this chapter examines how to modify a form to permit file uploads and shows what the new HTTP request to our server looks like. Before we do that, however, we look at how to configure PHP for file uploads. Configuring PHP for UploadingAlthough PHP includes built-in support for file uploads, it typically requires some configuration before it can be used without problems. You must inspect and configure five directives in php.ini before permitting users to upload files to your server, as shown in Table 25-1.
Most of the options shown are easily understood except perhaps for the max_input_time directive. This effectively limits the amount of time a client may stay connected to a particular server uploading the contents of a request (including any attached files). Therefore, if our web application is designed to allow users to attach 15MB files on a regular basis, and we expect them to be using normal Internet connections such as DSL or cable modems, we are definitely going to need to increase the value beyond 60 seconds. For sites that are going to want to limit their data to maybe 500KB, this would be an entirely acceptable value. Many installations of PHP come without the upload_tmp_dir configured at all. You need to set this to some directory to which the user the PHP server operates as has write permissions. If not, no uploads will succeed, and you might spend some time scratching your head trying to understand why. As we explain in a bit, we will use some empty and unimportant file system where no problems will be caused if it completely fills up: upload_tmp_dir = z:/webapp_uploads ; Windows upload_tmp_dir = /export/uploads ; Unix The Client FormModifying a form to allow file uploads in HTML requires two changes:
If we had a simple user registration form that took a username, a password, and a picture to represent them (sometimes called an avatar), our form might look like this: <form enctype="multipart/form-data" action="processnewuser.php" method="POST"> User Name: <input type='text' name='user_name' size='30'/><br/> Password: <input type='password' name='password' size='30'/><br/> User Image File: <input name="avatarfile" type="file"/><br/> <input type="submit" value="Register" /><br/> </form> This form might look something similar to that shown in Figure 25-1. Figure 25-1. A simple registration form with a file upload option.As you can see from the form, most web browsers add a small Browse button next to the User Image File field for us. When the user enters the data and clicks the Register button, the client browser sends a new request to the server. Based on the suggestions of RFC 1867 discussed previously, this request will look something like this: POST /webapp/processnewuser.php HTTP/1.1 Host: phpsrvr User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; rv:1.7.3) Gecko/20040913 Firefox/0.10.1 Accept: text/xml,application/xml, application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8, image/png,*/*;q=0.5 Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 Content-Type: multipart/form-data; boundary=---------------------------4664151417711 Content-Length: 49335 -----------------------------4664151417711 Content-Disposition: form-data; name="user_name" chippy -----------------------------4664151417711 Content-Disposition: form-data; name="password" i_like_nuts -----------------------------4664151417711 Content-Disposition: form-data; name="avatarfile"; filename="face.jpg" Content-Type: image/jpeg [ ~48k of binary data ] As we can clearly see, instead of the request body being a simple collection of form data, it is now a complicated multipart MIME construction. The last section in our particular request includes the full contents of the file being included, which goes until the beginning of the next MIME boundary marker or the end of the request (as is the case here). The Server CodeAfter we have the request on the way to the server with any files attached to it, we must look at how to actually access these files on the server. This is primarily done through the superglobal array called $_FILES. This contains one element, with the key being the same name as the <input> field from the HTML file. (In the preceding example, this was avatarfile.) The value of this is itself an array containing information about the uploaded file: array(1) { ["avatarfile"]=> array(5) { ["name"]=> string(8) "fair.jpg" ["type"]=> string(10) "image/jpeg" ["tmp_name"]=> string(28) "/export/uploads/phpC9.tmp" ["error"]=> int(0) ["size"]=> int(48823) } } One feature of file uploads in PHP is that they are not immediately placed for all to see on the file system. When the server first receives the uploads, if they are smaller than the permitted size of uploaded files, they are placed in the location specified by the upload_tmp_dir directive in php.ini. From here, you must perform validation on the uploads (if necessary) and move them to some other location. You find the location of the temporary file location by querying the tmp_name key in the $_FILES array for the appropriate uploaded file. PHP deletes any uploaded files still in the temporary upload directory after script execution ends in the name of security. To process an uploaded file, we must perform the following actions:
The error field for our file in the $_FILES array will have one of the values shown in Table 25-2.
Therefore, only if the error code in $_FILES['avatarfile']['error'] is UPLOAD_ERR_OK (0) should we continue processing the file at all. In this case, we could do some validation, depending on how advanced our system is and what requirements we have. If we were allowing users to upload arbitrary binary data, we might want to run a virus scanner on the file to make sure it is safe for our networks. We might otherwise just want to make sure the file is an image file and reject other types. After we have done this, we need to move the file from its temporary location to its final resting place (at least as far as this page is concerned). Although this can be done with any file functions such as copy or rename, it is best done with the move_uploaded_file function, which makes sure that the file being moved truly was one of the files uploaded to the server with the request. This helps prevent possible situations where a malicious user could try to trick us into moving a system file (/etc/passwd, c:\windows\php.ini) into the location where we eventually put uploaded files. The move_uploaded_file function actually makes sure that the specified file was uploaded fully and successfully. By using this function and checking the error result in the $_FILES superglobal, we significantly reduce the exposure to attacks through file uploading. Our code in the file to process the uploaded therefore becomes something along the following lines: // // did the upload succeed or fail? // if ($_FILES['avatarfile']['error'] == UPLOAD_ERR_OK) { // // verify (casually) that this appears to be an image file // $ext = strtolower(pathinfo($_FILES['avatarfile']['name'], PATHINFO_EXTENSION)); switch ($ext) { case 'jpg': case 'jpeg': case 'gif': case 'png': case 'bmp': break; // file type is okay! default: throw new InvalidFileTypeException($ext); } // // move the file to the appropriate location // $destfile = '../user_photos/' . basename($_FILES['avatarfile']['name']; $ret = @move_uploaded_file($_FILES['avatarfile']['tmp_name'], $destfile); if ($ret === FALSE) echo "Unable to move user photo!<br/>\n"; else echo "Moved user avatar to photos directory<br/>\n"; } else { // // see what the error was. // switch ($_FILES['avatarfile']['error']) { case UPLOAD_ERR_INI_SIZE: case UPLOAD_ERR_FORM_SIZE: throw new FileSizeException(); break; case UPLOAD_ERR_PARTIAL: throw new IncompleteUploadException(); break; case UPLOAD_ERR_NO_FILE: throw new NoFileReceivedException(); break; // PHP 5.0.3 + only!! case UPLOAD_ERR_NO_TMP_DIR: throw new InternalError('no upload directory'); >\n"; break; default: echo "say what?"; break; } } Limiting Uploaded File SizeOne of the things we would very much like to do with file uploads is limit the size of any given file sent to our server. RFC 1867 does, in fact, specify an attribute to add to the <input type="file"> markup element to ask browsers to voluntarily limit file sizes. This attribute is called maxlength, and it is used as follows: <input name="avatarfile" type="file" maxlength="50000"/> The number specified with the attribute is the size limit in bytes for the file being uploaded to our server. Unfortunately, not a single browser yet appears to support this field. (It is simply ignored.) Therefore, we must look for other ways to limit the size of files being uploaded to us. One method, which we have already seen, is to be sure to set a reasonable limit in php.ini for the upload_max_size option. PHP does not allow files greater than this to be uploaded to our server (and sets the error field in the $_FILES array to UPLOAD_ERR_INI_SIZE). However, if we have a web application that allows users to upload documents as large as 2MB in one place, but we want to limit a specific upload such as their user picture, to 50KB, it would be nice if there were a way to specify, along with a form, a file size limit. Because the maxlength attribute does not work, PHP has implemented a rather novel solution to the problem. If you include in your form a hidden field with the name MAX_FILE_SIZE, this field and its value are sent back to the server along with the rest of the form request. If PHP sees a submitted form value of MAX_FILE_SIZE along with a file being uploaded, it limits the file to that size (or sets the error field in $_FILES to UPLOAD_ERR_FORM_SIZE). Our form would now look like this: <form enctype="multipart/form-data" action="processnewuser.php" method="POST"> User Name: <input type='text' name='user_name' size='30'/><br/> Password: <input type='password' name='password' size='30'/><br/> User Image File: <input type="hidden" name="MAX_FILE_SIZE" value="100000"/> <input name="avatarfile" type="file"/><br/> <input type="submit" value="Register" /><br/> </form> As have noted repeatedly in this book, however, anybody with the telnet program can easily send his own HTTP requests to our server, so there is little guarantee that this hidden field will actually come back to us with the correct value, or at all. Enforcing file size limits on uploaded files is mostly a server-side effort. Handling Multiple FilesPHP actually supports more than one file being uploaded at a time. Suppose we have the following two files, abc.txt abc def ghi jkl mno and 123.txt 123 456 789 000 To create a form through which both could be submitted, we would write a new HTML form that had two <input type="file"> fields. We would be sure to give them each separate names, as follows: <form enctype="multipart/form-data" action="uploadfile.php" method="POST"> Upload File #1: <input name="file1" type="file"/><br/> Upload File #2: <input name="file2" type="file"/><br/> <input type="submit" value="Submit" /><br/> </form> These files would be sent to our server as part of an HTTP request that look like this: POST /uploadfile.php HTTP/1.1 Host: phpsrvr User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; rv:1.7.3) Gecko/20040913 Firefox/0.10.1 Accept: text/xml,application/xml,application/xhtml+xml, text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5 Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 Referer: http://phpsrvr/phpwasrc/chapter25/upload_form.html Content-Type: multipart/form-data; boundary=---------------------------313223033317673 Content-Length: 395 -----------------------------24393354819629 Content-Disposition: form-data; name="file1"; filename="abc.txt" Content-Type: text/plain ?abc def ghi jkl mno -----------------------------24393354819629 Content-Disposition: form-data; name="file2"; filename="123.txt" Content-Type: text/plain ?123 456 789 000 -----------------------------24393354819629-- On our server, the $_FILES array now has two indices with data: "file1" and "file2." Its contents will look something like this: array(2) { ["file1"]=> array(5) { ["name"]=> string(7) "abc.txt" ["type"]=> string(10) "text/plain" ["tmp_name"]=> string(27) "Z:\webapp_uploads\phpE2.tmp" ["error"]=> int(0) ["size"]=> int(45) } ["file2"]=> array(5) { ["name"]=> string(7) "123.txt" ["type"]=> string(10) "text/plain" ["tmp_name"]=> string(27) "Z:\webapp_uploads\phpE3.tmp" ["error"]=> int(0) ["size"]=> int(23) } } |