<?php
# -- BEGIN LICENSE BLOCK ---------------------------------------
#
# This file is part of Dotclear 2.
#
# 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 -----------------------------------------
if (!defined('DC_RC_PATH')) { return; }

/**
@ingroup DC_CORE
@brief Dotclear media manage

This class handles Dotclear media items.
*/
class dcMedia extends filemanager
{
	protected $core;		///< <b>dcCore</b> dcCore instance
	protected $con;		///< <b>connection</b> Database connection
	protected $table;		///< <b>string</b> Media table name
	protected $type;		///< <b>string</b> Media type filter
	protected $postmedia;
	protected $file_sort = 'name-asc';

	protected $file_handler = array();	///< <b>array</b> Array of callbacks

	public $thumb_tp = '%s/.%s_%s.jpg';	///< <b>string</b> Thumbnail file pattern
	public $thumb_tp_alpha = '%s/.%s_%s.png'; ///< <b>string</b> Thumbnail file pattern (with alpha layer)

	/**
	<b>array</b> Tubmnail sizes:
	- m: medium image
	- s: small image
	- t: thumbnail image
	- sq: square image
	*/
	public $thumb_sizes = array(
		'm' => array(448,'ratio','medium'),
		's' => array(240,'ratio','small'),
		't' => array(100,'ratio','thumbnail'),
		'sq' => array(48,'crop','square')
	);

	public $icon_img = 'images/media/%s.png';	///< <b>string</b> Icon file pattern

	/**
	Object constructor.

	@param	core		<b>dcCore</b>		dcCore instance
	@param	type		<b>string</b>		Media type filter
	*/
	public function __construct($core,$type='')
	{
		$this->core =& $core;
		$this->con =& $core->con;
		$this->postmedia = new dcPostMedia($core);

		if ($this->core->blog == null) {
			throw new Exception(__('No blog defined.'));
		}

		$this->table = $this->core->prefix.'media';
		$root = $this->core->blog->public_path;

		if (preg_match('#^http(s)?://#',$this->core->blog->settings->system->public_url)) {
			$root_url = rawurldecode($this->core->blog->settings->system->public_url);
		} else {
			$root_url = rawurldecode($this->core->blog->host.path::clean($this->core->blog->settings->system->public_url));
		}

		if (!is_dir($root)) {
			# Check public directory
			if ( $core->auth->isSuperAdmin() ) {
				throw new Exception(__("There is no writable directory /public/ at the location set in about:config \"public_path\". You must create this directory with sufficient rights (or change this setting)."));
			} else {
				throw new Exception(__("There is no writable root directory for the media manager. You should contact your administrator."));
			}
		}

		$this->type = $type;

		parent::__construct($root,$root_url);
		$this->chdir('');

		$this->path = $this->core->blog->settings->system->public_path;

		$this->addExclusion(DC_RC_PATH);
		$this->addExclusion(dirname(__FILE__).'/../');

		$this->exclude_pattern = $core->blog->settings->system->media_exclusion;

		# Event handlers
		$this->addFileHandler('image/jpeg','create',array($this,'imageThumbCreate'));
		$this->addFileHandler('image/png','create',array($this,'imageThumbCreate'));
		$this->addFileHandler('image/gif','create',array($this,'imageThumbCreate'));

		$this->addFileHandler('image/png','update',array($this,'imageThumbUpdate'));
		$this->addFileHandler('image/jpeg','update',array($this,'imageThumbUpdate'));
		$this->addFileHandler('image/gif','update',array($this,'imageThumbUpdate'));

		$this->addFileHandler('image/png','remove',array($this,'imageThumbRemove'));
		$this->addFileHandler('image/jpeg','remove',array($this,'imageThumbRemove'));
		$this->addFileHandler('image/gif','remove',array($this,'imageThumbRemove'));

		$this->addFileHandler('image/jpeg','create',array($this,'imageMetaCreate'));

		$this->addFileHandler('image/jpeg','recreate',array($this,'imageThumbCreate'));
		$this->addFileHandler('image/png','recreate',array($this,'imageThumbCreate'));
		$this->addFileHandler('image/gif','recreate',array($this,'imageThumbCreate'));

		$this->addFileHandler('image/jpeg','recreate',array($this,'imageThumbCreate'));
		$this->addFileHandler('image/png','recreate',array($this,'imageThumbCreate'));
		$this->addFileHandler('image/gif','recreate',array($this,'imageThumbCreate'));

		# Thumbnails sizes
		$this->thumb_sizes['m'][0] = abs($core->blog->settings->system->media_img_m_size);
		$this->thumb_sizes['s'][0] = abs($core->blog->settings->system->media_img_s_size);
		$this->thumb_sizes['t'][0] = abs($core->blog->settings->system->media_img_t_size);

		# Thumbnails sizes names
		$this->thumb_sizes['m'][2] = __($this->thumb_sizes['m'][2]);
		$this->thumb_sizes['s'][2] = __($this->thumb_sizes['s'][2]);
		$this->thumb_sizes['t'][2] = __($this->thumb_sizes['t'][2]);
		$this->thumb_sizes['sq'][2] = __($this->thumb_sizes['sq'][2]);

		# --BEHAVIOR-- coreMediaConstruct
		$this->core->callBehavior('coreMediaConstruct',$this);
	}

	/**
	Changes working directory.

	@param	dir		<b>string</b>		Directory name.
	*/
	public function chdir($dir)
	{
		parent::chdir($dir);
		$this->relpwd = preg_replace('/^'.preg_quote($this->root,'/').'\/?/','',$this->pwd);
	}

