<?php
// To support legacy updates with old add-ins
if (class_exists('W3_PgCache'))
    return;

/**
 * W3 PgCache
 */

/**
 * Class W3_PgCache
 */
class W3_PgCache {
    /**
     * Advanced cache config
     *
     * @var W3_Config
     */
    var $_config = null;

    /**
     * Caching flag
     *
     * @var boolean
     */
    var $_caching = false;

    /**
     * Time start
     *
     * @var double
     */
    var $_time_start = 0;

    /**
     * Lifetime
     * @var integer
     */
    var $_lifetime = 0;

    /**
     * Enhanced mode flag
     *
     * @var boolean
     */
    var $_enhanced_mode = false;

    /**
     * Debug flag
     *
     * @var boolean
     */
    var $_debug = false;

    /**
     * Request host
     * @var string
     */
    var $_request_host = '';

    /**
     * Request URI
     * @var string
     */
    var $_request_uri = '';

    /**
     * Page key
     * @var string
     */
    var $_page_key = '';

    /**
     * Shutdown buffer
     * @var string
     */
    var $_shutdown_buffer = '';

    /**
     * Shutdown compression
     * @var string
     */
    var $_shutdown_compression = '';

    /**
     * Mobile object
     * @var W3_Mobile
     */
    var $_mobile = null;

    /**
     * Referrer object
     * @var W3_Referrer
     */
    var $_referrer = null;

    /**
     * Cache reject reason
     *
     * @var string
     */
    var $cache_reject_reason = '';

    /**
     * @var If sitemap was matched
     */
    var $_sitemap_matched;

    /**
     * Returns instance. 
     * For backward compatibility with 0.9.2.3 version of /wp-content files
     *
     * @return W3_PgCache
     */
    function instance() {
        $i = w3_instance('W3_PgCache');
        $i->_legacy = true;
        return $i;
    }

    /**
     * PHP5 Constructor
     */
    function __construct() {
        $this->_config = w3_instance('W3_Config');
        $this->_debug = $this->_config->get_boolean('pgcache.debug');

        $request_host = w3_get_host();
        $pos = strpos($request_host, ':');
        if ($pos !== false)
            $request_host = substr($request_host, 0, $pos);
        $this->_request_host = $request_host;

        $this->_request_uri = $_SERVER['REQUEST_URI'];
        $this->_lifetime = $this->_config->get_integer('pgcache.lifetime');
        $this->_enhanced_mode = ($this->_config->get_string('pgcache.engine') == 'file_generic');

        if ($this->_config->get_boolean('mobile.enabled')) {
            $this->_mobile = w3_instance('W3_Mobile');
        }

        if ($this->_config->get_boolean('referrer.enabled')) {
            $this->_referrer = w3_instance('W3_Referrer');
        }
    }

    /**
     * Do cache logic
     */
    function process() {
        /**
         * Skip caching for some pages
         */
        switch (true) {
            case defined('DONOTCACHEPAGE'):
            case defined('DOING_AJAX'):
            case defined('DOING_CRON'):
            case defined('APP_REQUEST'):
            case defined('XMLRPC_REQUEST'):
            case defined('WP_ADMIN'):
            case (defined('SHORTINIT') && SHORTINIT):
                return;
        }

        /*
         * Legacy mode with 0.9.2.3 version of /wp-content add-ins
         */
        if (isset($this->_legacy)) {
            $redirect = w3_instance('W3_Redirect');
            $redirect->process();
        }

        /**
         * Do page cache logic
         */
        if ($this->_debug) {
            $this->_time_start = w3_microtime();
        }

        $this->_caching = $this->_can_cache();
        if ($this->_caching) {
            $success = $this->_extract_cached_page();
            if ($success)
                exit();
        }

        /**
         * Start output buffering
         */
        ob_start(array(
            &$this,
            'ob_callback'
        ));
    }

