<?php
/**
 * eGroupWare eTemplate Extension - VFS Widgets
 *
 * @link http://www.egroupware.org
 * @author Ralf Becker <RalfBecker@outdoor-training.de>
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
 * @copyright 2008-10 by RalfBecker@outdoor-training.de
 * @package etemplate
 * @subpackage extensions
 * @version $Id$
 */

/**
 * eTemplate extension to display stuff from the VFS system
 *
 * Contains the following widgets:
 * - vfs      aka File name+link:	clickable filename, with evtl. clickable path-components
 * - vfs-name aka Filename:         filename automatically urlencoded on return (urldecoded on display to user)
 * - vfs-size aka File size:		human readable filesize, eg. 1.4k
 * - vfs-mode aka File mode:		posix mode as string eg. drwxr-x---
 * - vfs-mime aka File icon:	    mime type icon or thumbnail (if configured AND enabled in the user-prefs)
 * - vfs-uid  aka File owner:       Owner of file, or 'root' if none
 * - vfs-gid  aka File group:       Group of file, or 'root' if none
 * - vfs-upload aka VFS file:       displays either download and delete (x) links or a file upload
 *   + value is either a vfs path or colon separated $app:$id:$relative_path, eg: infolog:123:special/offer
 *   + if empty($id) / new entry, file is created in a hidden temporary directory in users home directory
 *     and calling app is responsible to move content of that dir to entry directory, after entry is saved
 *   + option: required mimetype or regular expression for mimetype to match, eg. '/^text\//i' for all text files
 *   + if path ends in a slash, multiple files can be uploaded, their original filename is kept then
 *
 * All widgets accept as value a full path.
 * vfs-mime and vfs itself also allow an array with values like stat (incl. 'path'!) as value.
 * vfs-mime also allows just the mime type as value.
 * All other widgets allow additionally the nummeric value from the stat call (to not call it again).
 */
class vfs_widget
{
	/**
	 * exported methods of this class
	 *
	 * @var array
	 */
	var $public_functions = array(
		'pre_process' => True,
		'post_process' => true,		// post_process is only used for vfs-upload (all other widgets set $cell['readlonly']!)
	);
	/**
	 * availible extensions and there names for the editor
	 *
	 * @var array
	 */
	var $human_name = array(
		'vfs'      => 'File name+link',	// clickable filename, with evtl. clickable path-components
		'vfs-name' => 'File name',		// filename automatically urlencoded
		'vfs-size' => 'File size',		// human readable filesize
		'vfs-mode' => 'File mode',		// posix mode as string eg. drwxr-x---
		'vfs-mime' => 'File icon',		// mime type icon or thumbnail
		'vfs-uid'  => 'File owner',		// Owner of file, or 'root' if none
		'vfs-gid'  => 'File group',		// Group of file, or 'root' if none
		'vfs-upload' => 'VFS file',		// displays either download and delete (x) links or a file upload
	);

