Completed
Push — master ( 00e4c5...e016d8 )
by Kevin
06:06
created

Client::isSsoEnabled()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
3
namespace PCextreme\Cloudstack;
4
5
use InvalidArgumentException;
6
use PCextreme\Cloudstack\Exception\ClientException;
7
use PCextreme\Cloudstack\Util\UrlHelpersTrait;
8
use Psr\Http\Message\ResponseInterface;
9
use RuntimeException;
10
11
class Client extends AbstractClient
12
{
13
    use UrlHelpersTrait;
14
15
    /**
16
     * @var array
17
     */
18
    protected $apiList;
19
20
    /**
21
     * @var string
22
     */
23
    private $urlApi;
24
25
    /**
26
     * @var string
27
     */
28
    private $urlClient;
0 ignored issues
show
Unused Code introduced by
The property $urlClient is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
29
30
    /**
31
     * @var string
32
     */
33
    private $urlConsole;
0 ignored issues
show
Unused Code introduced by
The property $urlConsole is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
34
35
    /**
36
     * @var string
37
     */
38
    protected $apiKey;
39
40
    /**
41
     * @var string
42
     */
43
    protected $secretKey;
44
45
    /**
46
     * @var string
47
     */
48
    protected $ssoKey;
49
50
    /**
51
     * @var string
52
     */
53
    private $responseError = 'errortext';
54
55
    /**
56
     * @var string
57
     */
58
    private $responseCode = 'errorcode';
59
60
    /**
61
     * @var bool
62
     */
63
    private $ssoEnabled = false;
64
65
    /**
66
     * Constructs a new Cloudstack client instance.
67
     *
68
     * @param  array  $options
69
     *     An array of options to set on this client. Options include
70
     *     'apiList', 'urlApi', 'urlClient', 'urlConsole', 'apiKey',
71
     *     'secretKey', 'responseError' and 'responseCode'.
72
     * @param  array  $collaborators
73
     *     An array of collaborators that may be used to override
74
     *     this provider's default behavior. Collaborators include
75
     *     `requestFactory` and `httpClient`.
76
     */
77
    public function __construct(array $options = [], array $collaborators = [])
78
    {
79
        $this->assertRequiredOptions($options);
80
81
        $possible   = $this->getConfigurableOptions();
82
        $configured = array_intersect_key($options, array_flip($possible));
83
84
        foreach ($configured as $key => $value) {
85
            $this->$key = $value;
86
        }
87
88
        // Remove all options that are only used locally
89
        $options = array_diff_key($options, $configured);
90
91
        parent::__construct($options, $collaborators);
92
    }
93
94
    /**
95
     * Returns all options that can be configured.
96
     *
97
     * @return array
98
     */
99
    protected function getConfigurableOptions()
100
    {
101
        return array_merge($this->getRequiredOptions(), [
102
            'apiList',
103
            'urlClient',
104
            'urlConsole',
105
            'ssoKey',
106
            'responseError',
107
            'responseCode',
108
        ]);
109
    }
110
111
    /**
112
     * Returns all options that are required.
113
     *
114
     * @return array
115
     */
116
    protected function getRequiredOptions()
117
    {
118
        return [
119
            'urlApi',
120
            'apiKey',
121
            'secretKey',
122
        ];
123
    }
124
125
    /**
126
     * Verifies that all required options have been passed.
127
     *
128
     * @param  array  $options
129
     * @return void
130
     * @throws InvalidArgumentException
131
     */
132
    private function assertRequiredOptions(array $options)
133
    {
134
        $missing = array_diff_key(array_flip($this->getRequiredOptions()), $options);
135
136
        if (! empty($missing)) {
137
            throw new InvalidArgumentException(
138
                'Required options not defined: ' . implode(', ', array_keys($missing))
139
            );
140
        }
141
    }
142
143
    public function command($command, array $options = [])
144
    {
145
        $this->assertRequiredCommandOptions($command, $options);
146
147
        $method  = $this->getCommandMethod($command);
148
        $url     = $this->getCommandUrl($command, $options);
149
        $request = $this->getRequest($method, $url, $options);
150
151
        return $this->getResponse($request);
152
    }
153
154
    /**
155
     * Verifies that all required options have been passed.
156
     *
157
     * @param  array $options
158
     * @return void
159
     * @throws RuntimeException
160
     * @throws InvalidArgumentException
161
     */
162
    private function assertRequiredCommandOptions($command, array $options = [])
163
    {
164
        $apiList = $this->getApiList();
165
166
        if (! array_key_exists($command, $apiList)) {
167
            throw new RuntimeException(
168
                "Call to unsupported API command [{$command}], this call is not present in the API list."
169
            );
170
        }
171
172
        foreach ($apiList[$command]['params'] as $key => $value) {
173
            if (! array_key_exists($key, $options) && (bool) $value['required']) {
174
                throw new InvalidArgumentException(
175
                    "Missing argument [{$key}] for command [{$command}] must be of type [{$value['type']}]."
176
                );
177
            }
178
        }
179
    }
180
181
    /**
182
     * Returns command method based on the command.
183
     *
184
     * @param  string  $command
185
     * @return array
186
     */
187
    public function getCommandMethod($command)
188
    {
189
        if (in_array($command, ['login', 'deployVirtualMachine'])) {
190
            return self::METHOD_POST;
191
        }
192
193
        return self::METHOD_GET;
194
    }
195
196
    /**
197
     * Builds the command URL's query string.
198
     *
199
     * @param  array  $params
200
     * @return string
201
     */
202
    public function getCommandQuery(array $params)
203
    {
204
        return $this->signCommandParameters($params);
205
    }
206
207
    /**
208
     * Builds the authorization URL.
209
     *
210
     * @param  string  $command
211
     * @param  array   $options
212
     * @return string
213
     */
214
    public function getCommandUrl($command, array $options = [])
215
    {
216
        $base   = $this->urlApi;
217
        $params = $this->getCommandParameters($command, $options);
218
        $query  = $this->getCommandQuery($params);
219
220
        return $this->appendQuery($base, $query);
221
    }
222
223
    /**
224
     * Returns command parameters based on provided options.
225
     *
226
     * @param  string  $command
227
     * @param  array   $options
228
     * @return array
229
     */
230
    protected function getCommandParameters($command, array $options)
231
    {
232
        return array_merge($options, [
233
            'command'  => $command,
234
            'response' => 'json',
235
            'apikey'   => $this->apiKey,
236
        ]);
237
    }
238
239
    /**
240
     * Signs the command parameters.
241
     *
242
     * @param  array  $params
243
     * @return array
244
     */
245
    protected function signCommandParameters(array $params = [])
246
    {
247
        if ($this->isSsoEnabled() && is_null($this->ssoKey)) {
248
            throw new InvalidArgumentException(
249
                'Required options not defined: ssoKey'
250
            );
251
        }
252
253
        ksort($params);
254
255
        $query = $this->buildQueryString($params);
256
257
        $key = $this->isSsoEnabled() ? $this->ssoKey : $this->secretKey;
258
        $signature = rawurlencode(base64_encode(hash_hmac(
259
            'SHA1',
260
            strtolower($query),
261
            $key,
262
            true
263
        )));
264
265
        // Reset SSO signing for the next request.
266
        $this->ssoEnabled = false;
267
268
        // To prevent the signature from being escaped we simply append
269
        // the signature to the previously build query.
270
        return $query . '&signature=' . $signature;
271
    }
272
273
    /**
274
     * Get Cloudstack Client API list.
275
     *
276
     * Tries to load the API list from the cache directory when
277
     * the 'apiList' on the class is empty.
278
     *
279
     * @return array
280
     * @throws RuntimeException
281
     */
282
    public function getApiList()
283
    {
284
        if (is_null($this->apiList)) {
285
            $path = __DIR__ . '/../cache/api_list.php';
286
287
            if (! file_exists($path)) {
288
                throw new RuntimeException(
289
                    "Cloudstack Client API list not found. This file needs to be generated before using the client."
290
                );
291
            }
292
293
            $this->apiList = require $path;
294
        }
295
296
        return $this->apiList;
297
    }
298
299
    /**
300
     * Set Cloudstack Client API list.
301
     *
302
     * @param  array  $apiList
303
     * @return void
304
     */
305
    public function setApiList(array $apiList)
306
    {
307
        $this->apiList = $apiList;
308
    }
309
310
    /**
311
     * Appends a query string to a URL.
312
     *
313
     * @param  string  $url
314
     * @param  string  $query
315
     * @return string
316
     */
317
    protected function appendQuery($url, $query)
318
    {
319
        $query = trim($query, '?&');
320
321
        if ($query) {
322
            return $url . '?' . $query;
323
        }
324
325
        return $url;
326
    }
327
328
    /**
329
     * Build a query string from an array.
330
     *
331
     * @param  array  $params
332
     * @return string
333
     */
334
    protected function buildQueryString(array $params)
335
    {
336
        return http_build_query($params, false, '&', PHP_QUERY_RFC3986);
337
    }
338
339
    /**
340
     * Checks a provider response for errors.
341
     *
342
     * @param  ResponseInterface  $response
343
     * @param  array|string       $data
344
     * @return void
345
     * @throws \PCextreme\Cloudstack\Exceptions\ClientException
346
     */
347
    protected function checkResponse(ResponseInterface $response, $data)
348
    {
349
        // Cloudstack returns multidimensional responses, keyed with the
350
        // command name. To handle errors in a generic way we need to 'reset'
351
        // the data array. To prevent strings from breaking this we ensure we
352
        // have an array to begin with.
353
        $data = is_array($data) ? $data : [$data];
354
355
        if (isset(reset($data)[$this->responseError])) {
356
            $error = reset($data)[$this->responseError];
357
            $code  = $this->responseCode ? reset($data)[$this->responseCode] : 0;
358
359
            throw new ClientException($error, $code, $data);
360
        }
361
    }
362
363
    /**
364
     * Enable SSO key signing for the next request.
365
     *
366
     * @param  bool  $enable
367
     * @return self
368
     */
369
    public function enableSso($enable = true)
370
    {
371
        $this->ssoEnabled = $enable;
372
373
        return $this;
374
    }
375
    /**
376
     * Determine if SSO signing is enabled.
377
     *
378
     * @return bool
379
     */
380
    public function isSsoEnabled()
381
    {
382
        return $this->ssoEnabled;
383
    }
384
385
    /**
386
     * Handle dynamic method calls into the method.
387
     *
388
     * @param  string  $method
389
     * @param  array   $parameters
390
     * @return mixed
391
     */
392
    public function __call($method, $parameters)
393
    {
394
        array_unshift($parameters, $method);
395
396
        return call_user_func_array(array($this, 'command'), $parameters);
397
    }
398
}
399