1 | <?php |
||
2 | |||
3 | namespace EmailChangeVerification\Token; |
||
4 | |||
5 | use EmailChangeVerification\User\HasEmailChangeVerification as HasEmailChangeVerificationContract; |
||
6 | use Illuminate\Contracts\Hashing\Hasher as HasherContract; |
||
7 | use Illuminate\Database\ConnectionInterface; |
||
8 | use Illuminate\Support\Carbon; |
||
9 | use Illuminate\Support\Str; |
||
10 | |||
11 | class DatabaseTokenRepository implements TokenRepositoryInterface |
||
12 | { |
||
13 | /** |
||
14 | * The database connection instance. |
||
15 | * |
||
16 | * @var \Illuminate\Database\ConnectionInterface |
||
17 | */ |
||
18 | protected $connection; |
||
19 | |||
20 | /** |
||
21 | * The Hasher implementation. |
||
22 | * |
||
23 | * @var \Illuminate\Contracts\Hashing\Hasher |
||
24 | */ |
||
25 | protected $hasher; |
||
26 | |||
27 | /** |
||
28 | * The token database table. |
||
29 | * |
||
30 | * @var string |
||
31 | */ |
||
32 | protected $table; |
||
33 | |||
34 | /** |
||
35 | * The hashing key. |
||
36 | * |
||
37 | * @var string |
||
38 | */ |
||
39 | protected $hashKey; |
||
40 | |||
41 | /** |
||
42 | * The number of seconds a token should last. |
||
43 | * |
||
44 | * @var int |
||
45 | */ |
||
46 | protected $expires; |
||
47 | |||
48 | /** |
||
49 | * Minimum number of seconds before re-redefining the token. |
||
50 | * |
||
51 | * @var int |
||
52 | */ |
||
53 | protected $throttle; |
||
54 | |||
55 | /** |
||
56 | * Create a new token repository instance. |
||
57 | * |
||
58 | * @param \Illuminate\Database\ConnectionInterface $connection |
||
59 | * @param \Illuminate\Contracts\Hashing\Hasher $hasher |
||
60 | * @param string $table |
||
61 | * @param string $hashKey |
||
62 | * @param int $expires |
||
63 | * @param int $throttle |
||
64 | * @return void |
||
65 | */ |
||
66 | 19 | public function __construct( |
|
67 | ConnectionInterface $connection, |
||
68 | HasherContract $hasher, |
||
69 | $table, |
||
70 | $hashKey, |
||
71 | $expires = 60, |
||
72 | $throttle = 60 |
||
73 | ) { |
||
74 | 19 | $this->table = $table; |
|
75 | 19 | $this->hasher = $hasher; |
|
76 | 19 | $this->hashKey = $hashKey; |
|
77 | 19 | $this->expires = $expires * 60; |
|
78 | 19 | $this->connection = $connection; |
|
79 | 19 | $this->throttle = $throttle; |
|
80 | } |
||
81 | |||
82 | /** |
||
83 | * @inheritDoc |
||
84 | */ |
||
85 | 13 | public function create(HasEmailChangeVerificationContract $user, string $newEmail): string |
|
86 | { |
||
87 | 13 | $email = $user->getEmailForChangeEmail(); |
|
88 | |||
89 | 13 | $this->deleteExisting($user); |
|
90 | |||
91 | // We will create a new, random token for the user so that we can e-mail them |
||
92 | // a safe link to the password reset form. Then we will insert a record in |
||
93 | // the database so that we can verify the token within the actual reset. |
||
94 | 13 | $token = $this->createNewToken(); |
|
95 | |||
96 | 13 | $this->getTable()->insert($this->getPayload($email, $newEmail, $token)); |
|
97 | |||
98 | 13 | return $token; |
|
99 | } |
||
100 | |||
101 | /** |
||
102 | * Delete all existing reset tokens from the database. |
||
103 | * |
||
104 | * @param HasEmailChangeVerificationContract $user |
||
105 | * @return int |
||
106 | */ |
||
107 | 13 | protected function deleteExisting(HasEmailChangeVerificationContract $user) |
|
108 | { |
||
109 | 13 | return $this->getTable()->where('email', $user->getEmailForChangeEmail())->delete(); |
|
110 | } |
||
111 | |||
112 | /** |
||
113 | * Build the record payload for the table. |
||
114 | * |
||
115 | * @param string $email |
||
116 | * @param string $newEmail |
||
117 | * @param string $token |
||
118 | * @return array |
||
119 | */ |
||
120 | 13 | protected function getPayload(string $email, string $newEmail, string $token) |
|
121 | { |
||
122 | 13 | return [ |
|
123 | 13 | 'email' => $email, |
|
124 | 13 | 'new_email' => $newEmail, |
|
125 | 13 | 'token' => $this->hasher->make($token), |
|
126 | 13 | 'created_at' => new Carbon, |
|
127 | 13 | ]; |
|
128 | } |
||
129 | |||
130 | /** |
||
131 | * @inheritDoc |
||
132 | */ |
||
133 | 5 | public function exists(HasEmailChangeVerificationContract $user, string $token, string $newEmail): bool |
|
134 | { |
||
135 | 5 | $record = (array) $this->getTable()->where( |
|
136 | 5 | 'email', |
|
137 | 5 | $user->getEmailForChangeEmail() |
|
138 | 5 | )->where( |
|
139 | 5 | 'new_email', |
|
140 | 5 | $newEmail |
|
141 | 5 | )->first(); |
|
142 | |||
143 | 5 | return $record && |
|
0 ignored issues
–
show
|
|||
144 | 5 | !$this->tokenExpired($record['created_at']) && |
|
145 | 5 | $this->hasher->check($token, $record['token']); |
|
146 | } |
||
147 | |||
148 | /** |
||
149 | * Determine if the token has expired. |
||
150 | * |
||
151 | * @param string $createdAt |
||
152 | * @return bool |
||
153 | */ |
||
154 | 6 | protected function tokenExpired($createdAt) |
|
155 | { |
||
156 | 6 | return Carbon::parse($createdAt)->addSeconds($this->expires)->isPast(); |
|
157 | } |
||
158 | |||
159 | /** |
||
160 | * @inheritDoc |
||
161 | */ |
||
162 | 9 | public function recentlyCreatedToken(HasEmailChangeVerificationContract $user): bool |
|
163 | { |
||
164 | 9 | $record = (array) $this->getTable()->where( |
|
165 | 9 | 'email', |
|
166 | 9 | $user->getEmailForChangeEmail() |
|
167 | 9 | )->first(); |
|
168 | |||
169 | 9 | return $record && $this->tokenRecentlyCreated($record['created_at']); |
|
0 ignored issues
–
show
The expression
$record of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent. Consider making the comparison explicit by using ![]() |
|||
170 | } |
||
171 | |||
172 | /** |
||
173 | * @inheritDoc |
||
174 | */ |
||
175 | 2 | public function lastRequestedEmail(HasEmailChangeVerificationContract $user): ?string |
|
176 | { |
||
177 | 2 | $record = (array) $this->getTable()->where( |
|
178 | 2 | 'email', |
|
179 | 2 | $user->getEmailForChangeEmail() |
|
180 | 2 | )->first(); |
|
181 | |||
182 | 2 | if ($record && !$this->tokenExpired($record['created_at'])) { |
|
0 ignored issues
–
show
The expression
$record of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent. Consider making the comparison explicit by using ![]() |
|||
183 | 1 | return $record['new_email']; |
|
184 | } |
||
185 | |||
186 | 1 | return null; |
|
187 | } |
||
188 | |||
189 | /** |
||
190 | * Determine if the token was recently created. |
||
191 | * |
||
192 | * @param string $createdAt |
||
193 | * @return bool |
||
194 | */ |
||
195 | 1 | protected function tokenRecentlyCreated($createdAt) |
|
196 | { |
||
197 | 1 | if ($this->throttle <= 0) { |
|
198 | return false; |
||
199 | } |
||
200 | |||
201 | 1 | return Carbon::parse($createdAt)->addSeconds( |
|
202 | 1 | $this->throttle |
|
203 | 1 | )->isFuture(); |
|
204 | } |
||
205 | |||
206 | /** |
||
207 | * Delete a token record by user. |
||
208 | * |
||
209 | * @param HasEmailChangeVerificationContract $user |
||
210 | * @return void |
||
211 | */ |
||
212 | 2 | public function delete(HasEmailChangeVerificationContract $user) |
|
213 | { |
||
214 | 2 | $this->deleteExisting($user); |
|
215 | } |
||
216 | |||
217 | /** |
||
218 | * Delete expired tokens. |
||
219 | * |
||
220 | * @return void |
||
221 | */ |
||
222 | 1 | public function deleteExpired() |
|
223 | { |
||
224 | 1 | $expiredAt = Carbon::now()->subSeconds($this->expires); |
|
225 | |||
226 | 1 | $this->getTable()->where('created_at', '<', $expiredAt)->delete(); |
|
227 | } |
||
228 | |||
229 | /** |
||
230 | * Create a new token for the user. |
||
231 | * |
||
232 | * @return string |
||
233 | */ |
||
234 | 13 | public function createNewToken() |
|
235 | { |
||
236 | 13 | return hash_hmac('sha256', Str::random(40), $this->hashKey); |
|
237 | } |
||
238 | |||
239 | /** |
||
240 | * Get the database connection instance. |
||
241 | * |
||
242 | * @return \Illuminate\Database\ConnectionInterface |
||
243 | */ |
||
244 | 2 | public function getConnection() |
|
245 | { |
||
246 | 2 | return $this->connection; |
|
247 | } |
||
248 | |||
249 | /** |
||
250 | * Begin a new database query against the table. |
||
251 | * |
||
252 | * @return \Illuminate\Database\Query\Builder |
||
253 | */ |
||
254 | 14 | protected function getTable() |
|
255 | { |
||
256 | 14 | return $this->connection->table($this->table); |
|
257 | } |
||
258 | |||
259 | /** |
||
260 | * Get the hasher instance. |
||
261 | * |
||
262 | * @return \Illuminate\Contracts\Hashing\Hasher |
||
263 | */ |
||
264 | 1 | public function getHasher() |
|
265 | { |
||
266 | 1 | return $this->hasher; |
|
267 | } |
||
268 | } |
||
269 |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.