<?php

/**
 +-----------------------------------------------------------------------+
 | This file is part of the Roundcube Webmail client                     |
 |                                                                       |
 | Copyright (C) The Roundcube Dev Team                                  |
 | Copyright (C) Kolab Systems AG                                        |
 |                                                                       |
 | Licensed under the GNU General Public License version 3 or            |
 | any later version with exceptions for skins & plugins.                |
 | See the README file for a full license statement.                     |
 |                                                                       |
 | PURPOSE:                                                              |
 |   Caching engine - SQL DB                                             |
 +-----------------------------------------------------------------------+
 | Author: Thomas Bruederli <roundcube@gmail.com>                        |
 | Author: Aleksander Machniak <alec@alec.pl>                            |
 +-----------------------------------------------------------------------+
*/

/**
 * Interface class for accessing SQL Database cache
 *
 * @package    Framework
 * @subpackage Cache
 */
class rcube_cache_db extends rcube_cache
{
    /**
     * Instance of database handler
     *
     * @var rcube_db
     */
    protected $db;

    /**
     * (Escaped) Cache table name (cache or cache_shared)
     *
     * @var string
     */
    protected $table;


    /**
     * Object constructor.
     *
     * @param int    $userid User identifier
     * @param string $prefix Key name prefix
     * @param string $ttl    Expiration time of memcache/apc items
     * @param bool   $packed Enables/disabled data serialization.
     *                       It's possible to disable data serialization if you're sure
     *                       stored data will be always a safe string
     */
    public function __construct($userid, $prefix = '', $ttl = 0, $packed = true)
    {
        parent::__construct($userid, $prefix, $ttl, $packed);

        $rcube = rcube::get_instance();

        $this->type  = 'db';
        $this->db    = $rcube->get_dbh();
        $this->table = $this->db->table_name($userid ? 'cache' : 'cache_shared', true);
    }

    /**
     * Remove cache records older than ttl
     */
    public function expunge()
    {
        if ($this->db && $this->ttl) {
            $this->db->query(
                "DELETE FROM {$this->table} WHERE "
                . ($this->userid ? "`user_id` = {$this->userid} AND " : "")
                . "`cache_key` LIKE ?"
                . " AND `expires` < " . $this->db->now(),
                $this->prefix . '.%');
        }
    }

    /**
     * Remove expired records of all caches
     */
    public static function gc()
    {
        $rcube = rcube::get_instance();
        $db    = $rcube->get_dbh();

        $db->query("DELETE FROM " . $db->table_name('cache', true) . " WHERE `expires` < " . $db->now());
        $db->query("DELETE FROM " . $db->table_name('cache_shared', true) . " WHERE `expires` < " . $db->now());
    }

    /**
     * Reads cache entry.
     *
     * @param string  $key     Cache key name
     * @param boolean $nostore Enable to skip in-memory store
     *
     * @return mixed Cached value
     */
    protected function read_record($key, $nostore=false)
    {
        if (!$this->db) {
            return;
        }

        $sql_result = $this->db->query(
                "SELECT `data`, `cache_key` FROM {$this->table} WHERE "
                . ($this->userid ? "`user_id` = {$this->userid} AND " : "")
                ."`cache_key` = ?",
                $this->prefix . '.' . $key);

        if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
            if (strlen($sql_arr['data']) > 0) {
                $md5sum = md5($sql_arr['data']);
                $data   = $this->unserialize($sql_arr['data']);
            }

            $this->db->reset();

            if ($nostore) {
                return $data;
            }

            $this->cache[$key]      = $data;
            $this->cache_sums[$key] = $md5sum;
        }
        else {
            $this->cache[$key] = null;
        }

        return $this->cache[$key];
    }

    /**
     * Writes single cache record into DB.
     *
     * @param string $key  Cache key name
     * @param mixed  $data Serialized cache data
     *
     * @param boolean True on success, False on failure
     */
    protected function write_record($key, $data)
    {
        if (!$this->db) {
            return false;
        }

        // don't attempt to write too big data sets
        if (strlen($data) > $this->max_packet_size()) {
            trigger_error("rcube_cache: max_packet_size ($this->max_packet) exceeded for key $key. Tried to write " . strlen($data) . " bytes", E_USER_WARNING);
            return false;
        }

        $db_key = $this->prefix . '.' . $key;

        // Remove NULL rows (here we don't need to check if the record exist)
        if ($data == 'N;') {
            $result = $this->db->query(
                "DELETE FROM {$this->table} WHERE "
                . ($this->userid ? "`user_id` = {$this->userid} AND " : "")
                ."`cache_key` = ?",
                $db_key);

            return !$this->db->is_error($result);
        }

        $key_exists = array_key_exists($key, $this->cache_sums);
        $expires    = $this->ttl ? $this->db->now($this->ttl) : 'NULL';

        if (!$key_exists) {
            // Try INSERT temporarily ignoring "duplicate key" errors
            $this->db->set_option('ignore_key_errors', true);

            if ($this->userid) {
                $result = $this->db->query(
                    "INSERT INTO {$this->table} (`expires`, `user_id`, `cache_key`, `data`)"
                    . " VALUES ($expires, ?, ?, ?)",
                    $this->userid, $db_key, $data);
            }
            else {
                $result = $this->db->query(
                    "INSERT INTO {$this->table} (`expires`, `cache_key`, `data`)"
                    . " VALUES ($expires, ?, ?)",
                    $db_key, $data);
            }

            $this->db->set_option('ignore_key_errors', false);
        }

        // otherwise try UPDATE
        if (!isset($result) || !($count = $this->db->affected_rows($result))) {
            $result = $this->db->query(
                "UPDATE {$this->table} SET `expires` = $expires, `data` = ? WHERE "
                . ($this->userid ? "`user_id` = {$this->userid} AND " : "")
                . "`cache_key` = ?",
                $data, $db_key);

            $count = $this->db->affected_rows($result);
        }

        return $count > 0;
    }

    /**
     * Deletes the cache record(s).
     *
     * @param string  $key         Cache key name or pattern
     * @param boolean $prefix_mode Enable it to clear all keys starting
     *                             with prefix specified in $key
     */
    protected function remove_record($key = null, $prefix_mode = false)
    {
        if (!$this->db) {
            return;
        }

        // Remove all keys (in specified cache)
        if ($key === null) {
            $where = "`cache_key` LIKE " . $this->db->quote($this->prefix . '.%');
        }
        // Remove keys by name prefix
        else if ($prefix_mode) {
            $where = "`cache_key` LIKE " . $this->db->quote($this->prefix . '.' . $key . '%');
        }
        // Remove one key by name
        else {
            $where = "`cache_key` = " . $this->db->quote($this->prefix . '.' . $key);
        }

        $this->db->query(
            "DELETE FROM {$this->table} WHERE "
            . ($this->userid ? "`user_id` = {$this->userid} AND " : "") . $where
        );
    }

    /**
     * Serializes data for storing
     */
    protected function serialize($data)
    {
        return $this->db ? $this->db->encode($data, $this->packed) : false;
    }

    /**
     * Unserializes serialized data
     */
    protected function unserialize($data)
    {
        return $this->db ? $this->db->decode($data, $this->packed) : false;
    }

    /**
     * Determine the maximum size for cache data to be written
     */
    protected function max_packet_size()
    {
        if ($this->max_packet < 0) {
            $this->max_packet = 2097152; // default/max is 2 MB

            if ($value = $this->db->get_variable('max_allowed_packet', $this->max_packet)) {
                $this->max_packet = $value;
            }

            $this->max_packet -= 2000;
        }

        return $this->max_packet;
    }
}