	/**
	 * pre-processing of the extension
	 *
	 * This function is called before the extension gets rendered
	 *
	 * @param string $form_name form-name of the control
	 * @param mixed &$value value / existing content, can be modified
	 * @param array &$cell array with the widget, can be modified for ui-independent widgets
	 * @param array &$readonlys names of widgets as key, to be made readonly
	 * @param mixed &$extension_data data the extension can store persisten between pre- and post-process
	 * @param object &$tmpl reference to the template we belong too
	 * @return boolean true if extra label is allowed, false otherwise
	 */
	function pre_process($form_name,&$value,&$cell,&$readonlys,&$extension_data,&$tmpl)
	{
		//echo "<p>".__METHOD__."($form_name,$value,".array2string($cell).",...)</p>\n";
		$type = $cell['type'];
		if (!in_array($type,array('vfs-name','vfs-upload'))) $cell['readonly'] = true;	// to not call post-process

		// check if we have a path and not the raw value, in that case we have to do a stat first
		if (in_array($type,array('vfs-size','vfs-mode','vfs-uid','vfs-gid')) && !is_numeric($value) || $type == 'vfs' && !$value)
		{
			if (!$value || !($stat = egw_vfs::stat($value)))
			{
				if ($value) $value = lang("File '%1' not found!",egw_vfs::decodePath($value));
				$cell = boetemplate::empty_cell();
				return true;	// allow extra value;
			}
		}
		$cell['type'] = 'label';

		switch($type)
		{
			case 'vfs-upload':		// option: required mimetype or regular expression for mimetype to match, eg. '/^text\//i' for all text files
				if (empty($value) && preg_match('/^exec.*\[([^]]+)\]$/',$form_name,$matches))	// if no value via content array, use widget name
				{
					$value = $matches[1];
				}
				$extension_data = array('value' => $value, 'mimetype' => $cell['size'], 'type' => $type);
				if ($value[0] != '/')
				{
					list($app,$id,$relpath) = explode(':',$value,3);
					if (empty($id))
					{
						static $tmppath = array();	// static var, so all vfs-uploads get created in the same temporary dir
						if (!isset($tmppath[$app])) $tmppath[$app] = '/home/'.$GLOBALS['egw_info']['user']['account_lid'].'/.'.$app.'_'.md5(time().session_id());
						$value = $tmppath[$app];
						unset($cell['onchange']);	// no onchange, if we have to use a temporary dir
					}
					else
					{
						$value = egw_link::vfs_path($app,$id,'',true);
					}
					if (!empty($relpath)) $value .= '/'.$relpath;
				}
				$path = $extension_data['path'] = $value;
				if (substr($path,-1) != '/' && self::file_exists($path) && !egw_vfs::is_dir($path))	// display download link and delete icon
				{
					$extension_data['path'] = $path;
					$cell = $this->file_widget($value,$path,$cell['name'],$cell['label']);
				}
				else	// file does NOT exists --> display file upload
				{
					$cell['type'] = 'file';
					// if no explicit help message set and we only allow certain file types --> show them
					if (empty($cell['help']) && $cell['size'])
					{
						if (($type = mime_magic::mime2ext($cell['size'])))
						{
							$type = '*.'.strtoupper($type);
						}
						else
						{
							$type = $cell['size'];
						}
						$cell['help'] = lang('Allowed file type: %1',$type);
					}
				}
				// check if directory (trailing slash) is given --> upload of multiple files
				if (substr($path,-1) == '/' && egw_vfs::file_exists($path) && ($files = egw_vfs::scandir($path)))
				{
					//echo $path; _debug_array($files);
					$upload = $cell;
					$cell = boetemplate::empty_cell('vbox','',array('size' => ',,0,0'));
					$extension_data['files'] = $files;
					$value = array();
					foreach($files as $file)
					{
						$file = $path.$file;
						$basename = basename($file);
						unset($widget);
						$widget = $this->file_widget($value[$basename],$file,$upload['name']."[$basename]");
						boetemplate::add_child($cell,$widget);
					}
					boetemplate::add_child($cell,$upload);
				}
				break;

			case 'vfs-size':	// option: add size in bytes in brackets
				$value = egw_vfs::hsize($size = is_numeric($value) ? $value : $stat['size']);
				if ($cell['size']) $value .= ' ('.$size.')';
				$cell['type'] = 'label';
				break;

			case 'vfs-mode':
				$value = egw_vfs::int2mode(is_numeric($value) ? $value : $stat['mode']);
				list($span,$class) = explode(',',$cell['span'],2);
				$class .= ($class ? ' ' : '') . 'vfsMode';
				$cell['span'] = $span.','.$class;
				$cell['no_lang'] = true;
				break;

			case 'vfs-uid':
			case 'vfs-gid':
				$uid = !is_numeric($value) ? $stat[$type=='vfs-uid'?'uid':'gid'] : $value;
				$value = !$uid ? 'root' : $GLOBALS['egw']->accounts->id2name($type=='vfs-uid'?$uid:-$uid);	// our internal gid's are negative!
				break;

			case 'vfs':
				if (is_array($value))
				{
					$name = $value['name'];
					$path = substr($value['path'],0,-strlen($name)-1);
					$mime = $value['mime'];
				}
				else
				{
					$name = $value;
					$path = '';
					$mime = egw_vfs::mime_content_type($value);
					$value = array();
				}
				if (($cell_name = $cell['name']) == '$row')
				{
					$arr = explode('][',substr($form_name,0,-1));
					$cell_name = array_pop($arr);
				}
				$cell['name'] = '';
				$cell['type'] = 'hbox';
				$cell['size'] = '0,,0,0';
				foreach($name != '/' ? explode('/',$name) : array('') as $n => $component)
				{
					if ($n > (int)($path === '/'))
					{
						$sep = soetemplate::empty_cell('label','',array('label' => '/'));
						soetemplate::add_child($cell,$sep);
						unset($sep);
					}
					$value['c'.$n] = $component !== '' ? egw_vfs::decodePath($component) : '/';
					$path .= ($path != '/' ? '/' : '').$component;
					// replace id's in /apps again with human readable titles
					$path_parts = explode('/',$path);
					if ($path_parts[1] == 'apps')
					{
						switch(count($path_parts))
						{
							case 2:
								$value['c'.$n] = lang('Applications');
								break;
							case 3:
								$value['c'.$n] = lang($path_parts[2]);
								break;
							case 4:
								if (is_numeric($value['c'.$n])) $value['c'.$n] .= egw_link::title($path_parts[2],$path_parts[3]);
								break;
						}
					}
					$popup = null;
					if (egw_vfs::is_readable($path))	// show link only if we have access to the file or dir
					{
						if ($n < count($comps)-1 || $mime == egw_vfs::DIR_MIME_TYPE || egw_vfs::is_dir($path))
						{
							$value['l'.$n] = egw_link::mime_open($path, egw_vfs::DIR_MIME_TYPE, $popup);
							$target = '';
						}
						else
						{
							$value['l'.$n] = egw_link::mime_open($path, $mime, $popup);
							$target = '_blank';
						}
					}

					if ($cell['onclick'])
					{
						$comp = boetemplate::empty_cell('button',$cell_name.'[c'.$n.']',array(
							'size'    => '1',
							'no_lang' => true,
							'span'    => ',vfsFilename',
							'label'   => $value['c'.$n],
							'onclick' => str_replace('$path',"'".addslashes($path)."'",$cell['onclick']),
						));
					}
					else
					{
						$comp = boetemplate::empty_cell('label',$cell_name.'[c'.$n.']',array(
							'size'    => ',@'.$cell_name.'[l'.$n.'],,,'.$target.','.$popup,
							'no_lang' => true,
							'span'    => ',vfsFilename',
						));
					}
					boetemplate::add_child($cell,$comp);
					unset($comp);
				}
				unset($cell['onclick']);	// otherwise it's handled by the grid too
				//_debug_array($comps); _debug_array($cell); _debug_array($value);
				break;

			case 'vfs-name':	// size: [length][,maxLength[,allowPath]]
				$cell['type'] = 'text';
				list($length,$maxLength,$allowPath) = $options = explode(',',$cell['size']);
				$preg = $allowPath ? '' : '/[^\\/]/';	// no slash '/' allowed, if not allowPath set
				$cell['size'] = "$length,$maxLength,$preg";
				$value = egw_vfs::decodePath($value);
				$extension_data = array('type' => $type,'allowPath' => $allowPath);
				break;

			case 'vfs-mime':  // size: [thsize] (thumbnail size)
				//Read the thumbnail size
				list($thsize) = explode(',', $cell['size']);
				if (!is_numeric($thsize))
				{
					$thsize = NULL;
				}

				if (!$value)
				{
					$cell = boetemplate::empty_cell();
					return true;
				}
				if (!is_array($value))
				{
					if ($value[0] == '/' || count(explode('/',$value)) != 2)
					{
						$mime = egw_vfs::mime_content_type($path=$value);
					}
					else
					{
						$mime = $value;
					}
				}
				else
				{
					$path = $value['path'];
					$mime = $value['mime'];
				}
				//error_log(__METHOD__."() type=vfs-mime: value=".array2string($value).": mime=$mime, path=$path");
				$cell['type'] = 'image';
				$cell['label'] = mime_magic::mime2label($mime);

				list($mime_main,$mime_sub) = explode('/',$mime);
				if ($mime_main == 'egw' || isset($GLOBALS['egw_info']['apps'][$mime_main]))
				{
					$value = $mime_main == 'egw' ? $mime_sub.'/navbar' : $mime;	// egw-applications for link-widget
					$cell['label'] = lang($mime_main == 'egw' ? $mime_sub : $mime_main);
					list($span,$class) = explode(',',$cell['span'],2);
					$class .= ($class ? ' ' : '') . 'vfsMimeIcon';
					$cell['span'] = $span.','.$class;
				}
				elseif($path && $mime_main == 'image' && in_array($mime_sub,array('png','jpeg','jpg','gif','bmp')) &&
					(string)$GLOBALS['egw_info']['server']['link_list_thumbnail'] != '0' &&
					(string)$GLOBALS['egw_info']['user']['preferences']['common']['link_list_thumbnail'] != '0' &&
					// check the size of the image, as too big images get no icon, but a PHP Fatal error:  Allowed memory size exhausted
					(!is_array($value) && ($stat = egw_vfs::stat($path)) ? $stat['size'] : $value['size']) < 1600000)
				{
					if (substr($path,0,6) == '/apps/')
					{
						$path = egw_vfs::parse_url(egw_vfs::resolve_url_symlinks($path),PHP_URL_PATH);
					}

					//Assemble the thumbnail parameters
					$thparams = array('path' => $path);

					if ($thsize)
					{
						$thparams['thsize'] = $thsize;
					}
					// add modification time to url to allow long caching
					if (is_array($value) && $value['mtime'])
					{
						$thparams['mtime'] = $value['mtime'];
					}

					$value = $GLOBALS['egw']->link('/etemplate/thumbnail.php', $thparams);
				}
				else
				{
					$value = egw_vfs::mime_icon($mime);
				}
				// mark symlinks (check if method exists, to allow etemplate to run on 1.6 API!)
				if (method_exists('egw_vfs','is_link') && egw_vfs::is_link($path))
				{
					$broken = !egw_vfs::stat($path);
					list($span,$class) = explode(',',$cell['span'],2);
					$class .= ($class ? ' ' : '') . ($broken ? 'vfsIsBrokenLink' : 'vfsIsLink');
					$cell['span'] = $span.','.$class;
					$cell['label'] = ($broken ? lang('Broken link') : lang('Link')).': '.egw_vfs::decodePath(egw_vfs::readlink($path)).
						(!$broken ? ' ('.$cell['label'].')' : '');
				}
				break;

			default:
				$value = 'Not yet implemented';
		}
		return true;
	}

