Completed
Branch master (ebb53c)
by
unknown
05:48
created

RestClient::delete()   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 5
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 29
    public function __construct($apiEndpoint, $apiVersion, $apiKey, $apiSecret) {
73 29
        $this->apiEndpoint = $apiEndpoint;
74 29
        $this->apiVersion = $apiVersion;
75 29
        $this->apiKey = $apiKey;
76 29
        $this->apiSecret = $apiSecret;
77
78 29
        $this->guzzle = $this->createGuzzleClient();
79 29
    }
80
81
    /**
82
     * @param array $options
83
     * @param array $curlOptions
84
     * @return Guzzle
85
     */
86 29
    protected function createGuzzleClient(array $options = [], array $curlOptions = []) {
87 29
        $options = $options + $this->options;
88 29
        $curlOptions = $curlOptions + $this->curlOptions;
89
90 29
        $context = new Context([
91 29
            'keys' => [$this->apiKey => $this->apiSecret],
92 29
            'algorithm' => 'hmac-sha256',
93
            'headers' => ['(request-target)', 'Content-MD5', 'Date'],
94
        ]);
95
96 29
        $curlHandler = new CurlHandler($curlOptions);
97 29
        $handler = HandlerStack::create($curlHandler);
98 29
        $handler->push(GuzzleHttpSignatures::middlewareFromContext($context));
99
100 29
        return new Guzzle($options + array(
101 29
            'handler' => $handler,
102 29
            'base_uri' => $this->apiEndpoint,
103
            'headers' => array(
104 29
                'User-Agent' => Blocktrail::SDK_USER_AGENT . '/' . Blocktrail::SDK_VERSION
105
            ),
106
            'http_errors' => false,
107 29
            'connect_timeout' => 3,
108 29
            'timeout' => 20.0, // tmp until we have a good matrix of all the requests and their expect min/max time
109
            'verify' => true,
110 29
            'proxy' => '',
111
            'debug' => false,
112
            'config' => array(),
113 29
            '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 19
    public function get($endpointUrl, $queryString = null, $auth = null, $timeout = null) {
174 19
        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 16
    public function post($endpointUrl, $queryString = null, $postData = '', $auth = null, $timeout = null) {
186 16
        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 1
    public function put($endpointUrl, $queryString = null, $putData = '', $auth = null, $timeout = null) {
198 1
        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 11
    public function delete($endpointUrl, $queryString = null, $postData = null, $auth = null, $timeout = null) {
210 11
        return $this->request('DELETE', $endpointUrl, $queryString, $postData, $auth, 'url', $timeout);
211
    }
212
213
    private static $replaceQuery = ['=' => '%3D', '&' => '%26'];
214 21
    public static function hasQueryValue(Uri $uri, $key) {
215 21
        $current = $uri->getQuery();
216 21
        $key = strtr($key, self::$replaceQuery);
217
218 21
        if (!$current) {
219 21
            $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 16
            $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 16
            foreach (explode('&', $current) as $part) {
223 16
                if (explode('=', $part)[0] === $key) {
224 16
                    return true;
225
                };
226
            }
227
        }
228
229 21
        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 21
    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 21
        if (is_null($contentMD5Mode)) {
246 21
            $contentMD5Mode = !is_null($body) ? 'body' : 'url';
247
        }
248
249 21
        $request = new Request($method, $this->apiEndpoint . $endpointUrl);
250 21
        $uri = $request->getUri();
251
252 21
        if ($queryString) {
253 16
            foreach ($queryString as $k => $v) {
254 16
                $uri = Uri::withQueryValue($uri, $k, $v);
255
            }
256
        }
257
258 21
        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 21
            $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 21
        $request = $request->withUri($uri->withQuery(\Symfony\Component\HttpFoundation\Request::normalizeQueryString($uri->getQuery())));
265
266 21
        if (!$request->hasHeader('Date')) {
267 21
            $request = $request->withHeader('Date', $this->getRFC1123DateString());
268
        }
269
270 21
        if (!is_null($body)) {
271 16
            if (!$request->hasHeader('Content-Type')) {
272 16
                $request = $request->withHeader('Content-Type', 'application/json');
273
            }
274
275 16
            if (!is_string($body)) {
276 16
                $body = json_encode($body);
277
            }
278 16
            $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 21
        if ($contentMD5Mode == 'body') {
283 16
            $request = $request->withHeader('Content-MD5', md5((string)$body));
284
        } else {
285 19
            $request = $request->withHeader('Content-MD5', md5($request->getRequestTarget()));
286
        }
287
288 21
        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 21
    public function request($method, $endpointUrl, $queryString = null, $body = null, $auth = null, $contentMD5Mode = null, $timeout = null) {
304 21
        $request = $this->buildRequest($method, $endpointUrl, $queryString, $body, $auth, $contentMD5Mode);
305 21
        $response = $this->guzzle->send($request, ['auth' => $auth, 'timeout' => $timeout]);
306
307 21
        return $this->responseHandler($response);
308
    }
309
310 21
    public function responseHandler(ResponseInterface $responseObj) {
311 21
        $httpResponseCode = (int)$responseObj->getStatusCode();
312 21
        $httpResponsePhrase = (string)$responseObj->getReasonPhrase();
313 21
        $body = $responseObj->getBody();
314
315 21
        if ($httpResponseCode == 200) {
316 21
            if (!$body) {
317
                throw new EmptyResponse(Blocktrail::EXCEPTION_EMPTY_RESPONSE, $httpResponseCode);
318
            }
319
320 21
            $result = new Response($httpResponseCode, $body);
321
322 21
            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 8
            if ($httpResponsePhrase == "Endpoint Not Found") {
339
                throw new MissingEndpoint($this->verboseErrors ? $body : Blocktrail::EXCEPTION_MISSING_ENDPOINT, $httpResponseCode);
340
            } else {
341 8
                throw new ObjectNotFound($this->verboseErrors ? $body : Blocktrail::EXCEPTION_OBJECT_NOT_FOUND, $httpResponseCode);
342
            }
343 1
        } elseif ($httpResponseCode == 500) {
344 1
            throw new GenericServerError(Blocktrail::EXCEPTION_GENERIC_SERVER_ERROR . "\nServer Response: " . $body, $httpResponseCode);
345
        } else {
346
            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 21
    private function getRFC1123DateString() {
356 21
        $date = new \DateTime(null, new \DateTimeZone("UTC"));
357 21
        return str_replace("+0000", "GMT", $date->format(\DateTime::RFC1123));
358
    }
359
}
360