Signer   A
last analyzed

Complexity

Total Complexity 24

Size/Duplication

Total Lines 193
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 24
eloc 47
c 1
b 0
f 0
dl 0
loc 193
rs 10

12 Methods

Rating   Name   Duplication   Size   Complexity  
A decrypt() 0 16 5
A encrypt() 0 20 2
A removeParams() 0 10 3
A makeQueryString() 0 3 1
A checkFields() 0 8 6
A retrieveHash() 0 3 1
A sign() 0 3 1
A addSeconds() 0 3 1
A parseQueryString() 0 7 1
A validate() 0 3 1
A __construct() 0 13 1
A expired() 0 3 1
1
<?php
2
3
namespace Tleckie\UrlSigner;
4
5
use InvalidArgumentException;
6
use Tleckie\UrlSigner\Exception\ExpiredUriException;
7
use Tleckie\UrlSigner\Exception\UnsignedException;
8
use HttpSoft\Message\UriFactory;
9
use Psr\Http\Message\UriFactoryInterface;
10
use Psr\Http\Message\UriInterface;
11
use function hash;
12
use function http_build_query;
13
use function parse_str;
14
use function sprintf;
15
16
/**
17
 * Class Signer
18
 *
19
 * @package Tleckie\UrlSigner
20
 * @author  Teodoro Leckie Westberg <[email protected]>
21
 */
22
class Signer
23
{
24
    /** @var string */
25
    private string $password;
26
27
    /** @var int */
28
    private int $ttl;
29
30
    /** @var string|null */
31
    private string|null $ttlField;
32
33
    /** @var string */
34
    private string $hashField;
35
36
    /** @var UriFactoryInterface */
37
    private UriFactoryInterface $uriFactory;
38
39
    /**
40
     * Signer constructor.
41
     *
42
     * @param string                   $password
43
     * @param string                   $hashField
44
     * @param string|null              $ttlField
45
     * @param int                      $ttl
46
     * @param UriFactoryInterface|null $uriFactory
47
     * @throws InvalidArgumentException
48
     */
49
    public function __construct(
50
        string $password,
51
        string $hashField = 'signature',
52
        string $ttlField = null,
53
        int $ttl = 0,
54
        UriFactoryInterface $uriFactory = null
55
    ) {
56
        $this->password = $password;
57
        $this->ttl = $ttl;
58
        $this->ttlField = $ttlField;
59
        $this->hashField = $hashField;
60
        $this->uriFactory = $uriFactory ?? new UriFactory();
61
        $this->checkFields();
62
    }
63
64
    /**
65
     * @param string $uri
66
     * @return string
67
     * @throws Exception
68
     */
69
    public function sign(string $uri): string
70
    {
71
        return $this->encrypt($uri);
72
    }
73
74
    /**
75
     * @param string $uri
76
     * @return string
77
     * @throws ExpiredUriException
78
     * @throws UnsignedException
79
     */
80
    public function validate(string $uri): string
81
    {
82
        return $this->decrypt($uri);
83
    }
84
85
    /**
86
     * @param string $uri
87
     * @return string
88
     */
89
    private function encrypt(string $uri): string
90
    {
91
        $uri = $this->removeParams(
92
            $this->uriFactory->createUri($uri)
93
        );
94
95
        $params = $this->parseQueryString($uri);
96
97
        if ($this->ttl > 0) {
98
            $params[$this->ttlField] = $this->addSeconds($this->ttl);
99
        }
100
101
        $uri = $uri->withQuery(
102
            $this->makeQueryString($params)
103
        );
104
105
        $params[$this->hashField] = $this->retrieveHash($uri);
106
107
        return $uri->withQuery(
108
            $this->makeQueryString($params)
109
        );
110
    }
111
112
    /**
113
     * @throws InvalidArgumentException
114
     */
115
    private function checkFields()
116
    {
117
        if (empty($this->hashField)) {
118
            throw new InvalidArgumentException(sprintf('Required $hashField argument'));
119
        }
120
121
        if ((empty($this->ttlField) && $this->ttl > 0)  || (!empty($this->ttlField) &&  $this->ttl <= 0)) {
122
            throw new InvalidArgumentException('Required $ttl or $ttlField argument');
123
        }
124
    }
125
126
    /**
127
     * @param UriInterface $uri
128
     * @param array        $keys
129
     * @return UriInterface
130
     */
131
    private function removeParams(UriInterface $uri, array $keys = []): UriInterface
132
    {
133
        $params = $this->parseQueryString($uri);
134
        foreach ($keys as $key) {
135
            if (isset($params[$key])) {
136
                unset($params[$key]);
137
            }
138
        }
139
140
        return $uri->withQuery($this->makeQueryString($params));
141
    }
142
143
    /**
144
     * @param UriInterface $uri
145
     * @return array
146
     */
147
    private function parseQueryString(UriInterface $uri): array
148
    {
149
        $params = [];
150
151
        parse_str($uri->getQuery(), $params);
152
153
        return $params;
154
    }
155
156
    /**
157
     * @param array $params
158
     * @return string
159
     */
160
    private function makeQueryString(array $params = []): string
161
    {
162
        return http_build_query($params);
163
    }
164
165
    /**
166
     * @param int $seconds
167
     * @return int
168
     * @throws Exception
169
     */
170
    private function addSeconds(int $seconds): int
171
    {
172
        return time() + $seconds;
173
    }
174
175
    /**
176
     * @param UriInterface $uri
177
     * @return string
178
     */
179
    private function retrieveHash(UriInterface $uri): string
180
    {
181
        return hash('sha1', sprintf("%s%s", $uri, $this->password));
182
    }
183
184
    /**
185
     * @param string $uri
186
     * @return string
187
     * @throws ExpiredUriException
188
     * @throws UnsignedException
189
     */
190
    private function decrypt(string $uri): string
191
    {
192
        $uri = $this->uriFactory->createUri($uri);
193
        $params = $this->parseQueryString($uri);
194
195
        $clearUri = $this->removeParams($uri, [$this->hashField]);
196
197
        if (isset($params[$this->hashField]) && $params[$this->hashField] === $this->retrieveHash($clearUri)) {
198
            if ($this->ttl > 0 && $this->expired($params[$this->ttlField])) {
199
                throw new ExpiredUriException(sprintf('Expired uri [%s]', $uri));
200
            }
201
202
            return $this->removeParams($clearUri, [$this->ttlField]);
203
        }
204
205
        throw new UnsignedException(sprintf('Decrypt failed [%s]', $uri));
206
    }
207
208
    /**
209
     * @param int $timestamp
210
     * @return bool
211
     */
212
    private function expired(int $timestamp): bool
213
    {
214
        return $timestamp < time();
215
    }
216
}
217