1<?php
2// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
3//
4// All Rights Reserved. See copyright.txt for details and a complete list of authors.
5// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
6// $Id$
7
8// Controller to process requests from the custom search plugin using the list plugin to display results
9// Refactored from customsearch_ajax.php for Tiki
10
11
12class Services_Search_CustomSearchController
13{
14	private $textranges = [];
15	private $dateranges = [];
16	private $distances = [];
17	private $contentFields;
18
19	function setUp()
20	{
21		Services_Exception_Disabled::check('wikiplugin_list');
22		Services_Exception_Disabled::check('wikiplugin_customsearch');
23		Services_Exception_Disabled::check('feature_search');
24
25		$this->contentFields = TikiLib::lib('tiki')->get_preference('unified_default_content', ['contents'], true);
26	}
27
28	function action_customsearch($input)
29	{
30		global $prefs;
31
32		$this->textranges = [];
33		$this->dateranges = [];
34		$this->distances = [];
35
36		$cachelib = TikiLib::lib('cache');
37		$definition = $input->definition->word();
38		if (empty($definition) || ! $definition = $cachelib->getSerialized($definition, 'customsearch')) {
39			$smarty = \TikiLib::lib('smarty');
40			$smarty->assign('url', $_SERVER['HTTP_REFERER']);
41			$value = $smarty->fetch('search_customsearch/cache_expired.tpl');
42			return ['html' => $value];
43		}
44
45		/** @var Search_Query $query */
46		$query = $definition['query'];
47		/** @var Search_Formatter_Builder $builder */
48		$builder = $definition['builder'];
49		/** @var Search_Elastic_FacetBuilder $facetsBuilder */
50		$facetsBuilder = $definition['facets'];
51
52		$tsettings = $definition['tsettings'];
53		$tsret = $definition['tsret'];
54
55		$matches = WikiParser_PluginMatcher::match($definition['data']);
56		$builder->apply($matches);
57
58		$adddata = json_decode($input->adddata->text(), true);
59
60		$recalllastsearch = $input->recalllastsearch->int() ? true : false;
61
62		$id = $input->searchid->text();
63		if (empty($id)) {
64			$id = '0';
65		}
66
67		if ($recalllastsearch && isset($_SESSION["customsearch_$id"])) {
68			unset($_SESSION["customsearch_$id"]);
69		}
70		if ($input->sort_mode->text()) {
71			if ($recalllastsearch) {
72				$_SESSION["customsearch_$id"]["sort_mode"] = $input->sort_mode->text();
73			}
74			$query->setOrder($input->sort_mode->text());
75		}
76		if ($input->maxRecords->int()) {
77			if ($recalllastsearch) {
78				$_SESSION["customsearch_$id"]["maxRecords"] = $input->maxRecords->int();
79			}
80			$maxRecords = $input->maxRecords->int();	// pass request data required by list
81		} else {
82			$maxRecords = $prefs['maxRecords'];
83		}
84		if ($input->offset->int()) {
85			if ($recalllastsearch) {
86				$_SESSION["customsearch_$id"]["offset"] = $input->offset->int();
87			}
88			$offset = $input->offset->int();
89		} else {
90			$offset = 0;
91		}
92		$query->setRange($offset, $maxRecords);
93
94		if ($adddata) {
95			foreach ($adddata as $fieldid => $d) {
96				$config = $d['config'];
97				$name = $d['name'];
98				$value = $d['value'];
99				$group = empty($config['_group']) ? null : $config['_group'];
100
101				// save values entered as defaults while session lasts
102				if (empty($value) && $value != 0) {
103					$value = '';		// remove false or null
104				}
105				if ($recalllastsearch) {
106					$_SESSION["customsearch_$id"][$fieldid] = $value;
107				}
108
109				if (empty($config['type'])) {
110					$config['type'] = $name;
111				}
112
113				$filter = 'content'; //default
114				if (isset($config['_filter']) || $name == 'categories' || $name == 'daterange' || $name == 'distance') {
115					if ($config['_filter'] == 'language') {
116						$filter = 'language';
117					} elseif ($config['_filter'] == 'type') {
118						$filter = 'type';
119					} elseif ($config['_filter'] == 'categories' || $name == 'categories') {
120						$filter = 'categories';
121					} elseif ($name == 'daterange') {
122						$filter = 'daterange';
123					} elseif ($name == 'distance') {
124						$filter = 'distance';
125						if (! $input->sort_mode->text()) {
126							$config['sort'] = true;
127						}
128					}
129				}
130
131				if (is_array($value) && count($value) > 1) {
132					$value = implode(' ', $value);
133				} elseif (is_array($value)) {
134					$value = current($value);
135				}
136
137				$function = "cs_dataappend_{$filter}";
138				if (method_exists($this, $function)) {
139					$this->$function($query->getSubQuery($group), $config, $value);
140				}
141			}
142
143			foreach ($this->textranges as $info) {
144				if (count($info['values']) == 2) {
145					$from = array_shift($info['values']);
146					$to = array_shift($info['values']);
147					$info['query']->filterTextRange($from, $to, $info['config']['_field']);
148				}
149			}
150
151			foreach ($this->dateranges as $info) {
152				if (count($info['values']) == 2) {
153					$from = array_shift($info['values']);
154					$to = array_shift($info['values']);
155					$info['query']->filterRange($from, $to, $info['config']['_field']);
156				}
157			}
158		}
159
160		if ($prefs['storedsearch_enabled'] == 'y' && $queryId = $input->store_query->int()) {
161			// Store prior to adding
162			$storedsearchlib = TikiLib::lib('storedsearch');
163			$storeResult = $storedsearchlib->storeUserQuery($queryId, $query);
164
165			if (! $storeResult) {
166				throw new Services_Exception('Failed to store the query.', 500);
167			}
168		}
169
170		$unifiedsearchlib = TikiLib::lib('unifiedsearch');
171		$unifiedsearchlib->initQuery($query); // Done after cache because permissions vary
172
173
174		if ($prefs['unified_highlight_results'] === 'y') {
175			$query->applyTransform(
176				new \Search\ResultSet\UrlHighlightTermsTransform(
177					$query->getTerms()
178				)
179			);
180		}
181
182		$facetsBuilder->build($query, $unifiedsearchlib->getFacetProvider());
183
184		$index = $unifiedsearchlib->getIndex();
185		$resultSet = $query->search($index);
186		if (! empty($_SESSION['tikifeedback']) && $_SESSION['tikifeedback'][0]['type'] === 'error') {
187			Feedback::send_headers();
188		} else {
189			$resultSet->setTsSettings($builder->getTsSettings());
190			$resultSet->setId('wpcs-' . $id);
191			$resultSet->setTsOn($tsret['tsOn']);
192
193			$formatter = $builder->getFormatter();
194			$results = $formatter->format($resultSet);
195
196			$parserLib = TikiLib::lib('parser');
197			$results = $parserLib->searchFilePreview($results, true);
198			$results = $parserLib->parse_data($results, ['is_html' => true, 'skipvalidation' => true]);
199
200			return ['html' => $results];
201		}
202	}
203
204	private function cs_dataappend_language(Search_Query $query, $config, $value)
205	{
206		if ($config['type'] != 'text') {
207			if (! empty($config['_value'])) {
208				$value = $config['_value'];
209				$query->filterLanguage($value);
210			} elseif ($value) {
211				$query->filterLanguage($value);
212			}
213		}
214	}
215
216	private function cs_dataappend_type(Search_Query $query, $config, $value)
217	{
218		if ($config['type'] != 'text') {
219			if (! empty($config['_value'])) {
220				$value = $config['_value'];
221				$query->filterType($value);
222			} elseif ($value) {
223				$query->filterType($value);
224			}
225		}
226	}
227
228	private function cs_dataappend_content(Search_Query $query, $config, $value)
229	{
230		if (( isset($config['_textrange']) || isset($config['_daterange']) ) && ( isset($config['_emptyfrom']) || isset($config['_emptyto']) )  && $value <= '') {
231			$value = isset($config['_emptyfrom']) ? $config['_emptyfrom'] : $config['_emptyto'];
232		}
233		if ($value > '') {
234			if (isset($config['_textrange'])) {
235				$this->cs_handle_textrange($config['_textrange'], $query, $config, $value);
236			} elseif (isset($config['_daterange'])) {
237				$this->cs_handle_daterange($config['_daterange'], $query, $config, $value);
238			} elseif ($config['type'] == 'checkbox') {
239				if (empty($config['_field'])) {
240					return;
241				}
242				if (! empty($config['_value'])) {
243					if ($config['_value'] == 'n') {
244						$config['_value'] = 'NOT y';
245					}
246					$query->filterContent($config['_value'], $config['_field']);
247				} else {
248					$query->filterContent('y', $config['_field']);
249				}
250			} elseif ($config['type'] == 'radio' && ! empty($config['_value'])) {
251				if (empty($config['_field'])) {
252					$query->filterContent($config['_value']);
253				} else {
254					$query->filterContent($config['_value'], $config['_field']);
255				}
256			} else {
257				if ($config['type'] == 'select' && ! empty($config['multiple']) && ! empty($config['_operator'])) {
258					$value = str_replace(' ', " {$config['_operator']} ", $value);
259				}
260				// covers everything else including radio that have no _value set (use sent value)
261				if (empty($config['_field'])) {
262					$query->filterContent($value, $this->contentFields);
263				} else {
264					$query->filterContent($value, $config['_field']);
265				}
266			}
267		}
268	}
269
270	private function cs_handle_textrange($rangeName, Search_Query $query, $config, $value)
271	{
272		if (! isset($this->textranges[$rangeName])) {
273			$this->textranges[$rangeName] = [
274				'query' => $query,
275				'config' => $config,
276				'values' => [],
277			];
278		}
279
280		if (isset($config['_emptyother']) && isset($config['_emptyfrom'])) {
281			// is "from" value
282			if (count($this->textranges[$rangeName]['values']) == 0) {
283				$this->textranges[$rangeName]['values'][] = $config['_emptyother'];
284			} elseif (count($this->textranges[$rangeName]['values']) == 2) {
285				array_shift($this->textranges[$rangeName]['values']);
286			}
287			array_unshift($this->textranges[$rangeName]['values'], $value);
288		} elseif (isset($config['_emptyother']) && isset($config['_emptyto'])) {
289			// is "to" value
290			if (count($this->textranges[$rangeName]['values']) == 0) {
291				$this->textranges[$rangeName]['values'][] = $config['_emptyother'];
292			} elseif (count($this->textranges[$rangeName]['values']) == 2) {
293				array_pop($this->textranges[$rangeName]['values']);
294			}
295			$this->textranges[$rangeName]['values'][] = $value;
296		} else {
297			$this->textranges[$rangeName]['values'][] = $value;
298		}
299	}
300
301	private function cs_handle_daterange($rangeName, Search_Query $query, $config, $value)
302	{
303		if (! isset($this->dateranges[$rangeName])) {
304			$this->dateranges[$rangeName] = [
305				'query' => $query,
306				'config' => $config,
307				'values' => [],
308			];
309		}
310
311		if (isset($config['_emptyother']) && isset($config['_emptyfrom'])) {
312			// is "from" value
313			if (count($this->dateranges[$rangeName]['values']) == 0) {
314				$this->dateranges[$rangeName]['values'][] = $config['_emptyother'];
315			} elseif (count($this->dateranges[$rangeName]['values']) == 2) {
316				array_shift($this->dateranges[$rangeName]['values']);
317			}
318			array_unshift($this->dateranges[$rangeName]['values'], $value);
319		} elseif (isset($config['_emptyother']) && isset($config['_emptyto'])) {
320			// is "to" value
321			if (count($this->dateranges[$rangeName]['values']) == 0) {
322				$this->dateranges[$rangeName]['values'][] = $config['_emptyother'];
323			} elseif (count($this->dateranges[$rangeName]['values']) == 2) {
324				array_pop($this->dateranges[$rangeName]['values']);
325			}
326			$this->dateranges[$rangeName]['values'][] = $value;
327		} else {
328			$this->dateranges[$rangeName]['values'][] = $value;
329		}
330	}
331
332	private function cs_dataappend_categories(Search_Query $query, $config, $value)
333	{
334		if (isset($config['_filter']) && $config['_filter'] == 'categories' && $config['type'] != 'text') {
335			if (! empty($config['_value'])) {
336				$value = $config['_value'];
337			}
338		} elseif (! isset($config['_style'])) {
339			return;
340		}
341		if ($value) {
342			$deep = (isset($config['_showdeep']) && $config['_showdeep'] != 'n') || (isset($config['_deep']) && $config['_deep'] != 'n');
343			$query->filterCategory($value, $deep);
344		}
345	}
346
347	private function cs_dataappend_daterange(Search_Query $query, $config, $value)
348	{
349		if ($vals = explode(',', $value)) {
350			if (count($vals) == 2) {
351				$from = $vals[0];
352				$to = $vals[1];
353				if ((empty($config['_showtime']) || $config['_showtime'] === 'n') &&
354						(empty($config['_toendofday']) || $config['_toendofday'] === 'y')) {
355					$to += (24 * 3600) - 1;	// end date should be the end of the day, not the beginning
356				}
357				if (! empty($config['_field'])) {
358					$field = $config['_field'];
359				} else {
360					$field = 'modification_date';
361				}
362				$query->filterRange($from, $to, $field);
363			}
364		}
365	}
366
367	private function cs_dataappend_distance(Search_Query $query, $config, $value)
368	{
369		if ($vals = array_filter(preg_split('/,/', $value))) {	// ignore if dist, lat or lon is missing
370			if (count($vals) == 3) {
371				$distance = $vals[0];
372				$lat = $vals[1];
373				$lon = $vals[2];
374				if (! empty($config['_field'])) {
375					$field = $config['_field'];
376				} else {
377					$field = 'geo_point';
378				}
379				$query->filterDistance($distance, $lat, $lon, $field);
380
381				if (! empty($config['sort']) || ! empty($config['_mode'])) {
382					$order = empty($config['_mode']) ? 'asc' : $config['_mode'];
383					$sortOrder = new Search_Query_Order(
384						$field,
385						'distance',
386						$order,
387						[
388							'distance' => $distance,
389							'lat' => $lat,
390							'lon' => $lon,
391						]
392					);
393					$query->setOrder($sortOrder);
394				}
395			}
396		}
397	}
398}
399