<?php
/**
 * Elgg filestore.
 * This file contains classes, interfaces and functions for saving and retrieving data to various file
 * stores.
 *
 * @package Elgg
 * @subpackage API
 * @author Curverider Ltd
 * @link http://elgg.org/
 */

include_once("objects.php");

/**
 * @class ElggFilestore
 * This class defines the interface for all elgg data repositories.
 * @author Curverider Ltd
 */
abstract class ElggFilestore {
	/**
	 * Attempt to open the file $file for storage or writing.
	 *
	 * @param ElggFile $file
	 * @param string $mode "read", "write", "append"
	 * @return mixed A handle to the opened file or false on error.
	 */
	abstract public function open(ElggFile $file, $mode);

	/**
	 * Write data to a given file handle.
	 *
	 * @param mixed $f The file handle - exactly what this is depends on the file system
	 * @param string $data The binary string of data to write
	 * @return int Number of bytes written.
	 */
	abstract public function write($f, $data);

	/**
	 * Read data from a filestore.
	 *
	 * @param mixed $f The file handle
	 * @param int $length Length in bytes to read.
	 * @param int $offset The optional offset.
	 * @return mixed String of data or false on error.
	 */
	abstract public function read($f, $length, $offset = 0);

	/**
	 * Seek a given position within a file handle.
	 *
	 * @param mixed $f The file handle.
	 * @param int $position The position.
	 */
	abstract public function seek($f, $position);

	/**
	 * Return a whether the end of a file has been reached.
	 *
	 * @param mixed $f The file handle.
	 * @return boolean
	 */
	abstract public function eof($f);

	/**
	 * Return the current position in an open file.
	 *
	 * @param mixed $f The file handle.
	 * @return int
	 */
	abstract public function tell($f);

	/**
	 * Close a given file handle.
	 *
	 * @param mixed $f
	 */
	abstract public function close($f);

	/**
	 * Delete the file associated with a given file handle.
	 *
	 * @param ElggFile $file
	 */
	abstract public function delete(ElggFile $file);

	/**
	 * Return the size in bytes for a given file.
	 *
	 * @param ElggFile $file
	 */
	abstract public function getFileSize(ElggFile $file);

	/**
	 * Return the filename of a given file as stored on the filestore.
	 *
	 * @param ElggFile $file
	 */
	abstract public function getFilenameOnFilestore(ElggFile $file);

	/**
	 * Get the filestore's creation parameters as an associative array.
	 * Used for serialisation and for storing the creation details along side a file object.
	 *
	 * @return array
	 */
	abstract public function getParameters();

	/**
	 * Set the parameters from the associative array produced by $this->getParameters().
	 */
	abstract public function setParameters(array $parameters);

	/**
	 * Get the contents of the whole file.
	 *
	 * @param mixed $file The file handle.
	 * @return mixed The file contents.
	 */
	abstract public function grabFile(ElggFile $file);

	/**
	 * Return whether a file physically exists or not.
	 *
	 * @param ElggFile $file
	 */
	abstract public function exists(ElggFile $file);
}

/**
 * @class ElggDiskFilestore
 * This class uses disk storage to save data.
 * @author Curverider Ltd
 */
class ElggDiskFilestore extends ElggFilestore {
	/**
	 * Directory root.
	 */
	private $dir_root;

	/**
	 * Default depth of file directory matrix
	 */
	private $matrix_depth = 5;

	/**
	 * Construct a disk filestore using the given directory root.
	 *
	 * @param string $directory_root Root directory, must end in "/"
	 */
	public function __construct($directory_root = "") {
		global $CONFIG;

		if ($directory_root) {
			$this->dir_root = $directory_root;
		} else {
			$this->dir_root = $CONFIG->dataroot;
		}
	}

	public function open(ElggFile $file, $mode) {
		$fullname = $this->getFilenameOnFilestore($file);

		// Split into path and name
		$ls = strrpos($fullname,"/");
		if ($ls===false) {
			$ls = 0;
		}

		$path = substr($fullname, 0, $ls);
		$name = substr($fullname, $ls);

		// Try and create the directory
		try {
			$this->make_directory_root($path);
		} catch (Exception $e) {

		}

		if (($mode!='write') && (!file_exists($fullname))) {
			return false;
		}

		switch ($mode) {
			case "read" :
				$mode = "rb";
				break;
			case "write" :
				$mode = "w+b";
				break;
			case "append" :
				$mode = "a+b";
				break;
			default:
				throw new InvalidParameterException(sprintf(elgg_echo('InvalidParameterException:UnrecognisedFileMode'), $mode));
		}

		return fopen($fullname, $mode);

	}

	public function write($f, $data) {
		return fwrite($f, $data);
	}

