HTML Drag and Drop File Upload With PHP Backend

This one fought me, for hours yesterday. A client wanted drag and drop implemented for file on their server. This can already be done in most modern browsers by simply dropping the files onto the file input, but that's a very awkward UI, likely to cause issues. Most users aren't experienced enough to know they can do that. So you have three reasonable choices:

  1. At a minimum, prevent drag and drop on non-registered elements causing the page to load that image. Force them to use the native handling of files dropped onto file inputs.
  2. A little more reasonable would be to create a region that makes dragging obvious, and a large target for them to aim for that lights up when they are in place.
  3. Or, perhaps the best if you only have a single file input, and it's the most important thing on the page anyway, make the entire page listen for a dropped file and handle it.

My particular scenario yesterday was #3, but #2 would be done in roughly the same way. I'm going to post some slightly more than bare bones code for javascript, and php. I'll leave the css and html up to you. It's still a bit raw, the goal was a functioning prototype for working scenarios.

Javascript

Drag and Drop Event Listeners, FileReaders, and XmlHttpRequest objects. All without an ounce of jQuery.

  1. example.upload = {
  2.   // Support flag, for reuse
  3.   dndSupported: false
  4.   // Testable initilization flag
  5.   ,behaviors: false
  6.   // Pointer to our <progress> element
  7.   ,progress: null
  8.   ,init: function(){
  9.     // Ensure we have drag and drop
  10.     var div = document.createElement('div');
  11.     // Simple test of capability similar to moderizer
  12.     example.upload.dndSupported = ('draggable' in div) || ('ondragstart' in div && 'ondrop' in div);
  13.  
  14.     if (!example.upload.dndSupported) {
  15.       alert('Drag and Drop not supported');
  16.     } else {
  17.       this.bindBehaviors();
  18.     }
  19.   }
  20.   ,bindBehaviors: function(){
  21.     if( !example.upload.behaviors ){
  22.       // Binding to the entire document
  23.       // You could target anything instead here
  24.       var doc = document.documentElement;
  25.       doc.ondragover = function () { this.className = 'hover'; return false; };
  26.       doc.ondragend = function () { this.className = ''; return false; };
  27.       doc.ondrop = function (event) {
  28.         example.upload.onDrop(event);
  29.       };
  30.       example.upload.progress = document.getElementById('progress');
  31.       example.upload.preview.target = document.getElementById('preview');
  32.       example.upload.behaviors = true;
  33.       example.upload.reset();
  34.     }
  35.   }
  36.   ,hideFields: function(){
  37.     document.getElementById('fields-wrapper').style.display = 'none';
  38.   }
  39.   ,showFields: function(){
  40.     document.getElementById('fields-wrapper').style.display = 'inline-block';
  41.   }
  42.   ,reset: function(){
  43.     example.upload.showFields();
  44.     example.upload.updateProgress(0);
  45.     example.upload.preview.clear();
  46.   }
  47.   ,onDrop: function(event){
  48.     example.upload.hideFields();
  49.     example.upload.preview.clear();
  50.     example.upload.updateProgress(0);
  51.     this.className = '';
  52.  
  53.     // Preventing default behavior (viewing file directly)
  54.     event.preventDefault && event.preventDefault();
  55.  
  56.     var files = event.dataTransfer.files;
  57.     if( files.length > 1){
  58.       alert('Only one file may be uploaded.');
  59.     } else if( files.length == 0 ) {
  60.       return;
  61.     }
  62.     var file = files[0];
  63.     // Display a preview if applicable
  64.     example.upload.preview.display(file);
  65.  
  66.     var xhr = new XMLHttpRequest();
  67.    
  68.     // Put is a little more resilient with large files
  69.     xhr.open("put", 'upload-file', true);
  70.     // Some variables to use saving the file
  71.     xhr.setRequestHeader("X-File-Name", file.name);
  72.     xhr.setRequestHeader("X-File-Size", file.size);
  73.     // File upload finished behavior
  74.     xhr.onload = function () {
  75.       // just in case we get stuck around 99%
  76.       example.upload.updateProgress(100);
  77.       console.log(xhr);
  78.       if (xhr.status === 200) {
  79.         console.log('all done: ' + xhr.status);
  80.       } else {
  81.         example.upload.reset();
  82.         console.log('Something went terribly wrong...');
  83.       }
  84.     };
  85.     // Progress update behavior
  86.     xhr.upload.onprogress = function (event) {
  87.       if (event.lengthComputable) {
  88.         example.upload.updateProgress( (event.loaded / event.total * 100 | 0) );
  89.       }
  90.     };
  91.    
  92.     // Send just the file contents
  93.     xhr.send(file);
  94.    
  95.     return false;
  96.   }
  97.   ,updateProgress: function(percentComplete){
  98.     if( percentComplete == 0 ){
  99.       example.upload.progress.style.display = 'none';
  100.     } else {
  101.       example.upload.progress.style.display = 'inline-block';
  102.     }
  103.     example.upload.progress.value = example.upload.progress.innerHTML = percentComplete;
  104.   }
  105.   ,preview: {
  106.     target: null
  107.     ,display: function(file){
  108.       example.upload.preview.clear();
  109.      
  110.       // Creating a div with the filename
  111.       nameDiv = document.createElement("div");
  112.       nameDiv.innerHTML = file.name;
  113.       nameDiv.className = 'file-name';
  114.       example.upload.preview.target.appendChild(nameDiv);
  115.      
  116.       // Allow previews of certain file types
  117.       switch(file.type){
  118.         case 'image/png':
  119.         case 'image/jpeg':
  120.         case 'image/gif':
  121.           // Creates an image on the fly with a generated src
  122.           var reader = new FileReader();
  123.           reader.onload = function (event) {
  124.             var image = new Image();
  125.             image.src = event.target.result;
  126.             image.width = 100; // a fake resize
  127.             example.upload.preview.target.appendChild(image);
  128.           }
  129.           reader.readAsDataURL(file);
  130.           break;
  131.       }
  132.      
  133.     }
  134.     ,clear: function(){
  135.       example.upload.preview.target.innerHTML = '';
  136.     }
  137.   }
  138. }
  139.  
  140. // A document ready call. Build your own as you like
  141. ready(function(){
  142.   example.upload.init();
  143. });

Php File Saving Code

  1.       // Read the input directly, there is no post, get, data
  2.       $in = fopen('php://input','r');
  3.       // Open any file you like for reading
  4.       $out = fopen(__DIR__./uploads/' . $_SERVER['HTTP_X_FILE_NAME'], "w+");
  5.      // Stream the data out to a file
  6.      while($data = fread($in, 1024)) fwrite($out, $data);
  7.      // Close handlers
  8.      fclose($in);
  9.      fclose($out);

Are there security concerns building it this way? Of course! The php code above should scare the hell out of you. But, security is your own pass, outside this example. Please, please make sure you review your own and implement a solution that works for your needs. Remember, no matter what the browser passes you, it's important to validate.

Add new comment

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
By submitting this form, you accept the Mollom privacy policy.