', but then it * will mess up your HTML comments ;-). * * When in "restrict" mode, ise of PHP commands not in the whitelists * will cause the compiler to * fail. Use of various constructs and * superglobals, likewise. * * Use {$var} or {$this->var} to print a variable. * * Use {: function-list} to print the results of function calls. * * Use {['pluginName', 'arg1', $arg2, $this->arg3]} to call plugins. * * Use these for looping: * {foreach ():} ... {endforeach} * {for ():} ... {endfor} * {while ():} ... {endwhile} * * Use these for conditionals (normal PHP can go in the parens): * {if (...):} * {elseif (...):} * {else:} * {endif} * {switch (...):} * {case ...:} * {default:} * {endswitch} * * {break} and {continue} are supported as well. * * Use this to include a template: * {tpl 'template.tpl.php'} * {tpl $tplname} * {tpl $this->tplname} * * $Id$ * * @author Paul M. Jones * * @package Savant2 * * @license http://www.gnu.org/copyleft/lesser.html LGPL * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * */ require_once 'Savant2/Compiler.php'; require_once 'Savant2/Error.php'; require_once 'Savant2/PHPCodeAnalyzer.php'; class Savant2_Compiler_basic extends Savant2_Compiler { /** * * The template directive prefix. * * @access public * * @var array * */ var $prefix = '{'; /** * * The template directive suffix. * * @access public * * @var array * */ var $suffix = '}'; /** * * The conversion regular expressions. * * @access public * * @var array * */ var $convert = array( // branching '(if\s*(.+):)' => '$1', '(elseif\s*(.+):)' => '$1', '(else\s*(.+):)' => '$1', '(endif)' => '$1', '(switch\s*(.+):)' => '$1', '(case\s*(.+):)' => '$1', '(default:)' => '$1', '(endswitch)' => '$1', '(break)' => '$1', // looping '(foreach\s*(.+):)' => '$1', '(endforeach)' => '$1', '(for\s*(.+):)' => '$1', '(endfor)' => '$1', '(while\s*(.+):)' => '$1', '(endwhile)' => '$1', '(continue)' => '$1', // simple variable printing '(\$(.+))' => 'print $1', // extended printing '(\:(.+))' => 'print ($2)', // comments '\*(.*)?\*' => '/**$1*/', // template includes 'tpl (.*)' => 'include $this->findTemplate($1)', // plugins '\[\s*(.+)?\s*\]' => '$this->plugin($1)', ); /** * * The list of allowed functions when in restricted mode. * * @access public * * @var array * */ var $allowedFunctions = array(); /** * * The list of allowed static methods when in restricted mode. * * @access public * * @var array * */ var $allowedStatic = array(); /** * * The directory where compiled templates are saved. * * @access public * * @var string * */ var $compileDir = null; /** * * Whether or not to force every template to be compiled every time. * * @access public * * @var bool * */ var $forceCompile = false; /** * * Whether or not to strict-check the compiled template. * * Strict-checks are off by default until all problems with * PhpCodeAnalyzer have been resolved. * * @access public * * @var bool * */ var $strict = false; /** * * Constructor. * */ function Savant2_Compiler_basic($conf = array()) { parent::Savant2_Compiler($conf); $this->ca = new PHPCodeAnalyzer(); $this->allowedFunctions = $this->allowedFunctions(); $this->allowedStatic = $this->allowedStatic(); } /** * * Has the source template changed since it was last compiled? * * @access public * * @var string $tpl The source template file. * */ function changed($tpl) { // the path for the compiled file $file = $this->getPath($tpl); // if the copmiled file does not exist, or if the mod-time of // the source is later than that of the existing compiled file, // then the source template file has changed. if (! file_exists($file) || filemtime($tpl) > filemtime($file)) { return true; } else { return false; } } /** * * Saves the PHP compiled from template source. * * @access public * * @var string $tpl The source template file. * */ function saveCompiled($tpl, $php) { $fp = fopen($this->getPath($tpl), 'w'); if (! $fp) { return false; } else { $result = fwrite($fp, $php); fclose($fp); return $result; } } /** * * Gets the path to the compiled PHP for a template source. * * @access public * * @var string $tpl The source template file. * */ function getPath($tpl) { $dir = $this->compileDir; if (substr($dir, -1) != DIRECTORY_SEPARATOR) { $dir .= DIRECTORY_SEPARATOR; } return $dir . 'Savant2_' . md5($tpl); } /** * * Compiles a template source into PHP code for Savant. * * @access public * * @var string $tpl The source template file. * */ function compile($tpl) { // create a end-tag so that text editors don't // stop colorizing text $end = '?' . '>'; // recompile only if we are forcing compiled, or // if the template source has changed. if ($this->forceCompile || $this->changed($tpl)) { // get the template source text $php = file_get_contents($tpl); /** * @todo Do we really care about PHP tags? The code analyzer * will disallow any offending PHP regardless. */ // disallow PHP long tags $php = str_replace('convert as $find => $replace) { // allow whitespace around the command $find = preg_quote($this->prefix) . '\s*' . $find . '\s*' . preg_quote($this->suffix); // actually do the find-and-replace $php = preg_replace( "/$find/U", "saveCompiled($tpl, $php); // --- DEBUG // are we doing strict checking? if ($this->strict) { // analyze the code for restriction violations. $report = $this->analyze($php); if (count($report) > 0) { // there were violations, report them as a generic // Savant error and return. Savant will wrap this // generic rror with another error that will report // properly to the customized error handler (if any). return new Savant2_Error( array( 'code' => SAVANT2_ERROR_COMPILE_FAIL, 'text' => $GLOBALS['_SAVANT2']['error'][SAVANT2_ERROR_COMPILE_FAIL], 'info' => $report ) ); } } // otherwise, save the compiled template $this->saveCompiled($tpl, $php); } // return the path to the compiled PHP script return $this->getPath($tpl); } /** * * Analyze a compiled template for restriction violations. * * @access public * * @var string $php The compiled PHP code from a template source. * * @return array An array of restriction violations; if empty, then * there were no violations discovered by analysis. * */ function analyze(&$php) { // analyze the compiled code $ca =& $this->ca; $ca->source =& $php; $ca->analyze(); // array of captured restriction violations $report = array(); // ------------------------------------------------------------- // // go through the list of called functions and make sure each // one is allowed via the whitelist. if not, record each non- // allowed function. this also restricts variable-functions // such as $var(). // foreach ($ca->calledFunctions as $func => $lines) { if (! in_array($func, $this->allowedFunctions)) { $report[$func] = $lines; } } // ------------------------------------------------------------- // // disallow use of various constructs (include is allowed, we // need it for {tpl}). // $tmp = array( 'eval', 'global', 'include_once', 'require', 'require_once', 'parent', 'self' ); foreach ($tmp as $val) { if (isset($ca->calledConstructs[$val])) { $report[$val] = $ca->calledConstructs[$val]; } } // ------------------------------------------------------------- // // disallow instantiation of new classes // foreach ($ca->classesInstantiated as $key => $val) { $report['new ' . $key] = $val; } // ------------------------------------------------------------- // // disallow access to the various superglobals // so that templates cannot manipulate them. // $tmp = array( '$_COOKIE', '$_ENV', '$_FILES', '$_GET', '$_POST', '$_REQUEST', '$_SERVER', '$_SESSION', '$GLOBALS', '$HTTP_COOKIE_VARS', '$HTTP_ENV_VARS', '$HTTP_GET_VARS', '$HTTP_POST_FILES', '$HTTP_POST_VARS', '$HTTP_SERVER_VARS', '$HTTP_SESSION_VARS' ); foreach ($ca->usedVariables as $var => $lines) { if (in_array(strtoupper($var), $tmp)) { $report[$var] = $lines; } } // ------------------------------------------------------------- // // allow only certain $this methods // $tmp = array('plugin', 'splugin', 'findTemplate'); if (isset($ca->calledMethods['$this'])) { foreach ($ca->calledMethods['$this'] as $method => $lines) { if (! in_array($method, $tmp)) { $report['$this->' . $method] = $lines; } } } // ------------------------------------------------------------- // // disallow private and variable-variable $this properties // if (isset($ca->usedMemberVariables['$this'])) { foreach ($ca->usedMemberVariables['$this'] as $prop => $lines) { $char = substr($prop, 0, 1); if ($char == '_' || $char == '$') { $report['$this->' . $prop] = $lines; } } } // ------------------------------------------------------------- // // allow only certain static method calls // foreach ($ca->calledStaticMethods as $class => $methods) { foreach ($methods as $method => $lines) { if (! array_key_exists($class, $this->allowedStatic)) { // the class itself is not allowed $report["$class::$method"] = $lines; } elseif (! in_array('*', $this->allowedStatic[$class]) && ! in_array($method, $this->allowedStatic[$class])){ // the specific method is not allowed, // and there is no wildcard for the class methods. $report["$class::$method"] = $lines; } } } // ------------------------------------------------------------- // // only allow includes via $this->findTemplate(*) // foreach ($ca->filesIncluded as $text => $lines) { // in each include statment, look for $this->findTemplate. preg_match( '/(.*)?\$this->findTemplate\((.*)?\)(.*)/i', $text, $matches ); if (! empty($matches[1]) || ! empty($matches[3]) || empty($matches[2])) { // there is something before or after the findTemplate call, // or it's a direct include (which is not allowed) $report["include $text"] = $lines; } } // ------------------------------------------------------------- // // do not allow the use of "$this" by itself; // it must be always be followed by "->" or another // valid variable-name character (a-z, 0-9, or _). // $regex = '/(.*)?\$this(?!(\-\>)|([a-z0-9_]))(.*)?/i'; preg_match_all($regex, $php, $matches, PREG_SET_ORDER); foreach ($matches as $val) { $report['\'$this\' without \'->\''][] = $val[0]; } /** @todo disallow standalone variable-variables, $$var */ /** @todo disallow vars from static classes? class::$var */ // ------------------------------------------------------------- // // done! // // +++ DEBUG //echo "
";
		//print_r($ca);
		//echo "
