<?php
/**
* @version	$Id: template_cache.php 13993 2010-10-19 09:09:38Z alex $
* @package	In-Portal
* @copyright	Copyright (C) 1997 - 2009 Intechnic. All rights reserved.
* @license      GNU/GPL
* In-Portal is Open Source software.
* This means that this software may have been modified pursuant
* the GNU General Public License, and as distributed it includes
* or is derivative of works licensed under the GNU General Public License
* or other free or open source software licenses.
* See http://www.in-portal.org/license for copyright notices and details.
*/

defined('FULL_PATH') or die('restricted access!');

class TemplatesCache extends kHelper {

	/**
	 * Base path for searching templates
	 *
	 * @var string
	 */
	var $BasePath;

	/**
	 * Force templates cache to search templates on front-end:
	 * true - search for theme name in template name
	 * false - don't search anywhere
	 * name - theme name to prepend for each template name
	 *
	 * @var mixed
	 */
	var $forceThemeName = false;

	/**
	 * Compile templates to database
	 *
	 * @var bool
	 */
	var $_compileToDatabase = false;

	/**
	 * Compress compiled templates
	 *
	 * @var bool
	 */
	var $_compressOutput = false;

	/**
	 * Template locations of each module
	 *
	 * @var Array
	 */
	var $_modulePaths = Array ();

	function TemplatesCache()
	{
		parent::kHelper();

		if ( !defined('T_DOC_COMMENT') ) {
			// method "TemplatesCache::_compress" uses such constant, but it's only available since PHP 5.0.0
			define('T_DOC_COMMENT', 366);
		}

		$this->BasePath = FULL_PATH . THEMES_PATH;

		$this->_compileToDatabase = defined('SAFE_MODE') && SAFE_MODE;
		$this->_compressOutput = $this->Application->ConfigValue('UseTemplateCompression');

		if ($this->Application->isAdmin) {
			// prepare module template paths for quick access
			$module_paths = Array ();

			foreach ($this->Application->ModuleInfo as $module_name => $module_info) {
				$module_paths[ mb_strtolower($module_name) ] = rtrim($module_info['Path'], '/');
			}

			$this->_modulePaths = $module_paths;
		}
	}

	/**
	 * Based on template name gets it's location on disk and owner module
	 *
	 * @param string $filename
	 * @return Array 0 - path on disk, 1 - template name
	 */
	function GetTemplatePaths($filename)
	{
		if ($this->Application->isAdmin && array_key_exists($filename, $this->Application->ReplacementTemplates)) {
			// process admin template replacement
			$filename = $this->Application->ReplacementTemplates[$filename];
		}

		// allows to use non-replaced version of replaced template
		$filename = preg_replace('/^original:(.*)/', '\\1', $filename);

		if (preg_match('#^[\/]{0,1}([^\/]*)\/(.*)#', $filename, $regs)) {
			$first_dir = $regs[1];
			$module_filename = $regs[2];
		}
		else {
			$first_dir = '';
			$module_filename = $filename;
		}

		if (is_string($this->forceThemeName)) {
			// when defined, then all templates are read from given theme name
			$first_dir = 'theme:' . $this->forceThemeName . ($first_dir ? '/' . $first_dir : '');
		}

		if ($this->Application->isAdmin && array_key_exists($first_dir, $this->_modulePaths)) {
			// template belongs to one of the modules
			$path = FULL_PATH . '/' . $this->_modulePaths[$first_dir] . '/admin_templates';
		}
		elseif ($this->forceThemeName && preg_match('/^theme:(.*)/', $first_dir, $regs)) {
			// ability to use Front-End templates in admin (only during mass compilation)
			$path = FULL_PATH . '/themes/' . $regs[1];
		}
		else {
			// template from "core" module
			$path = $this->BasePath;
			$module_filename = $first_dir . '/' . $module_filename;
		}

		return Array ($path, $module_filename);
	}

	/**
	 * Returns template filename by given template name
	 *
	 * @param string $filename
	 * @return string
	 */
	function GetRealFilename($filename)
	{
		list ($path, $module_filename) = $this->GetTemplatePaths($filename);
		return $path.'/'.trim($module_filename, '/');
	}

	/**
	 * Checks, that given template exists on disk
	 *
	 * @param string $filename
	 * @return bool
	 */
	function TemplateExists($filename)
	{
		if ((strpos($filename, '../') !== false) || (trim($filename) !== $filename)) {
			// when relative paths or special chars are found template names from url, then it's hacking attempt
			return false;
		}

		$real_file = $this->GetRealFilename( strtolower($filename) );
		if (substr($real_file, -4) != '.tpl') {
			// add ".tpl" file extension, when not specified in template name
			$real_file .= '.tpl';
		}

		return file_exists($real_file);
	}

