_performExactSearch = $this->Application->ConfigValue('PerformExactSearch'); } /** * Splits search phrase into keyword using quotes,plus and minus sings and spaces as split criteria * * @param string $keyword * @return Array * @access public */ public function splitKeyword($keyword) { if ( $this->Application->ConfigValue('CheckStopWords') ) { $keyword_after_remove = $this->_removeStopWords($keyword); if ( $keyword_after_remove ) { // allow to search through stop word grid $keyword = $keyword_after_remove; } } $final = Array (); $quotes_re = '/([+\-]?)"(.*?)"/'; $no_quotes_re = '/([+\-]?)([^ ]+)/'; preg_match_all($quotes_re, $keyword, $res); foreach ($res[2] as $index => $kw) { $final[$kw] = $res[1][$index]; } $keyword = preg_replace($quotes_re, '', $keyword); preg_match_all($no_quotes_re, $keyword, $res); foreach ($res[2] as $index => $kw) { $final[$kw] = $res[1][$index]; } if ( $this->_performExactSearch ) { foreach ($final AS $kw => $plus_minus) { if ( !$plus_minus ) { $final[$kw] = '+'; } } } return $final; } function getPositiveKeywords($keyword) { $keywords = $this->splitKeyword($keyword); $ret = Array(); foreach ($keywords as $keyword => $sign) { if ($sign == '+' || $sign == '') { $ret[] = $keyword; } } return $ret; } /** * Replace wildcards to match MySQL * * @param string $keyword * @return string */ function transformWildcards($keyword) { return str_replace(Array ('%', '_', '*', '?') , Array ('\%', '\_', '%', '_'), $keyword); } function buildWhereClause($keyword, $fields) { $keywords = $this->splitKeyword( $this->transformWildcards($keyword) ); $normal_conditions = $plus_conditions = $minus_conditions = Array(); foreach ($keywords as $keyword => $sign) { $keyword = $this->Conn->escape($keyword); switch ($sign) { case '+': $plus_conditions[] = implode(" LIKE '%" . $keyword . "%' OR ", $fields) . " LIKE '%" . $keyword . "%'"; break; case '-': $condition = Array (); foreach ($fields as $field) { $condition[] = $field . " NOT LIKE '%" . $keyword . "%' OR " . $field . ' IS NULL'; } $minus_conditions[] = '(' . implode(') AND (', $condition) . ')'; break; case '': $normal_conditions[] = implode(" LIKE '%" . $keyword . "%' OR ", $fields) . " LIKE '%" . $keyword . "%'"; break; } } // building where clause if ($normal_conditions) { $where_clause = '(' . implode(') OR (', $normal_conditions) . ')'; } else { $where_clause = '1'; } if ($plus_conditions) { $where_clause = '(' . $where_clause . ') AND (' . implode(') AND (', $plus_conditions) . ')'; } if ($minus_conditions) { $where_clause = '(' . $where_clause . ') AND (' . implode(') AND (', $minus_conditions) . ')'; } return $where_clause; } /** * Returns additional information about search field * * @param kDBList $object * @param string $field_name * @return Array */ function _getFieldInformation(&$object, $field_name) { $sql_filter_type = $object->isVirtualField($field_name) ? 'having' : 'where'; $field_options = $object->GetFieldOptions($field_name); $table_name = ''; $field_type = isset($field_options['type']) ? $field_options['type'] : 'string'; if (preg_match('/(.*)\.(.*)/', $field_name, $regs)) { $table_name = '`'.$regs[1].'`.'; // field from external table $field_name = $regs[2]; } elseif ($sql_filter_type == 'where') { $table_name = '`'.$object->TableName.'`.'; // field from local table } $table_name = ($sql_filter_type == 'where') ? $table_name : ''; // Replace wid inside table name to WID_MARK constant value. $is_temp_table = preg_match( '/(.*)' . TABLE_PREFIX . 'ses_[\d]+(_[\d]+){0,1}_edit_(.*)/', $table_name, $regs ); // The EDIT_MARK will be replaced with sid[_main_wid] in AddFilters. if ( $is_temp_table ) { $table_name = $regs[1] . TABLE_PREFIX . 'ses_' . EDIT_MARK . '_edit_' . $regs[3]; } return Array ($field_name, $field_type, $table_name, $sql_filter_type); } /** * Removes stop words from keyword * * @param string $keyword * @return string */ function _removeStopWords($keyword) { static $stop_words = Array (); if (!$stop_words) { $sql = 'SELECT StopWord FROM ' . $this->Application->getUnitOption('stop-word', 'TableName') . ' ORDER BY LENGTH(StopWord) DESC, StopWord ASC'; $stop_words = $this->Conn->GetCol($sql); foreach ($stop_words as $index => $stop_word) { $stop_words[$index] = '/(^| )' . preg_quote($stop_word, '/') . '( |$)/'; } } $keyword = preg_replace($stop_words, ' ', $keyword); return trim( preg_replace('/[ ]+/', ' ', $keyword) ); } /** * Performs new search on a given grid * * @param kEvent $event * @return void * @access public */ public function performSearch($event) { /** @var kDBItem $object */ $object = $event->getObject(); // process search keyword $search_keyword = $this->Application->GetVar($event->getPrefixSpecial(true) . '_search_keyword'); $this->Application->StoreVar($event->getPrefixSpecial() . '_search_keyword', $search_keyword); $custom_filter = $this->processCustomFilters($event); if ( !$search_keyword && $custom_filter === false ) { $this->resetSearch($event); return ; } if ( $search_keyword ) { $this->processAutomaticFilters($event, $search_keyword, $custom_filter); } } /** * Creates filtering sql clauses based on given search restrictions * * @param kEvent $event * @param string $search_keyword * @param Array $custom_filter * @return void */ function processAutomaticFilters($event, $search_keyword, $custom_filter) { $grid_name = $this->Application->GetVar('grid_name'); $grids = $this->Application->getUnitOption($event->Prefix, 'Grids'); $search_fields = array_keys($grids[$grid_name]['Fields']); $search_filter = Array(); /** @var kDBList $object */ $object = $event->getObject(); foreach ($search_fields as $search_field) { $custom_search = isset($custom_filter[$search_field]); $filter_data = $this->getSearchClause($object, $search_field, $search_keyword, $custom_search); if ($filter_data) { $search_filter[$search_field] = $filter_data; } else { unset($search_filter[$search_field]); } } $this->Application->StoreVar($event->getPrefixSpecial().'_search_filter', serialize($search_filter) ); } /** * Returns search clause for any particular field * * @param kDBList $object * @param string $field_name * @param string $search_keyword what we are searching (false, when building custom filter clause) * @param string $custom_search already found using custom filter * @return Array */ function getSearchClause(&$object, $field_name, $search_keyword, $custom_search) { if ($object->isVirtualField($field_name) && !$object->isCalculatedField($field_name)) { // Virtual field, that is shown in grid, but it doesn't have corresponding calculated field. // Happens, when field value is calculated on the fly (during grid display) and it is not searchable. return ''; } $search_keywords = $this->splitKeyword($search_keyword); list ($field_name, $field_type, $table_name, $sql_filter_type) = $this->_getFieldInformation($object, $field_name); $filter_value = ''; // get field clause by formatter name and/or parameters $field_options = $object->GetFieldOptions($field_name); $formatter = getArrayValue($field_options, 'formatter'); switch ($formatter) { case 'kOptionsFormatter': $search_keys = Array(); if ($custom_search === false) { // if keywords passed through simple search filter (on each grid) $use_phrases = getArrayValue($field_options, 'use_phrases'); $multiple = array_key_exists('multiple', $field_options) && $field_options['multiple']; foreach ($field_options['options'] as $key => $val) { $match_to = mb_strtolower($use_phrases ? $this->Application->Phrase($val) : $val); foreach ($search_keywords as $keyword => $sign) { // doesn't support wildcards if (strpos($match_to, mb_strtolower($keyword)) === false) { if ($sign == '+') { $filter_value = $table_name.'`'.$field_name.'` = NULL'; break; } else { continue; } } if ($sign == '+' || $sign == '') { // don't add single quotes to found option ids when multiselect (but escape string anyway) $search_keys[$key] = $multiple ? $this->Conn->escape($key) : $this->Conn->qstr($key); } elseif($sign == '-') { // if same value if found as exclusive too, then remove from search result unset($search_keys[$key]); } } } } if ($search_keys) { if ($multiple) { $filter_value = $table_name.'`'.$field_name.'` LIKE "%|' . implode('|%" OR ' . $table_name.'`'.$field_name.'` LIKE "%|', $search_keys) . '|%"'; } else { $filter_value = $table_name.'`'.$field_name.'` IN ('.implode(',', $search_keys).')'; } } $field_processed = true; break; case 'kDateFormatter': // if date is searched using direct filter, then do nothing here, otherwise search using LIKE clause $field_processed = ($custom_search !== false) ? true : false; break; default: $field_processed = false; break; } // if not already processed by formatter, then get clause by field type if (!$field_processed && $search_keywords) { switch($field_type) { case 'int': case 'integer': case 'numeric': $search_keys = Array(); foreach ($search_keywords as $keyword => $sign) { if (!is_numeric($keyword) || ($sign == '-')) { continue; } $search_keys[] = $this->Conn->qstr($keyword); } if ($search_keys) { $filter_value = $table_name.'`'.$field_name.'` IN ('.implode(',', $search_keys).')'; } break; case 'double': case 'float': case 'real': $search_keys = Array(); foreach ($search_keywords as $keyword => $sign) { $keyword = str_replace(',', '.', $keyword); if (!is_numeric($keyword) || ($sign == '-')) continue; $search_keys[] = 'ABS('.$table_name.'`'.$field_name.'` - '.$this->Conn->qstr($keyword).') <= 0.0001'; } if ($search_keys) { $filter_value = '('.implode(') OR (', $search_keys).')'; } break; case 'string': $filter_value = $this->buildWhereClause($search_keyword, Array($table_name.'`'.$field_name.'`')); break; } } if ($filter_value) { return Array('type' => $sql_filter_type, 'value' => $filter_value); } return false; } /** * Processes custom filters from submit * * @param kEvent $event * @return Array|bool */ function processCustomFilters($event) { $grid_name = $this->Application->GetVar('grid_name'); // update "custom filter" with values from submit: begin $view_name = $this->Application->RecallVar($event->getPrefixSpecial().'_current_view'); $custom_filters = $this->Application->RecallPersistentVar($event->getPrefixSpecial().'_custom_filter.'.$view_name/*, ALLOW_DEFAULT_SETTINGS*/); if ($custom_filters) { $custom_filters = unserialize($custom_filters); $custom_filter = isset($custom_filters[$grid_name]) ? $custom_filters[$grid_name] : Array (); } else { $custom_filter = Array (); } // submit format custom_filters[prefix_special][field] $submit_filters = $this->Application->GetVar('custom_filters'); if ($submit_filters) { $submit_filters = getArrayValue($submit_filters, $event->getPrefixSpecial(), $grid_name); if ($submit_filters) { foreach ($submit_filters as $field_name => $field_options) { $filter_type = key($field_options); $field_value = $field_options[$filter_type]; $is_empty = strlen(is_array($field_value) ? implode('', $field_value) : $field_value) == 0; if ($is_empty) { if (isset($custom_filter[$field_name])) { // use isset, because non-existing key will cause "php notice"! unset($custom_filter[$field_name][$filter_type]); // remove filter if (!$custom_filter[$field_name]) { // if no filters left for field, then delete record at all unset($custom_filter[$field_name]); } } } else { $custom_filter[$field_name][$filter_type]['submit_value'] = $field_value; } } } } if ($custom_filter) { $custom_filters[$grid_name] = $custom_filter; } else { unset($custom_filters[$grid_name]); } // update "custom filter" with values from submit: end if (!$custom_filter) { // in case when no filters specified, there are nothing to process $this->Application->StorePersistentVar($event->getPrefixSpecial().'_custom_filter.'.$view_name, serialize($custom_filters) ); return false; } $object = $event->getObject(); // don't recall it each time in getCustomFilterSearchClause $grid_info = $this->Application->getUnitOption($event->Prefix.'.'.$grid_name, 'Grids'); foreach ($custom_filter as $field_name => $field_options) { $filter_type = key($field_options); $field_options = $field_options[$filter_type]; $field_options['grid_options'] = $grid_info['Fields'][$field_name]; $field_options = $this->getCustomFilterSearchClause($object, $field_name, $filter_type, $field_options); if ($field_options['value']) { unset($field_options['grid_options']); $custom_filter[$field_name][$filter_type] = $field_options; } } $custom_filters[$grid_name] = $custom_filter; $this->Application->StorePersistentVar($event->getPrefixSpecial().'_custom_filter.'.$view_name, serialize($custom_filters) ); return $custom_filter; } /** * Checks, that range filters "To" part is defined for given grid * * @param string $prefix_special * @param string $grid_name * @return bool */ function rangeFiltersUsed($prefix_special, $grid_name) { static $cache = Array (); $cache_key = $prefix_special . $grid_name; if (array_key_exists($cache_key, $cache)) { return $cache[$cache_key]; } $view_name = $this->Application->RecallVar($prefix_special . '_current_view'); $custom_filters = $this->Application->RecallPersistentVar($prefix_special . '_custom_filter.' . $view_name/*, ALLOW_DEFAULT_SETTINGS*/); if (!$custom_filters) { // filters not defined for given prefix $cache[$cache_key] = false; return false; } $custom_filters = unserialize($custom_filters); if (!is_array($custom_filters) || !array_key_exists($grid_name, $custom_filters)) { // filters not defined for given grid $cache[$cache_key] = false; return false; } $range_filter_defined = false; $custom_filter = $custom_filters[$grid_name]; foreach ($custom_filter as $field_name => $field_options) { $filter_type = key($field_options); $field_options = $field_options[$filter_type]; if (strpos($filter_type, 'range') === false) { continue; } $to_value = (string)$field_options['submit_value']['to']; if ($to_value !== '') { $range_filter_defined = true; break; } } $cache[$cache_key] = $range_filter_defined; return $range_filter_defined; } /** * Return numeric range filter value + checking that it's number * * @param Array $value array containing range filter value * @return unknown */ function getRangeValue($value) { // fix user typing error, since MySQL only sees "." as decimal separator $value = str_replace(',', '.', $value); return strlen($value) && is_numeric($value) ? $this->Conn->qstr($value) : false; } /** * Returns filter clause * * @param kDBItem $object * @param string $field_name * @param string $filter_type * @param Array $field_options * @return Array */ function getCustomFilterSearchClause(&$object, $field_name, $filter_type, $field_options) { // this is usually used for mutlilingual fields and date fields if (isset($field_options['grid_options']['sort_field'])) { $field_name = $field_options['grid_options']['sort_field']; } list ($field_name, $field_type, $table_name, $sql_filter_type) = $this->_getFieldInformation($object, $field_name); $filter_value = ''; switch ($filter_type) { case 'range': $from = $this->getRangeValue($field_options['submit_value']['from']); $to = $this->getRangeValue($field_options['submit_value']['to']); if ( $from !== false && $to !== false ) { // add range filter $filter_value = $table_name . '`' . $field_name . '` >= ' . $from . ' AND ' . $table_name . '`' . $field_name . '` <= ' . $to; } elseif ( $field_type == 'int' || $field_type == 'integer' ) { if ( $from !== false ) { // add equals filter on $from $filter_value = $table_name . '`' . $field_name . '` = ' . $from; } elseif ( $to !== false ) { // add equals filter on $to $filter_value = $table_name . '`' . $field_name . '` = ' . $to; } } else { // MySQL can't compare values in "float" type columns using "=" operator if ( $from !== false ) { // add equals filter on $from $filter_value = 'ABS(' . $table_name . '`' . $field_name . '` - ' . $from . ') <= 0.0001'; } elseif ( $to !== false ) { // add equals filter on $to $filter_value = 'ABS(' . $table_name . '`' . $field_name . '` - ' . $to . ') <= 0.0001'; } } break; case 'date_range': $from = $this->processRangeField($object, $field_name, $field_options['submit_value'], 'from'); $to = $this->processRangeField($object, $field_name, $field_options['submit_value'], 'to'); $day_seconds = 23 * 60 * 60 + 59 * 60 + 59; if ( is_numeric($from) && $to === null && date('H:i:s', $from) == '00:00:00' ) { $to = $from + $day_seconds; } elseif ( $from === null && is_numeric($to) && date('H:i:s', $to) == '00:00:00' ) { $from = $to; $to += $day_seconds; } if ( is_numeric($from) && $to === null || $from === null && is_numeric($to) ) { $from = $from === null ? $to : $from; $to = $from; } if ( is_numeric($from) && is_numeric($to) ) { $from = strtotime(date('Y-m-d H:i', $from) . ':00', $from); $to = strtotime(date('Y-m-d H:i', $to) . ':59', $to); $filter_value = $table_name.'`'.$field_name.'` >= '.$from.' AND '.$table_name.'`'.$field_name.'` <= '.$to; } else { $filter_value = 'FALSE'; } break; case 'equals': case 'options': $field_value = strlen($field_options['submit_value']) ? $this->Conn->qstr($field_options['submit_value']) : false; if ($field_value) { $filter_value = $table_name.'`'.$field_name.'` = '.$field_value; } break; case 'picker': $field_value = strlen($field_options['submit_value']) ? $this->Conn->escape($field_options['submit_value']) : false; if ($field_value) { $filter_value = $table_name.'`'.$field_name.'` LIKE "%|'.$field_value.'|%"'; } break; case 'multioptions': $field_value = $field_options['submit_value']; if ( $field_value ) { $field_value = explode('|', substr($field_value, 1, -1)); $multiple = $object->GetFieldOption($field_name, 'multiple'); $field_value = $this->Conn->qstrArray($field_value, $multiple ? 'escape' : 'qstr'); if ( $multiple ) { $filter_value = $table_name . '`' . $field_name . '` LIKE "%|' . implode('|%" OR ' . $table_name . '`' . $field_name . '` LIKE "%|', $field_value) . '|%"'; } else { $filter_value = $table_name . '`' . $field_name . '` IN (' . implode(',', $field_value) . ')'; } } break; case 'like': $filter_value = $this->buildWhereClause($field_options['submit_value'], Array($table_name.'`'.$field_name.'`')); break; default: break; } $field_options['sql_filter_type'] = $sql_filter_type; $field_options['value'] = $filter_value; return $field_options; } /** * Enter description here... * * @param kdbItem $object * @param string $search_field * @param string $value * @param string $type * @param string $format_option_prefix Format option prefix. */ function processRangeField(&$object, $search_field, $value, $type, $format_option_prefix = '') { $value_by_type = $value[$type]; if ( !strlen($value_by_type) ) { return null; } $options = $object->GetFieldOptions($search_field); $dt_separator = array_key_exists('date_time_separator', $options) ? $options['date_time_separator'] : ' '; $value_by_type = trim($value_by_type, $dt_separator); // trim any $tmp_value = explode($dt_separator, $value_by_type, 2); if ( count($tmp_value) == 1 ) { $time_format = $this->_getInputTimeFormat($options, $format_option_prefix . 'time_format'); if ( $time_format ) { // time is missing, but time format available -> guess time and add to date $time = adodb_mktime(0, 0, 0); $time = adodb_date($time_format, $time); $value_by_type .= $dt_separator . $time; } } /** @var kFormatter $formatter */ $formatter = $this->Application->recallObject($options['formatter']); $format = $options[$format_option_prefix . 'format']; $value_ts = $formatter->Parse($value_by_type, $search_field, $object, $format); if ( $object->GetErrorPseudo($search_field) ) { // invalid format -> ignore this date in search $object->RemoveError($search_field); if ( $format_option_prefix == 'input_' ) { return false; } return $this->processRangeField($object, $search_field, $value, $type, 'input_'); } return $value_ts; } /** * Returns InputTimeFormat using given field options * * @param Array $field_options * @param string $format_option_name Format option name. * @return string */ function _getInputTimeFormat($field_options, $format_option_name = 'input_time_format') { if ( array_key_exists($format_option_name, $field_options) ) { return $field_options[$format_option_name]; } /** @var LanguagesItem $lang_current */ $lang_current = $this->Application->recallObject('lang.current'); $field_name = str_replace(' ', '', ucwords(str_replace('_', ' ', $format_option_name))); return $lang_current->GetDBField($field_name); } /** * Resets current search * * @param kEvent $event */ function resetSearch($event) { $this->Application->RemoveVar($event->getPrefixSpecial().'_search_filter'); $this->Application->RemoveVar($event->getPrefixSpecial().'_search_keyword'); $view_name = $this->Application->RecallVar($event->getPrefixSpecial().'_current_view'); $this->Application->RemovePersistentVar($event->getPrefixSpecial().'_custom_filter.'.$view_name); } /** * Creates filters based on "types" & "except" parameters from PrintList * * @param kEvent $event * @param Array $type_clauses * @param string $types * @param string $except_types */ function SetComplexFilter($event, &$type_clauses, $types, $except_types) { /** @var kMultipleFilter $includes_or_filter */ $includes_or_filter = $this->Application->makeClass('kMultipleFilter', Array (kDBList::FLT_TYPE_OR)); /** @var kMultipleFilter $excepts_and_filter */ $excepts_and_filter = $this->Application->makeClass('kMultipleFilter', Array (kDBList::FLT_TYPE_AND)); /** @var kMultipleFilter $includes_or_filter_h */ $includes_or_filter_h = $this->Application->makeClass('kMultipleFilter', Array (kDBList::FLT_TYPE_OR)); /** @var kMultipleFilter $excepts_and_filter_h */ $excepts_and_filter_h = $this->Application->makeClass('kMultipleFilter', Array (kDBList::FLT_TYPE_AND)); if ( $types ) { $types = explode(',', $types); foreach ($types as $type) { $type = trim($type); if ( isset($type_clauses[$type]) ) { if ( $type_clauses[$type]['having_filter'] ) { $includes_or_filter_h->addFilter('filter_' . $type, $type_clauses[$type]['include']); } else { $includes_or_filter->addFilter('filter_' . $type, $type_clauses[$type]['include']); } } } } if ( $except_types ) { $except_types = explode(',', $except_types); foreach ($except_types as $type) { $type = trim($type); if ( isset($type_clauses[$type]) ) { if ( $type_clauses[$type]['having_filter'] ) { $excepts_and_filter_h->addFilter('filter_' . $type, $type_clauses[$type]['except']); } else { $excepts_and_filter->addFilter('filter_' . $type, $type_clauses[$type]['except']); } } } } /** @var kDBList $object */ $object = $event->getObject(); $object->addFilter('includes_filter', $includes_or_filter); $object->addFilter('excepts_filter', $excepts_and_filter); $object->addFilter('includes_filter_h', $includes_or_filter_h, kDBList::HAVING_FILTER); $object->addFilter('excepts_filter_h', $excepts_and_filter_h, kDBList::HAVING_FILTER); } /** * Ensures empty search table * * @return void */ public function ensureEmptySearchTable() { $search_table = $this->getSearchTable(); $sql = 'CREATE TABLE IF NOT EXISTS ' . $search_table . ' ( `Relevance` decimal(8,5) DEFAULT NULL, `ItemId` int(11) NOT NULL DEFAULT 0, `ResourceId` int(11) DEFAULT NULL, `ItemType` int(1) NOT NULL DEFAULT 0, `EdPick` tinyint(4) NOT NULL DEFAULT 0, KEY `ResourceId` (`ResourceId`), KEY `Relevance` (`Relevance`) ) ENGINE = MEMORY'; $this->Conn->Query($sql); $sql = 'TRUNCATE TABLE ' . $search_table; $this->Conn->Query($sql); } /** * Search table name * * @return string */ public function getSearchTable() { try { $sid = $this->Application->GetSID(Session::PURPOSE_REFERENCE); } catch ( RuntimeException $e ) { // 1. Put some real variable into session, or otherwise it won't even save to the database. $this->Application->StoreVar('search_made', 'yes'); // 2. Create session on they fly to store search results. $this->Application->Session->SaveData(); // 3. Get ID of the created session. $sid = $this->Application->GetSID(Session::PURPOSE_REFERENCE); } return TABLE_PREFIX . 'ses_' . $sid . '_' . TABLE_PREFIX . 'Search'; } }