<?php

// To support legacy updates with old add-ins

if (class_exists('W3_ObjectCache'))

    return;



/**

 * W3 Object Cache object

 */

class W3_ObjectCache {

    /**

     * Internal cache array

     *

     * @var array

     */

    var $cache = array();



    /**

     * Array of global groups

     *

     * @var array

     */

    var $global_groups = array();



    /**

     * List of non-persistent groups

     *

     * @var array

     */

    var $nonpersistent_groups = array();



    /**

     * Total count of calls

     *

     * @var integer

     */

    var $cache_total = 0;



    /**

     * Cache hits count

     *

     * @var integer

     */

    var $cache_hits = 0;



    /**

     * Cache misses count

     *

     * @var integer

     */

    var $cache_misses = 0;



    /**

     * Total time

     *

     * @var integer

     */

    var $time_total = 0;



    /**

     * Store debug information of w3tc using

     *

     * @var array

     */

    var $debug_info = array();



    /**

     * Blog id of cache

     *

     * @var integer

     */

    private $_blog_id;



    /**

     * Key cache

     *

     * @var array

     */

    var $_key_cache = array();



    /**

     * Config

     *

     * @var W3_Config

     */

    var $_config = null;



    /**

     * Caching flag

     *

     * @var boolean

     */

    var $_caching = false;



    /**

     * Dynamic Caching flag

     *

     * @var boolean

     */

    var $_can_cache_dynamic = null;

    /**

     * Cache reject reason

     *

     * @var string

     */

    var $cache_reject_reason = '';



    /**

     * Lifetime

     *

     * @var integer

     */

    var $_lifetime = null;



    /**

     * Debug flag

     *

     * @var boolean

     */

    var $_debug = false;



    /**

     * Returns instance. for backward compatibility with 0.9.2.3 version of /wp-content files

     *

     * @return W3_ObjectCache

     */

    function instance() {

        return w3_instance('W3_ObjectCache');

    }



    /**

     * PHP5 style constructor

     */

    function __construct() {

        global $_wp_using_ext_object_cache;



        $this->_config = w3_instance('W3_Config');

        $this->_lifetime = $this->_config->get_integer('objectcache.lifetime');

        $this->_debug = $this->_config->get_boolean('objectcache.debug');

        $this->_caching = $_wp_using_ext_object_cache = $this->_can_cache();

        $this->global_groups = $this->_config->get_array('objectcache.groups.global');

        $this->nonpersistent_groups = $this->_config->get_array('objectcache.groups.nonpersistent');



        $this->_blog_id = w3_get_blog_id();

    }



    /**

     * Get from the cache

     *

     * @param string $id

     * @param string $group

     * @return mixed

     */

    function get($id, $group = 'default') {

        if ($this->_debug) {

            $time_start = w3_microtime();

        }



        $key = $this->_get_cache_key($id, $group);

        $internal = isset($this->cache[$key]);



        if ($internal) {

            $value = $this->cache[$key];

        } elseif ($this->_caching &&

                  !in_array($group, $this->nonpersistent_groups) &&

                    $this->_check_can_cache_runtime($group)) {

            $cache = $this->_get_cache(null, $group);

            $v = $cache->get($key);

            if (is_array($v) && $v['content'] != null)

                $value = $v['content'];

            else

                $value = false;

        } else {

            $value = false;

        }



        if ($value === null) {

            $value = false;

        }



        if (is_object($value)) {

            $value = wp_clone($value);

        }



        $this->cache[$key] = $value;

        $this->cache_total++;



        if ($value !== false) {

            $cached = true;

            $this->cache_hits++;

        } else {

            $cached = false;

            $this->cache_misses++;

        }



        /**

         * Add debug info

         */

        if ($this->_debug) {

            $time = w3_microtime() - $time_start;

            $this->time_total += $time;



            if (!$group) {

                $group = 'default';

            }



            $this->debug_info[] = array(

                'id' => $id,

                'group' => $group,

                'cached' => $cached,

                'internal' => $internal,

                'data_size' => ($value ? strlen(serialize($value)) : ''),

                'time' => $time

            );

        }



        return $value;

    }



    /**

     * Set to the cache

     *

     * @param string $id

     * @param mixed $data

     * @param string $group

     * @param integer $expire

     * @return boolean

     */

