<?php
/**
 * Preferences storage implementation for PHP's PEAR database abstraction
 * layer.
 *
 * Required parameters:
 * ====================
 *   'charset'   --  The database's internal charset.
 *   'database'  --  The name of the database.
 *   'hostspec'  --  The hostname of the database server.
 *   'password'  --  The password associated with 'username'.
 *   'phptype'   --  The database type (ie. 'pgsql', 'mysql, etc.).
 *   'protocol'  --  The communication protocol ('tcp', 'unix', etc.).
 *   'username'  --  The username with which to connect to the database.
 *
 * Optional preferences:
 * =====================
 *   'table'  --  The name of the preferences table in 'database'.
 *                DEFAULT: 'horde_prefs'
 *
 * Required by some database implementations:
 * ==========================================
 *   'options'  --  Additional options to pass to the database.
 *   'port'     --  The port on which to connect to the database.
 *   'tty'      --  The TTY on which to connect to the database.
 *
 *
 * The table structure for the preferences is as follows:
 *
 *  create table horde_prefs (
 *      pref_uid        varchar(255) not null,
 *      pref_scope      varchar(16) not null default '',
 *      pref_name       varchar(32) not null,
 *      pref_value      text null,
 *      primary key (pref_uid, pref_scope, pref_name)
 *  );
 *
 *
 * If setting up as the Horde preference handler in conf.php, simply configure
 * $conf['sql'] and don't enter anything for $conf['prefs']['params'].
 *
 *
 * $Horde: horde/lib/Prefs/sql.php,v 1.67 2003/07/10 22:06:32 slusarz Exp $
 *
 * Copyright 1999-2003 Jon Parise <jon@horde.org>
 *
 * See the enclosed file COPYING for license information (LGPL). If you
 * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
 *
 * @author  Jon Parise <jon@horde.org>
 * @version $Revision: 1.67 $
 * @since   Horde 1.3
 * @package horde.prefs
 */
class Prefs_sql extends Prefs {

    /**
     * Hash containing connection parameters.
     *
     * @var array $params
     */
    var $_params = array();

    /**
     * Handle for the current database connection.
     *
     * @var object DB $db
     */
    var $_db;

    /**
     * Boolean indicating whether or not we're connected to the SQL server.
     *
     * @var boolean $_connected
     */
    var $_connected = false;

    /**
     * Constructs a new SQL preferences object.
     *
     * @access public
     *
     * @param string $user               The user who owns these preferences.
     * @param string $password           The password associated with $user.
     *                                   (Unused)
     * @param string $scope              The current preferences scope.
     * @param array $params              A hash containing connection
     *                                   parameters.
     * @param optional boolean $caching  Should caching be used?
     */
    function Prefs_sql($user, $password = '', $scope = '',
                       $params = array(), $caching = false)
    {
        $this->user = $user;
        $this->scope = $scope;
        $this->_params = $params;
        $this->caching = $caching;

        parent::Prefs();
    }

    /**
     * Returns the charset used by the concrete preference backend.
     *
     * @access public
     *
     * @return string  The preference backend's charset.
     */
    function getCharset()
    {
        return $this->_params['charset'];
    }