	/**
	 * Returns information about template compilation status
	 *
	 * @param string $template
	 * @return Array
	 */
	function GetPreParsed($template)
	{
		$real_name = $this->GetRealFilename($template);
		$fname = str_replace(FULL_PATH, WRITEABLE . '/cache', $real_name . '.php');

		$tname = $real_name . '.tpl';
		if (!file_exists($tname)) {
			// given template doesn't exist
			return false;
		}

		if ($this->_compileToDatabase) {
			$sql = 'SELECT *
					FROM ' . TABLE_PREFIX . 'Cache
					WHERE VarName = "' . $fname . '"';
			$cached = $this->Conn->GetRow($sql);

			if ($cached !== false && $cached['Cached'] > filemtime($tname)) {
				return Array ('active' => 1, 'fname' => $fname, 'tname' => $tname, 'mode' => 'db', 'content' => $cached['Data']);
			}
		}
		else {
			if (file_exists($fname) && file_exists($tname) && filemtime($fname) > filemtime($tname)) {
				return Array ('active' => 1, 'fname' => $fname, 'tname' => $tname, 'mode' => 'file');
			}

			if (!file_exists($fname)) {
				 // make sure to create directory if pre-parsed file does not exist
				$this->CheckDir(dirname($fname), WRITEABLE . '/cache');
			}
		}

		// when compiled template is expired or doesn't exist

		return Array ('active' => 0, 'fname' => $fname, 'tname' => $tname, 'mode' => 'file');
	}

	/**
	 * Saves compiled template version to database or disk
	 *
	 * @param string $filename
	 * @param string $compiled_template
	 */
	function saveTemplate($filename, &$compiled_template)
	{
		if ($this->_compileToDatabase) {
			$fields_hash = Array (
				'VarName' => $filename,
				'Data' => &$compiled_template,
				'Cached' => adodb_mktime(),
			);

			$this->Conn->doInsert($fields_hash, TABLE_PREFIX . 'Cache', 'REPLACE');
		}
		else {
			$fp = fopen($filename, 'w');

			if ($this->_compressOutput) {
				$compiled_template = $this->_compress($compiled_template);
			}

			if (!fwrite($fp, $compiled_template)) {
				trigger_error('Saving compiled template failed', E_USER_ERROR);
			}

			fclose($fp);
		}
	}

	/**
	 * Runs template and returns result (template already should be compiled by now)
	 *
	 * @param NParser $_parser
	 * @param Array $pre_parsed
	 * @return string
	 */
	function &runTemplate(&$_parser, &$pre_parsed)
	{
		ob_start();

		if ($this->_compileToDatabase) {
			$sql = 'SELECT *
					FROM ' . TABLE_PREFIX . 'Cache
					WHERE VarName = "' . $pre_parsed['fname'] . '"';

			$cached = $this->Conn->GetRow($sql);

			if (($cached !== false) && ($cached['Cached'] > filemtime($pre_parsed['tname']))) {
				eval('?' . '>' . $cached['Data']);
			}
		}
		else {
			if ($pre_parsed['mode'] == 'file') {
				include($pre_parsed['fname']);
			}
			else {
				eval('?' . '>' . $pre_parsed['content']);
			}
		}

		$output = ob_get_clean();

		return $output;
	}

