<?php
# -- BEGIN LICENSE BLOCK ---------------------------------------
#
# This file is part of Clearbricks.
#
# Copyright (c) 2003-2013 Olivier Meunier & Association Dotclear
# Licensed under the GPL version 2.0 license.
# See LICENSE file or
# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
#
# -- END LICENSE BLOCK -----------------------------------------

/**
* Files manager
*
* Files management class
*
* @package Clearbricks
* @subpackage Filemanager
*/
class filemanager
{
	/** @var string	Files manager root path */
	public $root;
	
	/** @var string	Files manager root URL */
	public $root_url;
	
	/** @var string	Working (current) directory */
	protected $pwd;
	
	/** @var array		Array of regexps defining excluded items */
	protected $exclude_list = array();
	
	/** @var string	Files exclusion regexp pattern */
	protected $exclude_pattern = '';
	
	/** @var array		Current directory content array */
	public $dir = array('dirs'=>array(),'files'=>array());
	
	/**
	* Constructor
	*
	* New filemanage istance. Note that filemanage is a jail in given root
	* path. You won't be able to access files outside {@link $root} path with
	* the object's methods.
	*
	* @param string	$root		Root path
	* @param string	$root_url		Root URL
	*/
	public function __construct($root,$root_url='')
	{
		$this->root = $this->pwd = path::real($root);
		$this->root_url = $root_url;
		
		if (!preg_match('#/$#',$this->root_url)) {
			$this->root_url = $this->root_url.'/';
		}
		
		if (!$this->root) {
			throw new Exception('Invalid root directory.');
		}
	}
	
	/**
	* Change directory
	*
	* Changes working directory. $dir is relative to instance {@link $root}
	* directory.
	*
	* @param string	$dir			Directory
	*/
	public function chdir($dir)
	{
		$realdir = path::real($this->root.'/'.path::clean($dir));
		if (!$realdir || !is_dir($realdir)) {
			throw new Exception('Invalid directory.');
		}
		
		if ($this->isExclude($realdir)) {
			throw new Exception('Directory is excluded.');
		}
		
		$this->pwd = $realdir;
	}
	
	/**
	* Get working directory
	*
	* Returns working directory path.
	*
	* @return string
	*/
	public function getPwd()
	{
		return $this->pwd;
	}
	
	/**
	* Current directory is writable
	*
	* @return boolean	true if working directory is writable
	*/
	public function writable()
	{
		if (!$this->pwd) {
			return false;
		}
		
		return is_writable($this->pwd);
	}
	
	/**
	* Add exclusion
	*
	* Appends an exclusion to exclusions list. $f should be a regexp.
	*
	* @see $exclude_list
	* @param string	$f			Exclusion regexp
	*/
	public function addExclusion($f)
	{
		if (is_array($f))
		{
			foreach ($f as $v) {
				if (($V = path::real($v)) !== false) {
					$this->exclude_list[] = $V;
				}
			}
		}
		elseif (($F = path::real($f)) !== false)
		{
			$this->exclude_list[] = $F;
		}
	}
	
	/**
	* Path is excluded
	*
	* Returns true if path (file or directory) $f is excluded. $f path is
	* relative to {@link $root} path.
	*
	* @see $exclude_list
	* @param string	$f			Path to match
	* @return boolean
	*/
	protected function isExclude($f)
	{
		foreach ($this->exclude_list as $v)
		{
			if (strpos($f,$v) === 0) {
				return true;
			}
		}
		
		return false;
	}
	
	/**
	* File is excluded
	*
	* Returns true if file $f is excluded. $f path is relative to {@link $root}
	* path.
	*
	* @see $exclude_pattern
	* @param string	$f			File to match
	* @return boolean
	*/
	protected function isFileExclude($f)
	{
		if (!$this->exclude_pattern) {
			return false;
		}
		
		return preg_match($this->exclude_pattern,$f);
	}
	
	/**
	* Item in jail
	*
	* Returns true if file or directory $f is in jail (ie. not outside the
	* {@link $root} directory).
	*
	* @param string	$f			Path to match
	* @return boolean
	*/
	protected function inJail($f)
	{
		$f = path::real($f);
		
		if ($f !== false) {
			return preg_match('|^'.preg_quote($this->root,'|').'|',$f);
		}
		
		return false;
	}
	
	/**
	* File in files
	*
	* Returns true if file $f is in files array of {@link $dir}.
	*
	* @param string	$f			File to match
	* @return boolean
	*/
	public function inFiles($f)
	{
		foreach ($this->dir['files'] as $v) {
			if ($v->relname == $f) {
				return true;
			}
		}
		return false;
	}
	
