[ Index ]

PHP Cross Reference of YOURLS

title

Body

[close]

/includes/vendor/maxmind/web-service-common/src/WebService/ -> Client.php (source)

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


Generated: Mon Jul 13 05:10:57 2026 Cross-referenced by PHPXref 0.7.1