<?php

/**
 * Error constants.
 */
define('SAVANT2_ERROR_ASSIGN', -1);
define('SAVANT2_ERROR_ASSIGNREF', -2);
define('SAVANT2_ERROR_COMPILER', -3);
define('SAVANT2_ERROR_NOFILTER', -4);
define('SAVANT2_ERROR_NOPLUGIN', -5);
define('SAVANT2_ERROR_NOSCRIPT', -6);
define('SAVANT2_ERROR_NOTEMPLATE', -7);
define('SAVANT2_ERROR_COMPILE_FAIL', -8);

/*
*
* Error messages.
*
*/

if (! isset($GLOBALS['_SAVANT2']['error'])) {
    $GLOBALS['_SAVANT2']['error'] = [
        SAVANT2_ERROR_ASSIGN       => 'assign() parameters not correct',
        SAVANT2_ERROR_ASSIGNREF    => 'assignRef() parameters not correct',
        SAVANT2_ERROR_COMPILER     => 'compiler not an object or has no compile() method',
        SAVANT2_ERROR_NOFILTER     => 'filter file not found',
        SAVANT2_ERROR_NOPLUGIN     => 'plugin file not found',
        SAVANT2_ERROR_NOSCRIPT     => 'compiled template script file not found',
        SAVANT2_ERROR_NOTEMPLATE   => 'template source file not found',
        SAVANT2_ERROR_COMPILE_FAIL => 'template source failed to compile',
    ];
}

/**
 * Provides an object-oriented template system.
 *
 * Savant2 helps you separate model logic from view logic using PHP as
 * the template language. By default, Savant2 does not compile templates.
 * However, you may pass an optional compiler object to compile template
 * source to include-able PHP code.
 *
 * Please see the documentation at {@link http://phpsavant.com/}, and be
 * sure to donate! :-)
 *
 * $Id: Savant2.php,v 1.26 2005/07/28 14:06:27 pmjones Exp $
 *
 * @author Paul M. Jones <pmjones@ciaweb.net>
 *
 *
 * @version 2.4.0 stable
 *
 * @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.
 */
class Savant2
{
    /**
     * PHP5 ONLY: What method __call() will alias to.
     *
     * Generally 'plugin' or 'splugin' (as __call() is intended for those).
     *
     *
     * @var string
     */
    public $_call = 'plugin';

    /**
     * The custom compiler (pre-processor) object, if any.
     *
     *
     * @var object
     */
    public $_compiler = null;

    /**
     * The class type to use when instantiating error objects.
     *
     *
     * @var string
     */
    public $_error = null;

    /**
     * Array of callbacks used to escape output.
     *
     *
     * @var array
     *
     * @see setEscape()
     *
     * @see addEscape()
     *
     * @see escape()
     *
     * @see _()
     */
    public $_escape = ['htmlspecialchars'];

    /**
     * Whether or not to extract assigned variables into fetch() scope.
     *
     * When true, all variables and references assigned to Savant2 are
     * extracted into the local scope of the template script at fetch()
     * time, and may be addressed as "$varname" instead of
     * "$this->varname".  The "$this->varname" notation will also work.
     *
     * When false, you //must// use "$this->varname" in your templates to
     * address a variable instead of "$varname".  This has three
     * benefits: speed (no time spent extracting variables), memory use
     * (saves RAM by not making new references to variables), and clarity
     * (any $this->varname is obviously an assigned var, and vars created
     * within the template are not prefixed with $this).
     *
     *
     * @var bool
     */
    public $_extract = false;

    /**
     * The output of the template script.
     *
     *
     * @var string
     */
    public $_output = null;

    /**
     * The set of search directories for resources (plugins/filters) and
     * templates.
     *
     *
     * @var array
     */
    public $_path = [
        'resource' => [],
        'template' => [],
    ];

    /**
     * Array of resource (plugin/filter) object instances.
     *
     *
     * @var array
     */
    public $_resource = [
        'plugin' => [],
        'filter' => [],
    ];

    /**
     * Whether or not to automatically self-reference in plugins and filters.
     *
     *
     * @var bool
     */
    public $_reference = false;

    /**
     * Whether or not to restrict template includes only to registered paths.
     *
     *
     * @var bool
     */
    public $_restrict = false;

    /**
     * The path to the compiled template script file.
     *
     * By default, the template source and template script are the same file.
     *
     *
     * @var string
     */
    public $_script = null;

