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