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(); |
||
0 ignored issues
–
show
|
|||
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 |
||
0 ignored issues
–
show
The property
libraries does not seem to exist in Scriptotek\Alma\Conf\Conf .
An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name. If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading. ![]() |
|||
160 | $this->jobs = $this->conf->jobs; // shortcut |
||
0 ignored issues
–
show
The property
jobs does not seem to exist in Scriptotek\Alma\Conf\Conf .
An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name. If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading. ![]() |
|||
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') |
|
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') |
|
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 |
Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.
Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..