Completed
Pull Request — master (#25)
by
unknown
01:28 queued 30s
created

BaseUrlSigner::hasValidSignature()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 13
rs 9.8333
c 0
b 0
f 0
cc 1
nc 1
nop 1
1
<?php
2
3
namespace Spatie\UrlSigner;
4
5
use DateTime;
6
use League\Uri\Http;
7
use League\Uri\QueryString;
8
use Psr\Http\Message\UriInterface;
9
use Spatie\UrlSigner\Exceptions\InvalidExpiration;
10
use Spatie\UrlSigner\Exceptions\InvalidSignatureKey;
11
12
abstract class BaseUrlSigner implements UrlSigner
13
{
14
    /**
15
     * The key that is used to generate secure signatures.
16
     *
17
     * @var string
18
     */
19
    protected $signatureKey;
20
21
    /**
22
     * The URL's query parameter name for the expiration.
23
     *
24
     * @var string
25
     */
26
    protected $expiresParameter;
27
28
    /**
29
     * The URL's query parameter name for the signature.
30
     *
31
     * @var string
32
     */
33
    protected $signatureParameter;
34
35
    /**
36
     * @param string $signatureKey
37
     * @param string $expiresParameter
38
     * @param string $signatureParameter
39
     *
40
     * @throws InvalidSignatureKey
41
     */
42
    public function __construct($signatureKey, $expiresParameter = 'expires', $signatureParameter = 'signature')
43
    {
44
        if ($signatureKey == '') {
45
            throw new InvalidSignatureKey('The signature key is empty');
46
        }
47
48
        $this->signatureKey = $signatureKey;
49
        $this->expiresParameter = $expiresParameter;
50
        $this->signatureParameter = $signatureParameter;
51
    }
52
53
    /**
54
     * Get a secure URL to a controller action.
55
     *
56
     * @param string        $url
57
     * @param \DateTime|int $expiration
58
     *
59
     * @throws InvalidExpiration
60
     *
61
     * @return string
62
     */
63
    public function sign($url, $expiration)
64
    {
65
        $url = Http::createFromString($url);
66
67
        $expiration = $this->getExpirationTimestamp($expiration);
68
        $signature = $this->createSignature((string) $url, $expiration);
69
70
        return (string) $this->signUrl($url, $expiration, $signature);
71
    }
72
73
    /**
74
     * Add expiration and signature query parameters to an url.
75
     *
76
     * @param UriInterface $url
77
     * @param string       $expiration
78
     * @param string       $signature
79
     *
80
     * @return \League\Url\UrlImmutable
81
     */
82
    protected function signUrl(UriInterface $url, $expiration, $signature)
83
    {
84
        $query = QueryString::extract($url->getQuery());
85
86
        $query[$this->expiresParameter] = $expiration;
87
        $query[$this->signatureParameter] = $signature;
88
89
        return $url->withQuery($this->buildQueryStringFromArray($query));
90
    }
91
92
    /**
93
     * Validate a signed url.
94
     *
95
     * @param string $url
96
     *
97
     * @return bool
98
     */
99
    public function validate($url)
100
    {
101
        $url = Http::createFromString($url);
102
103
        $query = QueryString::extract($url->getQuery());
104
105
        if ($this->isMissingAQueryParameter($query)) {
106
            return false;
107
        }
108
109
        $expiration = $query[$this->expiresParameter];
110
111
        if (!$this->isFuture($expiration)) {
112
            return false;
113
        }
114
115
        if (!$this->hasValidSignature($url)) {
116
            return false;
117
        }
118
119
        return true;
120
    }
121
122
    /**
123
     * Check if a query is missing a necessary parameter.
124
     *
125
     * @param array $query
126
     *
127
     * @return bool
128
     */
129
    protected function isMissingAQueryParameter(array $query)
130
    {
131
        if (!isset($query[$this->expiresParameter])) {
132
            return true;
133
        }
134
135
        if (!isset($query[$this->signatureParameter])) {
136
            return true;
137
        }
138
139
        return false;
140
    }
141
142
    /**
143
     * Check if a timestamp is in the future.
144
     *
145
     * @param int $timestamp
146
     *
147
     * @return bool
148
     */
149
    protected function isFuture($timestamp)
150
    {
151
        return ((int) $timestamp) >= (new DateTime())->getTimestamp();
152
    }
153
154
    /**
155
     * Retrieve the intended URL by stripping off the UrlSigner specific parameters.
156
     *
157
     * @param UriInterface $url
158
     *
159
     * @return UriInterface
160
     */
161
    protected function getIntendedUrl(UriInterface $url)
162
    {
163
        $intendedQuery = QueryString::extract($url->getQuery());
164
165
        unset($intendedQuery[$this->expiresParameter]);
166
        unset($intendedQuery[$this->signatureParameter]);
167
168
        return $url->withQuery($this->buildQueryStringFromArray($intendedQuery));
169
    }
170
171
    /**
172
     * Retrieve the expiration timestamp for a link based on an absolute DateTime or a relative number of days.
173
     *
174
     * @param \DateTime|int $expiration The expiration date of this link.
175
     *                                  - DateTime: The value will be used as expiration date
176
     *                                  - int: The expiration time will be set to X days from now
177
     *
178
     * @throws \Spatie\UrlSigner\Exceptions\InvalidExpiration
179
     *
180
     * @return string
181
     */
182
    protected function getExpirationTimestamp($expiration)
183
    {
184
        if (is_int($expiration)) {
185
            $expiration = (new DateTime())->modify((int) $expiration.' days');
186
        }
187
188
        if (!$expiration instanceof DateTime) {
189
            throw new InvalidExpiration('Expiration date must be an instance of DateTime or an integer');
190
        }
191
192
        if (!$this->isFuture($expiration->getTimestamp())) {
193
            throw new InvalidExpiration('Expiration date must be in the future');
194
        }
195
196
        return (string) $expiration->getTimestamp();
197
    }
198
199
    /**
200
     * Determine if the url has a forged signature.
201
     *
202
     * @param UriInterface $url
203
     *
204
     * @return bool
205
     */
206
    protected function hasValidSignature(UriInterface $url)
207
    {
208
        $query = QueryString::extract($url->getQuery());
209
210
        $expiration = $query[$this->expiresParameter];
211
        $providedSignature = $query[$this->signatureParameter];
212
213
        $intendedUrl = $this->getIntendedUrl($url);
214
215
        $validSignature = $this->createSignature($intendedUrl, $expiration);
216
217
        return hash_equals($validSignature, $providedSignature);
218
    }
219
220
    /**
221
     * Turn a key => value associate array into a query string.
222
     *
223
     * @param array $query
224
     *
225
     * @return string|null
226
     */
227
    protected function buildQueryStringFromArray(array $query)
228
    {
229
        $buildQuery = [];
230
        foreach ($query as $key => $value) {
231
            $buildQuery[] = [$key, $value];
232
        }
233
234
        return QueryString::build($buildQuery);
235
    }
236
}
237