Location: PHPKode > projects > SolarPHP > solar-system-1.1.1/solar/source/solar/Solar/Filter.php
<?php
/**
 * 
 * Handler for validating and sanitizing user input.
 * 
 * Includes one-off filtering as well as filter chains for flat data arrays.
 * 
 * @category Solar
 * 
 * @package Solar_Filter Filters to sanitize and validate user input.
 * 
 * @author Paul M. Jones <hide@address.com>
 * 
 * @author Matthew Weier O'Phinney <hide@address.com>
 * 
 * @license http://opensource.org/licenses/bsd-license.php BSD
 * 
 * @version $Id: Filter.php 4533 2010-04-23 16:35:15Z pmjones $
 * 
 */
class Solar_Filter extends Solar_Base
{
    /**
     * 
     * User-defined configuration values.
     * 
     * @config array classes Base class names for filters.
     * 
     * @var array
     * 
     */
    protected $_Solar_Filter = array(
        'classes' => array(),
    );
    
    /**
     * 
     * The chain of filters to be applied to the data array.
     * 
     * Format is 'data_key' => array(), where the sub-array is a sequential
     * array of callbacks.
     * 
     * For example, this will filter the $data['rank'] value to validate as
     * an integer in the range 0-9.
     * 
     *     $this->_chain_filters = array(
     *         'rank' => array(
     *             'validateInt',
     *             array('validateRange', 0, 9),
     *         )
     *     );
     * 
     * @var array
     * 
     * @see addFilter()
     * 
     * @see addFilters()
     * 
     * @see process()
     * 
     */
    protected $_chain_filters = array();
    
    /**
     * 
     * After processing, this contains the list of validation failure messages
     * for each data key.
     * 
     * @var array
     * 
     */
    protected $_chain_invalid = array();
    
    /**
     * 
     * The object used for generating "invalid" messages.
     * 
     * Defaults to $this.
     * 
     * @var Solar_Base
     * 
     */
    protected $_chain_locale_object;
    
    /**
     * 
     * Tells the filter chain if a particular data key is required.
     * 
     * The key is the data key name, the value is a boolean (true if required,
     * false if not).
     * 
     * @var array
     * 
     * @see setRequire()
     * 
     */
    protected $_chain_require = array();
    
    /**
     * 
     * Tells the filter chain which data keys to filter.
     * 
     * If the whitelist is empty, filter all data keys.
     * 
     * @var array
     * 
     * @see setRequire()
     * 
     */
    protected $_chain_whitelist = array();
    
    /**
     * 
     * The data array to be filtered by the chain.
     * 
     * @var array
     * 
     * @see process()
     * 
     */
    protected $_data;
    
    /**
     * 
     * The name of the data key currently being processed.
     * 
     * @var string
     * 
     * @see process()
     * 
     */
    protected $_data_key;
    
    /**
     * 
     * Filter objects, keyed on method name.
     * 
     * For example, 'sanitizeTrim' => Solar_Filter_Sanitize_Trim object.
     * 
     * @var array
     * 
     */
    protected $_filter = array();
    
    /**
     * 
     * Are values required to be not-blank?
     * 
     * For validate methods, when $_require is true, the value must be
     * non-blank for it to validate; when false, blank values are considered
     * valid.
     * 
     * For sanitize methods, when $_require is true, the method will attempt
     * to sanitize blank values; when false, the method will return blank
     * values as nulls.
     * 
     * @var bool
     * 
     * @see setRequire()
     * 
     * @see getRequire()
     * 
     */
    protected $_require = true;
    
    /**
     * 
     * Class stack for finding filters.
     * 
     * @var Solar_Class_Stack
     * 
     */
    protected $_stack;
    
    /**
     * 
     * Post-construction tasks to complete object construction.
     * 
     * @return void
     * 
     */
    protected function _postConstruct()
    {
        parent::_postConstruct();
        
        // build the filter class stack
        $this->_stack = Solar::factory('Solar_Class_Stack');
        $this->setFilterClass();
        
        // set default chain locale object
        $this->setChainLocaleObject($this);
        
        // extended setup
        $this->_setup();
    }
    
    /**
     * 
     * Call this method before you unset() this instance to fully recover
     * memory from circular-referenced objects.
     * 
     * @return void
     * 
     */
    public function free()
    {
        // each filter object instance
        foreach ($this->_filter as $key => $val) {
            unset($this->_filter[$key]);
        }
        
        // certain to be an object
        unset($this->_stack);
        
        // might be objects
        unset($this->_chain_locale_object);
        unset($this->_data);
    }
    
