Completed
Pull Request — master (#267)
by
unknown
01:08
created

HttpClient::buildUrlQuery()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 2
1
<?php
2
/**
3
 * WooCommerce REST API HTTP Client
4
 *
5
 * @category HttpClient
6
 * @package  Automattic/WooCommerce
7
 */
8
9
namespace Automattic\WooCommerce\HttpClient;
10
11
use Automattic\WooCommerce\Client;
12
use Automattic\WooCommerce\HttpClient\BasicAuth;
13
use Automattic\WooCommerce\HttpClient\HttpClientException;
14
use Automattic\WooCommerce\HttpClient\OAuth;
15
use Automattic\WooCommerce\HttpClient\Options;
16
use Automattic\WooCommerce\HttpClient\Request;
17
use Automattic\WooCommerce\HttpClient\Response;
18
19
/**
20
 * REST API HTTP Client class.
21
 *
22
 * @package Automattic/WooCommerce
23
 */
24
class HttpClient
25
{
26
27
    /**
28
     * cURL handle.
29
     *
30
     * @var resource
31
     */
32
    protected $ch;
33
34
    /**
35
     * Store API URL.
36
     *
37
     * @var string
38
     */
39
    protected $url;
40
41
    /**
42
     * Consumer key.
43
     *
44
     * @var string
45
     */
46
    protected $consumerKey;
47
48
    /**
49
     * Consumer secret.
50
     *
51
     * @var string
52
     */
53
    protected $consumerSecret;
54
55
    /**
56
     * Client options.
57
     *
58
     * @var Options
59
     */
60
    protected $options;
61
62
    /**
63
     * The custom cURL options to use in the requests.
64
     *
65
     * @var array
66
     */
67
    private $customCurlOptions = [];
68
69
    /**
70
     * Request.
71
     *
72
     * @var Request
73
     */
74
    private $request;
75
76
    /**
77
     * Response.
78
     *
79
     * @var Response
80
     */
81
    private $response;
82
83
    /**
84
     * Response headers.
85
     *
86
     * @var string
87
     */
88
    private $responseHeaders;
89
90
    /**
91
     * Initialize HTTP client.
92
     *
93
     * @param string $url            Store URL.
94
     * @param string $consumerKey    Consumer key.
95
     * @param string $consumerSecret Consumer Secret.
96
     * @param array  $options        Client options.
97
     */
98
    public function __construct($url, $consumerKey, $consumerSecret, $options)
99
    {
100
        if (!\function_exists('curl_version')) {
101
            throw new HttpClientException('cURL is NOT installed on this server', -1, new Request(), new Response());
102
        }
103
104
        $this->options        = new Options($options);
105
        $this->url            = $this->buildApiUrl($url);
106
        $this->consumerKey    = $consumerKey;
107
        $this->consumerSecret = $consumerSecret;
108
    }
109
110
    /**
111
     * Check if is under SSL.
112
     *
113
     * @return bool
114
     */
115
    protected function isSsl()
116
    {
117
        return 'https://' === \substr($this->url, 0, 8);
118
    }
119
120
    /**
121
     * Build API URL.
122
     *
123
     * @param string $url Store URL.
124
     *
125
     * @return string
126
     */
127
    protected function buildApiUrl($url)
128
    {
129
        $api = $this->options->isWPAPI() ? $this->options->apiPrefix() : '/wc-api/';
130
131
        return \rtrim($url, '/') . $api . $this->options->getVersion() . '/';
132
    }
133
134
    /**
135
     * Build URL.
136
     *
137
     * @param string $url        URL.
138
     * @param array  $parameters Query string parameters.
139
     *
140
     * @return string
141
     */
142
    protected function buildUrlQuery($url, $parameters = [])
143
    {
144
        if (!empty($parameters)) {
145
            $url .= '?' . \http_build_query($parameters);
146
        }
147
148
        return $url;
149
    }
150
151
    /**
152
     * Authenticate.
153
     *
154
     * @param string $url        Request URL.
155
     * @param string $method     Request method.
156
     * @param array  $parameters Request parameters.
157
     *
158
     * @return array
159
     */
160
    protected function authenticate($url, $method, $parameters = [])
161
    {
162
        // Setup authentication.
163
        if ($this->isSsl()) {
164
            $basicAuth  = new BasicAuth(
165
                $this->ch,
166
                $this->consumerKey,
167
                $this->consumerSecret,
168
                $this->options->isQueryStringAuth(),
169
                $parameters
170
            );
171
            $parameters = $basicAuth->getParameters();
172
        } else {
173
            $oAuth      = new OAuth(
174
                $url,
175
                $this->consumerKey,
176
                $this->consumerSecret,
177
                $this->options->getVersion(),
178
                $method,
179
                $parameters,
180
                $this->options->oauthTimestamp()
181
            );
182
            $parameters = $oAuth->getParameters();
183
        }
184
185
        return $parameters;
186
    }
187
188
    /**
189
     * Setup method.
190
     *
191
     * @param string $method Request method.
192
     */
193
    protected function setupMethod($method)
194
    {
195
        if ('POST' == $method) {
196
            \curl_setopt($this->ch, CURLOPT_POST, true);
197
        } elseif (\in_array($method, ['PUT', 'DELETE', 'OPTIONS'])) {
198
            \curl_setopt($this->ch, CURLOPT_CUSTOMREQUEST, $method);
199
        }
200
    }
201
202
    /**
203
     * Get request headers.
204
     *
205
     * @param  bool $sendData If request send data or not.
206
     *
207
     * @return array
208
     */
209
    protected function getRequestHeaders($sendData = false)
210
    {
211
        $headers = [
212
            'Accept'     => 'application/json',
213
            'User-Agent' => $this->options->userAgent() . '/' . Client::VERSION,
214
        ];
215
216
        if ($sendData) {
217
            $headers['Content-Type'] = 'application/json;charset=utf-8';
218
        }
219
220
        return $headers;
221
    }
222
223
    /**
224
     * Create request.
225
     *
226
     * @param string $endpoint   Request endpoint.
227
     * @param string $method     Request method.
228
     * @param array  $data       Request data.
229
     * @param array  $parameters Request parameters.
230
     *
231
     * @return Request
232
     */
233
    protected function createRequest($endpoint, $method, $data = [], $parameters = [])
234
    {
235
        $body    = '';
236
        $url     = $this->url . $endpoint;
237
        $hasData = !empty($data);
238
239
        // Setup authentication.
240
        $parameters = $this->authenticate($url, $method, $parameters);
241
242
        // Setup method.
243
        $this->setupMethod($method);
244
245
        // Include post fields.
246
        if ($hasData) {
247
            $body = \json_encode($data);
248
            \curl_setopt($this->ch, CURLOPT_POSTFIELDS, $body);
249
        }
250
251
        $this->request = new Request(
252
            $this->buildUrlQuery($url, $parameters),
253
            $method,
254
            $parameters,
255
            $this->getRequestHeaders($hasData),
256
            $body
257
        );
258
259
        return $this->getRequest();
260
    }
261
262
    /**
263
     * Get response headers.
264
     *
265
     * @return array
266
     */
267
    protected function getResponseHeaders()
268
    {
269
        $headers = [];
270
        $lines   = \explode("\n", $this->responseHeaders);
271
        $lines   = \array_filter($lines, 'trim');
272
273
        foreach ($lines as $index => $line) {
274
            // Remove HTTP/xxx params.
275
            if (strpos($line, ': ') === false) {
276
                continue;
277
            }
278
279
            list($key, $value) = \explode(': ', $line);
280
281
            $headers[$key] = isset($headers[$key]) ? $headers[$key] . ', ' . trim($value) : trim($value);
282
        }
283
284
        return $headers;
285
    }
286
287
    /**
288
     * Create response.
289
     *
290
     * @return Response
291
     */
292
    protected function createResponse()
293
    {
294
295
        // Set response headers.
296
        $this->responseHeaders = '';
297
        \curl_setopt($this->ch, CURLOPT_HEADERFUNCTION, function ($_, $headers) {
298
            $this->responseHeaders .= $headers;
299
            return \strlen($headers);
300
        });
301
302
        // Get response data.
303
        $body    = \curl_exec($this->ch);
304
        $code    = \curl_getinfo($this->ch, CURLINFO_HTTP_CODE);
305
        $headers = $this->getResponseHeaders();
306
307
        // Register response.
308
        $this->response = new Response($code, $headers, $body);
309
310
        return $this->getResponse();
311
    }
312
313
    /**
314
     * Set default cURL settings.
315
     */
316
    protected function setDefaultCurlSettings()
317
    {
318
        $verifySsl       = $this->options->verifySsl();
319
        $timeout         = $this->options->getTimeout();
320
        $followRedirects = $this->options->getFollowRedirects();
321
322
        \curl_setopt($this->ch, CURLOPT_SSL_VERIFYPEER, $verifySsl);
323
        if (!$verifySsl) {
324
            \curl_setopt($this->ch, CURLOPT_SSL_VERIFYHOST, $verifySsl);
325
        }
326
        if ($followRedirects) {
327
            \curl_setopt($this->ch, CURLOPT_FOLLOWLOCATION, true);
328
        }
329
        \curl_setopt($this->ch, CURLOPT_CONNECTTIMEOUT, $timeout);
330
        \curl_setopt($this->ch, CURLOPT_TIMEOUT, $timeout);
331
        \curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, true);
332
        \curl_setopt($this->ch, CURLOPT_HTTPHEADER, $this->request->getRawHeaders());
333
        \curl_setopt($this->ch, CURLOPT_URL, $this->request->getUrl());
334
335
        foreach ($this->customCurlOptions as $customCurlOptionKey => $customCurlOptionValue) {
336
            \curl_setopt($this->ch, $customCurlOptionKey, $customCurlOptionValue);
337
        }
338
    }
339
340
    /**
341
     * Look for errors in the request.
342
     *
343
     * @param array $parsedResponse Parsed body response.
344
     */
345
    protected function lookForErrors($parsedResponse)
346
    {
347
        // Any non-200/201/202 response code indicates an error.
348
        if (!\in_array($this->response->getCode(), ['200', '201', '202'])) {
349
            $errors = isset($parsedResponse->errors) ? $parsedResponse->errors : $parsedResponse;
350
            $errorMessage = '';
351
            $errorCode = '';
352
353
            if (is_array($errors)) {
354
                $errorMessage = $errors[0]->message;
355
                $errorCode    = $errors[0]->code;
356
            } elseif (isset($errors->message, $errors->code)) {
357
                $errorMessage = $errors->message;
358
                $errorCode    = $errors->code;
359
            }
360
361
            throw new HttpClientException(
362
                \sprintf('Error: %s [%s]', $errorMessage, $errorCode),
363
                $this->response->getCode(),
364
                $this->request,
365
                $this->response
366
            );
367
        }
368
    }
369
370
    /**
371
     * Process response.
372
     *
373
     * @return array
374
     */
375
    protected function processResponse()
376
    {
377
        $body = $this->response->getBody();
378
379
        // Look for UTF-8 BOM and remove.
380
        if (0 === strpos(bin2hex(substr($body, 0, 4)), 'efbbbf')) {
381
            $body = substr($body, 3);
382
        }
383
384
        $parsedResponse = \json_decode($body);
385
386
        // Test if return a valid JSON.
387
        if (JSON_ERROR_NONE !== json_last_error()) {
388
            $message = function_exists('json_last_error_msg') ? json_last_error_msg() : 'Invalid JSON returned';
389
            throw new HttpClientException(
390
                sprintf('JSON ERROR: %s', $message),
391
                $this->response->getCode(),
392
                $this->request,
393
                $this->response
394
            );
395
        }
396
397
        $this->lookForErrors($parsedResponse);
398
399
        return $parsedResponse;
400
    }
401
402
    /**
403
     * Make requests.
404
     *
405
     * @param string $endpoint   Request endpoint.
406
     * @param string $method     Request method.
407
     * @param array  $data       Request data.
408
     * @param array  $parameters Request parameters.
409
     *
410
     * @return array
411
     */
412
    public function request($endpoint, $method, $data = [], $parameters = [])
413
    {
414
        // Initialize cURL.
415
        $this->ch = \curl_init();
416
417
        // Set request args.
418
        $request = $this->createRequest($endpoint, $method, $data, $parameters);
419
420
        // Default cURL settings.
421
        $this->setDefaultCurlSettings();
422
423
        // Get response.
424
        $response = $this->createResponse();
425
426
        // Check for cURL errors.
427
        if (\curl_errno($this->ch)) {
428
            throw new HttpClientException('cURL Error: ' . \curl_error($this->ch), 0, $request, $response);
429
        }
430
431
        \curl_close($this->ch);
432
433
        return $this->processResponse();
434
    }
435
436
    /**
437
     * Get request data.
438
     *
439
     * @return Request
440
     */
441
    public function getRequest()
442
    {
443
        return $this->request;
444
    }
445
446
    /**
447
     * Get response data.
448
     *
449
     * @return Response
450
     */
451
    public function getResponse()
452
    {
453
        return $this->response;
454
    }
455
456
    /**
457
     * Set custom cURL options to use in requests.
458
     *
459
     * @param array $curlOptions
460
     */
461
    public function setCustomCurlOptions(array $curlOptions)
462
    {
463
        $this->customCurlOptions = $curlOptions;
464
    }
465
}
466