Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
3.03% covered (danger)
3.03%
2 / 66
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
AbstractAPI
3.03% covered (danger)
3.03%
2 / 66
0.00% covered (danger)
0.00%
0 / 5
384.73
0.00% covered (danger)
0.00%
0 / 1
 preRequest
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 debugRequest
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 failureCodeIsAllowed
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 makeRequest
0.00% covered (danger)
0.00%
0 / 46
0.00% covered (danger)
0.00%
0 / 1
156
 setConfig
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
1<?php
2
3/**
4 * Abstract Driver for API-based ILS drivers
5 *
6 * PHP version 8
7 *
8 * Copyright (C) Villanova University 2018.
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  ILS_Drivers
25 * @author   Chris Hallberg <challber@villanova.edu>
26 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
27 * @link     https://vufind.org/wiki/development:plugins:ils_drivers Wiki
28 */
29
30namespace VuFind\ILS\Driver;
31
32use Laminas\Log\LoggerAwareInterface;
33use VuFind\Exception\BadConfig;
34use VuFind\Exception\ILS as ILSException;
35use VuFindHttp\HttpServiceAwareInterface;
36
37use function in_array;
38use function is_string;
39
40/**
41 * Abstract Driver for API-based ILS drivers
42 *
43 * @category VuFind
44 * @package  ILS_Drivers
45 * @author   Chris Hallberg <challber@villanova.edu>
46 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
47 * @link     https://vufind.org/wiki/development:plugins:ils_drivers Wiki
48 */
49abstract class AbstractAPI extends AbstractBase implements
50    HttpServiceAwareInterface,
51    LoggerAwareInterface
52{
53    use \VuFind\Log\LoggerAwareTrait {
54        logError as error;
55    }
56    use \VuFindHttp\HttpServiceAwareTrait;
57
58    /**
59     * Allow default corrections to all requests
60     *
61     * @param \Laminas\Http\Headers $headers the request headers
62     * @param array                 $params  the parameters object
63     *
64     * @return array
65     */
66    protected function preRequest(\Laminas\Http\Headers $headers, $params)
67    {
68        return [$headers, $params];
69    }
70
71    /**
72     * Function that obscures and logs debug data
73     *
74     * @param string                $method      Request method
75     * (GET/POST/PUT/DELETE/etc.)
76     * @param string                $path        Request URL
77     * @param array                 $params      Request parameters
78     * @param \Laminas\Http\Headers $req_headers Headers object
79     *
80     * @return void
81     */
82    protected function debugRequest($method, $path, $params, $req_headers)
83    {
84        $logParams = [];
85        $logHeaders = [];
86        if ($method == 'GET') {
87            $logParams = $params;
88            $logHeaders = $req_headers->toArray();
89        }
90        $this->debug(
91            $method . ' request.' .
92            ' URL: ' . $path . '.' .
93            ' Params: ' . $this->varDump($logParams) . '.' .
94            ' Headers: ' . $this->varDump($logHeaders)
95        );
96    }
97
98    /**
99     * Does $code match the setting for allowed failure codes?
100     *
101     * @param int               $code                Code to check.
102     * @param true|int[]|string $allowedFailureCodes HTTP failure codes that should
103     * NOT cause an ILSException to be thrown. May be an array of integers, a regular
104     * expression, or boolean true to allow all codes.
105     *
106     * @return bool
107     */
108    protected function failureCodeIsAllowed(int $code, $allowedFailureCodes): bool
109    {
110        if ($allowedFailureCodes === true) {    // "allow everything" case
111            return true;
112        }
113        return is_string($allowedFailureCodes)
114            ? preg_match($allowedFailureCodes, (string)$code)
115            : in_array($code, (array)$allowedFailureCodes);
116    }
117
118    /**
119     * Make requests
120     *
121     * @param string            $method              GET/POST/PUT/DELETE/etc
122     * @param string            $path                API path (with a leading /)
123     * @param string|array      $params              Query parameters
124     * @param array             $headers             Additional headers
125     * @param true|int[]|string $allowedFailureCodes HTTP failure codes that should
126     * NOT cause an ILSException to be thrown. May be an array of integers, a regular
127     * expression, or boolean true to allow all codes.
128     * @param string|array      $debugParams         Value to use in place of $params
129     * in debug messages (useful for concealing sensitive data, etc.)
130     *
131     * @return \Laminas\Http\Response
132     * @throws ILSException
133     */
134    public function makeRequest(
135        $method = 'GET',
136        $path = '/',
137        $params = [],
138        $headers = [],
139        $allowedFailureCodes = [],
140        $debugParams = null
141    ) {
142        $client = $this->httpService->createClient(
143            $this->config['API']['base_url'] . $path,
144            $method,
145            120
146        );
147
148        // Add default headers and parameters
149        $req_headers = $client->getRequest()->getHeaders();
150        $req_headers->addHeaders($headers);
151        [$req_headers, $params] = $this->preRequest($req_headers, $params);
152
153        if ($this->logger) {
154            $this->debugRequest($method, $path, $debugParams ?? $params, $req_headers);
155        }
156
157        // Add params
158        if ($method == 'GET') {
159            $client->setParameterGet($params);
160        } else {
161            if (is_string($params)) {
162                $client->getRequest()->setContent($params);
163            } else {
164                $client->setParameterPost($params);
165            }
166        }
167        try {
168            $response = $client->send();
169        } catch (\Exception $e) {
170            $this->logError('Unexpected ' . $e::class . ': ' . (string)$e);
171            throw new ILSException('Error during send operation.');
172        }
173        $code = $response->getStatusCode();
174        if (
175            !$response->isSuccess()
176            && !$this->failureCodeIsAllowed($code, $allowedFailureCodes)
177        ) {
178            $this->logError(
179                "Unexpected error response; code: $code, body: "
180                . $response->getBody()
181            );
182            throw new ILSException('Unexpected error code.');
183        }
184        if ($jsonLog = ($this->config['API']['json_log_file'] ?? false)) {
185            if (APPLICATION_ENV !== 'development') {
186                $this->logError(
187                    'SECURITY: json_log_file enabled outside of development mode; disabling feature.'
188                );
189            } else {
190                $body = $response->getBody();
191                $jsonBody = @json_decode($body);
192                $json = file_exists($jsonLog)
193                    ? json_decode(file_get_contents($jsonLog)) : [];
194                $json[] = [
195                    'expectedMethod' => $method,
196                    'expectedPath' => $path,
197                    'expectedParams' => $params,
198                    'body' => $jsonBody ? $jsonBody : $body,
199                    'bodyType' => $jsonBody ? 'json' : 'string',
200                    'status' => $code,
201                ];
202                file_put_contents($jsonLog, json_encode($json));
203            }
204        }
205        return $response;
206    }
207
208    /**
209     * Set the configuration for the driver.
210     *
211     * @param array $config Configuration array (usually loaded from a VuFind .ini
212     * file whose name corresponds with the driver class name).
213     *
214     * @throws BadConfig if base url excluded
215     * @return void
216     */
217    public function setConfig($config)
218    {
219        parent::setConfig($config);
220        // Base URL required for API drivers
221        if (!isset($config['API']['base_url'])) {
222            throw new BadConfig('API Driver configured without base url.');
223        }
224    }
225}