    /**
     * 
     * Magic call to filter methods represented as classes.
     * 
     * @param string $method The filter method to call; e.g., 'sanitizeTrim'
     * maps to `Solar_Filter_SanitizeTrim::sanitizeTrim()`.
     * 
     * @param array $params Params passed to the method, if any.
     * 
     * @return mixed
     * 
     */
    public function __call($method, $params)
    {
        $filter = $this->getFilter($method);
        return call_user_func_array(
            array($filter, $method),
            $params
        );
    }
    
    /**
     * 
     * Reset the filter class stack.
     * 
     * @param string|array $list The classes to set for the stack.
     * 
     * @return void
     * 
     * @see Solar_Class_Stack::set()
     * 
     * @see Solar_Class_Stack::add()
     * 
     */
    public function setFilterClass($list = null)
    {
        $this->_stack->setByParents($this);
        $this->_stack->add($this->_config['classes']);
        $this->_stack->add($list);
    }
    
    /**
     * 
     * Add to the filter class stack.
     * 
     * @param string|array $list The classes to add to the stack.
     * 
     * @return void
     * 
     * @see Solar_Class_Stack::add()
     * 
     */
    public function addFilterClass($list)
    {
        $this->_stack->add($list);
    }
    
    /**
     * 
     * Returns the filter class stack.
     * 
     * @return array The stack of filter classes.
     * 
     * @see Solar_Class_Stack::get()
     * 
     */
    public function getFilterClass()
    {
        return $this->_stack->get();
    }
    
    /**
     * 
     * Gets the stored filter object by method name.
     * 
     * Creates the filter object if it does not already exist.
     * 
     * @param string $method The method name, e.g. 'sanitizeTrim'.
     * 
     * @return Solar_Filter_Abstract The stored filter object.
     * 
     */
    public function getFilter($method)
    {
        if (empty($this->_filter[$method])) {
            $this->_filter[$method] = $this->newFilter($method);
        }
        
        return $this->_filter[$method];
    }
    
    /**
     * 
     * Creates a new filter object by method name.
     * 
     * @param string $method The method name, e.g. 'sanitizeTrim'.
     * 
     * @return Solar_Filter_Abstract The new filter object.
     * 
     */
    public function newFilter($method)
    {
        $method[0] = strtolower($method[0]);
        $class = $this->_stack->load($method);
        $obj = Solar::factory($class, array('filter' => $this));
        return $obj;
    }
    
    /**
     * 
     * Sets the value of the 'require' flag.
     * 
     * @param bool $flag Turn 'require' on (true) or off (false).
     * 
     * @return void
     * 
     * @see $_require
     * 
     */
    public function setRequire($flag)
    {
        $this->_require = (bool) $flag;
    }
    
    /**
     * 
     * Returns the value of the 'require' flag.
     * 
     * @return bool
     * 
     * @see $_require
     * 
     */
    public function getRequire()
    {
        return $this->_require;
    }
    
    /**
     * 
     * Sets the object used for getting locale() translations during
     * [[Solar_Filter::applyChain() | ]].
     * 
     * @param Solar_Base|null|false $spec Any Solar object with a locale() 
     * method. When null, uses $this for locale(); when false, does not 
     * localize.
     * 
     * @return void
     * 
     * @see applyChain()
     * 
     */
    public function setChainLocaleObject($spec)
    {
        if ($spec === null) {
            $this->_chain_locale_object = $this;
        } elseif ($spec === false) {
            $this->_chain_locale_object = false;
        } elseif ($spec instanceof Solar_Base) {
            $this->_chain_locale_object = $spec;
        } else {
            throw $this->_exception('ERR_CHAIN_LOCALE_OBJECT', array(
                'spec' => $spec,
            ));
        }
    }
    
    /**
     * 
     * Sets whether or not a particular data key is required to be present and
     * non-blank in the data being processed by [[Solar_Filter::applyChain() | ]].
     * 
     * @param string $key The data key.
     * 
     * @param bool $flag True if required, false if not.  Default true.
     * 
     * @return void
     * 
     * @see applyChain()
     * 
     */
    public function setChainRequire($key, $flag = true)
    {
        $this->_chain_require[$key] = (bool) $flag;
    }
    
