Файл: source/library/Savant3/Savant3.php
Строк: 960
<?php
/**
*
* Provides an object-oriented template system for PHP5.
*
* @package Savant3
*
* @author Paul M. Jones <pmjones@ciaweb.net>
*
* @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 <pmjones@ciaweb.net>
*
* @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' => "nntemplate error, examine fetch() resultnn",
'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:
*
* <code>
* $savant->setEscape(
* 'stripslashes',
* 'htmlspecialchars',
* array('StaticClass', 'method'),
* array($object, $method)
* );
* </code>
*
* @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:
*
* <code>
* $savant->addEscape(
* 'stripslashes',
* 'htmlspecialchars',
* array('StaticClass', 'method'),
* array($object, $method)
* );
* </code>
*
* @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.
*
* <code>
* // use predefined callbacks
* $result = $savant->escape($value);
*
* // use replacement callbacks
* $result = $savant->escape(
* $value,
* 'stripslashes',
* 'htmlspecialchars',
* array('StaticClass', 'method'),
* array($object, $method)
* );
* </code>
*
*
* 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.
*
* <code>
* // use predefined callbacks
* $this->eprint($value);
*
* // use replacement callbacks
* $this->eprint(
* $value,
* 'stripslashes',
* 'htmlspecialchars',
* array('StaticClass', 'method'),
* array($object, $method)
* );
* </code>
*
* @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".
*
* <code>
* $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';
* </code>
*
* @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.
*
* <code>
* $Savant3 = new Savant3();
*
* // assign by name and value
* $Savant3->assignRef('var1', $ref);
*
* // assign directly
* $Savant3->ref =& $var1;
* </code>
*
* @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 <code>echo $tpl</code>).
*
* @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:
*
* <code>
* include $this->template($tpl);
* </code>
*
* @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);
}
}
}
?>