	public function read($f, $length, $offset = 0) {
		if ($offset) {
			$this->seek($f, $offset);
		}

		return fread($f, $length);
	}

	public function close($f) {
		return fclose($f);
	}

	public function delete(ElggFile $file) {
		$filename = $this->getFilenameOnFilestore($file);
		if (file_exists($filename)) {
			return unlink($filename);
		} else {
			return true;
		}
	}

	public function seek($f, $position) {
		return fseek($f, $position);
	}

	public function tell($f) {
		return ftell($f);
	}

	public function eof($f) {
		return feof($f);
	}

	public function getFileSize(ElggFile $file) {
		return filesize($this->getFilenameOnFilestore($file));
	}

	public function getFilenameOnFilestore(ElggFile $file) {
		$owner = $file->getOwnerEntity();
		if (!$owner) {
			$owner = get_loggedin_user();
		}

		if ((!$owner) || (!$owner->username)) {
			throw new InvalidParameterException(sprintf(elgg_echo('InvalidParameterException:MissingOwner'), $file->getFilename(), $file->guid));
		}

		return $this->dir_root . $this->make_file_matrix($owner->guid) . $file->getFilename();
	}

	public function grabFile(ElggFile $file) {
		return file_get_contents($file->getFilenameOnFilestore());
	}

	public function exists(ElggFile $file) {
		return file_exists($this->getFilenameOnFilestore($file));
	}

	public function getSize($prefix,$container_guid) {
		if ($container_guid) {
			return get_dir_size($this->dir_root.$this->make_file_matrix($container_guid).$prefix);
		} else {
			return false;
		}
	}

	/**
	 * Make the directory root.
	 *
	 * @param string $dirroot
	 */
	protected function make_directory_root($dirroot) {
		if (!file_exists($dirroot)) {
			if (!@mkdir($dirroot, 0700, true)) {
				throw new IOException(sprintf(elgg_echo('IOException:CouldNotMake'), $dirroot));
			}
		}

		return true;
	}

	/**
	 * Multibyte string tokeniser.
	 *
	 * Splits a string into an array. Will fail safely if mbstring is not installed (although this may still
	 * not handle .
	 *
	 * @param string $string String
	 * @param string $charset The charset, defaults to UTF8
	 * @return array
	 */
	private function mb_str_split($string, $charset = 'UTF8') {
		if (is_callable('mb_substr')) {
			$length = mb_strlen($string);
			$array = array();

			while ($length) {
				$array[] = mb_substr($string, 0, 1, $charset);
				$string = mb_substr($string, 1, $length, $charset);

				$length = mb_strlen($string);
			}

			return $array;
		} else {
			return str_split($string);
		}

		return false;
	}

	/**
	 * Construct the filename matrix.
	 *
	 * @param int | string $identifier
	 * @return str
	 */
	protected function make_file_matrix($identifier) {
		if (is_numeric($identifier)) {
			return $this->user_file_matrix($identifier);
		}

		return $this->deprecated_file_matrix($identifier);
	}

	/**
	 * Construct the filename matrix with user info
	 *
	 * This method will generate a matrix using the entity's creation time and
	 * unique guid. This is intended only to determine a user's data directory.
	 *
	 * @param int $guid
	 * @return str
	 */
	protected function user_file_matrix($guid) {
		// lookup the entity
		$user = get_entity($guid);
		if ($user->type != 'user')
		{
			// only to be used for user directories
			return FALSE;
		}

		if (!$user->time_created) {
			// fall back to deprecated method
			return $this->deprecated_file_matrix($user->username);
		}

		$time_created = date('Y/m/d', $user->time_created);
		return "$time_created/$user->guid/";
	}

	/**
	 * Construct the filename matrix using a string
	 *
	 * Particularly, this is used with a username to generate the file storage
	 * location.
	 *
	 * @deprecated for user directories: use user_file_matrix() instead.
	 *
	 * @param str $filename
	 * @return str
	 */
	protected function deprecated_file_matrix($filename) {
		// throw a warning for using deprecated method
		$error  = 'Deprecated use of ElggDiskFilestore::make_file_matrix. ';
		$error .= 'Username passed instead of guid.';
		elgg_log($error, WARNING);

		$user = new ElggUser($filename);
		return $this->user_file_matrix($user->guid);
	}

	public function getParameters() {
		return array("dir_root" => $this->dir_root);
	}

	public function setParameters(array $parameters) {
		if (isset($parameters['dir_root'])) {
			$this->dir_root = $parameters['dir_root'];
			return true;
		}

		return false;
	}
}

