<?php
/**
 * Nag storage implementation for PHP's PEAR database abstraction layer.
 *
 * Required values for $params:<pre>
 *      'phptype'       The database type (e.g. 'pgsql', 'mysql', etc.).
 *      'hostspec'      The hostname of the database server.
 *      'protocol'      The communication protocol ('tcp', 'unix', etc.).
 *      'username'      The username with which to connect to the database.
 *      'password'      The password associated with 'username'.
 *      'database'      The name of the database.
 *      'table'         The name of the tasks table in 'database'.</pre>
 *      'charset'       The database's internal charset.
 *
 * Required by some database implementations:
 *      'options'       Additional options to pass to the database.
 *      'tty'           The TTY on which to connect to the database.
 *      'port'          The port on which to connect to the database.
 *
 * The table structure can be created by the scripts/drivers/nag_tasks.sql
 * script.
 *
 * $Horde: nag/lib/Driver/sql.php,v 1.60.2.1 2004/12/24 05:15:12 chuck Exp $
 *
 * @author  Jon Parise <jon@horde.org>
 * @since   Nag 0.1
 * @package Nag
 */
class Nag_Driver_sql extends Nag_Driver {

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

    /**
     * Are we connected to the SQL server?
     * @var boolean $_connected
     */
    var $_connected = false;

    /**
     * Construct a new SQL storage object.
     *
     * @param string $tasklist  The tasklist to load.
     * @param array  $params    A hash containing connection parameters.
     */
    function Nag_Driver_sql($tasklist, $params = array())
    {
        $this->_tasklist = $tasklist;
        $this->_params = $params;
    }

    /**
     * Retrieve one task from the database.
     *
     * @param string $taskId  The id of the task to retrieve.
     *
     * @return array  The array of task attributes.
     */
    function get($taskId)
    {
        /* Make sure we have a valid database connection. */
        $this->_connect();

        /* Build the SQL query. */
        $query = sprintf('SELECT * FROM %s WHERE task_owner = %s and task_id = %s',
                         $this->_params['table'], $this->_db->quote($this->_tasklist), $this->_db->quote($taskId));

        /* Log the query at a DEBUG log level. */
        Horde::logMessage(sprintf('Nag_Driver_sql::get(): %s', $query),
                          __FILE__, __LINE__, PEAR_LOG_DEBUG);

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

        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }

        $row = $result->fetchRow(DB_FETCHMODE_ASSOC);
        if (is_a($row, 'PEAR_Error')) {
            return $row;
        }