    /**
     * 
     * Sets the whitelist of data keys for the filter chain.
     * 
     * @param array $keys The data keys to filter; if empty, will filter all
     * data keys.
     * 
     * @return void
     * 
     * @see applyChain()
     * 
     */
    public function setChainWhitelist($keys)
    {
        if (empty($keys)) {
            $this->_chain_whitelist = array();
        } else {
            $this->_chain_whitelist = (array) $keys;
        }
    }
    
    /**
     * 
     * Adds one filter-chain method for a data key.
     * 
     * @param string $key The data key.
     * 
     * @param string|array $spec The filter specification.  If a string, it's
     * just a method name. If an array, the first element is a method name,
     * and all remaining elements are parameters to that method.
     * 
     * @return void
     * 
     * @see applyChain()
     * 
     */
    public function addChainFilter($key, $spec)
    {
        $this->_chain_filters[$key][] = (array) $spec;
    }
    
    /**
     * 
     * Adds many filter-chain methods for a data key.
     * 
     * @param string $key The data key.
     * 
     * @param array $list An array of data keys and filter specifications.
     * 
     * @return void
     * 
     * @see applyChain()
     * 
     */
    public function addChainFilters($key, $list)
    {
        foreach ((array) $list as $spec) {
            $this->addChainFilter($key, $spec);
        }
    }
    
    /**
     * 
     * Resets the filter chain and required keys.
     * 
     * @param string $key Reset only this key. If empty, resets all keys.
     * 
     * @return void
     * 
     * @see applyChain()
     * 
     */
    public function resetChain($key = null)
    {
        if ($key === null) {
            $this->_chain_filters = array();
            $this->_chain_require = array();
            $this->_chain_invalid = array();
        } else {
            unset($this->_chain_filters[$key]);
            unset($this->_chain_require[$key]);
            unset($this->_chain_invalid[$key]);
        }
    }
    
    /**
     * 
     * Gets the list of invalid keys and feedback messages from the filter chain.
     * 
     * @param string $key Get messages for only this key. If empty, returns
     * messages for all keys.
     * 
     * @return array
     * 
     * @see applyChain()
     * 
     */
    public function getChainInvalid($key = null)
    {
        if ($key === null) {
            return $this->_chain_invalid;
        } elseif (! empty($this->_chain_invalid[$key])) {
            return $this->_chain_invalid[$key];
        }
    }
    
    /**
     * 
     * Gets a copy of the data array, or a specific element of data, being
     * processed by [[Solar_Filter::applyChain() | ]].
     * 
     * @param string $key If empty, returns the whole data array; otherwise,
     * returns just that key element of data.
     * 
     * @return mixed A copy of the data array or element.
     * 
     * @see applyChain()
     * 
     */
    public function getData($key = null)
    {
        if ($key === null) {
            return $this->_data;
        }
        
        if ($this->dataKeyExists($key)) {
            return $this->_data[$key];
        }
        
        return null;
    }
    
    /**
     * 
     * Sets one data element being processed by [[Solar_Filter::applyChain() | ]].
     * 
     * @param string $key Set this element key.
     * 
     * @param string $val Set the element to this value.
     * 
     * @return void
     * 
     * @see applyChain()
     * 
     */
    public function setData($key, $val)
    {
        $this->_data[$key] = $val;
    }
    
    /**
     * 
     * Gets the current data key being processed by the filter chain.
     * 
     * @return string
     * 
     * @see applyChain()
     * 
     */
    public function getDataKey()
    {
        return $this->_data_key;
    }
    
    /**
     * 
     * Does the requested key exist in the data?
     * 
     * @param string $key Checks to see if the data array has this key in it.
     * 
     * @return bool True if the data key is present, false if not.
     * 
     */
    public function dataKeyExists($key = null)
    {
        if ($this->_data instanceof Solar_Struct && isset($this->_data[$key])) {
            return true;
        }
        
        if (array_key_exists($key, $this->_data)) {
            return true;
        }
        
        return false;
    }
    