	/**
	Adds a new file handler for a given media type and event.

	Available events are:
	- create: file creation
	- update: file update
	- remove: file deletion

	@param	type		<b>string</b>		Media type
	@param	event	<b>string</b>		Event
	@param	function	<b>callback</b>
	*/
	public function addFileHandler($type,$event,$function)
	{
		if (is_callable($function)) {
			$this->file_handler[$type][$event][] = $function;
		}
	}

	protected function callFileHandler($type,$event)
	{
		if (!empty($this->file_handler[$type][$event]))
		{
			$args = func_get_args();
			array_shift($args);
			array_shift($args);

			foreach ($this->file_handler[$type][$event] as $f)
			{
				call_user_func_array($f,$args);
			}
		}
	}

	/**
	Returns HTML breadCrumb for media manager navigation.

	@param	href		<b>string</b>		URL pattern
	@param	last		<b>string</b>		Last item pattern
	@return	<b>string</b> HTML code
	*/
	public function breadCrumb($href,$last='')
	{
		$res = '';
		if ($this->relpwd && $this->relpwd != '.') {
			$pwd = '';
			$arr = explode('/',$this->relpwd);
			$count = count($arr);
			foreach ($arr as $v) {
				if (($last != '') && (0 === --$count)) {
					$res .= sprintf($last,$v);
				} else {
					$pwd .= rawurlencode($v).'/';
					$res .= '<a href="'.sprintf($href,$pwd).'">'.$v.'</a> / ';
				}
			}
		}
		return $res;

	}

	protected function fileRecord($rs)
	{
		if ($rs->isEmpty()) { return null; }

		if (!$this->isFileExclude($this->root.'/'.$rs->media_file) && is_file($this->root.'/'.$rs->media_file))
		{
			$f = new fileItem($this->root.'/'.$rs->media_file,$this->root,$this->root_url);

			if ($this->type && $f->type_prefix != $this->type) {
				return null;
			}

			$meta = @simplexml_load_string($rs->media_meta);

			$f->editable = true;
			$f->media_id = $rs->media_id;
			$f->media_title = $rs->media_title;
			$f->media_meta =  $meta instanceof SimpleXMLElement ? $meta : simplexml_load_string('<meta></meta>');
			$f->media_user = $rs->user_id;
			$f->media_priv = (boolean) $rs->media_private;
			$f->media_dt = strtotime($rs->media_dt);
			$f->media_dtstr = dt::str('%Y-%m-%d %H:%M',$f->media_dt);

			$f->media_image = false;

			if (!$this->core->auth->check('media_admin',$this->core->blog->id)
			&& $this->core->auth->userID() != $f->media_user) {
				$f->del = false;
				$f->editable = false;
			}

			$type_prefix = explode('/',$f->type);
			$type_prefix = $type_prefix[0];

			switch ($type_prefix) {
				case 'image':
					$f->media_image = true;
					$f->media_icon = 'image';
					break;
				case 'audio':
					$f->media_icon = 'audio';
					break;
				case 'text':
					$f->media_icon = 'text';
					break;
				case 'video':
					$f->media_icon = 'video';
					break;
				default:
					$f->media_icon = 'blank';
			}
			switch ($f->type) {
				case 'application/msword':
				case 'application/vnd.oasis.opendocument.text':
				case 'application/vnd.sun.xml.writer':
				case 'application/pdf':
				case 'application/postscript':
					$f->media_icon = 'document';
					break;
				case 'application/msexcel':
				case 'application/vnd.oasis.opendocument.spreadsheet':
				case 'application/vnd.sun.xml.calc':
					$f->media_icon = 'spreadsheet';
					break;
				case 'application/mspowerpoint':
				case 'application/vnd.oasis.opendocument.presentation':
				case 'application/vnd.sun.xml.impress':
					$f->media_icon = 'presentation';
					break;
				case 'application/x-debian-package':
				case 'application/x-bzip':
				case 'application/x-gzip':
				case 'application/x-java-archive':
				case 'application/rar':
				case 'application/x-redhat-package-manager':
				case 'application/x-tar':
				case 'application/x-gtar':
				case 'application/zip':
					$f->media_icon = 'package';
					break;
				case 'application/octet-stream':
					$f->media_icon = 'executable';
					break;
				case 'application/x-shockwave-flash':
					$f->media_icon = 'video';
					break;
				case 'application/ogg':
					$f->media_icon = 'audio';
					break;
				case 'text/html':
					$f->media_icon = 'html';
					break;
			}

			$f->media_type = $f->media_icon;
			$f->media_icon = sprintf($this->icon_img,$f->media_icon);

			# Thumbnails
			$f->media_thumb = array();
			$p = path::info($f->relname);

			$alpha = ($p['extension'] == 'png') || ($p['extension'] == 'PNG');

			$thumb = sprintf(($alpha ? $this->thumb_tp_alpha : $this->thumb_tp),$this->root.'/'.$p['dirname'],$p['base'],'%s');
			$thumb_url = sprintf(($alpha ? $this->thumb_tp_alpha : $this->thumb_tp),$this->root_url.$p['dirname'],$p['base'],'%s');

			# Cleaner URLs
			$thumb_url = preg_replace('#\./#','/',$thumb_url);
			$thumb_url = preg_replace('#(?<!:)/+#','/',$thumb_url);

			if ($alpha) {
				$thumb_alt = sprintf($this->thumb_tp,$this->root.'/'.$p['dirname'],$p['base'],'%s');
				$thumb_url_alt = sprintf($this->thumb_tp,$this->root_url.$p['dirname'],$p['base'],'%s');
				# Cleaner URLs
				$thumb_url_alt = preg_replace('#\./#','/',$thumb_url_alt);
				$thumb_url_alt = preg_replace('#(?<!:)/+#','/',$thumb_url_alt);
			}

			foreach ($this->thumb_sizes as $suffix => $s) {
				if (file_exists(sprintf($thumb,$suffix))) {
					$f->media_thumb[$suffix] = sprintf($thumb_url,$suffix);
				} elseif ($alpha && file_exists(sprintf($thumb_alt,$suffix))) {
					$f->media_thumb[$suffix] = sprintf($thumb_url_alt,$suffix);
				}
			}

			if (isset($f->media_thumb['sq']) && $f->media_type == 'image') {
				$f->media_icon = $f->media_thumb['sq'];
			}

			return $f;
		}

		return null;
	}