/**
 * @class ElggFile
 * This class represents a physical file.
 *
 * Usage:
 *		Create a new ElggFile object and specify a filename, and optionally a FileStore (if one isn't specified
 *		then the default is assumed.
 *
 * 		Open the file using the appropriate mode, and you will be able to read and write to the file.
 *
 * 		Optionally, you can also call the file's save() method, this will turn the file into an entity in the
 * 		system and permit you to do things like attach tags to the file etc. This is not done automatically since
 * 		there are many occasions where you may want access to file data on datastores using the ElggFile interface
 * 		but do not want to create an Entity reference to it in the system (temporary files for example).
 *
 * @author Curverider Ltd
 */
class ElggFile extends ElggObject {
	/** Filestore */
	private $filestore;

	/** File handle used to identify this file in a filestore. Created by open. */
	private $handle;

	protected function initialise_attributes() {
		parent::initialise_attributes();

		$this->attributes['subtype'] = "file";
	}

	public function __construct($guid = null) {
		parent::__construct($guid);

		// Set default filestore
		$this->filestore = $this->getFilestore();
	}

	/**
	 * Set the filename of this file.
	 *
	 * @param string $name The filename.
	 */
	public function setFilename($name) {
		$this->filename = $name;
	}

	/**
	 * Return the filename.
	 */
	public function getFilename() {
		return $this->filename;
	}

	/**
	 * Return the filename of this file as it is/will be stored on the filestore, which may be different
	 * to the filename.
	 */
	public function getFilenameOnFilestore() {
		return $this->filestore->getFilenameOnFilestore($this);
	}

	/*
	 * Return the size of the filestore associated with this file
	 *
	 */
	public function getFilestoreSize($prefix='',$container_guid=0) {
		if (!$container_guid) {
			$container_guid = $this->container_guid;
		}
		$fs = $this->getFilestore();
		return $fs->getSize($prefix,$container_guid);
	}

	/**
	 * Get the mime type of the file.
	 */
	public function getMimeType() {
		if ($this->mimetype) {
			return $this->mimetype;
		}

		// TODO : Guess mimetype if not here
	}

	/**
	 * Set the mime type of the file.
	 *
	 * @param $mimetype The mimetype
	 */
	public function setMimeType($mimetype) {
		return $this->mimetype = $mimetype;
	}

	/**
	 * Set the optional file description.
	 *
	 * @param string $description The description.
	 */
	public function setDescription($description) {
		$this->description = $description;
	}

	/**
	 * Open the file with the given mode
	 *
	 * @param string $mode Either read/write/append
	 */
	public function open($mode) {
		if (!$this->getFilename()) {
			throw new IOException(elgg_echo('IOException:MissingFileName'));
		}

		// See if file has already been saved
		// seek on datastore, parameters and name?

		// Sanity check
		if (
			($mode!="read") &&
			($mode!="write") &&
			($mode!="append")
		) {
			throw new InvalidParameterException(sprintf(elgg_echo('InvalidParameterException:UnrecognisedFileMode'), $mode));
		}

		// Get the filestore
		$fs = $this->getFilestore();

		// Ensure that we save the file details to object store
		//$this->save();

		// Open the file handle
		$this->handle = $fs->open($this, $mode);

		return $this->handle;
	}

	/**
	 * Write some data.
	 *
	 * @param string $data The data
	 */
	public function write($data) {
		$fs = $this->getFilestore();

		return $fs->write($this->handle, $data);
	}

	/**
	 * Read some data.
	 *
	 * @param int $length Amount to read.
	 * @param int $offset The offset to start from.
	 */
	public function read($length, $offset = 0) {
		$fs = $this->getFilestore();

		return $fs->read($this->handle, $length, $offset);
	}

	/**
	 * Gets the full contents of this file.
	 *
	 * @return mixed The file contents.
	 */
	public function grabFile() {
		$fs = $this->getFilestore();
		return $fs->grabFile($this);
	}

	/**
	 * Close the file and commit changes
	 */
	public function close() {
		$fs = $this->getFilestore();

		if ($fs->close($this->handle)) {
			$this->handle = NULL;

			return true;
		}

		return false;
	}

	/**
	 * Delete this file.
	 */
	public function delete() {
		$fs = $this->getFilestore();
		if ($fs->delete($this)) {
			return parent::delete();
		}
	}

	/**
	 * Seek a position in the file.
	 *
	 * @param int $position
	 */
	public function seek($position) {
		$fs = $this->getFilestore();

		return $fs->seek($this->handle, $position);
	}

	/**
	 * Return the current position of the file.
	 *
	 * @return int The file position
	 */
	public function tell() {
		$fs = $this->getFilestore();

		return $fs->tell($this->handle);
	}

	/**
	 * Return the size of the file in bytes.
	 */
	public function size() {
		return $this->filestore->getFileSize($this);
	}