    /**
     * The name of the default template source file.
     *
     *
     * @var string
     */
    public $_template = null;

    // -----------------------------------------------------------------
    //
    // Constructor and general property setters
    //
    // -----------------------------------------------------------------

    /**
     * Constructor.
     *
     *
     * @param array $conf An associative array of configuration keys for
     * the Savant2 object.  Any, or none, of the keys may be set. The
     * keys are:
     *
     * 'template_path' => The default path string or array of directories
     * to search for templates.
     *
     * 'resource_path' => The default path string or array of directories
     * to search for plugin and filter resources.
     *
     * 'error' => The custom error class that Savant2 should use
     * when returning errors.
     *
     * 'extract' => Whether or not to extract variables into the local
     * scope when executing a template.
     *
     * 'template' => The default template source name to use.
     */
    public function __construct($conf = [])
    {
        // set the default template search dirs
        if (isset($conf['template_path'])) {
            // user-defined dirs
            $this->setPath('template', $conf['template_path']);
        } else {
            // default directory only
            $this->setPath('template', null);
        }

        // set the default filter search dirs
        if (isset($conf['resource_path'])) {
            // user-defined dirs
            $this->setPath('resource', $conf['resource_path']);
        } else {
            // default directory only
            $this->setPath('resource', null);
        }

        // set the error class
        if (isset($conf['error'])) {
            $this->setError($conf['error']);
        }

        // set the extraction flag
        if (isset($conf['extract'])) {
            $this->setExtract($conf['extract']);
        }

        // set the restrict flag
        if (isset($conf['restrict'])) {
            $this->setRestrict($conf['restrict']);
        }

        // set the Savant reference flag
        if (isset($conf['reference'])) {
            $this->setReference($conf['reference']);
        }

        // set the default template
        if (isset($conf['template'])) {
            $this->setTemplate($conf['template']);
        }

        // set the output escaping callbacks
        if (isset($config['escape'])) {
            call_user_func_array(
                [$this, 'setEscape'],
                (array) $config['escape']
            );
        }
    }

    /**
     * Sets a custom compiler/pre-processor for template sources.
     *
     * By default, Savant2 does not use a compiler; use this to set your
     * own custom compiler (pre-processor) for template sources.
     *
     *
     * @param object $compiler The compiler object; it must have a
     * "compile()" method.  If null or false, the current compiler object
     * is removed from Savant2.
     *
     * @return void
     *
     * @throws object An error object with a SAVANT2_ERROR_COMPILER code.
     */
    public function setCompiler(&$compiler)
    {
        if (! $compiler) {
            // nullify any current compiler
            $this->_compiler = null;
        } elseif (is_object($compiler) && method_exists($compiler, 'compile')) {
            // refer to a compiler object
            $this->_compiler = &$compiler;
        } else {
            // no usable compiler passed
            $this->_compiler = null;

            return $this->error(SAVANT2_ERROR_COMPILER);
        }
    }

    /**
     * Sets the method that __call() will alias to.
     *
     *
     * @param string $method The Savant2 method for __call() to alias to,
     * generally 'plugin' or 'splugin'.
     *
     * @return void
     */
    public function setCall($method = 'plugin')
    {
        $this->_call = $method;
    }

    /**
     * Sets the custom error class for Savant2 errors.
     *
     *
     * @param string $error The name of the custom error class name; if
     * null or false, resets the error class to 'Savant2_Error'.
     *
     * @return void
     */
    public function setError($error)
    {
        if (! $error) {
            $this->_error = null;
        } else {
            $this->_error = $error;
        }
    }

    /**
     * Turns path checking on/off.
     *
     *
     * @param bool $flag True to turn on path checks, false to turn off.
     *
     * @return void
     */
    public function setRestrict($flag = false)
    {
        if ($flag) {
            $this->_restrict = true;
        } else {
            $this->_restrict = false;
        }
    }

    /**
     * Turns extraction of variables on/off.
     *
     *
     * @param bool $flag True to turn on extraction, false to turn off.
     *
     * @return void
     */
    public function setExtract($flag = true)
    {
        if ($flag) {
            $this->_extract = true;
        } else {
            $this->_extract = false;
        }
    }

    /**
     * Sets the automated Savant reference for plugins and filters.
     *
     *
     * @param bool $flag Whether to reference Savant2 or not.
     *
     * @return void
     */
    public function setReference($flag = false)
    {
        $this->_reference = $flag;
    }