	public function setFileSort($type='name')
	{
		if (in_array($type,array('name-asc','name-desc','date-asc','date-desc'))) {
			$this->file_sort = $type;
		}
	}

	protected function sortFileHandler($a,$b)
	{
		if (is_null($a) || is_null($b)) {
			return (is_null($a) ? 1 : -1);
		}
		switch ($this->file_sort)
		{
			case 'date-asc':
				if ($a->media_dt == $b->media_dt) {
					return 0;
				}
				return ($a->media_dt < $b->media_dt) ? -1 : 1;
			case 'date-desc':
				if ($a->media_dt == $b->media_dt) {
					return 0;
				}
				return ($a->media_dt > $b->media_dt) ? -1 : 1;
			case 'name-desc':
				return strcasecmp($b->basename,$a->basename);
			case 'name-asc':
			default:
				return strcasecmp($a->basename,$b->basename);
		}
	}

	/**
	Gets current working directory content (using filesystem)

	*/
	public function getFSDir()
	{
		parent::getDir();
	}

	/**
	Gets current working directory content.

	@param	type		<b>string</b>		Media type filter
	*/
	public function getDir($type=null)
	{
		if ($type) {
			$this->type = $type;
		}

		$media_dir = $this->relpwd ?: '.';

		$strReq =
		'SELECT media_file, media_id, media_path, media_title, media_meta, media_dt, '.
		'media_creadt, media_upddt, media_private, user_id '.
		'FROM '.$this->table.' '.
		"WHERE media_path = '".$this->path."' ".
		"AND media_dir = '".$this->con->escape($media_dir)."' ";

		if (!$this->core->auth->check('media_admin',$this->core->blog->id))
		{
			$strReq .= 'AND (media_private <> 1 ';

			if ($this->core->auth->userID()) {
				$strReq .= "OR user_id = '".$this->con->escape($this->core->auth->userID())."'";
			}
			$strReq .= ') ';
		}

		$rs = $this->con->select($strReq);

		parent::getDir();

		$f_res = array();
		$p_dir = $this->dir;

		# If type is set, remove items from p_dir
		if ($this->type)
		{
			foreach ($p_dir['files'] as $k => $f) {
				if ($f->type_prefix != $this->type) {
					unset($p_dir['files'][$k]);
				}
			}
		}

		$f_reg = array();

		while ($rs->fetch())
		{
			# File in subdirectory, forget about it!
			if (dirname($rs->media_file) != '.' && dirname($rs->media_file) != $this->relpwd) {
				continue;
			}

			if ($this->inFiles($rs->media_file))
			{
				$f = $this->fileRecord($rs);
				if ($f !== null) {
					if (isset($f_reg[$rs->media_file]))
					{
						# That media is duplicated in the database,
						# time to do a bit of house cleaning.
						$this->con->execute(
							'DELETE FROM '.$this->table.' '.
							"WHERE media_id = ".$this->fileRecord($rs)->media_id
						);
					} else {
						$f_res[] = $this->fileRecord($rs);
						$f_reg[$rs->media_file] = 1;
					}
				}
			}
			elseif (!empty($p_dir['files']) && $this->relpwd == '')
			{
				# Physical file does not exist remove it from DB
				# Because we don't want to erase everything on
				# dotclear upgrade, do it only if there are files
				# in directory and directory is root
				$this->con->execute(
					'DELETE FROM '.$this->table.' '.
					"WHERE media_path = '".$this->con->escape($this->path)."' ".
					"AND media_file = '".$this->con->escape($rs->media_file)."' "
				);
				$this->callFileHandler(files::getMimeType($rs->media_file),'remove',$this->pwd.'/'.$rs->media_file);
			}
		}

		$this->dir['files'] = $f_res;
		foreach ($this->dir['dirs'] as $k => $v) {
			$v->media_icon = sprintf($this->icon_img,($v->parent ? 'folder-up' : 'folder'));
		}

		# Check files that don't exist in database and create them
		if ($this->core->auth->check('media,media_admin',$this->core->blog->id))
		{
			foreach ($p_dir['files'] as $f)
			{
				if (!isset($f_reg[$f->relname])) {
					if (($id = $this->createFile($f->basename,null,false,null,false)) !== false) {
						$this->dir['files'][] = $this->getFile($id);
					}
				}
			}
		}
		try {
			usort($this->dir['files'],array($this,'sortFileHandler'));
		} catch (Exception $e) {}
	}

