<?php
/**
 * phpaga
 *
 * Database functionality.
 *
 * This file contains the necessary routines to connect to a supported
 * RDMBS.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @version $Id: db.php 765 2006-12-08 00:34:04Z florian $
 *
 * Copyright (c) 2002, 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.
 */

/**
 * Connects to the database server and selects the database. All
 * connection parameters (login, passwd host, database) are defined
 * in the config file.
 *
 * @param string $link    Returns the link to the selected database.
 *
 * @return int   $result  A result code.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.1
 */

function phpaga_db_connect (&$db)
{
    /* connect to database server and select database */

    $db = DB::connect(PHPAGA_DSN);

    if (DB::isError($db))
    {
        print("<h3>phpaga: "._("Unable to connect to the database.")."</h2>");

        if(defined("PHPAGA_ERR_VERBOSE") && (PHPAGA_ERR_VERBOSE == true))
            die($db->getDebugInfo());
        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"));
    }
    else
    {
        $db->setFetchMode(DB_FETCHMODE_ASSOC);

        if ((PHPAGA_DB_SYSTEM == 'pgsql') && defined("PHPAGA_PGSQL_ENCODING"))
            $db->query("set client_encoding to '".PHPAGA_PGSQL_ENCODING."'");

        return PHPAGA_ERR_NONE;
    }
}

/**
 * Starts a transaction if the underlying RDBMS can handle transactions.
 *
 * @param int    $db       Database connection handler
 *
 * @return int   $result   A result code.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.3
 */

function phpaga_db_transaction_begin ($db)
{

    switch(PHPAGA_DB_SYSTEM)
    {

        case "pgsql":
            $result = $db->query("BEGIN TRANSACTION;");

            if (DB::isError($result))
                return PHPAGA_ERR_DB_TRANSACTBEGIN;

        default:
            break;
    }

    return PHPAGA_ERR_NONE;
}

/**
 * Makes a rollback if the underlying RDBMS can handle transactions.
 *
 * @param int    $db       Database connection handler
 *
 * @return int   $result   A result code.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.3
 */

function phpaga_db_transaction_rollback ($db)
{

    switch(PHPAGA_DB_SYSTEM)
    {

        case "pgsql":
            $result = $db->query("ROLLBACK;");

            if (DB::isError($result))
                return PHPAGA_ERR_DB_TRANSACTROLLBACK;

        default:
            break;
    }

    return PHPAGA_ERR_NONE;
}

/**
 * Commits a transaction if the underlying RDBMS can handle transactions.
 *
 * @param int    $db       Database connection handler
 *
 * @return int   $result   A result code.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.3
 */

function phpaga_db_transaction_commit ($db)
{

    switch(PHPAGA_DB_SYSTEM)
    {

        case "pgsql":
            $result = $db->query("COMMIT;");

            if (DB::isError($result))
                return PHPAGA_ERR_DB_TRANSACTCOMMIT;

        default:
            break;
    }

    return PHPAGA_ERR_NONE;
}


/**
 * Checks whether the database structure (tables etc) is installed.
 * Currently this done in an extremely simple way: just check if there
 * is _any_ table in this database.
 *
 * @param  bool    &$status  TRUE when structure is installed, FALSE otherwise
 *
 * @return int     $result   A result code.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.3
 */

function phpaga_db_checkstructure (&$status)
{

    $status = null;

    phpaga_db_connect($db);

    switch(PHPAGA_DB_SYSTEM)
    {

        case "pgsql":

            $query =<<<EOQ
                SELECT n.nspname, c.relname,
                CASE c.relkind WHEN 'r' THEN 'table' WHEN 'v' THEN 'view' WHEN 'i' THEN 'index'
                WHEN 'S' THEN 'sequence' WHEN 's' THEN 'special' END as Type,
                u.usename
                FROM pg_catalog.pg_class c
                LEFT JOIN pg_catalog.pg_user u ON u.usesysid = c.relowner
                LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
                WHERE c.relkind IN ('r','')
                AND n.nspname NOT IN ('pg_catalog', 'pg_toast')
                AND pg_catalog.pg_table_is_visible(c.oid)
EOQ;

            $result = $db->query($query);

            if (DB::isError($result)) {

                if(defined("PHPAGA_ERR_VERBOSE") && (PHPAGA_ERR_VERBOSE == true))
                    die("<pre>".$result->getDebugInfo()."</pre>");
                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"));

                return PHPAGA_ERR_DB_EXEC_QUERY;
            }

            if ($result->numRows() == 0)
                $status = false;
            else
                if ($qrow = $result->fetchRow())
                    $status = true;

            return PHPAGA_ERR_NONE;

            break;

        case  "mysql":
            $result = $db->query("show tables");

            if (DB::isError($result))
                return PHPAGA_ERR_DB_EXEC_QUERY;

            if ($result->numRows() < 10)
                $status = false;
            else
                if ($qrow = $result->fetchRow())
                    $status = true;

            return PHPAGA_ERR_NONE;

            break;

        default:
            $status = false;
            return PHPAGA_ERR_NONE;
            break;
    }

}


