. */ /** * Admin-side search. * * A collection of search-related features that allow search forms to be output * and permit the DB to be queried using sets of pre-defined criteria-to-DB-field * mappings. * * @since 4.6.0 * @package Search */ namespace Textpattern\Search; class Filter { /** * The filter's event. * * @var string */ public $event; /** * The available search methods as an array of Textpattern\Search\Method. * * @var array */ protected $methods; /** * The filter's in-use search method(s). * * @var string[] */ protected $search_method; /** * The filter's user-supplied search criteria. * * @var string */ protected $crit; /** * The SQL-safe (escaped) filter's search criteria. * * @var string */ protected $crit_escaped; /** * Whether the user-supplied search criteria is to be considered verbatim (quoted) or not. * * @var bool */ protected $verbatim; /** * General constructor for searches. * * @param string $event The admin-side event to which this search relates * @param string $methods Available search methods * @param string $crit Criteria to be used in filter. If omitted, uses GET/POST value * @param string[] $method Search method(s) to filter by. If omitted, uses GET/POST value or last-used method */ public function __construct($event, $methods, $crit = null, $method = null) { $this->event = $event; callback_event_ref('search_criteria', $event, 0, $methods); $this->setMethods($methods); if ($crit === null) { $this->crit = gps('crit'); } $this->setSearchMethod($method); $this->verbatim = (bool) preg_match('/^"(.*)"$/', $this->crit, $m); $this->crit_escaped = ($this->verbatim) ? doSlash($m[1]) : doLike($this->crit); } /** * Sets filter's search methods. * * @param array $methods Array of column indices and their human-readable names/types */ private function setMethods($methods) { foreach ($methods as $key => $atts) { $this->methods[$key] = new \Textpattern\Search\Method($key, $atts); } } /** * Sets filter's options. * * @param array $options Array of method indices and their corresponding array of attributes */ private function setOptions($options) { foreach ($options as $method => $opts) { if (isset($this->methods[$method])) { $this->methods[$method]->setOptions($opts); } } } /** * Sets a method's aliases. * * @param string $method Method index to which the aliases should apply * @param array $tuples DB criteria => comma-separated list of user criteria values that are equivalent to it */ public function setAliases($method, $tuples) { if (isset($this->methods[$method])) { foreach ($tuples as $key => $value) { $columns = $this->methods[$method]->getInfo('column'); if (!$this->verbatim) { $value = strtolower($value); } foreach ($columns as $column) { $this->methods[$method]->setAlias($column, $key, do_list($value)); } } } } /** * Generates SQL statements from the current criteria and search_method. * * @param array $options Options * @return array The criteria SQL, searched value and the search locations */ public function getFilter($options = array()) { $out = array('criteria' => 1); if ($this->search_method && $this->crit !== '') { $this->setOptions($options); $search_criteria = array(); foreach ($this->search_method as $selected_method) { if (array_key_exists($selected_method, $this->methods)) { $search_criteria[] = join(' or ', $this->methods[$selected_method]->getCriteria($this->crit_escaped, $this->verbatim)); } } if ($search_criteria) { $out['crit'] = $this->crit; $out['criteria'] = join(' or ', $search_criteria); if (is_array($this->search_method)) { $out['search_method'] = join(',', $this->search_method); $this->saveDefaultSearchMethod(); } } else { $out['crit'] = ''; $out['search_method'] = join(',', $this->loadDefaultSearchMethod()); } } else { $out['crit'] = ''; $out['search_method'] = join(',', $this->loadDefaultSearchMethod()); } $out['criteria'] .= callback_event('admin_criteria', $this->event.'_list', 0, $out['criteria']); return array_values($out); } /** * Renders an admin-side search form. * * @param string $step Textpattern Step for the form submission * @param array $options Options * @return string HTML */ public function renderForm($step, $options = array()) { static $id_counter = 0; $event = $this->event; $methods = $this->getMethods(); $selected = $this->search_method; extract(lAtts(array( 'default_method' => 'all', 'submit_as' => 'get', // or 'post' 'placeholder' => '', 'label_all' => 'search_all', 'class' => '', ), (array) $options)); $selected = ($selected) ? $selected : $default_method; $submit_as = (in_array($submit_as, array('get', 'post')) ? $submit_as : 'get'); if (!is_array($selected)) { $selected = do_list($selected); } $set_all = ((count($selected) === 1 && $selected[0] === 'all') || (count($selected) === count($methods)) || (count($selected) === 0)); if ($label_all) { $methods = array('all' => gTxt($label_all)) + $methods; } $method_list = array(); foreach ($methods as $key => $value) { $name = ($key === 'all') ? 'select_all' : 'search_method[]'; $method_list[] = tag( n.tag( checkbox($name, $key, ($set_all || in_array($key, $selected)), 0, 'search-'.$key.$id_counter). n.tag($value, 'label', array('for' => 'search-'.$key.$id_counter)).n, 'div').n, 'li' ); } $button_set = n.''; if (count($method_list) > 1) { $button_set .= n.''.n; } $buttons = n.tag($button_set, 'span', array('class' => 'txp-search-buttons')).n; // So the search can be used multiple times on a page without id clashes. $id_counter++; // TODO: consider moving Route.add() to textpattern.js, but that involves adding one // call per panel that requires search, instead of auto-adding it when invoked here. return form( ( $this->crit ? span( href(gTxt('search_clear'), array('event' => $event)), array('class' => 'txp-search-clear')) : '' ). fInput('search', 'crit', $this->crit, 'txp-search-input', '', '', 24, 0, '', false, false, gTxt($placeholder)). eInput($event). sInput($step). $buttons. n.tag(join(n, $method_list), 'ul', array('class' => 'txp-dropdown')), '', '', $submit_as, 'txp-search'.($class ? ' '.$class : ''), '', '', 'search' ). script_js(<<label array. * * @return array */ public function getMethods() { $out = array(); foreach ($this->methods as $key => $method) { $out[$key] = $this->methods[$key]->getInfo('label'); } return $out; } /** * Search method(s) to filter by. If omitted, uses GET/POST value or last-used method. * * @param string[]|string $method The method key(s) as either an array of strings or a comma-separated list. */ public function setSearchMethod($method = null) { $this->search_method = empty($method) ? gps('search_method'): $method; if ($this->search_method === '') { $this->loadDefaultSearchMethod($this->event); } // Normalise to an array of trimmed trueish strings, containing keys of known $methods. $this->search_method = array_filter(do_list(join(',', (array)$this->search_method))); $this->search_method = array_intersect($this->search_method, array_keys($this->methods)); } /** * Load default search method from a private preference. * * @return string[] The default search method key(s). */ public function loadDefaultSearchMethod() { assert_string($this->event); $this->search_method = array_filter(do_list(get_pref('search_options_'.$this->event))); $this->search_method = array_intersect($this->search_method, array_keys($this->methods)); return $this->search_method; } /** * Save default search method to a private preference. */ public function saveDefaultSearchMethod() { assert_string($this->event); assert_array($this->search_method); set_pref('search_options_'.$this->event, join(', ', $this->search_method), $this->event, PREF_HIDDEN, 'text_input', 0, PREF_PRIVATE); } }