	/**
	Gets file by its id. Returns a filteItem object.

	@param	id		<b>integer</b>		File ID
	@return	<b>fileItem</b>
	*/
	public function getFile($id)
	{
		$strReq =
		'SELECT media_id, media_path, media_title, '.
		'media_file, media_meta, media_dt, media_creadt, '.
		'media_upddt, media_private, user_id '.
		'FROM '.$this->table.' '.
		"WHERE media_path = '".$this->path."' ".
		'AND media_id = '.(integer) $id.' ';

		if (!$this->core->auth->check('media_admin',$this->core->blog->id))
		{
			$strReq .= 'AND (media_private <> 1 ';

			if ($this->core->auth->userID()) {
				$strReq .= "OR user_id = '".$this->con->escape($this->core->auth->userID())."'";
			}
			$strReq .= ') ';
		}

		$rs = $this->con->select($strReq);
		return $this->fileRecord($rs);
	}

	/**
	Search into media db (only).

	@param	query		<b>string</b>		Search query
	@return boolean 	true or false if nothing found
	*/
	public function searchMedia($query)
	{
		if ($query == '') {
			return false;
		}

		$strReq =
		'SELECT media_file, media_id, media_path, media_title, media_meta, media_dt, '.
		'media_creadt, media_upddt, media_private, user_id '.
		'FROM '.$this->table.' '.
		"WHERE media_path = '".$this->path."' ".
		"AND (media_title LIKE '%".$this->con->escape($query)."%' ".
		"	OR media_file LIKE '%".$this->con->escape($query)."%' ".
		"	OR media_meta LIKE '<Description>%".$this->con->escape($query)."%</Description>')";

		if (!$this->core->auth->check('media_admin',$this->core->blog->id))
		{
			$strReq .= 'AND (media_private <> 1 ';

			if ($this->core->auth->userID()) {
				$strReq .= "OR user_id = '".$this->con->escape($this->core->auth->userID())."'";
			}
			$strReq .= ') ';
		}

		$rs = $this->con->select($strReq);

		$this->dir = array('dirs' => array(),'files' => array());
		$f_res = array();
		while ($rs->fetch())
		{
			$fr = $this->fileRecord($rs);
			if ($fr) {
				$f_res[] = $fr;
			}
		}
		$this->dir['files'] = $f_res;

		try {
			usort($this->dir['files'],array($this,'sortFileHandler'));
		} catch (Exception $e) {}

		return (count($f_res) > 0 ? true : false);
	}

	/**
	Returns media items attached to a blog post. Result is an array containing
	fileItems objects.

	@param	post_id		<b>integer</b>		Post ID
	@param	media_id	<b>integer</b>		Optionnal media ID
	@param	link_type	<b>string</b>		Optionnal link type
	@return	<b>array</b> Array of fileItems
	*/
	public function getPostMedia($post_id,$media_id=null,$link_type=null)
	{
		$params = array(
			'post_id' => $post_id,
			'media_path' => $this->path
		);
		if ($media_id) {
			$params['media_id'] = (integer) $media_id;
		}
		if ($link_type) {
			$params['link_type'] = $link_type;
		}
		$rs = $this->postmedia->getPostMedia($params);

		$res = array();

		while ($rs->fetch()) {
			$f = $this->fileRecord($rs);
			if ($f !== null) {
				$res[] = $f;
			}
		}

		return $res;
	}

	/**
	@deprecated since version 2.4
	@see dcPostMedia::addPostMedia
	*/
	public function addPostMedia($post_id,$media_id,$link_type='attachment')
	{
		$this->postmedia->addPostMedia($post_id,$media_id,$link_type);
	}

	/**
	@deprecated since version 2.4
	@see dcPostMedia::removePostMedia
	*/
	public function removePostMedia($post_id,$media_id,$link_type='attachment')
	{
		$this->postmedia->removePostMedia($post_id,$media_id,$link_type);
	}

	/**
	Rebuilds database items collection. Optional <var>$pwd</var> parameter is
	the path where to start rebuild.

	@param	pwd		<b>string</b>		Directory to rebuild
	*/
	public function rebuild($pwd='')
	{
		if (!$this->core->auth->isSuperAdmin()) {
			throw new Exception(__('You are not a super administrator.'));
		}

		$this->chdir($pwd);
		parent::getDir();

		$dir = $this->dir;

		foreach ($dir['dirs'] as $d) {
			if (!$d->parent) {
				$this->rebuild($d->relname,false);
			}
		}

		foreach ($dir['files'] as $f) {
			$this->chdir(dirname($f->relname));
			$this->createFile($f->basename);
		}

		$this->rebuildDB($pwd);
	}

	protected function rebuildDB($pwd)
	{
		$media_dir = $pwd ?: '.';

		$strReq =
		'SELECT media_file, media_id '.
		'FROM '.$this->table.' '.
		"WHERE media_path = '".$this->path."' ".
		"AND media_dir = '".$this->con->escape($media_dir)."' ";

		$rs = $this->con->select($strReq);

		$delReq = 'DELETE FROM '.$this->table.' '.
				'WHERE media_id IN (%s) ';
		$del_ids = array();

		while ($rs->fetch())
		{
			if (!is_file($this->root.'/'.$rs->media_file)) {
				$del_ids[] = (integer) $rs->media_id;
			}
		}

		if (!empty($del_ids)) {
			$this->con->execute(sprintf($delReq,implode(',',$del_ids)));
		}
	}

	public function makeDir($d)
	{
		$d = files::tidyFileName($d);
		parent::makeDir($d);
	}

