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
8require_once 'lib/wiki/pluginslib.php';
9
10function wikiplugin_list_info()
11{
12	return [
13		'name' => tra('List'),
14		'documentation' => 'PluginList',
15		'description' => tra('Search for, list, and filter all types of items and display custom-formatted results.'),
16		'prefs' => ['wikiplugin_list', 'feature_search'],
17		'body' => tra('List configuration information'),
18		'filter' => 'wikicontent',
19		'profile_reference' => 'search_plugin_content',
20		'iconname' => 'list',
21		'introduced' => 7,
22		'tags' => [ 'basic' ],
23		'params' => [
24			'searchable_only' => [
25				'required' => false,
26				'name' => tra('Searchable Only Results'),
27				'description' => tra('Only include results marked as searchable in the index.'),
28				'filter' => 'digits',
29				'default' => '1',
30				'options' => [
31					['text' => tra(''), 'value' => ''],
32					['text' => tra('Yes'), 'value' => '1'],
33					['text' => tra('No'), 'value' => '0'],
34				],
35			],
36			'gui' => [
37				'required' => false,
38				'name' => tra('Use List GUI'),
39				'description' => tra('Use the graphical user interface for editing this list plugin.'),
40				'filter' => 'digits',
41				'default' => '1',
42				'options' => [
43					['text' => tra(''), 'value' => ''],
44					['text' => tra('Yes'), 'value' => '1'],
45					['text' => tra('No'), 'value' => '0'],
46				],
47			],
48			'cache' => [
49				'required' => false,
50				'name' => tra('Cache Output'),
51				'description' => tra('Cache output of this list plugin.'),
52				'filter' => 'word',
53				'since' => '20.0',
54				'options' => [
55					['text' => tra('Yes'), 'value' => 'y'],
56					['text' => tra('No'), 'value' => 'n'],
57				]
58			],
59			'cacheexpiry' => [
60				'required' => false,
61				'name' => tra('Cache Expiry Time'),
62				'description' => tra('Time before cache is expired in minutes.'),
63				'filter' => 'word',
64				'since' => '20.0',
65			],
66			'cachepurgerules' => [
67				'required' => false,
68				'name' => tra('Cache Purge Rules'),
69				'description' => tra('Purge the cache when the type:id objects are updated. Set id=0 for any of that type. Or set type:withparam:x. Examples: trackeritem:20, trackeritem:trackerId:3, file:galleryId:5, forum post:forum_id:7, forum post:parent_id:8. Note that rule changes affect future caching, not past caches.'),
70				'separator' => ',',
71				'default' => '',
72				'filter' => 'text',
73				'since' => '20.0',
74			],
75			'multisearchid' => [
76				'required' => false,
77				'name' => 'ID of MULTISEARCH block from which to render results',
78				'description' => tra('This is for much better performance by doing one search for multiple LIST plugins together. Render results from previous {MULTISEARCH(id-x)}...{MULTISEARCH} block by providing the ID used in that block.'),
79				'filter' => 'text',
80				'since' => '20.0',
81			],
82		],
83	];
84}
85
86function wikiplugin_list($data, $params)
87{
88	global $prefs;
89	global $user;
90
91	static $multisearchResults;
92	static $originalQueries;
93	static $i;
94	$i++;
95
96	if (! isset($params['cache'])) {
97		if ($prefs['unified_list_cache_default_on'] == 'y') {
98			$params['cache'] = 'y';
99		} else {
100			$params['cache'] = 'n';
101		}
102	}
103
104	if ($params['cache'] == 'y') {
105		// Exclude any type of admin from caching
106		foreach (TikiLib::lib('user')->get_user_permissions($user) as $permission) {
107			if (substr($permission, 0, 12) == 'tiki_p_admin') {
108				$params['cache'] = 'n';
109				break;
110			}
111		}
112	}
113
114	if (! isset($params['gui'])) {
115		$params['gui'] = 1;
116	}
117
118	if ($prefs['wikiplugin_list_gui'] === 'y' && $params['gui']) {
119		TikiLib::lib('header')
120			->add_jsfile('lib/jquery_tiki/pluginedit_list.js')
121			->add_jsfile('vendor_bundled/vendor/jquery-plugins/nestedsortable/jquery.ui.nestedSortable.js');
122	}
123
124	$tosearch = [];
125
126	if (isset($params['multisearchid']) && $params['multisearchid'] > '') {
127		// If 'multisearchid' is provided as a parameter to the LIST plugin, it means the list plugin
128		// is to render the results of that ID specified in the MULTISEARCH block of the "pre-searching" LIST plugin.
129		$renderMultisearch = true;
130	} else {
131		$renderMultisearch = false;
132	}
133
134	$now = TikiLib::lib('tiki')->now;
135	$cachelib = TikiLib::lib('cache');
136	$cacheType = 'listplugin';
137	if ($user) {
138		$cacheName = md5($data);
139	} else {
140		$cacheName = md5($data."loggedout");
141	}
142	if (isset($params['cacheexpiry'])) {
143		$cacheExpiry = $params['cacheexpiry'];
144	} else {
145		$cacheExpiry = $prefs['unified_list_cache_default_expiry'];
146	}
147
148	// First need to check for {MULTISEARCH()} blocks as then will need to do all queries at the same time
149	$multisearch = false;
150	$matches = WikiParser_PluginMatcher::match($data);
151	$offset_arg = 'offset';
152	$argParser = new WikiParser_PluginArgumentParser();
153
154	foreach ($matches as $match) {
155		if ($match->getName() == 'multisearch') {
156			if ($prefs['unified_engine'] != 'elastic') {
157				return tra("Error: {MULTISEARCH(id=x)} requires use of Elasticsearch as the engine.");
158			}
159			$args = $argParser->parse($match->getArguments());
160			if (!isset($args['id'])) {
161				return tra("Error: {MULTISEARCH(id=x)} needs an ID to be specified.");
162			}
163			$tosearch[$args['id']] = $match->getBody();
164			$multisearch = true;
165		}
166		if ($match->getName() == 'list' || $match->getName() == 'pagination') {
167			$args = $argParser->parse($match->getArguments());
168			if (!empty($args['offset_arg'])) {
169				// Update cacheName by offset arg to have different cache for each page of paginated list
170				$offset_arg = $args['offset_arg'];
171			}
172		}
173	}
174	if (!empty($_REQUEST[$offset_arg])) {
175		$cacheName .= '_' . $args['offset_arg'] . '=' . $_REQUEST[$offset_arg];
176	}
177	if (!$multisearch) {
178		$tosearch = [ $data ];
179	}
180
181	if ($params['cache'] == 'y') {
182		// Clean rules setting
183		$rules = array();
184		foreach ($params['cachepurgerules'] as $r) {
185			$parts = explode(':', $r, 2);
186			$cleanrule['type'] = trim($parts[0]);
187			$cleanrule['object'] = trim($parts[1]);
188			$rules[] = $cleanrule;
189		}
190		// Need to check if existing rules have been changed and therefore have to be deleted first
191		$oldrules = $cachelib->get_purge_rules_for_cache($cacheType, $cacheName);
192		if ($oldrules != $rules) {
193			$cachelib->clear_purge_rules_for_cache($cacheType, $cacheName);
194		}
195		// Now set rules
196		foreach ($rules as $rule) {
197			$cachelib->set_cache_purge_rule($rule['type'], $rule['object'], $cacheType, $cacheName);
198		}
199		// Now retrieve cache if any
200		if ($cachelib->isCached($cacheName, $cacheType)) {
201			list($date, $out) = $cachelib->getSerialized($cacheName, $cacheType);
202			if ($date > $now - $cacheExpiry * 60) {
203				if ($multisearch) {
204					$multisearchResults = $out;
205				} else {
206					return $out;
207				}
208			} else {
209				$cachelib->invalidate($cacheName, $cacheType);
210			}
211		}
212	}
213
214	$unifiedsearchlib = TikiLib::lib('unifiedsearch');
215
216	if (! $index = $unifiedsearchlib->getIndex()) {
217		return '';
218	}
219
220	if ($renderMultisearch && isset($originalQueries[$params['multisearchid']])) {
221		// Skip searching if rendering already retrieved results.
222		$query = $originalQueries[$params['multisearchid']];
223		$result = $query->search($index, '', $multisearchResults[$params['multisearchid']]);
224	} else {
225		// Perform searching
226		foreach ($tosearch as $id => $body) {
227			if ($renderMultisearch) {
228				// when rendering and if not already in $originalQueries, then just need to get the one that matches.
229				if ($params['multisearchid'] != $id) {
230					continue;
231				}
232			}
233			// Handle each query. If not multisearch will just be one.
234			$query = new Search_Query;
235			if (! isset($params['searchable_only']) || $params['searchable_only'] == 1) {
236				$query->filterIdentifier('y', 'searchable');
237			}
238			$unifiedsearchlib->initQuery($query);
239
240			$matches = WikiParser_PluginMatcher::match($body);
241
242			$builder = new Search_Query_WikiBuilder($query);
243			$builder->enableAggregate();
244			$builder->apply($matches);
245			$tsret = $builder->applyTablesorter($matches);
246			if (! empty($tsret['max']) || ! empty($_GET['numrows'])) {
247				$max = !empty($_GET['numrows']) ? $_GET['numrows'] : $tsret['max'];
248				$builder->wpquery_pagination_max($query, $max);
249			}
250			$paginationArguments = $builder->getPaginationArguments();
251
252			if (! empty($_REQUEST[$paginationArguments['sort_arg']])) {
253				$query->setOrder($_REQUEST[$paginationArguments['sort_arg']]);
254			}
255
256			PluginsLibUtil::handleDownload($query, $index, $matches);
257
258			/* set up facets/aggregations */
259			$facetsBuilder = new Search_Query_FacetWikiBuilder();
260			$facetsBuilder->apply($matches);
261			if ($facetsBuilder->getFacets()) {
262				$facetsBuilder->build($query, $unifiedsearchlib->getFacetProvider());
263			}
264
265			if ($multisearch) {
266				$originalQueries[$id] = $query;
267				$query->search($index, (string)$id);
268			} elseif ($renderMultisearch) {
269				$result = $query->search($index, '', $multisearchResults[$params['multisearchid']]);
270			} else {
271				$result = $query->search($index);
272			}
273		} // END: Foreach loop of queries
274		if ($multisearch) {
275			// Now that all the queries are in the stack, the actual search can be performed
276			$multisearchResults = $index->triggerMultisearch();
277			if ($params['cache'] == 'y') {
278				$cachelib->cacheItem($cacheName, serialize([$now, $multisearchResults]), $cacheType);
279			}
280			// No output is required when saving results of multisearch for later rendering on page by other LIST plugins
281			return '';
282		}
283	} // END: Perform searching
284
285	$result->setId('wplist-' . $i);
286
287	$resultBuilder = new Search_ResultSet_WikiBuilder($result);
288	$resultBuilder->setPaginationArguments($paginationArguments);
289	$resultBuilder->apply($matches);
290
291	$builder = new Search_Formatter_Builder;
292	$builder->setPaginationArguments($paginationArguments);
293	$builder->setId('wplist-' . $i);
294	$builder->setCount($result->count());
295	$builder->setTsOn($tsret['tsOn']);
296	$builder->apply($matches);
297
298	$result->setTsSettings($builder->getTsSettings());
299
300	$formatter = $builder->getFormatter();
301
302	$result->setTsOn($tsret['tsOn']);
303
304	if (!empty($params['resultCallback']) && is_callable($params['resultCallback'])) {
305		return $params['resultCallback']($formatter->getPopulatedList($result), $formatter);
306	}
307
308	$out = $formatter->format($result);
309
310	if ($params['cache'] == 'y') {
311		$cachelib->cacheItem($cacheName, serialize([$now, $out]), $cacheType);
312	}
313
314	return $out;
315}
316