"; // --- DEBUG return $report; } /** * * A list of allowed static method calls for templates. * * The format is ... * * array( * 'Class1' => array('method1', 'method2'), * 'Class2' => array('methodA', 'methodB'), * 'Class3' => '*' * ); * * If you want to allow all methods from the static class to be allowed, * use a '*' in the method name list. * */ function allowedStatic() { return array(); } /** * * A list of allowed functions for templates. * */ function allowedFunctions() { return array( // arrays 'array_count_values', 'array_key_exists', 'array_keys', 'array_sum', 'array_values', 'compact', 'count', 'current', 'each', 'end', 'extract', 'in_array', 'key', 'list', 'next', 'pos', 'prev', 'reset', 'sizeof', // calendar 'cal_days_in_month', 'cal_from_jd', 'cal_to_jd', 'easter_date', 'easter_days', 'FrenchToJD', 'GregorianToJD', 'JDDayOfWeek', 'JDMonthName', 'JDToFrench', 'JDToGregorian', 'jdtojewish', 'JDToJulian', 'jdtounix', 'JewishToJD', 'JulianToJD', 'unixtojd', // date 'checkdate', 'date_sunrise', 'date_sunset', 'date', 'getdate', 'gettimeofday', 'gmdate', 'gmmktime', 'gmstrftime', 'idate', 'localtime', 'microtime', 'mktime', 'strftime', 'strptime', 'strtotime', 'time', // gettext '_', 'gettext', 'ngettext', // math 'abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'base_convert', 'bindec', 'ceil', 'cos', 'cosh', 'decbin', 'dechex', 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh', // strings 'chop', 'count_chars', 'echo', 'explode', 'hebrev', 'hebrevc', 'html_entity_decode', 'htmlentities', 'htmlspecialchars', 'implode', 'join', 'localeconv', 'ltrim', 'money_format', 'nl_langinfo', 'nl2br', 'number_format', 'ord', 'print', 'printf', 'quoted_printable_decode', 'rtrim', 'sprintf', 'sscanf', 'str_pad', 'str_repeat', 'str_replace', 'str_rot13', 'str_shuffle', 'str_word_count', 'strcasecmp', 'strchr', 'strcmp', 'strcoll', 'strcspn', 'strip_tags', 'stripcslashes', 'stripos', 'stripslashes', 'stristr', 'strlen', 'strnatcasecmp', 'strnatcmp', 'strncasecmp', 'strncmp', 'strpbrk', 'strpos', 'strrchr', 'strrev', 'strripos', 'strrpos', 'strspn', 'strstr', 'strtok', 'strtolower', 'strtoupper', 'strtr', 'substr_compare', 'substr_count', 'substr_replace', 'substr', 'trim', 'ucfirst', 'ucwords', 'wordwrap', // url 'base64_decode', 'base64_encode', 'rawurldecode', 'rawurlencode', 'urldecode', 'urlencode', // variables 'empty', 'is_array', 'is_bool', 'is_double', 'is_float', 'is_int', 'is_integer', 'is_long', 'is_null', 'is_numeric', 'is_object', 'is_real', 'is_resource', 'is_scalar', 'is_string', 'isset', 'print_r', 'unset', 'var_dump', ); } } ?>