	/**
	Creates or updates a file in database. Returns new media ID or false if
	file does not exist.

	@param	name		<b>string</b>		File name (relative to working directory)
	@param	title	<b>string</b>		File title
	@param	private	<b>boolean</b>		File is private
	@param	dt		<b>string</b>		File date
	@return	<b>integer</b> New media ID
	*/
	public function createFile($name,$title=null,$private=false,$dt=null,$force=true)
	{
		if (!$this->core->auth->check('media,media_admin',$this->core->blog->id)) {
			throw new Exception(__('Permission denied.'));
		}

		$file = $this->pwd.'/'.$name;
		if (!file_exists($file)) {
			return false;
		}

		$media_file = $this->relpwd ? path::clean($this->relpwd.'/'.$name) : path::clean($name);
		$media_type = files::getMimeType($name);

		$cur = $this->con->openCursor($this->table);

		$strReq = 'SELECT media_id '.
				'FROM '.$this->table.' '.
				"WHERE media_path = '".$this->con->escape($this->path)."' ".
				"AND media_file = '".$this->con->escape($media_file)."' ";

		$rs = $this->con->select($strReq);

		if ($rs->isEmpty())
		{
			$this->con->writeLock($this->table);
			try
			{
				$rs = $this->con->select('SELECT MAX(media_id) FROM '.$this->table);
				$media_id = (integer) $rs->f(0) + 1;

				$cur->media_id = $media_id;
				$cur->user_id = (string) $this->core->auth->userID();
				$cur->media_path = (string) $this->path;
				$cur->media_file = (string) $media_file;
				$cur->media_dir = (string) dirname($media_file);
				$cur->media_creadt = date('Y-m-d H:i:s');
				$cur->media_upddt = date('Y-m-d H:i:s');

				$cur->media_title = !$title ? (string) $name : (string) $title;
				$cur->media_private = (integer) (boolean) $private;

				if ($dt) {
					$cur->media_dt = (string) $dt;
				} else {
					$cur->media_dt = strftime('%Y-%m-%d %H:%M:%S',filemtime($file));
				}

				try {
					$cur->insert();
				} catch (Exception $e) {
					@unlink($name);
					throw $e;
				}
				$this->con->unlock();
			}
			catch (Exception $e)
			{
				$this->con->unlock();
				throw $e;
			}
		}
		else
		{
			$media_id = (integer) $rs->media_id;

			$cur->media_upddt = date('Y-m-d H:i:s');

			$cur->update('WHERE media_id = '.$media_id);
		}

		$this->callFileHandler($media_type,'create',$cur,$name,$media_id,$force);

		return $media_id;
	}

	/**
	Updates a file in database.

	@param	file		<b>fileItem</b>	Current fileItem object
	@param	newFile	<b>fileItem</b>	New fileItem object
	*/
	public function updateFile($file,$newFile)
	{
		if (!$this->core->auth->check('media,media_admin',$this->core->blog->id)) {
			throw new Exception(__('Permission denied.'));
		}

		$id = (integer) $file->media_id;

		if (!$id) {
			throw new Exception('No file ID');
		}

		if (!$this->core->auth->check('media_admin',$this->core->blog->id)
		&& $this->core->auth->userID() != $file->media_user) {
			throw new Exception(__('You are not the file owner.'));
		}

		$cur = $this->con->openCursor($this->table);

		# We need to tidy newFile basename. If dir isn't empty, concat to basename
		$newFile->relname = files::tidyFileName($newFile->basename);
		if ($newFile->dir) {
			$newFile->relname = $newFile->dir.'/'.$newFile->relname;
		}

		if ($file->relname != $newFile->relname) {
			$newFile->file = $this->root.'/'.$newFile->relname;

			if ($this->isFileExclude($newFile->relname)) {
				throw new Exception(__('This file is not allowed.'));
			}

			if (file_exists($newFile->file)) {
				throw new Exception(__('New file already exists.'));
			}

			$this->moveFile($file->relname,$newFile->relname);

			$cur->media_file = (string) $newFile->relname;
			$cur->media_dir = (string) dirname($newFile->relname);
		}

		$cur->media_title = (string) $newFile->media_title;
		$cur->media_dt = (string) $newFile->media_dtstr;
		$cur->media_upddt = date('Y-m-d H:i:s');
		$cur->media_private = (integer) $newFile->media_priv;

		$cur->update('WHERE media_id = '.$id);

		$this->callFileHandler($file->type,'update',$file,$newFile);
	}

	/**
	Uploads a file.

	@param	tmp		<b>string</b>		Full path of temporary uploaded file
	@param	name		<b>string</b>		File name (relative to working directory)
	@param	title	<b>string</b>		File title
	@param	private	<b>boolean</b>		File is private
	*/
	public function uploadFile($tmp,$name,$title=null,$private=false,$overwrite=false)
	{
		if (!$this->core->auth->check('media,media_admin',$this->core->blog->id)) {
			throw new Exception(__('Permission denied.'));
		}

		$name = files::tidyFileName($name);

		parent::uploadFile($tmp,$name,$overwrite);

		return $this->createFile($name,$title,$private);
	}

	/**
	Creates a file from binary content.

	@param	name		<b>string</b>		File name (relative to working directory)
	@param	bits		<b>string</b>		Binary file content
	*/
	public function uploadBits($name,$bits)
	{
		if (!$this->core->auth->check('media,media_admin',$this->core->blog->id)) {
			throw new Exception(__('Permission denied.'));
		}

		$name = files::tidyFileName($name);

		parent::uploadBits($name,$bits);

		return $this->createFile($name,null,null);
	}

