Completed
Pull Request — master (#99)
by thomas
18:19
created

RestClient::get()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 4
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?PHP
2
3
namespace Blocktrail\SDK\Connection;
4
5
use Blocktrail\SDK\Connection\Exceptions\BannedIP;
6
use GuzzleHttp\Client as Guzzle;
7
use GuzzleHttp\Handler\CurlHandler;
8
use GuzzleHttp\HandlerStack;
9
use GuzzleHttp\Psr7\Request;
10
use GuzzleHttp\Psr7\Uri;
11
use HttpSignatures\Context;
12
use Blocktrail\SDK\Blocktrail;
13
use Blocktrail\SDK\Connection\Exceptions\EndpointSpecificError;
14
use Blocktrail\SDK\Connection\Exceptions\GenericServerError;
15
use Blocktrail\SDK\Connection\Exceptions\ObjectNotFound;
16
use Blocktrail\SDK\Connection\Exceptions\UnknownEndpointSpecificError;
17
use Blocktrail\SDK\Connection\Exceptions\EmptyResponse;
18
use Blocktrail\SDK\Connection\Exceptions\InvalidCredentials;
19
use Blocktrail\SDK\Connection\Exceptions\MissingEndpoint;
20
use Blocktrail\SDK\Connection\Exceptions\GenericHTTPError;
21
use HttpSignatures\GuzzleHttpSignatures;
22
use Psr\Http\Message\ResponseInterface;
23
24
/**
25
 * Class RestClient
26
 *
27
 */
28
class RestClient {
29
30
    const AUTH_HTTP_SIG = 'http-signatures';
31
32
    /**
33
     * @var Guzzle
34
     */
35
    protected $guzzle;
36
37
    /**
38
     * @var string
39
     */
40
    protected $apiKey;
41
42
    /**
43
     * @var string
44
     */
45
    protected $apiEndpoint;
46
47
    /**
48
     * @var string
49
     */
50
    protected $apiVersion;
51
52
    /**
53
     * @var string
54
     */
55
    protected $apiSecret;
56
57
    /**
58
     * @var array
59
     */
60
    protected $options = [];
61
62
    /**
63
     * @var array
64
     */
65
    protected $curlOptions = [];
66
67
    /**
68
     * @var bool
69
     */
70
    protected $verboseErrors = false;
71
72 95
    public function __construct($apiEndpoint, $apiVersion, $apiKey, $apiSecret) {
73 95
        $this->apiEndpoint = $apiEndpoint;
74 95
        $this->apiVersion = $apiVersion;
75 95
        $this->apiKey = $apiKey;
76 95
        $this->apiSecret = $apiSecret;
77
78 95
        $this->guzzle = $this->createGuzzleClient();
79 95
    }
80
81
    /**
82
     * @param array $options
83
     * @param array $curlOptions
84
     * @return Guzzle
85
     */
86 95
    protected function createGuzzleClient(array $options = [], array $curlOptions = []) {
87 95
        $options = $options + $this->options;
88 95
        $curlOptions = $curlOptions + $this->curlOptions;
89
90 95
        $context = new Context([
91 95
            'keys' => [$this->apiKey => $this->apiSecret],
92 95
            'algorithm' => 'hmac-sha256',
93
            'headers' => ['(request-target)', 'Content-MD5', 'Date'],
94
        ]);
95
96 95
        $curlHandler = new CurlHandler($curlOptions);
97 95
        $handler = HandlerStack::create($curlHandler);
98 95
        $handler->push(GuzzleHttpSignatures::middlewareFromContext($context));
99
100 95
        return new Guzzle($options + array(
101 95
            'handler' => $handler,
102 95
            'base_uri' => $this->apiEndpoint,
103
            'headers' => array(
104 95
                'User-Agent' => Blocktrail::SDK_USER_AGENT . '/' . Blocktrail::SDK_VERSION,
105
            ),
106
            'http_errors' => false,
107 95
            'connect_timeout' => 3,
108 95
            'timeout' => 20.0, // tmp until we have a good matrix of all the requests and their expect min/max time
109
            'verify' => true,
110 95
            'proxy' => '',
111
            'debug' => false,
112
            'config' => array(),
113 95
            'auth' => '',
114
        ));
115
    }
116
117
    /**
118
     * @return Guzzle
119
     */
120
    public function getGuzzleClient() {
121
        return $this->guzzle;
122
    }
123
124
    /**
125
     * enable CURL debugging output
126
     *
127
     * @param   bool        $debug
128
     */
129
    public function setCurlDebugging($debug = true) {
130
        $this->options['debug'] = $debug;
131
132
        $this->guzzle = $this->createGuzzleClient();
133
    }
134
135
    /**
136
     * enable verbose errors
137
     *
138
     * @param   bool        $verboseErrors
139
     */
140
    public function setVerboseErrors($verboseErrors = true) {
141
        $this->verboseErrors = $verboseErrors;
142
    }
143
        
144
    /**
145
     * set cURL default option on Guzzle client
146
     * @param string    $key
147
     * @param bool      $value
148
     */
149
    public function setCurlDefaultOption($key, $value) {
150
        $this->curlOptions[$key] = $value;
151
152
        $this->guzzle = $this->createGuzzleClient();
153
    }
154
155
    /**
156
     * set the proxy config for Guzzle
157
     *
158
     * @param   $proxy
159
     */
160
    public function setProxy($proxy) {
161
        $this->options['proxy'] = $proxy;
162
163
        $this->guzzle = $this->createGuzzleClient();
164
    }
165
166
    /**
167
     * @param   string          $endpointUrl
168
     * @param   array           $queryString
169
     * @param   string          $auth           http-signatures to enable http-signature signing
170
     * @param   float           $timeout        timeout in seconds
171
     * @return  Response
172
     */
173 29
    public function get($endpointUrl, $queryString = null, $auth = null, $timeout = null) {
174 29
        return $this->request('GET', $endpointUrl, $queryString, null, $auth, null, $timeout);
175
    }
176
177
    /**
178
     * @param   string          $endpointUrl
179
     * @param   null            $queryString
180
     * @param   array|string    $postData
181
     * @param   string          $auth           http-signatures to enable http-signature signing
182
     * @param   float           $timeout        timeout in seconds
183
     * @return  Response
184
     */
185 22
    public function post($endpointUrl, $queryString = null, $postData = '', $auth = null, $timeout = null) {
186 22
        return $this->request('POST', $endpointUrl, $queryString, $postData, $auth, null, $timeout);
187
    }
188
189
    /**
190
     * @param   string          $endpointUrl
191
     * @param   null            $queryString
192
     * @param   array|string    $putData
193
     * @param   string          $auth           http-signatures to enable http-signature signing
194
     * @param   float           $timeout        timeout in seconds
195
     * @return  Response
196
     */
197
    public function put($endpointUrl, $queryString = null, $putData = '', $auth = null, $timeout = null) {
198
        return $this->request('PUT', $endpointUrl, $queryString, $putData, $auth, null, $timeout);
199
    }
200
201
    /**
202
     * @param   string          $endpointUrl
203
     * @param   null            $queryString
204
     * @param   array|string    $postData
205
     * @param   string          $auth           http-signatures to enable http-signature signing
206
     * @param   float           $timeout        timeout in seconds
207
     * @return  Response
208
     */
209 10
    public function delete($endpointUrl, $queryString = null, $postData = null, $auth = null, $timeout = null) {
210 10
        return $this->request('DELETE', $endpointUrl, $queryString, $postData, $auth, 'url', $timeout);
211
    }
212
213
    private static $replaceQuery = ['=' => '%3D', '&' => '%26'];
214 32
    public static function hasQueryValue(Uri $uri, $key) {
215 32
        $current = $uri->getQuery();
216 32
        $key = strtr($key, self::$replaceQuery);
217
218 32
        if (!$current) {
219 32
            $result = [];
0 ignored issues
show
Unused Code introduced by
$result is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
220
        } else {
221 18
            $result = [];
0 ignored issues
show
Unused Code introduced by
$result is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
222 18
            foreach (explode('&', $current) as $part) {
223 18
                if (explode('=', $part)[0] === $key) {
224 18
                    return true;
225
                };
226
            }
227
        }
228
229 32
        return false;
230
    }
231
232
    /**
233
     * generic request executor
234
     *
235
     * @param   string          $method         GET, POST, PUT, DELETE
236
     * @param   string          $endpointUrl
237
     * @param   array           $queryString
238
     * @param   array|string    $body
239
     * @param   string          $auth           http-signatures to enable http-signature signing
240
     * @param   string          $contentMD5Mode body or url
241
     * @param   float           $timeout        timeout in seconds
242
     * @return Request
243
     */
244 32
    public function buildRequest($method, $endpointUrl, $queryString = null, $body = null, $auth = null, $contentMD5Mode = null, $timeout = null) {
0 ignored issues
show
Unused Code introduced by
The parameter $auth is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $timeout is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
245 32
        if (is_null($contentMD5Mode)) {
246 32
            $contentMD5Mode = !is_null($body) ? 'body' : 'url';
247
        }
248
249 32
        $request = new Request($method, $this->apiEndpoint . $endpointUrl);
250 32
        $uri = $request->getUri();
251
252 32
        if ($queryString) {
253 18
            foreach ($queryString as $k => $v) {
254 18
                $uri = Uri::withQueryValue($uri, $k, $v);
255
            }
256
        }
257
258 32
        if (!self::hasQueryValue($uri, 'api_key')) {
0 ignored issues
show
Compatibility introduced by
$uri of type object<Psr\Http\Message\UriInterface> is not a sub-type of object<GuzzleHttp\Psr7\Uri>. It seems like you assume a concrete implementation of the interface Psr\Http\Message\UriInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
259 32
            $uri = Uri::withQueryValue($uri, 'api_key', $this->apiKey);
260
        }
261
262
        // normalize the query string the same way the server expects it
263
        /** @var Request $request */
264 32
        $request = $request->withUri($uri->withQuery(\Symfony\Component\HttpFoundation\Request::normalizeQueryString($uri->getQuery())));
265
266 32
        if (!$request->hasHeader('Date')) {
267 32
            $request = $request->withHeader('Date', $this->getRFC1123DateString());
268
        }
269
270 32
        if (!is_null($body)) {
271 22
            if (!$request->hasHeader('Content-Type')) {
272 22
                $request = $request->withHeader('Content-Type', 'application/json');
273
            }
274
275 22
            if (!is_string($body)) {
276 22
                $body = json_encode($body);
277
            }
278 22
            $request = $request->withBody(\GuzzleHttp\Psr7\stream_for($body));
279
        }
280
281
        // for GET/DELETE requests, MD5 the request URI (excludes domain, includes query strings)
282 32
        if ($contentMD5Mode == 'body') {
283 22
            $request = $request->withHeader('Content-MD5', md5((string)$body));
284
        } else {
285 29
            $request = $request->withHeader('Content-MD5', md5($request->getRequestTarget()));
286
        }
287
288 32
        return $request;
289
    }
290
291
    /**
292
     * generic request executor
293
     *
294
     * @param   string          $method         GET, POST, PUT, DELETE
295
     * @param   string          $endpointUrl
296
     * @param   array           $queryString
297
     * @param   array|string    $body
298
     * @param   string          $auth           http-signatures to enable http-signature signing
299
     * @param   string          $contentMD5Mode body or url
300
     * @param   float           $timeout        timeout in seconds
301
     * @return Response
302
     */
303 32
    public function request($method, $endpointUrl, $queryString = null, $body = null, $auth = null, $contentMD5Mode = null, $timeout = null) {
304 32
        $request = $this->buildRequest($method, $endpointUrl, $queryString, $body, $auth, $contentMD5Mode);
305 32
        $response = $this->guzzle->send($request, ['auth' => $auth, 'timeout' => $timeout]);
306
307 32
        return $this->responseHandler($response);
308
    }
309
310 32
    public function responseHandler(ResponseInterface $responseObj) {
311 32
        $httpResponseCode = (int)$responseObj->getStatusCode();
312 32
        $httpResponsePhrase = (string)$responseObj->getReasonPhrase();
313 32
        $body = $responseObj->getBody();
314
315 32
        if ($httpResponseCode == 200) {
316 31
            if (!$body) {
317
                throw new EmptyResponse(Blocktrail::EXCEPTION_EMPTY_RESPONSE, $httpResponseCode);
318
            }
319
320 31
            $result = new Response($httpResponseCode, $body);
321
322 31
            return $result;
323 10
        } elseif ($httpResponseCode == 400 || $httpResponseCode == 403) {
324 6
            $data = json_decode($body, true);
325
326 6
            if ($data && isset($data['msg'], $data['code'])) {
327 6
                throw new EndpointSpecificError(!is_string($data['msg']) ? json_encode($data['msg']) : $data['msg'], $data['code']);
328
            } else {
329
                if (preg_match("/^banned( IP)? \[(.+)\]\n?$/", $body, $m)) {
330
                    throw new BannedIP($m[2]);
331
                }
332
333
                throw new UnknownEndpointSpecificError($this->verboseErrors ? $body : Blocktrail::EXCEPTION_UNKNOWN_ENDPOINT_SPECIFIC_ERROR);
334
            }
335 10
        } elseif ($httpResponseCode == 401) {
336 1
            throw new InvalidCredentials($this->verboseErrors ? $body : Blocktrail::EXCEPTION_INVALID_CREDENTIALS, $httpResponseCode);
337 9
        } elseif ($httpResponseCode == 404) {
338 7
            if ($httpResponsePhrase == "Endpoint Not Found") {
339
                throw new MissingEndpoint($this->verboseErrors ? $body : Blocktrail::EXCEPTION_MISSING_ENDPOINT, $httpResponseCode);
340
            } else {
341 7
                throw new ObjectNotFound($this->verboseErrors ? $body : Blocktrail::EXCEPTION_OBJECT_NOT_FOUND, $httpResponseCode);
342
            }
343 2
        } elseif ($httpResponseCode == 500) {
344 1
            throw new GenericServerError(Blocktrail::EXCEPTION_GENERIC_SERVER_ERROR . "\nServer Response: " . $body, $httpResponseCode);
345
        } else {
346 1
            throw new GenericHTTPError(Blocktrail::EXCEPTION_GENERIC_HTTP_ERROR . "\nServer Response: " . $body, $httpResponseCode);
347
        }
348
    }
349
350
    /**
351
     * Returns curent fate time in RFC1123 format, using UTC time zone
352
     *
353
     * @return  string
354
     */
355 32
    private function getRFC1123DateString() {
356 32
        $date = new \DateTime(null, new \DateTimeZone("UTC"));
357 32
        return str_replace("+0000", "GMT", $date->format(\DateTime::RFC1123));
358
    }
359
}
360