    /**
     * Extracts page from cache
     *
     * @return boolean
     */
    function _extract_cached_page() {
        $cache = $this->_get_cache();

        $mobile_group = $this->_get_mobile_group();
        $referrer_group = $this->_get_referrer_group();
        $encryption = $this->_get_encryption();
        $compression = $this->_get_compression();
        $raw = !$compression;
        $this->_page_key = $this->_get_page_key($mobile_group, $referrer_group,
            $encryption, $compression);

        $group = '0';
        $sitemap_regex = $this->_config->get_string('pgcache.purge.sitemap_regex');
        if ($sitemap_regex && preg_match('/' . $sitemap_regex . '/', basename($this->_request_uri))) {
            $group = 'sitemaps';
            $this->_sitemap_matched = true;
        } else {
            $this->_sitemap_matched = false;
        }

        /**
         * Check if page is cached
         */
        $data = $cache->get($this->_page_key, $group);

        /**
         * Try to get uncompressed version of cache
         */
        if ($compression && !$data) {
            $raw = true;
            $this->_page_key = $this->_get_page_key($mobile_group,
                $referrer_group, $encryption, false);
            $data = $cache->get($this->_page_key, $group);
        }

        if (!$data)
            return false;

        /**
         * If cache exists
         */

        /**
         * Do Bad Behavior check
         */
        $this->_bad_behavior();

        $is_404 = $data['404'];
        $headers = $data['headers'];
        $time = $data['time'];
        $content = & $data['content'];

        /**
         * Calculate content etag
         */
        $etag = md5($content);

        /**
         * Send headers
         */
        $this->_send_headers($is_404, $time, $etag, $compression, $headers);

        /**
         * Do manual compression for uncompressed page
         */
        if ($raw) {
            /**
             * Append debug info
             */
            if ($this->_debug) {
                $time_total = w3_microtime() - $this->_time_start;
                $debug_info = $this->_get_debug_info(true, '', true, $time_total);
                $content .= "\r\n\r\n" . $debug_info;
            }

            /**
             * Parse dynamic tags
             */
            $this->_parse_dynamic($content);

            /**
             * Compress content
             */
            $this->_compress($content, $compression);
        }

        echo $content;
        return true;
    }
        
    /**
     * Output buffering callback
     *
     * @param string $buffer
     * @return string
     */
    function ob_callback(&$buffer) {
        if ($buffer != '' && $this->_is_cacheable_content_type()) {
            $compression = false;
            $has_dynamic = $this->_has_dynamic($buffer);
            $can_cache = $this->_can_cache2($buffer);

            if ($can_cache) {
                $mobile_group = $this->_get_mobile_group();
                $referrer_group = $this->_get_referrer_group();
                $encryption = $this->_get_encryption();
                $compression = $this->_get_compression();
                $compressions = $this->_get_compressions();

                /**
                 * Don't use compression for debug mode or dynamic tags
                 * because we need to modify buffer before send it to client
                 */
                $raw = ($this->_debug || $has_dynamic);
                if ($raw) {
                    $compression = false;
                }

                $content_type = '';
                $is_404 = (function_exists('is_404') ? is_404() : false);
                $headers = $this->_get_cached_headers();

                if ($this->_enhanced_mode) {
                    $this->_check_rules_present();
                    if (isset($headers['Content-Type']))
                        $content_type = $headers['Content-Type'];
                }

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

                /**
                 * Store different versions of cache
                 */
                $buffers = array();

                $group = '0';
                if (!isset($this->_sitemap_matched)) {
                    $sitemap_regex = $this->_config->get_string('pgcache.purge.sitemap_regex');
                    if ($sitemap_regex && preg_match('/' . $sitemap_regex . '/', basename($this->_request_uri))) {
                        $group = 'sitemaps';
                        $this->_sitemap_matched = true;
                    }
                } elseif ($this->_sitemap_matched)
                    $group = 'sitemaps';

                foreach ($compressions as $_compression) {
                    $_page_key = $this->_get_page_key($mobile_group,
                        $referrer_group, $encryption, $_compression,
                        $content_type);

                    /**
                     * Compress content
                     */
                    $buffers[$_compression] = $buffer;

                    $this->_compress($buffers[$_compression], $_compression);

                    /**
                     * Store cache data
                     */
                    $_data = array(
                        '404' => $is_404,
                        'headers' => $headers,
                        'time' => $time,
                        'content' => &$buffers[$_compression]
                    );

                    $cache->set($_page_key, $_data, $this->_lifetime, $group);
                }

                /**
                 * Change buffer if using compression
                 */
                if ($compression && isset($buffers[$compression])) {
                    $buffer = & $buffers[$compression];
                }

                /**
                 * Calculate content etag
                 */
                $etag = md5($buffer);

                /**
                 * Send headers
                 */
                $this->_send_headers($is_404, $time, $etag, $compression, $headers);

                if ($raw) {
                    if ($this->_debug) {
                        /**
                         * Set page key for debug
                         */
                        $this->_page_key = $this->_get_page_key($mobile_group, 
                            $referrer_group, $encryption, $compression,
                            $content_type);

                        /**
                         * Append debug info
                         */
                        $time_total = w3_microtime() - $this->_time_start;
                        $debug_info = $this->_get_debug_info(true, '', false, $time_total);
                        $buffer .= "\r\n\r\n" . $debug_info;
                    }

                    /**
                     * Don't use shutdown function below
                     */
                    if (!$has_dynamic) {
                        $this->_compress($buffer, $compression);
                    }
                }
            } elseif ($this->_debug) {
                $mobile_group = $this->_get_mobile_group();
                $referrer_group = $this->_get_referrer_group();
                $encryption = $this->_get_encryption();

                /**
                 * Set page key for debug
                 */
                $this->_page_key = $this->_get_page_key($mobile_group,
                    $referrer_group, $encryption, $compression);

                /**
                 * Append debug info
                 */
                $time_total = w3_microtime() - $this->_time_start;
                $debug_info = $this->_get_debug_info(false, $this->cache_reject_reason, false, $time_total);
                $buffer .= "\r\n\r\n" . $debug_info;
            }

            /**
             * We can't capture output in ob_callback
             * so we use shutdown function
             */
            if ($has_dynamic) {
                $this->_shutdown_buffer = $buffer;
                $this->_shutdown_compression = $compression;

                $buffer = '';

                register_shutdown_function(array(
                    &$this,
                    'shutdown'
                ));
            }
        }

        return $buffer;
    }

