Ïðèãëàøàåì ïîñåòèòü
ßçûêîâ (yazykov.lit-info.ru)

Uploading User Files

Previous
Table of Contents
Next

Uploading User Files

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.

Uploading User Files

A Caution Regarding File Uploads: By allowing users to upload files to your server, you are exposing yourself to a whole new series of security problems beyond those discussed in Chapter 17, "Securing Your Web Applications: Software and Hardware Security."

For the safety of your application, your servers, your data, and your customers' data, it is vital that you spend the time to understand the security implications of allowing users to send files to your server. We discuss security more in this chapter in the section "Security Considerations."


How File Uploading Works

HTML 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:

  • There would be support for a new form <input> element type in HTML, called "file".

  • The standard application/x-www-form-urlencoded MIME type used to transmit POST data with HTTP requests (see the section "MIME Types" in Chapter 13, "Web Applications and the Internet") would be supplemented with a new multipart/form-data MIME type.

  • When submitting a form via POST with one or more files for uploading, each field in the form would get its own MIME section in the request, including one for each file being uploaded.

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 Uploading

Although 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.

Table 25-1. File Upload Configuration of php.ini

Option

Default Value

Description

post_max_size

"8M"

Controls the maximum size of an incoming POST request. This must be greater than the upload_max_filesize option value.

max_input_time

60

Specifies the amount of time (in seconds) a POST request may take to submit all of its data, after which it is cut off.

file_uploads

"1"

Indicates whether file uploads are permitted. Defaults to yes (1).

upload_max_filesize

"2M"

Controls the maximum size of file that PHP accepts. If bigger, PHP writes a 0 byte placeholder file instead.

upload_tmp_dir

NULL

Must be set to a valid directory into which uploaded files can be temporarily placed to await processing.


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 Form

Modifying a form to allow file uploads in HTML requires two changes:

  1. You add a new <input> markup tag with the "file" type.

  2. You add the enctype attribute to the form to show that we will use the new multipart/form-data MIME type.

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.

Uploading User Files


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 Code

After 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:

  1. Look at the error code associated with that file to see whether everything went well for the upload (more in a moment).

  2. If the error code indicates the file was uploaded properly, we should perform any validation or antivirus scanning we might want to do on this file.

  3. When we are comfortable with the file, we must move it to whatever location we want it to reside in. This move should be done with the move_uploaded_file function.

The error field for our file in the $_FILES array will have one of the values shown in Table 25-2.

Table 25-2. Error Codes for File Uploads

Code

Integer Value

Description

UPLOAD_ERR_OK

0

The file uploaded successfully.

UPLOAD_ERR_INI_SIZE

1

The file was larger than the value in upload_max_filesize in php.ini.

UPLOAD_ERR_FORM_SIZE

2

The file was larger than the value specified in the MAX_FILE_SIZE field of the form. (See the section "Limiting Uploaded File Size.")

UPLOAD_ERR_PARTIAL

3

The file was not completely uploaded. (Usually the request took too long to complete and was cut off.)

UPLOAD_ERR_NO_FILE

4

No file was uploaded with the request.

UPLOAD_ERR_NO_TMP_DIR

6

There is no temporary folder specified in php.ini. (This error code was added as of PHP 5.0.3.)


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 Size

One 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 Files

PHP 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
Uploading User Files

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
Uploading User Files

-----------------------------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)
  }
}


Previous
Table of Contents
Next