GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

UrlSigner::addExpiration()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 5
cts 5
cp 1
rs 9.9666
c 0
b 0
f 0
cc 3
nc 3
nop 2
crap 3
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 26
    public function init(): void
47
    {
48 26
        parent::init();
49 26
        if (!isset($this->_defaultExpirationInterval)) {
50 22
            $this->setDefaultExpirationInterval('P7D');
51
        }
52 26
        if (empty($this->secret)
53 26
            || empty($this->hmacParam)
54 26
            || empty($this->paramsParam)
55 26
            || 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 26
    public function setDefaultExpirationInterval(string $interval): void
64
    {
65 26
        $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
144 10
    private function checkExpiration(array $params): void
145
    {
146
        // Check expiration date.
147 10
        if (isset($params[$this->expirationParam])
148 10
            && $params[$this->expirationParam] <= $this->time()
149
        ) {
150 2
            throw new ExpiredLinkException();
151
        }
152
    }
153
154
    /**
155
     * Adds the keys of all params to the param array so it is included for signing.
156
     * @param array $params
157
     */
158 12
    private function addParamKeys(array &$params): void
159
    {
160 12
        $keys = \array_keys($params);
161 12
        if ($keys[0] === 0) {
162 12
            unset($keys[0]);
163
        }
164 12
        $params[$this->paramsParam] = \implode(',', $keys);
165
    }
166
167
    /**
168
     * Extracts the signed params from an array of params.
169
     * @param array $params
170
     * @return array
171
     */
172 14
    private function getSignedParams(array $params): array
173
    {
174 14
        if (empty($params[$this->paramsParam])) {
175
            // HMAC itself is never signed.
176 6
            unset($params[$this->hmacParam]);
177 6
            return $params;
178
        }
179
180 8
        $signedParams = [];
181 8
        $signedParams[$this->paramsParam] = $params[$this->paramsParam];
182
183 8
        foreach(\explode(',', $params[$this->paramsParam]) as $signedParam) {
184 8
            $signedParams[$signedParam] = $params[$signedParam] ?? null;
185
        }
186
187 8
        return $signedParams;
188
    }
189
190
    /**
191
     * Verifies the params for a specific route.
192
     * Checks that the HMAC is present and valid.
193
     * Checks that the HMAC is not expired.
194
     * @param array $params
195
     * @throws \Exception
196
     * @return bool
197
     */
198 16
    public function verify(array $params, string $route):void
199
    {
200 16
        if (!isset($params[$this->hmacParam])) {
201 4
            throw new MissingHmacException();
202
        }
203 14
        $hmac = $params[$this->hmacParam];
204
205 14
        $signedParams = $this->getSignedParams($params);
206
207 14
        $calculated = $this->calculateHMAC($signedParams, $route);
208 14
        if (!\hash_equals($calculated, $hmac)) {
209 4
            throw new InvalidHmacException();
210
        }
211
212 10
        $this->checkExpiration($params);
213
    }
214
215 18
    private function urlEncode(string $bytes): string
216
    {
217 18
        return \trim(StringHelper::base64UrlEncode($bytes), '=');
218
    }
219
}
220