    /**
     * Shutdown callback
     * @return void
     */
    function shutdown() {
        /**
         * Parse dynamic content
         */
        $this->_parse_dynamic($this->_shutdown_buffer);

        /**
         * Compress page
         */
        $this->_compress($this->_shutdown_buffer, $this->_shutdown_compression);

        echo $this->_shutdown_buffer;
    }

    /**
     * Checks if can we do cache logic
     *
     * @return boolean
     */
    function _can_cache() {
        /**
         * Don't cache in console mode
         */
        if (PHP_SAPI === 'cli') {
            $this->cache_reject_reason = 'Console mode';

            return false;
        }

        /**
         * Skip if session defined
         */
        if (defined('SID') && SID != '') {
            $this->cache_reject_reason = 'Session started';

            return false;
        }

        /**
         * Skip if posting
         */
        if ($_SERVER['REQUEST_METHOD'] == 'POST') {
            $this->cache_reject_reason = 'Requested method is POST';

            return false;
        }

        /**
         * Skip if there is query in the request uri
         */
        if (!$this->_config->get_boolean('pgcache.cache.query') && strstr($this->_request_uri, '?') !== false) {
            $this->cache_reject_reason = 'Requested URI contains query';

            return false;
        }

        /**
         * Check request URI
         */
        if (!in_array($_SERVER['PHP_SELF'], $this->_config->get_array('pgcache.accept.files')) && !$this->_check_request_uri()) {
            $this->cache_reject_reason = 'Requested URI is rejected';

            return false;
        }

        /**
         * Check User Agent
         */
        if (!$this->_check_ua()) {
            $this->cache_reject_reason = 'User agent is rejected';

            return false;
        }

        /**
         * Check WordPress cookies
         */
        if (!$this->_check_cookies()) {
            $this->cache_reject_reason = 'Cookie is rejected';

            return false;
        }

        /**
         * Skip if user is logged in or user role is logged in
         */
        if ($this->_config->get_boolean('pgcache.reject.logged') ) {
            if (!$this->_check_logged_in()) {
                $this->cache_reject_reason = 'User is logged in';
                return false;
            }
        } else {
            if (!$this->_check_logged_in_role_allowed()) {
                $this->cache_reject_reason = 'An user role is logged in';
                return false;
            }
        }

        return true;
    }

    /**
     * Checks if can we do cache logic
     *
     * @param string $buffer
     * @return boolean
     */
    function _can_cache2(&$buffer) {
        /**
         * Skip if caching is disabled
         */
        if (!$this->_caching) {
            return false;
        }

        /**
         * Check for database error
         */
        if (w3_is_database_error($buffer)) {
            $this->cache_reject_reason = 'Database error occurred';

            return false;
        }

        /**
         * Check for DONOTCACHEPAGE constant
         */
        if (defined('DONOTCACHEPAGE') && DONOTCACHEPAGE) {
            $this->cache_reject_reason = 'DONOTCACHEPAGE constant is defined';

            return false;
        }

        /**
         * Check hostname
         */
        if ((!w3_is_multisite() || (w3_is_multisite() && !w3_force_master())) &&
            $this->_config->get_boolean('pgcache.check.domain') && w3_get_host() != w3_get_home_domain()) {
            $this->cache_reject_reason = 'Hostname mismatch';

            return false;
        }

        /**
         * Don't cache 404 pages
         */
        if (!$this->_config->get_boolean('pgcache.cache.404') && function_exists('is_404') && is_404()) {
            $this->cache_reject_reason = 'Page is 404';

            return false;
        }

        /**
         * Don't cache homepage
         */
        if (!$this->_config->get_boolean('pgcache.cache.home') && function_exists('is_home') && is_home()) {
            $this->cache_reject_reason = is_front_page() && is_home() ? 'Page is front page' : 'Page is posts page';

            return false;
        }

        /**
         * Don't cache front page
         */
        if ($this->_config->get_boolean('pgcache.reject.front_page') && function_exists('is_front_page') && is_front_page() && !is_home()) {
            $this->cache_reject_reason = 'Page is front page';

            return false;
        }


        /**
         * Don't cache feed
         */
        if (!$this->_config->get_boolean('pgcache.cache.feed') && function_exists('is_feed') && is_feed()) {
            $this->cache_reject_reason = 'Page is feed';

            return false;
        }

        /**
         * Check if page contains dynamic tags
         */
        if ($this->_enhanced_mode && $this->_has_dynamic($buffer)) {
            $this->cache_reject_reason = 'Page contains dynamic tags (mfunc or mclude) can not be cached in enhanced mode';

            return false;
        }

        return true;
    }

