Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 227 |
|
0.00% |
0 / 14 |
CRAP | |
0.00% |
0 / 1 |
SearchController | |
0.00% |
0 / 227 |
|
0.00% |
0 / 14 |
3080 | |
0.00% |
0 / 1 |
collectionfacetlistAction | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
editmemoryAction | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
72 | |||
emailAction | |
0.00% |
0 / 34 |
|
0.00% |
0 / 1 |
110 | |||
historyAction | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
30 | |||
newitemAction | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
12 | |||
newitemresultsAction | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
42 | |||
reservesAction | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
30 | |||
reservesfacetlistAction | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
reservessearchAction | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
2 | |||
reservesresultsAction | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
56 | |||
resultsAction | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
opensearchAction | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
12 | |||
suggestAction | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
2 | |||
resultScrollerActive | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | /** |
4 | * Default 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 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
27 | * @link https://vufind.org Main Site |
28 | */ |
29 | |
30 | namespace VuFind\Controller; |
31 | |
32 | use VuFind\Exception\Mail as MailException; |
33 | use VuFind\Search\Factory\UrlQueryHelperFactory; |
34 | |
35 | use function array_slice; |
36 | use function count; |
37 | |
38 | /** |
39 | * Redirects the user to the appropriate default VuFind action. |
40 | * |
41 | * @category VuFind |
42 | * @package Controller |
43 | * @author Demian Katz <demian.katz@villanova.edu> |
44 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
45 | * @link https://vufind.org Main Site |
46 | */ |
47 | class SearchController extends AbstractSolrSearch |
48 | { |
49 | /** |
50 | * Show facet list for Solr-driven collections. |
51 | * |
52 | * @return mixed |
53 | */ |
54 | public function collectionfacetlistAction() |
55 | { |
56 | $this->searchClassId = 'SolrCollection'; |
57 | return $this->facetListAction(); |
58 | } |
59 | |
60 | /** |
61 | * Edit search memory action. |
62 | * |
63 | * @return mixed |
64 | */ |
65 | public function editmemoryAction() |
66 | { |
67 | // Get the user's referer, with the home page as a fallback; we'll |
68 | // redirect here after the work is done. |
69 | $from = $this->getRequest()->getServer()->get('HTTP_REFERER') ?? null; |
70 | if (empty($from) || !$this->isLocalUrl($from)) { |
71 | $from = $this->url()->fromRoute('home'); |
72 | } |
73 | |
74 | // Get parameters: |
75 | $searchClassId = $this->params() |
76 | ->fromQuery('searchClassId', DEFAULT_SEARCH_BACKEND); |
77 | $removeAllFilters = $this->params()->fromQuery('removeAllFilters'); |
78 | $removeFacet = $this->params()->fromQuery('removeFacet'); |
79 | $removeFilter = $this->params()->fromQuery('removeFilter'); |
80 | |
81 | // Retrieve and manipulate the parameters: |
82 | $searchHelper = $this->getViewRenderer()->plugin('searchMemory'); |
83 | $params = $searchHelper->getLastSearchParams($searchClassId); |
84 | $factory = $this->serviceLocator->get(UrlQueryHelperFactory::class); |
85 | $initialParams = $factory->fromParams($params); |
86 | |
87 | if ($removeAllFilters) { |
88 | $defaultFilters = $params->getOptions()->getDefaultFilters(); |
89 | $query = $initialParams->removeAllFilters(); |
90 | foreach ($defaultFilters as $filter) { |
91 | $query = $query->addFilter($filter); |
92 | } |
93 | } elseif ($removeFacet) { |
94 | $query = $initialParams->removeFacet( |
95 | $removeFacet['field'] ?? '', |
96 | $removeFacet['value'] ?? '', |
97 | $removeFacet['operator'] ?? 'AND' |
98 | ); |
99 | } elseif ($removeFilter) { |
100 | $query = $initialParams->removeFilter($removeFilter); |
101 | } else { |
102 | $query = null; |
103 | } |
104 | |
105 | // Remember the altered parameters: |
106 | if ($query) { |
107 | $base = $this->url() |
108 | ->fromRoute($params->getOptions()->getSearchAction()); |
109 | $this->getSearchMemory() |
110 | ->rememberSearch($base . $query->getParams(false)); |
111 | } |
112 | |
113 | // Send the user back where they came from: |
114 | return $this->redirect()->toUrl($from); |
115 | } |
116 | |
117 | /** |
118 | * Email action - Allows the email form to appear. |
119 | * |
120 | * @return mixed |
121 | */ |
122 | public function emailAction() |
123 | { |
124 | // If a URL was explicitly passed in, use that; otherwise, try to |
125 | // find the HTTP referrer. |
126 | $mailer = $this->serviceLocator->get(\VuFind\Mailer\Mailer::class); |
127 | $view = $this->createEmailViewModel(null, $mailer->getDefaultLinkSubject()); |
128 | $mailer->setMaxRecipients($view->maxRecipients); |
129 | // Set up Captcha |
130 | $view->useCaptcha = $this->captcha()->active('email'); |
131 | $view->url = $this->params()->fromPost('url') |
132 | ?? $this->params()->fromQuery('url') |
133 | ?? $this->getRequest()->getServer()->get('HTTP_REFERER'); |
134 | if (!$this->isLocalUrl($view->url)) { |
135 | throw new \Exception('Unexpected value passed to emailAction: ' . $view->url); |
136 | } |
137 | |
138 | // Force login if necessary: |
139 | $config = $this->getConfig(); |
140 | if (($config->Mail->require_login ?? true) && !$this->getUser()) { |
141 | return $this->forceLogin(null, ['emailurl' => $view->url]); |
142 | } |
143 | |
144 | // Check if we have a URL in login followup data -- this should override |
145 | // any existing referer to avoid emailing a login-related URL! |
146 | $followupUrl = $this->followup()->retrieveAndClear('emailurl'); |
147 | if (!empty($followupUrl)) { |
148 | $view->url = $followupUrl; |
149 | } |
150 | |
151 | // Fail if we can't figure out a URL to share: |
152 | if (empty($view->url)) { |
153 | throw new \Exception('Cannot determine URL to share.'); |
154 | } |
155 | |
156 | // Process form submission: |
157 | if ($this->formWasSubmitted(useCaptcha: $view->useCaptcha)) { |
158 | // Attempt to send the email and show an appropriate flash message: |
159 | try { |
160 | // If we got this far, we're ready to send the email: |
161 | $cc = $this->params()->fromPost('ccself') && $view->from != $view->to |
162 | ? $view->from : null; |
163 | $mailer->sendLink( |
164 | $view->to, |
165 | $view->from, |
166 | $view->message, |
167 | $view->url, |
168 | $this->getViewRenderer(), |
169 | $view->subject, |
170 | $cc |
171 | ); |
172 | $this->flashMessenger()->addMessage('email_success', 'success'); |
173 | return $this->redirect()->toUrl($view->url); |
174 | } catch (MailException $e) { |
175 | $this->flashMessenger()->addMessage($e->getDisplayMessage(), 'error'); |
176 | } |
177 | } |
178 | return $view; |
179 | } |
180 | |
181 | /** |
182 | * Handle search history display && purge |
183 | * |
184 | * @return mixed |
185 | */ |
186 | public function historyAction() |
187 | { |
188 | // Force login if necessary |
189 | $user = $this->getUser(); |
190 | if ($this->params()->fromQuery('require_login', 'no') !== 'no') { |
191 | // If user is already logged in, drop the require_login parameter to |
192 | // allow for a cleaner log-out experience. |
193 | return $user |
194 | ? $this->redirect()->toRoute('search-history') |
195 | : $this->forceLogin(); |
196 | } |
197 | $userId = $user?->getId(); |
198 | |
199 | $searchHistoryHelper = $this->serviceLocator |
200 | ->get(\VuFind\Search\History::class); |
201 | |
202 | if ($this->params()->fromQuery('purge')) { |
203 | $searchHistoryHelper->purgeSearchHistory($userId); |
204 | |
205 | // We don't want to remember the last search after a purge: |
206 | $this->getSearchMemory()->forgetSearch(); |
207 | } |
208 | $viewData = $searchHistoryHelper->getSearchHistory($userId); |
209 | // Eliminate schedule settings if scheduled searches are disabled; add |
210 | // user email data if scheduled searches are enabled. |
211 | $scheduleOptions = $this->serviceLocator |
212 | ->get(\VuFind\Search\History::class) |
213 | ->getScheduleOptions(); |
214 | if (empty($scheduleOptions)) { |
215 | unset($viewData['schedule']); |
216 | } else { |
217 | $viewData['scheduleOptions'] = $scheduleOptions; |
218 | $viewData['alertemail'] = $user?->getEmail(); |
219 | } |
220 | return $this->createViewModel($viewData); |
221 | } |
222 | |
223 | /** |
224 | * New item search form |
225 | * |
226 | * @return mixed |
227 | */ |
228 | public function newitemAction() |
229 | { |
230 | // Search parameters set? Process results. |
231 | if ($this->params()->fromQuery('range') !== null) { |
232 | return $this->forwardTo('Search', 'NewItemResults'); |
233 | } |
234 | |
235 | $view = $this->createViewModel( |
236 | [ |
237 | 'defaultSort' => $this->newItems()->getDefaultSort(), |
238 | 'fundList' => $this->newItems()->getFundList(), |
239 | 'ranges' => $this->newItems()->getRanges(), |
240 | ] |
241 | ); |
242 | if ($this->newItems()->includeFacets()) { |
243 | $view->options = $this->serviceLocator |
244 | ->get(\VuFind\Search\Options\PluginManager::class) |
245 | ->get($this->searchClassId); |
246 | $this->addFacetDetailsToView($view, 'NewItems'); |
247 | } |
248 | return $view; |
249 | } |
250 | |
251 | /** |
252 | * New item result list |
253 | * |
254 | * @return mixed |
255 | */ |
256 | public function newitemresultsAction() |
257 | { |
258 | // Retrieve new item list: |
259 | $range = $this->params()->fromQuery('range'); |
260 | $dept = $this->params()->fromQuery('department'); |
261 | |
262 | // Validate the range parameter -- it should not exceed the greatest |
263 | // configured value: |
264 | $maxAge = $this->newItems()->getMaxAge(); |
265 | if ($maxAge > 0 && $range > $maxAge) { |
266 | $range = $maxAge; |
267 | } |
268 | |
269 | // Are there "new item" filter queries specified in the config file? |
270 | // If so, load them now; we may add more values. These will be applied |
271 | // later after the whole list is collected. |
272 | $hiddenFilters = $this->newItems()->getHiddenFilters(); |
273 | |
274 | // Depending on whether we're in ILS or Solr mode, we need to do some |
275 | // different processing here to retrieve the correct items: |
276 | if ($this->newItems()->getMethod() == 'ils') { |
277 | // Use standard search action with override parameter to show results: |
278 | $bibIDs = $this->newItems()->getBibIDsFromCatalog( |
279 | $this->getILS(), |
280 | $this->getResultsManager()->get('Solr')->getParams(), |
281 | $range, |
282 | $dept, |
283 | $this->flashMessenger() |
284 | ); |
285 | $this->getRequest()->getQuery()->set('overrideIds', $bibIDs); |
286 | } else { |
287 | // Use a Solr filter to show results: |
288 | $hiddenFilters[] = $this->newItems()->getSolrFilter($range); |
289 | } |
290 | |
291 | // If we found hidden filters above, apply them now: |
292 | if (!empty($hiddenFilters)) { |
293 | $this->getRequest()->getQuery()->set('hiddenFilters', $hiddenFilters); |
294 | } |
295 | |
296 | // Don't save to history -- history page doesn't handle correctly: |
297 | $this->saveToHistory = false; |
298 | |
299 | // Call rather than forward, so we can use custom template |
300 | $view = $this->resultsAction(); |
301 | |
302 | // Customize the URL helper to make sure it builds proper new item URLs |
303 | // (check it's set first -- RSS feed will return a response model rather |
304 | // than a view model): |
305 | if (isset($view->results)) { |
306 | $view->results->getUrlQuery() |
307 | ->setDefaultParameter('range', $range) |
308 | ->setDefaultParameter('department', $dept) |
309 | ->setSuppressQuery(true); |
310 | } |
311 | |
312 | // We don't want new items hidden filters to propagate to other searches: |
313 | $view->ignoreHiddenFilterMemory = true; |
314 | $view->ignoreHiddenFiltersInRequest = true; |
315 | |
316 | return $view; |
317 | } |
318 | |
319 | /** |
320 | * Course reserves |
321 | * |
322 | * @return mixed |
323 | */ |
324 | public function reservesAction() |
325 | { |
326 | // Search parameters set? Process results. |
327 | if ( |
328 | $this->params()->fromQuery('inst') !== null |
329 | || $this->params()->fromQuery('course') !== null |
330 | || $this->params()->fromQuery('dept') !== null |
331 | ) { |
332 | return $this->forwardTo('Search', 'ReservesResults'); |
333 | } |
334 | |
335 | // No params? Show appropriate form (varies depending on whether we're |
336 | // using driver-based or Solr-based reserves searching). |
337 | if ($this->reserves()->useIndex()) { |
338 | return $this->forwardTo('Search', 'ReservesSearch'); |
339 | } |
340 | |
341 | // If we got this far, we're using driver-based searching and need to |
342 | // send options to the view: |
343 | $catalog = $this->getILS(); |
344 | return $this->createViewModel( |
345 | [ |
346 | 'deptList' => $catalog->getDepartments(), |
347 | 'instList' => $catalog->getInstructors(), |
348 | 'courseList' => $catalog->getCourses(), |
349 | ] |
350 | ); |
351 | } |
352 | |
353 | /** |
354 | * Show facet list for Solr-driven reserves. |
355 | * |
356 | * @return mixed |
357 | */ |
358 | public function reservesfacetlistAction() |
359 | { |
360 | $this->searchClassId = 'SolrReserves'; |
361 | return $this->facetListAction(); |
362 | } |
363 | |
364 | /** |
365 | * Show search form for Solr-driven reserves. |
366 | * |
367 | * @return mixed |
368 | */ |
369 | public function reservessearchAction() |
370 | { |
371 | $request = new \Laminas\Stdlib\Parameters( |
372 | $this->getRequest()->getQuery()->toArray() |
373 | + $this->getRequest()->getPost()->toArray() |
374 | ); |
375 | $view = $this->createViewModel(); |
376 | $runner = $this->serviceLocator->get(\VuFind\Search\SearchRunner::class); |
377 | $view->results = $runner->run( |
378 | $request, |
379 | 'SolrReserves', |
380 | $this->getSearchSetupCallback() |
381 | ); |
382 | $view->params = $view->results->getParams(); |
383 | return $view; |
384 | } |
385 | |
386 | /** |
387 | * Show results of reserves search. |
388 | * |
389 | * @return mixed |
390 | */ |
391 | public function reservesresultsAction() |
392 | { |
393 | // Retrieve course reserves item list: |
394 | $course = $this->params()->fromQuery('course'); |
395 | $inst = $this->params()->fromQuery('inst'); |
396 | $dept = $this->params()->fromQuery('dept'); |
397 | $result = $this->reserves()->findReserves($course, $inst, $dept); |
398 | |
399 | // Build a list of unique IDs |
400 | $callback = function ($i) { |
401 | return $i['BIB_ID']; |
402 | }; |
403 | $bibIDs = array_unique(array_map($callback, $result)); |
404 | |
405 | // Truncate the list if it is too long: |
406 | $limit = $this->getResultsManager()->get('Solr')->getParams() |
407 | ->getQueryIDLimit(); |
408 | if (count($bibIDs) > $limit) { |
409 | $bibIDs = array_slice($bibIDs, 0, $limit); |
410 | $this->flashMessenger()->addMessage('too_many_reserves', 'info'); |
411 | } |
412 | |
413 | // Use standard search action with override parameter to show results: |
414 | $this->getRequest()->getQuery()->set('overrideIds', $bibIDs); |
415 | |
416 | // Don't save to history -- history page doesn't handle correctly: |
417 | $this->saveToHistory = false; |
418 | |
419 | // Set up RSS feed title just in case: |
420 | $this->getViewRenderer()->plugin('resultfeed') |
421 | ->setOverrideTitle('Reserves Search Results'); |
422 | |
423 | // Call rather than forward, so we can use custom template |
424 | $view = $this->resultsAction(); |
425 | |
426 | // Pass some key values to the view, if found: |
427 | if (isset($result[0]['instructor']) && !empty($result[0]['instructor'])) { |
428 | $view->instructor = $result[0]['instructor']; |
429 | } |
430 | if (isset($result[0]['course']) && !empty($result[0]['course'])) { |
431 | $view->course = $result[0]['course']; |
432 | } |
433 | |
434 | // Customize the URL helper to make sure it builds proper reserves URLs |
435 | // (but only do this if we have access to a results object, which we |
436 | // won't in RSS mode): |
437 | if (isset($view->results)) { |
438 | $view->results->getUrlQuery() |
439 | ->setDefaultParameter('course', $course) |
440 | ->setDefaultParameter('inst', $inst) |
441 | ->setDefaultParameter('dept', $dept) |
442 | ->setSuppressQuery(true); |
443 | } |
444 | return $view; |
445 | } |
446 | |
447 | /** |
448 | * Results action. |
449 | * |
450 | * @return mixed |
451 | */ |
452 | public function resultsAction() |
453 | { |
454 | // Special case -- redirect tag searches. |
455 | $tag = $this->params()->fromQuery('tag'); |
456 | if (!empty($tag)) { |
457 | $query = $this->getRequest()->getQuery(); |
458 | $query->set('lookfor', $tag); |
459 | $query->set('type', 'tag'); |
460 | } |
461 | if ($this->params()->fromQuery('type') == 'tag') { |
462 | // Because we're coming in from a search, we want to do a fuzzy |
463 | // tag search, not an exact search like we would when linking to a |
464 | // specific tag name. |
465 | $query = $this->getRequest()->getQuery()->set('fuzzy', 'true'); |
466 | return $this->forwardTo('Tag', 'Home'); |
467 | } |
468 | |
469 | // Default case -- standard behavior. |
470 | return parent::resultsAction(); |
471 | } |
472 | |
473 | /** |
474 | * Handle OpenSearch. |
475 | * |
476 | * @return \Laminas\Http\Response |
477 | */ |
478 | public function opensearchAction() |
479 | { |
480 | switch ($this->params()->fromQuery('method')) { |
481 | case 'describe': |
482 | $config = $this->getConfig(); |
483 | $xml = $this->getViewRenderer()->render( |
484 | 'search/opensearch-describe.phtml', |
485 | ['site' => $config->Site] |
486 | ); |
487 | break; |
488 | default: |
489 | $xml = $this->getViewRenderer() |
490 | ->render('search/opensearch-error.phtml'); |
491 | break; |
492 | } |
493 | |
494 | $response = $this->getResponse(); |
495 | $headers = $response->getHeaders(); |
496 | $headers->addHeaderLine('Content-type', 'text/xml'); |
497 | $response->setContent($xml); |
498 | return $response; |
499 | } |
500 | |
501 | /** |
502 | * Provide OpenSearch suggestions as specified at |
503 | * http://www.opensearch.org/Specifications/OpenSearch/Extensions/Suggestions/1.0 |
504 | * |
505 | * @return \Laminas\Http\Response |
506 | */ |
507 | public function suggestAction() |
508 | { |
509 | // Always use 'AllFields' as our autosuggest type: |
510 | $query = $this->getRequest()->getQuery(); |
511 | $query->set('type', 'AllFields'); |
512 | |
513 | // Get suggestions and make sure they are an array (we don't want to JSON |
514 | // encode them into an object): |
515 | $suggester = $this->serviceLocator |
516 | ->get(\VuFind\Autocomplete\Suggester::class); |
517 | $suggestions = $suggester->getSuggestions($query, 'type', 'lookfor'); |
518 | |
519 | // Send the JSON response: |
520 | $response = $this->getResponse(); |
521 | $headers = $response->getHeaders(); |
522 | $headers->addHeaderLine('Content-type', 'application/json'); |
523 | $response->setContent( |
524 | json_encode([$query->get('lookfor', ''), $suggestions]) |
525 | ); |
526 | return $response; |
527 | } |
528 | |
529 | /** |
530 | * Is the result scroller active? |
531 | * |
532 | * @return bool |
533 | */ |
534 | protected function resultScrollerActive() |
535 | { |
536 | $config = $this->serviceLocator->get(\VuFind\Config\PluginManager::class) |
537 | ->get('config'); |
538 | return $config->Record->next_prev_navigation ?? false; |
539 | } |
540 | } |