    function set($id, $data, $group = 'default', $expire = 0) {

        $key = $this->_get_cache_key($id, $group);



        if (is_object($data)) {

            $data = wp_clone($data);

        }



        $this->cache[$key] = $data;



        if ($this->_caching && 

                !in_array($group, $this->nonpersistent_groups) &&

                $this->_check_can_cache_runtime($group)) {

            $cache = $this->_get_cache(null, $group);



            if ($id == 'alloptions' && $group == 'options') {

                // alloptions are deserialized on the start when some classes are not loaded yet

                // so postpone it until requested

                foreach ($data as $k => $v) {

                    if (is_object($v)) {

                        $data[$k] = serialize($v);

                    }

                }

            }



            $v = array('content' => $data);

            return $cache->set($key, $v,

                ($expire ? $expire : $this->_lifetime));

        }



        return true;

    }



    /**

     * Delete from the cache

     *

     * @param string $id

     * @param string $group

     * @param bool $force

     * @return boolean

     */

    function delete($id, $group = 'default', $force = false) {

        if (!$force && $this->get($id, $group) === false) {

            return false;

        }



        $key = $this->_get_cache_key($id, $group);



        unset($this->cache[$key]);



        if ($this->_caching && !in_array($group, $this->nonpersistent_groups)) {

            $cache = $this->_get_cache(null, $group);



            return $cache->delete($key);

        }



        return true;

    }



    /**

     * Add to the cache

     *

     * @param string $id

     * @param mixed $data

     * @param string $group

     * @param integer $expire

     * @return boolean

     */

    function add($id, $data, $group = 'default', $expire = 0) {

        if ($this->get($id, $group) !== false) {

            return false;

        }



        return $this->set($id, $data, $group, $expire);

    }



    /**

     * Replace in the cache

     *

     * @param string $id

     * @param mixed $data

     * @param string $group

     * @param integer $expire

     * @return boolean

     */

    function replace($id, $data, $group = 'default', $expire = 0) {

        if ($this->get($id, $group) === false) {

            return false;

        }



        return $this->set($id, $data, $group, $expire);

    }



    /**

     * Reset keys

     *

     * @return boolean

     */

    function reset() {

        global $_wp_using_ext_object_cache;



        $_wp_using_ext_object_cache = $this->_caching;



        return true;

    }



    /**

     * Flush cache

     *

     * @return boolean

     */

    function flush() {

        $this->cache = array();



        global $w3_multisite_blogs;

        if (isset($w3_multisite_blogs)) {

            foreach($w3_multisite_blogs as $blog ){

                $cache = $this->_get_cache($blog->userblog_id);

                $cache->flush();

            }

        } else {

            //Global groups are now stored in master cache so need to be able to flush it form network

            if (is_network_admin())

                $cache = $this->_get_cache(0);

            else

                $cache = $this->_get_cache();



            return $cache->flush();

        }



        return true;

    }



    /**

     * Add global groups

     *

     * @param array $groups

     * @return void

     */

    function add_global_groups($groups) {

        if (!is_array($groups)) {

            $groups = (array) $groups;

        }



        $this->global_groups = array_merge($this->global_groups, $groups);

        $this->global_groups = array_unique($this->global_groups);

    }



    /**

     * Add non-persistent groups

     *

     * @param array $groups

     * @return void

     */

    function add_nonpersistent_groups($groups) {

        if (!is_array($groups)) {

            $groups = (array) $groups;

        }



        $this->nonpersistent_groups = array_merge($this->nonpersistent_groups, $groups);

        $this->nonpersistent_groups = array_unique($this->nonpersistent_groups);

    }



    /**

     * Increment numeric cache item's value

     *

     * @param int|string $key The cache key to increment

     * @param int $offset The amount by which to increment the item's value. Default is 1.

     * @param string $group The group the key is in.

     * @return bool|int False on failure, the item's new value on success.

     */

    function incr( $key, $offset = 1, $group = 'default' ) {

        $value = $this->get($key, $group);

        if ($value === false)

            return false;



        if (!is_numeric($value))

            $value = 0;



        $offset = (int) $offset;

        $value += $offset;



        if ( $value < 0 )

            $value = 0;

        $this->replace($key, $value, $group);

        return $value;

    }



