Completed
Pull Request — master (#140)
by
unknown
01:22
created

HttpClient::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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