PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.12.1
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.12.1
5.12.1 5.12.0 5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / vendor / prefixed / maxmind / web-service-common / src / WebService / Client.php
matomo / app / vendor / prefixed / maxmind / web-service-common / src / WebService Last commit date
Http 1 year ago Client.php 1 year ago
Client.php
355 lines
1 <?php
2
3 declare (strict_types=1);
4 namespace Matomo\Dependencies\MaxMind\WebService;
5
6 use Composer\CaBundle\CaBundle;
7 use Matomo\Dependencies\MaxMind\Exception\AuthenticationException;
8 use Matomo\Dependencies\MaxMind\Exception\HttpException;
9 use Matomo\Dependencies\MaxMind\Exception\InsufficientFundsException;
10 use Matomo\Dependencies\MaxMind\Exception\InvalidInputException;
11 use Matomo\Dependencies\MaxMind\Exception\InvalidRequestException;
12 use Matomo\Dependencies\MaxMind\Exception\IpAddressNotFoundException;
13 use Matomo\Dependencies\MaxMind\Exception\PermissionRequiredException;
14 use Matomo\Dependencies\MaxMind\Exception\WebServiceException;
15 use Matomo\Dependencies\MaxMind\WebService\Http\RequestFactory;
16 /**
17 * This class is not intended to be used directly by an end-user of a
18 * MaxMind web service. Please use the appropriate client API for the service
19 * that you are using.
20 *
21 * @internal
22 */
23 class Client
24 {
25 public const VERSION = '0.2.0';
26 /**
27 * @var string|null
28 */
29 private $caBundle;
30 /**
31 * @var float|null
32 */
33 private $connectTimeout;
34 /**
35 * @var string
36 */
37 private $host = 'api.maxmind.com';
38 /**
39 * @var bool
40 */
41 private $useHttps = \true;
42 /**
43 * @var RequestFactory
44 */
45 private $httpRequestFactory;
46 /**
47 * @var string
48 */
49 private $licenseKey;
50 /**
51 * @var string|null
52 */
53 private $proxy;
54 /**
55 * @var float|null
56 */
57 private $timeout;
58 /**
59 * @var string
60 */
61 private $userAgentPrefix;
62 /**
63 * @var int
64 */
65 private $accountId;
66 /**
67 * @param int $accountId your MaxMind account ID
68 * @param string $licenseKey your MaxMind license key
69 * @param array $options an array of options. Possible keys:
70 * * `host` - The host to use when connecting to the web service.
71 * * `useHttps` - A boolean flag for sending the request via https.(True by default)
72 * * `userAgent` - The prefix of the User-Agent to use in the request.
73 * * `caBundle` - The bundle of CA root certificates to use in the request.
74 * * `connectTimeout` - The connect timeout to use for the request.
75 * * `timeout` - The timeout to use for the request.
76 * * `proxy` - The HTTP proxy to use. May include a schema, port,
77 * username, and password, e.g., `http://username:password@127.0.0.1:10`.
78 */
79 public function __construct(int $accountId, string $licenseKey, array $options = [])
80 {
81 $this->accountId = $accountId;
82 $this->licenseKey = $licenseKey;
83 $this->httpRequestFactory = isset($options['httpRequestFactory']) ? $options['httpRequestFactory'] : new RequestFactory();
84 if (isset($options['host'])) {
85 $this->host = $options['host'];
86 }
87 if (isset($options['useHttps'])) {
88 $this->useHttps = $options['useHttps'];
89 }
90 if (isset($options['userAgent'])) {
91 $this->userAgentPrefix = $options['userAgent'] . ' ';
92 }
93 $this->caBundle = isset($options['caBundle']) ? $this->caBundle = $options['caBundle'] : $this->getCaBundle();
94 if (isset($options['connectTimeout'])) {
95 $this->connectTimeout = $options['connectTimeout'];
96 }
97 if (isset($options['timeout'])) {
98 $this->timeout = $options['timeout'];
99 }
100 if (isset($options['proxy'])) {
101 $this->proxy = $options['proxy'];
102 }
103 }
104 /**
105 * @param string $service name of the service querying
106 * @param string $path the URI path to use
107 * @param array $input the data to be posted as JSON
108 *
109 * @throws InvalidInputException when the request has missing or invalid
110 * data
111 * @throws AuthenticationException when there is an issue authenticating the
112 * request
113 * @throws InsufficientFundsException when your account is out of funds
114 * @throws InvalidRequestException when the request is invalid for some
115 * other reason, e.g., invalid JSON in the POST.
116 * @throws HttpException when an unexpected HTTP error occurs
117 * @throws WebServiceException when some other error occurs. This also
118 * serves as the base class for the above exceptions.
119 *
120 * @return array|null The decoded content of a successful response
121 */
122 public function post(string $service, string $path, array $input) : ?array
123 {
124 $requestBody = json_encode($input);
125 if ($requestBody === \false) {
126 throw new InvalidInputException('Error encoding input as JSON: ' . $this->jsonErrorDescription());
127 }
128 $request = $this->createRequest($path, ['Content-Type: application/json']);
129 [$statusCode, $contentType, $responseBody] = $request->post($requestBody);
130 return $this->handleResponse($statusCode, $contentType, $responseBody, $service, $path);
131 }
132 public function get(string $service, string $path) : ?array
133 {
134 $request = $this->createRequest($path);
135 [$statusCode, $contentType, $responseBody] = $request->get();
136 return $this->handleResponse($statusCode, $contentType, $responseBody, $service, $path);
137 }
138 private function userAgent() : string
139 {
140 $curlVersion = curl_version();
141 return $this->userAgentPrefix . 'MaxMind-WS-API/' . self::VERSION . ' PHP/' . \PHP_VERSION . ' curl/' . $curlVersion['version'];
142 }
143 private function createRequest(string $path, array $headers = []) : Http\Request
144 {
145 array_push($headers, 'Authorization: Basic ' . base64_encode($this->accountId . ':' . $this->licenseKey), 'Accept: application/json');
146 return $this->httpRequestFactory->request($this->urlFor($path), ['caBundle' => $this->caBundle, 'connectTimeout' => $this->connectTimeout, 'headers' => $headers, 'proxy' => $this->proxy, 'timeout' => $this->timeout, 'userAgent' => $this->userAgent()]);
147 }
148 /**
149 * @param int $statusCode the HTTP status code of the response
150 * @param string|null $contentType the Content-Type of the response
151 * @param string|null $responseBody the response body
152 * @param string $service the name of the service
153 * @param string $path the path used in the request
154 *
155 * @throws AuthenticationException when there is an issue authenticating the
156 * request
157 * @throws InsufficientFundsException when your account is out of funds
158 * @throws InvalidRequestException when the request is invalid for some
159 * other reason, e.g., invalid JSON in the POST.
160 * @throws HttpException when an unexpected HTTP error occurs
161 * @throws WebServiceException when some other error occurs. This also
162 * serves as the base class for the above exceptions
163 *
164 * @return array|null The decoded content of a successful response
165 */
166 private function handleResponse(int $statusCode, ?string $contentType, ?string $responseBody, string $service, string $path) : ?array
167 {
168 if ($statusCode >= 400 && $statusCode <= 499) {
169 $this->handle4xx($statusCode, $contentType, $responseBody, $service, $path);
170 } elseif ($statusCode >= 500) {
171 $this->handle5xx($statusCode, $service, $path);
172 } elseif ($statusCode !== 200 && $statusCode !== 204) {
173 $this->handleUnexpectedStatus($statusCode, $service, $path);
174 }
175 return $this->handleSuccess($statusCode, $responseBody, $service);
176 }
177 /**
178 * @return string describing the JSON error
179 */
180 private function jsonErrorDescription() : string
181 {
182 $errno = json_last_error();
183 switch ($errno) {
184 case \JSON_ERROR_DEPTH:
185 return 'The maximum stack depth has been exceeded.';
186 case \JSON_ERROR_STATE_MISMATCH:
187 return 'Invalid or malformed JSON.';
188 case \JSON_ERROR_CTRL_CHAR:
189 return 'Control character error.';
190 case \JSON_ERROR_SYNTAX:
191 return 'Syntax error.';
192 case \JSON_ERROR_UTF8:
193 return 'Malformed UTF-8 characters.';
194 default:
195 return "Other JSON error ({$errno}).";
196 }
197 }
198 /**
199 * @param string $path the path to use in the URL
200 *
201 * @return string the constructed URL
202 */
203 private function urlFor(string $path) : string
204 {
205 return ($this->useHttps ? 'https://' : 'http://') . $this->host . $path;
206 }
207 /**
208 * @param int $statusCode the HTTP status code
209 * @param string|null $contentType the response content-type
210 * @param string|null $body the response body
211 * @param string $service the service name
212 * @param string $path the path used in the request
213 *
214 * @throws AuthenticationException
215 * @throws HttpException
216 * @throws InsufficientFundsException
217 * @throws InvalidRequestException
218 */
219 private function handle4xx(int $statusCode, ?string $contentType, ?string $body, string $service, string $path) : void
220 {
221 if ($body === null || $body === '') {
222 throw new HttpException("Received a {$statusCode} error for {$service} with no body", $statusCode, $this->urlFor($path));
223 }
224 if ($contentType === null || !strstr($contentType, 'json')) {
225 throw new HttpException("Received a {$statusCode} error for {$service} with " . 'the following body: ' . $body, $statusCode, $this->urlFor($path));
226 }
227 $message = json_decode($body, \true);
228 if ($message === null) {
229 throw new HttpException("Received a {$statusCode} error for {$service} but could " . 'not decode the response as JSON: ' . $this->jsonErrorDescription() . ' Body: ' . $body, $statusCode, $this->urlFor($path));
230 }
231 if (!isset($message['code']) || !isset($message['error'])) {
232 throw new HttpException('Error response contains JSON but it does not ' . 'specify code or error keys: ' . $body, $statusCode, $this->urlFor($path));
233 }
234 $this->handleWebServiceError($message['error'], $message['code'], $statusCode, $path);
235 }
236 /**
237 * @param string $message the error message from the web service
238 * @param string $code the error code from the web service
239 * @param int $statusCode the HTTP status code
240 * @param string $path the path used in the request
241 *
242 * @throws AuthenticationException
243 * @throws InvalidRequestException
244 * @throws InsufficientFundsException
245 */
246 private function handleWebServiceError(string $message, string $code, int $statusCode, string $path) : void
247 {
248 switch ($code) {
249 case 'IP_ADDRESS_NOT_FOUND':
250 case 'IP_ADDRESS_RESERVED':
251 throw new IpAddressNotFoundException($message, $code, $statusCode, $this->urlFor($path));
252 case 'ACCOUNT_ID_REQUIRED':
253 case 'ACCOUNT_ID_UNKNOWN':
254 case 'AUTHORIZATION_INVALID':
255 case 'LICENSE_KEY_REQUIRED':
256 case 'USER_ID_REQUIRED':
257 case 'USER_ID_UNKNOWN':
258 throw new AuthenticationException($message, $code, $statusCode, $this->urlFor($path));
259 case 'OUT_OF_QUERIES':
260 case 'INSUFFICIENT_FUNDS':
261 throw new InsufficientFundsException($message, $code, $statusCode, $this->urlFor($path));
262 case 'PERMISSION_REQUIRED':
263 throw new PermissionRequiredException($message, $code, $statusCode, $this->urlFor($path));
264 default:
265 throw new InvalidRequestException($message, $code, $statusCode, $this->urlFor($path));
266 }
267 }
268 /**
269 * @param int $statusCode the HTTP status code
270 * @param string $service the service name
271 * @param string $path the URI path used in the request
272 *
273 * @throws HttpException
274 */
275 private function handle5xx(int $statusCode, string $service, string $path) : void
276 {
277 throw new HttpException("Received a server error ({$statusCode}) for {$service}", $statusCode, $this->urlFor($path));
278 }
279 /**
280 * @param int $statusCode the HTTP status code
281 * @param string $service the service name
282 * @param string $path the URI path used in the request
283 *
284 * @throws HttpException
285 */
286 private function handleUnexpectedStatus(int $statusCode, string $service, string $path) : void
287 {
288 throw new HttpException('Received an unexpected HTTP status ' . "({$statusCode}) for {$service}", $statusCode, $this->urlFor($path));
289 }
290 /**
291 * @param int $statusCode the HTTP status code
292 * @param string|null $body the successful request body
293 * @param string $service the service name
294 *
295 * @throws WebServiceException if a response body is included but not
296 * expected, or is not expected but not
297 * included, or is expected and included
298 * but cannot be decoded as JSON
299 *
300 * @return array|null the decoded request body
301 */
302 private function handleSuccess(int $statusCode, ?string $body, string $service) : ?array
303 {
304 // A 204 should have no response body
305 if ($statusCode === 204) {
306 if ($body !== null && $body !== '') {
307 throw new WebServiceException("Received a 204 response for {$service} along with an " . "unexpected HTTP body: {$body}");
308 }
309 return null;
310 }
311 // A 200 should have a valid JSON body
312 if ($body === null || $body === '') {
313 throw new WebServiceException("Received a 200 response for {$service} but did not " . 'receive a HTTP body.');
314 }
315 $decodedContent = json_decode($body, \true);
316 if ($decodedContent === null) {
317 throw new WebServiceException("Received a 200 response for {$service} but could " . 'not decode the response as JSON: ' . $this->jsonErrorDescription() . ' Body: ' . $body);
318 }
319 return $decodedContent;
320 }
321 private function getCaBundle() : ?string
322 {
323 $curlVersion = curl_version();
324 // On OS X, when the SSL version is "SecureTransport", the system's
325 // keychain will be used.
326 if ($curlVersion['ssl_version'] === 'SecureTransport') {
327 return null;
328 }
329 $cert = CaBundle::getSystemCaRootBundlePath();
330 // Check if the cert is inside a phar. If so, we need to copy the cert
331 // to a temp file so that curl can see it.
332 if (substr($cert, 0, 7) === 'phar://') {
333 $tempDir = sys_get_temp_dir();
334 $newCert = tempnam($tempDir, 'geoip2-');
335 if ($newCert === \false) {
336 throw new \RuntimeException("Unable to create temporary file in {$tempDir}");
337 }
338 if (!copy($cert, $newCert)) {
339 throw new \RuntimeException("Could not copy {$cert} to {$newCert}: " . var_export(error_get_last(), \true));
340 }
341 // We use a shutdown function rather than the destructor as the
342 // destructor isn't called on a fatal error such as an uncaught
343 // exception.
344 register_shutdown_function(function () use($newCert) {
345 unlink($newCert);
346 });
347 $cert = $newCert;
348 }
349 if (!file_exists($cert)) {
350 throw new \RuntimeException("CA cert does not exist at {$cert}");
351 }
352 return $cert;
353 }
354 }
355