Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 87
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
GetSearchResults
0.00% covered (danger)
0.00%
0 / 87
0.00% covered (danger)
0.00%
0 / 11
702
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 handleRequest
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getSearchResults
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 getElements
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 renderResults
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
72
 renderPagination
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 renderPaginationSimple
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 renderPaginationTop
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 renderSearchStats
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 renderAnalytics
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 saveSearchToHistory
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * "Get Search Results" AJAX handler
5 *
6 * PHP version 8
7 *
8 * Copyright (C) The National Library of Finland 2023.
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  AJAX
25 * @author   Ere Maijala <ere.maijala@helsinki.fi>
26 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
27 * @link     https://vufind.org/wiki/development Wiki
28 */
29
30namespace VuFind\AjaxHandler;
31
32use Laminas\Mvc\Controller\Plugin\Params as ParamsHelper;
33use Laminas\Stdlib\Parameters;
34use Laminas\View\Model\ViewModel;
35use Laminas\View\Renderer\PhpRenderer;
36use VuFind\Db\Entity\UserEntityInterface;
37use VuFind\Db\Table\Search;
38use VuFind\Record\Loader as RecordLoader;
39use VuFind\Search\Base\Results;
40use VuFind\Search\Memory;
41use VuFind\Search\Results\PluginManager as ResultsManager;
42use VuFind\Search\SearchNormalizer;
43use VuFind\Session\Settings as SessionSettings;
44
45use function call_user_func;
46
47/**
48 * "Get Search Results" AJAX handler
49 *
50 * @category VuFind
51 * @package  AJAX
52 * @author   Ere Maijala <ere.maijala@helsinki.fi>
53 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
54 * @link     https://vufind.org/wiki/development Wiki
55 */
56class GetSearchResults extends \VuFind\AjaxHandler\AbstractBase implements
57    \Laminas\Log\LoggerAwareInterface,
58    \VuFind\I18n\Translator\TranslatorAwareInterface
59{
60    use \VuFind\I18n\Translator\TranslatorAwareTrait;
61    use \VuFind\Log\LoggerAwareTrait;
62
63    /**
64     * Elements to render for each search results page.
65     *
66     * Note that results list is last before scripts so that we update most controls
67     * before hiding the loading indicator (in practice this only affects tests).
68     *
69     * Key is a selector that finds all elements to update.
70     * Value is an associative array with the following keys:
71     *
72     *   method  Method to create the response content
73     *   target  Target attribute in the element for the content
74     *           (inner for innerHTML, outer for outerHTML or null for none)
75     *   attrs   New attributes for the element
76     *
77     * @var array
78     */
79    protected $elements = [
80        '.js-pagination.js-pagination__top' => [
81            'method' => 'renderPaginationTop',
82            'target' => 'outer',
83        ],
84        '.js-pagination:not(.js-pagination__top)' => [
85            'method' => 'renderPagination',
86            'target' => 'outer',
87        ],
88        '.js-pagination-simple' => [
89            'method' => 'renderPaginationSimple',
90            'target' => 'outer',
91        ],
92        '.js-search-stats' => [
93            'method' => 'renderSearchStats',
94            'target' => 'inner',
95            'attrs' => [
96                'aria-live' => 'polite',
97            ],
98        ],
99        '.js-result-list' => [
100            'method' => 'renderResults',
101            'target' => 'outer',
102        ],
103        'head' => [
104            'method' => 'renderAnalytics',
105            'target' => null,
106        ],
107    ];
108
109    /**
110     * Constructor
111     *
112     * @param SessionSettings      $sessionSettings  Session settings
113     * @param ResultsManager       $resultsManager   Results Manager
114     * @param PhpRenderer          $renderer         View renderer
115     * @param RecordLoader         $recordLoader     Record loader
116     * @param ?UserEntityInterface $user             Logged-in user
117     * @param string               $sessionId        Session ID
118     * @param SearchNormalizer     $searchNormalizer Search normalizer
119     * @param array                $config           Main configuration
120     * @param Memory               $searchMemory     Search memory
121     */
122    public function __construct(
123        SessionSettings $sessionSettings,
124        protected ResultsManager $resultsManager,
125        protected PhpRenderer $renderer,
126        protected RecordLoader $recordLoader,
127        protected ?UserEntityInterface $user,
128        protected string $sessionId,
129        protected SearchNormalizer $searchNormalizer,
130        protected array $config,
131        protected Memory $searchMemory
132    ) {
133        $this->sessionSettings = $sessionSettings;
134    }
135
136    /**
137     * Handle a request.
138     *
139     * @param ParamsHelper $requestParams Parameter helper from controller
140     *
141     * @return array [response data, HTTP status code]
142     */
143    public function handleRequest(ParamsHelper $requestParams)
144    {
145        $results = $this->getSearchResults($requestParams);
146        if (!$results) {
147            return $this->formatResponse(['error' => 'Invalid request'], 400);
148        }
149        $elements = $this->getElements($requestParams, $results);
150        return $this->formatResponse(compact('elements'));
151    }
152
153    /**
154     * Get search results
155     *
156     * @param ParamsHelper $requestParams Request params
157     *
158     * @return ?Results
159     */
160    protected function getSearchResults(ParamsHelper $requestParams): ?Results
161    {
162        parse_str($requestParams->fromQuery('querystring', ''), $searchParams);
163        $backend = $requestParams->fromQuery('source', DEFAULT_SEARCH_BACKEND);
164
165        $results = $this->resultsManager->get($backend);
166        $paramsObj = $results->getParams();
167        $paramsObj->getOptions()->spellcheckEnabled(false);
168        $paramsObj->initFromRequest(new Parameters($searchParams));
169
170        if ($requestParams->fromQuery('history')) {
171            $this->saveSearchToHistory($results);
172        }
173
174        // Always save search parameters, since these are namespaced by search
175        // class ID.
176        $this->searchMemory->rememberParams($results->getParams());
177
178        return $results;
179    }
180
181    /**
182     * Render page elements
183     *
184     * @param ParamsHelper $requestParams Request params
185     * @param Results      $results       Search results
186     *
187     * @return array
188     */
189    protected function getElements(ParamsHelper $requestParams, Results $results): array
190    {
191        $result = [];
192        foreach ($this->elements as $selector => $element) {
193            $content = call_user_func([$this, $element['method']], $requestParams, $results);
194            if (null !== $content) {
195                $result[$selector] = [
196                    'content' => $content,
197                    'target' => $element['target'],
198                    'attrs' => $element['attrs'] ?? [],
199                ];
200            }
201        }
202        return $result;
203    }
204
205    /**
206     * Render search results
207     *
208     * @param ParamsHelper $requestParams Request params
209     * @param Results      $results       Search results
210     *
211     * @return ?string
212     */
213    protected function renderResults(ParamsHelper $requestParams, Results $results): ?string
214    {
215        [$baseAction] = explode('-', $results->getOptions()->getSearchAction());
216        $templatePath = "$baseAction/results-list.phtml";
217        if ('search' !== $baseAction && !$this->renderer->resolver($templatePath)) {
218            $templatePath = 'search/results-list.phtml';
219        }
220        $options = $results->getOptions();
221        $cart = $this->renderer->plugin('cart');
222        $showBulkOptions = $options->supportsCart()
223            && ($this->config['Site']['showBulkOptions'] ?? false);
224        // Checkboxes if appropriate:
225        $showCartControls = $options->supportsCart()
226            && $cart()->isActive()
227            && ($showBulkOptions || !$cart()->isActiveInSearch());
228        // Enable bulk options if appropriate:
229        $showCheckboxes = $showCartControls || $showBulkOptions;
230        // Include request parameters:
231        parse_str($requestParams->fromQuery('querystring', ''), $searchQueryParams);
232
233        return $this->renderer->render(
234            $templatePath,
235            [
236                'request' => $searchQueryParams,
237                'results' => $results,
238                'params' => $results->getParams(),
239                'showBulkOptions' => $showBulkOptions,
240                'showCartControls' => $showCartControls,
241                'showCheckboxes' => $showCheckboxes,
242                'saveToHistory' => (bool)$requestParams->fromQuery('history', false),
243            ]
244        );
245    }
246
247    /**
248     * Render pagination
249     *
250     * @param ParamsHelper $requestParams Request params
251     * @param Results      $results       Search results
252     * @param string       $template      Paginator template
253     * @param string       $ulClass       Additional class for the pagination container
254     * @param string       $navClass      Additional class for the nav element
255     *
256     * @return ?string
257     */
258    protected function renderPagination(
259        ParamsHelper $requestParams,
260        Results $results,
261        string $template = 'search/pagination.phtml',
262        string $ulClass = '',
263        string $navClass = ''
264    ): ?string {
265        $paginationOptions = [];
266        if ($ulClass) {
267            $paginationOptions['className'] = $ulClass;
268        }
269        if ($navClass) {
270            $paginationOptions['navClassName'] = $navClass;
271        }
272        $pagination = $this->renderer->plugin('paginationControl');
273        return $pagination(
274            $results->getPaginator(),
275            'Sliding',
276            $template,
277            ['results' => $results, 'options' => $paginationOptions]
278        );
279    }
280
281    /**
282     * Render simple pagination
283     *
284     * @param ParamsHelper $requestParams Request params
285     * @param Results      $results       Search results
286     *
287     * @return ?string
288     */
289    protected function renderPaginationSimple(ParamsHelper $requestParams, Results $results): ?string
290    {
291        return $this->renderPagination($requestParams, $results, 'search/pagination_simple.phtml');
292    }
293
294    /**
295     * Render top pagination
296     *
297     * @param ParamsHelper $requestParams Request params
298     * @param Results      $results       Search results
299     *
300     * @return ?string
301     */
302    protected function renderPaginationTop(ParamsHelper $requestParams, Results $results): ?string
303    {
304        return $this->renderPagination($requestParams, $results, 'search/pagination-top.phtml');
305    }
306
307    /**
308     * Render search stats
309     *
310     * @param ParamsHelper $requestParams Request params
311     * @param Results      $results       Search results
312     *
313     * @return ?string
314     */
315    protected function renderSearchStats(ParamsHelper $requestParams, Results $results): ?string
316    {
317        if (!($statsKey = $requestParams->fromQuery('statsKey'))) {
318            return null;
319        }
320
321        $localizedNumber = $this->renderer->plugin('localizedNumber');
322        $escapeHtml = $this->renderer->plugin('escapeHtml');
323        $lookfor = $results->getUrlQuery()->isQuerySuppressed()
324            ? '' : $results->getParams()->getDisplayQuery();
325        $transParams = [
326            '%%start%%' => $localizedNumber($results->getStartRecord()),
327            '%%end%%' => $localizedNumber($results->getEndRecord()),
328            '%%total%%' => $localizedNumber($results->getResultTotal()),
329            '%%lookfor%%' => $escapeHtml($lookfor),
330        ];
331
332        return $this->translate($statsKey, $transParams);
333    }
334
335    /**
336     * Render analytics
337     *
338     * @param ParamsHelper $requestParams Request params
339     * @param Results      $results       Search results
340     *
341     * @return ?string
342     */
343    protected function renderAnalytics(ParamsHelper $requestParams, Results $results): ?string
344    {
345        // Mimic the typical page structure so that analytics helpers can find the
346        // search results:
347        $view = new ViewModel();
348        $view->setTemplate('Helpers/analytics.phtml');
349        $view->addChild(new ViewModel(compact('results')));
350        return $this->renderer->render($view);
351    }
352
353    /**
354     * Save a search to the history in the database.
355     *
356     * @param Results $results Search results
357     *
358     * @return void
359     */
360    protected function saveSearchToHistory(Results $results): void
361    {
362        $this->searchNormalizer->saveNormalizedSearch(
363            $results,
364            $this->sessionId,
365            $this->user?->getId()
366        );
367    }
368}