Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 335 |
|
0.00% |
0 / 30 |
CRAP | |
0.00% |
0 / 1 |
AbstractSearch | |
0.00% |
0 / 335 |
|
0.00% |
0 / 30 |
10506 | |
0.00% |
0 / 1 |
createViewModel | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
advancedAction | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
30 | |||
redirectToSavedSearch | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
resultScrollerActive | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
rememberSearch | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
getActiveRecommendationSettings | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
90 | |||
getSearchSetupCallback | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
30 | |||
homeAction | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
redirectToLegalSearchPage | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
resultsAction | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getRssSearchResponse | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
6 | |||
getSearchResultsView | |
0.00% |
0 / 47 |
|
0.00% |
0 / 1 |
240 | |||
processJumpTo | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
20 | |||
processJumpToOnlyResult | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
30 | |||
getRedirectForRecord | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
retrieveSearchSecurely | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
saveSearchToHistory | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
restoreAdvancedSearch | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
30 | |||
getResultsManager | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getRangeSettings | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
56 | |||
getRangeFieldList | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
getDateRangeSettings | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getFullDateRangeSettings | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getGenericRangeSettings | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getNumericRangeSettings | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getAllRangeSettings | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
30 | |||
parseSpecialFacetsSetting | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
processAdvancedCheckboxes | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
72 | |||
facetListAction | |
0.00% |
0 / 58 |
|
0.00% |
0 / 1 |
56 | |||
getOptionsForClass | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | /** |
4 | * VuFind Search Controller |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * Copyright (C) Villanova University 2010. |
9 | * |
10 | * This program is free software; you can redistribute it and/or modify |
11 | * it under the terms of the GNU General Public License version 2, |
12 | * as published by the Free Software Foundation. |
13 | * |
14 | * This program is distributed in the hope that it will be useful, |
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
17 | * GNU General Public License for more details. |
18 | * |
19 | * You should have received a copy of the GNU General Public License |
20 | * along with this program; if not, write to the Free Software |
21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
22 | * |
23 | * @category VuFind |
24 | * @package Controller |
25 | * @author Demian Katz <demian.katz@villanova.edu> |
26 | * @author Juha Luoma <juha.luoma@helsinki.fi> |
27 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
28 | * @link https://vufind.org Main Page |
29 | */ |
30 | |
31 | namespace VuFind\Controller; |
32 | |
33 | use Exception; |
34 | use Laminas\Http\Response as HttpResponse; |
35 | use Laminas\Session\SessionManager; |
36 | use Laminas\Stdlib\ResponseInterface as Response; |
37 | use Laminas\View\Model\ViewModel; |
38 | use VuFind\Db\Entity\SearchEntityInterface; |
39 | use VuFind\Db\Service\SearchServiceInterface; |
40 | use VuFind\Search\RecommendListener; |
41 | use VuFind\Solr\Utils as SolrUtils; |
42 | |
43 | use function count; |
44 | use function in_array; |
45 | use function intval; |
46 | use function is_array; |
47 | |
48 | /** |
49 | * VuFind Search Controller |
50 | * |
51 | * @category VuFind |
52 | * @package Controller |
53 | * @author Demian Katz <demian.katz@villanova.edu> |
54 | * @author Juha Luoma <juha.luoma@helsinki.fi> |
55 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
56 | * @link https://vufind.org Main Page |
57 | */ |
58 | class AbstractSearch extends AbstractBase |
59 | { |
60 | /** |
61 | * Search class family to use. |
62 | * |
63 | * @var string |
64 | */ |
65 | protected $searchClassId = 'Solr'; |
66 | |
67 | /** |
68 | * Should we save searches to history? |
69 | * |
70 | * @var bool |
71 | */ |
72 | protected $saveToHistory = true; |
73 | |
74 | /** |
75 | * Should we remember the search for breadcrumb purposes? |
76 | * |
77 | * @var bool |
78 | */ |
79 | protected $rememberSearch = true; |
80 | |
81 | /** |
82 | * Create a new ViewModel. |
83 | * |
84 | * @param array $params Parameters to pass to ViewModel constructor. |
85 | * |
86 | * @return ViewModel |
87 | */ |
88 | protected function createViewModel($params = null) |
89 | { |
90 | $view = parent::createViewModel($params); |
91 | $view->searchClassId = $this->searchClassId; |
92 | return $view; |
93 | } |
94 | |
95 | /** |
96 | * Handle an advanced search |
97 | * |
98 | * @return ViewModel |
99 | */ |
100 | public function advancedAction() |
101 | { |
102 | $view = $this->createViewModel(); |
103 | $view->options = $this->getOptionsForClass(); |
104 | if ($view->options->getAdvancedSearchAction() === false) { |
105 | throw new \Exception('Advanced search not supported.'); |
106 | } |
107 | |
108 | // Handle request to edit existing saved search: |
109 | $view->saved = false; |
110 | $searchId = $this->params()->fromQuery('edit', false); |
111 | if ($searchId !== false) { |
112 | $view->saved = $this->restoreAdvancedSearch($searchId); |
113 | } |
114 | |
115 | // If we have default filters, set them up as a fake "saved" search |
116 | // to properly populate special controls on the advanced screen. |
117 | if (!$view->saved && count($view->options->getDefaultFilters()) > 0) { |
118 | $view->saved = $this->serviceLocator |
119 | ->get(\VuFind\Search\Results\PluginManager::class) |
120 | ->get($this->searchClassId); |
121 | $view->saved->getParams()->initFromRequest( |
122 | new \Laminas\Stdlib\Parameters([]) |
123 | ); |
124 | } |
125 | |
126 | return $view; |
127 | } |
128 | |
129 | /** |
130 | * Given a saved search ID, redirect the user to the appropriate place. |
131 | * |
132 | * @param int $id ID from search history |
133 | * |
134 | * @return mixed |
135 | */ |
136 | protected function redirectToSavedSearch($id) |
137 | { |
138 | $search = $this->retrieveSearchSecurely($id); |
139 | if (empty($search)) { |
140 | // User is trying to view a saved search from another session |
141 | // (deliberate or expired) or associated with another user. |
142 | throw new \Exception('Attempt to access invalid search ID'); |
143 | } |
144 | |
145 | // If we got this far, the user is allowed to view the search, so we can |
146 | // deminify it to a new object. |
147 | $savedSearch = $search->getSearchObject()?->deminify($this->getResultsManager()); |
148 | if (!$savedSearch) { |
149 | throw new Exception("Problem getting search object from search {$search->getId()}."); |
150 | } |
151 | |
152 | // Now redirect to the URL associated with the saved search; this |
153 | // simplifies problems caused by mixing different classes of search |
154 | // object, and it also prevents the user from ever landing on a |
155 | // "?saved=xxxx" URL, which may not persist beyond the current session. |
156 | // (We want all searches to be persistent and bookmarkable). |
157 | $details = $savedSearch->getOptions()->getSearchAction(); |
158 | $url = $this->url()->fromRoute($details); |
159 | $url .= $savedSearch->getUrlQuery()->getParams(false); |
160 | return $this->redirect()->toUrl($url); |
161 | } |
162 | |
163 | /** |
164 | * Is the result scroller active? |
165 | * |
166 | * @return bool |
167 | */ |
168 | protected function resultScrollerActive() |
169 | { |
170 | // Disabled by default: |
171 | return false; |
172 | } |
173 | |
174 | /** |
175 | * Store the URL of the provided search (if appropriate). |
176 | * |
177 | * @param \VuFind\Search\Base\Results $results Search results object |
178 | * |
179 | * @return void |
180 | */ |
181 | protected function rememberSearch($results) |
182 | { |
183 | // Only save search URL if the property tells us to... |
184 | if ($this->rememberSearch) { |
185 | $searchUrl = $this->url()->fromRoute( |
186 | $results->getOptions()->getSearchAction() |
187 | ) . $results->getUrlQuery()->getParams(false); |
188 | $this->getSearchMemory() |
189 | ->rememberSearch($searchUrl, $results->getSearchId()); |
190 | } |
191 | |
192 | // Always save search parameters, since these are namespaced by search |
193 | // class ID. |
194 | $this->getSearchMemory()->rememberParams($results->getParams()); |
195 | } |
196 | |
197 | /** |
198 | * Get active recommendation module settings |
199 | * |
200 | * @return array |
201 | */ |
202 | protected function getActiveRecommendationSettings() |
203 | { |
204 | // Enable recommendations unless explicitly told to disable them: |
205 | $all = ['top', 'side', 'noresults', 'bottom']; |
206 | $noRecommend = $this->params()->fromQuery('noRecommend', false); |
207 | if ( |
208 | $noRecommend === 1 || $noRecommend === '1' |
209 | || $noRecommend === 'true' || $noRecommend === true |
210 | ) { |
211 | return []; |
212 | } elseif ( |
213 | $noRecommend === 0 || $noRecommend === '0' |
214 | || $noRecommend === 'false' || $noRecommend === false |
215 | ) { |
216 | return $all; |
217 | } |
218 | return array_diff( |
219 | $all, |
220 | array_map('trim', explode(',', strtolower($noRecommend))) |
221 | ); |
222 | } |
223 | |
224 | /** |
225 | * Get a callback for setting up a search (or null if callback is unnecessary). |
226 | * |
227 | * @return mixed |
228 | */ |
229 | protected function getSearchSetupCallback() |
230 | { |
231 | // Setup callback to attach listener if appropriate: |
232 | $activeRecs = $this->getActiveRecommendationSettings(); |
233 | if (empty($activeRecs)) { |
234 | return null; |
235 | } |
236 | |
237 | $rManager = $this->serviceLocator |
238 | ->get(\VuFind\Recommend\PluginManager::class); |
239 | |
240 | $override = $this->params()->fromQuery('recommendOverride'); |
241 | |
242 | // Retrieve recommend settings from params object: |
243 | return function ($runner, $params, $searchId) use ($rManager, $activeRecs, $override) { |
244 | $listener = new RecommendListener($rManager, $searchId); |
245 | $config = []; |
246 | $rawConfig = $params->getOptions() |
247 | ->getRecommendationSettings($params->getSearchHandler()); |
248 | foreach ($rawConfig as $key => $value) { |
249 | if (in_array($key, $activeRecs)) { |
250 | $config[$key] = $value; |
251 | } |
252 | } |
253 | |
254 | // Special case: override recommend settings through parameter (used by |
255 | // combined search) |
256 | if (is_array($override)) { |
257 | $config = array_merge($config, $override); |
258 | } |
259 | |
260 | $listener->setConfig($config); |
261 | $listener->attach($runner->getEventManager()->getSharedManager()); |
262 | }; |
263 | } |
264 | |
265 | /** |
266 | * Home action |
267 | * |
268 | * @return mixed |
269 | */ |
270 | public function homeAction() |
271 | { |
272 | $blocks = $this->serviceLocator->get(\VuFind\ContentBlock\BlockLoader::class) |
273 | ->getFromSearchClassId($this->searchClassId); |
274 | return $this->createViewModel(compact('blocks')); |
275 | } |
276 | |
277 | /** |
278 | * If the search backend has thrown a "deep paging" exception, we should show a |
279 | * flash message and redirect the user to a legal page. |
280 | * |
281 | * @param array $request Incoming request parameters |
282 | * @param int $page Legal page number |
283 | * |
284 | * @return mixed |
285 | */ |
286 | protected function redirectToLegalSearchPage(array $request, int $page) |
287 | { |
288 | if (($request['page'] ?? 0) <= $page) { |
289 | throw new \Exception('Unrecoverable deep paging error.'); |
290 | } |
291 | $request['page'] = $page; |
292 | $this->flashMessenger()->addErrorMessage( |
293 | [ |
294 | 'msg' => 'deep_paging_failure', |
295 | 'tokens' => ['%%page%%' => $page], |
296 | ] |
297 | ); |
298 | return $this->redirect()->toUrl('?' . http_build_query($request)); |
299 | } |
300 | |
301 | /** |
302 | * Send search results to results view |
303 | * |
304 | * @return Response|ViewModel |
305 | */ |
306 | public function resultsAction() |
307 | { |
308 | return $this->getSearchResultsView(); |
309 | } |
310 | |
311 | /** |
312 | * Support method for getSearchResultsView() -- return the search results |
313 | * reformatted as an RSS feed. |
314 | * |
315 | * @param $view ViewModel View model |
316 | * |
317 | * @return Response |
318 | */ |
319 | protected function getRssSearchResponse(ViewModel $view): Response |
320 | { |
321 | // Build the RSS feed: |
322 | $feedHelper = $this->getViewRenderer()->plugin('resultfeed'); |
323 | $feed = $feedHelper($view->results); |
324 | $writer = new \Laminas\Feed\Writer\Renderer\Feed\Rss($feed); |
325 | $writer->render(); |
326 | |
327 | // Apply XSLT if we can find a relevant file: |
328 | $themeInfo = $this->serviceLocator->get(\VuFindTheme\ThemeInfo::class); |
329 | $themeHits = $themeInfo->findInThemes('assets/xsl/rss.xsl'); |
330 | if ($themeHits) { |
331 | $xsl = $this->url()->fromRoute('home') . 'themes/' |
332 | . $themeHits[0]['theme'] . '/' . $themeHits[0]['relativeFile']; |
333 | $writer->getElement()->parentNode->insertBefore( |
334 | $writer->getDomDocument()->createProcessingInstruction( |
335 | 'xml-stylesheet', |
336 | 'type="text/xsl" href="' . $xsl . '"' |
337 | ), |
338 | $writer->getElement() |
339 | ); |
340 | } |
341 | |
342 | // Format the response: |
343 | $response = $this->getResponse(); |
344 | $response->getHeaders()->addHeaderLine('Content-type', 'text/xml'); |
345 | $response->setContent($writer->saveXml()); |
346 | return $response; |
347 | } |
348 | |
349 | /** |
350 | * Perform a search and send results to a results view |
351 | * |
352 | * @param callable $setupCallback Optional setup callback that overrides the |
353 | * default one |
354 | * |
355 | * @return Response|ViewModel |
356 | */ |
357 | protected function getSearchResultsView($setupCallback = null) |
358 | { |
359 | $view = $this->createViewModel(); |
360 | |
361 | // Handle saved search requests: |
362 | $savedId = $this->params()->fromQuery('saved', false); |
363 | if ($savedId !== false) { |
364 | return $this->redirectToSavedSearch($savedId); |
365 | } |
366 | |
367 | $runner = $this->serviceLocator->get(\VuFind\Search\SearchRunner::class); |
368 | |
369 | // Send both GET and POST variables to search class: |
370 | $request = $this->getRequest()->getQuery()->toArray() |
371 | + $this->getRequest()->getPost()->toArray(); |
372 | $view->request = $request; |
373 | |
374 | $lastView = $this->getSearchMemory() |
375 | ->retrieveLastSetting($this->searchClassId, 'view'); |
376 | try { |
377 | $view->results = $results = $runner->run( |
378 | $request, |
379 | $this->searchClassId, |
380 | $setupCallback ?: $this->getSearchSetupCallback(), |
381 | $lastView |
382 | ); |
383 | } catch (\VuFindSearch\Backend\Exception\DeepPagingException $e) { |
384 | return $this->redirectToLegalSearchPage($request, $e->getLegalPage()); |
385 | } |
386 | $view->params = $params = $results->getParams(); |
387 | |
388 | // For page parameter being out of results list, we want to redirect to correct page |
389 | $page = $params->getPage(); |
390 | $totalResults = $results->getResultTotal(); |
391 | $limit = $params->getLimit(); |
392 | $lastPage = $limit ? ceil($totalResults / $limit) : 1; |
393 | if ($totalResults > 0 && $page > $lastPage) { |
394 | $queryParams = $request; |
395 | $queryParams['page'] = $lastPage; |
396 | return $this->redirect()->toRoute('search-results', [], [ 'query' => $queryParams ]); |
397 | } |
398 | |
399 | // If we received an EmptySet back, that indicates that the real search |
400 | // failed due to some kind of syntax error, and we should display a |
401 | // warning to the user; otherwise, we should proceed with normal post-search |
402 | // processing. |
403 | if ($results instanceof \VuFind\Search\EmptySet\Results) { |
404 | $view->parseError = true; |
405 | } else { |
406 | // If a "jumpto" parameter is set, deal with that now: |
407 | if ($jump = $this->processJumpTo($results)) { |
408 | return $jump; |
409 | } |
410 | |
411 | // Remember the current URL as the last search. |
412 | $this->rememberSearch($results); |
413 | |
414 | // Add to search history: |
415 | if ($this->saveToHistory) { |
416 | $this->saveSearchToHistory($results); |
417 | } |
418 | |
419 | // Jump to only result, if configured: |
420 | if ($jump = $this->processJumpToOnlyResult($results)) { |
421 | return $jump; |
422 | } |
423 | |
424 | // Set up results scroller: |
425 | if ($this->resultScrollerActive()) { |
426 | $this->resultScroller()->init($results); |
427 | } |
428 | |
429 | foreach ($results->getErrors() as $error) { |
430 | $this->flashMessenger()->addErrorMessage($error); |
431 | } |
432 | } |
433 | |
434 | // Special case: If we're in RSS view, we need to render differently: |
435 | if (isset($view->params) && $view->params->getView() == 'rss') { |
436 | return $this->getRssSearchResponse($view); |
437 | } |
438 | |
439 | // Schedule options for footer tools |
440 | $view->scheduleOptions = $this->serviceLocator |
441 | ->get(\VuFind\Search\History::class) |
442 | ->getScheduleOptions(); |
443 | $view->saveToHistory = $this->saveToHistory; |
444 | return $view; |
445 | } |
446 | |
447 | /** |
448 | * Process the jumpto parameter -- either redirect to a specific record and |
449 | * return view model, or ignore the parameter and return false. |
450 | * |
451 | * @param \VuFind\Search\Base\Results $results Search results object. |
452 | * |
453 | * @return bool|HttpResponse |
454 | */ |
455 | protected function processJumpTo($results) |
456 | { |
457 | // Missing/invalid parameter? Ignore it: |
458 | $jumpto = $this->params()->fromQuery('jumpto'); |
459 | if (empty($jumpto) || !is_numeric($jumpto)) { |
460 | return false; |
461 | } |
462 | |
463 | $recordList = $results->getResults(); |
464 | return isset($recordList[$jumpto - 1]) |
465 | ? $this->getRedirectForRecord($recordList[$jumpto - 1]) : false; |
466 | } |
467 | |
468 | /** |
469 | * Process jump to record if there is only one result. |
470 | * |
471 | * @param \VuFind\Search\Base\Results $results Search results object. |
472 | * |
473 | * @return bool|HttpResponse |
474 | */ |
475 | protected function processJumpToOnlyResult($results) |
476 | { |
477 | // If jumpto is explicitly disabled (set to false, e.g. by combined search), |
478 | // we should NEVER jump to a result regardless of other factors. |
479 | $jumpto = $this->params()->fromQuery('jumpto', true); |
480 | if ( |
481 | $jumpto |
482 | && ($this->getConfig()->Record->jump_to_single_search_result ?? false) |
483 | && $results->getResultTotal() == 1 |
484 | && $recordList = $results->getResults() |
485 | ) { |
486 | return $this->getRedirectForRecord( |
487 | reset($recordList), |
488 | ['sid' => $results->getSearchId()] |
489 | ); |
490 | } |
491 | |
492 | return false; |
493 | } |
494 | |
495 | /** |
496 | * Get a redirection response to a single record |
497 | * |
498 | * @param \VuFind\RecordDriver\AbstractBase $record Record driver |
499 | * @param array $queryParams Any query parameters |
500 | * |
501 | * @return HttpResponse |
502 | */ |
503 | protected function getRedirectForRecord( |
504 | \VuFind\RecordDriver\AbstractBase $record, |
505 | array $queryParams = [] |
506 | ): HttpResponse { |
507 | $details = $this->getRecordRouter()->getTabRouteDetails($record); |
508 | return $this->redirect()->toRoute( |
509 | $details['route'], |
510 | $details['params'], |
511 | ['query' => $queryParams] |
512 | ); |
513 | } |
514 | |
515 | /** |
516 | * Get a saved search, enforcing user ownership. Returns row if found, null |
517 | * otherwise. |
518 | * |
519 | * @param int $searchId Primary key value |
520 | * |
521 | * @return ?SearchEntityInterface |
522 | */ |
523 | protected function retrieveSearchSecurely($searchId) |
524 | { |
525 | $sessId = $this->serviceLocator->get(SessionManager::class)->getId(); |
526 | return $this->getDbService(SearchServiceInterface::class) |
527 | ->getSearchByIdAndOwner($searchId, $sessId, $this->getUser()); |
528 | } |
529 | |
530 | /** |
531 | * Save a search to the history in the database. |
532 | * |
533 | * @param \VuFind\Search\Base\Results $results Search results |
534 | * |
535 | * @return void |
536 | */ |
537 | protected function saveSearchToHistory($results) |
538 | { |
539 | $sessId = $this->serviceLocator->get(SessionManager::class)->getId(); |
540 | $this->serviceLocator->get(\VuFind\Search\SearchNormalizer::class)->saveNormalizedSearch( |
541 | $results, |
542 | $sessId, |
543 | $this->getUser()?->getId() |
544 | ); |
545 | } |
546 | |
547 | /** |
548 | * Either assign the requested search object to the view or display a flash |
549 | * message indicating why the operation failed. |
550 | * |
551 | * @param string $searchId ID value of a saved advanced search. |
552 | * |
553 | * @return bool|object Restored search object if found, false otherwise. |
554 | */ |
555 | protected function restoreAdvancedSearch($searchId) |
556 | { |
557 | // Look up search in database and fail if it is not found: |
558 | $search = $this->retrieveSearchSecurely($searchId); |
559 | if (empty($search)) { |
560 | $this->flashMessenger()->addMessage('advSearchError_notFound', 'error'); |
561 | return false; |
562 | } |
563 | |
564 | // Restore the full search object: |
565 | $savedSearch = $search->getSearchObject()?->deminify($this->getResultsManager()); |
566 | if (!$savedSearch) { |
567 | throw new Exception("Problem getting search object from search {$search->getId()}."); |
568 | } |
569 | |
570 | // Fail if this is not the right type of search: |
571 | if ($savedSearch->getParams()->getSearchType() != 'advanced') { |
572 | try { |
573 | $savedSearch->getParams()->convertToAdvancedSearch(); |
574 | } catch (\Exception $ex) { |
575 | $this->flashMessenger() |
576 | ->addMessage('advSearchError_notAdvanced', 'error'); |
577 | return false; |
578 | } |
579 | } |
580 | |
581 | // Make the object available to the view: |
582 | return $savedSearch; |
583 | } |
584 | |
585 | /** |
586 | * Convenience method for accessing results |
587 | * |
588 | * @return \VuFind\Search\Results\PluginManager |
589 | */ |
590 | protected function getResultsManager() |
591 | { |
592 | return $this->serviceLocator |
593 | ->get(\VuFind\Search\Results\PluginManager::class); |
594 | } |
595 | |
596 | /** |
597 | * Get the current settings for the specified range facet, if it is set: |
598 | * |
599 | * @param array $fields Fields to check |
600 | * @param string $type Type of range to include in return value |
601 | * @param object $savedSearch Saved search object (false if none) |
602 | * |
603 | * @return array |
604 | */ |
605 | protected function getRangeSettings($fields, $type, $savedSearch = false) |
606 | { |
607 | $parts = []; |
608 | |
609 | foreach ($fields as $field) { |
610 | // Default to blank strings: |
611 | $from = $to = ''; |
612 | |
613 | // Check to see if there is an existing range in the search object: |
614 | if ($savedSearch) { |
615 | $filters = $savedSearch->getParams()->getRawFilters(); |
616 | foreach ($filters[$field] ?? [] as $current) { |
617 | if ($range = SolrUtils::parseRange($current)) { |
618 | $from = $range['from'] == '*' ? '' : $range['from']; |
619 | $to = $range['to'] == '*' ? '' : $range['to']; |
620 | $savedSearch->getParams() |
621 | ->removeFilter($field . ':' . $current); |
622 | break; |
623 | } |
624 | } |
625 | } |
626 | |
627 | // Send back the settings: |
628 | $parts[] = [ |
629 | 'field' => $field, |
630 | 'type' => $type, |
631 | 'values' => [$from, $to], |
632 | ]; |
633 | } |
634 | |
635 | return $parts; |
636 | } |
637 | |
638 | /** |
639 | * Get the range facet configurations from the specified config section and |
640 | * filter them appropriately. |
641 | * |
642 | * @param string $config Name of config file |
643 | * @param string $section Configuration section to check |
644 | * @param array $filter List of fields to include (if empty, all |
645 | * fields will be returned) |
646 | * |
647 | * @return array |
648 | */ |
649 | protected function getRangeFieldList($config, $section, $filter) |
650 | { |
651 | $config = $this->serviceLocator->get(\VuFind\Config\PluginManager::class) |
652 | ->get($config); |
653 | $fields = isset($config->SpecialFacets->$section) |
654 | ? $config->SpecialFacets->$section->toArray() : []; |
655 | |
656 | if (!empty($filter)) { |
657 | $fields = array_intersect($fields, $filter); |
658 | } |
659 | |
660 | return $fields; |
661 | } |
662 | |
663 | /** |
664 | * Get the current settings for the date range facets, if set: |
665 | * |
666 | * @param object $savedSearch Saved search object (false if none) |
667 | * @param string $config Name of config file |
668 | * @param array $filter List of fields to include (if empty, all |
669 | * fields will be returned) |
670 | * |
671 | * @return array |
672 | */ |
673 | protected function getDateRangeSettings( |
674 | $savedSearch = false, |
675 | $config = 'facets', |
676 | $filter = [] |
677 | ) { |
678 | $fields = $this->getRangeFieldList($config, 'dateRange', $filter); |
679 | return $this->getRangeSettings($fields, 'date', $savedSearch); |
680 | } |
681 | |
682 | /** |
683 | * Get the current settings for the full date range facets, if set: |
684 | * |
685 | * @param object $savedSearch Saved search object (false if none) |
686 | * @param string $config Name of config file |
687 | * @param array $filter List of fields to include (if empty, all |
688 | * fields will be returned) |
689 | * |
690 | * @return array |
691 | */ |
692 | protected function getFullDateRangeSettings( |
693 | $savedSearch = false, |
694 | $config = 'facets', |
695 | $filter = [] |
696 | ) { |
697 | $fields = $this->getRangeFieldList($config, 'fullDateRange', $filter); |
698 | return $this->getRangeSettings($fields, 'fulldate', $savedSearch); |
699 | } |
700 | |
701 | /** |
702 | * Get the current settings for the generic range facets, if set: |
703 | * |
704 | * @param object $savedSearch Saved search object (false if none) |
705 | * @param string $config Name of config file |
706 | * @param array $filter List of fields to include (if empty, all |
707 | * fields will be returned) |
708 | * |
709 | * @return array |
710 | */ |
711 | protected function getGenericRangeSettings( |
712 | $savedSearch = false, |
713 | $config = 'facets', |
714 | $filter = [] |
715 | ) { |
716 | $fields = $this->getRangeFieldList($config, 'genericRange', $filter); |
717 | return $this->getRangeSettings($fields, 'generic', $savedSearch); |
718 | } |
719 | |
720 | /** |
721 | * Get the current settings for the numeric range facets, if set: |
722 | * |
723 | * @param object $savedSearch Saved search object (false if none) |
724 | * @param string $config Name of config file |
725 | * @param array $filter List of fields to include (if empty, all |
726 | * fields will be returned) |
727 | * |
728 | * @return array |
729 | */ |
730 | protected function getNumericRangeSettings( |
731 | $savedSearch = false, |
732 | $config = 'facets', |
733 | $filter = [] |
734 | ) { |
735 | $fields = $this->getRangeFieldList($config, 'numericRange', $filter); |
736 | return $this->getRangeSettings($fields, 'numeric', $savedSearch); |
737 | } |
738 | |
739 | /** |
740 | * Get all active range facets: |
741 | * |
742 | * @param array $specialFacets Special facet setting (in parsed format) |
743 | * @param object $savedSearch Saved search object (false if none) |
744 | * @param string $config Name of config file |
745 | * |
746 | * @return array |
747 | */ |
748 | protected function getAllRangeSettings( |
749 | $specialFacets, |
750 | $savedSearch = false, |
751 | $config = 'facets' |
752 | ) { |
753 | $result = []; |
754 | if (isset($specialFacets['daterange'])) { |
755 | $dates = $this->getDateRangeSettings( |
756 | $savedSearch, |
757 | $config, |
758 | $specialFacets['daterange'] |
759 | ); |
760 | $result = array_merge($result, $dates); |
761 | } |
762 | if (isset($specialFacets['fulldaterange'])) { |
763 | $fulldates = $this->getFullDateRangeSettings( |
764 | $savedSearch, |
765 | $config, |
766 | $specialFacets['fulldaterange'] |
767 | ); |
768 | $result = array_merge($result, $fulldates); |
769 | } |
770 | if (isset($specialFacets['genericrange'])) { |
771 | $generic = $this->getGenericRangeSettings( |
772 | $savedSearch, |
773 | $config, |
774 | $specialFacets['genericrange'] |
775 | ); |
776 | $result = array_merge($result, $generic); |
777 | } |
778 | if (isset($specialFacets['numericrange'])) { |
779 | $numeric = $this->getNumericRangeSettings( |
780 | $savedSearch, |
781 | $config, |
782 | $specialFacets['numericrange'] |
783 | ); |
784 | $result = array_merge($result, $numeric); |
785 | } |
786 | return $result; |
787 | } |
788 | |
789 | /** |
790 | * Parse the "special facets" setting. |
791 | * |
792 | * @param string $specialFacets Unparsed string |
793 | * |
794 | * @return array |
795 | */ |
796 | protected function parseSpecialFacetsSetting($specialFacets) |
797 | { |
798 | // Parse the special facets into a more useful format: |
799 | $parsed = []; |
800 | foreach (explode(',', $specialFacets) as $current) { |
801 | $parts = explode(':', $current); |
802 | $key = array_shift($parts); |
803 | $parsed[$key] = $parts; |
804 | } |
805 | return $parsed; |
806 | } |
807 | |
808 | /** |
809 | * Process the checkbox setting from special facets. |
810 | * |
811 | * @param array $params Parameters to the checkbox setting |
812 | * @param object $savedSearch Saved search object (false if none) |
813 | * |
814 | * @return array |
815 | */ |
816 | protected function processAdvancedCheckboxes($params, $savedSearch = false) |
817 | { |
818 | // Set defaults for missing parameters: |
819 | $config = $params[0] ?? 'facets'; |
820 | $section = $params[1] ?? 'CheckboxFacets'; |
821 | |
822 | // Load config file: |
823 | $config = $this->serviceLocator->get(\VuFind\Config\PluginManager::class) |
824 | ->get($config); |
825 | |
826 | // Process checkbox settings in config: |
827 | $flipCheckboxes = false; |
828 | if (str_starts_with($section, '~')) { // reverse flag |
829 | $section = substr($section, 1); |
830 | $flipCheckboxes = true; |
831 | } |
832 | $checkboxFacets = ($section && isset($config->$section)) |
833 | ? $config->$section->toArray() : []; |
834 | if ($flipCheckboxes) { |
835 | $checkboxFacets = array_flip($checkboxFacets); |
836 | } |
837 | |
838 | // Reformat for convenience: |
839 | $formatted = []; |
840 | foreach ($checkboxFacets as $filter => $desc) { |
841 | $current = compact('desc', 'filter'); |
842 | $current['selected'] |
843 | = $savedSearch && $savedSearch->getParams()->hasFilter($filter); |
844 | // We don't want to double-display checkboxes on advanced search, so |
845 | // if they are checked, we should remove them from the object to |
846 | // prevent display in the "other filters" area. |
847 | if ($current['selected']) { |
848 | $savedSearch->getParams()->removeFilter($filter); |
849 | } |
850 | $formatted[] = $current; |
851 | } |
852 | |
853 | return $formatted; |
854 | } |
855 | |
856 | /** |
857 | * Returns a list of all items associated with one facet for the lightbox |
858 | * |
859 | * Parameters: |
860 | * facet The facet to retrieve |
861 | * searchParams Facet search params from $results->getUrlQuery()->getParams() |
862 | * |
863 | * @return mixed |
864 | */ |
865 | public function facetListAction() |
866 | { |
867 | $this->disableSessionWrites(); // avoid session write timing bug |
868 | // Get results |
869 | $results = $this->getResultsManager()->get($this->searchClassId); |
870 | $params = $results->getParams(); |
871 | $params->initFromRequest($this->getRequest()->getQuery()); |
872 | // Get parameters |
873 | $facet = $this->params()->fromQuery('facet'); |
874 | $contains = $this->params()->fromQuery('contains', ''); |
875 | $page = (int)$this->params()->fromQuery('facetpage', 1); |
876 | // Has the request been sent in an AJAX context? |
877 | $ajax = (int)$this->params()->fromQuery('ajax', 0); |
878 | $urlBase = $this->params()->fromQuery('urlBase', ''); |
879 | $searchAction = $this->params()->fromQuery('searchAction', ''); |
880 | // $urlBase and $searchAction should be relative URLs; if there is an |
881 | // absolute URL passed in, this may be a sign of malicious activity and |
882 | // we should fail. |
883 | if (str_contains($urlBase . $searchAction, '://')) { |
884 | throw new \Exception('Unexpected absolute URL found.'); |
885 | } |
886 | $options = $results->getOptions(); |
887 | $facetSortOptions = $options->getFacetSortOptions($facet); |
888 | $sort = $this->params()->fromQuery('facetsort', null); |
889 | if ($sort === null || !in_array($sort, array_keys($facetSortOptions))) { |
890 | $sort = empty($facetSortOptions) |
891 | ? 'count' |
892 | : current(array_keys($facetSortOptions)); |
893 | } |
894 | $config = $this->serviceLocator->get(\VuFind\Config\PluginManager::class) |
895 | ->get($options->getFacetsIni()); |
896 | $limit = $config->Results_Settings->lightboxLimit ?? 50; |
897 | $limit = $this->params()->fromQuery('facetlimit', $limit); |
898 | if (!empty($contains)) { |
899 | $params->setFacetContains($contains); |
900 | $params->setFacetContainsIgnoreCase(true); |
901 | } |
902 | $facets = $results->getPartialFieldFacets( |
903 | [$facet], |
904 | false, |
905 | $limit, |
906 | $sort, |
907 | $page, |
908 | $this->params()->fromQuery('facetop', 'AND') == 'OR' |
909 | ); |
910 | $list = $facets[$facet]['data']['list'] ?? []; |
911 | $facetLabel = $params->getFacetLabel($facet); |
912 | |
913 | $viewParams = [ |
914 | 'contains' => $contains, |
915 | 'data' => $list, |
916 | 'exclude' => intval($this->params()->fromQuery('facetexclude', 0)), |
917 | 'facet' => $facet, |
918 | 'facetLabel' => $facetLabel, |
919 | 'operator' => $this->params()->fromQuery('facetop', 'AND'), |
920 | 'page' => $page, |
921 | 'results' => $results, |
922 | 'anotherPage' => $facets[$facet]['more'] ?? '', |
923 | 'sort' => $sort, |
924 | 'sortOptions' => $facetSortOptions, |
925 | 'baseUriExtra' => $this->params()->fromQuery('baseUriExtra'), |
926 | 'active' => $sort, |
927 | 'key' => $sort, |
928 | 'urlBase' => $urlBase, |
929 | 'searchAction' => $searchAction, |
930 | ]; |
931 | $viewParams['delegateParams'] = $viewParams; |
932 | $view = $this->createViewModel($viewParams); |
933 | $view->setTemplate($ajax ? 'search/facet-list-content' : 'search/facet-list'); |
934 | return $view; |
935 | } |
936 | |
937 | /** |
938 | * Get proper options file for search class |
939 | * |
940 | * @return \VuFind\Search\Base\Options |
941 | */ |
942 | public function getOptionsForClass(): \VuFind\Search\Base\Options |
943 | { |
944 | return $this->serviceLocator |
945 | ->get(\VuFind\Search\Options\PluginManager::class) |
946 | ->get($this->searchClassId); |
947 | } |
948 | } |