    /**
     * Returns cache object
     *
     * @return W3_Cache_Base
     */
    function _get_cache() {
        static $cache = array();

        if (!isset($cache[0])) {
            $engine = $this->_config->get_string('pgcache.engine');

            switch ($engine) {
                case 'memcached':
                    $engineConfig = array(
                        'servers' => $this->_config->get_array('pgcache.memcached.servers'),
                        'persistant' => $this->_config->get_boolean('pgcache.memcached.persistant')
                    );
                    break;

                case 'file':
                    $engineConfig = array(
                        'section' => 'page',
                        'flush_parent' => (w3_get_blog_id() == 0),
                        'locking' => $this->_config->get_boolean('pgcache.file.locking'),
                        'flush_timelimit' => $this->_config->get_integer('timelimit.cache_flush')
                    );
                    break;

                case 'file_generic':
                    if (w3_get_blog_id() == 0)
                        $flush_dir = W3TC_CACHE_PAGE_ENHANCED_DIR;
                    else
                        $flush_dir = W3TC_CACHE_PAGE_ENHANCED_DIR . '/' . w3_get_domain(w3_get_host());

                    $engineConfig = array(
                        'exclude' => array(
                            '.htaccess'
                        ),
                        'expire' => $this->_lifetime,
                        'cache_dir' => W3TC_CACHE_PAGE_ENHANCED_DIR,
                        'locking' => $this->_config->get_boolean('pgcache.file.locking'),
                        'flush_timelimit' => $this->_config->get_integer('timelimit.cache_flush'),
                        'flush_dir' => $flush_dir,
                    );
                    break;

                default:
                    $engineConfig = array();
            }

            $engineConfig['use_expired_data'] = true;
            $engineConfig['module'] = 'pgcache';
            $engineConfig['host'] = w3_get_host();
            $engineConfig['instance_id'] = w3_get_instance_id();

            w3_require_once(W3TC_LIB_W3_DIR . '/Cache.php');
            $cache[0] = W3_Cache::instance($engine, $engineConfig);
        }

        return $cache[0];
    }

