e107inc/e107

View on GitHub
e107_handlers/cache_handler.php

Summary

Maintainability
A
2 hrs
Test Coverage
C
72%
<?php
/*
 * e107 website system
 *
 * Copyright (C) 2008-2010 e107 Inc (e107.org)
 * Released under the terms and conditions of the
 * GNU General Public License (http://www.gnu.org/licenses/gpl.txt)
 *
 * Cache handler
 *
 * $URL$
 * $Id$
*/

if (!defined('e107_INIT')) { exit; }

define('CACHE_PREFIX','<?php exit; ?>');

/**
 * Class to cache data as files, improving site speed and throughput.
 * FIXME - pref independant cache handler, cache drivers
 *
 * @package     e107
 * @subpackage    e107_handlers
 * @version     $Id$
 * @author      e107 Inc
 */
class ecache {

    public $CachePageMD5;
    public $CachenqMD5;
    public $UserCacheActive;            // Checkable flag - TRUE if user cache enabled
    public $SystemCacheActive;            // Checkable flag - TRUE if system cache enabled
    private $lastError;
    private $lastFile;

    const CACHE_PREFIX = '<?php exit; ?>';

    function __construct()
    {
        //$this->UserCacheActive = e107::getPref('cachestatus');
        //$this->SystemCacheActive = e107::getPref('syscachestatus');
    }

    /**
     * Set the MD5 Hash
     */
    public function setMD5($text, $hash=true)
    {
        if($text === null)
        {
            $this->CachePageMD5 = md5(e_BASE.e_LANGUAGE.THEME.USERCLASS_LIST.defset('e_QUERY').filemtime(THEME.'theme.php'));
            return $this;
        }

        $this->CachePageMD5 = ($hash === true) ? md5($text) : $text;
        return $this;
    }


    /**
     * @return mixed
     */
    public function getMD5()
    {
        return $this->CachePageMD5;
    }

    /**
     * @param $CacheTag
     * @param bool $syscache
     * @return string
     * @desc Internal class function that returns the filename of a cache file based on the query.
     * @scope private
     * If the tag begins 'menu_', e_QUERY is not included in the hash which creates the file name
     */
    function cache_fname($CacheTag, $syscache = false)
    {
        if(strpos($CacheTag, "nomd5_") === 0) {
            // Add 'nomd5' to indicate we are not calculating an md5
            $CheckTag = '_nomd5';
        }
        elseif (isset($this) && $this instanceof ecache)
        {
            if (defined("THEME"))
            {
                if (strpos($CacheTag, "nq_") === 0)
                {
                    // We do not care about e_QUERY, so don't use it in the md5 calculation
                    if (!$this->CachenqMD5)
                    {
                        $this->CachenqMD5 = md5(e_BASE.(defined("ADMIN") && ADMIN == true ? "admin" : "").e_LANGUAGE.THEME.USERCLASS_LIST.filemtime(THEME.'theme.php'));
                    }
                    // Add 'nq' to indicate we are not using e_QUERY
                    $CheckTag = '_nq_'.$this->CachenqMD5;

                }
                else
                {
                    // It's a page - need the query in the hash
                    if (!$this->CachePageMD5)
                    {
                        $this->CachePageMD5 = md5(e_BASE.e_LANGUAGE.THEME.USERCLASS_LIST.defset('e_QUERY').filemtime(THEME.'theme.php'));
                    }
                    $CheckTag = '_'.$this->CachePageMD5;
                }
            }
            else
            {
                // Check if a custom CachePageMD5 is in use in e_module.php.
                $CheckTag = ($this->CachePageMD5) ? "_".$this->CachePageMD5 : "";
            }
        }
        else
        {
            $CheckTag = '';
        }
        $q = ($syscache ? "S_" : "C_").preg_replace("#\W#", "_", $CacheTag);
        
        if($syscache === true)
        {
            $CheckTag = ''; // no MD5 on system cache. XXX To be Checked. 
        }

        $fname = e_CACHE_CONTENT.$q.$CheckTag.'.cache.php';
        //echo "cache f_name = $fname <br />";
        $this->lastFile = $fname;
        return $fname;
    }

