<?php
/**
 * ZeroBin
 *
 * a zero-knowledge paste bin
 *
 * @link      http://sebsauvage.net/wiki/doku.php?id=php:zerobin
 * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
 * @license   http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
 * @version   0.21.1
 */

/**
 * zerobin_db
 *
 * Model for DB access, implemented as a singleton.
 */
class zerobin_db extends zerobin_abstract
{
    /**
     * cache for select queries
     *
     * @var array
     */
    private static $_cache = array();

    /**
     * instance of database connection
     *
     * @access private
     * @static
     * @var PDO
     */
    private static $_db;

    /**
     * table prefix
     *
     * @access private
     * @static
     * @var string
     */
    private static $_prefix = '';

    /**
     * database type
     *
     * @access private
     * @static
     * @var string
     */
    private static $_type = '';

    /**
     * get instance of singleton
     *
     * @access public
     * @static
     * @param  array $options
     * @throws Exception
     * @return zerobin_db
     */
    public static function getInstance($options = null)
    {
        // if needed initialize the singleton
        if(!(self::$_instance instanceof zerobin_db)) {
            self::$_instance = new self;
        }

        if (is_array($options))
        {
            // set table prefix if given
            if (array_key_exists('tbl', $options)) self::$_prefix = $options['tbl'];

            // initialize the db connection with new options
            if (
                array_key_exists('dsn', $options) &&
                array_key_exists('usr', $options) &&
                array_key_exists('pwd', $options) &&
                array_key_exists('opt', $options)
            )
            {
                // check if the database contains the required tables
                self::$_type = strtolower(
                    substr($options['dsn'], 0, strpos($options['dsn'], ':'))
                );
                switch(self::$_type)
                {
                    case 'ibm':
                        $sql = 'SELECT tabname FROM SYSCAT.TABLES ';
                        break;
                    case 'informix':
                        $sql = 'SELECT tabname FROM systables ';
                        break;
                    case 'mssql':
                        $sql = "SELECT name FROM sysobjects "
                             . "WHERE type = 'U' ORDER BY name";
                        break;
                    case 'mysql':
                        $sql = 'SHOW TABLES';
                        break;
                    case 'oci':
                        $sql = 'SELECT table_name FROM all_tables';
                        break;
                    case 'pgsql':
                        $sql = "SELECT c.relname AS table_name "
                             . "FROM pg_class c, pg_user u "
                             . "WHERE c.relowner = u.usesysid AND c.relkind = 'r' "
                             . "AND NOT EXISTS (SELECT 1 FROM pg_views WHERE viewname = c.relname) "
                             . "AND c.relname !~ '^(pg_|sql_)' "
                             . "UNION "
                             . "SELECT c.relname AS table_name "
                             . "FROM pg_class c "
                             . "WHERE c.relkind = 'r' "
                             . "AND NOT EXISTS (SELECT 1 FROM pg_views WHERE viewname = c.relname) "
                             . "AND NOT EXISTS (SELECT 1 FROM pg_user WHERE usesysid = c.relowner) "
                             . "AND c.relname !~ '^pg_'";
                        break;
                    case 'sqlite':
                        $sql = "SELECT name FROM sqlite_master WHERE type='table' "
                             . "UNION ALL SELECT name FROM sqlite_temp_master "
                             . "WHERE type='table' ORDER BY name";
                        break;
                    default:
                        throw new Exception(
                            'PDO type ' .
                            self::$_type .
                            ' is currently not supported.',
                            5
                        );
                }
                self::$_db = new PDO(
                    $options['dsn'],
                    $options['usr'],
                    $options['pwd'],
                    $options['opt']
                );
                $statement = self::$_db->query($sql);
                $tables = $statement->fetchAll(PDO::FETCH_COLUMN, 0);

                // create paste table if needed
                if (!array_key_exists(self::$_prefix . 'paste', $tables))
                {
                    self::$_db->exec(
                        'CREATE TABLE ' . self::$_prefix . 'paste ( ' .
                        'dataid CHAR(16), ' .
                        'data TEXT, ' .
                        'postdate INT, ' .
                        'expiredate INT, ' .
                        'opendiscussion INT, ' .
                        'burnafterreading INT, ' .
                        'meta TEXT );'
                    );
                }
                // check if the meta column exists
                else
                {
                    try {
                        self::$_db->exec('SELECT meta FROM ' . self::$_prefix . 'paste LIMIT 1;');
                    } catch (PDOException $e) {
                        if ($e->getCode() == 'HY000')
                        {
                            self::$_db->exec('ALTER TABLE ' . self::$_prefix . 'paste ADD COLUMN meta TEXT;');
                        }
                    }
                }

                // create comment table if needed
                if (!array_key_exists(self::$_prefix . 'comment', $tables))
                {
                    self::$_db->exec(
                        'CREATE TABLE ' . self::$_prefix . 'comment ( ' .
                        'dataid CHAR(16), ' .
                        'pasteid CHAR(16), ' .
                        'parentid CHAR(16), ' .
                        'data TEXT, ' .
                        'nickname VARCHAR(255), ' .
                        'vizhash TEXT, ' .
                        'postdate INT );'
                    );
                }
            }
        }

        return parent::$_instance;
    }

