Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
74.05% covered (warning)
74.05%
117 / 158
23.53% covered (danger)
23.53%
4 / 17
CRAP
0.00% covered (danger)
0.00%
0 / 1
SearchRequestModel
74.05% covered (warning)
74.05%
117 / 158
23.53% covered (danger)
23.53%
4 / 17
211.25
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 formatDateLimiter
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
 setParameters
80.00% covered (warning)
80.00%
12 / 15
0.00% covered (danger)
0.00%
0 / 1
10.80
 convertToQueryString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 convertToQueryStringParameterArray
84.62% covered (warning)
84.62%
33 / 39
0.00% covered (danger)
0.00%
0 / 1
26.10
 convertToSearchRequestJSON
91.94% covered (success)
91.94%
57 / 62
0.00% covered (danger)
0.00%
0 / 1
26.35
 isParameterIndexed
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getIndexedParameterName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addAction
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addQuery
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addLimiter
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 addExpander
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addfilter
66.67% covered (warning)
66.67%
8 / 12
0.00% covered (danger)
0.00%
0 / 1
4.59
 escapeSpecialCharacters
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 escapeSpecialCharactersForActions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 __get
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 __set
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3/**
4 * EBSCO EDS API Search Model
5 *
6 * PHP version 8
7 *
8 * Copyright (C) Serials Solutions 2011.
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 EBSCOIndustries
24 * @package  EBSCO
25 * @author   Michelle Milton <mmilton@epnet.com>
26 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
27 * @link     https://vufind.org
28 */
29
30namespace VuFindSearch\Backend\EDS;
31
32use function array_key_exists;
33use function count;
34use function intval;
35use function strlen;
36
37/**
38 * EBSCO EDS API Search Model
39 *
40 * @category EBSCOIndustries
41 * @package  EBSCO
42 * @author   Michelle Milton <mmilton@epnet.com>
43 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
44 * @link     https://vufind.org
45 */
46class SearchRequestModel
47{
48    /**
49     * What to search for, formatted as [{boolean operator},][{field code}:]{term}
50     *
51     * @var array
52     */
53    protected $query = [];
54
55    /**
56     * Whether or not to return facets with the search results. valid values are
57     * 'y' or 'n'
58     *
59     * @var string
60     */
61    protected $includeFacets;
62
63    /**
64     * Array of filters to apply to the search
65     *
66     * @var array
67     */
68    protected $facetFilters = [];
69
70    /**
71     * Array mapping a facet field to the AND/OR operator to use with it
72     *
73     * @var array
74     */
75    protected $facetOperators = [];
76
77    /**
78     * Sort option to apply
79     *
80     * @var string
81     */
82    protected $sort;
83
84    /**
85     * Options to limit the results by
86     *
87     * @var array
88     */
89    protected $limiters = [];
90
91    /**
92     * Mode to be effective in the search
93     *
94     * @var string
95     */
96    protected $searchMode;
97
98    /**
99     * Expanders to use. Comma separated.
100     *
101     * @var array
102     */
103    protected $expanders = [];
104
105    /**
106     * Requested level of detail to return the results with
107     *
108     * @var string
109     */
110    protected $view;
111
112    /**
113     * Number of records to return
114     *
115     * @var int
116     */
117    protected $resultsPerPage;
118
119    /**
120     * Page number of records to return. This is used in conjunction with the
121     * {@link $resultsPerPage} to determine the set of records to return.
122     *
123     * @var int
124     */
125    protected $pageNumber;
126
127    /**
128     * Whether or not to highlight the search term in the results.
129     *
130     * @var bool
131     */
132    protected $highlight;
133
134    /**
135     * Collection of user actions to apply to current request
136     *
137     * @var array
138     */
139    protected $actions = [];
140
141    /**
142     * Constructor
143     *
144     * Sets up the EDS API Search Request model
145     *
146     * @param array $parameters parameters to populate request
147     */
148    public function __construct($parameters = [])
149    {
150        $this->setParameters($parameters);
151    }
152
153    /**
154     * Format a date limiter
155     *
156     * @param string $filter Filter value
157     *
158     * @return string
159     */
160    protected function formatDateLimiter($filter)
161    {
162        // PublicationDate:[xxxx TO xxxx]
163        $dates = substr($filter, 17);
164        $dates = substr($dates, 0, strlen($dates) - 1);
165        $parts = explode(' TO ', $dates, 2);
166        $start = $end = null;
167        if (count($parts) == 2) {
168            $start = trim($parts[0]);
169            $end = trim($parts[1]);
170        }
171        if ('*' == $start || null == $start) {
172            $start = '0000';
173        }
174        if ('*' == $end || null == $end) {
175            $end = date('Y') + 1;
176        }
177        return "DT1:$start-01/$end-12";
178    }
179
180    /**
181     * Set properties from parameters
182     *
183     * @param array $parameters Parameters to set
184     *
185     * @return void
186     */
187    public function setParameters($parameters = [])
188    {
189        foreach ($parameters as $key => $values) {
190            switch ($key) {
191                case 'filters':
192                    foreach ($values as $filter) {
193                        if (str_starts_with($filter, 'LIMIT|')) {
194                            $this->addLimiter(substr($filter, 6));
195                        } elseif (str_starts_with($filter, 'EXPAND:')) {
196                            $this->addExpander(substr($filter, 7));
197                        } elseif (str_starts_with($filter, 'SEARCHMODE:')) {
198                            $this->searchMode = substr($filter, 11, null);
199                        } elseif (str_starts_with($filter, 'PublicationDate')) {
200                            $this->addLimiter($this->formatDateLimiter($filter));
201                        } else {
202                            $this->addFilter($filter);
203                        }
204                    }
205                    break;
206                default:
207                    if (property_exists($this, $key)) {
208                        $this->$key = $values;
209                    }
210            }
211        }
212    }
213
214    /**
215     * Converts properties to a querystring to send to the EdsAPI
216     *
217     * @return string
218     */
219    public function convertToQueryString()
220    {
221        return http_build_query($this->convertToQueryStringParameterArray());
222    }
223
224    /**
225     * Converts properties to a querystring to send to the EdsAPI
226     *
227     * @return string
228     */
229    public function convertToQueryStringParameterArray()
230    {
231        $qs = [];
232        if (isset($this->query) && 0 < count($this->query)) {
233            $formatQuery = function ($json) {
234                $query = json_decode($json, true);
235                $queryString = empty($query['bool'])
236                    ? '' : ($query['bool'] . ',');
237                if (!empty($query['field'])) {
238                    $queryString .= $query['field'] . ':';
239                }
240                $queryString .= static::escapeSpecialCharacters($query['term']);
241                return $queryString;
242            };
243            $qs['query-x'] = array_map($formatQuery, $this->query);
244        }
245
246        if (isset($this->facetFilters) && 0 < count($this->facetFilters)) {
247            $filterId = 1;
248            $qs['facetfilter'] = [];
249            foreach ($this->facetFilters as $field => $values) {
250                $values = array_map(fn ($value) => static::escapeSpecialCharacters($value), $values);
251                $operator = $this->facetOperators[$field];
252                if ('OR' == $operator) {
253                    $valuesString = implode(',', array_map(fn ($value) => "{$field}:{$value}", $values));
254                    $qs['facetfilter'][] = "{$filterId},{$valuesString}";
255                    $filterId++;
256                } else {
257                    foreach ($values as $value) {
258                        $qs['facetfilter'][] = "{$filterId},{$field}:{$value}";
259                        $filterId++;
260                    }
261                }
262            }
263        }
264
265        if (isset($this->limiters) && 0 < count($this->limiters)) {
266            $qs['limiter'] = $this->limiters;
267        }
268
269        if (isset($this->actions) && 0 < count($this->actions)) {
270            $qs['action-x'] = $this->actions;
271        }
272
273        if (isset($this->includeFacets)) {
274            $qs['includefacets'] = $this->includeFacets;
275        }
276
277        if (isset($this->sort)) {
278            $qs['sort'] = $this->sort;
279        }
280
281        if (isset($this->searchMode)) {
282            $qs['searchmode'] = $this->searchMode;
283        }
284
285        if (isset($this->expanders) && 0 < count($this->expanders)) {
286            $qs['expander'] = implode(',', $this->expanders);
287        }
288
289        if (isset($this->view)) {
290            $qs['view'] = $this->view;
291        }
292
293        if (isset($this->resultsPerPage)) {
294            $qs['resultsperpage'] = $this->resultsPerPage;
295        }
296
297        if (isset($this->pageNumber)) {
298            $qs['pagenumber'] = $this->pageNumber;
299        }
300
301        $highlightVal = isset($this->highlight) && $this->highlight ? 'y' : 'n';
302        $qs['highlight'] = $highlightVal;
303
304        return $qs;
305    }
306
307    /**
308     * Converts properties to a search request JSON document to send to the EdsAPI
309     *
310     * @return string
311     */
312    public function convertToSearchRequestJSON()
313    {
314        $json = new \stdClass();
315        $json->SearchCriteria = new \stdClass();
316        $json->RetrievalCriteria = new \stdClass();
317        $json->Actions = null;
318        if (isset($this->query) && 0 < count($this->query)) {
319            $json->SearchCriteria->Queries = [];
320            foreach ($this->query as $queryJson) {
321                $query = json_decode($queryJson, true);
322                $queryObj = new \stdClass();
323                if (!empty($query['bool'])) {
324                    $queryObj->BooleanOperator = $query['bool'];
325                }
326                if (!empty($query['field'])) {
327                    $queryObj->FieldCode = $query['field'];
328                }
329                $queryObj->Term = $query['term'];
330                $json->SearchCriteria->Queries[] = $queryObj;
331            }
332        }
333
334        if (isset($this->facetFilters) && 0 < count($this->facetFilters)) {
335            $json->SearchCriteria->FacetFilters = [];
336            $id = 1;
337            foreach ($this->facetFilters as $field => $values) {
338                if ('OR' == $this->facetOperators[$field]) {
339                    $filterObj = new \stdClass();
340                    $filterObj->FilterId = $id++;
341                    $filterObj->FacetValues = [];
342                    foreach ($values as $value) {
343                        $valueObj = new \stdClass();
344                        $valueObj->Id = $field;
345                        $valueObj->Value = $value;
346                        $filterObj->FacetValues[] = $valueObj;
347                    }
348                    $json->SearchCriteria->FacetFilters[] = $filterObj;
349                } else {
350                    foreach ($values as $value) {
351                        $filterObj = new \stdClass();
352                        $filterObj->FilterId = $id++;
353                        $valueObj = new \stdClass();
354                        $valueObj->Id = $field;
355                        $valueObj->Value = $value;
356                        $filterObj->FacetValues = [$valueObj];
357                        $json->SearchCriteria->FacetFilters[] = $filterObj;
358                    }
359                }
360            }
361        }
362
363        if (isset($this->limiters) && 0 < count($this->limiters)) {
364            $json->SearchCriteria->Limiters = [];
365            foreach ($this->limiters as $field => $values) {
366                // All EDS limiter values are combined as 'OR'.
367                // There is no alternate 'AND' syntax as with filters.
368                $limiterObj = new \stdClass();
369                $limiterObj->Id = $field;
370                $limiterObj->Values = $values;
371                $json->SearchCriteria->Limiters[] = $limiterObj;
372            }
373        }
374
375        if (isset($this->actions) && 0 < count($this->actions)) {
376            $json->Actions = $this->actions;
377        }
378
379        $json->SearchCriteria->IncludeFacets = $this->includeFacets ?? 'y';
380
381        if (isset($this->sort)) {
382            $json->SearchCriteria->Sort = $this->sort;
383        }
384
385        if (isset($this->searchMode)) {
386            $json->SearchCriteria->SearchMode = $this->searchMode;
387        }
388
389        if (isset($this->expanders) && 0 < count($this->expanders)) {
390            $json->SearchCriteria->Expanders = $this->expanders;
391        }
392
393        if (isset($this->view)) {
394            $json->RetrievalCriteria->View = $this->view;
395        }
396
397        if (isset($this->resultsPerPage)) {
398            $json->RetrievalCriteria->ResultsPerPage = intval($this->resultsPerPage);
399        }
400
401        if (isset($this->pageNumber)) {
402            $json->RetrievalCriteria->PageNumber = intval($this->pageNumber);
403        }
404
405        $highlightVal = isset($this->highlight) && $this->highlight ? 'y' : 'n';
406        $json->RetrievalCriteria->Highlight = $highlightVal;
407
408        return json_encode($json, JSON_PRETTY_PRINT);
409    }
410
411    /**
412     * Determines whether or not a querystring parameter is indexed
413     *
414     * @param string $value parameter key to check
415     *
416     * @return bool
417     */
418    public static function isParameterIndexed($value)
419    {
420        // Indexed parameter names end with '-x'
421        return str_ends_with($value, '-x');
422    }
423
424    /**
425     * Get the querystring parameter name of an indexed parameter to send to the Eds
426     * Api
427     *
428     * @param string $value Indexed parameter name
429     *
430     * @return string
431     */
432    public static function getIndexedParameterName($value)
433    {
434        // Indexed parameter names end with '-x'
435        return substr($value, 0, -2);
436    }
437
438    /**
439     * Add a new action
440     *
441     * @param string $action Action to add to the existing collection of actions
442     *
443     * @return void
444     */
445    public function addAction($action)
446    {
447        $this->actions[] = $action;
448    }
449
450    /**
451     * Add a new query expression
452     *
453     * @param string $query Query expression to add
454     *
455     * @return void
456     */
457    public function addQuery($query)
458    {
459        $this->query[] = $query;
460    }
461
462    /**
463     * Add a new limiter
464     *
465     * @param string $limiter Limiter to add
466     *
467     * @return void
468     */
469    public function addLimiter($limiter)
470    {
471        [$field, $value] = explode(':', $limiter);
472        if (!array_key_exists($field, $this->limiters)) {
473            $this->limiters[$field] = [];
474        }
475        $this->limiters[$field][] = $value;
476    }
477
478    /**
479     * Add a new expander
480     *
481     * @param string $expander Expander to add
482     *
483     * @return void
484     */
485    public function addExpander($expander)
486    {
487        $this->expanders[] = $expander;
488    }
489
490    /**
491     * Add a new facet filter
492     *
493     * @param string $facetFilter Facet Filter to add
494     *
495     * @return void
496     */
497    public function addfilter($facetFilter)
498    {
499        $filterComponents = explode(':', $facetFilter, 3);
500        if (count($filterComponents) < 3) {
501            [$field, $value] = $filterComponents;
502            // Default to AND, since it's already the default in EDS.ini.
503            $operator = 'AND';
504        } else {
505            [$field, $operator, $value] = $filterComponents;
506        }
507        if (str_starts_with($field, '~')) {
508            $field = substr($field, 1);
509            $operator = 'OR';
510        }
511        if (!array_key_exists($field, $this->facetFilters)) {
512            $this->facetFilters[$field] = [];
513        }
514        $this->facetFilters[$field][] = $value;
515        $this->facetOperators[$field] = $operator;
516    }
517
518    /**
519     * Escape characters that may be present in the parameter syntax
520     *
521     * @param string $value The value to escape
522     *
523     * @return string       The value with special characters escaped
524     */
525    public static function escapeSpecialCharacters($value)
526    {
527        return addcslashes($value, ':,');
528    }
529
530    /**
531     * Escape characters that may be present in the action parameter syntax
532     *
533     * @param string $value The value to escape
534     *
535     * @return string       The value with special characters escaped
536     */
537    public static function escapeSpecialCharactersForActions($value)
538    {
539        return addcslashes($value, ':,()');
540    }
541
542    /**
543     * Magic getter
544     *
545     * @param string $property Property to retrieve
546     *
547     * @return mixed
548     */
549    public function __get($property)
550    {
551        if (property_exists($this, $property)) {
552            return $this->$property;
553        }
554    }
555
556    /**
557     * Magic setter
558     *
559     * @param string $property Property to set
560     * @param mixed  $value    Value to set
561     *
562     * @return SearchRequestModel
563     */
564    public function __set($property, $value)
565    {
566        if (property_exists($this, $property)) {
567            $this->$property = $value;
568        }
569
570        return $this;
571    }
572}