	/**
	 * Return a boolean value whether the file handle is at the end of the file
	 */
	public function eof() {
		$fs = $this->getFilestore();

		return $fs->eof($this->handle);
	}

	public function exists() {
		$fs = $this->getFilestore();

		return $fs->exists($this);
	}

	/**
	 * Set a filestore.
	 *
	 * @param ElggFilestore $filestore The file store.
	 */
	public function setFilestore(ElggFilestore $filestore) {
		$this->filestore = $filestore;
	}

	/**
	 * Return a filestore suitable for saving this file.
	 * This filestore is either a pre-registered filestore, a filestore loaded from metatags saved
	 * along side this file, or the system default.
	 */
	protected function getFilestore() {
		// Short circuit if already set.
		if ($this->filestore) {
			return $this->filestore;
		}

		// If filestore meta set then retrieve filestore TODO: Better way of doing this?
		$metas = get_metadata_for_entity($this->guid);
		$parameters = array();
		if (is_array($metas)) {
			foreach ($metas as $meta) {
				if (strpos($meta->name, "filestore::")!==false) {
					// Filestore parameter tag
					$comp = explode("::", $meta->name);
					$name = $comp[1];

					$parameters[$name] = $meta->value;
				}
			}
		}

		// If parameters loaded then create new filestore
		if (count($parameters)!=0) {
			// Create new filestore object
			if ((!isset($parameters['filestore'])) || (!class_exists($parameters['filestore']))) {
				throw new ClassNotFoundException(elgg_echo('ClassNotFoundException:NotFoundNotSavedWithFile'));
			}

			$this->filestore = new $parameters['filestore']();

			// Set parameters
			$this->filestore->setParameters($parameters);
		}


		// if still nothing then set filestore to default
		if (!$this->filestore) {
			$this->filestore = get_default_filestore();
		}

		return $this->filestore;
	}

	public function save() {
		if (!parent::save()) {
			return false;
		}

		// Save datastore metadata
		$params = $this->filestore->getParameters();
		foreach ($params as $k => $v) {
			$this->setMetaData("filestore::$k", $v);
		}

		// Now make a note of the filestore class
		$this->setMetaData("filestore::filestore", get_class($this->filestore));

		return true;
	}
}

/**
 * Get the size of the specified directory.
 *
 * @param string $dir The full path of the directory
 * @return int The size of the directory.
 */
function get_dir_size($dir, $totalsize = 0){
	$handle = @opendir($dir);
	while ($file = @readdir ($handle)){
		if (eregi("^\.{1,2}$", $file)) {
			continue;
		}
		if(is_dir($dir . $file)) {
			$totalsize = get_dir_size($dir . $file . "/", $totalsize);
		} else{
			$totalsize += filesize($dir . $file);
		}
	}
	@closedir($handle);

	return($totalsize);
}

/**
 * Get the contents of an uploaded file.
 * (Returns false if there was an issue.)
 *
 * @param string $input_name The name of the file input field on the submission form
 * @return mixed|false The contents of the file, or false on failure.
 */
function get_uploaded_file($input_name) {
	// If the file exists ...
	if (isset($_FILES[$input_name]) && $_FILES[$input_name]['error'] == 0) {
		return file_get_contents($_FILES[$input_name]['tmp_name']);
	}
	return false;
}

/**
 * Gets the jpeg contents of the resized version of an uploaded image
 * (Returns false if the uploaded file was not an image)
 *
 * @param string $input_name The name of the file input field on the submission form
 * @param int $maxwidth The maximum width of the resized image
 * @param int $maxheight The maximum height of the resized image
 * @param true|false $square If set to true, will take the smallest of maxwidth and maxheight and use it to set the dimensions on all size; the image will be cropped.
 * @return false|mixed The contents of the resized image, or false on failure
 */
function get_resized_image_from_uploaded_file($input_name, $maxwidth, $maxheight, $square = false) {
	// If our file exists ...
	if (isset($_FILES[$input_name]) && $_FILES[$input_name]['error'] == 0) {
		return get_resized_image_from_existing_file($_FILES[$input_name]['tmp_name'], $maxwidth, $maxheight, $square);
	}

	return false;
}

/**
 * Gets the jpeg contents of the resized version of an already uploaded image
 * (Returns false if the file was not an image)
 *
 * @param string $input_name The name of the file on the disk
 * @param int $maxwidth The desired width of the resized image
 * @param int $maxheight The desired height of the resized image
 * @param true|false $square If set to true, takes the smallest of maxwidth and
 * 			maxheight and use it to set the dimensions on the new image. If no
 * 			crop parameters are set, the largest square that fits in the image
 * 			centered will be used for the resize. If square, the crop must be a
 * 			square region.
 * @param int $x1 x coordinate for top, left corner
 * @param int $y1 y coordinate for top, left corner
 * @param int $x2 x coordinate for bottom, right corner
 * @param int $y2 y coordinate for bottom, right corner
 * @param bool $upscale Resize images smaller than $maxwidth x $maxheight?
 * @return false|mixed The contents of the resized image, or false on failure
 */