    /**

     * Decrement numeric cache item's value

     *

     * @param int|string $key The cache key to increment

     * @param int $offset The amount by which to decrement the item's value. Default is 1.

     * @param string $group The group the key is in.

     * @return bool|int False on failure, the item's new value on success.

     */

    function decr( $key, $offset = 1, $group = 'default' ) {

        $value = $this->get($key, $group);

        if ($value === false)

            return false;



        if (!is_numeric($value))

            $value = 0;



        $offset = (int) $offset;

        $value -= $offset;



        if ( $value < 0 )

            $value = 0;

        $this->replace($key, $value, $group);

        return $value;

    }



    /**

     * Print Object Cache stats

     *

     * @return void

     */

    function stats()

    {

        echo '<h2>Summary</h2>';

        echo '<p>';

        echo '<strong>Engine</strong>: ' . w3_get_engine_name($this->_config->get_string('objectcache.engine')) . '<br />';

        echo '<strong>Caching</strong>: ' . ($this->_caching ? 'enabled' : 'disabled') . '<br />';



        if (!$this->_caching) {

            echo '<strong>Reject reason</strong>: ' . $this->cache_reject_reason . '<br />';

        }



        echo '<strong>Total calls</strong>: ' . $this->cache_total . '<br />';

        echo '<strong>Cache hits</strong>: ' . $this->cache_hits . '<br />';

        echo '<strong>Cache misses</strong>: ' . $this->cache_misses . '<br />';

        echo '<strong>Total time</strong>: '. round($this->time_total, 4) . 's';

        echo '</p>';



        echo '<h2>Cache info</h2>';



        if ($this->_debug) {

            echo '<table cellpadding="0" cellspacing="3" border="1">';

            echo '<tr><td>#</td><td>Status</td><td>Source</td><td>Data size (b)</td><td>Query time (s)</td><td>ID:Group</td></tr>';



            foreach ($this->debug_info as $index => $debug) {

                echo '<tr>';

                echo '<td>' . ($index + 1) . '</td>';

                echo '<td>' . ($debug['cached'] ? 'cached' : 'not cached') . '</td>';

                echo '<td>' . ($debug['internal'] ? 'internal' : 'persistent') . '</td>';

                echo '<td>' . $debug['data_size'] . '</td>';

                echo '<td>' . round($debug['time'], 4) . '</td>';

                echo '<td>' . sprintf('%s:%s', $debug['id'], $debug['group']) . '</td>';

                echo '</tr>';

            }



            echo '</table>';

        } else {

            echo '<p>Enable debug mode.</p>';

        }

    }



    /**

     * Switches context to another blog

     *

     * @param integer $blog_id

     */

    function switch_blog($blog_id) {

        $this->reset();

        $this->_blog_id = $blog_id;

    }



    /**

     * Returns cache key

     *

     * @param string $id

     * @param string $group

     * @return string

     */

    function _get_cache_key($id, $group = 'default') {

        if (!$group) {

            $group = 'default';

        }



        $blog_id = $this->_blog_id;

        if (in_array($group, $this->global_groups))

            $blog_id = 0;



        $key_cache_id = $blog_id . $group . $id;



        if (isset($this->_key_cache[$key_cache_id])) {

            $key = $this->_key_cache[$key_cache_id];

        } else {

            $key = md5($blog_id . $group . $id);

            $this->_key_cache[$key_cache_id] = $key;

        }



        return $key;

    }



    /**

     * Returns cache object

     *

     * @param int|null $blog_id

     * @param string $group

     * @return W3_Cache_Base

     */

    function _get_cache($blog_id = null, $group = '') {

        static $cache = array();



        if (is_null($blog_id) && !in_array($group, $this->global_groups))

            $blog_id = $this->_blog_id;

        elseif (is_null($blog_id))

            $blog_id = 0;



        if (!isset($cache[$blog_id])) {

            $engine = $this->_config->get_string('objectcache.engine');



            switch ($engine) {

                case 'memcached':

                    $engineConfig = array(

                        'servers' => $this->_config->get_array('objectcache.memcached.servers'),

                        'persistant' => $this->_config->get_boolean('objectcache.memcached.persistant')

                    );

                    break;



                case 'file':

                    $engineConfig = array(

                        'section' => 'object',

                        'locking' => $this->_config->get_boolean('objectcache.file.locking'),

                        'flush_timelimit' => $this->_config->get_integer('timelimit.cache_flush')

                    );

                    break;



                default:

                    $engineConfig = array();

            }

            $engineConfig['blog_id'] = $blog_id;

            $engineConfig['module'] = 'object';

            $engineConfig['host'] = w3_get_host();

            $engineConfig['instance_id'] = w3_get_instance_id();



            w3_require_once(W3TC_LIB_W3_DIR . '/Cache.php');



            $cache[$blog_id] = W3_Cache::instance($engine, $engineConfig);

        }



        return $cache[$blog_id];

    }