	/**
	 * Create widget with download and delete (only if dir is writable) link
	 *
	 * @param mixed &$value
	 * @param string $path vfs path of download
	 * @param string $name name of widget
	 * @param string $label=null label, if not set basename($path) is used
	 * @return array
	 */
	static function file_widget(&$value,$path,$name,$label=null)
	{
		$value = empty($label) ? egw_vfs::decodePath(egw_vfs::basename($path)) : lang($label);	// display (translated) Label or filename (if label empty)

		$vfs_link = boetemplate::empty_cell('label',$name,array(
			'size' => ','.egw_vfs::download_url($path).',,,_blank,,'.$path,
		));
		// if dir is writable, add delete link
		if (egw_vfs::is_writable(egw_vfs::dirname($path)))
		{
			$cell = boetemplate::empty_cell('hbox','',array('size' => ',,0,0'));
			boetemplate::add_child($cell,$vfs_link);
			$delete_icon = boetemplate::empty_cell('button',$path,array(
				'label' => 'delete',
				'size'  => 'delete',	// icon
				'onclick' => "return confirm('Delete this file');",
				'span' => ',leftPad5',
			));
			boetemplate::add_child($cell,$delete_icon);
		}
		else
		{
			$cell = $vfs_link;
		}
		return $cell;
	}