    /**
     * Retrieve Cache data.
     * @param $CacheTag
     * @param bool|int $MaximumAge the time in minutes before the cache file 'expires'
     * @param bool $ForcedCheck check even if cache pref is disabled.
     * @param bool $syscache set to true when checking sys cache.
     * @return string
     * @desc Returns the data from the cache file associated with $query, else it returns false if there is no cache for $query.
     * @scope public
     */
    public function retrieve($CacheTag, $MaximumAge = false, $ForcedCheck = false, $syscache = false)
    {
        if(($ForcedCheck != false ) || ($syscache == false && $this->UserCacheActive) || ($syscache == true && $this->SystemCacheActive) && !e107::getParser()->checkHighlighting())
        {
            $cache_file = (isset($this) && $this instanceof ecache ? $this->cache_fname($CacheTag, $syscache) : self::cache_fname($CacheTag, $syscache));

            if(file_exists($cache_file))
            {
                if ($MaximumAge !== false && (filemtime($cache_file) + ($MaximumAge * 60)) < time()) {
                    unlink($cache_file);
                    return false;
                }
                else
                {
                    $ret = file_get_contents($cache_file);

                    if($ret === false)
                    {
                        $this->lastError = "Couldn't read ".$cache_file;
                    }

                    if (strpos($ret, self::CACHE_PREFIX) === 0)
                    {
                        $ret = substr($ret, strlen(self::CACHE_PREFIX));
                    }
                    elseif(strpos($ret, '<?php exit;') === 0)
                    {
                        $ret = substr($ret, 11);
                    }
                    elseif(strpos($ret,'<?php') === 0)
                    {
                        $ret = substr($ret, 5);        // Handle the history for now
                    }

                    return $ret;
                }
            }
            else
            {
                $this->lastError = "Cache file not found: ".$cache_file;
                return false;
            }
        }
        return false;
    }

    /**
     * Return the last error encountered during cache processing.
     * @return mixed
     */
    public function getLastError()
    {
        return $this->lastError;
    }

    /**
     * Return the last error encountered during cache processing.
     * @return mixed
     */
    public function getLastFile()
    {
        return $this->lastFile;
    }

    /**
     * @param string $CacheTag
     * @param bool $MaximumAge the time in minutes before the cache file 'expires'
     * @param bool $ForcedCheck
     * @return string
     * @desc Returns the data from the cache file associated with $query, else it returns false if there is no cache for $query.
     * @scope public
     */
    function retrieve_sys($CacheTag, $MaximumAge = false, $ForcedCheck = false)
    {
        if(isset($this) && $this instanceof ecache)
        {
            return $this->retrieve($CacheTag, $MaximumAge, $ForcedCheck, true);
        }
        else
        {
            return self::retrieve($CacheTag, $MaximumAge, $ForcedCheck, true);
        }
    }


    /**
     *
     * @param string $CacheTag - name of tag for future retrieval - should NOT contain an MD5. 
     * @param string $Data - data to be cached
     * @param boolean $ForceCache [optional] if TRUE, writes cache even when disabled in admin prefs. 
     * @param boolean $bRaw [optional] if TRUE, writes data exactly as provided instead of prefacing with php leadin
     * @param boolean $syscache [optional]
     * @return void|null
     */
    public function set($CacheTag, $Data, $ForceCache = false, $bRaw=0, $syscache = false)
    {
        if(defined('E107_INSTALL') && E107_INSTALL === true)
        {
            return null;
        }

        if(($ForceCache != false ) || ($syscache == false && $this->UserCacheActive) || ($syscache == true && $this->SystemCacheActive) && !e107::getParser()->checkHighlighting())
        {
            $cache_file = (isset($this) && $this instanceof ecache ? $this->cache_fname($CacheTag, $syscache) : self::cache_fname($CacheTag, $syscache));
            @file_put_contents($cache_file, ($bRaw? $Data : self::CACHE_PREFIX.$Data) );
            @chmod($cache_file, 0755); //Cache should not be world-writeable
            @touch($cache_file);
        }
    }

    /**
    * @return void
    * @param string $CacheTag - name of tag for future retrieval - should NOT contain an MD5
    * @param string $Data - data to be cached
    * @param bool   $ForceCache (optional, default false) - if TRUE, writes cache even when disabled
    * @param bool   $bRaw (optional, default false) - if TRUE, writes data exactly as provided instead of prefacing with php leadin
    * @desc Creates / overwrites the cache file for $query, $text is the data to store for $query.
    * @scope public
    */
    function set_sys($CacheTag, $Data, $ForceCache = false, $bRaw=0)
    {
        if(isset($this) && $this instanceof ecache)
        {
            $this->set($CacheTag, $Data, $ForceCache, $bRaw, true);
        }
        else
        {
            self::set($CacheTag, $Data, $ForceCache, $bRaw, true);            
        }
    }