	/**
	* Directory list
	*
	* Creates list of items in working directory and append it to {@link $dir}
	*
	* @uses sortHandler()
	* @uses fileItem
	*/
	public function getDir()
	{
		$dir = path::clean($this->pwd);
		
		$dh = @opendir($dir);
		
		if ($dh === false) {
			throw new Exception('Unable to read directory.');
		}
		
		$d_res = $f_res = array();
			
		while (($file = readdir($dh)) !== false)
		{
			$fname = $dir.'/'.$file;
			
			if ($this->inJail($fname) && !$this->isExclude($fname))
			{
				if (is_dir($fname) && $file != '.') {
					$tmp = new fileItem($fname,$this->root,$this->root_url);
					if ($file == '..') {
						$tmp->parent = true;
					}
					$d_res[] = $tmp;
				}
				
				if (is_file($fname) && strpos($file,'.') !== 0 && !$this->isFileExclude($file)) {
					$f_res[] = new fileItem($fname,$this->root,$this->root_url);
				}
			}
		}
		closedir($dh);
		
		$this->dir = array('dirs'=>$d_res,'files'=>$f_res);
		usort($this->dir['dirs'],array($this,'sortHandler'));
		usort($this->dir['files'],array($this,'sortHandler'));
	}
	
	/**
	* Root directories
	*
	* Returns an array of directory under {@link $root} directory.
	*
	* @uses fileItem
	* @return array
	*/
	public function getRootDirs()
	{
		$d = files::getDirList($this->root);
		
		$dir = array();
		
		foreach ($d['dirs'] as $v) {
			$dir[] = new fileItem($v,$this->root,$this->root_url);
		}
		
		return $dir;
	}
	
	/**
	* Upload file
	*
	* Move <var>$tmp</var> file to its final destination <var>$dest</var> and
	* returns the destination file path.
	* <var>$dest</var> should be in jail. This method will throw exception
	* if the file cannot be written.
	*
	* You should first verify upload status, with {@link files::uploadStatus()}
	* or PHP native functions.
	*
	* @see files::uploadStatus()
	* @param string	$tmp			Temporary uploaded file path
	* @param string	$dest		Destination file
	* @param string	$overwrite	overwrite mode
	* @return string				Destination real path
	*/
	public function uploadFile($tmp,$dest,$overwrite=false)
	{
		$dest = $this->pwd.'/'.path::clean($dest);
		
		if ($this->isFileExclude($dest)) {
			throw new Exception(__('Uploading this file is not allowed.'));
		}
		
		if (!$this->inJail(dirname($dest))) {
			throw new Exception(__('Destination directory is not in jail.'));
		}
		
		if (!$overwrite && file_exists($dest)) {
			throw new Exception(__('File already exists.'));
		}
		
		if (!is_writable(dirname($dest))) {
			throw new Exception(__('Cannot write in this directory.'));
		}
		
		if (@move_uploaded_file($tmp,$dest) === false) {
			throw new Exception(__('An error occurred while writing the file.'));
		}
		
		files::inheritChmod($dest);
		return path::real($dest);
	}
	
	/**
	* Upload file by bits
	*
	* Creates a new file <var>$dest</var> with contents of <var>$bits</var> and
	* return the destination file path.
	* <var>$dest</var> should be in jail. This method will throw exception
	* if file cannot be written.
	*
	* @param string	$bits		Destination file content
	* @param string	$dest		Destination file
	* @return string				Destination real path
	*/
	public function uploadBits($name,$bits)
	{
		$dest = $this->pwd.'/'.path::clean($name);
		
		if ($this->isFileExclude($dest)) {
			throw new Exception(__('Uploading this file is not allowed.'));
		}
		
		if (!$this->inJail(dirname($dest))) {
			throw new Exception(__('Destination directory is not in jail.'));
		}
		
		if (!is_writable(dirname($dest))) {
			throw new Exception(__('Cannot write in this directory.'));
		}
		
		$fp = @fopen($dest,'wb');
		if ($fp === false) {
			throw new Exception(__('An error occurred while writing the file.'));
		}
		
		fwrite($fp,$bits);
		fclose($fp);
		files::inheritChmod($dest);
		
		return path::real($dest);
	}
	
	/**
	* New directory
	*
	* Creates a new directory <var>$d</var> relative to working directory.
	*
	* @param string	$d			Directory name
	*/
	public function makeDir($d)
	{
		files::makeDir($this->pwd.'/'.path::clean($d)); 
	}
	
	/**
	* Move file
	*
	* Moves a file <var>$s</var> to a new destination <var>$d</var>. Both
	* <var>$s</var> and <var>$d</var> are relative to {@link $root}.
	*
	* @param string	$s			Source file
	* @param string	$d			Destination file
	*/
	public function moveFile($s,$d)
	{
		$s = $this->root.'/'.path::clean($s);
		$d = $this->root.'/'.path::clean($d);
		
		if (($s = path::real($s)) === false) {
			throw new Exception(__('Source file does not exist.'));
		}
		
		$dest_dir = path::real(dirname($d));
		
		if (!$this->inJail($s)) {
			throw new Exception(__('File is not in jail.'));
		}
		if (!$this->inJail($dest_dir)) {
			throw new Exception(__('File is not in jail.'));
		}
		
		if (!is_writable($dest_dir)) {
			throw new Exception(__('Destination directory is not writable.'));
		}
		
		if (@rename($s,$d) === false) {
			throw new Exception(__('Unable to rename file.'));
		}
	}
	
