[ Index ] |
PHP Cross Reference of YOURLS |
[Summary view] [Print] [Text view]
1 <?php 2 3 declare(strict_types=1); 4 5 namespace MaxMind\WebService; 6 7 use Composer\CaBundle\CaBundle; 8 use MaxMind\Exception\AuthenticationException; 9 use MaxMind\Exception\HttpException; 10 use MaxMind\Exception\InsufficientFundsException; 11 use MaxMind\Exception\InvalidInputException; 12 use MaxMind\Exception\InvalidRequestException; 13 use MaxMind\Exception\IpAddressNotFoundException; 14 use MaxMind\Exception\PermissionRequiredException; 15 use MaxMind\Exception\WebServiceException; 16 use MaxMind\WebService\Http\RequestFactory; 17 18 /** 19 * This class is not intended to be used directly by an end-user of a 20 * MaxMind web service. Please use the appropriate client API for the service 21 * that you are using. 22 * 23 * @internal 24 */ 25 class Client 26 { 27 public const VERSION = '0.2.0'; 28 29 /** 30 * @var string|null 31 */ 32 private $caBundle; 33 34 /** 35 * @var float|null 36 */ 37 private $connectTimeout; 38 39 /** 40 * @var string 41 */ 42 private $host = 'api.maxmind.com'; 43 44 /** 45 * @var bool 46 */ 47 private $useHttps = true; 48 49 /** 50 * @var RequestFactory 51 */ 52 private $httpRequestFactory; 53 54 /** 55 * @var string 56 */ 57 private $licenseKey; 58 59 /** 60 * @var string|null 61 */ 62 private $proxy; 63 64 /** 65 * @var float|null 66 */ 67 private $timeout; 68 69 /** 70 * @var string 71 */ 72 private $userAgentPrefix; 73 74 /** 75 * @var int 76 */ 77 private $accountId; 78 79 /** 80 * @param int $accountId your MaxMind account ID 81 * @param string $licenseKey your MaxMind license key 82 * @param array $options an array of options. Possible keys: 83 * * `host` - The host to use when connecting to the web service. 84 * * `useHttps` - A boolean flag for sending the request via https.(True by default) 85 * * `userAgent` - The prefix of the User-Agent to use in the request. 86 * * `caBundle` - The bundle of CA root certificates to use in the request. 87 * * `connectTimeout` - The connect timeout to use for the request. 88 * * `timeout` - The timeout to use for the request. 89 * * `proxy` - The HTTP proxy to use. May include a schema, port, 90 * username, and password, e.g., `http://username:[email protected]:10`. 91 */ 92 public function __construct( 93 int $accountId, 94 string $licenseKey, 95 array $options = [] 96 ) { 97 $this->accountId = $accountId; 98 $this->licenseKey = $licenseKey; 99 100 $this->httpRequestFactory = isset($options['httpRequestFactory']) 101 ? $options['httpRequestFactory'] 102 : new RequestFactory(); 103 104 if (isset($options['host'])) { 105 $this->host = $options['host']; 106 } 107 if (isset($options['useHttps'])) { 108 $this->useHttps = $options['useHttps']; 109 } 110 if (isset($options['userAgent'])) { 111 $this->userAgentPrefix = $options['userAgent'] . ' '; 112 } 113 114 $this->caBundle = isset($options['caBundle']) ? 115 $this->caBundle = $options['caBundle'] : $this->getCaBundle(); 116 117 if (isset($options['connectTimeout'])) { 118 $this->connectTimeout = $options['connectTimeout']; 119 } 120 if (isset($options['timeout'])) { 121 $this->timeout = $options['timeout']; 122 } 123 124 if (isset($options['proxy'])) { 125 $this->proxy = $options['proxy']; 126 } 127 } 128 129 /** 130 * @param string $service name of the service querying 131 * @param string $path the URI path to use 132 * @param array $input the data to be posted as JSON 133 * 134 * @throws InvalidInputException when the request has missing or invalid 135 * data 136 * @throws AuthenticationException when there is an issue authenticating the 137 * request 138 * @throws InsufficientFundsException when your account is out of funds 139 * @throws InvalidRequestException when the request is invalid for some 140 * other reason, e.g., invalid JSON in the POST. 141 * @throws HttpException when an unexpected HTTP error occurs 142 * @throws WebServiceException when some other error occurs. This also 143 * serves as the base class for the above exceptions. 144 * 145 * @return array|null The decoded content of a successful response 146 */ 147 public function post(string $service, string $path, array $input): ?array 148 { 149 $requestBody = json_encode($input); 150 if ($requestBody === false) { 151 throw new InvalidInputException( 152 'Error encoding input as JSON: ' 153 . $this->jsonErrorDescription() 154 ); 155 } 156 157 $request = $this->createRequest( 158 $path, 159 ['Content-Type: application/json'] 160 ); 161 162 [$statusCode, $contentType, $responseBody] = $request->post($requestBody); 163 164 return $this->handleResponse( 165 $statusCode, 166 $contentType, 167 $responseBody, 168 $service, 169 $path 170 ); 171 } 172 173 public function get(string $service, string $path): ?array 174 { 175 $request = $this->createRequest( 176 $path 177 ); 178 179 [$statusCode, $contentType, $responseBody] = $request->get(); 180 181 return $this->handleResponse( 182 $statusCode, 183 $contentType, 184 $responseBody, 185 $service, 186 $path 187 ); 188 } 189 190 private function userAgent(): string 191 { 192 $curlVersion = curl_version(); 193 194 return $this->userAgentPrefix . 'MaxMind-WS-API/' . self::VERSION . ' PHP/' . \PHP_VERSION . 195 ' curl/' . $curlVersion['version']; 196 } 197 198 private function createRequest(string $path, array $headers = []): Http\Request 199 { 200 array_push( 201 $headers, 202 'Authorization: Basic ' 203 . base64_encode($this->accountId . ':' . $this->licenseKey), 204 'Accept: application/json' 205 ); 206 207 return $this->httpRequestFactory->request( 208 $this->urlFor($path), 209 [ 210 'caBundle' => $this->caBundle, 211 'connectTimeout' => $this->connectTimeout, 212 'headers' => $headers, 213 'proxy' => $this->proxy, 214 'timeout' => $this->timeout, 215 'userAgent' => $this->userAgent(), 216 ] 217 ); 218 } 219 220 /** 221 * @param int $statusCode the HTTP status code of the response 222 * @param string|null $contentType the Content-Type of the response 223 * @param string|null $responseBody the response body 224 * @param string $service the name of the service 225 * @param string $path the path used in the request 226 * 227 * @throws AuthenticationException when there is an issue authenticating the 228 * request 229 * @throws InsufficientFundsException when your account is out of funds 230 * @throws InvalidRequestException when the request is invalid for some 231 * other reason, e.g., invalid JSON in the POST. 232 * @throws HttpException when an unexpected HTTP error occurs 233 * @throws WebServiceException when some other error occurs. This also 234 * serves as the base class for the above exceptions 235 * 236 * @return array|null The decoded content of a successful response 237 */ 238 private function handleResponse( 239 int $statusCode, 240 ?string $contentType, 241 ?string $responseBody, 242 string $service, 243 string $path 244 ): ?array { 245 if ($statusCode >= 400 && $statusCode <= 499) { 246 $this->handle4xx($statusCode, $contentType, $responseBody, $service, $path); 247 } elseif ($statusCode >= 500) { 248 $this->handle5xx($statusCode, $service, $path); 249 } elseif ($statusCode !== 200 && $statusCode !== 204) { 250 $this->handleUnexpectedStatus($statusCode, $service, $path); 251 } 252 253 return $this->handleSuccess($statusCode, $responseBody, $service); 254 } 255 256 /** 257 * @return string describing the JSON error 258 */ 259 private function jsonErrorDescription(): string 260 { 261 $errno = json_last_error(); 262 263 switch ($errno) { 264 case \JSON_ERROR_DEPTH: 265 return 'The maximum stack depth has been exceeded.'; 266 267 case \JSON_ERROR_STATE_MISMATCH: 268 return 'Invalid or malformed JSON.'; 269 270 case \JSON_ERROR_CTRL_CHAR: 271 return 'Control character error.'; 272 273 case \JSON_ERROR_SYNTAX: 274 return 'Syntax error.'; 275 276 case \JSON_ERROR_UTF8: 277 return 'Malformed UTF-8 characters.'; 278 279 default: 280 return "Other JSON error ($errno)."; 281 } 282 } 283 284 /** 285 * @param string $path the path to use in the URL 286 * 287 * @return string the constructed URL 288 */ 289 private function urlFor(string $path): string 290 { 291 return ($this->useHttps ? 'https://' : 'http://') . $this->host . $path; 292 } 293 294 /** 295 * @param int $statusCode the HTTP status code 296 * @param string|null $contentType the response content-type 297 * @param string|null $body the response body 298 * @param string $service the service name 299 * @param string $path the path used in the request 300 * 301 * @throws AuthenticationException 302 * @throws HttpException 303 * @throws InsufficientFundsException 304 * @throws InvalidRequestException 305 */ 306 private function handle4xx( 307 int $statusCode, 308 ?string $contentType, 309 ?string $body, 310 string $service, 311 string $path 312 ): void { 313 if ($body === null || $body === '') { 314 throw new HttpException( 315 "Received a $statusCode error for $service with no body", 316 $statusCode, 317 $this->urlFor($path) 318 ); 319 } 320 if ($contentType === null || !strstr($contentType, 'json')) { 321 throw new HttpException( 322 "Received a $statusCode error for $service with " . 323 'the following body: ' . $body, 324 $statusCode, 325 $this->urlFor($path) 326 ); 327 } 328 329 $message = json_decode($body, true); 330 if ($message === null) { 331 throw new HttpException( 332 "Received a $statusCode error for $service but could " . 333 'not decode the response as JSON: ' 334 . $this->jsonErrorDescription() . ' Body: ' . $body, 335 $statusCode, 336 $this->urlFor($path) 337 ); 338 } 339 340 if (!isset($message['code']) || !isset($message['error'])) { 341 throw new HttpException( 342 'Error response contains JSON but it does not ' . 343 'specify code or error keys: ' . $body, 344 $statusCode, 345 $this->urlFor($path) 346 ); 347 } 348 349 $this->handleWebServiceError( 350 $message['error'], 351 $message['code'], 352 $statusCode, 353 $path 354 ); 355 } 356 357 /** 358 * @param string $message the error message from the web service 359 * @param string $code the error code from the web service 360 * @param int $statusCode the HTTP status code 361 * @param string $path the path used in the request 362 * 363 * @throws AuthenticationException 364 * @throws InvalidRequestException 365 * @throws InsufficientFundsException 366 */ 367 private function handleWebServiceError( 368 string $message, 369 string $code, 370 int $statusCode, 371 string $path 372 ): void { 373 switch ($code) { 374 case 'IP_ADDRESS_NOT_FOUND': 375 case 'IP_ADDRESS_RESERVED': 376 throw new IpAddressNotFoundException( 377 $message, 378 $code, 379 $statusCode, 380 $this->urlFor($path) 381 ); 382 383 case 'ACCOUNT_ID_REQUIRED': 384 case 'ACCOUNT_ID_UNKNOWN': 385 case 'AUTHORIZATION_INVALID': 386 case 'LICENSE_KEY_REQUIRED': 387 case 'USER_ID_REQUIRED': 388 case 'USER_ID_UNKNOWN': 389 throw new AuthenticationException( 390 $message, 391 $code, 392 $statusCode, 393 $this->urlFor($path) 394 ); 395 396 case 'OUT_OF_QUERIES': 397 case 'INSUFFICIENT_FUNDS': 398 throw new InsufficientFundsException( 399 $message, 400 $code, 401 $statusCode, 402 $this->urlFor($path) 403 ); 404 405 case 'PERMISSION_REQUIRED': 406 throw new PermissionRequiredException( 407 $message, 408 $code, 409 $statusCode, 410 $this->urlFor($path) 411 ); 412 413 default: 414 throw new InvalidRequestException( 415 $message, 416 $code, 417 $statusCode, 418 $this->urlFor($path) 419 ); 420 } 421 } 422 423 /** 424 * @param int $statusCode the HTTP status code 425 * @param string $service the service name 426 * @param string $path the URI path used in the request 427 * 428 * @throws HttpException 429 */ 430 private function handle5xx(int $statusCode, string $service, string $path): void 431 { 432 throw new HttpException( 433 "Received a server error ($statusCode) for $service", 434 $statusCode, 435 $this->urlFor($path) 436 ); 437 } 438 439 /** 440 * @param int $statusCode the HTTP status code 441 * @param string $service the service name 442 * @param string $path the URI path used in the request 443 * 444 * @throws HttpException 445 */ 446 private function handleUnexpectedStatus(int $statusCode, string $service, string $path): void 447 { 448 throw new HttpException( 449 'Received an unexpected HTTP status ' . 450 "($statusCode) for $service", 451 $statusCode, 452 $this->urlFor($path) 453 ); 454 } 455 456 /** 457 * @param int $statusCode the HTTP status code 458 * @param string|null $body the successful request body 459 * @param string $service the service name 460 * 461 * @throws WebServiceException if a response body is included but not 462 * expected, or is not expected but not 463 * included, or is expected and included 464 * but cannot be decoded as JSON 465 * 466 * @return array|null the decoded request body 467 */ 468 private function handleSuccess(int $statusCode, ?string $body, string $service): ?array 469 { 470 // A 204 should have no response body 471 if ($statusCode === 204) { 472 if ($body !== null && $body !== '') { 473 throw new WebServiceException( 474 "Received a 204 response for $service along with an " . 475 "unexpected HTTP body: $body" 476 ); 477 } 478 479 return null; 480 } 481 482 // A 200 should have a valid JSON body 483 if ($body === null || $body === '') { 484 throw new WebServiceException( 485 "Received a 200 response for $service but did not " . 486 'receive a HTTP body.' 487 ); 488 } 489 490 $decodedContent = json_decode($body, true); 491 if ($decodedContent === null) { 492 throw new WebServiceException( 493 "Received a 200 response for $service but could " . 494 'not decode the response as JSON: ' 495 . $this->jsonErrorDescription() . ' Body: ' . $body 496 ); 497 } 498 499 return $decodedContent; 500 } 501 502 private function getCaBundle(): ?string 503 { 504 $curlVersion = curl_version(); 505 506 // On OS X, when the SSL version is "SecureTransport", the system's 507 // keychain will be used. 508 if ($curlVersion['ssl_version'] === 'SecureTransport') { 509 return null; 510 } 511 $cert = CaBundle::getSystemCaRootBundlePath(); 512 513 // Check if the cert is inside a phar. If so, we need to copy the cert 514 // to a temp file so that curl can see it. 515 if (substr($cert, 0, 7) === 'phar://') { 516 $tempDir = sys_get_temp_dir(); 517 $newCert = tempnam($tempDir, 'geoip2-'); 518 if ($newCert === false) { 519 throw new \RuntimeException( 520 "Unable to create temporary file in $tempDir" 521 ); 522 } 523 if (!copy($cert, $newCert)) { 524 throw new \RuntimeException( 525 "Could not copy $cert to $newCert: " 526 . var_export(error_get_last(), true) 527 ); 528 } 529 530 // We use a shutdown function rather than the destructor as the 531 // destructor isn't called on a fatal error such as an uncaught 532 // exception. 533 register_shutdown_function( 534 function () use ($newCert) { 535 unlink($newCert); 536 } 537 ); 538 $cert = $newCert; 539 } 540 if (!file_exists($cert)) { 541 throw new \RuntimeException("CA cert does not exist at $cert"); 542 } 543 544 return $cert; 545 } 546 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Sep 19 05:10:04 2024 | Cross-referenced by PHPXref 0.7.1 |