	/**
	 * Check if vfs file exists *without* using the extension
	 *
	 * If you rename a file, you have to clear the cache ($clear_after=true)!
	 *
	 * @param string &$path on call path without extension, if existing on return full path incl. extension
	 * @param boolean $clear_after=null clear file-cache after (true) or before (false), default dont clear
	 * @return
	 */
	static function file_exists(&$path,$clear_after=null)
	{
		static $files = array();	// static var, to scan each directory only once
		$dir = egw_vfs::dirname($path);
		if ($clear_after === false) unset($files[$dir]);
		if (!isset($files[$dir])) $files[$dir] = egw_vfs::file_exists($dir) ? egw_vfs::scandir($dir) : array();

		$basename = egw_vfs::basename($path);
		$basename_len = strlen($basename);
		$found = false;
		foreach($files[$dir] as $file)
		{
			if (substr($file,0,$basename_len) == $basename)
			{
				$path = $dir.'/'.$file;
				$found = true;
			}
		}
		if ($clear_after === true) unset($files[$dir]);
		//echo "<p>".__METHOD__."($path) returning ".array2string($found)."</p>\n";
		return $found;
	}

	/**
	 * postprocessing method, called after the submission of the form
	 *
	 * It has to copy the allowed/valid data from $value_in to $value, otherwise the widget
	 * will return no data (if it has a preprocessing method). The framework insures that
	 * the post-processing of all contained widget has been done before.
	 *
	 * Only used by vfs-upload so far
	 *
	 * @param string $name form-name of the widget
	 * @param mixed &$value the extension returns here it's input, if there's any
	 * @param mixed &$extension_data persistent storage between calls or pre- and post-process
	 * @param boolean &$loop can be set to true to request a re-submision of the form/dialog
	 * @param object &$tmpl the eTemplate the widget belongs too
	 * @param mixed &value_in the posted values (already striped of magic-quotes)
	 * @return boolean true if $value has valid content, on false no content will be returned!
	 */
	function post_process($name,&$value,&$extension_data,&$loop,&$tmpl,$value_in)
	{
		//error_log(__METHOD__."('$name',".array2string($value).','.array2string($extension_data).",$loop,,".array2string($value_in).')');
		//echo '<p>'.__METHOD__."('$name',".array2string($value).','.array2string($extension_data).",$loop,,".array2string($value_in).")</p>\n";

		if (!$extension_data) return false;

		switch($extension_data['type'])
		{
			case 'vfs-name':
				$value = $extension_data['allowPath'] ? egw_vfs::encodePath($value_in) : egw_vfs::encodePathComponent($value_in);
				return true;

			case 'vfs-upload':
				break;	// handeled below

			default:
				return false;
		}
		// from here on vfs-upload only!

		// check if delete icon clicked
		if ($_POST['submit_button'] == ($fname = str_replace($extension_data['value'],$extension_data['path'],$name)) ||
			substr($extension_data['path'],-1) == '/' && substr($_POST['submit_button'],0,strlen($fname)-1) == substr($fname,0,-1))
		{
			if (substr($extension_data['path'],-1) == '/')	// multiple files?
			{
				foreach($extension_data['files'] as $file)	// check of each single file, to not allow deleting of arbitrary files
				{
					if ($_POST['submit_button'] == substr($fname,0,-1).$file.']')
					{
						if (!egw_vfs::unlink($extension_data['path'].$file))
						{
							etemplate::set_validation_error($name,lang('Error deleting %1!',egw_vfs::decodePath($extension_data['path'].$file)));
						}
						break;
					}
				}
			}
			elseif (!egw_vfs::unlink($extension_data['path']))
			{
				etemplate::set_validation_error($name,lang('Error deleting %1!',egw_vfs::decodePath($extension_data['path'])));
			}
			$loop = true;
			return false;
		}

		// handle file upload
		$name = preg_replace('/^exec\[([^]]+)\](.*)$/','\\1\\2',$name);	// remove exec prefix

		if (!is_array($_FILES['exec']) || !($filename = boetemplate::get_array($_FILES['exec']['name'],$name)))
		{
			return false;	// no file attached
		}
		$tmp_name = boetemplate::get_array($_FILES['exec']['tmp_name'],$name);
		$error = boetemplate::get_array($_FILES['exec']['error'],$name);
		if ($error)
		{
			etemplate::set_validation_error($name,lang('Error uploading file!')."\n".
				etemplate::max_upload_size_message());
			$loop = true;
			return false;
		}
		if (empty($tmp_name) || function_exists('is_uploaded_file') && !is_uploaded_file($tmp_name) || !file_exists($tmp_name))
		{
			return false;
		}
		// check if type matches required mime-type, if specified
		if (!empty($extension_data['mimetype']))
		{
			$type = boetemplate::get_array($_FILES['exec']['type'],$name);
			$is_preg = $extension_data['mimetype'][0] == '/';
			if (!$is_preg && strcasecmp($extension_data['mimetype'],$type) || $is_preg && !preg_match($extension_data['mimetype'],$type))
			{
				etemplate::set_validation_error($name,lang('File is of wrong type (%1 != %2)!',$type,$extension_data['mimetype']));
				return false;
			}
		}
		$path = $extension_data['path'];
		if (substr($path,-1) != '/')
		{
			// add extension to path
			$parts = explode('.',$filename);
			if (($extension = array_pop($parts)) && mime_magic::ext2mime($extension))	// really an extension --> add it to path
			{
				$path .= '.'.$extension;
			}
		}
		else	// multiple upload with dir given (trailing slash)
		{
			$path .= egw_vfs::encodePathComponent($filename);
		}
		if (!egw_vfs::file_exists($dir = egw_vfs::dirname($path)) && !egw_vfs::mkdir($dir,null,STREAM_MKDIR_RECURSIVE))
		{
			etemplate::set_validation_error($name,lang('Error create parent directory %1!',egw_vfs::decodePath($dir)));
			return false;
		}
		if (!copy($tmp_name,egw_vfs::PREFIX.$path))
		{
			etemplate::set_validation_error($name,lang('Error copying uploaded file to vfs!'));
			return false;
		}
		$value = $path;	// return path of file, important if only a temporary location is used

		return true;
	}
}