<?php
/**
 * sh404SEF - SEO extension for Joomla!
 *
 * @author      Yannick Gaultier
 * @copyright   (c) Yannick Gaultier 2014
 * @package     sh404SEF
 * @license     http://www.gnu.org/copyleft/gpl.html GNU/GPL
 * @version     4.4.4.1791
 * @date		2014-07-01
 */

// Security check to ensure this file is being included by a parent file.
if (!defined('_JEXEC')) die('Direct Access to this location is not allowed.');

// load a few constants
require_once JPATH_ROOT . '/administrator/components/com_sh404sef/defines.php';

//define('SH_SHOW_CACHE_STATS', 0);

// URL Cache management

global $shURLDiskCache, $shURLMemCache, $shURLCacheFileName, $shURLTotalCount, $shURLCacheCreationDate;
global $shURLCacheMisses, $shURLCacheHits, $shURLCacheMissesList,$shURLRam, $shCacheFileLocked;

$shURLCacheMisses = 0;
$shURLCacheMissesList = array();
$shURLCacheHits = 0;

$shURLDiskCache = null;
$shURLMemCache = null;
$shURLCacheFileName = sh404SEF_FRONT_ABS_PATH.'cache/shCacheContent.php';
$shURLTotalCount = 0;  // v 1.2.4.c added total cache size control
$shURLCacheCreationDate = null;

$sefConfig = & Sh404sefFactory::getConfig();
if (!empty($sefConfig->shUseURLCache)) {
  register_shutdown_function('shWriteURLCacheToDisk');
}

/**
 * A small class to implement a file-access locking mechanism
 * @author Yannick Gaultier
 *
 */
class Shlock {

  // max number of attempts to acquire the file if already locked
  var $_maxAttempts;
  // microseconds to wait between attempts to acquire an already locked lock
  var $_maxWait;
  // time to live in seconds for a lock
  var $_lockTtl;
  // in the future, we may use the db as backend to store locks
  var $_backend;
  // default path to store lock files (if using file backend)
  var $_path;
  // extension to use for lock file
  var $_extension;

  function __construct( $backend = 'file', $path = '', $attempts = 1, $maxWait = 0, $lockTtl = 20, $extension = '.shlock') {
    $this->_maxAttempts = $attempts;
    $this->_maxWait = $maxWait;
    $this->_lockTtl = $lockTtl;  // 2 secs defaults
    $this->_backend = $backend;
    $this->_path = empty( $path) ? sh404SEF_FRONT_ABS_PATH . 'cache/' : $path;
    $this->_extension = $extension;
  }

  public static function &getInstance($backend = 'file') {
    static $_shlock = null;

    if (is_null($_shlock)) {
      $_shlock = new Shlock($backend);
    }
    return $_shlock;
  }

  function acquire( $name) {
    if (empty( $name)) {
      return false;
    }

    // call backend
    $backend = '_acquire'.ucfirst( $this->_backend);
    $result = method_exists( $this, $backend) ? $this->$backend( $name) : false;

    return $result;
  }

  function release( $name) {

    if (empty( $name)) {
      return false;
    }

    // call backend
    $backend = '_release'.ucfirst( $this->_backend);
    $result = method_exists( $this, $backend) ? $this->$backend( $name) : false;

    return $result;

  }

  function _acquireFile( $name) {
    if (empty( $name)) {
      return false;
    }
    $attempts = 0;
    $fileName = $this->_path . $name . $this->_extension;
    do {
      // directly attempts to create the lock. If it fails, then element is already locked
      $handle = @fopen( $fileName, 'x');
      if ($handle) {
        // we could create file for writing, element is not locked.
        // store time, close and return
        $result = fwrite( $handle, time());
        fclose($handle);
        // return true if we could write the time stamp
        return $result !== false;
      } else {
        $attempts++;
        // we could not create lock file, element is already locked
        // read lock file, and check TTL
        $lockTime = file_get_contents( $fileName);
        $lockTime = (int) JString::trim( $lockTime);
        if ((time() - $lockTime) > $this->_lockTtl) {
          // existing lock has timed out, we can release it
          $this->release( $name);
        } else {
          // element is locked by another process
          // go to sleep for a while. Won't work in windows if php less than 5.0.0
          // but should not have side effects, only we'll lost
          // some data that could have been saved to the cache
          if ( $attempts != 0 && $attempts < $this->_maxAttempts) {
            usleep( $this->_maxWait);
          }
        }
      }
    }
    while ($attempts != 0 && $attempts < $this->_maxAttempts);

    // if we get here, we could not get the lock
    return false;
  }

  function _releaseFile( $name) {
    if (empty( $name)) {
      return false;
    }

    $fileName = $this->_path . $name . $this->_extension;
    // simply delete the lock file
    $result = is_file( $fileName) ? unlink( $fileName) : false;
  }

}