    /**
     * Sets the default template name.
     *
     *
     * @param string $template The default template name.
     *
     * @return void
     */
    public function setTemplate($template)
    {
        $this->_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>
     *
     *
     * @return void
     */
    public function setEscape()
    {
        $this->_escape = 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>
     *
     *
     * @return void
     */
    public function addEscape()
    {
        $this->_escape = array_merge($this->_escape, func_get_args());
    }

    /**
     * Gets the array of output-escaping callbacks.
     *
     *
     * @return array The array of output-escaping callbacks.
     */
    public function getEscape()
    {
        return $this->_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>
     *
     *
     * @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->_escape as $func) {
                $value = call_user_func($func, $value);
            }
        } else {

            // yes, use the custom callbacks instead.
            $callbacks = func_get_args();

            // drop $value
            array_shift($callbacks);

            // loop through custom callbacks.
            foreach ($callbacks as $func) {
                $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->_($value);
     *
     * // use replacement callbacks
     * $this->_(
     *     $value,
     *     'stripslashes',
     *     'htmlspecialchars',
     *     array('StaticClass', 'method'),
     *     array($object, $method)
     * );
     * </code>
     *
     *
     * @param mixed $value The value to be escaped and printed.
     *
     * @return void
     */
    public function _($value)
    {
        $args = func_get_args();
        echo call_user_func_array(
            [$this, 'escape'],
            $args
        );
    }

    // -----------------------------------------------------------------
    //
    // Path management and file finding
    //
    // -----------------------------------------------------------------

    /**
     * Sets an entire array of search paths.
     *
     *
     * @param string $type The type of path to set, typcially 'template'
     * or 'resource'.
     *
     * @param string|array $new The new set of search paths.  If null or
     * false, resets to the current directory only.
     *
     * @return void
     */
    public function setPath($type, $new)
    {
        // clear out the prior search dirs
        $this->_path[$type] = [];

        // convert from string to path
        if (is_string($new) && ! strpos('://', $new)) {
            // the search config is a string, and it's not a stream
            // identifier (the "://" piece), add it as a path
            // string.
            $new = explode(PATH_SEPARATOR, $new);
        } else {
            // force to array
            settype($new, 'array');
        }

        // always add the fallback directories as last resort
        switch (strtolower($type)) {
        case 'template':
            $this->addPath($type, '.');

            break;
        case 'resource':
            $this->addPath($type, dirname(__FILE__).'/Savant2/');

            break;
        }

        // actually add the user-specified directories
        foreach ($new as $dir) {
            $this->addPath($type, $dir);
        }
    }

    /**
     * Adds a search directory for templates.
     *
     *
     * @param string $dir The directory or stream to search.
     *
     * @return void
     */
    public function addPath($type, $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->_path[$type], $dir);
    }

    /**
     * Gets the array of search directories for template sources.
     *
     *
     * @return array The array of search directories for template sources.
     */
    public function getPath($type = null)
    {
        if (! $type) {
            return $this->_path;
        } else {
            return $this->_path[$type];
        }
    }

    /**
     * Searches a series of paths for a given file.
     *
     * @param array $type The type of paths to search (template, plugin,
     * or filter).
     *
     * @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.
     */
    public function findFile($type, $file)
    {
        // get the set of paths
        $set = $this->getPath($type);

        // start looping through them
        foreach ($set as $path) {

            // get the path to the file
            $fullname = $path.$file;

            // are we doing path checks?
            if (! $this->_restrict) {

                // no.  this is faster but less secure.
                if (file_exists($fullname) && is_readable($fullname)) {
                    return $fullname;
                }
            } else {

                // yes.  this is slower, but attempts to restrict
                // access only to defined paths.

                // 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.
     *
     * 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 set variables that begin with an underscore;
     * these are either private properties for Savant2 or private variables
     * within the template script itself.
     *
     * <code>
     *
     * $Savant2 =& new Savant2();
     *
     * // assign directly
     * $Savant2->var1 = 'something';
     * $Savant2->var2 = 'else';
     *
     * // assign by name and value
     * $Savant2->assign('var1', 'something');
     * $Savant2->assign('var2', 'else');
     *
     * // assign by assoc-array
     * $ary = array('var1' => 'something', 'var2' => 'else');
     * $Savant2->assign($obj);
     *
     * // assign by object
     * $obj = new stdClass;
     * $obj->var1 = 'something';
     * $obj->var2 = 'else';
     * $Savant2->assign($obj);
     *
     * </code>
     *
     * Greg Beaver came up with the idea of assigning to public class
     * properties.
     *
     *
     * @return void
     *
     * @throws object An error object with a SAVANT2_ERROR_ASSIGN code.
     */
    public function assign()
    {
        // this method is overloaded.
        $arg = func_get_args();

        // must have at least one argument. no error, just do nothing.
        if (! isset($arg[0])) {
            return;
        }

        // assign by object
        if (is_object($arg[0])) {
            // assign public properties
            foreach (get_object_vars($arg[0]) as $key => $val) {
                if (substr($key, 0, 1) != '_') {
                    $this->{$key} = $val;
                }
            }

            return;
        }

        // assign by associative array
        if (is_array($arg[0])) {
            foreach ($arg[0] as $key => $val) {
                if (substr($key, 0, 1) != '_') {
                    $this->{$key} = $val;
                }
            }

            return;
        }

        // assign by string name and mixed value.
        //
        // we use array_key_exists() instead of isset() becuase isset()
        // fails if the value is set to null.
        if (is_string($arg[0]) &&
            substr($arg[0], 0, 1) != '_' &&
            array_key_exists(1, $arg)) {
            $this->{$arg[0]} = $arg[1];
        } else {
            return $this->error(SAVANT2_ERROR_ASSIGN, $arg);
        }
    }

    /**
     * Sets references for the template.
     *
     * // assign by name and value
     * $Savant2->assignRef('ref', $reference);
     *
     * // assign directly
     * $Savant2->ref =& $reference;
     *
     * Greg Beaver came up with the idea of assigning to public class
     * properties.
     *
     *
     * @param string $key The name for the reference in the template.
     *
     * @param mixed &$val The referenced variable.
     *
     * @return void
     *
     * @throws object An error object with a SAVANT2_ERROR_ASSIGNREF code.
     */
    public function assignRef($key, &$val)
    {
        if (is_string($key) && substr($key, 0, 1) != '_') {
            $this->$key = &$val;
        } else {
            return $this->error(
                SAVANT2_ERROR_ASSIGNREF,
                ['key' => $key, 'val' => $val]
            );
        }
    }

    /**
     * Unsets assigned variables and references.
     *
     *
     * @param mixed $var If null, clears all variables; if a string, clears
     * the one variable named by the string; if a sequential array, clears
     * the variables names in that array.
     *
     * @return void
     */
    public function clear($var = null)
    {
        if (is_null($var)) {
            // clear all variables
            $var = array_keys(get_object_vars($this));
        } else {
            // clear specific variables
            settype($var, 'array');
        }

        // clear out the selected variables
        foreach ($var as $name) {
            if (substr($name, 0, 1) != '_' && isset($this->$name)) {
                unset($this->$name);
            }
        }
    }

    /**
     * Gets the current value of one, many, or all assigned variables.
     *
     * Never returns variables starting with an underscore; these are
     * reserved for internal Savant2 use.
     *
     *
     * @param mixed $key If null, returns a copy of all variables and
     * their values; if an array, returns an only those variables named
     * in the array; if a string, returns only that variable.
     *
     * @return mixed If multiple variables were reqested, returns an
     * associative array where the key is the variable name and the
     * value is the variable value; if one variable was requested,
     * returns the variable value only.
     */
    public function getVars($key = null)
    {
        if (is_null($key)) {
            $key = array_keys(get_object_vars($this));
        }

        if (is_array($key)) {
            // return a series of vars
            $tmp = [];
            foreach ($key as $var) {
                if (substr($var, 0, 1) != '_' && isset($this->$var)) {
                    $tmp[$var] = $this->$var;
                }
            }

            return $tmp;
        } else {
            // return a single var
            if (substr($key, 0, 1) != '_' && isset($this->$key)) {
                return $this->$key;
            }
        }
    }

    // -----------------------------------------------------------------
    //
    // Template processing
    //
    // -----------------------------------------------------------------

    /**
     * Loads a template script for execution (does not execute the script).
     *
     * This will optionally compile the template source into a PHP script
     * if a compiler object has been passed into Savant2.
     *
     * Also good for including templates from the template paths within
     * another template, like so:
     *
     * include $this->loadTemplate('template.tpl.php');
     *
     *
     * @param string $tpl The template source name to look for.
     *
     * @param bool $setScript Default false; if true, sets the $this->_script
     * property to the resulting script path (or null on error).  Normally,
     * only $this->fetch() will need to set this to true.
     *
     * @return string The full path to the compiled template script.
     *
     * @throws object An error object with a SAVANT2_ERROR_NOTEMPLATE code.
     */
    public function loadTemplate($tpl = null, $setScript = false)
    {
        // set to default template if none specified.
        if (is_null($tpl)) {
            $tpl = $this->_template;
        }

        // find the template source.
        $file = $this->findFile('template', $tpl);
        if (! $file) {
            return $this->error(
                SAVANT2_ERROR_NOTEMPLATE,
                ['template' => $tpl]
            );
        }

        // are we compiling source into a script?
        if (is_object($this->_compiler)) {
            // compile the template source and get the path to the
            // compiled script (will be returned instead of the
            // source path)
            $result = $this->_compiler->compile($file);
        } else {
            // no compiling requested, return the source path
            $result = $file;
        }

        // is there a script from the compiler?
        if (! $result || $this->isError($result)) {
            if ($setScript) {
                $this->_script = null;
            }

            // return an error, along with any error info
            // generated by the compiler.
            return $this->error(
                SAVANT2_ERROR_NOSCRIPT,
                [
                    'template' => $tpl,
                    'compiler' => $result,
                ]
            );
        } else {
            if ($setScript) {
                $this->_script = $result;
            }

            return $result;
        }
    }

    /**
     * This is a an alias to loadTemplate() that cannot set the script.
     *
     *
     * @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 SAVANT2_ERROR_NOTEMPLATE code.
     */
    public function findTemplate($tpl = null)
    {
        return $this->loadTemplate($tpl, false);
    }

    /**
     * Executes a template script and returns the results as a string.
     *
     * @param string $_tpl The name of the template source file ...
     * automatically searches the template paths and compiles as needed.
     *
     * @return string The output of the the template script.
     *
     * @throws object An error object with a SAVANT2_ERROR_NOSCRIPT code.
     */
    public function fetch($_tpl = null)
    {
        // clear prior output
        $this->_output = null;

        // load the template script
        $_result = $this->loadTemplate($_tpl, true);

        // is there a template script to be processed?
        if ($this->isError($_result)) {
            return $_result;
        }

        // unset so as not to introduce into template scope
        unset($_tpl);
        unset($_result);

        // never allow a 'this' property
        if (isset($this->this)) {
            unset($this->this);
        }

        // are we extracting variables into local scope?
        if ($this->_extract) {
            // extract references to this object's public properties.
            // this allows variables assigned by-reference to refer all
            // the way back to the model logic.  variables assigned
            // by-copy only refer back to the property.
            foreach (array_keys(get_object_vars($this)) as $_prop) {
                if (substr($_prop, 0, 1) != '_') {
                    // set a variable-variable to an object property
                    // reference
                    $$_prop = &$this->$_prop;
                }
            }

            // unset private loop vars
            unset($_prop);
        }

        // start capturing output into a buffer
        ob_start();

        // include the requested template filename in the local scope
        // (this will execute the view logic).
        include $this->_script;

        // done with the requested template; get the buffer and
        // clear it.
        $this->_output = ob_get_contents();
        ob_end_clean();

        // done!
        return $this->applyFilters();
    }

    /**
     * Execute and display a template script.
     *
     * @param string $tpl The name of the template file to parse;
     * automatically searches through the template paths.
     *
     * @return void
     *
     * @throws object An error object with a SAVANT2_ERROR_NOSCRIPT code.
     *
     * @see fetch()
     */
    public function display($tpl = null)
    {
        $result = $this->fetch($tpl);
        if ($this->isError($result)) {
            return $result;
        } else {
            echo $result;
        }
    }

    // -----------------------------------------------------------------
    //
    // Plugins
    //
    // -----------------------------------------------------------------

    /**
     * Loads a plugin class and instantiates it within Savant2.
     *
     *
     * @param string $name The plugin name (not including Savant2_Plugin_
     * prefix).
     *
     * @param array $conf An associative array of plugin configuration
     * options.
     *
     * @param bool $savantRef Default false.  When true, sets the $Savant
     * property of the filter to a reference to this Savant object.
     *
     * @return void
     *
     * @throws object An error object with a SAVANT2_ERROR_NOPLUGIN code.
     */
    public function loadPlugin($name, $conf = [], $savantRef = null)
    {
        // if no $savantRef is provided, use the default.
        if (is_null($savantRef)) {
            $savantRef = $this->_reference;
        }

        // some basic information
        $class = "Savant2_Plugin_$name";
        $file = "$class.php";

        // is it loaded?
        if (! class_exists($class)) {
            $result = $this->findFile('resource', $file);
            if (! $result) {
                return $this->error(
                    SAVANT2_ERROR_NOPLUGIN,
                    ['plugin' => $name]
                );
            } else {
                include_once $result;
            }
        }

        // is it instantiated?
        if (! isset($this->_resource['plugin'][$name]) ||
            ! is_object($this->_resource['plugin'][$name]) ||
            ! is_a($this->_resource['plugin'][$name], $class)) {

            // instantiate it
            $this->_resource['plugin'][$name] = new $class($conf);

            // add a Savant reference if requested
            if ($savantRef) {
                $this->_resource['plugin'][$name]->Savant = &$this;
            }
        }
    }

    /**
     * Unloads one or more plugins from Savant2.
     *
     *
     * @param string|array $name The plugin name (not including Savant2_Plugin_
     * prefix).  If null, unloads all plugins; if a string, unloads that one
     * plugin; if an array, unloads all plugins named as values in the array.
     *
     * @return void
     */
    public function unloadPlugin($name = null)
    {
        if (is_null($name)) {
            $this->_resource['plugin'] = [];
        } else {
            settype($name, 'array');
            foreach ($name as $key) {
                if (isset($this->_resource['plugin'][$key])) {
                    unset($this->_resource['plugin'][$key]);
                }
            }
        }
    }

    /**
     * Executes a plugin with arbitrary parameters and returns the
     * result.
     *
     *
     * @param string $name The plugin name (not including Savant2_Plugin_
     * prefix).
     *
     * @return mixed The plugin results.
     *
     * @throws object An error object with a SAVANT2_ERROR_NOPLUGIN code.
     *
     * @see loadPlugin()
     */
    public function splugin($name)
    {
        // attempt to load the plugin
        $result = $this->loadPlugin($name);
        if ($this->isError($result)) {
            return $result;
        }

        // call the plugin's "plugin()" method with arguments,
        // dropping the first argument (the plugin name)
        $args = func_get_args();
        array_shift($args);

        return call_user_func_array(
            [&$this->_resource['plugin'][$name], 'plugin'], $args
        );
    }

    /**
     * Executes a plugin with arbitrary parameters and displays the
     * result.
     *
     *
     * @param string $name The plugin name (not including Savant2_Plugin_
     * prefix).
     *
     * @return void
     *
     * @throws object An error object with a SAVANT2_ERROR_NOPLUGIN code.
     */
    public function plugin($name)
    {
        $args = func_get_args();

        $result = call_user_func_array(
            [&$this, 'splugin'],
            $args
        );

        if ($this->isError($result)) {
            return $result;
        } else {
            echo $result;
        }
    }

    /**
     * PHP5 ONLY: Magic method alias to plugin().
     *
     * E.g., instead of $this->plugin('form', ...) you would use
     * $this->form(...).  You can set this to use any other Savant2 method
     * by issuing, for example, setCall('splugin') to use splugin() ... which
     * is really the only other sensible choice.
     *
     *
     * @param string $func The plugin name.
     *
     * @param array $args Arguments passed to the plugin.
     *
     * @return void
     *
     * @throws object An error object with a SAVANT2_ERROR_NOPLUGIN code.
     */
    public function __call($func, $args)
    {
        // add the plugin name to the args
        array_unshift($args, $func);

        // call the plugin() method
        return call_user_func_array(
            [&$this, $this->_call],
            $args
        );
    }

    // -----------------------------------------------------------------
    //
    // Filters
    //
    // -----------------------------------------------------------------

    /**
     * Loads a filter class and instantiates it within Savant2.
     *
     *
     * @param string $name The filter name (not including Savant2_Filter_
     * prefix).
     *
     * @param array $conf An associative array of filter configuration
     * options.
     *
     * @param bool $savantRef Default false.  When true, sets the $Savant
     * property of the filter to a reference to this Savant object.
     *
     * @return void
     *
     * @throws object An error object with a SAVANT2_ERROR_NOFILTER code.
     */
    public function loadFilter($name, $conf = [], $savantRef = null)
    {
        // if no $savantRef is provided, use the default.
        if (is_null($savantRef)) {
            $savantRef = $this->_reference;
        }

        // some basic information
        $class = "Savant2_Filter_$name";
        $file = "$class.php";

        // is it loaded?
        if (! class_exists($class)) {
            $result = $this->findFile('resource', $file);
            if (! $result) {
                return $this->error(
                    SAVANT2_ERROR_NOFILTER,
                    ['filter' => $name]
                );
            } else {
                include_once $result;
            }
        }

        // is it instantiated?
        if (! isset($this->_resource['filter'][$name]) ||
            ! is_object($this->_resource['filter'][$name]) ||
            ! is_a($this->_resource['filter'][$name], $class)) {

            // instantiate it
            $this->_resource['filter'][$name] = new $class($conf);

            // add a Savant reference if requested
            if ($savantRef) {
                $this->_resource['filter'][$name]->Savant = &$this;
            }
        }
    }

    /**
     * Unloads one or more filters from Savant2.
     *
     *
     * @param string|array $name The filter name (not including Savant2_Filter_
     * prefix).  If null, unloads all filters; if a string, unloads that one
     * filter; if an array, unloads all filters named as values in the array.
     *
     * @return void
     */
    public function unloadFilter($name = null)
    {
        if (is_null($name)) {
            $this->_resource['filter'] = [];
        } else {
            settype($name, 'array');
            foreach ($name as $key) {
                if (isset($this->_resource['filter'][$key])) {
                    unset($this->_resource['filter'][$key]);
                }
            }
        }
    }

    /**
     * Apply all loaded filters, in order, to text.
     *
     *
     * @param string $text The text to which filters should be applied.
     * If null, sets the text to $this->_output.
     *
     * @return string The text after being passed through all loded
     * filters.
     */
    public function applyFilters($text = null)
    {
        // set to output text if no text specified
        if (is_null($text)) {
            $text = $this->_output;
        }

        // get the list of filter names...
        $filter = array_keys($this->_resource['filter']);

        // ... and apply them each in turn.
        foreach ($filter as $name) {
            $this->_resource['filter'][$name]->filter($text);
        }

        // done
        return $text;
    }

    // -----------------------------------------------------------------
    //
    // Error handling
    //
    // -----------------------------------------------------------------

    /**
     * Returns an error object.
     *
     *
     * @param int $code A SAVANT2_ERROR_* constant.
     *
     * @param array $info An array of error-specific information.
     *
     * @return object An error object of the type specified by
     * $this->_error.
     */
    public function &error($code, $info = [])
    {
        // the error config array
        $conf = [
            'code' => $code,
            'text' => 'Savant2: ',
            'info' => (array) $info,
        ];

        // set an error message from the globals
        if (isset($GLOBALS['_SAVANT2']['error'][$code])) {
            $conf['text'] .= $GLOBALS['_SAVANT2']['error'][$code];
        } else {
            $conf['text'] .= '???';
        }

        // set up the error class name
        if ($this->_error) {
            $class = 'Savant2_Error_'.$this->_error;
        } else {
            $class = 'Savant2_Error';
        }

        // set up the error class file name
        $file = $class.'.php';

        // is it loaded?
        if (! class_exists($class)) {

            // find the error class
            $result = $this->findFile('resource', $file);
            if (! $result) {
                // could not find the custom error class, revert to
                // Savant_Error base class.
                $class = 'Savant2_Error';
                $result = dirname(__FILE__).'/Savant2/Error.php';
            }

            // include the error class
            include_once $result;
        }

        // instantiate and return the error class
        $err = new $class($conf);

        return $err;
    }

    /**
     * Tests if an object is of the Savant2_Error class.
     *
     *
     * @param object &$obj The object to be tested.
     *
     * @return bool True if $obj is an error object of the type
     * Savant2_Error, or is a subclass that Savant2_Error. False if not.
     */
    public function isError(&$obj)
    {
        if (is_object($obj)) {
            if (is_a($obj, 'Savant2_Error') ||
                is_subclass_of($obj, 'Savant2_Error')) {
                return true;
            }
        }

        return false;
    }
}
