Completed
Push — master ( 881994...60bec7 )
by Sam
02:07
created

UrlSigner::setCurrentTimestamp()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
1
<?php
2
declare(strict_types=1);
3
4
5
namespace SamIT\Yii2\UrlSigner;
6
7
8
use yii\base\Component;
9
use yii\base\InvalidConfigException;
10
use yii\helpers\StringHelper;
11
12
class UrlSigner extends Component
13
{
14
    /**
15
     * @var string The name of the URL param for the HMAC
16
     */
17
    public $hmacParam = 'hmac';
18
19
    /**
20
     * @var string The name of the URL param for the parameters
21
     */
22
    public $paramsParam = 'params';
23
24
    /**
25
     * @var string The name of the URL param for the expiration date time
26
     */
27
    public $expirationParam = 'expires';
28
29
    /**
30
     * Note that expiration dates cannot be disabled. If you really need to you can set a longer duration for the links.
31
     * @var \DateInterval The default interval for link validity (default: 1 week)
32
     */
33
    private $_defaultExpirationInterval;
34
35
    /**
36
     * Stores the current timestamp, primarily used for testing.
37
     * @var int
38
     */
39
    private $_currentTimestamp;
40
41
    /**
42
     * @var string
43
     */
44
    public $secret;
45
46 24
    public function init(): void
47
    {
48 24
        parent::init();
49 24
        if (!isset($this->_defaultExpirationInterval)) {
50 20
            $this->setDefaultExpirationInterval('P7D');
51
        }
52 24
        if (empty($this->secret)
53 24
            || empty($this->hmacParam)
54 24
            || empty($this->paramsParam)
55 24
            || empty($this->expirationParam)
56
        ) {
57 2
            throw new InvalidConfigException('The following configuration params are required: secret, hmacParam, paramsParam and expirationParam');
58
        }
59
60
61
    }
62
63 24
    public function setDefaultExpirationInterval(string $interval): void
64
    {
65 24
        $this->_defaultExpirationInterval = new \DateInterval($interval);
66
    }
67
68 2
    public function setCurrentTimestamp(?int $time): void
69
    {
70 2
        $this->_currentTimestamp = $time;
71
    }
72
73
    /**
74
     * Calculates the HMAC for a URL.
75
     **/
76 18
    public function calculateHMAC(
77
        array $params,
78
        string $route
79
    ): string {
80 18
        if (isset($params[0])) {
81 18
            unset($params[0]);
82
        }
83
84 18
        \ksort($params);
85
86 18
        $hash = \hash_hmac('sha256',
87 18
            \trim($route, '/') . '|' . \implode('#', $params),
88 18
            $this->secret,
89 18
            true
90
        );
91
92 18
        return $this->urlEncode($hash);
93
    }
94
95
    /**
96
     * This adds an HMAC to a list of query params.
97
     * If
98
     * @param array $queryParams List of query parameters
99
     * @param bool $allowAddition Whether to allow extra parameters to be added.
100
     * @throws \Exception
101
     * @return void
102
     */
103 20
    public function signParams(
104
        array &$queryParams,
105
        $allowAddition = true,
106
        ?\DateTimeInterface $expiration = null
107
    ): void {
108 20
        if (isset($queryParams[$this->hmacParam])) {
109 2
            throw new \RuntimeException("HMAC param is already present");
110
        }
111
112 20
        $route = $queryParams[0];
113
114 20
        if (\strncmp($route, '/', 1) !== 0) {
115 2
            throw new \RuntimeException("Route must be absolute (start with /)");
116
        }
117
118 18
        $this->addExpiration($queryParams, $expiration);
119 18
        if ($allowAddition) {
120 12
            $this->addParamKeys($queryParams);
121
        }
122
123 18
        $queryParams[$this->hmacParam] = $this->calculateHMAC($queryParams, $route);
124
    }
125
126
    /**
127
     * Adds the expiration param if needed.
128
     */
129 18
    private function addExpiration(array &$params, ?\DateTimeInterface $expiration = null): void
130
    {
131 18
        if (!empty($this->expirationParam)) {
132 16
            if (!isset($expiration)) {
133 16
                $expiration = (new \DateTime('@' . $this->time()))->add($this->_defaultExpirationInterval);
134
            }
135 16
            $params[$this->expirationParam] = $expiration->getTimestamp();
136
        }
137
    }
138
139 16
    private function time(): int
140
    {
141 16
        return $this->_currentTimestamp ?? \time();
142
    }
143 10
    private function checkExpiration(array $params): bool
144
    {
145
        // Check expiration date.
146 10
        return !isset($params[$this->expirationParam]) || $params[$this->expirationParam] > $this->time();
147
    }
148
149
    /**
150
     * Adds the keys of all params to the param array so it is included for signing.
151
     * @param array $params
152
     */
153 12
    private function addParamKeys(array &$params): void
154
    {
155 12
        $keys = \array_keys($params);
156 12
        if ($keys[0] === 0) {
157 12
            unset($keys[0]);
158
        }
159 12
        $params[$this->paramsParam] = \implode(',', $keys);
160
    }
161
162
    /**
163
     * Extracts the signed params from an array of params.
164
     * @param array $params
165
     * @return array
166
     */
167 14
    private function getSignedParams(array $params): array
168
    {
169 14
        if (empty($params[$this->paramsParam])) {
170
            // HMAC itself is never signed.
171 6
            unset($params[$this->hmacParam]);
172 6
            return $params;
173
        }
174
175 8
        $signedParams = [];
176 8
        $signedParams[$this->paramsParam] = $params[$this->paramsParam];
177
178 8
        foreach(\explode(',', $params[$this->paramsParam]) as $signedParam) {
179 8
            $signedParams[$signedParam] = $params[$signedParam] ?? null;
180
        }
181
182 8
        return $signedParams;
183
    }
184
185
    /**
186
     * Verifies the params for a specific route.
187
     * Checks that the HMAC is present and valid.
188
     * Checks that the HMAC is not expired.
189
     * @param array $params
190
     * @throws \Exception
191
     * @return bool
192
     */
193 14
    public function verify(array $params, string $route):bool
194
    {
195 14
        if (!isset($params[$this->hmacParam])) {
196 2
           return false;
197
        }
198 14
        $hmac = $params[$this->hmacParam];
199
200 14
        $signedParams = $this->getSignedParams($params);
201
202 14
        $calculated = $this->calculateHMAC($signedParams, $route);
203 14
        if (!\hash_equals($calculated, $hmac)) {
204 4
            return false;
205
        }
206
207 10
        return $this->checkExpiration($params);
208
    }
209
210 18
    private function urlEncode(string $bytes): string
211
    {
212 18
        return \trim(StringHelper::base64UrlEncode($bytes), '=');
213
    }
214
}
215