function sh_var_export( $cache, $start) {
  // export content of array $cache, inserting a numeric key starting at $start
  $size = count($cache);
  if (empty($size)) return '';
  $ret = '';
  for ($i=0; $i<$size; $i++) {  // use for instead of foreach to reduce memory usage
    // new version, smaller RAM footprint
    $ret .= "\n".'$shURLDiskCache['.$start++.']=\''.$cache[$i].'\';';
  }
  // new version, less ram footprint

  return $ret;
}

function shWriteURLCacheToDisk() {
  global $shURLDiskCache, $shURLMemCache, $shURLCacheFileName, $shURLCacheCreationDate, $shCacheFileLocked;

  $sefConfig = & Sh404sefFactory::getConfig();;

  // If we acquired lock at page load, we can write
  if (count($shURLMemCache) && $shCacheFileLocked) {
    $now = time();
    if (!file_exists($shURLCacheFileName)) {
      $cache = '<?php // shCache : URL cache file for sh404SEF
//'.$sefConfig->version.'
if (!defined(\'_JEXEC\')) die(\'Direct Access to this location is not allowed.\');
$shURLCacheCreationDate = '.$now.';'."\n";
    } else {
      $cache = '<?php'."\n";
      // check cache TTL
      if (empty($shURLCacheCreationDate)){  // file exists, but creation date is missing : we are upgrading from a previous version
        $status = stat($shURLCacheFileName);  // lets's read from file status : use last change date as creation date
        if (!empty($status)) {
          $shURLCacheCreationDate = $status[9];
          $cache .= "\n".'$shURLCacheCreationDate='.$shURLCacheCreationDate.";\n";
        }
      }
      if (SH404SEF_URL_CACHE_TTL && mt_rand(1, SH404SEF_URL_CACHE_WRITES_TO_CHECK_TTL) == 1) { // probability = 1/SH404SEF_WRITES_TO_CLEAN_LOGS
        if (!empty($shURLCacheCreationDate)){  // if we have a valid creation date, check  TTL
          if (($now-$shURLCacheCreationDate) > SH404SEF_URL_CACHE_TTL*3600) { // cache must be cleared
            $GLOBALS['shURLDiskCache'] = array();
            unlink($shURLCacheFileName);
            $shURLCacheCreationDate = $now;
            $cache = '<?php // shCache : URL cache file for sh404SEF
//'.$sefConfig->version.'
if (!defined(\'_JEXEC\')) die(\'Direct Access to this location is not allowed.\');
$shURLCacheCreationDate = '.$now.';'."\n";
          }
        }
      }
    }
    $count = count( $shURLDiskCache);
    $cache .= sh_var_export( $shURLMemCache, $count); // only need to write memory cache, ie: those URL added since last read of cache from disk
    $cache .= "\n".'?'.'>';
    $cacheFile=fopen( $shURLCacheFileName,'ab');
    if ($cacheFile) {
      fwrite( $cacheFile, $cache);
      fclose( $cacheFile);
    }

  }

  if ($shCacheFileLocked) {
    // in any case, release lock to file
    $lock = & Shlock::getInstance();
    if($lock) {
      $lock->release( 'shCacheContent');
      $shCacheFileLocked = false;
    }
  }
}

// load cached URL from disk into an array in memory
function shLoadURLCache() {
  global $shURLDiskCache, $shURLCacheFileName, $shURLTotalCount, $shURLMemCache, $shURLRam, $shCacheFileLocked;
  static $shDiskCacheLoaded = false;
  if (!$shDiskCacheLoaded) {
    $shCacheFileLocked = false;
    ShlSystem_Log::debug( 'sh404sef', 'Cache not loaded - trying to load '.$shURLCacheFileName);
    if (file_exists( $shURLCacheFileName)) {
      $startMem = function_exists('memory_get_usage')? memory_get_usage():'unavailable';
      ShlSystem_Log::debug( 'sh404sef', 'Including cache file (mem = '.$startMem.')');
      $GLOBALS['shURLDiskCache'] = array();  // erase global, not local copy
      $lock = & Shlock::getInstance();
      // we try lock the cache file until the end of the request
      // so as to avoid other concurrent requests writing to it
      // while we have some pending data
      if ($lock && $lock->acquire('shCacheContent')) {
        $shCacheFileLocked = true;
      }
      include($shURLCacheFileName);
      $endMem = function_exists('memory_get_usage')? memory_get_usage():'unavailable';
      $shURLRam = $startMem == 'unavailable' ? $startMem: $endMem-$startMem;
      $shDiskCacheLoaded = !empty($shURLDiskCache);
      $shURLTotalCount = !empty($shURLDiskCache) ? count($shURLDiskCache) : 0;
      ShlSystem_Log::debug( 'sh404sef', 'Cache file included : '.($startMem == 'unavailable' ? $startMem: $endMem-$startMem).' bytes used, '.$shURLTotalCount.' URLs');
    } else {
      // cache file not there, create it
      $now = time();
      $sefConfig = & Sh404sefFactory::getConfig();
      $cache = '<?php // shCache : URL cache file for sh404SEF
//'.$sefConfig->version.'
if (!defined(\'_JEXEC\')) die(\'Direct Access to this location is not allowed.\');
$shURLCacheCreationDate = '.$now.';'."\n";
      $cache .= "\n".'?'.'>';
      // lock cache file before using it
      $lock = & Shlock::getInstance();
      if ($lock && $lock->acquire('shCacheContent')) {
        $shCacheFileLocked = true;
        $cacheFile=fopen( $shURLCacheFileName,'ab');
        if ($cacheFile) {
          fwrite( $cacheFile, $cache);
          fclose( $cacheFile);
        }
      }
      $GLOBALS['shURLDiskCache'] = array();
      $shDiskCacheLoaded = true; // we don't want to try again if it failed first time
      ShlSystem_Log::debug( 'sh404sef', 'Cache file does not exists');
    }
  }
}

// fetch an URL from cache, return null if not found
function shGetSefURLFromCache($string, &$url) {
  global $shURLCacheMisses, $shURLCacheHits, $shURLCacheMissesList;

  $sefConfig = & Sh404sefFactory::getConfig();

  if (!$sefConfig->shUseURLCache) {
    $url = null;
    $shURLCacheMisses += 1;
    return sh404SEF_URLTYPE_NONE;
  }
  shLoadURLCache();
  $diskCacheSize = count($GLOBALS['shURLDiskCache']);
  $memCacheSize = count($GLOBALS['shURLMemCache']);
  if (empty($diskCacheSize) && empty($memCacheSize)) {
    $url = null;
    $shURLCacheMisses += 1;
    return sh404SEF_URLTYPE_NONE;
  }
  $string = htmlentities( $string, ENT_QUOTES);
  for ($i=0; $i<$diskCacheSize; $i++) {
    if (strpos($GLOBALS['shURLDiskCache'][$i], $string) !== false) {
      $tmp = explode('#', $GLOBALS['shURLDiskCache'][$i]);  // cache format : non-sef#sef#type
      if ($string == $tmp[0]) {
        $url = $tmp[1];
        ShlSystem_Log::debug( 'sh404sef', 'Retrieved SEF from disk cache : '.$url.' => '.html_entity_decode( $tmp[0], ENT_QUOTES).'('.$tmp[2].')');
        $shURLCacheHits += 1;
        return $tmp[2];
      }
    }
  }
  for ($i=0; $i<$memCacheSize; $i++) {
    if (strpos($GLOBALS['shURLMemCache'][$i], $string) !== false) {
      $tmp = explode('#', $GLOBALS['shURLMemCache'][$i]);  // cache format : non-sef#sef#type
      if ($string == $tmp[0]) {
        $url = $tmp[1];
        _log('Retrieved SEF from mem cache : '.$url.' => '.html_entity_decode( $tmp[0], ENT_QUOTES).'('.$tmp[2].')');
        $shURLCacheHits += 1;
        return $tmp[2];
      }
    }
  }
  $shURLCacheMisses += 1;
  $shURLCacheMissesList[] = $string;
  return sh404SEF_URLTYPE_NONE;
}

// fetch an URL from cache, return null if not found
function shGetNonSefURLFromCache($string, &$url) {

  $sefConfig = & Sh404sefFactory::getConfig();

  if (!$sefConfig->shUseURLCache) {
    $url = null;
    return sh404SEF_URLTYPE_NONE;
  }
  shLoadURLCache();
  $diskCacheSize = count($GLOBALS['shURLDiskCache']);
  $memCacheSize = count($GLOBALS['shURLMemCache']);
  if (empty($diskCacheSize) && empty($memCacheSize)) {
    $url = null;
    return sh404SEF_URLTYPE_NONE;
  }
  for ($i=0; $i<$diskCacheSize; $i++) {
    if (strpos($GLOBALS['shURLDiskCache'][$i], $string) !== false) {
      $tmp = explode('#', $GLOBALS['shURLDiskCache'][$i]);  // cache format : non-sef#sef#type
      $nonSef = html_entity_decode( $tmp[0], ENT_QUOTES);
      if ($string == $tmp[1]) {
        $url = $nonSef;
        ShlSystem_Log::debug( 'sh404sef', 'Retrieved Non SEF from disk cache : '.$url.' => '.$tmp[1].'('.$tmp[2].')');
        return $tmp[2];
      }
    }
  }
  for ($i=0; $i<$memCacheSize; $i++) {
    if (strpos($GLOBALS['shURLMemCache'][$i], $string) !== false) {
      $tmp = explode('#', $GLOBALS['shURLMemCache'][$i]);  // cache format : non-sef#sef#type
      $nonSef = html_entity_decode( $tmp[0], ENT_QUOTES);
      if ($string == $tmp[1]) {
        $url = $nonSef;
        ShlSystem_Log::debug( 'sh404sef', 'Retrieved Non SEF from mem cache : '.$url.' => '.$tmp[1].'('.$tmp[2].')');
        return $tmp[2];
      }
    }
  }
  return sh404SEF_URLTYPE_NONE;
}

function shAddSefURLToCache( $nonSefURL, $sefURL, $URLType) {
  global $shURLMemCache, $shURLTotalCount;

  $sefConfig = & Sh404sefFactory::getConfig();

  if (!$sefConfig->shUseURLCache) return null;
  if ($shURLTotalCount >= $sefConfig->shMaxURLInCache) return null;  // v 1.2.4.c added total cache size control
  // Filter out non sef url which include &mosmsg, as I don't want to have a cache entry for every single msg
  // that can be thrown at me, including every 404 error
  if (strpos(strtolower($nonSefURL), '&mosmsg')) return null;
  $count = count($shURLMemCache);
  // new cache format : non-sef#sef#type
  $shURLMemCache[$count] = htmlentities( $nonSefURL, ENT_QUOTES).'#'.$sefURL.'#'.$URLType;
  ShlSystem_Log::debug( 'sh404sef', 'Adding to URL cache : '.$sefURL.' <= '.$nonSefURL);
  $shURLTotalCount++;  // v 1.2.4.c added total cache size control
  return true;
}

function shRemoveURLFromCache( $nonSefURLList) {
  global $shURLMemCache, $shURLDiskCache, $shURLTotalCount;

  $sefConfig = & Sh404sefFactory::getConfig();
  if (!$sefConfig->shUseURLCache || empty($nonSefURLList)) return null;
  $foundInDiskCache = false;
  $foundInMemCache = false;
  foreach ($nonSefURLList as $nonSefURL) {
    if (!empty($shURLMemCache)) {
      foreach ($shURLMemCache as $key => $cacheItem) { // look up in memory cache
        $tmp = explode('#', $cacheItem);
        $cacheNonSef = html_entity_decode( $tmp[0], ENT_QUOTES);
        if ($cacheNonSef == $nonSefURL) {
          unset($shURLMemCache[$key]);
          $shURLTotalCount--;
          $foundInMemCache = true;
        }
      }
    }
    if (!empty($shURLDiskCache)) {
      foreach ($shURLDiskCache as $key => $cacheItem) {  // look up disk cache
        $tmp = explode('#', $cacheItem);
        $cacheNonSef = html_entity_decode( $tmp[0], ENT_QUOTES);
        if ($cacheNonSef == $nonSefURL) {
          unset($shURLDiskCache[$key]);
          $shURLTotalCount--;
          $foundInDiskCache = true;
        }
      }
    }
  }
  if ($foundInMemCache) {
    $shURLMemCache = array_values($shURLMemCache); // simply reindex mem cache
    return;
  }
  if ($foundInDiskCache) { // we need to remove these url from the disk cache file
    // to make it simpler, I simply rewrite the complete file
    $shURLMemCache = (empty($shURLMemCache) ?
    array_values($shURLDiskCache)
    :array_merge($shURLDiskCache, $shURLMemCache));
    $shURLDiskCache = array();  // don't need disk cache anymore, as all URL are in mem cache
    // so we remove both on disk cache and in memory copy of on disk cache
    $cacheFile = sh404SEF_FRONT_ABS_PATH.'cache/shCacheContent.php';
    if (file_exists($cacheFile)) {
      unlink($cacheFile);
    }
    // no need to write new URL list in disk file, as this will be done automatically at shutdown
  }
}

function shShowCacheStats() {
  global $shURLCacheMisses, $shURLCacheMissesList, $shURLCacheHits, $shURLRam, $shURLTotalCount;

  $cacheTotal = $shURLCacheMisses+$shURLCacheHits;
  $out = 'Cache hits   : '. $shURLCacheHits . "  [".(int)(100*$shURLCacheHits/$cacheTotal) .']<br />';
  $out .= 'Cache misses : '. $shURLCacheMisses . "  [".(int)(100*$shURLCacheMisses/$cacheTotal). ']<br />';
  $out .=  'Cache total  : '. $cacheTotal . '<br />';
  $out .=  'In cache  : '. $shURLTotalCount . '<br />';
  $out .=  'Ram  : '. $shURLRam . '<br />';
  $out .=  '<br /><br /><br />Misses list';
  foreach($shURLCacheMissesList as $url) {
    $out .=  '<pre>'.$url.'</pre><br />';
  }
  return $out;
}