	/**
	Removes a file.

	@param	f		<b>fileItem</b>	fileItem object
	*/
	public function removeFile($f)
	{
		if (!$this->core->auth->check('media,media_admin',$this->core->blog->id)) {
			throw new Exception(__('Permission denied.'));
		}

		$media_file = $this->relpwd ? path::clean($this->relpwd.'/'.$f) : path::clean($f);

		$strReq = 'DELETE FROM '.$this->table.' '.
				"WHERE media_path = '".$this->con->escape($this->path)."' ".
				"AND media_file = '".$this->con->escape($media_file)."' ";

		if (!$this->core->auth->check('media_admin',$this->core->blog->id))
		{
			$strReq .= "AND user_id = '".$this->con->escape($this->core->auth->userID())."'";
		}

		$this->con->execute($strReq);

		if ($this->con->changes() == 0) {
			throw new Exception(__('File does not exist in the database.'));
		}

		parent::removeFile($f);

		$this->callFileHandler(files::getMimeType($media_file),'remove',$f);
	}

	/**
	* Root directories
	*
	* Returns an array of directory under {@link $root} directory.
	*
	* @uses fileItem
	* @return array
	*/
	public function getDBDirs()
	{
		$media_dir = $this->relpwd ?: '.';

		$strReq =
		'SELECT distinct media_dir '.
		'FROM '.$this->table.' '.
		"WHERE media_path = '".$this->path."'";
		$rs = $this->con->select($strReq);
		while ($rs->fetch()) {
			if (is_dir($this->root.'/'.$rs->media_dir))
				$dir[] = ($rs->media_dir == '.' ? '' : $rs->media_dir);
		}

		return $dir;
	}

	/**
	Extract zip file in current location

	@param	f		<b>fileRecord</b>	fileRecord object
	*/
	public function inflateZipFile($f,$create_dir=true)
	{
		$zip = new fileUnzip($f->file);
		$zip->setExcludePattern($this->exclude_pattern);
		$list = $zip->getList(false,'#(^|/)(__MACOSX|\.svn|\.DS_Store|\.directory|Thumbs\.db)(/|$)#');

		if ($create_dir)
		{
			$zip_root_dir = $zip->getRootDir();
			if ($zip_root_dir != false) {
				$destination = $zip_root_dir;
				$target = $f->dir;
			} else {
				$destination = preg_replace('/\.([^.]+)$/','',$f->basename);
				$target = $f->dir.'/'.$destination;
			}

			if (is_dir($f->dir.'/'.$destination)) {
				throw new Exception(sprintf(__('Extract destination directory %s already exists.'),dirname($f->relname).'/'.$destination));
			}
		}
		else
		{
			$target = $f->dir;
			$destination = '';
		}

		$zip->unzipAll($target);
		$zip->close();

		// Clean-up all extracted filenames
		$clean = function ($name) {
			$n = text::deaccent($name);
			$n = preg_replace('/^[.]/u','',$n);
			return preg_replace('/[^A-Za-z0-9._\-\/]/u','_',$n);
		};
		foreach ($list as $zk => $zv) {
			// Check if extracted file exists
			$zf = $target.'/'.$zk;
			if (!$zv['is_dir'] && file_exists($zf)) {
				$zt = $clean($zf);
				if ($zt != $zf) {
					rename($zf,$zt);
				}
			}
		}

		return dirname($f->relname).'/'.$destination;
	}

	/**
	Returns zip file content

	@param	f		<b>fileRecord</b>	fileRecord object
	@return <b>array</b>
	*/
	public function getZipContent($f)
	{
		$zip = new fileUnzip($f->file);
		$list = $zip->getList(false,'#(^|/)(__MACOSX|\.svn|\.DS_Store|\.directory|Thumbs\.db)(/|$)#');
		$zip->close();
		return $list;
	}

	/**
	Calls file handlers registered for recreate event

	@param	f	<b>fileItem</b>	fileItem object
	*/
	public function mediaFireRecreateEvent($f)
	{
		$media_type = files::getMimeType($f->basename);
		$this->callFileHandler($media_type,'recreate',null,$f->basename); // Args list to be completed as necessary (Franck)
	}

	/* Image handlers
	------------------------------------------------------- */
	public function imageThumbCreate($cur,$f,$force=true)
	{
		$file = $this->pwd.'/'.$f;

		if (!file_exists($file)) {
			return false;
		}

		$p = path::info($file);
		$alpha = ($p['extension'] == 'png') || ($p['extension'] == 'PNG');
		$thumb = sprintf(($alpha ? $this->thumb_tp_alpha : $this->thumb_tp),$p['dirname'],$p['base'],'%s');

		try
		{
			$img = new imageTools();
			$img->loadImage($file);

			$w = $img->getW();
			$h = $img->getH();

			if ($force) $this->imageThumbRemove($f);

			foreach ($this->thumb_sizes as $suffix => $s) {
				$thumb_file = sprintf($thumb,$suffix);
				if (!file_exists($thumb_file) && $s[0] > 0 &&
					($suffix == 'sq' || $w > $s[0] || $h > $s[0]))
				{
					$rate = ($s[0] < 100 ? 95 : ($s[0] < 600 ? 90 : 85));
					$img->resize($s[0],$s[0],$s[1]);
					$img->output(($alpha ? 'png' : 'jpeg'),$thumb_file,$rate);
					$img->loadImage($file);
				}
			}
			$img->close();
		}
		catch (Exception $e)
		{
			if ($cur === null) { # Called only if cursor is null (public call)
				throw $e;
			}
		}
	}

