<?php
/**
* @version	$Id: search_helper.php 14241 2011-03-16 20:24:35Z alex $
* @package	In-Portal
* @copyright	Copyright (C) 1997 - 2009 Intechnic. All rights reserved.
* @license      GNU/GPL
* In-Portal is Open Source software.
* This means that this software may have been modified pursuant
* the GNU General Public License, and as distributed it includes
* or is derivative of works licensed under the GNU General Public License
* or other free or open source software licenses.
* See http://www.in-portal.org/license for copyright notices and details.
*/

	defined('FULL_PATH') or die('restricted access!');

	class kSearchHelper extends kHelper {


		/**
		 * Splits search phrase into keyword using quotes,plus and minus sings and spaces as split criteria
		 *
		 * @param string $keyword
		 * @return string
		 */
		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;
				}
			}

			$quotes_re = '/([+\-]?)"(.*?)"/';
			$no_quotes_re = '/([+\-]?)([^ ]+)/';

			$quoted_kw = preg_match_all($quotes_re, $keyword, $res);
			foreach ($res[2] as $index => $kw) $final[$kw] = $res[1][$index];
			$keyword = preg_replace($quotes_re, '', $keyword);
			$not_quoted_kw =  preg_match_all($no_quotes_re, $keyword, $res);
			foreach ($res[2] as $index => $kw) $final[$kw] = $res[1][$index];

			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 = Array();
			$plus_conditions = Array();
			$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 '-':
						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 = isset($object->VirtualFields[$field_name]) ? 'having' : 'where';
			$field_type = isset($object->Fields[$field_name]['type']) ? $object->Fields[$field_name]['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_'.$this->Application->GetSID().'(_[\d]+){0,1}_edit_(.*)/', $table_name, $regs);
			if ($is_temp_table) {
				$table_name = $regs[1].TABLE_PREFIX.'ses_'.EDIT_MARK.'_edit_'.$regs[3]; // edit_mark will be replaced with sid[_main_wid] in AddFilters
			}

			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) );
		}

		/**
		 * Enter description here...
		 *
		 * @param kEvent $event
		 */
		function performSearch(&$event)
		{
			$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 true;
			}

			if ($search_keyword) {
				$this->processAutomaticFilters($event, $search_keyword, $custom_filter);
			}
		}


		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();
			$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 $search_field
		 * @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 (array_key_exists($field_name, $object->VirtualFields) && !array_key_exists($field_name, $object->CalculatedFields)) {
				// 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
			$formatter = getArrayValue($object->Fields[$field_name], '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($object->Fields[$field_name], 'use_phrases');
						$field_options = $object->GetFieldOptions($field_name);
						$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 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) {
						list ($filter_type, $field_value) = each($field_options);
						$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) {
				list ($filter_type, $field_options) = each($field_options);
				$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 (!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) {
				list ($filter_type, $field_options) = each($field_options);

				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;
		}

		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 ($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;
					}
					break;

				case 'float_range':
					// MySQL can't compare values in "float" type columns using "=" operator
					$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 ($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 ($from !== false && $to === false) {
						$from = strtotime(date('Y-m-d', $from) . ' 00:00:00', $from); // reset to morning
						$to = $from + $day_seconds;
					}
					elseif ($from === false && $to !== false) {
						$to = strtotime(date('Y-m-d', $to) . ' 23:59:59', $to); // reset to evening
						$from = $to - $day_seconds;
					}

					if ($from !== false && $to !== false) {
						$filter_value = $table_name.'`'.$field_name.'` >= '.$from.' AND '.$table_name.'`'.$field_name.'` <= '.$to;
					}
					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 '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
		 */
		function processRangeField(&$object, $search_field, $value, $type)
		{
			if (!strlen($value[$type])) {
				return false;
			}

			$options = $object->GetFieldOptions($search_field);
			$dt_separator = array_key_exists('date_time_separator', $options) ? $options['date_time_separator'] : ' ';
			$value[$type] = trim($value[$type], $dt_separator); // trim any

			$tmp_value = explode($dt_separator, $value[$type], 2);
			if (count($tmp_value) == 1) {
				$time_format = $this->_getInputTimeFormat($options);
				if ($time_format) {
					// time is missing, but time format available -> guess time and add to date
					$time = ($type == 'from') ? adodb_mktime(0, 0, 0) : adodb_mktime(23, 59, 59);
					$time = adodb_date($time_format, $time);
					$value[$type] .= $dt_separator . $time;
				}
			}

			$formatter =& $this->Application->recallObject($object->Fields[$search_field]['formatter']);

			$value_ts = $formatter->Parse($value[$type], $search_field, $object);
			$pseudo = getArrayValue($object->FieldErrors, $search_field, 'pseudo');
			if ($pseudo) {
				unset($object->FieldErrors[$search_field]); // remove error!
				// invalid format -> ignore this date in search
				return false;
			}
			return $value_ts;
		}

		/**
		 * Returns InputTimeFormat using given field options
		 *
		 * @param Array $field_options
		 * @return string
		 */
		function _getInputTimeFormat($field_options)
		{
			if (array_key_exists('input_time_format', $field_options)) {
				return $field_options['input_time_format'];
			}

			$lang_current =& $this->Application->recallObject('lang.current');
			return $lang_current->GetDBField('InputTimeFormat');
		}

		/**
		 * 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)
		{
			$includes_or_filter =& $this->Application->makeClass('kMultipleFilter', FLT_TYPE_OR);
			$excepts_and_filter =& $this->Application->makeClass('kMultipleFilter', FLT_TYPE_AND);

			$includes_or_filter_h =& $this->Application->makeClass('kMultipleFilter', FLT_TYPE_OR);
			$excepts_and_filter_h =& $this->Application->makeClass('kMultipleFilter', 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']);
						}
					}
				}
			}

			$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, HAVING_FILTER);
			$object->addFilter('excepts_filter_h', $excepts_and_filter_h, HAVING_FILTER);
		}
	}