File upload class (collaborative)

Not for 'how-to' coding questions but PHP theory instead, this forum is here for those of us who wish to learn about design aspects of programming with PHP.

Moderator: General Moderators

Post Reply
User avatar
Luke
The Ninja Space Mod
Posts: 6424
Joined: Fri Aug 05, 2005 1:53 pm
Location: Paradise, CA

File upload class (collaborative)

Post by Luke »

I have been discussing ways one could establish uploading/downloading of files and folders with arborint. In this thread, we will be building a set of classes meant to do just that. What we have to start with is Arborint's Upload and Download classes. What I would like to do is take this, and transform them into smaller, more specific classes. I was thinking maybe a File class, a Folder class, an upload class, and a download class. I am open to suggestions. I will post the code right now while I am working on the solution, just to get some input.

Upload.php

Code: Select all

<?php
define('A_HTTP_UPLOAD_NOT_UPLOAD_FILE', 1001);
define('A_HTTP_UPLOAD_ERR_MAX_SIZE', 1002);
define('A_HTTP_UPLOAD_ERR_FILE_EXISTS', 1003);

class A_Http_Upload {
var $file_param = 'file';		// form/http parameter name for file(s)
var $submit_param = 'upload';	// form/http parameter name for the submit button
var $path_param = 'path';		// form/http parameter name for path relative to dir
var $base_path = '/tmp';				// destination directory for uploaded file, or array of dirs will shows a select box, see formSelectPath()
var $file_mode = 0777;			// mode to create new directories and files
var $filename_regexp_pattern = array('/[^a-zA-Z0-9_\-\.]/');
var $filename_regexp_replace = array('_');

var $replace = true;			// if destination file exists, delete and the upload
var $min_size = 1;			// set minimum size of files, 0 to allow zero size files
var $max_size = 0;			// cap size of file with this value
var $allowed_types = array();

var $paths = array();
var $labels = array();		// text labels for form select in formSelectPath(), one matching text label for each path in dir array


function A_Http_Upload() {
	$this->setMaxFilesize(0);
}

function isSubmitted() {
	return($_REQUEST[$this->submit_param]);
}

function setFileParam($name) {
	$this->file_param = $name;
}

function setPathParam($path) {
	$this->path_param = $path;
}

function setSubmitParam($name) {
	$this->submit_param = $name;
}

function setBasePath($base_path) {
	if ($base_path) {
		if (substr($base_path, -1) != '/') {
			$base_path .= '/';
		}
		$this->base_path = $base_path;
	}
}

function setMinFilesize($min) {
	$this->min_size = $min;
}

function setMaxFilesize($max)
{
	$this->max_size = $max;
	$max = $this->getMaxFilesize();
	if ($max && (($max < $this->max_size) || ($this->max_size == 0)) ) {
		$this->max_size = $max;
	}
}

function setAllowedTypes($types=array()) {
	$this->allowed_types = $types;
}

function setReplace($replace) {
	$this->replace = $replace;
}

function addPath($id, $path, $label='') {
	$this->paths[$id] = $path;
	$this->labels[$id] = $label;
}

function fileCount() {
	$n = 0;
	if (isset($_FILES[$this->file_param]['name']) && ($_FILES[$this->file_param]['name'][0] != '')) {
		$n = count($_FILES[$this->file_param]['name']);
	}
	return $n;
}

function getMaxFilesize() {
	$max = ini_get('upload_max_filesize');
	if (is_string($max)) {
		$n = strlen($max) - 1;
		switch ($max[$n]) {
		case 'K':
			$m = 1024;
			$max[$n] = "\0";
			break;
		case 'M':
			$m = 1048576;
			$max[$n] = "\0";
			break;
		case 'G':
			$m = 1073741824;
			$max[$n] = "\0";
			break;
		default:
			$m = 1;
			break;
		}
		return($max * $m);
	} else {
		return 0;
	}
}

function getFileOption($option, $n=0, $param='') {
	if ($param == '') {
		$param = $this->file_param;
	}
	if (isset($_FILES[$param][$option])) {
		if (is_array($_FILES[$param][$option])) {
			return $_FILES[$param][$option][$n];
		} else {
			return $_FILES[$param][$option];
		}
	} else {
		return '';
	}
}

function setFileOption($value, $option, $n=0, $param='') {
	if ($param == '') {
		$param = $this->file_param;
	}
	if (is_array($_FILES[$param][$option])) {
		$_FILES[$param][$option][$n] = $value;
	} else {
		$_FILES[$param][$option] = $value;
	}
}

function getFileName($n=0, $param='') {
	return preg_replace($this->filename_regexp_pattern, $this->filename_regexp_replace, $this->getFileOption('name', $n, $param));
}

function getFileTmpName($n=0, $param='') {
	return $this->getFileOption('tmp_name', $n, $param);
}

function getFileType($n=0, $param='') {
	return $this->getFileOption('type', $n, $param);
}

function getFileSize($n=0, $param='') {
	return $this->getFileOption('size', $n, $param);
}

function getImageData($n=0, $param='') {
#	$imagedata = getimagesize($this->getFileTmpName($n, $param));
#	$width = $imagedata[0];
#	$height = $imagedata[1];
#	$type = $imagedata[2];	// 1 = GIF, 2 = JPG, 3 = PNG, 4 = SWF, 5 = PSD, 6 = BMP, 7 = TIFF(intel byte order), 8 = TIFF(motorola byte order), 9 = JPC, 10 = JP2, 11 = JPX, 12 = JB2, 13 = SWC, 14 = IFF, 15 = WBMP, 16 = XBM
#	$atrs = $imagedata[3];	// 'height="yyy" width="xxx"'
#	return $imagedata;
	return getimagesize($this->getFileTmpName($n, $param));
}

function getFileError($n=0, $param='') {
	return $this->getFileOption('error', $n, $param);
}

function getFileErrorMsg($n=0, $param='') {
	$error = $this->getFileOption('error', $n, $param);
	$errmsg = array(
		UPLOAD_ERR_INI_SIZE => 'The uploaded file exceeds the maximum size allowed. ', 
		UPLOAD_ERR_FORM_SIZE => 'The uploaded file exceeds the maximum size allowed in the form. ', 
		UPLOAD_ERR_PARTIAL => 'The uploaded file was only partially uploaded. ', 
		UPLOAD_ERR_NO_FILE => 'No file was uploaded. ', 
		UPLOAD_ERR_NO_TMP_DIR => 'Error with temporary folder. ', 
		A_HTTP_UPLOAD_NOT_UPLOAD_FILE => 'Error with uploaded file. ',
		A_HTTP_UPLOAD_ERR_MAX_SIZE => 'The uploaded file exceeds the maximum allowed size. ', 
		A_HTTP_UPLOAD_ERR_FILE_EXISTS => 'A file by that name already exists. ', 
		);
	return isset($errmsg[$error]) ? $errmsg[$error] : '';
}

function isUploadedFile($n=0, $param='') {
	if (is_uploaded_file($this->getFileTmpName($n, $param))) {
		return true;
	} else {
		$this->setFileOption(A_HTTP_UPLOAD_NOT_UPLOAD_FILE, 'error', $n, $param);
		return false;
	}
}

function isAllowedFilesize($n=0, $param='') {
	
	$size = $this->getFileOption('size', $n, $param);
	if ($this->min_size >= $size) {
		return false;
	}
	if ($this->max_size > 0) {
		if ($size <= $this->max_size) {
			return true;
		} else {
			$this->setFileOption(A_HTTP_UPLOAD_ERR_MAX_SIZE, 'error', $n, $param);
			return false;
		}
	}
	
	return true;
}

function isAllowedType($n=0, $param='') {
	if ($this->allowed_types) {
		return in_array($this->getFileOption('type', $n, $param), $this->allowed_types);
	}
	return true;
}

function isAllowed($n=0, $param='') {
	return $this->isUploadedFile($n, $param) && $this->isAllowedFilesize($n, $param) && $this->isAllowedType($n, $param);
}

function moveUploadedFile($n=0, $param='', $filename='') {
	if ($filename == '') {
		$filename = $this->getFileName($n, $param);
	}
	$path_param = $this->getPath();
	if ($path_param != '') {
		$path = $this->paths[$path_param];
	} else {
		$path = '';
	}
	$filename = $this->base_path . $path . $filename;
	if (file_exists($filename)) {
		if ($this->replace) {
			@unlink($filename);
		} else {
			$this->setFileOption(A_HTTP_UPLOAD_ERR_FILE_EXISTS, 'error', $n, $param);
			return false;
		}
	}
	return @move_uploaded_file($this->getFileTmpName($n, $param), $filename);
}

function createDir($path, $mode=0) {
	if ($path) {
		if (! $mode) {
			$mode = $this->file_mode;
		}
		if (file_exists($path)) {
			if (is_dir($path)) {
				return true;
			}
		} elseif (@mkdir($path, $mode, true)) {
			return true;
		}
	}
	return false;
}

function deleteTmpFile($n, $param='') {
	if ($filename = $this->getFileTmpName($n, $param)) {
		return @unlink($filename);
	}
}

function getPath() {
	if (isset($_REQUEST[$this->path_param])) {
		return preg_replace('/[^a-zA-Z0-9_\-\.]/', '', $_REQUEST[$this->path_param]);
	} else {
		return '';
	}
}

}

