<?php
/**
 * @brief Dotclear media manage
 *
 * This class handles Dotclear media items.
 *
 * @package Dotclear
 * @subpackage Core
 *
 * @copyright Olivier Meunier & Association Dotclear
 * @copyright GPL-2.0-only
 */

if (!defined('DC_RC_PATH')) {return;}

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;}

        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;
            }

            $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;
    }

    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>';
    }
}
