File Upload Progress in PHP 5.4

With the release of PHP 5.4 the file upload progress feature becomes available. Yes, “available” as in “you can use it in your project now”.

PHP 5.4 File Upload Progress

This is a quick start guide with the least amount of code required, using jQuery’s compact AJAX requests and iframes.

The code

The code is subdivided into three main parts – the upload form, the AJAX requests and the upload progress response generation.

The form

The form is injected into the page via an iframe, since Webkit browsers lock all JavaScript requests once an upload is underway.

<form action="" method="POST" enctype="multipart/form-data">
  <input type="hidden" name="<?php echo ini_get("session.upload_progress.name"); ?>" value="demo">
  <input type="file" name="uploaded_file">
  <input type="submit" value="Upload">
</form>

<script type="text/javascript">
window.location.hash = ""; /* reset */
jQuery("form").bind("submit", function() { window.location.hash = "uploading"; });
</script>

PHP listens for a POST variable matching the session.upload_progress.name value. Other than that it’s a regular form. This code will be shown in the iframe. Tracking of whether upload has started yet or not is done via the window.location.hash value. There are many other valid (and probably better) methods, this is one I decided to try out today.

The AJAX requests

This is shown on the upload page itself, including the iframe.

<iframe src="?iframe" id="upload_form"></iframe>
<script type="text/javascript">
  jQuery(document).ready(update_file_upload_progress);

  function update_file_upload_progress() {
    if ( window.frames.upload_form.location.hash != "#uploading" ) {
      setTimeout( update_file_upload_progress, 100 ); /* has upload started yet? */
      return;
    }
    $.get( /* lather */
      "?progress",
      function(data) {
        /* rinse */
        jQuery("#file_upload_progress").html(data);
        /* repeat */
        setTimeout( update_file_upload_progress, 500 ); /* call itself in # ms */
      }
    ).error(function(jqXHR, error) { alert(error); });
  }
</script>

<div id="file_upload_progress"></div>

The JavaScript is simple, constantly polls the hash of the upload iframe and as soon as it changes starts polling the server for upload progress.

The response

  if ( isset( $_GET['progress'] ) ) {

    $progress_key = strtolower(ini_get("session.upload_progress.prefix").'demo');
  
    if ( !isset( $_SESSION[$progress_key] ) ) exit( "uploading..." );

    $upload_progress = $_SESSION[$progress_key];
    /* get percentage */
    $progress = round( ($upload_progress['bytes_processed'] / $upload_progress['content_length']) * 100, 2 );

    exit( "Upload progress: $progress%" );
  }

The key for the session upload progress is assembled by combining the session.upload_progress.prefix directive along with whatever value the form supplied for the progress upload hidden field.

The structure of the data inside the upload progress array is the following:

  • int start_time
  • int content_length
  • int bytes_processed
  • bool done
  • array files
    • string field_name
    • string name (filename)
    • string tmp_name
    • int error
    • bool done
    • int start_time
    • int bytes_processed

All together on one page

<?php
  /* File upload progress in PHP 5.4 */
  
  /* needs a 5.4+ version */
  $version = explode( '.', phpversion() );
  if ( ($version[0] * 10000 + $version[1] * 100 + $version[2]) < 50400 )
    die( 'PHP 5.4.0 or higher is required' );

  if ( !intval(ini_get('session.upload_progress.enabled')) )
    die( 'session.upload_progress.enabled is not enabled' );

  session_start();

  if ( isset( $_GET['progress'] ) ) {

    $progress_key = strtolower(ini_get("session.upload_progress.prefix").'demo');
  
    if ( !isset( $_SESSION[$progress_key] ) ) exit( "uploading..." );

    $upload_progress = $_SESSION[$progress_key];
    /* get percentage */
    $progress = round( ($upload_progress['bytes_processed'] / $upload_progress['content_length']) * 100, 2 );

    exit( "Upload progress: $progress%" );
  }  
?>

<script type="text/javascript" src="http://code.jquery.com/jquery-1.7.1.min.js"></script>

<?php if ( isset($_GET['iframe']) ): /* thank you Webkit... */ ?>
<form action="" method="POST" enctype="multipart/form-data">
  <input type="hidden" name="<?php echo ini_get("session.upload_progress.name"); ?>" value="demo">
  <input type="file" name="uploaded_file">
  <input type="submit" value="Upload">
</form>

<script type="text/javascript">
window.location.hash = ""; /* reset */
jQuery("form").bind("submit", function() { window.location.hash = "uploading"; });
</script>

<?php else: ?>

<iframe src="?iframe" id="upload_form"></iframe>
<script type="text/javascript">
  jQuery(document).ready(init);

  function init() {
    /* start listening on submit */
    update_file_upload_progress();
  }

  function update_file_upload_progress() {
    if ( window.frames.upload_form.location.hash != "#uploading" ) {
      setTimeout( update_file_upload_progress, 100 ); /* has upload started yet? */
      return;
    }
    $.get( /* lather */
      "?progress",
      function(data) {
        /* rinse */
        jQuery("#file_upload_progress").html(data);
        /* repeat */
        setTimeout( update_file_upload_progress, 500 );
      }
    ).error(function(jqXHR, error) { alert(error); });
  }
</script>

<div id="file_upload_progress"></div>
<?php endif; ?>

Troubleshooting

If PHP’s new file upload progress feature does not work ($_SESSION is empty, for example) for you check and make sure of the following:

Other methods

The new feature is very convenient in many cases, however it has its disadvantages, like the constant polling of the server, the iframe, among others. Upload progress indicators with a lot of bells and whistles come in such upload plugins as Plupload (used in WordPress 3.3), SWFupload, etc. and should be used in larger-scale projects.

Conclusion

PHP’s new file upload progress function is lightweight, useful and interesting to work with. It will never match more powerful solutions with their multiple-file selections, built-in image converters and client-side progress calculation, but it’s something that is easy to setup, is quite manageable and clean.

So, have you tried it already? Are you using it in any of your projects? Hit any limitations? Had any difficulties in setting it up?