class A_Http_UploadForm {
var $upload;
var $hidden = array();

function A_Http_UploadForm(&$upload) {
	$this->upload =& $upload;
}

function addHidden($name, $value) {
	$this->hidden[$name] = $value;
}

function formOpen($action='', $method='') {
	if ($method == '') {
		$method = 'post';
	}
	$str = "<form enctype=\"multipart/form-data\" action=\"$action\" method=\"$method\">\n";
	if (is_array($this->hidden) ) {
		foreach ($this->hidden as $name => $value) {
			$str .= "<input type=\"hidden\" name=\"$name\" value=\"$value\">\n";
		}
	}
	return $str;
}

function formSelectPath() {
	$str = '';
	if ($this->upload->paths && is_array($this->upload->paths) ) {
		$n = 0;
		$str = "<select name=\"{$this->upload->path_param}\">";
		foreach ($this->upload->labels as $id => $label) {
			$str .= "<option value=\"$id\">$label</option>";
		}
		$str .= '</select>';
	}

	return $str;
}

function formInput($size=0) {
	if ($size == 0) {
		$size = 20;
	}
	return "<input type=\"file\" name=\"{$this->upload->file_param}[]\" size=\"$size\">";
}

function formSubmit($value='') {
	if ($value == '') {
		$value = 'Upload';
	}
	return "<input type=\"submit\" name=\"{$this->upload->submit_param}\" value=\"$value\">";
}

function formClose() {
	return "</form>";
}

function form($action, $value='', $method='', $size=0) {
	$str = $this->formOpen($action, $method) . "\n";
	$str .= $this->formSelectPath() . "\n";
	$str .= $this->formInput($size) . "\n";
	$str .= $this->formSubmit($value) . "\n";
	$str .= $this->formClose() . "\n";

	return $str;
}


}
Download.php

