<?php

/**
  * BLOG:CMS: PHP/MySQL Personal Content Management System (CMS)
  * http://blogcms.com/
  * ----------------------------------------------------------------
  *
  * Copyright (C) 2003-2005 Radek HULÁN
  * http://hulan.cz/contact/
  *
  * ----------------------------------------------------------------
  * This program is free software; you can redistribute it and/or
  * modify it under the terms of the GNU General Public License
  * as published by the Free Software Foundation; either version 2
  * of the License, or (at your option) any later version.
**/

                                                                             
global $DIR_PLUGINS;

if (!is_dir($DIR_PLUGINS)) die('System is not configured properly - NP_Cache.php');

require($DIR_PLUGINS.'cache/Lite.php');

function cache_getmicrotime(){ 
  list($usec, $sec) = explode(" ",microtime()); 
  return ((float)$usec + (float)$sec); 
} 

class Cache_Lite_Output extends Cache_Lite{

  function Cache_Lite_Output($options){  
    $this->Cache_Lite($options);   
  }
    
  function doParse($content,$skinType) {
    global $CONF, $blogid, $manager, $blog;
	if (!isset($blogid)) $blogid = $CONF['DefaultBlog'];
  	$blog =& $manager->getBlog($blogid);
  	$skinid = $blog->getDefaultSkin();
  	$skin = new SKIN($skinid);
    $handler = new ACTIONS($skinType);
    $parser = new PARSER(SKIN::getAllowedActionsForType($skinType), $handler);
    $parser->setProperty('IncludeMode',$skin->getIncludeMode());
    $parser->setProperty('IncludePrefix',$skin->getIncludePrefix());
    $handler->setSkin($skin);
    $handler->parser =& $parser;
    $parser->parse($content);
  }

  function myparseCache($data,$skinType,$skinId){
    $first=true;
    $skinId=strval($skinId);
    do { 
      $pos1=strpos($data,'<!-- DYNAMIC_CONTENT_START -->');
      if ($pos1!==false){
        $pos2=strpos(substr($data,$pos1+30),'<!-- DYNAMIC_CONTENT_STOP -->');
        if ($pos2!==false){
          if ($first) {
            $first=false;
			if (is_numeric($skinId))
            		$query=sql_query("select scontent from ".sql_table('skin')." where sdesc=$skinId and stype='$skinType'");
			else
				$query=sql_query("select skin as scontent from ".sql_table('plug_extra_skin')." where url='$skinId'");
            if (!$row=sql_fetch_object($query)) {$pos1=false; $pos2=false; break;}
            $skin=sql_unescape($row->scontent);
          }
          $pos1_skin=strpos($skin,'<!-- DYNAMIC_CONTENT_START -->');
          $pos2_skin=strpos(substr($skin,$pos1_skin+30),'<!-- DYNAMIC_CONTENT_STOP -->');
          echo substr($data,0,$pos1);
          $this->doParse(substr($skin,$pos1_skin+30,$pos2_skin),$skinType);
          $data=substr($data,$pos1+30+$pos2+29);
          $skin=substr($skin,$pos1_skin+30+$pos2_skin+29);
        }
      }
    } while ( ($pos1!==false) && ($pos2!==false) );
    echo $data;
  }
  
  function startcache($id, $group, $gzip, $starttime, $skinType, $skinId){
    $data = $this->get($skinId.$id, $group);
    if ($data !== false) {
        global $np_cache_gzip_started;
        // GZip
        $obStarted=false;
        if ($gzip==1 && !$np_cache_gzip_started) {
          $obStarted = smartStartGzip();
          $np_cache_gzip_started=true;
          if ($obStarted) $data=str_replace('CACHE_USING_GZIP',_YES,$data);
        }
        if ($np_cache_gzip_started)
          $data=str_replace('CACHE_USING_GZIP',_YES,$data);
        else
          $data=str_replace('CACHE_USING_GZIP',_NO,$data);
        // calculate cache parse time
        $time = cache_getmicrotime()-$starttime;
        $time= strval(floatval(intval($time*100000))/100000);
        $data=str_replace('CACHE_PARSE_TIME',$time,$data);
        $data=str_replace('CACHE_DATA',_YES,$data);
        // parse and echo cache
        $this->myparseCache($data,$skinType,$skinId);
        // GZip
        if ($obStarted) {
          $np_cache_gzip_started=false;
          ob_end_flush();
        }
        return true;
    } else {
        // start caching the page
        ob_start();
        ob_implicit_flush(false);
        return false;
    }
  }
  