    /**
     * Create a paste.
     *
     * @access public
     * @param  string $pasteid
     * @param  array  $paste
     * @return bool
     */
    public function create($pasteid, $paste)
    {
        if (
            array_key_exists($pasteid, self::$_cache)
        ) {
            if(false !== self::$_cache[$pasteid]) {
                return false;
            } else {
                unset(self::$_cache[$pasteid]);
            }
        }

        $opendiscussion = $burnafterreading = false;
        $meta = $paste['meta'];
        unset($meta['postdate']);
        unset($meta['expire_date']);
        if (array_key_exists('opendiscussion', $paste['meta']))
        {
            $opendiscussion = (bool) $paste['meta']['opendiscussion'];
            unset($meta['opendiscussion']);
        }
        if (array_key_exists('burnafterreading', $paste['meta']))
        {
            $burnafterreading = (bool) $paste['meta']['burnafterreading'];
            unset($meta['burnafterreading']);
        }
        return self::_exec(
            'INSERT INTO ' . self::$_prefix . 'paste VALUES(?,?,?,?,?,?,?)',
            array(
                $pasteid,
                $paste['data'],
                $paste['meta']['postdate'],
                $paste['meta']['expire_date'],
                (int) $opendiscussion,
                (int) $burnafterreading,
                json_encode($meta),
            )
        );
    }

    /**
     * Read a paste.
     *
     * @access public
     * @param  string $pasteid
     * @return stdClass|false
     */
    public function read($pasteid)
    {
        if (
            !array_key_exists($pasteid, self::$_cache)
        ) {
            self::$_cache[$pasteid] = false;
            $paste = self::_select(
                'SELECT * FROM ' . self::$_prefix . 'paste WHERE dataid = ?',
                array($pasteid), true
            );

            if(false !== $paste) {
                // create object
                self::$_cache[$pasteid] = new stdClass;
                self::$_cache[$pasteid]->data = $paste['data'];
                self::$_cache[$pasteid]->meta = json_decode($paste['meta']);
                self::$_cache[$pasteid]->meta->postdate = (int) $paste['postdate'];
                self::$_cache[$pasteid]->meta->expire_date = (int) $paste['expiredate'];
                if (
                    $paste['opendiscussion']
                ) self::$_cache[$pasteid]->meta->opendiscussion = true;
                if (
                    $paste['burnafterreading']
                ) self::$_cache[$pasteid]->meta->burnafterreading = true;
            }
        }

        return self::$_cache[$pasteid];
    }

