Completed
Pull Request — master (#25)
by
unknown
09:40
created

BaseUrlSigner::buildQueryStringFromArray()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 9
rs 9.9666
c 0
b 0
f 0
cc 2
nc 2
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
     * @return string
60
     * @throws InvalidExpiration
61
     */
62
    public function sign($url, $expiration)
63
    {
64
        $url = Http::createFromString($url);
65
66
        $expiration = $this->getExpirationTimestamp($expiration);
67
        $signature = $this->createSignature((string) $url, $expiration);
68
69
        return (string) $this->signUrl($url, $expiration, $signature);
70
    }
71
72
    /**
73
     * Add expiration and signature query parameters to an url.
74
     *
75
     * @param UriInterface $url
76
     * @param string       $expiration
77
     * @param string       $signature
78
     *
79
     * @return \League\Url\UrlImmutable
80
     */
81
    protected function signUrl(UriInterface $url, $expiration, $signature)
82
    {
83
        $query = QueryString::extract($url->getQuery());
84
85
        $query[$this->expiresParameter] = $expiration;
86
        $query[$this->signatureParameter] = $signature;
87
88
        return $url->withQuery($this->buildQueryStringFromArray($query));
89
    }
90
91
    /**
92
     * Validate a signed url.
93
     *
94
     * @param string $url
95
     *
96
     * @return bool
97
     */
98
    public function validate($url)
99
    {
100
        $url = Http::createFromString($url);
101
102
        $query = QueryString::extract($url->getQuery());
103
104
        if ($this->isMissingAQueryParameter($query)) {
105
            return false;
106
        }
107
108
        $expiration = $query[$this->expiresParameter];
109
110
        if (!$this->isFuture($expiration)) {
111
            return false;
112
        }
113
114
        if (!$this->hasValidSignature($url)) {
115
            return false;
116
        }
117
118
        return true;
119
    }
120
121
    /**
122
     * Check if a query is missing a necessary parameter.
123
     *
124
     * @param array $query
125
     *
126
     * @return bool
127
     */
128
    protected function isMissingAQueryParameter(array $query)
129
    {
130
        if (!isset($query[$this->expiresParameter])) {
131
            return true;
132
        }
133
134
        if (!isset($query[$this->signatureParameter])) {
135
            return true;
136
        }
137
138
        return false;
139
    }
140
141
    /**
142
     * Check if a timestamp is in the future.
143
     *
144
     * @param int $timestamp
145
     *
146
     * @return bool
147
     */
148
    protected function isFuture($timestamp)
149
    {
150
        return ((int) $timestamp) >= (new DateTime())->getTimestamp();
151
    }
152
153
    /**
154
     * Retrieve the intended URL by stripping off the UrlSigner specific parameters.
155
     *
156
     * @param UriInterface $url
157
     *
158
     * @return UriInterface
159
     */
160
    protected function getIntendedUrl(UriInterface $url)
161
    {
162
        $intendedQuery = QueryString::extract($url->getQuery());
163
164
        unset($intendedQuery[$this->expiresParameter]);
165
        unset($intendedQuery[$this->signatureParameter]);
166
167
        return $url->withQuery($this->buildQueryStringFromArray($intendedQuery));
168
    }
169
170
    /**
171
     * Retrieve the expiration timestamp for a link based on an absolute DateTime or a relative number of days.
172
     *
173
     * @param \DateTime|int $expiration The expiration date of this link.
174
     *                                  - DateTime: The value will be used as expiration date
175
     *                                  - int: The expiration time will be set to X days from now
176
     *
177
     * @throws \Spatie\UrlSigner\Exceptions\InvalidExpiration
178
     *
179
     * @return string
180
     */
181
    protected function getExpirationTimestamp($expiration)
182
    {
183
        if (is_int($expiration)) {
184
            $expiration = (new DateTime())->modify((int) $expiration.' days');
185
        }
186
187
        if (!$expiration instanceof DateTime) {
188
            throw new InvalidExpiration('Expiration date must be an instance of DateTime or an integer');
189
        }
190
191
        if (!$this->isFuture($expiration->getTimestamp())) {
192
            throw new InvalidExpiration('Expiration date must be in the future');
193
        }
194
195
        return (string) $expiration->getTimestamp();
196
    }
197
198
    /**
199
     * Determine if the url has a forged signature.
200
     *
201
     * @param UriInterface $url
202
     *
203
     * @return bool
204
     */
205
    protected function hasValidSignature(UriInterface $url)
206
    {
207
        $query = QueryString::extract($url->getQuery());
208
209
        $expiration = $query[$this->expiresParameter];
210
        $providedSignature = $query[$this->signatureParameter];
211
212
        $intendedUrl = $this->getIntendedUrl($url);
213
214
        $validSignature = $this->createSignature($intendedUrl, $expiration);
215
216
        return hash_equals($validSignature, $providedSignature);
217
    }
218
219
    /**
220
     * Turn a key => value associate array into a query string
221
     *
222
     * @param array $query
223
     *
224
     * @return string|null
225
     */
226
    protected function buildQueryStringFromArray(array $query)
227
    {
228
        $buildQuery = [];
229
        foreach ($query as $key => $value) {
230
            $buildQuery[] = [$key, $value];
231
        }
232
233
        return QueryString::build($buildQuery);
234
    }
235
}
236