        /* Decode and return the task. */
        return $this->_buildTask($row);
    }

    /**
     * Retrieve one task from the database by UID.
     *
     * @param string $uid  The UID of the task to retrieve.
     *
     * @return array  The array of task attributes.
     */
    function getByUID($uid)
    {
        /* Make sure we have a valid database connection. */
        $this->_connect();

        /* Build the SQL query. */
        $query = sprintf('SELECT * FROM %s WHERE task_uid = %s',
                         $this->_params['table'], $this->_db->quote($uid));

        /* Log the query at a DEBUG log level. */
        Horde::logMessage(sprintf('Nag_Driver_sql::getByUID(): %s', $query),
                          __FILE__, __LINE__, PEAR_LOG_DEBUG);

        /* Execute the query. */
        $result = $this->_db->query($query);
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }

        $row = $result->fetchRow(DB_FETCHMODE_ASSOC);
        if (is_a($row, 'PEAR_Error')) {
            return $row;
        }

        /* Decode and return the task. */
        $this->_tasklist = $row['task_owner'];
        return $this->_buildTask($row);
    }

    /**
     * Add a task to the backend storage.
     *
     * @param string           $name      The name (short) of the task.
     * @param string           $desc      The description (long) of the task.
     * @param optional integer $due       The due date of the task.
     * @param optional integer $priority  The priority of the task.
     * @param optional integer $completed The completion state of the task.
     * @param optional string  $category  The category of the task.
     * @param optional integer $completed The alarm associatesd to the task.
     * @param optional string  $uid       A Unique Identifier for the task.
     *
     * @return string  The Nag ID of the new task.
     */
    function add($name, $desc, $due = 0, $priority = 0, $completed = 0,
                 $category = '', $alarm = 0, $uid = null)
    {
        /* Make sure we have a valid database connection. */
        $this->_connect();

        $taskId = md5(uniqid(mt_rand(), true));
        if (is_null($uid)) {
            $uid = $this->generateUID();
        }

        $query = sprintf(
            'INSERT INTO %s (task_owner, task_id, task_name, task_uid, ' .
            'task_desc, task_due, task_priority, task_completed, ' .
            'task_category, task_alarm) ' .
            'VALUES (%s, %s, %s, %s, %s, %d, %d, %d, %s, %d)',
            $this->_params['table'],
            $this->_db->quote($this->_tasklist),
            $this->_db->quote($taskId),
            $this->_db->quote(String::convertCharset($name, NLS::getCharset(), $this->_params['charset'])),
            $this->_db->quote(String::convertCharset($uid, NLS::getCharset(), $this->_params['charset'])),
            $this->_db->quote(String::convertCharset($desc, NLS::getCharset(), $this->_params['charset'])),
            $due,
            $priority,
            $completed,
            $this->_db->quote(String::convertCharset($category, NLS::getCharset(), $this->_params['charset'])),
            $alarm);

        /* Log the query at a DEBUG log level. */
        Horde::logMessage(sprintf('Nag_Driver_sql::add(): %s', $query),
                          __FILE__, __LINE__, PEAR_LOG_DEBUG);

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

        /* Return an error immediately if the query failed. */
        if (is_a($result, 'PEAR_Error')) {
            Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
            return $result;
        }

        /* Log the creation of this item in the history log. */
        $history = &Horde_History::singleton();
        $history->log('nag:' . $this->_tasklist . ':' . $uid, array('action' => 'add'), true);

        return $taskId;
    }

    /**
     * Modify an existing task.
     *
     * @param string           $taskId    The task to modify.
     * @param string           $name      The name (short) of the task.
     * @param string           $desc      The description (long) of the task.
     * @param optional integer $due       The due date of the task.
     * @param optional integer $priority  The priority of the task.
     * @param optional integer $completed The completion state of the task.
     * @param optional string  $category  The category of the task.
     * @param optional integer $completed The alarm associatesd to the task.
     */
    function modify($taskId, $name, $desc, $due = 0, $priority = 0,
                    $completed = 0, $category = '', $alarm = 0)
    {
        /* Make sure we have a valid database connection. */
        $this->_connect();

        $query = sprintf('UPDATE %s SET' .
                         ' task_name = %s, ' .
                         ' task_desc = %s, ' .
                         ' task_due = %d, ' .
                         ' task_priority = %d, ' .
                         ' task_completed = %d, ' .
                         ' task_category = %s, ' .
                         ' task_alarm = %d ' .
                         'WHERE task_owner = %s AND task_id = %s',
                         // Fill in fields in order. Watch out for
                         // sprintf formatters when changing this (%s
                         // versus %d).
                         $this->_params['table'],
                         $this->_db->quote(String::convertCharset($name, NLS::getCharset(), $this->_params['charset'])),
                         $this->_db->quote(String::convertCharset($desc, NLS::getCharset(), $this->_params['charset'])),
                         (int)$due,
                         (int)$priority,
                         (int)$completed,
                         $this->_db->quote(String::convertCharset($category, NLS::getCharset(), $this->_params['charset'])),
                         (int)$alarm,
                         $this->_db->quote($this->_tasklist),
                         $this->_db->quote($taskId));

        /* Log the query at a DEBUG log level. */
        Horde::logMessage(sprintf('Nag_Driver_sql::modify(): %s', $query),
                          __FILE__, __LINE__, PEAR_LOG_DEBUG);

        /* Attempt the update query. */
        $result = $this->_db->query($query);
        if (is_a($result, 'PEAR_Error')) {
            Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
            return $result;
        }

        /* Log the modification of this item in the history log. */
        $task = $this->get($taskId);
        if (!empty($task['uid'])) {
            $history = &Horde_History::singleton();
            $history->log('nag:' . $this->_tasklist . ':' . $task['uid'], array('action' => 'modify'), true);
        }

        return true;
    }

    /**
     * Move a task to a different tasklist.
     *
     * @param string $taskId       The task to move.
     * @param string $newTasklist  The new tasklist.
     */
    function move($taskId, $newTasklist)
    {
        /* Make sure we have a valid database connection. */
        $this->_connect();

        $query = sprintf('UPDATE %s SET task_owner = %s WHERE task_owner = %s AND task_id = %s',
                         $this->_params['table'],
                         $this->_db->quote($newTasklist),
                         $this->_db->quote($this->_tasklist),
                         $this->_db->quote($taskId));

        /* Log the query at a DEBUG log level. */
        Horde::logMessage(sprintf('Nag_Driver_sql::move(): %s', $query),
                          __FILE__, __LINE__, PEAR_LOG_DEBUG);

        /* Attempt the move query. */
        $result = $this->_db->query($query);
        if (is_a($result, 'PEAR_Error')) {
            Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
            return $result;
        }

        return true;
    }

    function delete($taskId)
    {
        $this->_connect();

        /* Get the task's details for use later. */
        $task = $this->get($taskId);

        $query = sprintf('DELETE FROM %s WHERE task_owner = %s AND task_id = %s',
                         $this->_params['table'],
                         $this->_db->quote($this->_tasklist),
                         $this->_db->quote($taskId));

        /* Log the query at a DEBUG log level. */
        Horde::logMessage(sprintf('Nag_Driver_sql::delete(): %s', $query),
                          __FILE__, __LINE__, PEAR_LOG_DEBUG);

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

        if (is_a($result, 'PEAR_Error')) {
            Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
            return $result;
        }

        /* Log the deletion of this item in the history log. */
        if (!empty($task['uid'])) {
            $history = &Horde_History::singleton();
            $history->log('nag:' . $this->_tasklist . ':' . $task['uid'], array('action' => 'delete'), true);
        }

        return true;
    }

    function deleteAll()
    {
        $this->_connect();

        $query = sprintf('DELETE FROM %s WHERE task_owner = %s',
                         $this->_params['table'],
                         $this->_db->quote($this->_tasklist));

        /* Log the query at a DEBUG log level. */
        Horde::logMessage(sprintf('Nag_Driver_sql::deleteAll(): %s', $query),
                          __FILE__, __LINE__, PEAR_LOG_DEBUG);

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

        return is_a($result, 'PEAR_Error') ? $result : true;
    }

    /**
     * Retrieves tasks from the database.
     *
     * @return mixed  True on success, PEAR_Error on failure.
     */
    function retrieve()
    {
        /* Make sure we have a valid database connection. */
        $this->_connect();

        /* Build the SQL query. */
        $query = sprintf('SELECT * FROM %s WHERE task_owner = %s',
                         $this->_params['table'], $this->_db->quote($this->_tasklist));

        /* Log the query at a DEBUG log level. */
        Horde::logMessage(sprintf('Nag_Driver_sql::retrieve(): %s', $query),
                          __FILE__, __LINE__, PEAR_LOG_DEBUG);

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

        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }

        $row = $result->fetchRow(DB_FETCHMODE_ASSOC);
        if (is_a($row, 'PEAR_Error')) {
            return $row;
        }

        /* Store the retrieved values in a fresh $tasks list. */
        $this->_tasks = array();
        while ($row && !is_a($row, 'PEAR_Error')) {
            /* Add this new task to the $tasks list. */
            $this->_tasks[$row['task_id']] = $this->_buildTask($row);

            /* Advance to the new row in the result set. */
            $row = $result->fetchRow(DB_FETCHMODE_ASSOC);
        }
        $result->free();

        return true;
    }

    /**
     * List all alarms near $date.
     *
     * @param integer $date  The unix epoch time to check for alarms.
     *
     * @return array  An array of tasks that have alarms that match.
     */
    function listAlarms($date)
    {
        $this->_connect();

        $q  = 'SELECT * FROM ' . $this->_params['table'];
        $q .= ' WHERE task_owner = ' . $this->_db->quote($this->_tasklist);
        $q .= ' AND task_alarm > 0';
        $q .= ' AND (task_due - (task_alarm * 60) <= ' . $this->_db->quote($date) . ')';
        $q .= ' AND task_due >= ' . $this->_db->quote(time());

        /* Log the query at a DEBUG log level. */
        Horde::logMessage(sprintf('SQL alarms list by %s: table = %s; query = "%s"',
                                  Auth::getAuth(), $this->_params['table'], $q),
                          __FILE__, __LINE__, PEAR_LOG_DEBUG);

        /* Run the query. */
        $qr = $this->_db->getAll($q, DB_FETCHMODE_ASSOC);
        if (is_a($qr, 'PEAR_Error')) {
            return $qr;
        }

        $tasks = array();
        foreach ($qr as $row) {
            $tasks[$row['task_id']] = $this->_buildTask($row);
        }
        return $tasks;
    }

    function _buildTask($row)
    {
        /* Create a new task based on $row's values. */
        return array('tasklist_id' => $row['task_owner'],
                     'task_id' => $row['task_id'],
                     'uid' => String::convertCharset($row['task_uid'], $this->_params['charset']),
                     'name' => String::convertCharset($row['task_name'], $this->_params['charset']),
                     'desc' => String::convertCharset($row['task_desc'], $this->_params['charset']),
                     'category' => String::convertCharset($row['task_category'], $this->_params['charset']),
                     'due' => $row['task_due'],
                     'priority' => $row['task_priority'],
                     'completed' => $row['task_completed'],
                     'alarm' => $row['task_alarm'],
                     'flags' => 0);
    }

    /**
     * Attempts to open a persistent connection to the SQL server.
     *
     * @return boolean    True on success; exits (Horde::fatal()) on error.
     */
    function _connect()
    {
        if (!$this->_connected) {
            Horde::assertDriverConfig($this->_params, 'storage',
                array('phptype', 'hostspec', 'username', 'database', 'charset'));

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

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

            $this->_db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS);

            $this->_connected = true;
        }

        return true;
    }

}