function get_resized_image_from_existing_file($input_name, $maxwidth, $maxheight, $square = FALSE, $x1 = 0, $y1 = 0, $x2 = 0, $y2 = 0, $upscale = FALSE) {
	// Get the size information from the image
	$imgsizearray = getimagesize($input_name);
	if ($imgsizearray == FALSE) {
		return FALSE;
	}

	// Get width and height
	$width = $imgsizearray[0];
	$height = $imgsizearray[1];

	// make sure we can read the image
	$accepted_formats = array(
		'image/jpeg' => 'jpeg',
		'image/pjpeg' => 'jpeg',
		'image/png' => 'png',
		'image/x-png' => 'png',
		'image/gif' => 'gif'
	);

	// make sure the function is available
	$load_function = "imagecreatefrom" . $accepted_formats[$imgsizearray['mime']];
	if (!is_callable($load_function)) {
		return FALSE;
	}

	// crop image first?
	$crop = TRUE;
	if ($x1 == 0 && $y1 == 0 && $x2 == 0 && $y2 == 0) {
		$crop = FALSE;
	}

	// how large a section of the image has been selected
	if ($crop) {
		$region_width = $x2 - $x1;
		$region_height = $y2 - $y1;
	} else {
		// everything selected if no crop parameters
		$region_width = $width;
		$region_height = $height;
	}

	// determine cropping offsets
	if ($square) {
		// asking for a square image back

		// detect case where someone is passing crop parameters that are not for a square
		if ($crop == TRUE && $region_width != $region_height) {
			return FALSE;
		}

		// size of the new square image
		$new_width = $new_height = min($maxwidth, $maxheight);

		// find largest square that fits within the selected region
		$region_width = $region_height = min($region_width, $region_height);

		// set offsets for crop
		if ($crop) {
			$widthoffset = $x1;
			$heightoffset = $y1;
			$width = $x2 - $x1;
			$height = $width;
		} else {
			// place square region in the center
			$widthoffset = floor(($width - $region_width) / 2);
			$heightoffset = floor(($height - $region_height) / 2);
		}
	} else {
		// non-square new image

		$new_width = $maxwidth;
		$new_height = $maxwidth;

		// maintain aspect ratio of original image/crop
		if (($region_height / (float)$new_height) > ($region_width / (float)$new_width)) {
			$new_width = floor($new_height * $region_width / (float)$region_height);
		} else {
			$new_height = floor($new_width * $region_height / (float)$region_width);
		}

		// by default, use entire image
		$widthoffset = 0;
		$heightoffset = 0;

		if ($crop) {
			$widthoffset = $x1;
			$heightoffset = $y1;
		}
	}

	// check for upscaling
	// @todo This ignores squares, coordinates, and cropping. It's probably not the best idea.
	// Size checking should be done in action code, but for backward compatibility
	// this duplicates the previous behavior.
	if (!$upscale && ($height < $new_height || $width < $new_width)) {
		// zero out offsets
		$widthoffset = $heightoffset = 0;

		// determine if we can scale it down at all
		// (ie, if only one dimension is too small)
		// if not, just use original size.
		if ($height < $new_height && $width < $new_width) {
			$ratio = 1;
		} elseif ($height < $new_height) {
			$ratio = $new_width / $width;
		} elseif ($width < $new_width) {
			$ratio = $new_height / $height;
		}
		$region_height = $height;
		$region_width = $width;
		$new_height = floor($height * $ratio);
		$new_width = floor($width * $ratio);
	}

	// load original image
	$orig_image = $load_function($input_name);
	if (!$orig_image) {
		return FALSE;
	}

	// allocate the new image
	$newimage = imagecreatetruecolor($new_width, $new_height);
	if (!$newimage) {
		return FALSE;
	}

	// create the new image
	$rtn_code = imagecopyresampled(	$newimage,
									$orig_image,
									0,
									0,
									$widthoffset,
									$heightoffset,
									$new_width,
									$new_height,
									$region_width,
									$region_height );
	if (!$rtn_code) {
		return FALSE;
	}

	// grab contents for return
	ob_start();
	imagejpeg($newimage, null, 90);
	$jpeg = ob_get_clean();

	imagedestroy($newimage);
	imagedestroy($orig_image);

	return $jpeg;
}