	/**
	* Remove item
	*
	* Removes a file or directory <var>$f</var> which is relative to working
	* directory.
	*
	* @param string	$f			Path to remove
	*/
	public function removeItem($f)
	{
		$file = path::real($this->pwd.'/'.path::clean($f));
		
		if (is_file($file)) {
			$this->removeFile($f);
		} elseif (is_dir($file)) {
			$this->removeDir($f);
		}
	}
	
	/**
	* Remove item
	*
	* Removes a file <var>$f</var> which is relative to working directory.
	*
	* @param string	$f			File to remove
	*/
	public function removeFile($f)
	{
		$f = path::real($this->pwd.'/'.path::clean($f));
		
		if (!$this->inJail($f)) {
			throw new Exception(__('File is not in jail.'));
		}
		
		if (!files::isDeletable($f)) {
			throw new Exception(__('File cannot be removed.'));
		}
		
		if (@unlink($f) === false) {
			throw new Exception(__('File cannot be removed.'));
		}
	}
	
	/**
	* Remove item
	*
	* Removes a directory <var>$d</var> which is relative to working directory.
	*
	* @param string	$d			Directory to remove
	*/
	public function removeDir($d)
	{
		$d = path::real($this->pwd.'/'.path::clean($d));
		
		if (!$this->inJail($d)) {
			throw new Exception(__('Directory is not in jail.'));
		}
		
		if (!files::isDeletable($d)) {
			throw new Exception(__('Directory cannot be removed.'));
		}
		
		if (@rmdir($d) === false) {
			throw new Exception(__('Directory cannot be removed.'));
		}
	}
	
	/**
	* SortHandler
	*
	* This method is called by {@link getDir()} to sort files. Can be overrided
	* in inherited classes.
	*
	* @param fileItem	$a			fileItem object
	* @param fileItem	$b			fileItem object
	* @return integer
	*/
	protected function sortHandler($a,$b)
	{
		if ($a->parent && !$b->parent || !$a->parent && $b->parent) {
			return ($a->parent) ? -1 : 1;
		}
		return strcasecmp($a->basename,$b->basename);
	}
}

/**
* File item
*
* File item class used by {@link filemanager}. In this class {@link $file} could
* be either a file or a directory.
*
* @package Clearbricks
* @subpackage Filemanager
*/
class fileItem
{
	/** @var string Complete path to file */
	public $file;
	
	/** @var string File basename */
	public $basename;
	
	/** @var string File directory name */
	public $dir;
	
	/** @var string File URL */
	public $file_url;
	
	/** @var string File directory URL */
	public $dir_url;
	
	/** @var string File extension */
	public $extension;
	
	/** @var string File path relative to <var>$root</var> given in constructor */
	public $relname;
	
	/** @var boolean Parent directory (ie. "..") */
	public $parent = false;
	
	
	/** @var string File MimeType. See {@link files::getMimeType()}.  */
	public $type;
	
	/** @var integer File modification timestamp */
	public $mtime;
	
	/** @var integer File size */
	public $size;
	
	/** @var integer File permissions mode */
	public $mode;
	
	/** @var integer File owner ID */
	public $uid;
	
	/** @var integer File group ID */
	public $gid;
	
	/** @var boolean True if file or directory is writable */
	public $w;
	
	/** @var boolean True if file is a directory */
	public $d;
	
	/** @var boolean True if file file is executable or directory is traversable */
	public $x;
	
	/** @var boolean True if file is a file */
	public $f;
	
	/** @var boolean True if file or directory is deletable */
	public $del;
	
	/**
	* Constructor
	*
	* Creates an instance of fileItem object.
	*
	* @param string	$file		Absolute file or directory path
	* @param string	$root		File root path
	* @param string	$root_url		File root URL
	*/
	public function __construct($file,$root,$root_url='')
	{
		$file = path::real($file);
		$stat = stat($file);
		$path = path::info($file);
		
		$rel = preg_replace('/^'.preg_quote($root,'/').'\/?/','',$file);
		
		$this->file = $file;
		$this->basename = $path['basename'];
		$this->dir = $path['dirname'];
		$this->relname = $rel;
		
		$this->file_url = str_replace('%2F','/',rawurlencode($rel));
		$this->file_url = $root_url.$this->file_url;
		
		$this->dir_url = dirname($this->file_url);
		$this->extension = $path['extension'];
		
		$this->mtime = $stat[9];
		$this->size = $stat[7];
		$this->mode = $stat[2];
		$this->uid = $stat[4];
		$this->gid = $stat[5];
		$this->w = is_writable($file);
		$this->d = is_dir($file);
		$this->f = is_file($file);
		if ($this->d) {
			$this->x = file_exists($file.'/.');
		} else {
			$this->x = false;
		}
		$this->del = files::isDeletable($file);
		
		$this->type = $this->d ? null : files::getMimeType($file);
		$this->type_prefix = preg_replace('/^(.+?)\/.+$/','$1',$this->type);
	}
}
?>