This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | |||
3 | namespace Scriptotek\Alma; |
||
4 | |||
5 | use Danmichaelo\QuiteSimpleXMLElement\QuiteSimpleXMLElement; |
||
6 | use Http\Client\Common\Plugin\ContentLengthPlugin; |
||
7 | use Http\Client\Common\Plugin\ErrorPlugin; |
||
8 | use Http\Client\Common\PluginClient; |
||
9 | use Http\Client\Exception\HttpException; |
||
10 | use Http\Client\Exception\NetworkException; |
||
11 | use Http\Factory\Discovery\HttpClient; |
||
12 | use Http\Factory\Discovery\HttpFactory; |
||
13 | use Http\Message\UriFactory; |
||
14 | use Psr\Http\Client\ClientInterface as HttpClientInterface; |
||
15 | use Psr\Http\Message\RequestFactoryInterface; |
||
16 | use Psr\Http\Message\RequestInterface; |
||
17 | use Psr\Http\Message\ResponseInterface; |
||
18 | use Psr\Http\Message\UriFactoryInterface; |
||
19 | use Psr\Http\Message\UriInterface; |
||
20 | use Scriptotek\Alma\Analytics\Analytics; |
||
21 | use Scriptotek\Alma\Bibs\Bibs; |
||
22 | use Scriptotek\Alma\Bibs\Items; |
||
23 | use Scriptotek\Alma\Conf\Conf; |
||
24 | use Scriptotek\Alma\Conf\Jobs; |
||
25 | use Scriptotek\Alma\Conf\Libraries; |
||
26 | use Scriptotek\Alma\Conf\Library; |
||
27 | use Scriptotek\Alma\Exception\ClientException as AlmaClientException; |
||
28 | use Scriptotek\Alma\Exception\InvalidApiKey; |
||
29 | use Scriptotek\Alma\Exception\MaxNumberOfAttemptsExhausted; |
||
30 | use Scriptotek\Alma\Exception\RequestFailed; |
||
31 | use Scriptotek\Alma\Exception\ResourceNotFound; |
||
32 | use Scriptotek\Alma\Exception\SruClientNotSetException; |
||
33 | use Scriptotek\Alma\TaskLists\LendingRequests; |
||
34 | use Scriptotek\Alma\TaskLists\TaskLists; |
||
35 | use Scriptotek\Alma\Users\Users; |
||
36 | use Scriptotek\Sru\Client as SruClient; |
||
37 | use function GuzzleHttp\Psr7\stream_for; |
||
38 | |||
39 | /** |
||
40 | * Alma client. |
||
41 | */ |
||
42 | class Client |
||
43 | { |
||
44 | public $entrypoint; |
||
45 | |||
46 | /** @var string Alma zone (institution or network) */ |
||
47 | public $zone; |
||
48 | |||
49 | /** @var string Alma Developers Network API key for this zone */ |
||
50 | public $key; |
||
51 | |||
52 | /** @var Client Network zone instance */ |
||
53 | public $nz; |
||
54 | |||
55 | /** @var HttpClientInterface */ |
||
56 | protected $http; |
||
57 | |||
58 | /** @var RequestFactoryInterface */ |
||
59 | protected $requestFactory; |
||
60 | |||
61 | /** @var UriFactory */ |
||
62 | protected $uriFactory; |
||
63 | |||
64 | /** @var SruClient */ |
||
65 | public $sru; |
||
66 | |||
67 | /** @var Bibs */ |
||
68 | public $bibs; |
||
69 | |||
70 | /** @var Analytics */ |
||
71 | public $analytics; |
||
72 | |||
73 | /** @var Users */ |
||
74 | public $users; |
||
75 | |||
76 | /** @var Items */ |
||
77 | public $items; |
||
78 | |||
79 | /** @var int Max number of retries if we get 429 errors */ |
||
80 | public $maxAttempts = 10; |
||
81 | |||
82 | /** @var int Max number of retries if we get 5XX errors */ |
||
83 | public $maxAttemptsOnServerError = 1; |
||
84 | |||
85 | /** @var float Number of seconds to sleep before retrying */ |
||
86 | public $sleepTimeOnRetry = 0.5; |
||
87 | |||
88 | /** @var float Number of seconds to sleep before retrying after a server error */ |
||
89 | public $sleepTimeOnServerError = 10; |
||
90 | |||
91 | /** @var array Extra request headers */ |
||
92 | public $headers = []; |
||
93 | |||
94 | /** |
||
95 | * @var Conf |
||
96 | */ |
||
97 | public $conf; |
||
98 | |||
99 | /** |
||
100 | * @var Libraries |
||
101 | */ |
||
102 | public $libraries; |
||
103 | |||
104 | /** |
||
105 | * @var Jobs |
||
106 | */ |
||
107 | public $jobs; |
||
108 | |||
109 | /** |
||
110 | * @var TaskLists |
||
111 | */ |
||
112 | public $taskLists; |
||
113 | |||
114 | /** |
||
115 | * Create a new client to connect to a given Alma instance. |
||
116 | * |
||
117 | * @param ?string $key API key |
||
118 | * @param string $region Hosted region code, used to build the entrypoint URL |
||
119 | * @param string $zone Alma zone (Either Zones::INSTITUTION or Zones::NETWORK) |
||
120 | * @param ?HttpClientInterface $http |
||
121 | * @param ?RequestFactoryInterface $requestFactory |
||
122 | * @param ?UriFactoryInterface $uriFactory |
||
123 | * |
||
124 | * @throws \ErrorException |
||
125 | */ |
||
126 | public function __construct( |
||
127 | $key = null, |
||
128 | $region = 'eu', |
||
129 | $zone = Zones::INSTITUTION, |
||
130 | HttpClientInterface $http = null, |
||
131 | RequestFactoryInterface $requestFactory = null, |
||
132 | UriFactoryInterface $uriFactory = null |
||
133 | ) { |
||
134 | $this->http = new PluginClient( |
||
135 | $http ?: HttpClient::client(), |
||
136 | [ |
||
137 | new ContentLengthPlugin(), |
||
138 | new ErrorPlugin(), |
||
139 | ] |
||
140 | ); |
||
141 | $this->requestFactory = $requestFactory ?: HttpFactory::requestFactory(); |
||
142 | $this->uriFactory = $uriFactory ?: HttpFactory::uriFactory(); |
||
143 | |||
144 | $this->key = $key; |
||
145 | |||
146 | if (!is_null($region)) { |
||
147 | $this->setRegion($region); |
||
148 | } |
||
149 | |||
150 | $this->zone = $zone; |
||
151 | |||
152 | $this->bibs = new Bibs($this); |
||
153 | $this->items = new Items($this); // Only needed for the fromBarcode method :/ |
||
154 | |||
155 | $this->analytics = new Analytics($this); |
||
156 | $this->users = new Users($this); |
||
157 | |||
158 | $this->conf = new Conf($this); |
||
159 | $this->libraries = $this->conf->libraries; // shortcut |
||
160 | $this->jobs = $this->conf->jobs; // shortcut |
||
161 | |||
162 | $this->taskLists = new TaskLists($this); |
||
163 | |||
164 | if ($zone == Zones::INSTITUTION) { |
||
165 | $this->nz = new self( |
||
166 | null, |
||
167 | $region, |
||
168 | Zones::NETWORK, |
||
169 | $this->http, |
||
170 | $this->requestFactory, |
||
171 | $this->uriFactory |
||
172 | ); |
||
173 | } elseif ($zone != Zones::NETWORK) { |
||
174 | throw new AlmaClientException('Invalid zone name.'); |
||
175 | } |
||
176 | } |
||
177 | |||
178 | public function lendingRequests(Library $library, $params = []) |
||
179 | { |
||
180 | return new LendingRequests($this, $library, $params); |
||
181 | } |
||
182 | |||
183 | /** |
||
184 | * Attach an SRU client (so you can search for Bib records). |
||
185 | * |
||
186 | * @param SruClient $sru |
||
187 | */ |
||
188 | public function setSruClient(SruClient $sru) |
||
189 | { |
||
190 | $this->sru = $sru; |
||
191 | } |
||
192 | |||
193 | /** |
||
194 | * Assert that an SRU client is connected. Throws SruClientNotSetException if not. |
||
195 | * |
||
196 | * @throws SruClientNotSetException |
||
197 | */ |
||
198 | public function assertHasSruClient() |
||
199 | { |
||
200 | if (!isset($this->sru)) { |
||
201 | throw new SruClientNotSetException(); |
||
202 | } |
||
203 | } |
||
204 | |||
205 | /** |
||
206 | * Set the API key for this Alma instance. |
||
207 | * |
||
208 | * @param string $key The API key |
||
209 | * |
||
210 | * @return $this |
||
211 | */ |
||
212 | public function setKey($key) |
||
213 | { |
||
214 | $this->key = $key; |
||
215 | |||
216 | return $this; |
||
217 | } |
||
218 | |||
219 | /** |
||
220 | * Set the Alma region code ('na' for North America, 'eu' for Europe, 'ap' for Asia Pacific). |
||
221 | * |
||
222 | * @param $regionCode |
||
223 | * @throws AlmaClientException |
||
224 | * @return $this |
||
225 | */ |
||
226 | public function setRegion($regionCode) |
||
227 | { |
||
228 | if (!in_array($regionCode, ['na', 'eu', 'ap'])) { |
||
229 | throw new AlmaClientException('Invalid region code'); |
||
230 | } |
||
231 | $this->setEntryPoint('https://api-' . $regionCode . '.hosted.exlibrisgroup.com/almaws/v1'); |
||
232 | |||
233 | return $this; |
||
234 | } |
||
235 | |||
236 | /** |
||
237 | * Set the Alma API base url. |
||
238 | * |
||
239 | * @param string $url |
||
240 | * @return $this |
||
241 | */ |
||
242 | public function setEntryPoint(string $url) |
||
243 | { |
||
244 | $this->entrypoint = $url; |
||
245 | |||
246 | return $this; |
||
247 | } |
||
248 | |||
249 | /** |
||
250 | * Set extra request headers. |
||
251 | * |
||
252 | * @param array $headers |
||
253 | * @return $this |
||
254 | */ |
||
255 | public function setExtraHeaders(array $headers) |
||
256 | { |
||
257 | $this->headers = $headers; |
||
258 | |||
259 | return $this; |
||
260 | } |
||
261 | |||
262 | /** |
||
263 | * Extend an URL with query string parameters and return an UriInterface object. |
||
264 | * |
||
265 | * @param string $url |
||
266 | * @param array $query |
||
267 | * |
||
268 | * @return UriInterface |
||
269 | */ |
||
270 | public function buildUrl($url, $query = []) |
||
271 | { |
||
272 | $url = explode('?', $url, 2); |
||
273 | if (count($url) == 2) { |
||
274 | parse_str($url[1], $query0); |
||
275 | $query = array_merge($query0, $query); |
||
276 | } |
||
277 | |||
278 | $url = $url[0]; |
||
279 | |||
280 | if (strpos($url, $this->entrypoint) === false) { |
||
281 | $url = $this->entrypoint . $url; |
||
282 | } |
||
283 | |||
284 | return $this->uriFactory->createUri($url) |
||
285 | ->withQuery(http_build_query($query)); |
||
286 | } |
||
287 | |||
288 | /** |
||
289 | * Make a synchronous HTTP request and return a PSR7 response if successful. |
||
290 | * In the case of intermittent errors (connection problem, 429 or 5XX error), the request is |
||
291 | * attempted a maximum of {$this->maxAttempts} times with a sleep of {$this->sleepTimeOnRetry} |
||
292 | * between each attempt to avoid hammering the server. |
||
293 | * |
||
294 | * @param RequestInterface $request |
||
295 | * @param int $attempt |
||
296 | * |
||
297 | * @return ResponseInterface |
||
298 | */ |
||
299 | public function request(RequestInterface $request, $attempt = 1) |
||
300 | { |
||
301 | if (isset($this->key)) { |
||
302 | $request = $request->withHeader('Authorization', 'apikey ' . $this->key); |
||
303 | } |
||
304 | foreach ($this->headers as $key => $val) { |
||
305 | $request = $request->withHeader($key, $val); |
||
306 | } |
||
307 | |||
308 | try { |
||
309 | return $this->http->sendRequest($request); |
||
310 | } catch (HttpException $e) { |
||
311 | // Thrown for 400 and 500 level errors. |
||
312 | $statusCode = $e->getResponse()->getStatusCode(); |
||
313 | |||
314 | $error = $this->parseClientError($e); |
||
315 | |||
316 | if ($error->getErrorCode() === 'PER_SECOND_THRESHOLD') { |
||
317 | // We've run into the "Max 25 API calls per institution per second" limit. |
||
318 | // Wait a sec and retry, unless we've tried too many times already. |
||
319 | if ($attempt >= $this->maxAttempts) { |
||
320 | throw new MaxNumberOfAttemptsExhausted( |
||
321 | 'Rate limiting error - max number of retry attempts exhausted.', |
||
322 | 0, |
||
323 | $e |
||
324 | ); |
||
325 | } |
||
326 | time_nanosleep(0, $this->sleepTimeOnRetry * 1000000000); |
||
327 | |||
328 | return $this->request($request, $attempt + 1); |
||
329 | } |
||
330 | |||
331 | if ($statusCode >= 500 && $statusCode < 600) { |
||
332 | if ($attempt >= $this->maxAttemptsOnServerError) { |
||
333 | throw $error; |
||
334 | } |
||
335 | time_nanosleep(0, $this->sleepTimeOnServerError * 1000000000); |
||
336 | |||
337 | return $this->request($request, $attempt + 1); |
||
338 | } |
||
339 | |||
340 | // Throw exception for other errors |
||
341 | throw $error; |
||
342 | } catch (NetworkException $e) { |
||
343 | // Thrown in case of a networking error |
||
344 | // Wait a sec and retry, unless we've tried too many times already. |
||
345 | if ($attempt > $this->maxAttempts) { |
||
346 | throw new MaxNumberOfAttemptsExhausted( |
||
347 | 'Network error - max number of retry attempts exhausted.', |
||
348 | 0, |
||
349 | $e |
||
350 | ); |
||
351 | } |
||
352 | time_nanosleep(0, $this->sleepTimeOnRetry * 1000000000); |
||
353 | |||
354 | return $this->request($request, $attempt + 1); |
||
355 | } |
||
356 | } |
||
357 | |||
358 | /** |
||
359 | * Make a GET request. |
||
360 | * |
||
361 | * @param string $url |
||
362 | * @param array $query |
||
363 | * @param string $contentType |
||
364 | * |
||
365 | * @return string The response body |
||
366 | */ |
||
367 | public function get($url, $query = [], $contentType = 'application/json') |
||
368 | { |
||
369 | $url = $this->buildUrl($url, $query); |
||
370 | $request = $this->requestFactory->createRequest('GET', $url) |
||
371 | ->withHeader('Accept', $contentType); |
||
372 | |||
373 | $response = $this->request($request); |
||
374 | |||
375 | return strval($response->getBody()); |
||
376 | } |
||
377 | |||
378 | /** |
||
379 | * Make a GET request, accepting JSON. |
||
380 | * |
||
381 | * @param string $url |
||
382 | * @param array $query |
||
383 | * |
||
384 | * @return \stdClass JSON response as an object. |
||
385 | */ |
||
386 | public function getJSON($url, $query = []) |
||
387 | { |
||
388 | $responseBody = $this->get($url, $query, 'application/json'); |
||
389 | |||
390 | return json_decode($responseBody); |
||
391 | } |
||
392 | |||
393 | /** |
||
394 | * Make a GET request, accepting XML. |
||
395 | * |
||
396 | * @param string $url |
||
397 | * @param array $query |
||
398 | * |
||
399 | * @return QuiteSimpleXMLElement |
||
400 | */ |
||
401 | public function getXML($url, $query = []) |
||
402 | { |
||
403 | $responseBody = $this->get($url, $query, 'application/xml'); |
||
404 | |||
405 | return new QuiteSimpleXMLElement($responseBody); |
||
406 | } |
||
407 | |||
408 | /** |
||
409 | * Make a PUT request. |
||
410 | * |
||
411 | * @param string $url |
||
412 | * @param $data |
||
413 | * @param string $contentType |
||
414 | * |
||
415 | * @return string The response body |
||
416 | */ |
||
417 | View Code Duplication | public function put($url, $data, $contentType = 'application/json') |
|
0 ignored issues
–
show
|
|||
418 | { |
||
419 | $uri = $this->buildUrl($url); |
||
420 | |||
421 | $request = $this->requestFactory->createRequest('PUT', $uri); |
||
422 | if (!is_null($contentType)) { |
||
423 | $request = $request->withHeader('Content-Type', $contentType); |
||
424 | $request = $request->withHeader('Accept', $contentType); |
||
425 | } |
||
426 | $request = $request->withBody(stream_for($data)); |
||
427 | |||
428 | $response = $this->request($request); |
||
429 | |||
430 | return strval($response->getBody()); |
||
431 | } |
||
432 | |||
433 | /** |
||
434 | * Make a PUT request, sending JSON data. |
||
435 | * |
||
436 | * @param string $url |
||
437 | * @param $data |
||
438 | * |
||
439 | * @return \stdClass |
||
440 | */ |
||
441 | public function putJSON($url, $data) |
||
442 | { |
||
443 | $responseBody = $this->put($url, json_encode($data), 'application/json'); |
||
444 | |||
445 | return json_decode($responseBody); |
||
446 | } |
||
447 | |||
448 | /** |
||
449 | * Make a PUT request, sending XML data. |
||
450 | * |
||
451 | * @param string $url |
||
452 | * @param $data |
||
453 | * |
||
454 | * @return QuiteSimpleXMLElement |
||
455 | */ |
||
456 | public function putXML($url, $data) |
||
457 | { |
||
458 | $responseBody = $this->put($url, $data, 'application/xml'); |
||
459 | |||
460 | return new QuiteSimpleXMLElement($responseBody); |
||
461 | } |
||
462 | |||
463 | /** |
||
464 | * Make a POST request. |
||
465 | * |
||
466 | * @param string $url |
||
467 | * @param $data |
||
468 | * @param string $contentType |
||
469 | * |
||
470 | * @return string The response body |
||
471 | */ |
||
472 | View Code Duplication | public function post($url, $data, $contentType = 'application/json') |
|
0 ignored issues
–
show
This method seems to be duplicated in your project.
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation. You can also find more detailed suggestions in the “Code” section of your repository. ![]() |
|||
473 | { |
||
474 | $uri = $this->buildUrl($url); |
||
475 | |||
476 | $request = $this->requestFactory->createRequest('POST', $uri); |
||
477 | if (!is_null($contentType)) { |
||
478 | $request = $request->withHeader('Content-Type', $contentType); |
||
479 | $request = $request->withHeader('Accept', $contentType); |
||
480 | } |
||
481 | $request = $request->withBody(stream_for($data)); |
||
482 | |||
483 | $response = $this->request($request); |
||
484 | |||
485 | return strval($response->getBody()); |
||
486 | } |
||
487 | |||
488 | /** |
||
489 | * Make a POST request, sending JSON data. |
||
490 | * |
||
491 | * @param string $url |
||
492 | * @param $data |
||
493 | * |
||
494 | * @return \stdClass |
||
495 | */ |
||
496 | public function postJSON($url, $data = null) |
||
497 | { |
||
498 | $responseBody = $this->post($url, json_encode($data), 'application/json'); |
||
499 | |||
500 | return json_decode($responseBody); |
||
501 | } |
||
502 | |||
503 | /** |
||
504 | * Make a POST request, sending XML data. |
||
505 | * |
||
506 | * @param string $url |
||
507 | * @param $data |
||
508 | * |
||
509 | * @return QuiteSimpleXMLElement |
||
510 | */ |
||
511 | public function postXML($url, $data = null) |
||
512 | { |
||
513 | $responseBody = $this->post($url, $data, 'application/xml'); |
||
514 | |||
515 | return new QuiteSimpleXMLElement($responseBody); |
||
516 | } |
||
517 | |||
518 | /** |
||
519 | * Get the redirect target location of an URL, or null if not a redirect. |
||
520 | * |
||
521 | * @param string $url |
||
522 | * @param array $query |
||
523 | * |
||
524 | * @return string|null |
||
525 | */ |
||
526 | public function getRedirectLocation($url, $query = []) |
||
527 | { |
||
528 | $url = $this->buildUrl($url, $query); |
||
529 | |||
530 | $request = $this->requestFactory->createRequest('GET', $url); |
||
531 | |||
532 | try { |
||
533 | $response = $this->request($request); |
||
534 | } catch (ResourceNotFound $e) { |
||
535 | return; |
||
536 | } |
||
537 | |||
538 | $locations = $response->getHeader('Location'); |
||
539 | |||
540 | return count($locations) ? $locations[0] : null; |
||
541 | } |
||
542 | |||
543 | /** |
||
544 | * @param class $className |
||
545 | * @param array ...$params |
||
546 | * |
||
547 | * @return mixed |
||
548 | */ |
||
549 | public function make($className, ...$params) |
||
550 | { |
||
551 | return new $className($this, ...$params); |
||
552 | } |
||
553 | |||
554 | /** |
||
555 | * Generate a client exception. |
||
556 | * |
||
557 | * @param HttpException $exception |
||
558 | * |
||
559 | * @return RequestFailed |
||
560 | */ |
||
561 | protected function parseClientError(HttpException $exception) |
||
562 | { |
||
563 | $contentType = explode(';', $exception->getResponse()->getHeaderLine('Content-Type'))[0]; |
||
564 | $responseBody = (string) $exception->getResponse()->getBody(); |
||
565 | |||
566 | switch ($contentType) { |
||
567 | case 'application/json': |
||
568 | $res = json_decode($responseBody, true); |
||
569 | if (isset($res['web_service_result'])) { |
||
570 | $res = $res['web_service_result']; |
||
571 | } |
||
572 | $err = isset($res['errorList']['error'][0]) ? $res['errorList']['error'][0] : $res['errorList']['error']; |
||
573 | $message = $err['errorMessage']; |
||
574 | $code = $err['errorCode']; |
||
575 | break; |
||
576 | |||
577 | case 'application/xml': |
||
578 | $xml = new QuiteSimpleXMLElement($responseBody); |
||
579 | $xml->registerXPathNamespace('xb', 'http://com/exlibris/urm/general/xmlbeans'); |
||
580 | |||
581 | $message = $xml->text('//xb:errorMessage'); |
||
582 | $code = $xml->text('//xb:errorCode'); |
||
583 | break; |
||
584 | |||
585 | default: |
||
586 | $message = $responseBody; |
||
587 | $code = ''; |
||
588 | } |
||
589 | |||
590 | // The error code is often an integer, but sometimes a string, |
||
591 | // so we generalize it as a string. |
||
592 | $code = empty($code) ? null : (string) $code; |
||
593 | |||
594 | if ($code == 'UNAUTHORIZED') { |
||
595 | return new InvalidApiKey($message, null, $exception); |
||
596 | } |
||
597 | |||
598 | if (strtolower($message) == 'invalid api key') { |
||
599 | return new InvalidApiKey($message, null, $exception); |
||
600 | } |
||
601 | |||
602 | if (preg_match('/(no items?|not) found/i', $message)) { |
||
603 | return new ResourceNotFound($message, $code, $exception); |
||
604 | } |
||
605 | |||
606 | return new RequestFailed($message, $code, $exception); |
||
607 | } |
||
608 | } |
||
609 |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.