* * @author Matthew Weier O'Phinney * * @license http://opensource.org/licenses/bsd-license.php BSD * * @version $Id$ * */ 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() { } }