Passed
Pull Request — 1.x (#65)
by Pavel
01:58
created

DigiSign::setClient()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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