/**
 * Returns the version of the installed database.
 *
 * @return string $version    Version of the installed database or null on error
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.3
 */

function phpaga_db_getVersion()
{

    phpaga_db_connect($db);

    $result = $db->getOne("SELECT cfg_value FROM config WHERE cfg_key = 'VERSION_DB'");

    if (DB::isError($result))
        return null;

    return $result;
}


/**
 * Returns information about all db upgrades that need to be applied
 * to bring the database structure and core data up-to-date.
 *
 * @return array $dbupgrades  Information about outstanding db upgrades
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.3
 */

function phpaga_db_getUpgradeInfo()
{

    $upgrfiles = array();
    $filename = null;

    $dbupgrade = array("files" => array(),
                        "maxversion" => null);

    if ($dh = dir(PHPAGA_BASEPATH."sql/upgrade/")) {

        $upgrfiles = glob(PHPAGA_BASEPATH."sql/upgrade/*.".PHPAGA_DB_SYSTEM);
        natsort($upgrfiles);

        foreach ($upgrfiles as $filename) {

            $m = array();

            if (preg_match('/^upgrade([0-9]+)\./i', basename($filename), $m)) {

                if (isset($m[1]) && is_numeric($m[1])) {

                    $version = $m[1];

                    if (phpaga_db_getVersion() < $version)
                        $dbupgrade["files"][] = array("name" => $filename,
                                                      "size" => filesize($filename));

                    if ($version > $dbupgrade["maxversion"])
                        $dbupgrade["maxversion"] = $version;
                }
            }
        }
    }

    return $dbupgrade;
}


/**
 * Applies an upgrade file to the database if its version is higher
 * than the version of the installed database.
 *
 * @param string $filename    Full path and name of the file containing upgrade instructions
 * @param string $error       Descriptive error message
 *
 * @return int   $result  A result code.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.3
 */

function phpaga_db_applyUpgrade($filename, &$error)
{

    $content = null;
    $error = null;

    $m = array();

    if (!strlen($filename) || !(preg_match('/^upgrade([0-9]+)\./i', basename($filename), $m)))
        return PHPAGA_ERR_INVALID_FILENAME;

    if (!file_exists($filename) || !is_readable($filename))
        return PHPAGA_ERR_FILE_READ;

    if (!isset($m[1]) || !is_numeric($m[1]))
        return PHPAGA_ERR_INVALID_VERSION;

    $version = $m[1];

    if (phpaga_db_getVersion() >= $version)
        return PHPAGA_ERR_DBVERSION_INSTHIGH;

    phpaga_db_connect($db);

    switch(PHPAGA_DB_SYSTEM)
    {

    case "pgsql":

        $content = file_get_contents($filename);
        if ($content === false)
            return PHPAGA_ERR_FILE_READ;

        if (!strlen($content))
            return PHPAGA_ERR_NONE;

        $content = "BEGIN TRANSACTION; ".$content." COMMIT;";
        $res = @pg_query($db->connection, $content);

        if ($res == false) {
            $error = sprintf(_("\nUpgrade to version: %s\nFile: %s\nError: %s"), $version, $filename, pg_last_error());
            $resr = pg_query($db->connection, "ROLLBACK;");

            return PHPAGA_ERR_DBUPGRADE_STATEMENT;
        }

        break;


    case "mysql":

        // NOTE: MySQL upgrades are NOT run within a transaction.
        //
        // This can lead to trouble if an error occurs somewhere in
        // the upgrade script, leaving the database in a half-upgraded
        // state. In such a case manual intervention is required to
        // bring the upgrade to a good end.

        $command = PHPAGA_MYSQLBIN.
            escapeshellcmd(sprintf(' -u%s -p%s %s ', PHPAGA_DB_LOGIN, PHPAGA_DB_PASSWD, PHPAGA_DB_NAME)).
            '<'.
            escapeshellcmd($filename).
            ' 2>&1';

        $res = null;
        $restring = null;

        $fp = popen($command,"r");

        while (!feof($fp))
            $restring .= fgets($fp, 4096);

        pclose($fp);

        if (preg_match('/^ERROR.*$/', $restring))
            $res = 1;

        if ($res != 0) {
            $error = sprintf(_("\nUpgrade to version: %s\nFile: %s\nError: %s\nConsistency of the database is no longer granted and manual intervention is required."), $version, $filename, $restring);

            return PHPAGA_ERR_DBUPGRADE_STATEMENT;
        }

        break;


    default:
        return PHPAGA_ERR_DB_QUERYNOTYETSUPPORTED;
        break;
    }

    phpaga_config_updateitem("VERSION_DB", $version);

    return PHPAGA_ERR_NONE;

}


