[ 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  $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  }


Generated: Thu Sep 19 05:10:04 2024 Cross-referenced by PHPXref 0.7.1