    /**

     * Check if caching allowed on init

     *

     * @return boolean

     */

    function _can_cache() {

        /**

         * Skip if disabled

         */

        if (!$this->_config->get_boolean('objectcache.enabled')) {

            $this->cache_reject_reason = 'Object caching is disabled';



            return false;

        }



        /**

         * Check for DONOTCACHEOBJECT constant

         */

        if (defined('DONOTCACHEOBJECT') && DONOTCACHEOBJECT) {

            $this->cache_reject_reason = 'DONOTCACHEOBJECT constant is defined';



            return false;

        }



        return true;

    }



    /**

     * Returns if we can cache, that condition can change in runtime

     *

     * @param $group

     * @return boolean

     */

    function _check_can_cache_runtime($group) {

        //Need to be handled in wp admin as well as frontend

        if (in_array($group, array('transient', 'site-transient')))

            return true;



        if ($this->_can_cache_dynamic != null)

            return $this->_can_cache_dynamic;



        if ($this->_caching) {

            if (defined('WP_ADMIN')) {

                $this->_can_cache_dynamic = false;

                $this->cache_reject_reason = 'WP_ADMIN defined';

                return $this->_can_cache_dynamic;

            }

        }



        return $this->_caching;

    }



    /**

     * Returns debug info

     *

     * @return string

     */

    function _get_debug_info() {

        $debug_info = "<!-- W3 Total Cache: Object Cache debug info:\r\n";

        $debug_info .= sprintf("%s%s\r\n", str_pad('Engine: ', 20), w3_get_engine_name($this->_config->get_string('objectcache.engine')));

        $debug_info .= sprintf("%s%s\r\n", str_pad('Caching: ', 20), ($this->_caching ? 'enabled' : 'disabled'));



        if (!$this->_caching) {

            $debug_info .= sprintf("%s%s\r\n", str_pad('Reject reason: ', 20), $this->cache_reject_reason);

        }



        $debug_info .= sprintf("%s%d\r\n", str_pad('Total calls: ', 20), $this->cache_total);

        $debug_info .= sprintf("%s%d\r\n", str_pad('Cache hits: ', 20), $this->cache_hits);

        $debug_info .= sprintf("%s%d\r\n", str_pad('Cache misses: ', 20), $this->cache_misses);

        $debug_info .= sprintf("%s%.4f\r\n", str_pad('Total time: ', 20), $this->time_total);



        $debug_info .= "W3TC Object Cache info:\r\n";

        $debug_info .= sprintf("%s | %s | %s | %s | %s | %s\r\n",

                               str_pad('#', 5, ' ', STR_PAD_LEFT),

                               str_pad('Status', 15, ' ', STR_PAD_BOTH),

                               str_pad('Source', 15, ' ', STR_PAD_BOTH),

                               str_pad('Data size (b)', 13, ' ', STR_PAD_LEFT),

                               str_pad('Query time (s)', 14, ' ', STR_PAD_LEFT),

                               'ID:Group');



        foreach ($this->debug_info as $index => $debug) {

            $debug_info .= sprintf("%s | %s | %s | %s | %s | %s\r\n",

                                   str_pad($index + 1, 5, ' ', STR_PAD_LEFT),

                                   str_pad(($debug['cached'] ? 'cached' : 'not cached'), 15, ' ', STR_PAD_BOTH),

                                   str_pad(($debug['internal'] ? 'internal' : 'persistent'), 15, ' ', STR_PAD_BOTH),

                                   str_pad($debug['data_size'], 13, ' ', STR_PAD_LEFT),

                                   str_pad(round($debug['time'], 4), 14, ' ', STR_PAD_LEFT),

                                   sprintf('%s:%s', $debug['id'], $debug['group']));

        }



        $debug_info .= '-->';



        return $debug_info;

    }

}