    /**
     * Retrieve a value or set of values for a specified user.
     *
     * @access public
     *
     * @param string $user            The user to retrieve prefs for.
     * @param mixed $retrieve         A string or array with the preferences
     *                                to retrieve.
     * @param optional string $scope  The preference scope to look in.
     *                                Defaults to 'horde'.
     *
     * @return mixed  If a single value was requested, the value for that
     *                preference. Otherwise, a hash, indexed by pref names,
     *                with the requested values.
     *
     * @since Horde 2.2
     */
    function getPref($user, $retrieve, $scope = 'horde')
    {
        /* Make sure we're connected. */
        $this->_connect();

        if ($scope != 'horde') {
            $scope = "pref_scope = " . $this->_db->quote($scope) . " OR pref_scope = 'horde'";
        } else {
            $scope = "pref_scope = 'horde'";
        }

        if (is_array($retrieve)) {
            $where = '';
            foreach ($retrieve as $pref) {
                if (!empty($where)) {
                    $where .= ' OR ';
                }
                $where .= 'pref_name=' . $this->_db->quote($pref);
            }
            $prefs = $this->_db->getAssoc('SELECT pref_name, pref_value FROM ' . $this->_params['table'] . ' WHERE pref_uid=' . $this->_db->quote($user) . ' AND (' . $where . ') AND (' . $scope . ')');
            foreach ($prefs as $pref => $value) {
                $prefs[$pref] = String::convertCharset($value, $this->_params['charset']);
            }
            return $prefs;
        } else {
            $pref = $this->_db->getOne('SELECT pref_value FROM ' . $this->_params['table'] . ' WHERE pref_uid=' . $this->_db->quote($user) . ' AND pref_name=' . $this->_db->quote($retrieve) . ' AND (' . $scope . ')');
            if (is_null($pref)) {
                return $this->getDefault($retrieve);
            }
            return String::convertCharset($pref, $this->_params['charset']);
        }
    }

    /**
     * Set a value or set of values for a specified user.
     *
     * @access public
     *
     * @param string $user            The user to set prefs for.
     * @param array $set              A hash with the preferences
     *                                to set.
     * @param optional string $scope  The preference scope to use.
     *                                Defaults to 'horde'.
     *
     * @return mixed  True on success, a PEAR_Error on failure.
     */
    function setPref($user, $set, $scope = 'horde')
    {
        /* Make sure we're connected. */
        $this->_connect();

        if ($scope != 'horde') {
            $scope_search = "pref_scope = " . $this->_db->quote($scope) . " OR pref_scope = 'horde'";
        } else {
            $scope_search = "pref_scope = 'horde'";
        }

        foreach ($set as $pref => $val) {
            // Does the preference already exist?
            $query = sprintf('SELECT 1 FROM %s WHERE pref_uid = %s and pref_name = %s and (%s)',
                             $this->_params['table'],
                             $this->_db->quote($user),
                             $this->_db->quote($pref),
                             $scope_search);
            $check = $this->_db->getOne($query);
            if (is_a($check, 'PEAR_Error')) {
                return $check;
            }
            if (!empty($check)) {
                $query = sprintf('UPDATE %s SET pref_value=%s WHERE pref_name=%s AND pref_uid=%s AND pref_scope=%s',
                                 $this->_params['table'],
                                 $this->_db->quote(String::convertCharset((string)$val, NLS::getCharset(), $this->_params['charset'])),
                                 $this->_db->quote($pref),
                                 $this->_db->quote($user),
                                 $this->_db->quote($scope));
            } else {
                $query = sprintf('INSERT INTO %s (pref_uid, pref_scope, pref_name, pref_value)' .
                                 ' VALUES (%s, %s, %s, %s)',
                                 $this->_params['table'],
                                 $this->_db->quote($user),
                                 $this->_db->quote($scope),
                                 $this->_db->quote($pref),
                                 $this->_db->quote(String::convertCharset((string)$val, NLS::getCharset(), $this->_params['charset'])));
            }

            $result = $this->_db->query($query);
            if (is_a($result, 'PEAR_Error')) {
                return $result;
            }
        }

        return true;
    }