	protected function imageThumbUpdate($file,$newFile)
	{
		if ($file->relname != $newFile->relname)
		{
			$p = path::info($file->relname);
			$alpha = ($p['extension'] == 'png') || ($p['extension'] == 'PNG');
			$thumb_old = sprintf(($alpha ? $this->thumb_tp_alpha : $this->thumb_tp),$p['dirname'],$p['base'],'%s');

			$p = path::info($newFile->relname);
			$alpha = ($p['extension'] == 'png') || ($p['extension'] == 'PNG');
			$thumb_new = sprintf(($alpha ? $this->thumb_tp_alpha : $this->thumb_tp),$p['dirname'],$p['base'],'%s');

			foreach ($this->thumb_sizes as $suffix => $s) {
				try {
					parent::moveFile(sprintf($thumb_old,$suffix),sprintf($thumb_new,$suffix));
				} catch (Exception $e) {}
			}
		}
	}

	public function imageThumbRemove($f)
	{
		$p = path::info($f);
		$alpha = ($p['extension'] == 'png') || ($p['extension'] == 'PNG');
		$thumb = sprintf(($alpha ? $this->thumb_tp_alpha : $this->thumb_tp),'',$p['base'],'%s');

		foreach ($this->thumb_sizes as $suffix => $s) {
			try {
				parent::removeFile(sprintf($thumb,$suffix));
			} catch (Exception $e) {}
		}
	}

	protected function imageMetaCreate($cur,$f,$id)
	{
		$file = $this->pwd.'/'.$f;

		if (!file_exists($file)) {
			return false;
		}

		$xml = new xmlTag('meta');
		$meta = imageMeta::readMeta($file);
		$xml->insertNode($meta);

		$c = $this->core->con->openCursor($this->table);
		$c->media_meta = $xml->toXML();

		if ($cur->media_title !== null && $cur->media_title == basename($cur->media_file))
		{
			if ($meta['Title']) {
				$c->media_title = $meta['Title'];
			}
		}

		if ($meta['DateTimeOriginal'] && $cur->media_dt === '')
		{
			# We set picture time to user timezone
			$media_ts = strtotime($meta['DateTimeOriginal']);
			if ($media_ts !== false) {
				$o = dt::getTimeOffset($this->core->auth->getInfo('user_tz'),$media_ts);
				$c->media_dt = dt::str('%Y-%m-%d %H:%M:%S',$media_ts+$o);
			}
		}

		$c->update('WHERE media_id = '.$id);
	}

	/**
	Returns HTML code for audio player (HTML5 and if possible fallback Flash player)

	@param  type		<b>string</b> 		audio mime type
	@param	url			<b>string</b>		audio URL to play
	@param	player		<b>string</b>		Player URL (flash player fallback)
	@param	args		<b>array</b>		Player parameters (flash player fallback)
	@param  fallback 	<b>boolean</b>		Include Flash player fallback
	@param 	preload		<b>boolean</b>		Add preload="auto" attribute if true, else preload="none"
	@return	<b>string</b>
	*/
	public static function audioPlayer($type,$url,$player=null,$args=null,$fallback=true,$preload=true)
	{
		$audio =
			'<audio controls preload="'.($preload ? 'auto' : 'none').'">'.
			'<source src="'.$url.'">';

		if ($fallback && $type == 'audio/mpeg3') {
			// Include Flash player fallback
			if (!$player) {
				$player = 'player_mp3.swf';
			}

			if (!is_array($args))
			{
				$args = array(
					'showvolume' => 1,
					'loadingcolor' => 'ff9900',
					'bgcolor1' => 'eeeeee',
					'bgcolor2' => 'cccccc',
					'buttoncolor' => '0066cc',
					'buttonovercolor' => 'ff9900',
					'slidercolor1' => 'cccccc',
					'slidercolor2' => '999999',
					'sliderovercolor' => '0066cc'
				);
			}

			$args['mp3'] = $url;

			if (empty($args['width'])) {
				$args['width'] = 200;
			}
			if (empty($args['height'])) {
				$args['height'] = 20;
			}

			$vars = array();
			foreach ($args as $k => $v) {
				$vars[] = $k.'='.$v;
			}

			$audio .=
				'<object type="application/x-shockwave-flash" '.
				'data="'.$player.'" '.
				'width="'.$args['width'].'" height="'.$args['height'].'">'.
				'<param name="movie" value="'.$player.'" />'.
				'<param name="wmode" value="transparent" />'.
				'<param name="FlashVars" value="'.implode('&amp;',$vars).'" />'.
				__('Embedded Audio Player').
				'</object>';
		}

		$audio .=
			'</audio>';

		return $audio;
	}