/**
 * Installs the database structure (used during first time
 * installation).
 *
 * @param string $error       Descriptive error message
 *
 * @return int   $result  A result code.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.3
 */

function phpaga_db_install(&$error)
{

    $instfiles = array('pgsql' => array('phpaga_tables.pgsql',
                                        'phpaga_data.sql',
                                        'phpaga_constraints.pgsql',
                                        'phpaga_setseqvals.pgsql',
                                        'phpaga_foreign_keys.psql'),

                       'mysql' => array('phpaga_tables.mysql',
                                        'phpaga_data.sql',
                                        'phpaga_sequences.mysql'));

    if (!isset($instfiles[PHPAGA_DB_SYSTEM]) || !count($instfiles[PHPAGA_DB_SYSTEM]))
        return PHPAGA_ERR_INVALID_FILENAME;

    $files = $instfiles[PHPAGA_DB_SYSTEM];

    $content = null;
    $error = null;

    phpaga_db_connect($db);

    switch(PHPAGA_DB_SYSTEM)
    {

        case "pgsql":

            pg_query($db->connection, "BEGIN TRANSACTION;");

            foreach ($files as $filename) {

                $filename = PHPAGA_BASEPATH.'sql/'.$filename;

                $content = file_get_contents($filename);
                if ($content === false)
                    return PHPAGA_ERR_FILE_READ;

                if (strlen($content)) {

                    $res = @pg_query($db->connection, $content);

                    if ($res == false) {
                        $error = sprintf(_("\nDB installation - File: %s\nError: %s"), $filename, pg_last_error());
                        $resr = pg_query($db->connection, "ROLLBACK;");
                        return PHPAGA_ERR_DBINSTALL;
                    }
                }
            }

            pg_query($db->connection, "COMMIT;");

            break;


        case "mysql":

            // NOTE: MySQL upgrades are NOT run within a transaction.
            //
            // This can lead to trouble if an error occurs somewhere in
            // the upgrade script, leaving the database in a half-upgraded
            // state. In such a case manual intervention is required to
            // bring the upgrade to a good end.


            foreach ($files as $filename) {

                $filename = PHPAGA_BASEPATH.'sql/'.$filename;

                $command = PHPAGA_MYSQLBIN.
                    escapeshellcmd(sprintf(' -u%s -p%s %s ', PHPAGA_DB_LOGIN, PHPAGA_DB_PASSWD, PHPAGA_DB_NAME)).
                    '<'.
                    escapeshellcmd($filename).
                    ' 2>&1';

                $res = null;
                $restring = null;

                $fp = popen($command,"r");

                while (!feof($fp))
                    $restring .= fgets($fp, 4096);

                pclose($fp);

                if (preg_match('/^ERROR.*$/', $restring))
                    $res = 1;

                if ($res != 0) {
                    $error = sprintf(_("\nUpgrade to version: %s\nFile: %s\nError: %s\nConsistency of the database is no longer granted and manual intervention is required."), $version, $filename, $restring);

                    return PHPAGA_ERR_DBINSTALL;
                }
            }

            break;


        default:
            return PHPAGA_ERR_DB_QUERYNOTYETSUPPORTED;
            break;
    }

    return PHPAGA_ERR_NONE;

}

?>