    /**
     * Retrieves the requested set of preferences from the user's database
     * entry.
     *
     * @access public
     *
     * @param optional array $prefs  An array listing the preferences to
     *                               retrieve. If not specified, retrieve all
     *                               of the preferences listed in the $prefs
     *                               hash.
     *
     * @return mixed  True on success or a PEAR_Error object on failure.
     */
    function retrieve($prefs = array())
    {
        /* If a list of preferences to store hasn't been provided in
           $prefs, assume all preferences are desired. */
        if (!count($prefs)) {
            $prefs = $this->listAll();
        }
        if (!is_array($prefs) || empty($prefs)) {
            return PEAR::raiseError(_("No preferences are available."));
        }

        /* Attempt to pull the values from the session cache first. */
        if ($this->cacheLookup($prefs)) {
            return true;
        }

        /* Make sure we're connected. */
        $this->_connect();

        /* Build the SQL query. */
        $query = 'SELECT pref_scope, pref_name, pref_value FROM ';
        $query .= $this->_params['table'] . ' ';
        $query .= 'WHERE pref_uid = ' . $this->_db->quote($this->user);
        $query .= ' AND (pref_scope = ' . $this->_db->quote($this->scope);
        $query .= " OR pref_scope = 'horde') ORDER BY pref_scope";

        /* Execute the query. */
        $result = $this->_db->query($query);

        if (isset($result) && !is_a($result, 'PEAR_Error')) {
            $row = $result->fetchRow(DB_FETCHMODE_ASSOC);
            if (is_a($row, 'PEAR_Error')) {
                Horde::logMessage('No preferences were retrieved.', __FILE__, __LINE__, PEAR_LOG_ERR);
                Horde::logMessage($row, __FILE__, __LINE__, PEAR_LOG_ERR);
                return;
            }

            /* Set the requested values in the $this->_prefs hash
               based on the contents of the SQL result.

               Note that Prefs::setValue() can't be used here because
               of the check for the "changeable" bit. We want to
               override that check when populating the $this->_prefs
               hash from the SQL server.
            */
            while ($row && !is_a($row, 'PEAR_Error')) {
                $name = trim($row['pref_name']);
                if (in_array($name, $prefs)) {
                    $this->_setValue($name, String::convertCharset($row['pref_value'], $this->_params['charset']), false);
                    $this->setDirty($name, false);
                } else {
                    $this->add($name, String::convertCharset($row['pref_value'], $this->_params['charset']), _PREF_SHARED);
                }
                $row = $result->fetchRow(DB_FETCHMODE_ASSOC);
            }
            $result->free();

            /* Call hooks. */
            $this->_callHooks();
        } else {
            Horde::logMessage('No preferences were retrieved.', __FILE__, __LINE__);
            return;
        }

        /* Update the session cache. */
        $this->cacheUpdate();

        return true;
    }