  function endcache($date, $gzip, $starttime){
    // calculate cache parse time
    $time = cache_getmicrotime()-$starttime;
    $time= strval(floatval(intval($time*100000))/100000);
    // get cache
    $data = ob_get_contents();
    ob_end_clean();
    // set last cache refresh
    $data=str_replace('CACHE_CREATED_TIME',date($date,time()),$data);
    // ********** SAVE CACHE **********
    $this->save($data, $this->_id, $this->_group);
	// replace conditional comments
	$data=str_replace('<!-- DYNAMIC_CONTENT_START -->','',$data);
	$data=str_replace('<!-- DYNAMIC_CONTENT_STOP -->','',$data);
    // replace creating time string
    $data=str_replace('CACHE_PARSE_TIME',$time,$data);
    // data from cache
    $data=str_replace('CACHE_DATA',_NO,$data);
    // GZip
    $obStarted=false;
    global $np_cache_gzip_started;
    if ($gzip==1 && !$np_cache_gzip_started)
    {
      $obStarted = smartStartGzip();
      $np_cache_gzip_started=true;
      if ($obStarted) $data=str_replace('CACHE_USING_GZIP',_YES,$data);
    }
    if ($np_cache_gzip_started)
      $data=str_replace('CACHE_USING_GZIP',_YES,$data);
    else
      $data=str_replace('CACHE_USING_GZIP',_NO,$data);
    // display it
    echo($data);
    // GZip
    if ($obStarted) {
      $np_cache_gzip_started=false;
      ob_end_flush();
    }
  }

}

class NP_Cache extends NucleusPlugin {
                                                                             
  function getName() { return 'Cache'; }
  function getAuthor()  { return 'Radek HULAN'; }
  function getURL()  { return 'http://hulan.cz/blog/item/np-cache-v1-2-now-you-re-able-to-mix-static-and-dynamic-content-in-your-skins'; }
  function getVersion() { return '1.2'; }
  function getDescription() {return 'Plugin to automatically cache pages and speed-up whole BLOG:CMS! Index, member and archivelist pages are refreshed every X minutes, or after adding/deleting/editing an item/comment. You can use following skinVar for complete cache parsing to display statistical data: <%Cache%> or <%Cache(stats)%>. If you want to cache only parts of skins, you have to use keywords: <%Cache(start,name-of-part1)%> <%if(notincache)%> ... skin part 1 ... <%Cache(stop,name-of-part1)%> <%endif%> in your skins. Last option is to cache complete pages, but include dynamic content inside <!-- DYNAMIC_CONTENT_START --> and <!-- DYNAMIC_CONTENT_STOP --> tags.';}
                                                                             
	function supportsFeature($feature) {
		switch($feature) {
			case 'SqlTablePrefix': return 1;
			default: return 0;
		}
	}

  function getEventList() { 
    return array(
      'PreSkinParse',
      'PostSkinParse',
      'PostAddItem',
      'PreUpdateItem',
      'PostDeleteItem',
      'PreDeleteItem',
      'PreAddComment',
      'PreDeleteComment',
      'PreAddCategory',
      'PreDeleteCategory',
      'PrepareCommentForEdit',
      'PostDeleteBlog',
      'PostPluginOptionsUpdate'
    ); 
  }
  
  
  function install() {
	$this->createOption('timeindex','Lifetime: *index* in minutes:','text','60');
	$this->createOption('timeitem','Lifetime: *item* in minutes:','text','60');
	$this->createOption('timearchive','Lifetime: *archive* in minutes:','text','720');
	$this->createOption('timearchivelist','Lifetime: *archivelist* in minutes:','text','720');
	$this->createOption('timemember','Lifetime: *member* in minutes:','text','720');
	$this->createOption('typeindex','Caching type: *index* skin:','select','0', 'complete page|0|only parts I specify|1');
	$this->createOption('typeitem','Caching type: *item* skin:','select','0', 'complete page|0|only parts I specify|1');
	$this->createOption('typearchive','Caching type: *archive* skin:','select','0', 'complete page|0|only parts I specify|1');
	$this->createOption('typearchivelist','Caching type: *archivelist* skin:','select','0', 'complete page|0|only parts I specify|1');
	$this->createOption('typemember','Caching type: *member* skin:','select','0', 'complete page|0|only parts I specify|1');
	$this->createOption('timeall','Lifetime: for partially parsed pages in minutes:','text','720');
	$this->createOption('url','Cache URL relative to DIR_MEDIA (should *not* end with a slash; must have "chmod 777"):','text','.cache');
	$this->createOption('date','Format for stamp of last cache refresh','text','H:i:s, d.m.y');
	$this->createOption('textcache','Text to display (as a skinVar) for cache:','textarea',"<h2>Cache</h2><p>it took: CACHE_PARSE_TIME seconds to create this page | page created at: CACHE_CREATED_TIME | using GZip: CACHE_USING_GZIP | cached: CACHE_DATA</p>");
	$this->createOption('textgzip','Text to display (as a skinVar) for gzip:','textarea',"<h2>Cache</h2><p>using GZip: yes</p>");
	$this->createOption('comment','Clean index cache when adding a comment?','select','1', 'no|0|yes|1');
	$this->createOption('gzip','Enable GZIP compression?','select','0', 'no|0|yes|1');
	$this->createOption('cache','Cache plugin enabled?','select','1', 'no|0|yes|1');
	$this->createOption('clean','Clean all cache completely now?','select','0', 'no|0|yes|1');
  }
  