    /**
     * 
     * Applies the filter chain to an array of data in-place.
     * 
     * @param array &$data A reference to the data to be filtered; sanitizing
     * methods will be applied to the data directly, so the data is modified
     * in-place.
     * 
     * @return bool True if all elements were validated, and all required keys
     * were present and non-blank; false if not validated or a key was missing
     * or blank.
     * 
     */
    public function applyChain(&$data)
    {
        // keep the data as a property, because some extended Filter classes
        // may need to refer to various pieces of data for validation.
        $this->_data =& $data;
        
        // reset the list of invalid keys
        $this->_chain_invalid = array();
        
        // see if we actually have all the required data keys
        foreach ((array) $this->_chain_require as $key => $flag) {
            
            // is the key required?
            if (! $flag) {
                // not required
                continue;
            }
            
            // if we have a whitelist, is the key in it?
            if (! $this->_isWhitelisted($key)) {
                // not in the whitelist, skip the key
                continue;
            }
            
            // "blank" means the key does not exist in the data, or that it
            // validates as a blank value
            $blank = ! isset($this->_data[$key]) ||
                     $this->validateBlank($this->_data[$key]);
            
            // is it blank?
            if ($blank) {
                $msg = $this->_chainLocale('INVALID_NOT_BLANK');
                $this->_chain_invalid[$key][] = $msg;
            }
        }
        
        // which elements to filter?
        $keys = array_keys($this->_chain_filters);
        
        // loop through each element to be filtered
        foreach ($keys as $key) {
            
            // if it's already invalid (from "require" above)
            // then skip it.  this avoids multiple validation
            // messages on missing elements.
            if (! empty($this->_chain_invalid[$key])) {
                continue;
            }
            
            // if we have a whitelist, is the key in it?
            if (! $this->_isWhitelisted($key)) {
                // not in the whitelist, skip the key
                continue;
            }
            
            // run the filters for the data element
            $this->_applyChain($key);
        }
        
        // return the validation status; if not empty, at least one of the
        // data elements was not valid.
        $result = empty($this->_chain_invalid);
        return $result;
    }
    
    /**
     * 
     * Tells if a particular data key is in the chain whitelist; when the
     * whitelist is empty, all keys are allowed.
     * 
     * @param string $key The data key to check against the whitelist.
     * 
     * @return bool
     * 
     */
    protected function _isWhitelisted($key)
    {
        if (! $this->_chain_whitelist) {
            return true;
        } else {
            return in_array($key, $this->_chain_whitelist);
        }
    }
    
    /**
     * 
     * Support method for [[Solar_Filter::applyChain() | ]] to apply all the filters on a
     * single data element.
     * 
     * @param string $key The data element key.
     * 
     * @return void
     * 
     */
    protected function _applyChain($key)
    {
        // keep the key name
        $this->_data_key = $key;
        
        // is this key required?
        if (! empty($this->_chain_require[$key])) {
            // required
            $this->setRequire(true);
        } else {
            // not required
            $this->setRequire(false);
            // if not present, skip it entirely
            if (! $this->dataKeyExists($key)) {
                return;
            }
        }
        
        // apply the filter chain
        foreach ((array) $this->_chain_filters[$key] as $params) {
            
            // take the method name off the top of the params ...
            $method = array_shift($params);
            
            // ... and put the value in its place. we use the
            // $data[$key] instead of $val so that the data
            // array itself is updated, not the local-scope $val.
            array_unshift($params, $this->_data[$key]);
            
            // call the filtering method
            $result = $this->__call($method, $params);
            
            // what to do with the result?
            $type = strtolower(substr($method, 0, 8));
            if ($type == 'sanitize') {
                // retain the sanitized value
                $this->_data[$key] = $result;
            } elseif ($type == 'validate' && ! $result) {
                // a validation method failed, get the locale key for the
                // invalid message and translate it.
                $invalid = $this->getFilter($method)->getInvalid();
                $this->_chain_invalid[$key][] = $this->_chainLocale($invalid);
                // skip remaining filters on this key
                return;
            }
        }
    }
    
    /**
     * 
     * Uses the chain locale object to get translations before falling back
     * to this object for locale.
     * 
     * @param string $key The translation key, typically a validation method
     * name.
     * 
     * @return string
     * 
     */
    protected function _chainLocale($key)
    {
        // the translated message; default to the translation key.
        $msg = $key;
        
        // if we have a locale object, get a message from it
        if ($this->_chain_locale_object) {
            
            // try to translate
            $msg = $this->_chain_locale_object->locale($key);
            
            // if the key failed to translate, fall back to the
            // translations from $this, but only if $this wasn't
            // the source to begin with.
            $failed = $msg === null || $msg == $key;
            if ($failed && $this != $this->_chain_locale_object) {
                $msg = $this->locale($key);
            }
        }
        
        // done
        return $msg;
    }
    
    /**
     * 
     * Allows specialized setup for extended classes.
     * 
     * @return void
     * 
     */
    protected function _setup()
    {
    }
}
Return current item: SolarPHP