// putting these here for now
function file_delete($guid) {
	if ($file = get_entity($guid)) {
		if ($file->canEdit()) {
			$container = get_entity($file->container_guid);

			$thumbnail = $file->thumbnail;
			$smallthumb = $file->smallthumb;
			$largethumb = $file->largethumb;
			if ($thumbnail) {
				$delfile = new ElggFile();
				$delfile->owner_guid = $file->owner_guid;
				$delfile->setFilename($thumbnail);
				$delfile->delete();
			}
			if ($smallthumb) {
				$delfile = new ElggFile();
				$delfile->owner_guid = $file->owner_guid;
				$delfile->setFilename($smallthumb);
				$delfile->delete();
			}
			if ($largethumb) {
				$delfile = new ElggFile();
				$delfile->owner_guid = $file->owner_guid;
				$delfile->setFilename($largethumb);
				$delfile->delete();
			}

			return $file->delete();
		}
	}

	return false;
}

/**
 * Returns an overall file type from the mimetype
 *
 * @param string $mimetype The MIME type
 * @return string The overall type
 */
function file_get_general_file_type($mimetype) {
	switch($mimetype) {

		case "application/msword":
			return "document";
			break;
		case "application/pdf":
			return "document";
			break;
	}

	if (substr_count($mimetype,'text/')) {
		return "document";
	}

	if (substr_count($mimetype,'audio/')) {
		return "audio";
	}

	if (substr_count($mimetype,'image/')) {
		return "image";
	}

	if (substr_count($mimetype,'video/')) {
		return "video";
	}

	if (substr_count($mimetype,'opendocument')) {
		return "document";
	}

	return "general";
}

function file_handle_upload($prefix,$subtype,$plugin) {
	$desc = get_input("description");
	$tags = get_input("tags");
	$tags = explode(",", $tags);
	$folder = get_input("folder_text");
	if (!$folder) {
		$folder = get_input("folder_select");
	}
	$access_id = (int) get_input("access_id");
	$container_guid = (int) get_input('container_guid', 0);
	if (!$container_guid) {
		$container_guid == get_loggedin_userid();
	}

	// Extract file from, save to default filestore (for now)

	// see if a plugin has set a quota for this user
	$file_quota = trigger_plugin_hook("$plugin:quotacheck",'user',array('container_guid'=>$container_guid));
	if (!$file_quota) {
		// no, see if there is a generic quota set
		$file_quota = get_plugin_setting('quota', $plugin);
	}
	if ($file_quota) {
		// convert to megabytes
		$file_quota = $file_quota*1000*1024;
	}

	// handle uploaded files
	$number_of_files = get_input('number_of_files',0);
	$quota_exceeded = false;
	$bad_mime_type = false;

	for ($i = 0; $i < $number_of_files; $i++) {
		$title = get_input("title_".$i);
		$uploaded = $_FILES["upload_".$i];
		if (!$uploaded || !$uploaded['name']) {
			// no such file, so skip it
			continue;
		}
		if ($plugin == "photo") {
			// do a mime type test
			if (in_array($uploaded['type'],array('image/jpeg','image/gif','image/png','image/jpg','image/jpe','image/pjpeg','image/x-png'))) {
				$file = new PhotoPluginFile();
			} else {
				$bad_mime_type = true;
				break;
			}
		} else {
			$file = new FilePluginFile();
		}
		$dir_size = $file->getFilestoreSize($prefix,$container_guid);
		$filestorename = strtolower(time().$uploaded['name']);
		$file->setFilename($prefix.$filestorename);
		$file->setMimeType($uploaded['type']);

		$file->originalfilename = $uploaded['name'];

		$file->subtype = $subtype;

		$file->access_id = $access_id;

		$uf = get_uploaded_file('upload_'.$i);

		if ($file_quota) {
			$file_size = strlen($uf);
			if (($dir_size + $file_size) > $file_quota) {
				$quota_exceeded = true;
			}
		}

		if (!$quota_exceeded) {
			// all clear, so try to save the data

			$file->open("write");
			$file->write($uf);
			$file->close();

			$file->title = $title;
			$file->description = $desc;
			if ($container_guid) {
				$file->container_guid = $container_guid;
			}

			// Save tags
			$file->tags = $tags;

			$file->simpletype = file_get_general_file_type($uploaded['type']);
			$file->folder = $folder;

			$result = $file->save();

			if ($result) {

				// Generate thumbnail (if image)
				if (substr_count($file->getMimeType(),'image/')) {
					$thumbnail = get_resized_image_from_existing_file($file->getFilenameOnFilestore(),60,60, true);
					$thumbsmall = get_resized_image_from_existing_file($file->getFilenameOnFilestore(),153,153, true);
					$thumblarge = get_resized_image_from_existing_file($file->getFilenameOnFilestore(),600,600, false);
					if ($thumbnail) {
						$thumb = new ElggFile();
						$thumb->setMimeType($uploaded['type']);

						$thumb->setFilename($prefix."thumb".$filestorename);
						$thumb->open("write");
						$thumb->write($thumbnail);
						$thumb->close();

						$file->thumbnail = $prefix."thumb".$filestorename;

						$thumb->setFilename($prefix."smallthumb".$filestorename);
						$thumb->open("write");
						$thumb->write($thumbsmall);
						$thumb->close();
						$file->smallthumb = $prefix."smallthumb".$filestorename;

						$thumb->setFilename($prefix."largethumb".$filestorename);
						$thumb->open("write");
						$thumb->write($thumblarge);
						$thumb->close();
						$file->largethumb = $prefix."largethumb".$filestorename;
					}
				}

				// add to this user's file folders
				file_add_to_folders($folder,$container_guid,$plugin);

				add_to_river("river/object/$plugin/create",'create',$_SESSION['user']->guid,$file->guid);
			} else {
				break;
			}
		} else {
			break;
		}
	}

	if ($quota_exceeded) {
		echo elgg_echo("$plugin:quotaexceeded");
	} else if ($bad_mime_type)	{
		echo elgg_echo("$plugin:badmimetype");
	} else if ($result) {
		if ($number_of_files > 1) {
			echo elgg_echo("$plugin:saved_multi");
		} else {
			echo elgg_echo("$plugin:saved");
		}
	} else {
		if ($number_of_files > 1) {
			echo elgg_echo("$plugin:uploadfailed_multi");
		} else {
			echo elgg_echo("$plugin:uploadfailed");
		}
	}
}