Code: Select all

<?php

class A_Http_Download
{
var $mime_type = 'text/none';
var $encoding = 'none';
var $content_length = 0;
var $source_file = '';
var $target_file = '';
var $errmsg = '';

/*
 * Set the mime type of the file to be downloaded to be specified in the header
 */
function setMimeType ($type)
{
	if ($type) {
		$this->mime_type = $type;
	}
}

/*
 * Set the Transfer Encoding of the file to be downloaded to be specified in the header
 */
function setEncoding ($encoding)
{
	if ($encoding) {
		$this->encoding = $encoding;
	}
}

/*
 * Set the path to a file on the server. 
 * The contents will be dumped following outputing the header and content length will be set
 */
function setSourceFilePath ($path)
{
	if ($path) {
		$this->source_file = $path;
	}
}

/*
 * Set the filename to be used on the client. 
 * Use if no source file or if you want a different name than the source file. 
 */
function setTargetFileName ($name)
{
	if ($name) {
		$this->target_file = $name;
	}
}

/*
 * Optional - if no source filename specified then use this to set length. 
 */
function setContentLength ($name)
{
	if ($name) {
		$this->target_file = $name;
	}
}

function _header ($name, $value)
{
	if (is_array($value)) {
		foreach ($value as $val) {
			header("$name: $val");
		}
	} else {
		header("$name: $value");
	}
}

/*
 * Send headers, followed by the contents of the source file if specified.
 * Output will be included in the file
 */
function doDownload ()
{
	if (! headers_sent()) {
		if ($this->mime_type) {
// set the mime type of the data to be downloaded
			$this->_header('Content-type', $this->mime_type);

			if ($this->encoding) {
				$this->_header('Content-Transfer-Encoding', $this->encoding);
			}
/*
// maybe implement some support for these
header('Cache-Control: no-store, no-cache, must-revalidate'); // HTTP/1.1
header('Cache-Control: pre-check=0, post-check=0, max-age=0'); // HTTP/1.1
header('Content-Transfer-Encoding: none');
header('Content-Type: application/octetstream');    //    IE and Opera
header('Content-Type: application/octet-stream');    //    All other browsers
header('Content-Transfer-Encoding: Binary');
header('Content-Disposition: attachment; filename="' . $name . '"');
header("Pragma: public");    //    Stop old IEs saving the download script by mistake
*/
// if target file name is supplied add it to header
			if ($this->target_file) {
				$this->_header('Content-Disposition', 'attachment; filename=' . $this->target_file);
			}
	
// if source file path is specified then dump the file following the header
			if ($this->source_file) {
				header('Content-Length: ' . @filesize($this->source_file));
				if (@readfile($this->source_file) === false) {
					$this->errmsg = 'Error reading file ' . $this->source_file . '. ';
				}
			} elseif ($this->content_length > 0){
				header('Content-Length: ' . $this->content_length);
			}
		} else {
			$this->errmsg = 'No MIME type. ';
		}
	} else {
		$this->errmsg = 'Headers sent. ';
	}
	return $this->errmsg;
}

function isError ()
{
	return $this->errmsg;
}


}
This thread is mainly just to have a centralized location for Arborint and I (as well as anybody else interested in helping) to collaboratively build these classes.
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post by Christopher »

