<?php
/**
 * phpaga
 *
 * Database functionality.
 *
 * This file contains the necessary routines to connect to a supported
 * RDMBS.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @version $Id$
 *
 * Copyright (c) 2007, Florian Lanthaler <florian@phpaga.net>
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *    * Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 *
 *    * Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in
 *      the documentation and/or other materials provided with the
 *      distribution.
 *
 *    * Neither the name of Florian Lanthaler nor the names of his
 *      contributors may be used to endorse or promote products derived
 *      from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


/**
 * The class PhPagaDbData handles database abstraction. It is a layer
 * between phpaga and the "real" database abstraction. After having
 * changed db abstraction twice in the past, I don't plan to rewrite
 * all of the libraries' calls a third time.
 *
 * Currently this class is based on PDO.
 */

class PhPagaDbData {

    private static $db = false;

    /* Constructor */

    public function __construct() {

        if (!(self::$db instanceof PDO)) {

            try {

                if (strlen(PHPAGA_DB_HOST))

                    self::$db = new PDO(sprintf('%s:host=%s;dbname=%s',
                                                PHPAGA_DB_SYSTEM, PHPAGA_DB_HOST, PHPAGA_DB_NAME),
                                        PHPAGA_DB_LOGIN, PHPAGA_DB_PASSWD);

                else

                    self::$db = new PDO(sprintf('%s:dbname=%s',
                                                PHPAGA_DB_SYSTEM, PHPAGA_DB_NAME),
                                        PHPAGA_DB_LOGIN, PHPAGA_DB_PASSWD);

                self::$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

                if ((PHPAGA_DB_SYSTEM == 'pgsql') && defined("PHPAGA_PGSQL_ENCODING"))
                    self::$db->exec("set client_encoding to '".PHPAGA_PGSQL_ENCODING."'");
                else if (PHPAGA_DB_SYSTEM == 'mysql') {
                    self::$db->exec("SET NAMES utf8;");
                }


            } catch (PDOException $e) {
                self::$db = null;

                print("<h3>phpaga: "._("Unable to connect to the database.")."</h3>");

                if(defined("PHPAGA_ERR_VERBOSE") && (PHPAGA_ERR_VERBOSE == true))
                    die($e->getMessage());
                else
                    die("<p>"._("Enable PHPAGA_ERR_VERBOSE in etc/config.local.php
                                 and set it to true in order to see a more descriptive error
                                 message"));
                die();
            }
        }
    }

    /* Execute a query and return all resulting records in an array.
     * If $values is not null, execute the query via a prepared
     * statement. */

    public function queryAll($query, $values = null, $offset = null, $limit = null) {

        $data = array();

        if (is_null($values))
            $values = array();

        if (!is_array($values))
            $values = array($values);

        switch(PHPAGA_DB_SYSTEM) {

            case 'pgsql':

                if (is_numeric($offset)) {
                    $query .= ' OFFSET ? ';
                    $values[] = (int)$offset;
                }

                if (is_numeric($limit)) {
                    $query .= ' LIMIT ? ';
                    $values[] = (int)$limit;
                }

                break;

            case 'mysql':

                if (is_numeric($offset) && is_numeric($limit)) {

                    /* For some reason, under some circumstances
                    either PHP/PDO or MySQL does not seem to handle
                    ints in prepared statements correctly - ints get
                    quoted, and MySQL balks when LIMIT parameters are
                    quoted, hence we cannot pass them as values to the
                    prepared statement. */

                    $query .= sprintf(' LIMIT %d,%d ', (int)$offset, (int)$limit);

                } else if (is_numeric($limit)) {
                    $query .= sprintf(' LIMIT %d ', (int)$limit);
                }

                break;

            default:
                /* Limit/Offset not supported */
                break;
        }

        try {

            if (count($values)) {
                $sth = self::$db->prepare($query);
                $sth->execute($values);
                $data = $sth->fetchAll();

            } else {
                $stmt = self::$db->query($query);
                $data = $stmt->fetchAll();
            }

        } catch (PDOException $e) {
            return PhPagaError::raiseErrorByCode(PHPAGA_ERR_DB_EXEC_QUERY, $e->getMessage());
        }

        return $data;
    }

    /* Execute a query and return the first resulting record as an
     * array. If $values is not null, execute the query via a prepared
     * statement. */

    public function queryRow($query, $values = null) {

        $data = array();

        if (is_null($values))
            $values = array();

        if (!is_array($values))
            $values = array($values);

        try {

            if (count($values)) {
                $sth = self::$db->prepare($query);
                $sth->execute($values);
                $data = $sth->fetch();
            } else {
                $stmt = self::$db->query($query);
                $data = $stmt->fetch();
            }

        } catch (PDOException $e) {
            return PhPagaError::raiseErrorByCode(PHPAGA_ERR_DB_EXEC_QUERY, $e->getMessage());
        }

        return $data;
    }


    /* Execute a query and return the first resulting value of the
     * first record. If $values is not null, execute the query via a
     * prepared statement. */

    public function queryOneVal($query, $values = null) {

        $r = array();
        $error = null;

        if (is_null($values))
            $values = array();

        if (!is_array($values))
            $values = array($values);

        try {

            if (count($values)) {
                $sth = self::$db->prepare($query);
                $sth->execute($values);
                $data = $sth->fetchColumn();
            } else {
                $stmt = self::$db->query($query);
                $data = $stmt->fetchColumn();
            }

        } catch (PDOException $e) {
            return PhPagaError::raiseErrorByCode(PHPAGA_ERR_DB_EXEC_QUERY, $e->getMessage());
        }

        if ($data === false) {
            return PhPagaError::raiseErrorByCode(PHPAGA_ERR_DB_FETCHVALUE);
        }

        return $data;
    }

    /* Fetch the next Id from a sequence. */

    public function nextId($sequence) {

        $id = null;

        $sequence .= '_seq';

        switch(PHPAGA_DB_SYSTEM) {
            case 'mysql':

                try {
                    self::$db->exec('UPDATE '.$sequence.' SET id = LAST_INSERT_ID(id + 1)');
                    $id = self::$db->lastInsertId();
                    return $id;
                } catch (PDOException $e) {
                    phpaga_header();
                    $r = PhPagaError::raiseErrorByCode(PHPAGA_ERR_DB_EXEC_QUERY, $e->getMessage());
                    $r->printMessage();
                    phpaga_footer();
                    die();
                }

                break;

            default:
                try {
                    $sth = self::$db->prepare('SELECT NEXTVAL(?)');
                    $sth->execute(array($sequence));
                    $id = $sth->fetchColumn();
                    return $id;
                } catch (PDOException $e) {
                    phpaga_header();
                    $r = PhPagaError::raiseErrorByCode(PHPAGA_ERR_DB_EXEC_QUERY, $e->getMessage());
                    $r->printMessage();
                    phpaga_footer();
                    die();
                }

                break;
        }

        return $id;
    }

    /* Escape a string according to the underlying rdbms */

    public function escape($s) {
        $q = self::$db->quote($s);
        $q = phpaga_substr($q, 1, -1);
        return $q;
    }

    /* Execute a prepared statement. Return true on success. */

    public function execute($query, $values) {

        if (is_null($values))
            $values = array();

        if (!is_array($values))
            $values = array($values);

        try {

            $sth = self::$db->prepare($query);
            $sth->execute($values);

        } catch (PDOException $e) {
            return PhPagaError::raiseErrorByCode(PHPAGA_ERR_DB_EXEC_QUERY, $e->getMessage());
        }

        if ($sth->errorCode() != '00000') {

            if (preg_match('/^23/', $sth->errorCode()))
                return PhPagaError::raiseErrorByCode(PHPAGA_ERR_DB_DUPLICATE);
            else {
                $t = $sth->errorInfo();
                return PhPagaError::raiseErrorByCode(PHPAGA_ERR_DB_EXEC_QUERY, $t[2]);
            }
        }

        return true;
    }

    /* Execute one or more statements. Return true on success. */

    public function exec($query) {

        try {
            $result = self::$db->exec($query);
        } catch (PDOException $e) {
            return PhPagaError::raiseErrorByCode(PHPAGA_ERR_DB_EXEC_QUERY, $e->getMessage());
        }

        if (self::$db->errorCode() != '00000') {
            $t = $result->errorInfo();
            return PhPagaError::raiseErrorByCode(PHPAGA_ERR_DB_EXEC_QUERY, $t[2]);
        }

        return true;
    }


    /* Insert a record. */

    public function insert($table, $data) {

        if (!is_array($data) || !count($data))
            return PhPagaError::raiseError(_('No data to insert.'));

        $v = array_fill(0, count($data), '?');

        $q = sprintf('INSERT INTO %s (%s) VALUES (%s)',
                     $this->escape($table),
                     implode(', ', array_keys($data)),
                     implode(', ', $v));

        $result = $this->execute($q, array_values($data));

        if (PhPagaError::isError($result))
            return $result;

        return true;
    }

    /* Insert multiple records. */

    public function insertMultiple($table, $fieldnames, $data) {

        if (!is_array($fieldnames) || !count($fieldnames))
            return PhPagaError::raiseError(_('No field names specified.'));

        if (!is_array($data) || !count($data))
            return PhPagaError::raiseError(_('No data to insert.'));

        $v = array_fill(0, count($fieldnames), '?');

        $q = sprintf('INSERT INTO %s (%s) VALUES (%s)',
                     $this->escape($table),
                     implode(', ', $fieldnames),
                     implode(', ', $v));

        try {

            $sth = self::$db->prepare($q);

            foreach ($data as $dv) {

                $sth->execute($dv);

                if ($sth->errorCode() != '00000') {

                    if (preg_match('/^23/', $sth->errorCode()))
                        return PhPagaError::raiseErrorByCode(PHPAGA_ERR_DB_DUPLICATE);
                    else {
                        $t = $sth->errorInfo();
                        return PhPagaError::raiseErrorByCode(PHPAGA_ERR_DB_EXEC_QUERY, $t[2]);
                    }
                }
            }

        } catch (PDOException $e) {
            return PhPagaError::raiseErrorByCode(PHPAGA_ERR_DB_EXEC_QUERY, $e->getMessage());
        }

        return true;
    }

    /* Update a record. */

    public function update($table, $data, $whereclause, $wherevalues) {

        if (!is_array($data) || !count($data))
            return PhPagaError::raiseError(_('No data to update.'));

        $u = array();
        $v = array();

        foreach ($data as $field => $value)
            $u[] = sprintf('%s = ?', $this->escape($field));

        $q = sprintf('UPDATE %s SET %s WHERE %s',
                     $this->escape($table),
                     implode(', ', $u),
                     $whereclause);

        $v = array_values($data);

        if (is_array($wherevalues) && count($wherevalues))
            foreach ($wherevalues as $w)
                $v[] = $w;
        else if (strlen($wherevalues))
            $v[] = $wherevalues;

        $result = $this->execute($q, $v);

        if (PhPagaError::isError($result))
            return $result;

        return true;
    }

    /* Start a transaction */

    public function beginTransaction() {

        switch(PHPAGA_DB_SYSTEM) {

            case 'pgsql':
                $r = $this->execute('BEGIN TRANSACTION;', null);
                if (PhPagaError::isError($r))
                    return PhPagaError::raiseErrorByCode(PHPAGA_ERR_DB_TRANSACTBEGIN);
                break;

            case 'mysql':
                $r = $this->execute('BEGIN;', null);
                if (PhPagaError::isError($r))
                    return PhPagaError::raiseErrorByCode(PHPAGA_ERR_DB_TRANSACTBEGIN);
                break;

            default:
                break;
        }

        return true;
    }

    /* Roll a transaction back */

    public function rollback() {

        $r = $this->execute('ROLLBACK;', null);
        if (PhPagaError::isError($r))
            return PhPagaError::raiseErrorByCode(PHPAGA_ERR_DB_TRANSACTROLLBACK);

        return true;
    }

    /* Commit a transaction */

    public function commit() {

        $r = $this->execute('COMMIT;', null);
        if (PhPagaError::isError($r))
            return PhPagaError::raiseErrorByCode(PHPAGA_ERR_DB_TRANSACTCOMMIT);

        return true;
    }

}

?>