function file_add_to_folders($folder,$container_guid,$plugin) {
	if ($container_guid && ($container = get_entity($container_guid))) {
		$folder_field_name = 'elgg_'.$plugin.'_folders';
		$folders = $container->$folder_field_name;
		if ($folders) {
			if (is_array($folders)) {
				if (!in_array($folder,$folders)) {
					$folders[] = $folder;
					$container->$folder_field_name = $folders;
				}
			} else {
				if ($folders != $folder) {
					$container->$folder_field_name = array($folders,$folder);
				}
			}
		} else {
			$container->$folder_field_name = $folder;
		}
	}
}

function file_handle_save($forward,$plugin) {
	// Get variables
	$title = get_input("title");
	$desc = get_input("description");
	$tags = get_input("tags");
	$folder = get_input("folder_text");
	if (!$folder) {
		$folder = get_input("folder_select");
	}
	$access_id = (int) get_input("access_id");

	$guid = (int) get_input('file_guid');

	if (!$file = get_entity($guid)) {
		register_error(elgg_echo("$plugin:uploadfailed"));
		forward($forward . $_SESSION['user']->username);
		exit;
	}

	$result = false;

	$container_guid = $file->container_guid;
	$container = get_entity($container_guid);

	if ($file->canEdit()) {
		$file->access_id = $access_id;
		$file->title = $title;
		$file->description = $desc;
		$file->folder = $folder;
		// add to this user's file folders
		file_add_to_folders($folder,$container_guid,$plugin);

		// Save tags
		$tags = explode(",", $tags);
		$file->tags = $tags;

		$result = $file->save();
	}

	if ($result) {
		system_message(elgg_echo("$plugin:saved"));
	} else {
		register_error(elgg_echo("$plugin:uploadfailed"));
	}
	forward($forward . $container->username);
}

/**
 * Manage a file download.
 *
 * @param unknown_type $plugin
 * @param unknown_type $file_guid If not specified then file_guid will be found in input.
 */
function file_manage_download($plugin, $file_guid = "") {
	// Get the guid
	$file_guid = (int)$file_guid;

	if (!$file_guid) {
		$file_guid = (int)get_input("file_guid");
	}

	// Get the file
	$file = get_entity($file_guid);

	if ($file) {
		$mime = $file->getMimeType();
		if (!$mime) {
			$mime = "application/octet-stream";
		}

		$filename = $file->originalfilename;

		header("Content-type: $mime");
		if (strpos($mime, "image/")!==false) {
			header("Content-Disposition: inline; filename=\"$filename\"");
		} else {
			header("Content-Disposition: attachment; filename=\"$filename\"");
		}

		echo $file->grabFile();
		exit;
	} else {
		register_error(elgg_echo("$plugin:downloadfailed"));
	}
}

/**
 * Manage the download of a file icon.
 *
 * @param unknown_type $plugin
 * @param unknown_type $file_guid The guid, if not specified this is obtained from the input.
 */
