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