    /**
     * Delete a paste and its discussion.
     *
     * @access public
     * @param  string $pasteid
     * @return void
     */
    public function delete($pasteid)
    {
        self::_exec(
            'DELETE FROM ' . self::$_prefix . 'paste WHERE dataid = ?',
            array($pasteid)
        );
        self::_exec(
            'DELETE FROM ' . self::$_prefix . 'comment WHERE pasteid = ?',
            array($pasteid)
        );
        if (
            array_key_exists($pasteid, self::$_cache)
        ) unset(self::$_cache[$pasteid]);
    }

    /**
     * Test if a paste exists.
     *
     * @access public
     * @param  string $dataid
     * @return void
     */
    public function exists($pasteid)
    {
        if (
            !array_key_exists($pasteid, self::$_cache)
        ) self::$_cache[$pasteid] = $this->read($pasteid);
        return (bool) self::$_cache[$pasteid];
    }

    /**
     * Create a comment in a paste.
     *
     * @access public
     * @param  string $pasteid
     * @param  string $parentid
     * @param  string $commentid
     * @param  array  $comment
     * @return int|false
     */
    public function createComment($pasteid, $parentid, $commentid, $comment)
    {
        return self::_exec(
            'INSERT INTO ' . self::$_prefix . 'comment VALUES(?,?,?,?,?,?,?)',
            array(
                $commentid,
                $pasteid,
                $parentid,
                $comment['data'],
                $comment['meta']['nickname'],
                $comment['meta']['vizhash'],
                $comment['meta']['postdate'],
            )
        );
    }

    /**
     * Read all comments of paste.
     *
     * @access public
     * @param  string $pasteid
     * @return array
     */
    public function readComments($pasteid)
    {
        $rows = self::_select(
            'SELECT * FROM ' . self::$_prefix . 'comment WHERE pasteid = ?',
            array($pasteid)
        );

        // create object
        $commentTemplate = new stdClass;
        $commentTemplate->meta = new stdClass;

        // create comment list
        $comments = array();
        if (count($rows))
        {
            foreach ($rows as $row)
            {
                $i = (int) $row['postdate'];
                $comments[$i] = clone $commentTemplate;
                $comments[$i]->data = $row['data'];
                $comments[$i]->meta->nickname = $row['nickname'];
                $comments[$i]->meta->vizhash = $row['vizhash'];
                $comments[$i]->meta->postdate = $i;
                $comments[$i]->meta->commentid = $row['dataid'];
                $comments[$i]->meta->parentid = $row['parentid'];
            }
            ksort($comments);
        }
        return $comments;
    }

    /**
     * Test if a comment exists.
     *
     * @access public
     * @param  string $dataid
     * @param  string $parentid
     * @param  string $commentid
     * @return void
     */
    public function existsComment($pasteid, $parentid, $commentid)
    {
        return (bool) self::_select(
            'SELECT dataid FROM ' . self::$_prefix . 'comment ' .
            'WHERE pasteid = ? AND parentid = ? AND dataid = ?',
            array($pasteid, $parentid, $commentid), true
        );
    }

    /**
     * execute a statement
     *
     * @access private
     * @static
     * @param  string $sql
     * @param  array $params
     * @throws PDOException
     * @return array
     */
    private static function _exec($sql, array $params)
    {
        $statement = self::$_db->prepare($sql);
        $result = $statement->execute($params);
        $statement->closeCursor();
        return $result;
    }

    /**
     * run a select statement
     *
     * @access private
     * @static
     * @param  string $sql
     * @param  array $params
     * @param  bool $firstOnly if only the first row should be returned
     * @throws PDOException
     * @return array
     */
    private static function _select($sql, array $params, $firstOnly = false)
    {
        $statement = self::$_db->prepare($sql);
        $statement->execute($params);
        $result = $firstOnly ?
            $statement->fetch(PDO::FETCH_ASSOC) :
            $statement->fetchAll(PDO::FETCH_ASSOC);
        $statement->closeCursor();
        return $result;
    }
}