    /**
     * Checks request URI
     *
     * @return boolean
     */
    function _check_request_uri() {
        $auto_reject_uri = array(
            'wp-login',
            'wp-register'
        );

        foreach ($auto_reject_uri as $uri) {
            if (strstr($this->_request_uri, $uri) !== false) {
                return false;
            }
        }

        $reject_uri = $this->_config->get_array('pgcache.reject.uri');
        $reject_uri = array_map('w3_parse_path', $reject_uri);

        foreach ($reject_uri as $expr) {
            $expr = trim($expr);
            if ($expr != '' && preg_match('~' . $expr . '~i', $this->_request_uri)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Checks User Agent
     *
     * @return boolean
     */
    function _check_ua() {
        w3_require_once(W3TC_LIB_W3_DIR . '/Request.php');

        $uas = $this->_config->get_array('pgcache.reject.ua');

        $uas = array_merge($uas, array(W3TC_POWERED_BY));

        foreach ($uas as $ua) {
            if (!empty($ua)) {
                if (isset($_SERVER['HTTP_USER_AGENT']) && stristr($_SERVER['HTTP_USER_AGENT'], $ua) !== false)
                    return false;
            }
        }

        return true;
    }

    /**
     * Checks WordPress cookies
     *
     * @return boolean
     */
    function _check_cookies() {
        foreach (array_keys($_COOKIE) as $cookie_name) {
            if ($cookie_name == 'wordpress_test_cookie') {
                continue;
            }
            if (preg_match('/^(wp-postpass|comment_author)/', $cookie_name)) {
                return false;
            }
        }

        foreach ($this->_config->get_array('pgcache.reject.cookie') as $reject_cookie) {
            if (!empty($reject_cookie)) {
                foreach (array_keys($_COOKIE) as $cookie_name) {
                    if (strstr($cookie_name, $reject_cookie) !== false) {
                        return false;
                    }
                }
            }
        }

        return true;
    }

    /**
     * Check if user is logged in
     *
     * @return boolean
     */
    function _check_logged_in() {
        foreach (array_keys($_COOKIE) as $cookie_name) {
            if (strpos($cookie_name, 'wordpress_logged_in') === 0)
                return false;
        }

        return true;
    }

    /**
     * Check if logged in user role is allwed to be cached
     *
     * @return boolean
     */
    function _check_logged_in_role_allowed() {
        if (!$this->_config->get_boolean('pgcache.reject.logged_roles'))
             return true;
        $roles = $this->_config->get_array('pgcache.reject.roles');
        
        if (empty($roles))
            return true;
        
        foreach (array_keys($_COOKIE) as $cookie_name) {
            if (strpos($cookie_name, 'w3tc_logged_') === 0) {
                foreach($roles as $role) {
                    if(strstr($cookie_name, md5(NONCE_KEY . $role)))
                        return false;
                }
            }
        }

        return true;
    }

    /**
     * Checks if rules file present and creates it if not
     */
    function _check_rules_present() {
        if (w3_is_nginx())
            return;   // nginx store it in a single file

        w3_require_once(W3TC_INC_DIR . '/functions/rule_cut.php');

        $filename = w3_get_pgcache_rules_cache_path();
        if (file_exists($filename))
            return;

        $plugin = w3_instance('W3_Plugin_PgCacheAdmin');
        try {
            $plugin->write_rules_cache();
        } catch (Exception $ex) {}
    }

    /**
     * Compress data
     *
     * @param string $data
     * @param string $compression
     * @return string
     */
    function _compress(&$data, $compression) {
        switch ($compression) {
            case 'gzip':
                $data = gzencode($data);
                break;

            case 'deflate':
                $data = gzdeflate($data);
                break;
        }
    }

    /**
     * Returns current mobile group
     *
     * @return string
     */
    function _get_mobile_group() {
        if ($this->_mobile) {
            return $this->_mobile->get_group();
        }

        return '';
    }

    /**
     * Returns current referrer group
     *
     * @return string
     */
    function _get_referrer_group() {
        if ($this->_referrer) {
            return $this->_referrer->get_group();
        }

        return '';
    }

    /**
     * Returns current encryption
     *
     * @return string
     */
    function _get_encryption() {
        if (w3_is_https()) {
            return 'ssl';
        }

        return '';
    }

    /**
     * Returns current compression
     *
     * @return boolean
     */
    function _get_compression() {
        if ($this->_debug)   // cannt generate/use compressed files during debug mode
            return false;

        if (!w3_zlib_output_compression() && !headers_sent() && !$this->_is_buggy_ie()) {
            $compressions = $this->_get_compressions();

            foreach ($compressions as $compression) {
                if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && stristr($_SERVER['HTTP_ACCEPT_ENCODING'], $compression) !== false) {
                    return $compression;
                }
            }
        }

        return false;
    }

    /**
     * Returns array of compressions
     *
     * @return array
     */
    function _get_compressions() {
        $compressions = array(
            false
        );

        if ($this->_config->get_boolean('browsercache.enabled') && $this->_config->get_boolean('browsercache.html.compression') && function_exists('gzencode')) {
            $compressions[] = 'gzip';
        }

        return $compressions;
    }

    /**
     * Returns array of response headers
     *
     * @return array
     */
    function _get_response_headers() {
        $headers = array();

        if (function_exists('headers_list')) {
            $headers_list = headers_list();
            if ($headers_list) {
                foreach ($headers_list as $header) {
                    $pos = strpos($header, ':');
                    if ($pos) {
                        $header_name = substr($header, 0, $pos);
                        $header_value = substr($header, $pos+1);
                    } else {
                        $header_name = $header;
                        $header_value = '';
                    }
                    $headers[$header_name] = $header_value;
                }
            }
        }

        return $headers;
    }

    /**
     * Checks for buggy IE6 that doesn't support compression
     *
     * @return boolean
     */
    function _is_buggy_ie() {
        if (isset($_SERVER['HTTP_USER_AGENT'])) {
            $ua = $_SERVER['HTTP_USER_AGENT'];

            if (strpos($ua, 'Mozilla/4.0 (compatible; MSIE ') === 0 && strpos($ua, 'Opera') === false) {
                $version = (float) substr($ua, 30);

                return ($version < 6 || ($version == 6 && strpos($ua, 'SV1') === false));
            }
        }

        return false;
    }

    /**
     * Returns array of data headers
     *
     * @return array
     */
    function _get_cached_headers() {
        $data_headers = array();
        $cache_headers = $this->_config->get_array('pgcache.cache.headers');
        $response_headers = $this->_get_response_headers();

        foreach ($response_headers as $header_name => $header_value) {
            foreach ($cache_headers as $cache_header_name) {
                if (strcasecmp($header_name, $cache_header_name) == 0) {
                    $data_headers[$header_name] = $header_value;
                }
            }
        }

        return $data_headers;
    }

    /**
     * Returns page key
     *
     * @param string $mobile_group
     * @param string $referrer_group
     * @param string $encryption
     * @param string $compression
     * @param string $content_type
     * @param string $request_uri
     * @return string
     */
    function _get_page_key($mobile_group = '', $referrer_group = '', 
        $encryption = '', $compression = '', $content_type = '', $request_uri = '') {

        if ($request_uri)
            $key = substr($request_uri, strtolower(substr($request_uri, 0, 8)) == 'https' ? 8: 7);
        else
            $key = $this->_request_host . $this->_request_uri;

        // replace fragment
        $key = preg_replace('~#.*$~', '', $key);

        if ($this->_enhanced_mode) {
            // URL decode
            $key = urldecode($key);

            // replace double slashes
            $key = preg_replace('~[/\\\]+~', '/', $key);

            // replace query string
            $key = preg_replace('~\?.*$~', '', $key);

            // replace index.php
            $key = str_replace('/index.php', '/', $key);

            // trim slash
            $key = ltrim($key, '/');

            if ($key && substr($key, -1) != '/') {
                $key .= '/';
            }

            $key .= '_index';
        } else {
            $key = md5($key);
        }

        /**
         * Append mobile group
         */
        if ($mobile_group) {
            $key .= '_' . $mobile_group;
        }

        /**
         * Append referrer group
         */
        if ($referrer_group) {
            $key .= '_' . $referrer_group;
        }

        /**
         * Append encryption
         */
        if ($encryption) {
            $key .= '_' . $encryption;
        }

        if ($this->_enhanced_mode) {
            /**
             * Append HTML extension.
             * For nginx - we create .xml cache entries and redirect to them
             */
            if (w3_is_nginx() && substr($content_type, 0, 8) == 'text/xml' &&
                  $this->_config->get_boolean('pgcache.cache.nginx_handle_xml'))
                $key .= '.xml';
            else
                $key .= '.html';
        }

        /**
         * Append compression
         */
        if ($compression) {
            $key .= '_' . $compression;
        }

        return $key;
    }

    /**
     * Returns debug info
     *
     * @param boolean $cache
     * @param string $reason
     * @param boolean $status
     * @param double $time
     * @return string
     */
    function _get_debug_info($cache, $reason, $status, $time) {
        $engine = $this->_config->get_string('pgcache.engine');
        $debug_info = "<!-- W3 Total Cache: Page cache debug info:\r\n";
        $debug_info .= sprintf("%s%s\r\n", str_pad('Engine: ', 20), w3_get_engine_name($engine));
        $debug_info .= sprintf("%s%s\r\n", str_pad('Cache key: ', 20), $this->_page_key);
        $debug_info .= sprintf("%s%s\r\n", str_pad('Caching: ', 20), ($cache ? 'enabled' : 'disabled'));

        if (!$cache) {
            $debug_info .= sprintf("%s%s\r\n", str_pad('Reject reason: ', 20), $reason);
        }

        if ($engine != 'file_generic')
            $debug_info .= sprintf("%s%s\r\n", str_pad('Status: ', 20), ($status ? 'cached' : 'not cached'));
        $debug_info .= sprintf("%s%.3fs\r\n", str_pad('Creation Time: ', 20), $time);

        $headers = $this->_get_response_headers();

        if (count($headers)) {
            $debug_info .= "Header info:\r\n";

            foreach ($headers as $header_name => $header_value) {
                $debug_info .= sprintf("%s%s\r\n", str_pad($header_name . ': ', 20), w3_escape_comment($header_value));
            }
        }

        $debug_info .= '-->';

        return $debug_info;
    }

    /**
     * Sends headers
     *
     * @param array $headers
     * @return boolean
     */
    function _headers($headers) {
        if (!headers_sent()) {
            foreach ($headers as $name => $value) {
                $header = ($name == 'Status' ? $value : $name . ': ' . $value);
                @header($header);
            }

            return true;
        }

        return false;
    }

    /**
     * Sends headers
     * @param boolean $is_404
     * @param string $etag
     * @param integer $time
     * @param string $compression
     * @param array $custom_headers
     * @return boolean
     */
    function _send_headers($is_404, $time, $etag, $compression, $custom_headers = array()) {
        $exit = false;
        $headers = array();
        $curr_time = time();

        $bc_lifetime = $this->_config->get_integer(
            'browsercache.html.lifetime');

        $expires = (is_null($time)? $curr_time: $time) + $bc_lifetime;
        $max_age = ($expires > $curr_time ? $expires - $curr_time : 0);

        if ($is_404) {
            /**
             * Add 404 header
             */
            $headers = array_merge($headers, array(
                'Status' => 'HTTP/1.1 404 Not Found'
            ));
        } elseif ((!is_null($time) && $this->_check_modified_since($time)) || $this->_check_match($etag)) {
            /**
             * Add 304 header
             */
            $headers = array_merge($headers, array(
                'Status' => 'HTTP/1.1 304 Not Modified'
            ));

            /**
             * Don't send content if it isn't modified
             */
            $exit = true;
        }

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

            if ($this->_config->get_boolean('browsercache.html.last_modified')) {
                $headers = array_merge($headers, array(
                    'Last-Modified' => w3_http_date($time)
                ));
            }

            if ($this->_config->get_boolean('browsercache.html.expires')) {
                $headers = array_merge($headers, array(
                    'Expires' => w3_http_date($expires)
                ));
            }

            if ($this->_config->get_boolean('browsercache.html.cache.control')) {
                switch ($this->_config->get_string('browsercache.html.cache.policy')) {
                    case 'cache':
                        $headers = array_merge($headers, array(
                            'Pragma' => 'public',
                            'Cache-Control' => 'public'
                        ));
                        break;

                    case 'cache_public_maxage':
                        $headers = array_merge($headers, array(
                            'Pragma' => 'public',
                            'Cache-Control' => sprintf('max-age=%d, public', $max_age)
                        ));
                        break;

                    case 'cache_validation':
                        $headers = array_merge($headers, array(
                            'Pragma' => 'public',
                            'Cache-Control' => 'public, must-revalidate, proxy-revalidate'
                        ));
                        break;

                    case 'cache_noproxy':
                        $headers = array_merge($headers, array(
                            'Pragma' => 'public',
                            'Cache-Control' => 'public, must-revalidate'
                        ));
                        break;

                    case 'cache_maxage':
                        $headers = array_merge($headers, array(
                            'Pragma' => 'public',
                            'Cache-Control' => sprintf('max-age=%d, public, must-revalidate, proxy-revalidate', $max_age)
                        ));
                        break;

                    case 'no_cache':
                        $headers = array_merge($headers, array(
                            'Pragma' => 'no-cache',
                            'Cache-Control' => 'max-age=0, private, no-store, no-cache, must-revalidate'
                        ));
                        break;
                }
            }

            if ($this->_config->get_boolean('browsercache.html.etag')) {
                $headers = array_merge($headers, array(
                    'Etag' => $etag
                ));
            }

            if ($this->_config->get_boolean('browsercache.html.w3tc')) {
                $headers = array_merge($headers, array(
                    'X-Powered-By' => W3TC_POWERED_BY
                ));
            }
        }

        $vary ='';
        //compressed && UAG
        if ($compression && $this->_get_mobile_group()) {
            $vary = 'Accept-Encoding,User-Agent,Cookie';
            $headers = array_merge($headers, array(
                'Content-Encoding' => $compression
            ));
            //compressed
        } elseif ($compression) {
            $vary = 'Accept-Encoding';
            $headers = array_merge($headers, array(
                'Content-Encoding' => $compression
            ));
            //uncompressed && UAG
        } elseif ($this->_get_mobile_group()) {
            $vary = 'User-Agent,Cookie';
        }

        //Add Cookie to vary if user logged in and not previously set
        if (!$this->_check_logged_in() && strpos($vary, 'Cookie') === false) {
            if ($vary)
                $vary .= ',Cookie';
            else
                $vary = 'Cookie';
        }

        /**
         * Add vary header
         */
        $headers = array_merge($headers, array(
            'Vary' => $vary));

        /**
         * Add custom headers
         */
        $headers = array_merge($headers, $custom_headers);

        /**
         * Disable caching for preview mode
         */
        if (w3_is_preview_mode()) {
            $headers = array_merge($headers, array(
                'Pragma' => 'private',
                'Cache-Control' => 'private'
            ));
        }

        /**
         * Send headers to client
         */
        $result = $this->_headers($headers);

        if ($exit) {
            exit();
        }

        return $result;
    }

    /**
     * Check if content was modified by time
     * @param integer $time
     * @return boolean
     */
    function _check_modified_since($time) {
        if (!empty($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
            $if_modified_since = $_SERVER['HTTP_IF_MODIFIED_SINCE'];

            // IE has tacked on extra data to this header, strip it
            if (($semicolon = strrpos($if_modified_since, ';')) !== false) {
                $if_modified_since = substr($if_modified_since, 0, $semicolon);
            }

            return ($time == strtotime($if_modified_since));
        }

        return false;
    }

    /**
     * Check if content was modified by etag
     * @param string $etag
     * @return boolean
     */
    function _check_match($etag) {
        if (!empty($_SERVER['HTTP_IF_NONE_MATCH'])) {
            $if_none_match = (get_magic_quotes_gpc() ? stripslashes($_SERVER['HTTP_IF_NONE_MATCH']) : $_SERVER['HTTP_IF_NONE_MATCH']);
            $client_etags = explode(',', $if_none_match);

            foreach ($client_etags as $client_etag) {
                $client_etag = trim($client_etag);

                if ($etag == $client_etag) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Bad Behavior support
     * @return void
     */
    function _bad_behavior() {
        $bb_file = $this->_config->get_cache_option('pgcache.bad_behavior_path');
        if ($bb_file != '')
            require_once $bb_file;
    }

    /**
     * Parses dynamic tags
     * @return string
     */
    function _parse_dynamic(&$buffer) {
        $buffer = preg_replace_callback('~<!--\s*mfunc(.*)-->(.*)<!--\s*/mfunc\s*-->~Uis', array(
            &$this,
            '_parse_dynamic_mfunc'
        ), $buffer);

        $buffer = preg_replace_callback('~<!--\s*mclude(.*)-->(.*)<!--\s*/mclude\s*-->~Uis', array(
            &$this,
            '_parse_dynamic_mclude'
        ), $buffer);
    }

    /**
     * Parse dynamic mfunc callback
     * @param array $matches
     * @return string
     */
    function _parse_dynamic_mfunc($matches) {
        $code1 = trim($matches[1]);
        $code2 = trim($matches[2]);
        $code = ($code1 ? $code1 : $code2);

        if ($code) {
            $code = trim($code, ';') . ';';

            ob_start();
            $result = eval($code);
            $output = ob_get_contents();
            ob_end_clean();

            if ($result === false) {
                $output = sprintf('Unable to execute code: %s', htmlspecialchars($code));
            }
        } else {
            $output = htmlspecialchars('Invalid mfunc tag syntax. The correct format is: <!-- mfunc PHP code --><!-- /mfunc --> or <!-- mfunc -->PHP code<!-- /mfunc -->.');
        }

        return $output;
    }

    /**
     * Parse dynamic mclude callback
     * @param array $matches
     * @return string
     */
    function _parse_dynamic_mclude($matches) {
        $file1 = trim($matches[1]);
        $file2 = trim($matches[2]);

        $file = ($file1 ? $file1 : $file2);

        if ($file) {
            $file = ABSPATH . $file;

            if (file_exists($file) && is_readable($file)) {
                ob_start();
                include $file;
                $output = ob_get_contents();
                ob_end_clean();
            } else {
                $output = sprintf('Unable to open file: %s', htmlspecialchars($file));
            }
        } else {
            $output = htmlspecialchars('Incorrect mclude tag syntax. The correct format is: <!-- mclude path/to/file.php --><!-- /mclude --> or <!-- mclude -->path/to/file.php<!-- /mclude -->.');
        }

        return $output;
    }

    /**
     * Checks if buffer has dynamic tags
     *
     * @param string $buffer
     * @return boolean
     */
    function _has_dynamic(&$buffer) {
        return preg_match('~<!--\s*m(func|clude)(.*)-->(.*)<!--\s*/m(func|clude)\s*-->~Uis', $buffer);
    }

    /**
     * Check whether requested page has content type that can be cached
     * @return bool
     */
    private function _is_cacheable_content_type() {
        $content_type = '';
        $headers = headers_list();
        foreach($headers as $header) {
            if (strpos($header, 'Content-Type') !==false) {
                $temp = array_shift(explode(';', $header));
                $temp = explode(':', $temp);
                $content_type = trim($temp[1]);
            }
        }

        $cache_headers = apply_filters('w3tc_is_cacheable_content_type',
            array('application/json', 'text/html', 'text/xml', 'application/xhtml+xml'));
        return in_array($content_type, $cache_headers);
    }
}
