1<?php
2/**
3 * @package tikiwiki
4 */
5// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
6//
7// All Rights Reserved. See copyright.txt for details and a complete list of authors.
8// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
9// $Id$
10
11$inputConfiguration = [
12  [ 'staticKeyFilters' => [
13	'date' => 'digits',
14	'maxRecords' => 'digits',
15	'highlight' => 'text',
16	'where' => 'text',
17	'find' => 'text',
18	'searchLang' => 'word',
19	'words' => 'text',
20	'boolean' => 'word',
21	'storeAs' => 'int',
22	]
23  ]
24];
25
26$section = 'search';
27require_once('tiki-setup.php');
28$access->check_feature('feature_search');
29$access->check_permission('tiki_p_search');
30$smarty->assign('headtitle', tr('Search'));
31
32//get_strings tra("Searchindex")
33//ini_set('display_errors', true);
34//error_reporting(E_ALL);
35
36foreach (['find', 'highlight', 'where'] as $possibleKey) {
37	if (empty($_REQUEST['filter']) && ! empty($_REQUEST[$possibleKey])) {
38		$_REQUEST['filter']['content'] = $_REQUEST[$possibleKey];
39	}
40}
41$filter = isset($_REQUEST['filter']) ? $_REQUEST['filter'] : [];
42$postfilter = isset($_REQUEST['postfilter']) ? $_REQUEST['postfilter'] : [];
43$facets = [];
44
45if (count($filter) || count($postfilter)) {
46	if (isset($_REQUEST['save_query'])) {
47		$_SESSION['quick_search'][(int) $_REQUEST['save_query']] = $_REQUEST;
48	}
49	$offset = isset($_REQUEST['offset']) ? $_REQUEST['offset'] : 0;
50	$maxRecords = empty($_REQUEST['maxRecords']) ? $prefs['maxRecords'] : $_REQUEST['maxRecords'];
51
52	if ($access->is_serializable_request(true)) {
53		$jitRequest->replaceFilter('fields', 'word');
54		$fetchFields = array_merge(['title', 'modification_date', 'url'], $jitRequest->asArray('fields', ','));
55		;
56
57		$results = tiki_searchindex_get_results($filter, $postfilter, $offset, $maxRecords);
58
59		$smarty->loadPlugin('smarty_function_object_link');
60		$smarty->loadPlugin('smarty_modifier_sefurl');
61		foreach ($results as &$res) {
62			foreach ($fetchFields as $f) {
63				if (isset($res[$f])) {
64					$res[$f]; // Dynamic load if applicable
65				}
66			}
67			$res['link'] = smarty_function_object_link(
68				[
69					'type' => $res['object_type'],
70					'id' => $res['object_id'],
71					'title' => $res['title'],
72				],
73				$smarty->getEmptyInternalTemplate()
74			);
75			$res = array_filter(
76				$res,
77				function ($v) {
78					return ! is_null($v);
79				}
80			);	// strip out null values
81		}
82		// add facet/aggregations to the serialised outout
83		if ($prefs['search_use_facets'] == 'y') {
84			$facets = array_map(
85				function ($facet) {
86					return $facet->getOptions();
87				},
88				$results->getFacets()
89			);
90			$resultArray = [
91				'count' => $results->count(),
92				'maxRecords' => $results->getMaxRecords(),
93				'offset' => $results->getOffset(),
94				'result' => (array) $results,
95				'facets' => $facets,
96			];
97			$results = $resultArray;
98		}
99
100		$access->output_serialized(
101			$results,
102			[
103				'feedTitle' => tr('%0: Results for "%1"', $prefs['sitetitle'], isset($filter['content']) ? $filter['content'] : ''),
104				'feedDescription' => tr('Search Results'),
105				'entryTitleKey' => 'title',
106				'entryUrlKey' => 'url',
107				'entryModificationKey' => 'modification_date',
108				'entryObjectDescriptors' => ['object_type', 'object_id'],
109			]
110		);
111		exit;
112	} else {
113		$cachelib = TikiLib::lib('cache');
114		$cacheType = 'search';
115		$cacheName = $user . '/' . $offset . '/' . $maxRecords . '/' . serialize($filter);
116		$isCached = false;
117		if (! empty($prefs['unified_user_cache']) && $cachelib->isCached($cacheName, $cacheType)) {
118			list($date, $html) = $cachelib->getSerialized($cacheName, $cacheType);
119			if ($date > $tikilib->now - $prefs['unified_user_cache'] * 60) {
120				$isCached = true;
121			}
122		}
123		$excludedFacets = tiki_searchindex_get_excluded_facets();
124		if (! $isCached) {
125			$results = tiki_searchindex_get_results($filter, $postfilter, $offset, $maxRecords);
126			$facets = array_filter(array_map(
127				function ($facet) use ($excludedFacets) {
128					$name = $facet->getName();
129					if (! in_array($name, $excludedFacets)) {
130						return $name;
131					} else {
132						return '';
133					}
134				},
135				$results->getFacets()
136			));
137
138			$plugin = new Search_Formatter_Plugin_SmartyTemplate('searchresults-plain.tpl');
139			$plugin->setData(
140				[
141					'prefs' => $prefs,
142				]
143			);
144			$fields = [
145				'title' => null,
146				'url' => null,
147				'modification_date' => null,
148				'highlight' => null,
149			];
150			if ($prefs['feature_search_show_visit_count'] === 'y') {
151				$fields['visits'] = null;
152			}
153			$plugin->setFields($fields);
154
155			$formatter = Search_Formatter_Factory::newFormatter($plugin);
156
157			$wiki = $formatter->format($results);
158			$parserLib = TikiLib::lib('parser');
159			$wiki = $parserLib->searchFilePreview($wiki);
160			$html = $parserLib->parse_data(
161				$wiki,
162				[
163					'is_html' => true,
164				]
165			);
166			if (! empty($prefs['unified_user_cache'])) {
167				$cachelib->cacheItem($cacheName, serialize([$tikilib->now, $html]), $cacheType);
168			}
169		}
170		$smarty->assign('results', $html);
171	}
172}
173
174$smarty->assign('filter', $filter);
175$smarty->assign('postfilter', $postfilter);
176$smarty->assign('facets', $facets);
177
178// disallow robots to index page:
179$smarty->assign('metatag_robots', 'NOINDEX, NOFOLLOW');
180
181if ($prefs['search_use_facets'] == 'y' && $prefs['unified_engine'] === 'elastic') {
182	$smarty->display("tiki-searchfacets.tpl");
183} else {
184	$smarty->display("tiki-searchindex.tpl");
185}
186
187/**
188 * @param array $filter
189 * @param array $postfilter
190 * @param int $offset
191 * @param int $maxRecords
192 *
193 * @return Search_ResultSet
194 * @throws Exception
195 */
196function tiki_searchindex_get_results($filter, $postfilter, $offset, $maxRecords)
197{
198	global $prefs;
199
200	$unifiedsearchlib = TikiLib::lib('unifiedsearch');
201
202	$query = new Search_Query;
203	$unifiedsearchlib->initQueryBase($query);
204	$query = $unifiedsearchlib->buildQuery($filter, $query);
205	$query->filterContent('y', 'searchable');
206
207	if (count($postfilter)) {
208		$unifiedsearchlib->buildQuery($postfilter, $query->getPostFilter());
209	}
210
211	if (isset($_REQUEST['sort_mode']) && $order = Search_Query_Order::parse($_REQUEST['sort_mode'])) {
212		$query->setOrder($order);
213	}
214
215	if ($prefs['storedsearch_enabled'] == 'y' && ! empty($_POST['storeAs'])) {
216		$storedsearch = TikiLib::lib('storedsearch');
217		$storedsearch->storeUserQuery($_POST['storeAs'], $query);
218		TikiLib::lib('smarty')->assign('display_msg', tr('Your query was stored.'));
219	}
220
221	$unifiedsearchlib->initQueryPermissions($query);
222
223	$query->setRange($offset, $maxRecords);
224
225	if ($prefs['feature_search_stats'] == 'y') {
226		$stats = TikiLib::lib('searchstats');
227		foreach ($query->getTerms() as $term) {
228			$stats->register_term_hit($term);
229		}
230	}
231
232	if ($prefs['search_use_facets'] == 'y') {
233		$provider = $unifiedsearchlib->getFacetProvider();
234		$facetLabels = [];
235
236		if ($prefs['search_avoid_duplicated_facet_labels'] === 'y') {
237			foreach ($provider->getFacets() as $facet) {
238				$facetLabels[] = $facet->getLabel();
239			}
240		}
241		$duplicateLabels = array_filter(
242			array_count_values($facetLabels),
243			function ($value) {
244				return $value > 1;
245			}
246		);
247
248		foreach ($provider->getFacets() as $facet) {
249			$name = $facet->getName();
250			if (! in_array($name, tiki_searchindex_get_excluded_facets())) {
251				if ($prefs['search_avoid_duplicated_facet_labels'] === 'y') {
252					$label = $facet->getLabel();
253					if (key_exists($label, $duplicateLabels)) {
254						// it's almost always tracker fields that are duplicated, so just them for now
255						if (strpos($name, 'tracker_field_') === 0) {
256							$field =TikiLib::lib('trk')->get_tracker_field(substr($name, 14));
257							$definition = \Tracker_Definition::get($field['trackerId']);
258							$facet->setLabel($label . ' (' . $definition->getConfiguration('name') . ')');
259						}
260					}
261				}
262				$query->requestFacet($facet);
263			}
264		}
265	}
266
267
268	if ($prefs['unified_highlight_results'] === 'y') {
269		$query->applyTransform(
270			new \Search\ResultSet\UrlHighlightTermsTransform(
271				$query->getTerms()
272			)
273		);
274	}
275
276	try {
277		if ($prefs['federated_enabled'] == 'y' && ! empty($filter['content'])) {
278			$fed = TikiLib::lib('federatedsearch');
279			$fed->augmentSimpleQuery($query, $filter['content']);
280		}
281
282		$resultset = $query->search($unifiedsearchlib->getIndex());
283
284		return $resultset;
285	} catch (Search_Elastic_TransportException $e) {
286		Feedback::error(tr('Search functionality currently unavailable.'));
287	} catch (Exception $e) {
288		Feedback::error($e->getMessage());
289	}
290
291	return new Search_ResultSet([], 0, 0, -1);
292}
293
294/**
295 * Temporarily exclude the two date range facets from tiki-searchindex.php as they don't work
296 *
297 * @return array
298 */
299function tiki_searchindex_get_excluded_facets()
300{
301	global $prefs;
302
303	return array_merge(
304		$prefs['search_excluded_facets'],
305		[
306			'date_histogram',
307			'date_range',
308		]
309	);
310}
311
312