DigiSign   B
last analyzed

Complexity

Total Complexity 44

Size/Duplication

Total Lines 256
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 44
eloc 83
c 1
b 0
f 0
dl 0
loc 256
rs 8.8798

26 Methods

Rating   Name   Duplication   Size   Complexity  
A my() 0 3 1
A envelopes() 0 3 1
A setCache() 0 10 2
A report() 0 3 1
A createBearer() 0 3 1
A request() 0 10 2
A setClient() 0 3 1
A getCredentials() 0 10 2
A files() 0 3 1
A validateSignature() 0 17 4
A setSignatureTolerance() 0 3 1
A useTesting() 0 6 2
A labels() 0 3 1
A removeVersion() 0 3 1
A account() 0 3 1
A enums() 0 3 1
A webhooks() 0 3 1
A auth() 0 3 1
B __construct() 0 43 10
A deliveries() 0 3 1
A images() 0 3 1
A createUserAgent() 0 11 3
A setApiBase() 0 3 1
A envelopeTemplates() 0 3 1
A addVersion() 0 3 1
A setCredentials() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like DigiSign often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DigiSign, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace DigitalCz\DigiSign;
6
7
use DigitalCz\DigiSign\Auth\ApiKeyCredentials;
8
use DigitalCz\DigiSign\Auth\CachedCredentials;
9
use DigitalCz\DigiSign\Auth\Credentials;
10
use DigitalCz\DigiSign\Endpoint\AccountEndpoint;
11
use DigitalCz\DigiSign\Endpoint\AuthEndpoint;
12
use DigitalCz\DigiSign\Endpoint\DeliveriesEndpoint;
13
use DigitalCz\DigiSign\Endpoint\EndpointInterface;
14
use DigitalCz\DigiSign\Endpoint\EnumsEndpoint;
15
use DigitalCz\DigiSign\Endpoint\EnvelopesEndpoint;
16
use DigitalCz\DigiSign\Endpoint\EnvelopeTemplatesEndpoint;
17
use DigitalCz\DigiSign\Endpoint\FilesEndpoint;
18
use DigitalCz\DigiSign\Endpoint\ImagesEndpoint;
19
use DigitalCz\DigiSign\Endpoint\LabelsEndpoint;
20
use DigitalCz\DigiSign\Endpoint\MyEndpoint;
21
use DigitalCz\DigiSign\Endpoint\ReportEndpoint;
22
use DigitalCz\DigiSign\Endpoint\WebhooksEndpoint;
23
use DigitalCz\DigiSign\Exception\InvalidSignatureException;
24
use InvalidArgumentException;
25
use LogicException;
26
use Psr\Http\Message\ResponseInterface;
27
use Psr\SimpleCache\CacheInterface;
28
29
final class DigiSign implements EndpointInterface
30
{
31
    public const VERSION = '1.11.0';
32
    public const API_BASE = 'https://api.digisign.org';
33
    public const API_BASE_TESTING = 'https://api.digisign.digital.cz';
34
35
    /** @var string The base URL for requests */
36
    private $apiBase = self::API_BASE;
37
38
    /** @var Credentials The credentials used to authenticate to API */
39
    private $credentials;
40
41
    /** @var DigiSignClientInterface The client used to send requests */
42
    private $client;
43
44
    /** @var array<string, string> */
45
    private $versions = [];
46
47
    /** @var int The tolerance for webhook signature age validation (in seconds) */
48
    private $signatureTolerance = 300;
49
50
    /**
51
     * Available options:
52
     *  access_key          - string; ApiKey access key
53
     *  secret_key          - string; ApiKey secret key
54
     *  credentials         - DigitalCz\DigiSign\Auth\Credentials instance
55
     *  client              - DigitalCz\DigiSign\DigiSignClient instance with your custom PSR17/18 objects
56
     *  http_client         - Psr\Http\Client\ClientInterface instance of your custom PSR18 client
57
     *  cache               - Psr\SimpleCache\CacheInterface for caching Credentials auth Tokens
58
     *  testing             - bool; whether to use testing or production API
59
     *  api_base            - string; override the base API url
60
     *  signature_tolerance - int; The tolerance for webhook signature age validation (in seconds)
61
     *
62
     * @param mixed[] $options
63
     */
64
    public function __construct(array $options = [])
65
    {
66
        $httpClient = $options['http_client'] ?? null;
67
        $this->setClient($options['client'] ?? new DigiSignClient($httpClient));
68
        $this->useTesting($options['testing'] ?? false);
69
        $this->addVersion('digitalcz/digisign', self::VERSION);
70
        $this->addVersion('PHP', PHP_VERSION);
71
72
        if (isset($options['api_base'])) {
73
            if (!is_string($options['api_base'])) {
74
                throw new InvalidArgumentException('Invalid value for "api_base" option');
75
            }
76
77
            $this->setApiBase($options['api_base']);
78
        }
79
80
        if (isset($options['access_key'], $options['secret_key'])) {
81
            $this->setCredentials(new ApiKeyCredentials($options['access_key'], $options['secret_key']));
82
        }
83
84
        if (isset($options['credentials'])) {
85
            if (!$options['credentials'] instanceof Credentials) {
86
                throw new InvalidArgumentException('Invalid value for "credentials" option');
87
            }
88
89
            $this->setCredentials($options['credentials']);
90
        }
91
92
        // if cache is provided, wrap Credentials with cache decorator
93
        if (isset($options['cache'])) {
94
            if (!$options['cache'] instanceof CacheInterface) {
95
                throw new InvalidArgumentException('Invalid value for "cache" option');
96
            }
97
98
            $this->setCache($options['cache']);
99
        }
100
101
        if (isset($options['signature_tolerance'])) {
102
            if (!is_int($options['signature_tolerance'])) {
103
                throw new InvalidArgumentException('Invalid value for "signature_tolerance" option');
104
            }
105
106
            $this->setSignatureTolerance($options['signature_tolerance']);
107
        }
108
    }
109
110
    /**
111
     * @throws InvalidSignatureException
112
     */
113
    public function validateSignature(string $payload, string $header, string $secret): void
114
    {
115
        if (preg_match('/t=(?<t>\d+),s=(?<s>\w+)/', $header, $matches) !== 1) {
116
            throw new InvalidSignatureException('Unable to parse signature header');
117
        }
118
119
        $ts = (int)($matches['t'] ?? 0);
120
        $signature = $matches['s'] ?? '';
121
122
        if ($ts < time() - $this->signatureTolerance) {
123
            throw new InvalidSignatureException("Request is older than {$this->signatureTolerance} seconds");
124
        }
125
126
        $expected = hash_hmac('sha256', $ts . '.' . $payload, $secret);
127
128
        if (hash_equals($expected, $signature) === false) {
129
            throw new InvalidSignatureException('Signature is invalid');
130
        }
131
    }
132
133
    public function setCache(CacheInterface $cache): void
134
    {
135
        $credentials = $this->getCredentials();
136
137
        // if credentials are already decorated, do not double wrap, but get inner
138
        if ($credentials instanceof CachedCredentials) {
139
            $credentials = $credentials->getInner();
140
        }
141
142
        $this->setCredentials(new CachedCredentials($credentials, $cache));
143
    }
144
145
    public function getCredentials(): Credentials
146
    {
147
        if (!isset($this->credentials)) {
148
            throw new LogicException(
149
                'No credentials were provided, Please use setCredentials() ' .
150
                'or constructor options to set them.'
151
            );
152
        }
153
154
        return $this->credentials;
155
    }
156
157
    public function setCredentials(Credentials $credentials): void
158
    {
159
        $this->credentials = $credentials;
160
    }
161
162
    public function setClient(DigiSignClientInterface $client): void
163
    {
164
        $this->client = $client;
165
    }
166
167
    public function useTesting(bool $bool = true): void
168
    {
169
        if ($bool) {
170
            $this->setApiBase(self::API_BASE_TESTING);
171
        } else {
172
            $this->setApiBase(self::API_BASE);
173
        }
174
    }
175
176
    public function setApiBase(string $apiBase): void
177
    {
178
        $this->apiBase = rtrim(trim($apiBase), '/');
179
    }
180
181
    public function addVersion(string $tool, string $version = ''): void
182
    {
183
        $this->versions[$tool] = $version;
184
    }
185
186
    public function removeVersion(string $tool): void
187
    {
188
        unset($this->versions[$tool]);
189
    }
190
191
    public function setSignatureTolerance(int $signatureTolerance): void
192
    {
193
        $this->signatureTolerance = $signatureTolerance;
194
    }
195
196
    /** @inheritDoc */
197
    public function request(string $method, string $path = '', array $options = []): ResponseInterface
198
    {
199
        $options['user-agent'] = $this->createUserAgent();
200
201
        // disable authorization header if options[no_auth]=true
202
        if (($options['no_auth'] ?? false) !== true) {
203
            $options['auth_bearer'] = $options['auth_bearer'] ?? $this->createBearer();
204
        }
205
206
        return $this->client->request($method, $this->apiBase . $path, $options);
207
    }
208
209
    public function auth(): AuthEndpoint
210
    {
211
        return new AuthEndpoint($this);
212
    }
213
214
    public function account(): AccountEndpoint
215
    {
216
        return new AccountEndpoint($this);
217
    }
218
219
    public function envelopes(): EnvelopesEndpoint
220
    {
221
        return new EnvelopesEndpoint($this);
222
    }
223
224
    public function envelopeTemplates(): EnvelopeTemplatesEndpoint
225
    {
226
        return new EnvelopeTemplatesEndpoint($this);
227
    }
228
229
    public function deliveries(): DeliveriesEndpoint
230
    {
231
        return new DeliveriesEndpoint($this);
232
    }
233
234
    public function files(): FilesEndpoint
235
    {
236
        return new FilesEndpoint($this);
237
    }
238
239
    public function images(): ImagesEndpoint
240
    {
241
        return new ImagesEndpoint($this);
242
    }
243
244
    public function labels(): LabelsEndpoint
245
    {
246
        return new LabelsEndpoint($this);
247
    }
248
249
    public function webhooks(): WebhooksEndpoint
250
    {
251
        return new WebhooksEndpoint($this);
252
    }
253
254
    public function enums(): EnumsEndpoint
255
    {
256
        return new EnumsEndpoint($this);
257
    }
258
259
    public function my(): MyEndpoint
260
    {
261
        return new MyEndpoint($this);
262
    }
263
264
    public function report(): ReportEndpoint
265
    {
266
        return new ReportEndpoint($this);
267
    }
268
269
    private function createUserAgent(): string
270
    {
271
        $userAgent = '';
272
273
        foreach ($this->versions as $tool => $version) {
274
            $userAgent .= $tool;
275
            $userAgent .= $version !== '' ? ":$version" : '';
276
            $userAgent .= ' ';
277
        }
278
279
        return $userAgent;
280
    }
281
282
    private function createBearer(): string
283
    {
284
        return $this->getCredentials()->provide($this)->getToken();
285
    }
286
}
287