	/**
	Returns HTML code for video player (HTML5 and if possible fallback Flash player)

	@param  type		<b>string</b> 		video mime type
	@param	url			<b>string</b>		video URL to play
	@param	player		<b>string</b>		Player URL (flash player fallback)
	@param	args		<b>array</b>		Player parameters (flash player fallback)
	@param  fallback 	<b>boolean</b>		Include Flash player fallback (if not .flv)
	@param 	preload		<b>boolean</b>		Add preload="auto" attribute if true, else preload="none"
	@return	<b>string</b>
	*/
	public static function videoPlayer($type,$url,$player=null,$args=null,$fallback=true,$preload=true)
	{
		$video = '';

		// Cope with width and height, if given
		$width = 400;
		$height = 300;
		if (is_array($args)) {
			if (!empty($args['width']) && $args['width']) {
				$width = (int) $args['width'];
			}
			if (!empty($args['height']) && $args['height']) {
				$height = (int) $args['height'];
			}
		}

		if ($type != 'video/x-flv') {
			$video =
				'<video controls preload="'.($preload ? 'auto' : 'none').'"'.
				($width ? ' width="'.$width.'"' : '').
				($height ? ' height="'.$height.'"' : '').'>'.
				'<source src="'.$url.'">';
		}

		if ($type == 'video/x-flv' || ($fallback && ($type == 'video/mp4' || $type == 'video/x-m4v')))
		{
			// Include Flash player fallback
			if (!$player) {
				$player = 'player_flv.swf';
			}

			if (!is_array($args))
			{
				$args = array(
					'margin' => 1,
					'showvolume' => 1,
					'showtime' => 1,
					'showfullscreen' => 1,
					'buttonovercolor' => 'ff9900',
					'slidercolor1' => 'cccccc',
					'slidercolor2' => '999999',
					'sliderovercolor' => '0066cc'
				);
			}

			$args['flv'] = $url;

			if (empty($args['width'])) {
				$args['width'] = 400;
			}
			if (empty($args['height'])) {
				$args['height'] = 300;
			}

			$vars = array();
			foreach ($args as $k => $v) {
				$vars[] = $k.'='.$v;
			}

			$video .=
				'<object type="application/x-shockwave-flash" '.
				'data="'.$player.'" '.
				'width="'.$args['width'].'" height="'.$args['height'].'">'.
				'<param name="movie" value="'.$player.'" />'.
				'<param name="wmode" value="transparent" />'.
				'<param name="allowFullScreen" value="true" />'.
				'<param name="FlashVars" value="'.implode('&amp;',$vars).'" />'.
				__('Embedded Video Player').
				'</object>';
		}

		if ($type != 'video/x-flv') {
			$video .=
				'</video>';
		}

		return $video;
	}

	/**
	Returns HTML code for MP3 player

	@param	url			<b>string</b>		MP3 URL to play
	@param	player		<b>string</b>		Player URL
	@param	args		<b>array</b>		Player parameters
	@param  fallback 	<b>boolean</b>		Include Flash player fallback
	@param 	preload		<b>boolean</b>		Add preload="auto" attribute if true, else preload="none"
	@return	<b>string</b>
	*/
	public static function mp3player($url,$player=null,$args=null,$fallback=true,$preload=true)
	{
		if (!$player) {
			$player = 'player_mp3.swf';
		}

		if (!is_array($args))
		{
			$args = array(
				'showvolume' => 1,
				'loadingcolor' => 'ff9900',
				'bgcolor1' => 'eeeeee',
				'bgcolor2' => 'cccccc',
				'buttoncolor' => '0066cc',
				'buttonovercolor' => 'ff9900',
				'slidercolor1' => 'cccccc',
				'slidercolor2' => '999999',
				'sliderovercolor' => '0066cc'
			);
		}

		$args['mp3'] = $url;

		if (empty($args['width'])) {
			$args['width'] = 200;
		}
		if (empty($args['height'])) {
			$args['height'] = 20;
		}

		$vars = array();
		foreach ($args as $k => $v) {
			$vars[] = $k.'='.$v;
		}

		return
		'<audio controls preload="'.($preload ? 'auto' : 'none').'">'.
		'<source src="'.$url.'" type="audio/mpeg">'.
		($fallback ?
			'<object type="application/x-shockwave-flash" '.
			'data="'.$player.'" '.
			'width="'.$args['width'].'" height="'.$args['height'].'">'.
			'<param name="movie" value="'.$player.'" />'.
			'<param name="wmode" value="transparent" />'.
			'<param name="FlashVars" value="'.implode('&amp;',$vars).'" />'.
			__('Embedded Audio Player').
			'</object>' : '').
		'</audio>';
	}

	/**
	Returns HTML code for FLV player

	@param	url		<b>string</b>		FLV URL to play
	@param	player	<b>string</b>		Player URL
	@param	args		<b>array</b>		Player parameters
	@return	<b>string</b>
	*/
	public static function flvplayer($url,$player=null,$args=null)
	{
		if (!$player) {
			$player = 'player_flv.swf';
		}

		if (!is_array($args))
		{
			$args = array(
				'margin' => 1,
				'showvolume' => 1,
				'showtime' => 1,
				'showfullscreen' => 1,
				'buttonovercolor' => 'ff9900',
				'slidercolor1' => 'cccccc',
				'slidercolor2' => '999999',
				'sliderovercolor' => '0066cc'
			);
		}

		$args['flv'] = $url;

		if (empty($args['width'])) {
			$args['width'] = 400;
		}
		if (empty($args['height'])) {
			$args['height'] = 300;
		}

		$vars = array();
		foreach ($args as $k => $v) {
			$vars[] = $k.'='.$v;
		}

		return
		'<object type="application/x-shockwave-flash" '.
		'data="'.$player.'" '.
		'width="'.$args['width'].'" height="'.$args['height'].'">'.
		'<param name="movie" value="'.$player.'" />'.
		'<param name="wmode" value="transparent" />'.
		'<param name="allowFullScreen" value="true" />'.
		'<param name="FlashVars" value="'.implode('&amp;',$vars).'" />'.
		__('Embedded Video Player').
		'</object>';
	}
}
