<?php
/**
 * phpaga
 *
 * Database tools.
 *
 * This file contains the necessary routines to manage database
 * structures (install/upgrade).
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @version $Id: dbtools.php 878 2007-10-31 22:11:00Z florian $
 *
 * 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.
 */


/**
 * 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.
 *
 * @return  bool   $status  TRUE when structure is installed, FALSE otherwise. A PhPagaError object on failure.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.3
 */


/**
 * 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.
 *
 * @return  bool   $status  TRUE when structure is installed, FALSE otherwise. A PhPagaError object on failure.
 *
 * @author Florian Lanthaler <florian@phpaga.net>
 * @since phpaga 0.3
 */

function phpaga_db_checkstructure()
{

    $d = new PhPagaDbData;

    switch(PHPAGA_DB_SYSTEM) {

        case 'pgsql':

            $query = "SELECT COUNT(n.nspname)
                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)";


            $n = $d->queryOneVal($query);

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

            if ($n > 0)
                return true;
            else
                return false;

            break;

        case  'mysql':

            $n = $d->queryAll('show tables');

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

            if (count($n) >= 10)
                return true;
            else
                return false;

            break;

        default:
            return false;
            break;
    }
}


/**
 * 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);

    $d = new PhPagaDbData;

    $v = $d->queryOneVal('SELECT cfg_value FROM config WHERE cfg_key = ?', 'VERSION_DB');

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

    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 ($v < $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 bool  $result      True. A PhPagaError object on failure.
 *
 * @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 PhPagaError::raiseErrorByCode(PHPAGA_ERR_INVALID_FILENAME);

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

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

    $version = $m[1];

    $d = new PhPagaDbData;

    $v = $d->queryOneVal('SELECT cfg_value FROM config WHERE cfg_key = ?', 'VERSION_DB');

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

    if ($v >= $version)
        return PhPagaError::raiseErrorByCode(PHPAGA_ERR_DBVERSION_INSTHIGH);

    switch(PHPAGA_DB_SYSTEM) {

    case "pgsql":

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

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

        $content = "BEGIN TRANSACTION; ".$content." COMMIT;";
        $res = $d->exec($content);

        if (PhPagaError::isError($res)) {
            $d->rollback();
            return $res;
        }

        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.

        $dbhost = "''";

        if (strlen(PHPAGA_DB_HOST))
            $dbhost = PHPAGA_DB_HOST;

        $command = PHPAGA_MYSQLBIN.
            escapeshellcmd(sprintf(' -h %s -u%s -p%s %s ', $dbhost, 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 PhPagaError::raiseErrorByCode(PHPAGA_ERR_DBUPGRADE_STATEMENT);
        }

        break;


    default:
        return PhPagaError::raiseErrorByCode(PHPAGA_ERR_DB_QUERYNOTYETSUPPORTED);
        break;
    }

    phpaga_config_updateitem('VERSION_DB', $version);

    return true;
}


/**
 * Installs the database structure (used during first time
 * installation).
 *
 * @param string $error       Descriptive error message
 *
 * @return bool  $result      True. A PhPagaError object on failure.
 *
 * @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 PhPagaError::raiseErrorByCode(PHPAGA_ERR_INVALID_FILENAME);

    $files = $instfiles[PHPAGA_DB_SYSTEM];

    $content = null;
    $error = null;

    $d = new PhPagaDbData;

    switch(PHPAGA_DB_SYSTEM) {

        case "pgsql":

            $d->beginTransaction();

            foreach ($files as $filename) {

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

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

                if (strlen($content)) {

                    $res = $d->exec($content);

                    if (PhPagaError::isError($res)) {
                        $d->rollback();
                        return $res;
                    }
                }
            }

            $d->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.

            $dbhost = "''";

            if (strlen(PHPAGA_DB_HOST))
                $dbhost = PHPAGA_DB_HOST;

            $dbpwd = (phpaga_strlen(PHPAGA_DB_PASSWD)) ? PHPAGA_DB_PASSWD : "''";

            foreach ($files as $filename) {

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

                $command = PHPAGA_MYSQLBIN.
                    escapeshellcmd(sprintf(' -h %s -u%s -p%s %s ', $dbhost, PHPAGA_DB_LOGIN, $dbpwd, 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(_("\nDB installation: %s\nFile: %s\nError: %s\nConsistency of the database is no longer granted and manual intervention is required."), $version, $filename, $restring);
                    return PhPagaError::raiseErrorByCode(PHPAGA_ERR_DBINSTALL, $error);
                }
            }

            break;


        default:
            return PhPagaError::raiseErrorByCode(PHPAGA_ERR_DB_QUERYNOTYETSUPPORTED);
            break;
    }

    return true;

}

?>