  function setCurrentBlog($itemid){
    $query=sql_query('select iblog from '.sql_table('item').' where inumber='.strval($itemid));
    if ($row=sql_fetch_object($query))
      $this->currentblog=strval($row->iblog);
    else
      $this->currentblog="1";
    $this->currentblog='_b'.$this->currentblog;
  }


  function getCurrentBlog(){
    // current blog id
    global $manager, $blog, $CONF; 
    if ($blog) $b =& $blog; else $b =& $manager->getBlog($CONF['DefaultBlog']); 
    $this->currentblog=strval($b->getID());
    if ($this->currentblog=='') $this->currentblog='1';
    $this->currentblog='_b'.$this->currentblog;
  }
  
  function init(){
    // cache options
    global $DIR_MEDIA;
    $url=$DIR_MEDIA.$this->getOption('url').'/';
    $this->cacheoptions = array();
    $this->typeoptions = array();
    $this->skintypes = array('index','item','archive','archivelist','member');
    foreach ($this->skintypes as $skin) {
      $this->cacheoptions[$skin] = array(
        'cacheDir' => $url,
        'lifeTime' => 60*intval($this->getOption('time'.$skin))
      );      
      $this->typeoptions[$skin] = intval($this->getOption('type'.$skin));
    }
    $this->timeall=intval($this->getOption('timeall'));
    $this->cacheoptions['all'] = array(
      'cacheDir' => $url,
      'lifeTime' => 60*$this->timeall
    );      
    $this->cacheoptions['clean'] = array(
      'cacheDir' => $url,
      'lifeTime' => 0
    );      
    // static parameters
    $this->date=$this->getOption('date');
    $this->comment=intval($this->getOption('comment'));
    $this->gzip=intval($this->getOption('gzip'));
    $this->cache=intval($this->getOption('cache'));
    $this->textcache=$this->getOption('textcache');
    $this->textgzip=$this->getOption('textgzip');
  }
  
  function getItemID($s){
    // normal links
    if (strpos($s,'itemid=')!==false) {
      $s=substr($s,strpos($s,'itemid=')+7);
      ereg("^([0-9]+)(.*)$",$s,$r);
      if (!empty($r[1])) return $r[1]; else return $s;
    }
    // fancy link
    if (strpos($s,'item/')!==false) $s=substr($s,strpos($s,'item/')+5);
	if (isset($_GET['item'])) $s=getVar('item');
    // numeric url?
    if (is_numeric($s)) return $s;
    // fancier link
    ereg("^([0-9a-zA-Z_-]+)(.*)$",$s,$r);
    $s=$r[1];
    $query=sql_query("select inumber from ".sql_table('item')." where iurltitle='$s'");
    if ($row=sql_fetch_object($query)) $s=strval($row->inumber);
    // old fancier link
    $query=sql_query("select inumber from ".sql_table('plugin_fancierurl')." where iurltitle='$s'");
    if ($row=sql_fetch_object($query)) $s=strval($row->inumber);
    // item not found
    return $s;
  }

  function startGZip(){
    global $np_cache_gzip_started;
    $this->obStarted=false;
    if ($this->gzip==1 && !$np_cache_gzip_started)
    {
      $this->obStarted = smartStartGzip();
      $np_cache_gzip_started=true;
    }
  }
  