That is probably too much code for most people to try to get thorugh.

I am trying to think of how to break things up. There are some random thoughts:

- The Upload class deals with the $_FILES superglobal and the file management -- MIME, moving/deleting, etc.
- The Download class is more dependent on MIME, but it is focused in formatting values for the header() function.

- The Upload class is definitely focused on files
- The Download class really is only interested in a buffer and doesn't care where the data came from -- but it could be a file.

- The Upload class is deals with the result of a POST with one or more file inputs, so some sort of form support is handy (there is a class for that above) mainly for aligning parameter names and potentiall dealing with checking file extensions and setting MAX_FILE_SIZE. It would be nice to add some Javascript filename checking and MAX_FILE_SIZE.
- The Download class a one shot thing that only generates a response and does not care about the request.
(#10850)
User avatar
Luke
The Ninja Space Mod
Posts: 6424
Joined: Fri Aug 05, 2005 1:53 pm
Location: Paradise, CA

Post by Luke »

I think that the upload form should be removed from this area and moved to helpers. I have started building an html helpers class that will be able to blast out an upload form very quickly

I am almost finished with the model class I am building. It will eventually be able to describe the relationship between many types of models, but for now, it just passes data to the controller.

I have also made a few changes to the Response class and changed a few other things here and there. The beauty of this is that we can both take from eachother what we like and leave the rest! Once again, thanks a lot for letting me borrow so much of your code arborint... saved me OODLES of time! :D :D 8O
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post by Christopher »

The Ninja Space Goat wrote:I think that the upload form should be removed from this area and moved to helpers. I have started building an html helpers class that will be able to blast out an upload form very quickly
I think if there is a Upload directory then maybe the Upload_Form class should go there to keep everything together. I would like it so that if someone only wanted the Upload classes or the Pager classes they could use those independent of the rest if possible.

Any ideas about reorganizing the classes above? It is not clear to me.
(#10850)
User avatar
TheMoose
Forum Contributor
Posts: 351
Joined: Tue May 23, 2006 10:42 am

Post by TheMoose »

If PHP5, then IMO then Upload class should be an interface, and then have the File and Folder classes extend that base upload interface to allow for their specific differences. If I understand it properly, then you could do the same with the Download class, although theoretically the Folder download class would be just a collection of File Download objects, but the method of fetching and handling those would be more to the specific Folder class than keeping it in the File class.
Post Reply