    /**
     * Deletes cache files. If $query is set, deletes files named {$CacheTag}*.cache.php, if not it deletes all cache files - (*.cache.php)
     *
     * @param string $CacheTag
     * @param boolean $syscache
     * @param boolean $related clear also 'nq_' and 'nomd5_' entries
     * @return bool
     *
     */
    public function clear($CacheTag = '', $syscache = false, $related = false)
    {

        $file = ($CacheTag) ? preg_replace("#\W#", "_", $CacheTag)."*.cache.php" : "*.cache.php";
        e107::getEvent()->triggerAdminEvent('cache_clear', "cachetag=$CacheTag&file=$file&syscache=$syscache");
        e107::getEvent()->trigger('cache_clear', ['tag'=>$CacheTag, 'file'=>$file, 'system'=>$syscache]);
        $ret = self::delete(e_CACHE_CONTENT, $file, $syscache);

        if($CacheTag && $related) //TODO - too dirty - add it to the $file pattern above
        {
            self::delete(e_CACHE_CONTENT, 'nq_'.$file, $syscache);
            self::delete(e_CACHE_CONTENT, 'nomd5_'.$file, $syscache);
            //ecache::delete(e_CACHE_CONTENT, 'nq_'.$file, $syscache);
            //ecache::delete(e_CACHE_CONTENT, 'nomd5_'.$file, $syscache);
        }
        return $ret;
    }

    /**
    * @return bool
    * @param string $CacheTag
    * @desc Deletes cache files. If $query is set, deletes files named {$CacheTag}*.cache.php, if not it deletes all cache files - (*.cache.php)
    */
    function clear_sys($CacheTag = '', $related = false)
    {


        if(isset($this) && $this instanceof ecache)
        {
            return $this->clear($CacheTag, true, $related);
        }
        else
        {
            self::clear($CacheTag, true, $related);
        //    ecache::clear($CacheTag, true, $related);
        }
    }

    /**
    * @return bool
    * @param string $dir
    * @param string $pattern
    * @desc Internal class function to allow deletion of cache files using a pattern, default '*.*'
    * @scope private
    */
    function delete($dir, $pattern = "*.*", $syscache = false) {
        $pattern = ($syscache ? "S_" : "C_").$pattern;
        $pattern = str_replace(array("\*", "\?"), array(".*", "."), preg_quote($pattern));
        if (substr($dir, -1) != "/") {
            $dir .= "/";
        }
        if (is_dir($dir))
        {
             $d = opendir($dir);
            while ($file = readdir($d)) {
                if (is_file($dir.$file) && preg_match("/^{$pattern}$/", $file)) {
                    unlink($dir.$file);
                }
            }
            closedir($d);
            return true;
        } else {
            return false;
        }
    }
    
    
    /**
     * Clear Full Cache
     * @param string $type: content | system| browser | db | image | js | css | library
     * @example clearAll('db');
     */
     
    function clearAll($type,$mask = null)
    {        
        $path = null;

        $event = e107::getEvent();

        if($type =='content')
        {
            $this->clear();
            $event->trigger('cache_clear_all', ['type'=>$type,'mask'=>$mask]);
            return;
        }
            
        if($type === 'system')
        {
            $this->clear_sys();
            $event->trigger('cache_clear_all', ['type'=>$type,'mask'=>$mask]);
            return;    
        }

        if($type === 'browser')
        {
            e107::getConfig()->set('e_jslib_browser_cache', time())->save(false);
            $event->trigger('cache_clear_all', ['type'=>$type,'mask'=>$mask]);
            return;    
        }

        if($type === 'db')
        {
            $path = e_CACHE_DB;
            $mask = ($mask == null) ? '.*\.php' : $mask;
        }

        if($type === 'image')
        {
            $path = e_CACHE_IMAGE;
            $mask = ($mask == null) ? '.*(\.cache\.bin|\.jpg|\.jpeg|\.png|\.gif)' : $mask;
        }

        if($type === 'js')
        {
            $path = e_WEB."cache/";
            $mask = ($mask == null) ? '.*\.js' : $mask;
        }

        if($type === 'css')
        {
            $path = e_WEB."cache/";
            $mask = ($mask == null) ? '.*\.css' : $mask;
        }

        if($type === 'library')
        {
            $path = e_CACHE_CONTENT;
            $mask = ($mask == null) ? 'S_Library_.*\.cache\.php' : $mask;
        }

        if((null == $path) || (null == $mask))
        {
            return;
        }
        
        $fl = e107::getFile(false);
        $fl->mode = 'fname';
        $files = $fl->get_files($path, $mask);

        if($files)
        {
            foreach ($files as $file)
            {
                unlink($path.$file);
            }
        }

        $event->trigger('cache_clear_all', ['type'=>$type,'mask'=>$mask]);
    }
    
}