  function event_PreSkinParse(&$data) {
   global $member, $np_cache_active, $np_cache_gzip_started,$CONF;
   if (isset($data['skin']->id)) $this->skinid=$data['skin']->id;
   if (isset($data['skinid'])) $this->skinid=$data['skinid'];
   if (isset($_GET['skinid'])) $this->skinid=intval($_GET['skinid']);
   $np_cache_gzip_started=false;
   $this->starttime=cache_getmicrotime();
   $np_cache_active=1;
   // cache not enabled
   if ($this->cache==0) return;
   // cache enabled, filter out conditional skin parts
   $np_cache_active=0;
   // current blog
   $this->getCurrentBlog();
   // get URI
   $this->id=serverVar('REQUEST_URI');
   $this->id=str_replace('index.php','',$this->id);
   // ignore anchors
   if (strpos($this->id,'#')!==false) $this->id=substr($this->id,0,strpos($this->id,'#'));
   // do not cache preview, pending comments, and results of single and multiple polls
   if (strpos($this->id,'pending=')!==false || 
       strpos($this->id,'cpreview=')!==false ||
       strpos($this->id,'results=')!==false ||
       strpos($this->id,'multiple=')!==false ) 
   {
     $this->startGZip();
     return;
   }
   // start COMPLETE CACHE, do not cache search for COMPLETE pages
   if (in_array($data['type'],$this->skintypes)) {
     if ($this->typeoptions[$data['type']]==0) { // COMPLETE caching
       // item id
       $this->itemid='';
       if ($data['type']=='item') {
         $this->id=$this->getItemID($this->id);
         $this->itemid='_i'.$this->id;
       }
       // special chars
	   preg_match_all('/[a-zA-Z0-9]+/', $this->id, $n);
	   $this->id = implode('-', $n[0]);
       // root dir?
       if (empty($this->id)) $this->id='rootdir';
       // category filter for item
	   global $catid;
	   if (($data['type']=='item') && isset($catid) && intval($catid)>0) $this->id.='_cat'.$catid;
	   // different pages for admins
	   if ($member->isAdmin() && $member->isLoggedIn()) $this->id.='_admin';
       // + .html extension
	   $this->id.='.html';
       // start caching
       $this->cache = new Cache_Lite_Output($this->cacheoptions[$data['type']]);
       // data in cache, output and exit
       if ( $this->cache->startcache( $this->id, $data['type'].$this->itemid.$this->currentblog, $this->gzip, $this->starttime, $data['type'], $this->skinid ) ) die;
       // caching...
       $this->cachestarted=true;  
     } else 
       $this->startGZip(); // otherwise just start GZip
   } else
     $this->startGZip(); // otherwise just start GZip
  }
  
  function event_PostSkinParse(&$data) {
   // output this page
   if ($this->cachestarted) 
     $this->cache->endcache($this->date,$this->gzip,$this->starttime);
   else
   // flush GZip
   global $np_cache_gzip_started;
   if ($np_cache_gzip_started) 
   if ($this->obStarted) {
     $np_cache_gzip_started=false;
     ob_end_flush();
   }
  }

  function cleanItem($id){
    // remove cache for this item page
	  $this->cleanArray( array( 'item'.'_i'.strval($id) ) );
  }

  function cleanArray($arr){
    $cache=new Cache_Lite($this->cacheoptions['clean']); 
    foreach ($arr as $s) $cache->clean($s.$this->currentblog);
  }
  
  function cleanAll(){ 
    $cache=new Cache_Lite($this->cacheoptions['clean']); 
    $cache->clean();
  }
  
  // when adding a new comment, we have to create new item page
	function event_PreAddComment(&$data) { 
	  $this->setCurrentBlog($data['comment']['itemid']);
    $this->cleanItem($data['comment']['itemid']); 
    if ($this->comment==1) $this->cleanArray(array('index'));
  }
  // updating a comment - clean item page
  function event_PrepareCommentForEdit(&$data) {
    $query=sql_query('select citem from  '.sql_table('comment').' where cnumber='.strval($data['comment']['commentid']));
    if ($row=sql_fetch_object($query)) {
      $this->setCurrentBlog($row->citem);
      $this->cleanItem($row->citem);
      if ($this->comment==1) $this->cleanArray(array('index'));
    }
  }
  // when deleting a comment, we have to create new item page, index
	function event_PreDeleteComment(&$data) {
	  $query=sql_query('select citem from '.sql_table('comment').' where cnumber='.strval($data['commentid']));
	  if ($row=sql_fetch_object($query)){
	    $this->setCurrentBlog($row->citem);
      $this->cleanItem($row->citem);
      $this->cleanArray(array('index'));
    }
	}

