Created
September 1, 2012 23:11
-
-
Save Twipped/3590667 to your computer and use it in GitHub Desktop.
Abstraction and sanitization of PHP's $_FILES superglobal.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
include 'UploadedFile.php'; | |
include 'UploadedFiles.php'; | |
$files = UploadedFiles::Get(); | |
?><!DOCTYPE HTML> | |
<html lang="en-US"> | |
<head> | |
<meta charset="UTF-8"> | |
<title>File Example</title> | |
</head> | |
<body> | |
<form method="post" accept-charset="utf-8" enctype="multipart/form-data"> | |
<pre> | |
<?php | |
if ($files->count()) { | |
foreach ($files as $file) { | |
var_dump($file); | |
} | |
} | |
?> | |
</pre> | |
<p>Single File</p> | |
<input type="file" name="standalone"> | |
<p>File Array:</p> | |
<input type="file" name="nested[]"><br> | |
<input type="file" name="nested[]"><br> | |
<br><br> | |
<input type="submit" value="Upload Files"> | |
</form> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
object(UploadedFile)[2] | |
public 'formname' => string 'standalone' (length=10) | |
public 'index' => int 0 | |
public 'valid' => boolean true | |
public 'path' => string '/tmp/phpGuSiWe' (length=14) | |
public 'type' => string 'image/png' (length=9) | |
public 'size' => int 71572 | |
public 'extension' => string 'PNG' (length=3) | |
public 'basename' => string 'Screen Shot 2012-08-02 at 1.48.07 PM.PNG' (length=40) | |
public 'filename' => string 'Screen Shot 2012-08-02 at 1.48.07 PM' (length=36) | |
public 'error' => int 0 | |
public 'raw' => | |
array (size=5) | |
'name' => string 'Screen Shot 2012-08-02 at 1.48.07 PM.PNG' (length=40) | |
'type' => string 'image/png' (length=9) | |
'tmp_name' => string '/tmp/phpGuSiWe' (length=14) | |
'error' => int 0 | |
'size' => int 71572 | |
object(UploadedFile)[3] | |
public 'formname' => string 'nested' (length=6) | |
public 'index' => int 0 | |
public 'valid' => boolean true | |
public 'path' => string '/tmp/phpPKHFxv' (length=14) | |
public 'type' => string 'image/png' (length=9) | |
public 'size' => int 33223 | |
public 'extension' => string 'PNG' (length=3) | |
public 'basename' => string 'Screen Shot 2012-08-05 at 9.56.48 PM.PNG' (length=40) | |
public 'filename' => string 'Screen Shot 2012-08-05 at 9.56.48 PM' (length=36) | |
public 'error' => int 0 | |
public 'raw' => | |
array (size=5) | |
'name' => string 'Screen Shot 2012-08-05 at 9.56.48 PM.PNG' (length=40) | |
'type' => string 'image/png' (length=9) | |
'tmp_name' => string '/tmp/phpPKHFxv' (length=14) | |
'error' => int 0 | |
'size' => int 33223 | |
object(UploadedFile)[4] | |
public 'formname' => string 'nested' (length=6) | |
public 'index' => int 1 | |
public 'valid' => boolean true | |
public 'path' => string '/tmp/phpHdFg9L' (length=14) | |
public 'type' => string 'image/png' (length=9) | |
public 'size' => int 135254 | |
public 'extension' => string 'PNG' (length=3) | |
public 'basename' => string 'Screen Shot 2012-08-24 at 10.24.11 AM.PNG' (length=41) | |
public 'filename' => string 'Screen Shot 2012-08-24 at 10.24.11 AM' (length=37) | |
public 'error' => int 0 | |
public 'raw' => | |
array (size=5) | |
'name' => string 'Screen Shot 2012-08-24 at 10.24.11 AM.PNG' (length=41) | |
'type' => string 'image/png' (length=9) | |
'tmp_name' => string '/tmp/phpHdFg9L' (length=14) | |
'error' => int 0 | |
'size' => int 135254 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
class UploadedFile { | |
const ERR_OK = 0; | |
const ERR_TOO_LARGE = 1; | |
const ERR_INCOMPLETE = 2; | |
public $formname; | |
public $index = 0; | |
public $valid = false; | |
public $path = false; | |
public $type = false; | |
public $size = false; | |
public $extension = false; | |
public $basename = false; | |
public $filename = false; | |
public $error = 0; | |
public $raw; | |
public function __construct($unit, $formname, $formname_index = 0) { | |
$this->formname = $formname; | |
$this->index = $formname_index; | |
$this->raw = $unit; | |
$this->valid = ($unit['error'] == UPLOAD_ERR_OK); | |
$this->basename = $unit['name'] ?: false; | |
if ($this->basename) { | |
$info = pathinfo($this->basename); | |
$this->filename = $info['filename']; | |
$this->extension = $info['extension']; | |
} | |
$this->size = $unit['size'] ?: false; | |
$this->path = $unit['tmp_name'] && file_exists($unit['tmp_name']) ? $unit['tmp_name'] : false; | |
if ($this->path) { | |
//if temp file exists, let's identify the file type | |
//first try detecting known filetypes using the contents. grab 4k from the file for checking | |
$h = fopen($this->path, 'r'); | |
$chunk = fread($h, 4096); | |
fclose($h); | |
$this->type = self::mimetype_by_content($chunk, $this->extension); | |
if (!$this->type && $this->extension) { | |
//couldn't identify by contents. If an extension is provided, try to identify from that | |
$this->type = self::mimetype_by_extension($this->extension); | |
} | |
if (!$this->type && $unit['type']) { | |
//still couldn't identify. Did the browser provide a type? | |
$this->type = $unit['type']; | |
} | |
} | |
} | |
/** | |
* Moves the uploaded file to a new destination, if the file can be moved. | |
* | |
* @param string $new_path | |
* @return boolean | |
*/ | |
public function moveTo($new_path) { | |
if (!$this->path) return false; | |
return move_uploaded_file($this->path, $new_path); | |
} | |
/** | |
* Opens the file for reading or writing and returns an SplFileObject | |
* See http://www.php.net/manual/en/class.splfileobject.php for more details | |
* | |
* @param string $mode | |
* @return SplFileObject | false | |
*/ | |
public function open($mode='r') { | |
if (!$this->path) return false; | |
return new SplFileObject($this->path, $mode); | |
} | |
/** | |
Private Utility Functions | |
*/ | |
/** | |
* Looks for specific bytes in a file to determine the mime type of the file | |
* | |
* @author Will Bond [wb] <will@flourishlib.com> | |
* @author Will Bond, iMarc LLC [wb-imarc] <will@imarc.net> | |
* @param string $content The first 4 bytes of the file content to use for byte checking | |
* @param string $extension The extension of the filetype, only used for difficult files such as Microsoft office documents | |
* @return string The mime type of the file | |
*/ | |
static private function mimetype_by_content($content, $extension) { | |
$length = strlen($content); | |
$_0_8 = substr($content, 0, 8); | |
$_0_6 = substr($content, 0, 6); | |
$_0_5 = substr($content, 0, 5); | |
$_0_4 = substr($content, 0, 4); | |
$_0_3 = substr($content, 0, 3); | |
$_0_2 = substr($content, 0, 2); | |
$_8_4 = substr($content, 8, 4); | |
// Images | |
if ($_0_4 == "MM\x00\x2A" || $_0_4 == "II\x2A\x00") { | |
return 'image/tiff'; | |
} | |
if ($_0_8 == "\x89PNG\x0D\x0A\x1A\x0A") { | |
return 'image/png'; | |
} | |
if ($_0_4 == 'GIF8') { | |
return 'image/gif'; | |
} | |
if ($_0_2 == 'BM' && $length > 14 && in_array($content[14], array("\x0C", "\x28", "\x40", "\x80"))) { | |
return 'image/x-ms-bmp'; | |
} | |
$normal_jpeg = $length > 10 && in_array(substr($content, 6, 4), array('JFIF', 'Exif')); | |
$photoshop_jpeg = $length > 24 && $_0_4 == "\xFF\xD8\xFF\xED" && substr($content, 20, 4) == '8BIM'; | |
if ($normal_jpeg || $photoshop_jpeg) { | |
return 'image/jpeg'; | |
} | |
if (preg_match('#^[^\n\r]*\%\!PS-Adobe-3#', $content)) { | |
return 'application/postscript'; | |
} | |
if ($_0_4 == "\x00\x00\x01\x00") { | |
return 'application/vnd.microsoft.icon'; | |
} | |
// Audio/Video | |
if ($_0_4 == 'MOVI') { | |
if (in_array($_4_4, array('moov', 'mdat'))) { | |
return 'video/quicktime'; | |
} | |
} | |
if ($length > 8 && substr($content, 4, 4) == 'ftyp') { | |
$_8_3 = substr($content, 8, 3); | |
$_8_2 = substr($content, 8, 2); | |
if (in_array($_8_4, array('isom', 'iso2', 'mp41', 'mp42'))) { | |
return 'video/mp4'; | |
} | |
if ($_8_3 == 'M4A') { | |
return 'audio/mp4'; | |
} | |
if ($_8_3 == 'M4V') { | |
return 'video/mp4'; | |
} | |
if ($_8_3 == 'M4P' || $_8_3 == 'M4B' || $_8_2 == 'qt') { | |
return 'video/quicktime'; | |
} | |
} | |
// MP3 | |
if (($_0_2 & "\xFF\xF6") == "\xFF\xF2") { | |
if (($content[2] & "\xF0") != "\xF0" && ($content[2] & "\x0C") != "\x0C") { | |
return 'audio/mpeg'; | |
} | |
} | |
if ($_0_3 == 'ID3') { | |
return 'audio/mpeg'; | |
} | |
if ($_0_8 == "\x30\x26\xB2\x75\x8E\x66\xCF\x11") { | |
if ($content[24] == "\x07") { | |
return 'audio/x-ms-wma'; | |
} | |
if ($content[24] == "\x08") { | |
return 'video/x-ms-wmv'; | |
} | |
return 'video/x-ms-asf'; | |
} | |
if ($_0_4 == 'RIFF' && $_8_4 == 'AVI ') { | |
return 'video/x-msvideo'; | |
} | |
if ($_0_4 == 'RIFF' && $_8_4 == 'WAVE') { | |
return 'audio/x-wav'; | |
} | |
if ($_0_4 == 'OggS') { | |
$_28_5 = substr($content, 28, 5); | |
if ($_28_5 == "\x01\x76\x6F\x72\x62") { | |
return 'audio/vorbis'; | |
} | |
if ($_28_5 == "\x07\x46\x4C\x41\x43") { | |
return 'audio/x-flac'; | |
} | |
// Theora and OGM | |
if ($_28_5 == "\x80\x74\x68\x65\x6F" || $_28_5 == "\x76\x69\x64\x65") { | |
return 'video/ogg'; | |
} | |
} | |
if ($_0_3 == 'FWS' || $_0_3 == 'CWS') { | |
return 'application/x-shockwave-flash'; | |
} | |
if ($_0_3 == 'FLV') { | |
return 'video/x-flv'; | |
} | |
// Documents | |
if ($_0_5 == '%PDF-') { | |
return 'application/pdf'; | |
} | |
if ($_0_5 == '{\rtf') { | |
return 'text/rtf'; | |
} | |
// Office '97-2003 or Office 2007 formats | |
if ($_0_8 == "\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1" || $_0_8 == "PK\x03\x04\x14\x00\x06\x00") { | |
if (in_array($extension, array('xlsx', 'xls', 'csv', 'tab'))) { | |
return 'application/vnd.ms-excel'; | |
} | |
if (in_array($extension, array('pptx', 'ppt'))) { | |
return 'application/vnd.ms-powerpoint'; | |
} | |
// We default to word since we need something if the extension isn't recognized | |
return 'application/msword'; | |
} | |
if ($_0_8 == "\x09\x04\x06\x00\x00\x00\x10\x00") { | |
return 'application/vnd.ms-excel'; | |
} | |
if ($_0_6 == "\xDB\xA5\x2D\x00\x00\x00" || $_0_5 == "\x50\x4F\x5E\x51\x60" || $_0_4 == "\xFE\x37\x0\x23" || $_0_3 == "\x94\xA6\x2E") { | |
return 'application/msword'; | |
} | |
// Archives | |
if ($_0_4 == "PK\x03\x04") { | |
return 'application/zip'; | |
} | |
if ($length > 257) { | |
if (substr($content, 257, 6) == "ustar\x00") { | |
return 'application/x-tar'; | |
} | |
if (substr($content, 257, 8) == "ustar\x40\x40\x00") { | |
return 'application/x-tar'; | |
} | |
} | |
if ($_0_4 == 'Rar!') { | |
return 'application/x-rar-compressed'; | |
} | |
if ($_0_2 == "\x1F\x9D") { | |
return 'application/x-compress'; | |
} | |
if ($_0_2 == "\x1F\x8B") { | |
return 'application/x-gzip'; | |
} | |
if ($_0_3 == 'BZh') { | |
return 'application/x-bzip2'; | |
} | |
if ($_0_4 == "SIT!" || $_0_4 == "SITD" || substr($content, 0, 7) == 'StuffIt') { | |
return 'application/x-stuffit'; | |
} | |
// Text files | |
if (strpos($content, '<?xml') !== FALSE) { | |
if (stripos($content, '<!DOCTYPE') !== FALSE) { | |
return 'application/xhtml+xml'; | |
} | |
if (strpos($content, '<svg') !== FALSE) { | |
return 'image/svg+xml'; | |
} | |
if (strpos($content, '<rss') !== FALSE) { | |
return 'application/rss+xml'; | |
} | |
return 'application/xml'; | |
} | |
if (strpos($content, '<?php') !== FALSE || strpos($content, '<?=') !== FALSE) { | |
return 'application/x-httpd-php'; | |
} | |
if (preg_match('#^\#\![/a-z0-9]+(python|perl|php|ruby)$#mi', $content, $matches)) { | |
switch (strtolower($matches[1])) { | |
case 'php': | |
return 'application/x-httpd-php'; | |
case 'python': | |
return 'application/x-python'; | |
case 'perl': | |
return 'application/x-perl'; | |
case 'ruby': | |
return 'application/x-ruby'; | |
} | |
} | |
// Default | |
return false; | |
} | |
/** | |
* Uses the extension of the all-text file to determine the mime type | |
* | |
* @author Will Bond [wb] <will@flourishlib.com> | |
* @author Will Bond, iMarc LLC [wb-imarc] <will@imarc.net> | |
* @param string $extension The file extension | |
* @return string The mime type of the file | |
*/ | |
static private function mimetype_by_extension($extension) { | |
switch ($extension) { | |
case 'css': | |
return 'text/css'; | |
case 'csv': | |
return 'text/csv'; | |
case 'htm': | |
case 'html': | |
case 'xhtml': | |
return 'text/html'; | |
case 'ics': | |
return 'text/calendar'; | |
case 'js': | |
return 'application/javascript'; | |
case 'php': | |
case 'php3': | |
case 'php4': | |
case 'php5': | |
case 'inc': | |
return 'application/x-httpd-php'; | |
case 'pl': | |
case 'cgi': | |
return 'application/x-perl'; | |
case 'py': | |
return 'application/x-python'; | |
case 'rb': | |
case 'rhtml': | |
return 'application/x-ruby'; | |
case 'rss': | |
return 'application/rss+xml'; | |
case 'tab': | |
return 'text/tab-separated-values'; | |
case 'vcf': | |
return 'text/x-vcard'; | |
case 'xml': | |
return 'application/xml'; | |
} | |
return false; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
class UploadedFiles implements \IteratorAggregate, \ArrayAccess, \Countable { | |
private $files = array(); | |
private $files_by_name = array(); | |
static private $singleton; | |
static public function Get() { | |
return self::$singleton ?: self::$singleton = new self(); | |
} | |
private function __construct() { | |
foreach ($_FILES as $formname => $unit) { | |
if (is_array($unit['error'])) { | |
//multiple files were uploaded under this key, so flop the array to produce a sane collection | |
$unit = self::array_flop($unit); | |
} else { | |
//only a single file was uploaded, but we want to work in a collection, so wrap it. | |
$unit = array($unit); | |
} | |
foreach ($unit as $index => $upload) { | |
$file = new UploadedFile($upload, $formname, $index); | |
if ($upload['error'] != UPLOAD_ERR_OK) { | |
//file did not upload correctly | |
//first check if no file was actually uploaded. In those cases, just skip the entry and move on | |
if ($upload['error'] == UPLOAD_ERR_NO_FILE) { | |
continue; | |
} | |
//now check if PHP itself has an issue | |
switch ($upload['error']) { | |
case UPLOAD_ERR_NO_TMP_DIR: | |
throw new UploadException("PHP Runtime Error: Upload temporary folder is either missing or undefined."); | |
case UPLOAD_ERR_CANT_WRITE: | |
throw new UploadException("PHP Runtime Error: Upload temporary folder is not writable."); | |
case UPLOAD_ERR_EXTENSION: | |
throw new UploadException("PHP Runtime Error: An unknown extension has blocked file uploads."); | |
case UPLOAD_ERR_PARTIAL: | |
$file->error = UploadedFile::ERR_INCOMPLETE; | |
break; | |
case UPLOAD_ERR_INI_SIZE: | |
case UPLOAD_ERR_FORM_SIZE: | |
$file->error = UploadedFile::ERR_TOO_LARGE; | |
break; | |
} | |
} | |
$this->files[] = $file; | |
$this->files_by_name[$formname][] = $file; | |
} | |
} | |
} | |
/** | |
arrayaccess | |
*/ | |
public function offsetGet($key){ | |
return $this->files_by_name[$key]; | |
} | |
public function offsetExists($key) { | |
isset($this->files_by_name[$key]); | |
} | |
public function offsetSet($key, $value){ | |
//ignore | |
} | |
public function offsetUnset($key){ | |
//ignore | |
} | |
/** | |
Countable and IteratorAggrigate | |
*/ | |
function count() { | |
return count($this->files); | |
} | |
public function getIterator() { | |
return new ArrayIterator($this->files); | |
} | |
/** | |
Private Utility Functions | |
*/ | |
/** | |
* Takes a named array of indexed arrays and returns an indexed array of named arrays. | |
* Useful for converting a collection of indexed form fields (ie: name="field[]") and getting individual records | |
* | |
* @param array $input The array to be flopped | |
* @return array | |
*/ | |
static private function array_flop($input) { | |
$output = array(); | |
foreach ($input as $key=>$collection) { | |
foreach ($collection as $i=>$value) { | |
$output[$i][$key] = $value; | |
} | |
} | |
return $output; | |
} | |
} | |
class UploadException extends \Exception {} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment