* * @license http://www.gnu.org/copyleft/lesser.html LGPL * * @version $Id: Savant3.php,v 1.42 2006/01/01 18:31:00 pmjones Exp $ * */ /** * Always have these classes available. */ include_once dirname(__FILE__) . '/Savant3/Filter.php'; include_once dirname(__FILE__) . '/Savant3/Plugin.php'; /** * * Provides an object-oriented template system for PHP5. * * Savant3 helps you separate business logic from presentation logic * using PHP as the template language. By default, Savant3 does not * compile templates. However, you may pass an optional compiler object * to compile template source to include-able PHP code. It is E_STRICT * compliant for PHP5. * * Please see the documentation at {@link http://phpsavant.com/}, and be * sure to donate! :-) * * @author Paul M. Jones * * @package Savant3 * * @version @package_version@ * */ class Savant3 { /** * * Array of configuration parameters. * * @access protected * * @var array * */ protected $__config = array( 'template_path' => array(), 'resource_path' => array(), 'error_text' => "\n\ntemplate error, examine fetch() result\n\n", 'exceptions' => false, 'autoload' => false, 'compiler' => null, 'filters' => array(), 'plugins' => array(), 'template' => null, 'plugin_conf' => array(), 'extract' => false, 'fetch' => null, 'escape' => array('htmlspecialchars'), ); // ----------------------------------------------------------------- // // Constructor and magic methods // // ----------------------------------------------------------------- /** * * Constructor. * * @access public * * @param array $config An associative array of configuration keys for * the Savant3 object. Any, or none, of the keys may be set. * * @return object Savant3 A Savant3 instance. * */ public function __construct($config = null) { // force the config to an array settype($config, 'array'); // set the default template search path if (isset($config['template_path'])) { // user-defined dirs $this->setPath('template', $config['template_path']); } else { // no directories set, use the // default directory only $this->setPath('template', null); } // set the default resource search path if (isset($config['resource_path'])) { // user-defined dirs $this->setPath('resource', $config['resource_path']); } else { // no directories set, use the // default directory only $this->setPath('resource', null); } // set the error reporting text if (isset($config['error_text'])) { $this->setErrorText($config['error_text']); } // set the autoload flag if (isset($config['autoload'])) { $this->setAutoload($config['autoload']); } // set the extraction flag if (isset($config['extract'])) { $this->setExtract($config['extract']); } // set the exceptions flag if (isset($config['exceptions'])) { $this->setExceptions($config['exceptions']); } // set the template to use for output if (isset($config['template'])) { $this->setTemplate($config['template']); } // set the output escaping callbacks if (isset($config['escape'])) { $this->setEscape($config['escape']); } // set the default plugin configs if (isset($config['plugin_conf']) && is_array($config['plugin_conf'])) { foreach ($config['plugin_conf'] as $name => $opts) { $this->setPluginConf($name, $opts); } } // set the default filter callbacks if (isset($config['filters'])) { $this->addFilters($config['filters']); } } /** * * Executes a main plugin method with arbitrary parameters. * * @access public * * @param string $func The plugin method name. * * @param array $args The parameters passed to the method. * * @return mixed The plugin output, or a Savant3_Error with an * ERR_PLUGIN code if it can't find the plugin. * */ public function __call($func, $args) { $plugin = $this->plugin($func); if ($this->isError($plugin)) { return $plugin; } // try to avoid the very-slow call_user_func_array() // for plugins with very few parameters. thanks to // Andreas Korthaus for profiling the code to find // the slowdown. switch (count($args)) { case 0: return $plugin->$func(); case 1: return $plugin->$func($args[0]); break; case 2: return $plugin->$func($args[0], $args[1]); break; case 3: return $plugin->$func($args[0], $args[1], $args[2]); break; default: return call_user_func_array(array($plugin, $func), $args); break; } } /** * * Magic method to echo this object as template output. * * Note that if there is an error, this will output a simple * error text string and will not return an error object. Use * fetch() to get an error object when errors occur. * * @access public * * @return string The template output. * */ public function __toString() { return $this->getOutput(); } /** * * Reports the API version for this class. * * @access public * * @return string A PHP-standard version number. * */ public function apiVersion() { return '@package_version@'; } /** * * Returns an internal plugin object; creates it as needed. * * @access public * * @param string $name The plugin name. If this plugin has not * been created yet, this method creates it automatically. * * @return mixed The plugin object, or a Savant3_Error with an * ERR_PLUGIN code if it can't find the plugin. * */ public function plugin($name) { // shorthand reference $plugins =& $this->__config['plugins']; $autoload = $this->__config['autoload']; // is the plugin method object already instantiated? if (! array_key_exists($name, $plugins)) { // not already instantiated, so load it up. // set up the class name. $class = "Savant3_Plugin_$name"; // has the class been loaded? if (! class_exists($class, $autoload)) { // class is not loaded, set up the file name. $file = "$class.php"; // make sure the class file is available from the resource path. $result = $this->findFile('resource', $file); if (! $result) { // not available, this is an error return $this->error( 'ERR_PLUGIN', array('method' => $name) ); } else { // available, load the class file include_once $result; } } // get the default configuration for the plugin. $plugin_conf =& $this->__config['plugin_conf']; if (! empty($plugin_conf[$name])) { $opts = $plugin_conf[$name]; } else { $opts = array(); } // add the Savant reference $opts['Savant'] = $this; // instantiate the plugin with its options. $plugins[$name] = new $class($opts); } // return the plugin object return $plugins[$name]; } // ----------------------------------------------------------------- // // Public configuration management (getters and setters). // // ----------------------------------------------------------------- /** * * Returns a copy of the Savant3 configuration parameters. * * @access public * * @param string $key The specific configuration key to return. If null, * returns the entire configuration array. * * @return mixed A copy of the $this->__config array. * */ public function getConfig($key = null) { if (is_null($key)) { // no key requested, return the entire config array return $this->__config; } elseif (empty($this->__config[$key])) { // no such key return null; } else { // return the requested key return $this->__config[$key]; } } /** * * Sets __autoload() usage on or off. * * @access public * * @param bool $flag True to use __autoload(), false to not use it. * * @return void * */ public function setAutoload($flag) { $this->__config['autoload'] = (bool) $flag; } /** * * Sets a custom compiler/pre-processor callback for template sources. * * By default, Savant3 does not use a compiler; use this to set your * own custom compiler (pre-processor) for template sources. * * @access public * * @param mixed $compiler A compiler callback value suitable for the * first parameter of call_user_func(). Set to null/false/empty to * use PHP itself as the template markup (i.e., no compiling). * * @return void * */ public function setCompiler($compiler) { $this->__config['compiler'] = $compiler; } /** * * Sets the custom error text for __toString(). * * @access public * * @param string $text The error text when a template is echoed. * * @return void * */ public function setErrorText($text) { $this->__config['error_text'] = $text; } /** * * Sets whether or not exceptions will be thrown. * * @access public * * @param bool $flag True to turn on exception throwing, false * to turn it off. * * @return void * */ public function setExceptions($flag) { $this->__config['exceptions'] = (bool) $flag; } /** * * Sets whether or not variables will be extracted. * * @access public * * @param bool $flag True to turn on variable extraction, false * to turn it off. * * @return void * */ public function setExtract($flag) { $this->__config['extract'] = (bool) $flag; } /** * * Sets config array for a plugin. * * @access public * * @param string $plugin The plugin to configure. * * @param array $config The configuration array for the plugin. * * @return void * */ public function setPluginConf($plugin, $config = null) { $this->__config['plugin_conf'][$plugin] = $config; } /** * * Sets the template name to use. * * @access public * * @param string $template The template name. * * @return void * */ public function setTemplate($template) { $this->__config['template'] = $template; } // ----------------------------------------------------------------- // // Output escaping and management. // // ----------------------------------------------------------------- /** * * Clears then sets the callbacks to use when calling $this->escape(). * * Each parameter passed to this function is treated as a separate * callback. For example: * * * $savant->setEscape( * 'stripslashes', * 'htmlspecialchars', * array('StaticClass', 'method'), * array($object, $method) * ); * * * @access public * * @return void * */ public function setEscape() { $this->__config['escape'] = (array) @func_get_args(); } /** * * Adds to the callbacks used when calling $this->escape(). * * Each parameter passed to this function is treated as a separate * callback. For example: * * * $savant->addEscape( * 'stripslashes', * 'htmlspecialchars', * array('StaticClass', 'method'), * array($object, $method) * ); * * * @access public * * @return void * */ public function addEscape() { $args = (array) @func_get_args(); $this->__config['escape'] = array_merge( $this->__config['escape'], $args ); } /** * * Gets the array of output-escaping callbacks. * * @access public * * @return array The array of output-escaping callbacks. * */ public function getEscape() { return $this->__config['escape']; } /** * * Applies escaping to a value. * * You can override the predefined escaping callbacks by passing * added parameters as replacement callbacks. * * * // use predefined callbacks * $result = $savant->escape($value); * * // use replacement callbacks * $result = $savant->escape( * $value, * 'stripslashes', * 'htmlspecialchars', * array('StaticClass', 'method'), * array($object, $method) * ); * * * * Unfortunately, a call to "echo htmlspecialchars()" is twice * as fast as a call to "echo $this->escape()" under the default * escaping (which is htmlspecialchars). The benchmark showed * 0.007 seconds for htmlspecialchars(), and 0.014 seconds for * $this->escape(), on 300 calls each. * * @access public * * @param mixed $value The value to be escaped. * * @return mixed * */ public function escape($value) { // were custom callbacks passed? if (func_num_args() == 1) { // no, only a value was passed. // loop through the predefined callbacks. foreach ($this->__config['escape'] as $func) { // this if() shaves 0.001sec off of 300 calls. if (is_string($func)) { $value = $func($value); } else { $value = call_user_func($func, $value); } } } else { // yes, use the custom callbacks $callbacks = func_get_args(); // drop $value array_shift($callbacks); // loop through custom callbacks. foreach ($callbacks as $func) { // this if() shaves 0.001sec off of 300 calls. if (is_string($func)) { $value = $func($value); } else { $value = call_user_func($func, $value); } } } return $value; } /** * * Prints a value after escaping it for output. * * You can override the predefined escaping callbacks by passing * added parameters as replacement callbacks. * * * // use predefined callbacks * $this->eprint($value); * * // use replacement callbacks * $this->eprint( * $value, * 'stripslashes', * 'htmlspecialchars', * array('StaticClass', 'method'), * array($object, $method) * ); * * * @access public * * @param mixed $value The value to be escaped and printed. * * @return void * */ public function eprint($value) { // avoid the very slow call_user_func_array() when there // are no custom escaping callbacks. thanks to // Andreas Korthaus for profiling the code to find // the slowdown. $num = func_num_args(); if ($num == 1) { echo $this->escape($value); } else { $args = func_get_args(); echo call_user_func_array( array($this, 'escape'), $args ); } } // ----------------------------------------------------------------- // // File management // // ----------------------------------------------------------------- /** * * Sets an entire array of search paths for templates or resources. * * @access public * * @param string $type The type of path to set, typically 'template' * or 'resource'. * * @param string|array $path The new set of search paths. If null or * false, resets to the current directory only. * * @return void * */ public function setPath($type, $path) { // clear out the prior search dirs $this->__config[$type . '_path'] = array(); // always add the fallback directories as last resort switch (strtolower($type)) { case 'template': // the current directory $this->addPath($type, '.'); break; case 'resource': // the Savant3 distribution resources $this->addPath($type, dirname(__FILE__) . '/Savant3/resources/'); break; } // actually add the user-specified directories $this->addPath($type, $path); } /** * * Adds to the search path for templates and resources. * * @access public * * @param string|array $path The directory or stream to search. * * @return void * */ public function addPath($type, $path) { // convert from path string to array of directories if (is_string($path) && ! strpos($path, '://')) { // the path config is a string, and it's not a stream // identifier (the "://" piece). add it as a path string. $path = explode(PATH_SEPARATOR, $path); // typically in path strings, the first one is expected // to be searched first. however, Savant3 uses a stack, // so the first would be last. reverse the path string // so that it behaves as expected with path strings. $path = array_reverse($path); } else { // just force to array settype($path, 'array'); } // loop through the path directories foreach ($path as $dir) { // no surrounding spaces allowed! $dir = trim($dir); // add trailing separators as needed if (strpos($dir, '://') && substr($dir, -1) != '/') { // stream $dir .= '/'; } elseif (substr($dir, -1) != DIRECTORY_SEPARATOR) { // directory $dir .= DIRECTORY_SEPARATOR; } // add to the top of the search dirs array_unshift( $this->__config[$type . '_path'], $dir ); } } /** * * Searches the directory paths for a given file. * * @param array $type The type of path to search (template or resource). * * @param string $file The file name to look for. * * @return string|bool The full path and file name for the target file, * or boolean false if the file is not found in any of the paths. * */ protected function findFile($type, $file) { // get the set of paths $set = $this->__config[$type . '_path']; // start looping through the path set foreach ($set as $path) { // get the path to the file $fullname = $path . $file; // is the path based on a stream? if (strpos($path, '://') === false) { // not a stream, so do a realpath() to avoid // directory traversal attempts on the local file // system. Suggested by Ian Eure, initially // rejected, but then adopted when the secure // compiler was added. $path = realpath($path); // needed for substr() later $fullname = realpath($fullname); } // the substr() check added by Ian Eure to make sure // that the realpath() results in a directory registered // with Savant so that non-registered directores are not // accessible via directory traversal attempts. if (file_exists($fullname) && is_readable($fullname) && substr($fullname, 0, strlen($path)) == $path) { return $fullname; } } // could not find the file in the set of paths return false; } // ----------------------------------------------------------------- // // Variable and reference assignment // // ----------------------------------------------------------------- /** * * Sets variables for the template (by copy). * * This method is overloaded; you can assign all the properties of * an object, an associative array, or a single value by name. * * You are not allowed to assign any variable named '__config' as * it would conflict with internal configuration tracking. * * In the following examples, the template will have two variables * assigned to it; the variables will be known inside the template as * "$this->var1" and "$this->var2". * * * $Savant3 = new Savant3(); * * // assign by object * $obj = new stdClass; * $obj->var1 = 'something'; * $obj->var2 = 'else'; * $Savant3->assign($obj); * * // assign by associative array * $ary = array('var1' => 'something', 'var2' => 'else'); * $Savant3->assign($ary); * * // assign by name and value * $Savant3->assign('var1', 'something'); * $Savant3->assign('var2', 'else'); * * // assign directly * $Savant3->var1 = 'something'; * $Savant3->var2 = 'else'; * * * @access public * * @return bool True on success, false on failure. * */ public function assign() { // get the arguments; there may be 1 or 2. $arg0 = @func_get_arg(0); $arg1 = @func_get_arg(1); // assign from object if (is_object($arg0)) { // assign public properties foreach (get_object_vars($arg0) as $key => $val) { // can't assign to __config if ($key != '__config') { $this->$key = $val; } } return true; } // assign from associative array if (is_array($arg0)) { foreach ($arg0 as $key => $val) { // can't assign to __config if ($key != '__config') { $this->$key = $val; } } return true; } // assign by name and value (can't assign to __config). if (is_string($arg0) && func_num_args() > 1 && $arg0 != '__config') { $this->$arg0 = $arg1; return true; } // $arg0 was not object, array, or string. return false; } /** * * Sets variables for the template (by reference). * * You are not allowed to assign any variable named '__config' as * it would conflict with internal configuration tracking. * * * $Savant3 = new Savant3(); * * // assign by name and value * $Savant3->assignRef('var1', $ref); * * // assign directly * $Savant3->ref =& $var1; * * * @access public * * @return bool True on success, false on failure. * */ public function assignRef($key, &$val) { // assign by name and reference (can't assign to __config). if ($key != '__config') { $this->$key =& $val; return true; } else { return false; } } // ----------------------------------------------------------------- // // Template processing // // ----------------------------------------------------------------- /** * * Displays a template directly (equivalent to echo $tpl). * * @access public * * @param string $tpl The template source to compile and display. * * @return void * */ public function display($tpl = null) { echo $this->getOutput($tpl); } /** * Returns output, including error_text if an error occurs. * * @param $tpl The template to process; if null, uses the * default template set with setTemplate(). * * @return string The template output. */ public function getOutput($tpl = null) { $output = $this->fetch($tpl); if ($this->isError($output)) { $text = $this->__config['error_text']; return $this->escape($text); } else { return $output; } } /** * * Compiles, executes, and filters a template source. * * @access public * * @param string $tpl The template to process; if null, uses the * default template set with setTemplate(). * * @return mixed The template output string, or a Savant3_Error. * */ public function fetch($tpl = null) { // make sure we have a template source to work with if (is_null($tpl)) { $tpl = $this->__config['template']; } // get a path to the compiled template script $result = $this->template($tpl); // did we get a path? if (! $result || $this->isError($result)) { // no. return the error result. return $result; } else { // yes. execute the template script. move the script-path // out of the local scope, then clean up the local scope to // avoid variable name conflicts. $this->__config['fetch'] = $result; unset($result); unset($tpl); // are we doing extraction? if ($this->__config['extract']) { // pull variables into the local scope. extract(get_object_vars($this), EXTR_REFS); } // buffer output so we can return it instead of displaying. ob_start(); // are we using filters? if ($this->__config['filters']) { // use a second buffer to apply filters. we used to set // the ob_start() filter callback, but that would // silence errors in the filters. Hendy Irawan provided // the next three lines as a "verbose" fix. ob_start(); include $this->__config['fetch']; echo $this->applyFilters(ob_get_clean()); } else { // no filters being used. include $this->__config['fetch']; } // reset the fetch script value, get the buffer, and return. $this->__config['fetch'] = null; return ob_get_clean(); } } /** * * Compiles a template and returns path to compiled script. * * By default, Savant does not compile templates, it uses PHP as the * markup language, so the "compiled" template is the same as the source * template. * * Used inside a template script like so: * * * include $this->template($tpl); * * * @access protected * * @param string $tpl The template source name to look for. * * @return string The full path to the compiled template script. * * @throws object An error object with a 'ERR_TEMPLATE' code. * */ protected function template($tpl = null) { // set to default template if none specified. if (is_null($tpl)) { $tpl = $this->__config['template']; } // find the template source. $file = $this->findFile('template', $tpl); if (! $file) { return $this->error( 'ERR_TEMPLATE', array('template' => $tpl) ); } // are we compiling source into a script? if ($this->__config['compiler']) { // compile the template source and get the path to the // compiled script (will be returned instead of the // source path) $result = call_user_func( array($this->__config['compiler'], 'compile'), $file ); } else { // no compiling requested, use the source path $result = $file; } // is there a script from the compiler? if (! $result || $this->isError($result)) { // return an error, along with any error info // generated by the compiler. return $this->error( 'ERR_COMPILER', array( 'template' => $tpl, 'compiler' => $result ) ); } else { // no errors, the result is a path to a script return $result; } } // ----------------------------------------------------------------- // // Filter management and processing // // ----------------------------------------------------------------- /** * * Resets the filter stack to the provided list of callbacks. * * @access protected * * @param array An array of filter callbacks. * * @return void * */ public function setFilters() { $this->__config['filters'] = (array) @func_get_args(); } /** * * Adds filter callbacks to the stack of filters. * * @access protected * * @param array An array of filter callbacks. * * @return void * */ public function addFilters() { // add the new filters to the static config variable // via the reference foreach ((array) @func_get_args() as $callback) { $this->__config['filters'][] = $callback; } } /** * * Runs all filter callbacks on buffered output. * * @access protected * * @param string The template output. * * @return void * */ protected function applyFilters($buffer) { $autoload = $this->__config['autoload']; foreach ($this->__config['filters'] as $callback) { // if the callback is a static Savant3_Filter method, // and not already loaded, try to auto-load it. if (is_array($callback) && is_string($callback[0]) && substr($callback[0], 0, 15) == 'Savant3_Filter_' && ! class_exists($callback[0], $autoload)) { // load the Savant3_Filter_*.php resource $file = $callback[0] . '.php'; $result = $this->findFile('resource', $file); if ($result) { include_once $result; } } // can't pass a third $this param, it chokes the OB system. $buffer = call_user_func($callback, $buffer); } return $buffer; } // ----------------------------------------------------------------- // // Error handling // // ----------------------------------------------------------------- /** * * Returns an error object or throws an exception. * * @access public * * @param string $code A Savant3 'ERR_*' string. * * @param array $info An array of error-specific information. * * @param int $level The error severity level, default is * E_USER_ERROR (the most severe possible). * * @param bool $trace Whether or not to include a backtrace, default * true. * * @return object Savant3_Error * */ public function error($code, $info = array(), $level = E_USER_ERROR, $trace = true) { $autoload = $this->__config['autoload']; // are we throwing exceptions? if ($this->__config['exceptions']) { if (! class_exists('Savant3_Exception', $autoload)) { include_once dirname(__FILE__) . '/Savant3/Exception.php'; } throw new Savant3_Exception($code); } // the error config array $config = array( 'code' => $code, 'info' => (array) $info, 'level' => $level, 'trace' => $trace ); // make sure the Savant3 error class is available if (! class_exists('Savant3_Error', $autoload)) { include_once dirname(__FILE__) . '/Savant3/Error.php'; } // return it $err = new Savant3_Error($config); return $err; } /** * * Tests if an object is of the Savant3_Error class. * * @access public * * @param object $obj The object to be tested. * * @return boolean True if $obj is an error object of the type * Savant3_Error, or is a subclass that Savant3_Error. False if not. * */ public function isError($obj) { $autoload = $this->__config['autoload']; // is it even an object? if (! is_object($obj)) { // not an object, so can't be a Savant3_Error return false; } else { // make sure the Savant3 error class is available for // comparison if (! class_exists('Savant3_Error', $autoload)) { include_once dirname(__FILE__) . '/Savant3/Error.php'; } // now compare the parentage $is = $obj instanceof Savant3_Error; $sub = is_subclass_of($obj, 'Savant3_Error'); return ($is || $sub); } } } ?>