    /**
     * Stores preferences to SQL server.
     *
     * @access public
     *
     * @param optional array $prefs  An array listing the preferences to be
     *                               stored. If not specified, store all of
     *                               the preferences listed in the $prefs hash.
     *
     * @return mixed  True on success or a PEAR_Error object on failure.
     */
    function store($prefs = array())
    {
        /* If a list of preferences to store hasn't been provided in
           $prefs, assume all preferences are desired. */
        if (!count($prefs)) {
            $prefs = $this->listAll();
        }
        if (!is_array($prefs) || empty($prefs)) {
            return PEAR::raiseError(_("No preferences are available."));
        }

        /* Check for any "dirty" preferences. If no "dirty" preferences are
           found, there's no need to update the SQL server.
           Exit successfully. */
        if (!($dirty_prefs = $this->_dirtyPrefs($prefs))) {
            return true;
        }

        /* Make sure we're connected. */
        $this->_connect();

        /* Loop through the "dirty" preferences.  If a row already exists for
           this preference, attempt to update it.  Otherwise, insert a new
           row. */
        foreach ($dirty_prefs as $name) {
            // Don't store locked preferences.
            if ($this->isLocked($name)) {
                continue;
            }

            $scope = $this->getScope($name);

            /* Does an entry already exist for this preference? */
            $query = 'SELECT 1 FROM ';
            $query .= $this->_params['table'] . ' ';
            $query .= 'WHERE pref_uid = ' . $this->_db->quote($this->user);
            $query .= ' AND pref_name = ' . $this->_db->quote($name);
            $query .= ' AND (pref_scope = ' . $this->_db->quote($scope);
            $query .= " OR pref_scope = 'horde')";

            /* Execute the query. */
            $check = $this->_db->getOne($query);

            /* Return an error if the query fails. */
            if (is_a($check, 'PEAR_Error')) {
                Horde::logMessage('Failed retrieving prefs for ' . $this->user, __FILE__, __LINE__, PEAR_LOG_NOTICE);
                return PEAR::raiseError(_("Failed retrieving preferences."));
            }

            /* Is there an existing row for this preference? */
            if (!empty($check)) {
                /* Update the existing row. */
                $query = 'UPDATE ' . $this->_params['table'] . ' ';
                $query .= 'SET pref_value = ' . $this->_db->quote(String::convertCharset((string)$this->getValue($name), NLS::getCharset(), $this->_params['charset']));
                $query .= ' WHERE pref_uid = ' . $this->_db->quote($this->user);
                $query .= ' AND pref_name = ' . $this->_db->quote($name);
                $query .= ' AND pref_scope = ' . $this->_db->quote($scope);
                $result = $this->_db->query($query);

                /* Return an error if the update fails. */
                if (is_a($result, 'PEAR_Error')) {
                    Horde::fatal($result, __FILE__, __LINE__);
                }
            } else {
                /* Insert a new row. */
                $query  = 'INSERT INTO ' . $this->_params['table'] . ' ';
                $query .= '(pref_uid, pref_scope, pref_name, pref_value) VALUES';
                $query .= '(' . $this->_db->quote($this->user) . ', ';
                $query .= $this->_db->quote($scope) . ', ' . $this->_db->quote($name) . ', ';
                $query .= $this->_db->quote(String::convertCharset((string)$this->getValue($name), NLS::getCharset(), $this->_params['charset'])) . ')';
                $result = $this->_db->query($query);

                /* Return an error if the insert fails. */
                if (is_a($result, 'PEAR_Error')) {
                    Horde::fatal($result, __FILE__, __LINE__);
                }
            }

            /* Mark this preference as "clean" now. */
            $this->setDirty($name, false);
        }

        /* Update the session cache. */
        $this->cacheUpdate();

        return true;
    }

    /**
     * Perform cleanup operations.
     *
     * @access public
     *
     * @param optional boolean $all  Cleanup all Horde preferences.
     */
    function cleanup($all = false)
    {
        /* Close the database connection. */
        $this->_disconnect();

        parent::cleanup($all);
    }

    /**
     * Attempts to open a persistent connection to the SQL server.
     *
     * @access private
     *
     * @return mixed  True on success or a PEAR_Error object on failure.
     */
    function _connect()
    {
        /* Check to see if we are already connected. */
        if ($this->_connected) {
            return true;
        }

        if (!is_array($this->_params)) {
            Horde::fatal(PEAR::raiseError(_("No configuration information specified for SQL Preferences.")), __FILE__, __LINE__);
        }

        $required = array('phptype', 'hostspec', 'username', 'password',
                          'database', 'charset');

        foreach ($required as $val) {
            if (!isset($this->_params[$val])) {
                Horde::fatal(PEAR::raiseError(sprintf(_("Required '%s' not specified in preferences configuration."), $val)), __FILE__, __LINE__);
            }
        }

        if (!isset($this->_params['table'])) {
            $this->_params['table'] = 'horde_prefs';
        }

        /* Connect to the SQL server using the supplied parameters. */
        require_once 'DB.php';
        $this->_db = &DB::connect($this->_params,
                                  array('persistent' => !empty($this->_params['persistent'])));
        if (is_a($this->_db, 'PEAR_Error')) {
            Horde::fatal($this->_db, __FILE__, __LINE__);
        }

        /* Enable the "portability" option. */
        $this->_db->setOption('optimize', 'portability');

        $this->_connected = true;

        return true;
    }

    /**
     * Disconnect from the SQL server and clean up the connection.
     *
     * @access private
     *
     * @return boolean  True on success, false on failure.
     */
    function _disconnect()
    {
        if ($this->_connected) {
            $this->_connected = false;
            return $this->_db->disconnect();
        } else {
            return true;
        }
    }

}
