Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 157 |
|
0.00% |
0 / 6 |
CRAP | |
0.00% |
0 / 1 |
CombinedController | |
0.00% |
0 / 157 |
|
0.00% |
0 / 6 |
1332 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
homeAction | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
resultAction | |
0.00% |
0 / 33 |
|
0.00% |
0 / 1 |
20 | |||
resultsAction | |
0.00% |
0 / 63 |
|
0.00% |
0 / 1 |
132 | |||
searchboxAction | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
72 | |||
adjustQueryForSettings | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
132 |
1 | <?php |
2 | |
3 | /** |
4 | * Combined Search Controller |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * Copyright (C) Villanova University 2010. |
9 | * Copyright (C) The National Library of Finland 2024. |
10 | * |
11 | * This program is free software; you can redistribute it and/or modify |
12 | * it under the terms of the GNU General Public License version 2, |
13 | * as published by the Free Software Foundation. |
14 | * |
15 | * This program is distributed in the hope that it will be useful, |
16 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
17 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
18 | * GNU General Public License for more details. |
19 | * |
20 | * You should have received a copy of the GNU General Public License |
21 | * along with this program; if not, write to the Free Software |
22 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
23 | * |
24 | * @category VuFind |
25 | * @package Controller |
26 | * @author Demian Katz <demian.katz@villanova.edu> |
27 | * @author Ere Maijala <ere.maijala@helsinki.fi> |
28 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
29 | * @link https://vufind.org Main Site |
30 | */ |
31 | |
32 | namespace VuFind\Controller; |
33 | |
34 | use Laminas\ServiceManager\ServiceLocatorInterface; |
35 | use VuFind\Search\SearchRunner; |
36 | |
37 | use function count; |
38 | use function in_array; |
39 | use function intval; |
40 | use function is_array; |
41 | |
42 | /** |
43 | * Redirects the user to the appropriate default VuFind action. |
44 | * |
45 | * @category VuFind |
46 | * @package Controller |
47 | * @author Demian Katz <demian.katz@villanova.edu> |
48 | * @author Ere Maijala <ere.maijala@helsinki.fi> |
49 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
50 | * @link https://vufind.org Main Site |
51 | */ |
52 | class CombinedController extends AbstractSearch |
53 | { |
54 | use AjaxResponseTrait; |
55 | |
56 | /** |
57 | * Constructor |
58 | * |
59 | * @param ServiceLocatorInterface $sm Service locator |
60 | */ |
61 | public function __construct(ServiceLocatorInterface $sm) |
62 | { |
63 | $this->searchClassId = 'Combined'; |
64 | parent::__construct($sm); |
65 | } |
66 | |
67 | /** |
68 | * Home action |
69 | * |
70 | * @return mixed |
71 | */ |
72 | public function homeAction() |
73 | { |
74 | // We need to load blocks differently in this controller since it |
75 | // doesn't follow the usual configuration pattern. |
76 | $blocks = $this->getService(\VuFind\ContentBlock\BlockLoader::class) |
77 | ->getFromConfig('combined'); |
78 | return $this->createViewModel(compact('blocks')); |
79 | } |
80 | |
81 | /** |
82 | * Single result action (used for AJAX) |
83 | * |
84 | * @return mixed |
85 | */ |
86 | public function resultAction() |
87 | { |
88 | $this->disableSessionWrites(); // avoid session write timing bug |
89 | |
90 | // Turn off search memory -- not relevant in this context: |
91 | $this->getSearchMemory()->disable(); |
92 | |
93 | // Validate configuration: |
94 | $sectionId = $this->params()->fromQuery('id'); |
95 | $optionsManager = $this->getService(\VuFind\Search\Options\PluginManager::class); |
96 | $combinedOptions = $optionsManager->get('combined'); |
97 | $tabConfig = $combinedOptions->getTabConfig(); |
98 | if (!isset($tabConfig[$sectionId])) { |
99 | throw new \Exception('Illegal ID'); |
100 | } |
101 | [$searchClassId] = explode(':', $sectionId); |
102 | |
103 | // Retrieve results: |
104 | $currentOptions = $optionsManager->get($searchClassId); |
105 | [$controller, $action] |
106 | = explode('-', $currentOptions->getSearchAction()); |
107 | $settings = $tabConfig[$sectionId]; |
108 | |
109 | $this->adjustQueryForSettings( |
110 | $settings, |
111 | $currentOptions->getHandlerForLabel($this->params()->fromQuery('type')) |
112 | ); |
113 | $settings['view'] = $this->forwardTo($controller, $action); |
114 | |
115 | // Should we suppress content due to emptiness? |
116 | if ( |
117 | ($settings['hide_if_empty'] ?? false) |
118 | && $settings['view']->results->getResultTotal() <= 0 |
119 | ) { |
120 | $html = ''; |
121 | } else { |
122 | $viewParams = [ |
123 | 'searchClassId' => $searchClassId, |
124 | 'currentSearch' => $settings, |
125 | 'domId' => 'combined_' . str_replace(':', '____', $sectionId), |
126 | ]; |
127 | // Initialize theme resources: |
128 | ($this->getViewRenderer()->plugin('setupThemeResources'))(true); |
129 | // Render content: |
130 | $html = $this->getViewRenderer()->render( |
131 | 'combined/results-list.phtml', |
132 | $viewParams |
133 | ); |
134 | // Prepend CSS in case of custom files added by templates: |
135 | $html = ($this->getViewRenderer()->plugin('headLink'))() . $html; |
136 | } |
137 | return $this->getAjaxResponse('text/html', $html); |
138 | } |
139 | |
140 | /** |
141 | * Results action |
142 | * |
143 | * @return mixed |
144 | */ |
145 | public function resultsAction() |
146 | { |
147 | // Set up current request context: |
148 | $request = $this->getRequest()->getQuery()->toArray() |
149 | + $this->getRequest()->getPost()->toArray(); |
150 | $results = $this->getService(SearchRunner::class)->run( |
151 | $request, |
152 | 'Combined', |
153 | $this->getSearchSetupCallback() |
154 | ); |
155 | |
156 | // Remember the current URL, then disable memory so multi-search results |
157 | // don't overwrite it: |
158 | $this->rememberSearch($results); |
159 | $this->getSearchMemory()->disable(); |
160 | |
161 | // Gather combined results: |
162 | $combinedResults = []; |
163 | $optionsManager = $this->getService(\VuFind\Search\Options\PluginManager::class); |
164 | $combinedOptions = $optionsManager->get('combined'); |
165 | // Save the initial type value, since it may get manipulated below: |
166 | $initialType = $this->params()->fromQuery('type'); |
167 | foreach ($combinedOptions->getTabConfig() as $current => $settings) { |
168 | [$searchClassId] = explode(':', $current); |
169 | $currentOptions = $optionsManager->get($searchClassId); |
170 | $this->adjustQueryForSettings( |
171 | $settings, |
172 | $currentOptions->getHandlerForLabel($initialType) |
173 | ); |
174 | [$controller, $action] |
175 | = explode('-', $currentOptions->getSearchAction()); |
176 | $combinedResults[$current] = $settings; |
177 | |
178 | // Calculate a unique DOM id for this section of the search results; |
179 | // $searchClassId may contain colons, which must be converted. |
180 | $combinedResults[$current]['domId'] |
181 | = 'combined_' . str_replace(':', '____', $current); |
182 | |
183 | $permissionDenied = isset($settings['permission']) |
184 | && !$this->permission()->isAuthorized($settings['permission']); |
185 | $isAjax = $settings['ajax'] ?? false; |
186 | $combinedResults[$current]['view'] = ($permissionDenied || $isAjax) |
187 | ? $this->createViewModel(['results' => $results]) |
188 | : $this->forwardTo($controller, $action); |
189 | |
190 | // Special case: include appropriate "powered by" message: |
191 | if (strtolower($searchClassId) == 'summon') { |
192 | $this->layout()->poweredBy = 'Powered by Summonâ„¢ from Serials ' |
193 | . 'Solutions, a division of ProQuest.'; |
194 | } |
195 | } |
196 | |
197 | // Restore the initial type value to the query to prevent weird behavior: |
198 | $this->getRequest()->getQuery()->type = $initialType; |
199 | |
200 | // Run the search to obtain recommendations: |
201 | $results->performAndProcessSearch(); |
202 | |
203 | $actualMaxColumns = count($combinedResults); |
204 | $config = $this->getService(\VuFind\Config\PluginManager::class)->get('combined')->toArray(); |
205 | $columnConfig = intval($config['Layout']['columns'] ?? $actualMaxColumns); |
206 | $columns = min($columnConfig, $actualMaxColumns); |
207 | $placement = $config['Layout']['stack_placement'] ?? 'distributed'; |
208 | if (!in_array($placement, ['distributed', 'left', 'right', 'grid'])) { |
209 | $placement = 'distributed'; |
210 | } |
211 | |
212 | // Identify if any modules use include_recommendations_side or |
213 | // include_recommendations_noresults_side. |
214 | $columnSideRecommendations = []; |
215 | $recommendationManager = $this->getService(\VuFind\Recommend\PluginManager::class); |
216 | foreach ($config as $subconfig) { |
217 | foreach (['include_recommendations_side', 'include_recommendations_noresults_side'] as $type) { |
218 | if (is_array($subconfig[$type] ?? false)) { |
219 | foreach ($subconfig[$type] as $recommendation) { |
220 | $recommendationModuleName = strtok($recommendation, ':'); |
221 | $recommendationModule = $recommendationManager->get($recommendationModuleName); |
222 | $columnSideRecommendations[] = str_replace('\\', '_', $recommendationModule::class); |
223 | } |
224 | } |
225 | } |
226 | } |
227 | |
228 | // Build view model: |
229 | return $this->createViewModel( |
230 | [ |
231 | 'columns' => $columns, |
232 | 'combinedResults' => $combinedResults, |
233 | 'config' => $config, |
234 | 'params' => $results->getParams(), |
235 | 'placement' => $placement, |
236 | 'results' => $results, |
237 | 'columnSideRecommendations' => $columnSideRecommendations, |
238 | ] |
239 | ); |
240 | } |
241 | |
242 | /** |
243 | * Action to process the combined search box. |
244 | * |
245 | * @return mixed |
246 | */ |
247 | public function searchboxAction() |
248 | { |
249 | [$type, $target] = explode(':', $this->params()->fromQuery('type'), 2); |
250 | switch ($type) { |
251 | case 'VuFind': |
252 | [$searchClassId, $type] = explode('|', $target); |
253 | $params = $this->getRequest()->getQuery()->toArray(); |
254 | $params['type'] = $type; |
255 | |
256 | // Disable retained filters if we are switching classes! |
257 | $activeClass = $this->params()->fromQuery('activeSearchClassId'); |
258 | if ($activeClass != $searchClassId) { |
259 | unset($params['filter']); |
260 | } |
261 | // We don't need to pass activeSearchClassId forward: |
262 | unset($params['activeSearchClassId']); |
263 | |
264 | $route = $this->getService(\VuFind\Search\Options\PluginManager::class) |
265 | ->get($searchClassId)->getSearchAction(); |
266 | $base = $this->url()->fromRoute($route); |
267 | return $this->redirect() |
268 | ->toUrl($base . '?' . http_build_query($params)); |
269 | case 'External': |
270 | $lookfor = $this->params()->fromQuery('lookfor'); |
271 | $finalTarget = (!str_contains($target, '%%lookfor%%')) |
272 | ? $target . urlencode($lookfor) |
273 | : str_replace('%%lookfor%%', urlencode($lookfor), $target); |
274 | return $this->redirect()->toUrl($finalTarget); |
275 | default: |
276 | // If parameters are completely missing, redirect to home instead |
277 | // of throwing an error; this is possibly a misbehaving crawler that |
278 | // followed the SearchBox URL without passing any parameters. |
279 | if (empty($type) && empty($target)) { |
280 | return $this->redirect()->toRoute('home'); |
281 | } |
282 | // If we have a weird value here, report it as an Exception: |
283 | throw new \VuFind\Exception\BadRequest( |
284 | 'Unexpected search type: "' . $type . '".' |
285 | ); |
286 | } |
287 | } |
288 | |
289 | /** |
290 | * Adjust the query context to reflect the current settings. |
291 | * |
292 | * @param array $settings Settings |
293 | * @param string $searchType Override for search handler name |
294 | * |
295 | * @return void |
296 | */ |
297 | protected function adjustQueryForSettings($settings, $searchType = null) |
298 | { |
299 | // Apply limit setting, if any: |
300 | $query = $this->getRequest()->getQuery(); |
301 | $query->limit = $settings['limit'] ?? null; |
302 | |
303 | // Apply filters, if any: |
304 | $query->filter = isset($settings['filter']) |
305 | ? (array)$settings['filter'] : null; |
306 | |
307 | // Apply hidden filters, if any: |
308 | $query->hiddenFilters = isset($settings['hiddenFilter']) |
309 | ? (array)$settings['hiddenFilter'] : null; |
310 | |
311 | // Apply shards, if any: |
312 | $query->shard = isset($settings['shard']) |
313 | ? (array)$settings['shard'] : null; |
314 | |
315 | // Reset override to avoid bleed-over from one section to the next! |
316 | $query->recommendOverride = false; |
317 | |
318 | // Always disable 'jumpto' setting, as it does not make sense to |
319 | // load a record view in the context of combined search. |
320 | $query->jumpto = false; |
321 | |
322 | // Override the search type: |
323 | $query->type = $searchType; |
324 | |
325 | // Display or hide top based on include_recommendations setting. |
326 | $recommendOverride = []; |
327 | $noRecommend = []; |
328 | $includeRecommendSetting = $settings['include_recommendations'] ?? false; |
329 | if (is_array($includeRecommendSetting)) { |
330 | $recommendOverride['top'] = $settings['include_recommendations']; |
331 | } elseif (!$includeRecommendSetting) { |
332 | $noRecommend[] = 'top'; |
333 | } |
334 | |
335 | // Display or hide side based on include_recommendations_side setting. |
336 | if (is_array($settings['include_recommendations_side'] ?? false)) { |
337 | $recommendOverride['side'] = $settings['include_recommendations_side']; |
338 | } else { |
339 | $noRecommend[] = 'side'; |
340 | } |
341 | |
342 | // Display or hide no results recommendations, based on |
343 | // include_recommendations_noresults setting (to display them in the bento box) or |
344 | // include_recommendations_noresults_side setting (to display them in the sidebar). |
345 | $includeRecommendNoResultsSetting = $settings['include_recommendations_noresults'] ?? false; |
346 | if (is_array($includeRecommendNoResultsSetting)) { |
347 | $recommendOverride['noresults'] = $settings['include_recommendations_noresults']; |
348 | } elseif (!$includeRecommendNoResultsSetting) { |
349 | $noRecommend[] = 'noresults'; |
350 | } |
351 | |
352 | if (is_array($settings['include_recommendations_noresults_side'] ?? false)) { |
353 | $recommendOverride['noresults_side'] = $settings['include_recommendations_noresults_side']; |
354 | } else { |
355 | $noRecommend[] = 'noresults_side'; |
356 | } |
357 | |
358 | $query->recommendOverride = $recommendOverride; |
359 | $query->noRecommend = count($noRecommend) ? implode(',', $noRecommend) : false; |
360 | } |
361 | } |