  // when adding an item, we have to create new index, and archive pages
	function event_PostAddItem(&$data) { 
	  $this->setCurrentBlog($data['itemid']);
    $this->cleanArray(array('index','archivelist','archive')); 
    $query=sql_query('select iblog from '.sql_table('item').' where inumber='.strval($data['itemid']));
    if ($row=sql_fetch_object($query)){
      $blogid=strval($row->iblog);
      // delete cache for a previous item
      $query=sql_query('select inumber, UNIX_TIMESTAMP(itime) as mytime from '.sql_table('item').' where iblog='.$blogid.' and idraft=0 and inumber<'.strval($data['itemid']).' order by mytime desc limit 0,1');
      if ($row=sql_fetch_object($query)) $this->cleanItem($row->inumber);
    }
  }
  // when updating an item, we have to create new index page and item page
  function event_PreUpdateItem(&$data) { 
    $this->setCurrentBlog($data['itemid']);
    $this->cleanItem($data['itemid']); 
    $this->cleanArray(array('index')); 
  }
  // when deleting an item, we have to create new index, and archive pages
  function event_PreDeleteItem(&$data) { 
    $this->setCurrentBlog($data['itemid']);
    $this->cleanItem($data['itemid']); 
  }
	function event_PostDeleteItem(&$data) { 
    $this->cleanArray(array('index','archivelist','archive'));
  }
	
  // when adding a category, we have to create new index, and archive pages
	function event_PreAddCategory(&$blog) { 
	  $this->currentblog='_b'.strval($blog['blog']->settings['bnumber']);
	  $this->cleanArray(array('index','archivelist','archive'));	
  }
  // when deleting a category, we have to create new index, and archive pages
	function event_PreDeleteCategory(&$data) { 
    $query=sql_query('select cblog from '.sql_table('category').' where catid='.strval($data['catid']));
    if ($row=sql_fetch_object($query))
      $this->currentblog=strval($row->cblog);
    else
      $this->currentblog="1";
    $this->currentblog='_b'.$this->currentblog;
    $this->cleanArray(array('index','archivelist','archive'));	
  }
	
	// delete all cached data when deleting a blog
	function event_PostDeleteBlog(&$data) { 
    $this->cleanAll(); 
  }

  // clean whole cache
	function event_PostPluginOptionsUpdate(&$data) {
		if ($this->getOption('clean')=='1') {
      $this->cleanAll();
      $this->setOption('clean','0');
		}
	}
	
	function doSkinVar($skinType,$type='',$cachepart='default',$time=0) { 
   // cache not enabled
   if ($this->cache==0) return;

   global $np_cache_active, $np_cache_gzip_started, $member;
	 $type=strtolower($type);

	 // COMPLETE caching info
	 if ($type=='' || $type=='stats') {
	   if ($this->cachestarted || ($np_cache_active==1)) echo $this->textcache;
	   if ( (isset($this->typeoptions[$skinType]) && ($this->typeoptions[$skinType]==0)) || !isset($this->typeoptions[$skinType]) )
     if ($np_cache_gzip_started) 
       echo $this->textgzip;
   }

       
   // PARTIAL caching START
   if ($this->typeoptions[$skinType]==1 || !isset($this->typeoptions[$skinType])) // PARTIAL caching
   if ($type=='start') {
     // current blog
     $this->getCurrentBlog();
     // PARTIAL caching file ID
     $this->id=serverVar('REQUEST_URI');
     $this->id=str_replace('index.php','',$this->id);
     // ignore anchors
     if (strpos($this->id,'#')!==false) $this->id=substr($this->id,0,strpos($this->id,'#'));
     // item id
     $this->itemid='';
     if ($skinType=='item') {
       $this->id=$this->getItemID($this->id);
       $this->itemid='_i'.$this->id;
     }
     // special chars
  	 preg_match_all('/[a-zA-Z0-9]+/', $this->id."_p".$cachepart, $n);
  	 $this->id = implode('-', $n[0]);
     // root dir?
     if (empty($this->id)) $this->id='rootdir';
     // .html extension
  	 $this->id.='.html';
     // start caching
     $this->cacheoptions['all']['lifeTime']=$this->timeall*60;
     if (isset($time)) if (intval($time)>0) $this->cacheoptions['all']['lifeTime']=intval($time)*60;
     $this->cache = new Cache_Lite_Output($this->cacheoptions['all']);
     // data in cache, output and exit
     if ( $this->cache->startcache( $this->id, $skinType.$this->itemid.$this->currentblog, $this->gzip, $this->starttime, $skinType, $this->skinid ) ) {
        // output everything from cache and do not parse skins
        $np_cache_active=0;
        return;
     } else 
       // now caching
       $np_cache_active=1;
   }
   
   // PARTIAL caching STOP
   if ($this->typeoptions[$skinType]==1 || !isset($this->typeoptions[$skinType])) // PARTIAL caching
   if ($type=='stop') 
   if ($np_cache_active==1){
     $this->cache->endcache($this->date,$this->gzip,$this->starttime);
     $np_cache_active=0;
   }
   
  }
  
}
?>