[ 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      /**
  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<string, mixed> $options    an array of options. Possible keys:
  83       *                                         * `host` - The host to use when connecting to the web service.
  84       *                                         * `useHttps` - Set to false to disable HTTPS.
  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<mixed> $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<mixed>|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      /**
 174       * @return array<mixed>|null
 175       */
 176      public function get(string $service, string $path): ?array
 177      {
 178          $request = $this->createRequest(
 179              $path
 180          );
 181  
 182          [$statusCode, $contentType, $responseBody] = $request->get();
 183  
 184          return $this->handleResponse(
 185              $statusCode,
 186              $contentType,
 187              $responseBody,
 188              $service,
 189              $path
 190          );
 191      }
 192  
 193      private function userAgent(): string
 194      {
 195          $curlVersion = curl_version();
 196  
 197          return $this->userAgentPrefix . 'MaxMind-WS-API/' . self::VERSION . ' PHP/' . \PHP_VERSION .
 198             ' curl/' . $curlVersion['version'];
 199      }
 200  
 201      /**
 202       * @param array<string> $headers
 203       */
 204      private function createRequest(string $path, array $headers = []): Http\Request
 205      {
 206          array_push(
 207              $headers,
 208              'Authorization: Basic '
 209              . base64_encode($this->accountId . ':' . $this->licenseKey),
 210              'Accept: application/json'
 211          );
 212  
 213          return $this->httpRequestFactory->request(
 214              $this->urlFor($path),
 215              [
 216                  'caBundle' => $this->caBundle,
 217                  'connectTimeout' => $this->connectTimeout,
 218                  'headers' => $headers,
 219                  'proxy' => $this->proxy,
 220                  'timeout' => $this->timeout,
 221                  'userAgent' => $this->userAgent(),
 222              ]
 223          );
 224      }
 225  
 226      /**
 227       * @param int         $statusCode   the HTTP status code of the response
 228       * @param string|null $contentType  the Content-Type of the response
 229       * @param string|null $responseBody the response body
 230       * @param string      $service      the name of the service
 231       * @param string      $path         the path used in the request
 232       *
 233       * @throws AuthenticationException    when there is an issue authenticating the
 234       *                                    request
 235       * @throws InsufficientFundsException when your account is out of funds
 236       * @throws InvalidRequestException    when the request is invalid for some
 237       *                                    other reason, e.g., invalid JSON in the POST.
 238       * @throws HttpException              when an unexpected HTTP error occurs
 239       * @throws WebServiceException        when some other error occurs. This also
 240       *                                    serves as the base class for the above exceptions
 241       *
 242       * @return array<mixed>|null The decoded content of a successful response
 243       */
 244      private function handleResponse(
 245          int $statusCode,
 246          ?string $contentType,
 247          ?string $responseBody,
 248          string $service,
 249          string $path
 250      ): ?array {
 251          if ($statusCode >= 400 && $statusCode <= 499) {
 252              $this->handle4xx($statusCode, $contentType, $responseBody, $service, $path);
 253          } elseif ($statusCode >= 500) {
 254              $this->handle5xx($statusCode, $service, $path);
 255          } elseif ($statusCode !== 200 && $statusCode !== 204) {
 256              $this->handleUnexpectedStatus($statusCode, $service, $path);
 257          }
 258  
 259          return $this->handleSuccess($statusCode, $responseBody, $service);
 260      }
 261  
 262      /**
 263       * @return string describing the JSON error
 264       */
 265      private function jsonErrorDescription(): string
 266      {
 267          $errno = json_last_error();
 268  
 269          switch ($errno) {
 270              case \JSON_ERROR_DEPTH:
 271                  return 'The maximum stack depth has been exceeded.';
 272  
 273              case \JSON_ERROR_STATE_MISMATCH:
 274                  return 'Invalid or malformed JSON.';
 275  
 276              case \JSON_ERROR_CTRL_CHAR:
 277                  return 'Control character error.';
 278  
 279              case \JSON_ERROR_SYNTAX:
 280                  return 'Syntax error.';
 281  
 282              case \JSON_ERROR_UTF8:
 283                  return 'Malformed UTF-8 characters.';
 284  
 285              default:
 286                  return "Other JSON error ($errno).";
 287          }
 288      }
 289  
 290      /**
 291       * @param string $path the path to use in the URL
 292       *
 293       * @return string the constructed URL
 294       */
 295      private function urlFor(string $path): string
 296      {
 297          return ($this->useHttps ? 'https://' : 'http://') . $this->host . $path;
 298      }
 299  
 300      /**
 301       * @param int         $statusCode  the HTTP status code
 302       * @param string|null $contentType the response content-type
 303       * @param string|null $body        the response body
 304       * @param string      $service     the service name
 305       * @param string      $path        the path used in the request
 306       *
 307       * @throws AuthenticationException
 308       * @throws HttpException
 309       * @throws InsufficientFundsException
 310       * @throws InvalidRequestException
 311       */
 312      private function handle4xx(
 313          int $statusCode,
 314          ?string $contentType,
 315          ?string $body,
 316          string $service,
 317          string $path
 318      ): void {
 319          if ($body === null || $body === '') {
 320              throw new HttpException(
 321                  "Received a $statusCode error for $service with no body",
 322                  $statusCode,
 323                  $this->urlFor($path)
 324              );
 325          }
 326          if ($contentType === null || !strstr($contentType, 'json')) {
 327              throw new HttpException(
 328                  "Received a $statusCode error for $service with " .
 329                  'the following body: ' . $body,
 330                  $statusCode,
 331                  $this->urlFor($path)
 332              );
 333          }
 334  
 335          $message = json_decode($body, true);
 336          if ($message === null) {
 337              throw new HttpException(
 338                  "Received a $statusCode error for $service but could " .
 339                  'not decode the response as JSON: '
 340                  . $this->jsonErrorDescription() . ' Body: ' . $body,
 341                  $statusCode,
 342                  $this->urlFor($path)
 343              );
 344          }
 345  
 346          if (!isset($message['code']) || !isset($message['error'])) {
 347              throw new HttpException(
 348                  'Error response contains JSON but it does not ' .
 349                  'specify code or error keys: ' . $body,
 350                  $statusCode,
 351                  $this->urlFor($path)
 352              );
 353          }
 354  
 355          $this->handleWebServiceError(
 356              $message['error'],
 357              $message['code'],
 358              $statusCode,
 359              $path
 360          );
 361      }
 362  
 363      /**
 364       * @param string $message    the error message from the web service
 365       * @param string $code       the error code from the web service
 366       * @param int    $statusCode the HTTP status code
 367       * @param string $path       the path used in the request
 368       *
 369       * @throws AuthenticationException
 370       * @throws InvalidRequestException
 371       * @throws InsufficientFundsException
 372       */
 373      private function handleWebServiceError(
 374          string $message,
 375          string $code,
 376          int $statusCode,
 377          string $path
 378      ): void {
 379          switch ($code) {
 380              case 'IP_ADDRESS_NOT_FOUND':
 381              case 'IP_ADDRESS_RESERVED':
 382                  throw new IpAddressNotFoundException(
 383                      $message,
 384                      $code,
 385                      $statusCode,
 386                      $this->urlFor($path)
 387                  );
 388  
 389              case 'ACCOUNT_ID_REQUIRED':
 390              case 'ACCOUNT_ID_UNKNOWN':
 391              case 'AUTHORIZATION_INVALID':
 392              case 'LICENSE_KEY_REQUIRED':
 393              case 'USER_ID_REQUIRED':
 394              case 'USER_ID_UNKNOWN':
 395                  throw new AuthenticationException(
 396                      $message,
 397                      $code,
 398                      $statusCode,
 399                      $this->urlFor($path)
 400                  );
 401  
 402              case 'OUT_OF_QUERIES':
 403              case 'INSUFFICIENT_FUNDS':
 404                  throw new InsufficientFundsException(
 405                      $message,
 406                      $code,
 407                      $statusCode,
 408                      $this->urlFor($path)
 409                  );
 410  
 411              case 'PERMISSION_REQUIRED':
 412                  throw new PermissionRequiredException(
 413                      $message,
 414                      $code,
 415                      $statusCode,
 416                      $this->urlFor($path)
 417                  );
 418  
 419              default:
 420                  throw new InvalidRequestException(
 421                      $message,
 422                      $code,
 423                      $statusCode,
 424                      $this->urlFor($path)
 425                  );
 426          }
 427      }
 428  
 429      /**
 430       * @param int    $statusCode the HTTP status code
 431       * @param string $service    the service name
 432       * @param string $path       the URI path used in the request
 433       *
 434       * @throws HttpException
 435       */
 436      private function handle5xx(int $statusCode, string $service, string $path): void
 437      {
 438          throw new HttpException(
 439              "Received a server error ($statusCode) for $service",
 440              $statusCode,
 441              $this->urlFor($path)
 442          );
 443      }
 444  
 445      /**
 446       * @param int    $statusCode the HTTP status code
 447       * @param string $service    the service name
 448       * @param string $path       the URI path used in the request
 449       *
 450       * @throws HttpException
 451       */
 452      private function handleUnexpectedStatus(int $statusCode, string $service, string $path): void
 453      {
 454          throw new HttpException(
 455              'Received an unexpected HTTP status ' .
 456              "($statusCode) for $service",
 457              $statusCode,
 458              $this->urlFor($path)
 459          );
 460      }
 461  
 462      /**
 463       * @param int         $statusCode the HTTP status code
 464       * @param string|null $body       the successful request body
 465       * @param string      $service    the service name
 466       *
 467       * @throws WebServiceException if a response body is included but not
 468       *                             expected, or is not expected but not
 469       *                             included, or is expected and included
 470       *                             but cannot be decoded as JSON
 471       *
 472       * @return array<mixed>|null the decoded request body
 473       */
 474      private function handleSuccess(int $statusCode, ?string $body, string $service): ?array
 475      {
 476          // A 204 should have no response body
 477          if ($statusCode === 204) {
 478              if ($body !== null && $body !== '') {
 479                  throw new WebServiceException(
 480                      "Received a 204 response for $service along with an " .
 481                      "unexpected HTTP body: $body"
 482                  );
 483              }
 484  
 485              return null;
 486          }
 487  
 488          // A 200 should have a valid JSON body
 489          if ($body === null || $body === '') {
 490              throw new WebServiceException(
 491                  "Received a 200 response for $service but did not " .
 492                  'receive a HTTP body.'
 493              );
 494          }
 495  
 496          $decodedContent = json_decode($body, true);
 497          if ($decodedContent === null) {
 498              throw new WebServiceException(
 499                  "Received a 200 response for $service but could " .
 500                  'not decode the response as JSON: '
 501                  . $this->jsonErrorDescription() . ' Body: ' . $body
 502              );
 503          }
 504  
 505          return $decodedContent;
 506      }
 507  
 508      private function getCaBundle(): ?string
 509      {
 510          $curlVersion = curl_version();
 511  
 512          // On OS X, when the SSL version is "SecureTransport", the system's
 513          // keychain will be used.
 514          if ($curlVersion['ssl_version'] === 'SecureTransport') {
 515              return null;
 516          }
 517          $cert = CaBundle::getSystemCaRootBundlePath();
 518  
 519          // Check if the cert is inside a phar. If so, we need to copy the cert
 520          // to a temp file so that curl can see it.
 521          if (substr($cert, 0, 7) === 'phar://') {
 522              $tempDir = sys_get_temp_dir();
 523              $newCert = tempnam($tempDir, 'geoip2-');
 524              if ($newCert === false) {
 525                  throw new \RuntimeException(
 526                      "Unable to create temporary file in $tempDir"
 527                  );
 528              }
 529              if (!copy($cert, $newCert)) {
 530                  throw new \RuntimeException(
 531                      "Could not copy $cert to $newCert: "
 532                      . var_export(error_get_last(), true)
 533                  );
 534              }
 535  
 536              // We use a shutdown function rather than the destructor as the
 537              // destructor isn't called on a fatal error such as an uncaught
 538              // exception.
 539              register_shutdown_function(
 540                  function () use ($newCert) {
 541                      unlink($newCert);
 542                  }
 543              );
 544              $cert = $newCert;
 545          }
 546          if (!file_exists($cert)) {
 547              throw new \RuntimeException("CA cert does not exist at $cert");
 548          }
 549  
 550          return $cert;
 551      }
 552  }


Generated: Mon Mar 31 05:10:02 2025 Cross-referenced by PHPXref 0.7.1