Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
58.62% covered (warning)
58.62%
51 / 87
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
Connector
58.62% covered (warning)
58.62%
51 / 87
0.00% covered (danger)
0.00%
0 / 5
48.34
0.00% covered (danger)
0.00%
0 / 1
 __construct
77.78% covered (warning)
77.78%
7 / 9
0.00% covered (danger)
0.00%
0 / 1
3.10
 query
43.75% covered (danger)
43.75%
7 / 16
0.00% covered (danger)
0.00%
0 / 1
4.60
 call
75.00% covered (warning)
75.00%
9 / 12
0.00% covered (danger)
0.00%
0 / 1
4.25
 process
80.00% covered (warning)
80.00%
20 / 25
0.00% covered (danger)
0.00%
0 / 1
6.29
 prepareParams
32.00% covered (danger)
32.00%
8 / 25
0.00% covered (danger)
0.00%
0 / 1
9.03
1<?php
2
3/**
4 * LibGuides connector.
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  Search
25 * @author   Chelsea Lobdell <clobdel1@swarthmore.edu>
26 * @author   Demian Katz <demian.katz@villanova.edu>
27 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
28 * @link     https://vufind.org
29 */
30
31namespace VuFindSearch\Backend\LibGuides;
32
33use Laminas\Http\Client as HttpClient;
34
35use function array_slice;
36use function count;
37use function strlen;
38
39/**
40 * LibGuides connector.
41 *
42 * @category VuFind
43 * @package  Search
44 * @author   Chelsea Lobdell <clobdel1@swarthmore.edu>
45 * @author   Demian Katz <demian.katz@villanova.edu>
46 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
47 * @link     https://vufind.org
48 */
49class Connector implements \Laminas\Log\LoggerAwareInterface
50{
51    use \VuFind\Log\LoggerAwareTrait;
52
53    /**
54     * The HTTP_Request object used for API transactions
55     *
56     * @var HttpClient
57     */
58    public $client;
59
60    /**
61     * Institution code
62     *
63     * @var string
64     */
65    protected $iid;
66
67    /**
68     * Base URL for API
69     *
70     * @var string
71     */
72    protected $host;
73
74    /**
75     * API version number
76     *
77     * @var float
78     */
79    protected $apiVersion;
80
81    /**
82     * Optionally load & display the description of each resource
83     *
84     * @var bool
85     */
86    protected $displayDescription;
87
88    /**
89     * Constructor
90     *
91     * Sets up the LibGuides Client
92     *
93     * @param string     $iid                Institution ID
94     * @param HttpClient $client             HTTP client
95     * @param float      $apiVersion         API version number
96     * @param string     $baseUrl            API base URL (optional)
97     * @param bool       $displayDescription Optionally load & display the description of each resource
98     */
99    public function __construct($iid, $client, $apiVersion = 1, $baseUrl = null, $displayDescription = false)
100    {
101        $this->apiVersion = $apiVersion;
102        if (empty($baseUrl)) {
103            $this->host = ($this->apiVersion < 2)
104                ? 'http://api.libguides.com/api_search.php?'
105                : 'http://lgapi.libapps.com/widgets.php?';
106        } else {
107            // Ensure appropriate number of question marks:
108            $this->host = rtrim($baseUrl, '?') . '?';
109        }
110        $this->iid = $iid;
111        $this->client = $client;
112        $this->displayDescription = $displayDescription;
113    }
114
115    /**
116     * Execute a search. Adds all the querystring parameters into
117     * $this->client and returns the parsed response
118     *
119     * @param array $params    Incoming search parameters.
120     * @param int   $offset    Search offset
121     * @param int   $limit     Search limit
122     * @param bool  $returnErr Should we return errors in a structured way (true)
123     * or simply throw an exception (false)?
124     *
125     * @throws \Exception
126     * @return array             An array of query results
127     */
128    public function query(array $params, $offset = 0, $limit = 20, $returnErr = true)
129    {
130        $args = $this->prepareParams($params);
131
132        // run search, deal with exceptions
133        try {
134            $result = $this->call(http_build_query($args));
135            $result['documents']
136                = array_slice($result['documents'], $offset, $limit);
137        } catch (\Exception $e) {
138            if ($returnErr) {
139                $this->debug($e->getMessage());
140                $result = [
141                    'recordCount' => 0,
142                    'documents' => [],
143                    'error' => $e->getMessage(),
144                ];
145            } else {
146                throw $e;
147            }
148        }
149        $result['offset'] = $offset;
150        $result['limit'] = $limit;
151        return $result;
152    }
153
154    /**
155     * Small wrapper for sendRequest, process to simplify error handling.
156     *
157     * @param string $qs     Query string
158     * @param string $method HTTP method
159     *
160     * @return object    The parsed data
161     * @throws \Exception
162     */
163    protected function call($qs, $method = 'GET')
164    {
165        $this->debug("{$method}{$this->host}{$qs}");
166        $this->client->resetParameters();
167        $baseUrl = null;
168        if ($method == 'GET') {
169            $baseUrl = $this->host . $qs;
170        } elseif ($method == 'POST') {
171            throw new \Exception('POST not supported');
172        }
173
174        // Send Request
175        $this->client->setUri($baseUrl);
176        $result = $this->client->setMethod($method)->send();
177        if (!$result->isSuccess()) {
178            throw new \Exception($result->getBody());
179        }
180        return $this->process($result->getBody());
181    }
182
183    /**
184     * Translate API response into more convenient format.
185     *
186     * @param array $data The raw response
187     *
188     * @return array      The processed response
189     */
190    protected function process($data)
191    {
192        // make sure data exists
193        if (strlen($data) == 0) {
194            throw new \Exception('LibGuides did not return any data');
195        }
196
197        $items = [];
198
199        $itemRegex = '/<li>(.*?)<\/li>/';
200        $linkRegex = '/<a href="([^"]*)"[^>]*>([^<]*)</';
201        $descriptionRegex = '/<div class="s-lg-(?:widget|guide)-list-description">(.*?)<\/div>/';
202
203        // Extract each result item
204        $itemCount = preg_match_all($itemRegex, $data, $itemMatches);
205
206        for ($i = 0; $i < $itemCount; $i++) {
207            // Extract the link, which contains both the title and URL.
208            $linkCount = preg_match_all($linkRegex, $itemMatches[1][$i], $linkMatches);
209            if ($linkCount != 1) {
210                throw new \Exception('LibGuides result item included more than one link: ' . $itemMatches[1][$i]);
211            }
212            $item = [
213                'id' => $linkMatches[1][0],    // ID = URL
214                'title' => $linkMatches[2][0],
215            ];
216
217            // Extract the description.
218            if ($this->displayDescription) {
219                $descriptionCount = preg_match_all($descriptionRegex, $itemMatches[1][$i], $descriptionMatches);
220                if ($descriptionCount >= 1) {
221                    $item['description'] = html_entity_decode(strip_tags($descriptionMatches[1][0]));
222                }
223            }
224
225            $items[] = $item;
226        }
227
228        $results = [
229            'recordCount' => count($items),
230            'documents' => $items,
231        ];
232
233        return $results;
234    }
235
236    /**
237     * Prepare API parameters
238     *
239     * @param array $params Incoming parameters
240     *
241     * @return array
242     */
243    protected function prepareParams(array $params)
244    {
245        // defaults for params (vary by version)
246        if ($this->apiVersion < 2) {
247            $args = [
248                'iid' => $this->iid,
249                'type' => 'guides',
250                'more' => 'false',
251                'sortby' => 'relevance',
252            ];
253        } else {
254            $args = [
255                'site_id' => $this->iid,
256                'sort_by' => 'relevance',
257                'widget_type' => 1,
258                'search_match' => 2,
259                'search_type' => 0,
260                'list_format' => 1,
261                'output_format' => 1,
262                'load_type' => 2,
263                'enable_description' => $this->displayDescription ? 1 : 0,
264                'enable_group_search_limit' => 0,
265                'enable_subject_search_limit' => 0,
266                'widget_embed_type' => 2,
267            ];
268            // remap v1 --> v2 params:
269            if (isset($params['search'])) {
270                $params['search_terms'] = $params['search'];
271                unset($params['search']);
272            }
273        }
274        return array_merge($args, $params);
275    }
276}