Passed
Pull Request — master (#61)
by
unknown
03:32
created

AcquiaHmacClient::__construct()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 28
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 13
c 1
b 0
f 0
nc 2
nop 7
dl 0
loc 28
rs 9.8333
1
<?php
2
3
namespace Acquia\Hmac\Client;
4
5
use Acquia\Hmac\Exception\KeyNotFoundException;
6
use Acquia\Hmac\Guzzle\HmacAuthMiddleware;
7
use Acquia\Hmac\Key;
8
use Acquia\Hmac\KeyLoader;
9
use Acquia\Hmac\RequestAuthenticator;
10
use Acquia\Search\Api\AdaptiveResponseDeserializer;
0 ignored issues
show
Bug introduced by
The type Acquia\Search\Api\AdaptiveResponseDeserializer was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
Bug introduced by
This use statement conflicts with another class in this namespace, Acquia\Hmac\Client\AdaptiveResponseDeserializer. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
11
use GuzzleHttp\Client as HttpClient;
12
use GuzzleHttp\ClientInterface;
13
use GuzzleHttp\Command\Guzzle\DescriptionInterface;
0 ignored issues
show
Bug introduced by
The type GuzzleHttp\Command\Guzzle\DescriptionInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
14
use GuzzleHttp\Command\Guzzle\GuzzleClient;
0 ignored issues
show
Bug introduced by
The type GuzzleHttp\Command\Guzzle\GuzzleClient was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
15
use GuzzleHttp\HandlerStack;
16
use InvalidArgumentException;
17
use Symfony\Component\HttpFoundation\Request;
18
19
/**
20
 * This client wraps GuzzleClient to make hmac requests to Acquia Services.
21
 *
22
 * Individual services must extend this client since the headers for each
23
 * service is different.
24
 */
25
abstract class AcquiaHmacClient extends GuzzleClient {
26
27
    /**
28
     * Human Readable Version of the Api Service.
29
     *
30
     * This must be set with setApi in the child class.
31
     *
32
     * @var string
33
     */
34
    protected $apiName;
35
36
    /**
37
     * Schema Path to the API JSON Description
38
     *
39
     * This must be set with setSchemaPath in the child class.
40
     *
41
     * @var string
42
     */
43
    protected $schemaPath;
44
45
    /**
46
     * API Description.
47
     *
48
     * @var \GuzzleHttp\Command\Guzzle\DescriptionInterface
49
     */
50
    protected $apiDescription;
51
52
    /**
53
     * Client configuration array.
54
     *
55
     * @var array
56
     */
57
    protected $clientConfig;
58
59
    /**
60
     * Client HMAC Credentials, ready for injection into into a Handler Stack.
61
     *
62
     * @var \Acquia\Hmac\Guzzle\HmacAuthMiddleware
63
     */
64
    protected $clientCredentials;
65
66
    /**
67
     * The client constructor accepts an associative array of configuration
68
     * options:
69
     *
70
     * - defaults: Associative array of default command parameters to add to
71
     *   each command created by the client.
72
     * - validate: Specify if command input is validated (defaults to true).
73
     *   Changing this setting after the client has been created will have no
74
     *   effect.
75
     * - process: Specify if HTTP responses are parsed (defaults to true).
76
     *   Changing this setting after the client has been created will have no
77
     *   effect.
78
     * - response_locations: Associative array of location types mapping to
79
     *   ResponseLocationInterface objects.
80
     *
81
     * @param array $config
82
     *   Configuration options
83
     * @param array $credentials
84
     *   Authentication credentials for API access. Should include the following
85
     *   values:
86
     *     - api_key: a Search API key
87
     *     - hmac_key: an Acquia HMAC public key
88
     *     - hmac_secret: an Acquia HMAC private key
89
     * @param \GuzzleHttp\ClientInterface $client
90
     *   HTTP client to use.
91
     * @param \GuzzleHttp\Command\Guzzle\DescriptionInterface $description
92
     *   Guzzle service description.
93
     * @param callable $commandToRequestTransformer
94
     *   Handler used to serializes requests for a given command.
95
     * @param callable $responseToResultTransformer
96
     *   Handler used to create response models based on an HTTP response and
97
     *   a service description.
98
     * @param \GuzzleHttp\HandlerStack $commandHandlerStack
99
     *   Middleware stack.
100
     *
101
     * @throws \InvalidArgumentException
102
     *   When $config has empty 'api_key', 'api_secret', or 'base_uri'.
103
     *
104
     * @SuppressWarnings(PHPMD.LongVariable) // Long parameter names are from
105
     *   Guzzle.
106
     */
107
    public function __construct(
108
        array                $config = [],
109
        array                $credentials = [],
110
        ClientInterface      $client = NULL,
111
        DescriptionInterface $description = NULL,
112
        callable             $commandToRequestTransformer = NULL,
113
        callable             $responseToResultTransformer = NULL,
114
        HandlerStack         $commandHandlerStack = NULL
115
    ) {
116
        // Set the Client API Name.
117
        $this->setApiName();
118
        $this->setClientConfig($config, $credentials, $commandHandlerStack);
119
        // Build the HTTP client.
120
        $client = $client ?: new HttpClient($this->getClientConfig());
121
122
        // Set the Description Path.
123
        $this->setSchemaPath();
124
        $this->setApiDescription($description);
125
126
        // Create the API client.
127
        $process = (!isset($this->getClientConfig()['process']) || $this->getClientConfig()['process'] === TRUE);
128
        parent::__construct(
129
            $client,
130
            $this->getApiDescription(),
131
            $commandToRequestTransformer,
132
            $responseToResultTransformer ?: new AdaptiveResponseDeserializer($this->getApiDescription(), $process),
133
            $commandHandlerStack,
134
            $this->getClientConfig()
135
        );
136
    }
137
138
    /**
139
     * Retrieve API description.
140
     *
141
     * @return \GuzzleHttp\Command\Guzzle\DescriptionInterface
142
     *   API description instance.
143
     */
144
    public function getApiDescription() {
145
        return $this->apiDescription;
146
    }
147
148
    /**
149
     * Retrieve client configuration.
150
     *
151
     * @return array
152
     *   Client config array.
153
     */
154
    protected function getClientConfig() {
155
        return $this->clientConfig;
156
    }
157
158
    /**
159
     * Set API description.
160
     *
161
     * Each implimenting service must define an API Description, specifically
162
     * the schema path.
163
     *
164
     * Example:
165
     *  {
166
     *     $schema_path = 'path/to/json/file.json'
167
     *     $this->apiDescription = $description ?: new ClientServiceDescription($schema_path);
168
     *  }
169
     *
170
     * @param \GuzzleHttp\Command\Guzzle\DescriptionInterface|null $description
171
     *   API description instance.
172
     *
173
     * @return $this
174
     */
175
    private function setApiDescription(DescriptionInterface $description = NULL) {
176
        $this->apiDescription = $description ?: new ClientServiceDescription($this->schemaPath);
177
        return $this;
178
    }
179
180
    /**
181
     * Prepares the API and HMAC credentials for the client.
182
     *
183
     * @param $credentials
184
     *   An array containing the client API and HMAC credentials.
185
     * @return $this
186
     */
187
    protected function setClientCredentials($credentials)
188
    {
189
        // Set API Key Headers, if they exist for the service.
190
        if (isset($credentials['api_key']) && $this->getServiceApiHeader()) {
191
            $this->clientConfig['headers'][$this->getServiceApiHeader()] = $credentials['service_key'];
192
        }
193
        if (isset($credentials['api_key'], $credentials['api_secret'])) {
194
            $this->clientCredentials = new HmacAuthMiddleware(
195
                new Key($credentials['api_key'], $credentials['api_secret']),
196
                $this->getRealm(),
197
                $this->getServiceApiHeader() ? [$this->getServiceApiHeader()]: []
198
            );
199
        }
200
        return $this;
201
    }
202
203
    /**
204
     * Set client configuration.
205
     *
206
     * @param array $config
207
     *   Config array.
208
     * @param array $credentials
209
     *   Credentials for API access.
210
     * @param \GuzzleHttp\HandlerStack|null $commandHandlerStack
211
     *   HandlerStack instance.
212
     *
213
     * @return $this
214
     */
215
    protected function setClientConfig(
216
        array $config = [],
217
        array $credentials = [],
218
        HandlerStack $commandHandlerStack = null
219
    ) {
220
        $this->clientConfig = $config;
221
        $this->validateClientConfig()
222
            ->normalizeClientConfig()
223
            ->setClientCredentials($credentials)
224
            ->validateClientCredentials()
225
            ->setClientConfigHandlerStack($commandHandlerStack);
226
        return $this;
227
    }
228
229
    /**
230
     * Set the client configuration handler stack.
231
     *
232
     * @param \GuzzleHttp\HandlerStack|null $commandHandlerStack
233
     *   HandlerStack instance.
234
     *
235
     * @return $this
236
     *
237
     * @SuppressWarnings(PHPMD.StaticAccess) // Allow creating a default
238
     *   HandlerStack via static access.
239
     */
240
    private function setClientConfigHandlerStack(HandlerStack $commandHandlerStack = null)
241
    {
242
        $handlerStack = $commandHandlerStack ?? HandlerStack::create();
243
244
        if ($this->clientCredentials) {
245
            $handlerStack->push($this->clientCredentials);
246
        }
247
        $this->clientConfig['handler'] = $handlerStack;
248
        return $this;
249
    }
250
251
    /**
252
     * Validate client configuration.
253
     *
254
     * @return $this
255
     *
256
     * @throws \InvalidArgumentException
257
     *   When $config has empty 'base_uri'.
258
     */
259
    private function validateClientConfig()
260
    {
261
        // Make certain that we have a base URI.
262
        if (empty($this->getClientConfig()['base_uri'])) {
263
            throw new InvalidArgumentException(
264
                "The $this->apiName client config is missing the base_uri."
265
            );
266
        }
267
        return $this;
268
    }
269
270
    /**
271
     * Validates the client API and HMAC credentials.
272
     *
273
     * @return $this
274
     *
275
     * @throws \InvalidArgumentException
276
     *   When $config has empty 'base_uri'.
277
     */
278
    private function validateClientCredentials()
279
    {
280
        // Make certain that we have an API key, HMAC key and HMAC secret.
281
        if (!isset($this->clientCredentials)) {
282
            throw new InvalidArgumentException(
283
                "The $this->apiName credential config is missing API and/or HMAC keys."
284
            );
285
        }
286
        return $this;
287
    }
288
289
    /**
290
     * Normalize client configuration.
291
     *
292
     * @return $this
293
     */
294
    private function normalizeClientConfig()
295
    {
296
        // Ensure the base_uri value ends with a slash so that relative URIs are
297
        // appended correctly.
298
        // @see: https://tools.ietf.org/html/rfc3986#section-5.2
299
        $this->clientConfig['base_uri'] = preg_replace(
300
            '#([^/])$#',
301
            '$1/',
302
            $this->clientConfig['base_uri']
303
        );
304
        // Make certain that config headers exist.
305
        $this->clientConfig['headers'] = $this->clientConfig['headers'] ?? [];
306
        // Add the User Agent header if it isn't already part of the headers.
307
        if (empty($this->clientConfig['headers']['User-Agent'])) {
308
            $this->clientConfig['headers']['User-Agent'] = $this->getUserAgent();
309
        }
310
        // Add Content Type header if not set.
311
        if (empty($this->clientConfig['headers']['Content-Type'])) {
312
            $this->clientConfig['headers']['Content-Type'] = 'application/json';
313
        }
314
315
        // Allow additional headers to be set by implementing services
316
        $this->clientConfig['headers'] = array_merge($this->clientConfig['headers'], $this->getCustomHeaders());
317
        return $this;
318
    }
319
320
    /**
321
     * Makes a call to get a client response based on the client name.
322
     *
323
     * Note, this receives a Symfony request, but uses a PSR7 Request to Auth.
324
     *
325
     * @param \Symfony\Component\HttpFoundation\Request $request
326
     *   Request.
327
     *
328
     * @return \Acquia\Hmac\KeyInterface|bool
329
     *   Authentication Key, FALSE otherwise.
330
     */
331
    public function authenticate(Request $request) {
332
        if (!$this->getClient()) {
333
            return FALSE;
334
        }
335
336
337
338
        $keys = [
339
            $this->clientCredentials['api_key'] => $this->client->getSettings()->getSecretKey(),
340
            'Webhook' => $this->client->getSettings()->getSharedSecret(),
341
        ];
342
        $keyLoader = new KeyLoader($keys);
343
344
        $authenticator = new RequestAuthenticator($keyLoader);
345
346
        $http_message_factory = $this->createPsrFactory();
347
        $psr7_request = $http_message_factory->createRequest($request);
348
349
        try {
350
            return $authenticator->authenticate($psr7_request);
351
        }
352
        catch (KeyNotFoundException $exception) {
353
            $this->loggerFactory
354
                ->get('acquia_contenthub')
355
                ->debug('HMAC validation failed. [authorization_header = %authorization_header]', [
356
                    '%authorization_header' => $request->headers->get('authorization'),
357
                ]);
358
        }
359
360
        return FALSE;
361
    }
362
363
    /**
364
     * Retrieve custom headers to append to normalized headers.
365
     *
366
     * Service Classes should override this method if they use custom headers.
367
     *
368
     * @return array
369
     */
370
    public function getCustomHeaders() {
371
        return [];
372
    }
373
374
    /**
375
     * Sets and returns the API Name.
376
     *
377
     * Implementing classes must set $apiName with this function.
378
     *
379
     * @return string
380
     *   The Human Readable Version of the API implementing this class.
381
     */
382
    abstract public function setApiName(): string;
383
384
    /**
385
     * Sets and returns the JSON Schema Path.
386
     *
387
     * Implementing classes must set $schemaPath with this function.
388
     *
389
     * @return string
390
     *   The full path to the JSON Schema Description.
391
     */
392
    abstract public function setSchemaPath(): string;
393
394
    /**
395
     * Services use different headers to set their API key.
396
     *
397
     * @return string
398
     */
399
    abstract public function getServiceApiHeader(): string;
400
401
    /**
402
     * Get the service realm
403
     *
404
     * @return string
405
     *   The Service Realm Machine Name.
406
     */
407
    abstract public function getRealm(): string;
408
409
    /**
410
     * Get the service user agent for the library.
411
     *
412
     * @return string
413
     */
414
    abstract public function getUserAgent(): string;
415
}
416