	/**
	 * Compress given php code
	 *
	 * @param string $src
	 * @return string
	 */
	function _compress($src) {
		// Whitespaces left and right from this signs can be ignored
		static $IW = array(
			T_CONCAT_EQUAL,             // .=
			T_DOUBLE_ARROW,             // =>
			T_BOOLEAN_AND,              // &&
			T_BOOLEAN_OR,               // ||
			T_IS_EQUAL,                 // ==
			T_IS_NOT_EQUAL,             // != or <>
			T_IS_SMALLER_OR_EQUAL,      // <=
			T_IS_GREATER_OR_EQUAL,      // >=
			T_INC,                      // ++
			T_DEC,                      // --
			T_PLUS_EQUAL,               // +=
			T_MINUS_EQUAL,              // -=
			T_MUL_EQUAL,                // *=
			T_DIV_EQUAL,                // /=
			T_IS_IDENTICAL,             // ===
			T_IS_NOT_IDENTICAL,         // !==
			T_DOUBLE_COLON,             // ::
			T_PAAMAYIM_NEKUDOTAYIM,     // ::
			T_OBJECT_OPERATOR,          // ->
			T_DOLLAR_OPEN_CURLY_BRACES, // ${
			T_AND_EQUAL,                // &=
			T_MOD_EQUAL,                // %=
			T_XOR_EQUAL,                // ^=
			T_OR_EQUAL,                 // |=
			T_SL,                       // <<
			T_SR,                       // >>
			T_SL_EQUAL,                 // <<=
			T_SR_EQUAL,                 // >>=
		);

		$tokens = token_get_all($src);

		$new = "";
		$c = sizeof($tokens);
		$iw = false; // ignore whitespace
		$ih = false; // in HEREDOC
		$ls = "";    // last sign
		$ot = null;  // open tag

		for ($i = 0; $i < $c; $i++) {
			$token = $tokens[$i];

			if (is_array($token)) {
				list ($tn, $ts) = $token; // tokens: number, string, line
				$tname = token_name($tn);

				if ($tn == T_INLINE_HTML) {
					$new .= $ts;
					$iw = false;
				} else {
					if ($tn == T_OPEN_TAG) {
						if (strpos($ts, " ") || strpos($ts, "\n") || strpos($ts, "\t") || strpos($ts, "\r")) {
							$ts = rtrim($ts);
						}

						$ts .= " ";
						$new .= $ts;
						$ot = T_OPEN_TAG;
						$iw = true;
					} elseif ($tn == T_OPEN_TAG_WITH_ECHO) {
						$new .= $ts;
						$ot = T_OPEN_TAG_WITH_ECHO;
						$iw = true;
					} elseif ($tn == T_CLOSE_TAG) {
						if ($ot == T_OPEN_TAG_WITH_ECHO) {
							$new = rtrim($new, "; ");
						} else {
							$ts = " ".$ts;
						}

						$new .= $ts;
						$ot = null;
						$iw = false;
					} elseif (in_array($tn, $IW)) {
						$new .= $ts;
						$iw = true;
					} elseif ($tn == T_CONSTANT_ENCAPSED_STRING	|| $tn == T_ENCAPSED_AND_WHITESPACE) {
						if ($ts[0] == '"') {
							$ts = addcslashes($ts, "\n\t\r");
						}

						$new .= $ts;
						$iw = true;
					} elseif ($tn == T_WHITESPACE) {
						$nt = @$tokens[$i+1];
						if (!$iw && (!is_string($nt) || $nt == '$') && !in_array($nt[0], $IW)) {
							$new .= " ";
						}

						$iw = false;
					} elseif ($tn == T_START_HEREDOC) {
						$new .= "<<<S\n";
						$iw = false;
						$ih = true; // in HEREDOC
	                } elseif ($tn == T_END_HEREDOC) {
	                	$new .= "S;";
	                	$iw = true;
	                	$ih = false; // in HEREDOC
	                	for ($j = $i + 1; $j < $c; $j++) {
	                		if (is_string($tokens[$j]) && $tokens[$j] == ";") {
	                			$i = $j;
	                			break;
	                		} else if ($tokens[$j][0] == T_CLOSE_TAG) {
	                			break;
	                		}
						}
					} elseif ($tn == T_COMMENT || $tn == T_DOC_COMMENT) {
						$iw = true;
					} else {
						/*if (!$ih) {
							// this also lowecases attribute names :(
							$ts = strtolower($ts);
						}*/

						$new .= $ts;
						$iw = false;
					}
				}

				$ls = "";
			} else {
				if (($token != ";" && $token != ":") || $ls != $token) {
					$new .= $token;
					$ls = $token;
				}

				$iw = true;
			}
	    }

	    return $new;
	}

	/**
	 * Recursive mkdir
	 *
	 * @param string $dir
	 * @param string $base_path base path to directory where folders should be created in
	 */
	function CheckDir($dir, $base_path = '')
	{
		if (file_exists($dir)) {
			return;
		}
		else {
			// remove $base_path from beggining because it is already created during install
			$dir = preg_replace('/^'.preg_quote($base_path.'/', '/').'/', '', $dir, 1);
			$segments = explode('/', $dir);
			$cur_path = $base_path;

			foreach ($segments as $segment) {
				// do not add leading / for windows paths (c:\...)
				$cur_path .= preg_match('/^[a-zA-Z]{1}:/', $segment) ? $segment : '/'.$segment;
				if (!file_exists($cur_path)) {
					mkdir($cur_path);
				}
			}
		}
	}

}