function file_manage_icon_download($plugin, $file_guid = "") {
	// Get the guid
	$file_guid = (int)$file_guid;

	if (!$file_guid) {
		$file_guid = (int)get_input("file_guid");
	}

	// Get the file
	$file = get_entity($file_guid);

	if ($file) {
		$mime = $file->getMimeType();
		if (!$mime) {
			$mime = "application/octet-stream";
		}

		$filename = $file->thumbnail;

		header("Content-type: $mime");
		if (strpos($mime, "image/")!==false) {
			header("Content-Disposition: inline; filename=\"$filename\"");
		} else {
			header("Content-Disposition: attachment; filename=\"$filename\"");
		}

		$readfile = new ElggFile();
		$readfile->owner_guid = $file->owner_guid;
		$readfile->setFilename($filename);

		/*
		if ($file->open("read"));
		{
			while (!$file->eof())
			{
				echo $file->read(10240, $file->tell());
			}
		}
		*/

		$contents = $readfile->grabFile();
		if (empty($contents)) {
			echo file_get_contents(dirname(dirname(__FILE__)) . "/graphics/icons/general.jpg" );
		} else {
			echo $contents;
		}
		exit;
	} else {
		register_error(elgg_echo("$plugin:downloadfailed"));
	}
}

function file_display_thumbnail($file_guid,$size) {
	// Get file entity
	if ($file = get_entity($file_guid)) {
		$simpletype = $file->simpletype;
		if ($simpletype == "image") {
			// Get file thumbnail
			if ($size == "small") {
				$thumbfile = $file->smallthumb;
			} else {
				$thumbfile = $file->largethumb;
			}

			// Grab the file
			if ($thumbfile && !empty($thumbfile)) {
				$readfile = new ElggFile();
				$readfile->owner_guid = $file->owner_guid;
				$readfile->setFilename($thumbfile);
				$mime = $file->getMimeType();
				$contents = $readfile->grabFile();

				header("Content-type: $mime");
				echo $contents;
				exit;
			}
		}
	}
}

function file_set_page_owner($file) {
	$page_owner = page_owner_entity();
	if ($page_owner === false || is_null($page_owner)) {
		$container_guid = $file->container_guid;
		if (!empty($container_guid)) {
			if ($page_owner = get_entity($container_guid)) {
				set_page_owner($page_owner->guid);
			}
		}

		if (empty($page_owner)) {
			$page_owner = $_SESSION['user'];
			set_page_owner($_SESSION['guid']);
		}
	}
}

/**
 * Recursively delete a directory
 *
 * @param str $directory
 */
function delete_directory($directory) {
	// sanity check: must be a directory
	if (!$handle = opendir($directory)) {
		return FALSE;
	}

	// loop through all files
	while (($file = readdir($handle)) !== FALSE) {
		if (in_array($file, array('.', '..'))) {
			continue;
		}

		$path = "$directory/$file";
		if (is_dir($path)) {
			// recurse down through directory
			if (!delete_directory($path)) {
				return FALSE;
			}
		} else {
			// delete file
			unlink($path);
		}
	}

	// remove empty directory
	closedir($handle);
	return rmdir($directory);
}

/**
 * Removes all user files
 *
 * @param ElggUser $user
 * @return void
 */
function clear_user_files($user) {
	global $CONFIG;

	$time_created = date('Y/m/d', (int)$user->time_created);
	$file_path = "$CONFIG->dataroot$time_created/$user->guid";
	if (file_exists($file_path)) {
		delete_directory($file_path);
	}
}


/// Variable holding the default datastore
$DEFAULT_FILE_STORE = NULL;

/**
 * Return the default filestore.
 *
 * @return ElggFilestore
 */
function get_default_filestore() {
	global $DEFAULT_FILE_STORE;

	return $DEFAULT_FILE_STORE;
}

/**
 * Set the default filestore for the system.
 */
function set_default_filestore(ElggFilestore $filestore) {
	global $DEFAULT_FILE_STORE;

	$DEFAULT_FILE_STORE = $filestore;

	return true;
}

/**
 * Run once and only once.
 */
function filestore_run_once() {
	// Register a class
	add_subtype("object", "file", "ElggFile");
}

/**
 * Initialise the file modules.
 * Listens to system boot and registers any appropriate file types and classes
 */
function filestore_init() {
	global $CONFIG;

	// Now register a default filestore
	set_default_filestore(new ElggDiskFilestore($CONFIG->dataroot));

	// Now run this stuff, but only once
	run_function_once("filestore_run_once");
}

// Register a startup event
register_elgg_event_handler('init', 'system', 'filestore_init', 100);

// Unit testing
register_plugin_hook('unit_test', 'system', 'filestore_test');
function filestore_test($hook, $type, $value, $params) {
	global $CONFIG;
	$value[] = "{$CONFIG->path}engine/tests/objects/filestore.php";
	return $value;
}