1 | <?php |
||||
2 | /****************************************************************************** |
||||
3 | * Wikipedia Account Creation Assistance tool * |
||||
4 | * ACC Development Team. Please see team.json for a list of contributors. * |
||||
5 | * * |
||||
6 | * This is free and unencumbered software released into the public domain. * |
||||
7 | * Please see LICENSE.md for the full licencing statement. * |
||||
8 | ******************************************************************************/ |
||||
9 | |||||
10 | namespace Waca\DataObjects; |
||||
11 | |||||
12 | use DateTime; |
||||
13 | use DateTimeImmutable; |
||||
14 | use Exception; |
||||
15 | use Waca\DataObject; |
||||
16 | use Waca\Exceptions\OptimisticLockFailedException; |
||||
17 | use Waca\RequestStatus; |
||||
18 | |||||
19 | /** |
||||
20 | * Request data object |
||||
21 | * |
||||
22 | * This data object is the main request object. |
||||
23 | */ |
||||
24 | class Request extends DataObject |
||||
25 | { |
||||
26 | private $email; |
||||
27 | private $ip; |
||||
28 | private $name; |
||||
29 | /** @var string|null */ |
||||
30 | private $status = RequestStatus::OPEN; |
||||
31 | private $queue; |
||||
32 | private $date; |
||||
33 | private $emailsent = 0; |
||||
34 | private $emailconfirm; |
||||
35 | /** @var int|null */ |
||||
36 | private $reserved = null; |
||||
37 | private $useragent; |
||||
38 | private $forwardedip; |
||||
39 | private $hasComments = false; |
||||
40 | private $hasCommentsResolved = false; |
||||
41 | private $originform; |
||||
42 | /** @var int */ |
||||
43 | private $domain; |
||||
44 | |||||
45 | /** |
||||
46 | * @throws Exception |
||||
47 | * @throws OptimisticLockFailedException |
||||
48 | */ |
||||
49 | public function save() |
||||
50 | { |
||||
51 | if ($this->isNew()) { |
||||
52 | // insert |
||||
53 | $statement = $this->dbObject->prepare(<<<SQL |
||||
54 | INSERT INTO `request` ( |
||||
55 | email, ip, name, status, date, emailsent, |
||||
56 | emailconfirm, reserved, useragent, forwardedip, |
||||
57 | queue, originform, domain |
||||
58 | ) VALUES ( |
||||
59 | :email, :ip, :name, :status, CURRENT_TIMESTAMP(), :emailsent, |
||||
60 | :emailconfirm, :reserved, :useragent, :forwardedip, |
||||
61 | :queue, :originform, :domain |
||||
62 | ); |
||||
63 | SQL |
||||
64 | ); |
||||
65 | $statement->bindValue(':email', $this->email); |
||||
66 | $statement->bindValue(':ip', $this->ip); |
||||
67 | $statement->bindValue(':name', $this->name); |
||||
68 | $statement->bindValue(':status', $this->status); |
||||
69 | $statement->bindValue(':emailsent', $this->emailsent); |
||||
70 | $statement->bindValue(':emailconfirm', $this->emailconfirm); |
||||
71 | $statement->bindValue(':reserved', $this->reserved); |
||||
72 | $statement->bindValue(':useragent', $this->useragent); |
||||
73 | $statement->bindValue(':forwardedip', $this->forwardedip); |
||||
74 | $statement->bindValue(':queue', $this->queue); |
||||
75 | $statement->bindValue(':originform', $this->originform); |
||||
76 | $statement->bindValue(':domain', $this->domain); |
||||
77 | |||||
78 | if ($statement->execute()) { |
||||
79 | $this->id = (int)$this->dbObject->lastInsertId(); |
||||
80 | } |
||||
81 | else { |
||||
82 | throw new Exception($statement->errorInfo()); |
||||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||
83 | } |
||||
84 | } |
||||
85 | else { |
||||
86 | // update |
||||
87 | $statement = $this->dbObject->prepare(<<<SQL |
||||
88 | UPDATE `request` SET |
||||
89 | status = :status, |
||||
90 | emailsent = :emailsent, |
||||
91 | emailconfirm = :emailconfirm, |
||||
92 | reserved = :reserved, |
||||
93 | queue = :queue, |
||||
94 | updateversion = updateversion + 1 |
||||
95 | WHERE id = :id AND updateversion = :updateversion; |
||||
96 | SQL |
||||
97 | ); |
||||
98 | |||||
99 | $statement->bindValue(':id', $this->id); |
||||
100 | $statement->bindValue(':updateversion', $this->updateversion); |
||||
101 | |||||
102 | $statement->bindValue(':status', $this->status); |
||||
103 | $statement->bindValue(':emailsent', $this->emailsent); |
||||
104 | $statement->bindValue(':emailconfirm', $this->emailconfirm); |
||||
105 | $statement->bindValue(':reserved', $this->reserved); |
||||
106 | $statement->bindValue(':queue', $this->queue); |
||||
107 | |||||
108 | if (!$statement->execute()) { |
||||
109 | throw new Exception($statement->errorInfo()); |
||||
110 | } |
||||
111 | |||||
112 | if ($statement->rowCount() !== 1) { |
||||
113 | throw new OptimisticLockFailedException(); |
||||
114 | } |
||||
115 | |||||
116 | $this->updateversion++; |
||||
117 | } |
||||
118 | } |
||||
119 | |||||
120 | /** |
||||
121 | * @return string |
||||
122 | */ |
||||
123 | public function getIp() |
||||
124 | { |
||||
125 | return $this->ip; |
||||
126 | } |
||||
127 | |||||
128 | /** |
||||
129 | * @param string $ip |
||||
130 | */ |
||||
131 | public function setIp($ip) |
||||
132 | { |
||||
133 | $this->ip = $ip; |
||||
134 | } |
||||
135 | |||||
136 | /** |
||||
137 | * @return string |
||||
138 | */ |
||||
139 | public function getName() |
||||
140 | { |
||||
141 | return $this->name; |
||||
142 | } |
||||
143 | |||||
144 | /** |
||||
145 | * @param string $name |
||||
146 | */ |
||||
147 | public function setName($name) |
||||
148 | { |
||||
149 | $this->name = $name; |
||||
150 | } |
||||
151 | |||||
152 | /** |
||||
153 | * @return string |
||||
154 | */ |
||||
155 | public function getStatus() |
||||
156 | { |
||||
157 | return $this->status; |
||||
158 | } |
||||
159 | |||||
160 | /** |
||||
161 | * @param string $status |
||||
162 | */ |
||||
163 | public function setStatus($status) |
||||
164 | { |
||||
165 | $this->status = $status; |
||||
166 | } |
||||
167 | |||||
168 | /** |
||||
169 | * Returns the time the request was first submitted |
||||
170 | * |
||||
171 | * @return DateTimeImmutable |
||||
172 | */ |
||||
173 | public function getDate() |
||||
174 | { |
||||
175 | return new DateTimeImmutable($this->date); |
||||
176 | } |
||||
177 | |||||
178 | /** |
||||
179 | * @return bool |
||||
180 | */ |
||||
181 | public function getEmailSent() |
||||
182 | { |
||||
183 | return $this->emailsent == "1"; |
||||
184 | } |
||||
185 | |||||
186 | /** |
||||
187 | * @param bool $emailSent |
||||
188 | */ |
||||
189 | public function setEmailSent($emailSent) |
||||
190 | { |
||||
191 | $this->emailsent = $emailSent ? 1 : 0; |
||||
192 | } |
||||
193 | |||||
194 | /** |
||||
195 | * @return int|null |
||||
196 | */ |
||||
197 | public function getReserved() |
||||
198 | { |
||||
199 | return $this->reserved; |
||||
200 | } |
||||
201 | |||||
202 | /** |
||||
203 | * @param int|null $reserved |
||||
204 | */ |
||||
205 | public function setReserved($reserved) |
||||
206 | { |
||||
207 | $this->reserved = $reserved; |
||||
208 | } |
||||
209 | |||||
210 | /** |
||||
211 | * @return string |
||||
212 | */ |
||||
213 | public function getUserAgent() |
||||
214 | { |
||||
215 | return $this->useragent; |
||||
216 | } |
||||
217 | |||||
218 | /** |
||||
219 | * @param string $useragent |
||||
220 | */ |
||||
221 | public function setUserAgent($useragent) |
||||
222 | { |
||||
223 | $this->useragent = $useragent; |
||||
224 | } |
||||
225 | |||||
226 | /** |
||||
227 | * @return string|null |
||||
228 | */ |
||||
229 | public function getForwardedIp() |
||||
230 | { |
||||
231 | return $this->forwardedip; |
||||
232 | } |
||||
233 | |||||
234 | /** |
||||
235 | * @param string|null $forwardedip |
||||
236 | */ |
||||
237 | public function setForwardedIp($forwardedip) |
||||
238 | { |
||||
239 | // Verify that the XFF chain only contains valid IP addresses, and silently discard anything that isn't. |
||||
240 | |||||
241 | $xff = explode(',', $forwardedip); |
||||
0 ignored issues
–
show
It seems like
$forwardedip can also be of type null ; however, parameter $string of explode() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
242 | $valid = array(); |
||||
243 | |||||
244 | foreach ($xff as $ip) { |
||||
245 | $ip = trim($ip); |
||||
246 | if (filter_var($ip, FILTER_VALIDATE_IP)) { |
||||
247 | $valid[] = $ip; |
||||
248 | } |
||||
249 | } |
||||
250 | $this->forwardedip = implode(", ", $valid); |
||||
251 | } |
||||
252 | |||||
253 | /** |
||||
254 | * @return bool |
||||
255 | */ |
||||
256 | public function hasComments() |
||||
257 | { |
||||
258 | if ($this->hasCommentsResolved) { |
||||
259 | return $this->hasComments; |
||||
260 | } |
||||
261 | |||||
262 | $commentsQuery = $this->dbObject->prepare("SELECT COUNT(*) AS num FROM comment WHERE request = :id;"); |
||||
263 | $commentsQuery->bindValue(":id", $this->id); |
||||
264 | |||||
265 | $commentsQuery->execute(); |
||||
266 | |||||
267 | $this->hasComments = ($commentsQuery->fetchColumn() != 0); |
||||
268 | $this->hasCommentsResolved = true; |
||||
269 | |||||
270 | return $this->hasComments; |
||||
271 | } |
||||
272 | |||||
273 | /** |
||||
274 | * @return string |
||||
275 | */ |
||||
276 | public function getEmailConfirm() |
||||
277 | { |
||||
278 | return $this->emailconfirm; |
||||
279 | } |
||||
280 | |||||
281 | /** |
||||
282 | * @param string $emailconfirm |
||||
283 | */ |
||||
284 | public function setEmailConfirm($emailconfirm) |
||||
285 | { |
||||
286 | $this->emailconfirm = $emailconfirm; |
||||
287 | } |
||||
288 | |||||
289 | public function generateEmailConfirmationHash() |
||||
290 | { |
||||
291 | $this->emailconfirm = bin2hex(openssl_random_pseudo_bytes(16)); |
||||
292 | } |
||||
293 | |||||
294 | /** |
||||
295 | * @return string|null |
||||
296 | */ |
||||
297 | public function getEmail() |
||||
298 | { |
||||
299 | return $this->email; |
||||
300 | } |
||||
301 | |||||
302 | /** |
||||
303 | * @param string|null $email |
||||
304 | */ |
||||
305 | public function setEmail($email) |
||||
306 | { |
||||
307 | $this->email = $email; |
||||
308 | } |
||||
309 | |||||
310 | /** |
||||
311 | * @return string |
||||
312 | * @throws Exception |
||||
313 | */ |
||||
314 | public function getClosureReason() |
||||
315 | { |
||||
316 | if ($this->status != 'Closed') { |
||||
317 | throw new Exception("Can't get closure reason for open request."); |
||||
318 | } |
||||
319 | |||||
320 | $statement = $this->dbObject->prepare(<<<SQL |
||||
321 | SELECT closes.mail_desc |
||||
322 | FROM log |
||||
323 | INNER JOIN closes ON log.action = closes.closes |
||||
324 | WHERE log.objecttype = 'Request' |
||||
325 | AND log.objectid = :requestId |
||||
326 | AND log.action LIKE 'Closed%' |
||||
327 | ORDER BY log.timestamp DESC |
||||
328 | LIMIT 1; |
||||
329 | SQL |
||||
330 | ); |
||||
331 | |||||
332 | $statement->bindValue(":requestId", $this->id); |
||||
333 | $statement->execute(); |
||||
334 | $reason = $statement->fetchColumn(); |
||||
335 | |||||
336 | return $reason; |
||||
337 | } |
||||
338 | |||||
339 | /** |
||||
340 | * Gets a value indicating whether the request was closed as created or not. |
||||
341 | */ |
||||
342 | public function getWasCreated() |
||||
343 | { |
||||
344 | if ($this->status != 'Closed') { |
||||
345 | throw new Exception("Can't get closure reason for open request."); |
||||
346 | } |
||||
347 | |||||
348 | $statement = $this->dbObject->prepare(<<<SQL |
||||
349 | SELECT emailtemplate.defaultaction, log.action |
||||
350 | FROM log |
||||
351 | LEFT JOIN emailtemplate ON CONCAT('Closed ', emailtemplate.id) = log.action |
||||
352 | WHERE log.objecttype = 'Request' |
||||
353 | AND log.objectid = :requestId |
||||
354 | AND log.action LIKE 'Closed%' |
||||
355 | ORDER BY log.timestamp DESC |
||||
356 | LIMIT 1; |
||||
357 | SQL |
||||
358 | ); |
||||
359 | |||||
360 | $statement->bindValue(":requestId", $this->id); |
||||
361 | $statement->execute(); |
||||
362 | $defaultAction = $statement->fetchColumn(0); |
||||
363 | $logAction = $statement->fetchColumn(1); |
||||
364 | $statement->closeCursor(); |
||||
365 | |||||
366 | if ($defaultAction === null) { |
||||
367 | return $logAction === "Closed custom-y"; |
||||
368 | } |
||||
369 | |||||
370 | return $defaultAction === EmailTemplate::ACTION_CREATED; |
||||
371 | } |
||||
372 | |||||
373 | /** |
||||
374 | * @return DateTime |
||||
375 | */ |
||||
376 | public function getClosureDate() |
||||
377 | { |
||||
378 | $logQuery = $this->dbObject->prepare(<<<SQL |
||||
379 | SELECT timestamp FROM log |
||||
380 | WHERE objectid = :request AND objecttype = 'Request' AND action LIKE 'Closed%' |
||||
381 | ORDER BY timestamp DESC LIMIT 1; |
||||
382 | SQL |
||||
383 | ); |
||||
384 | $logQuery->bindValue(":request", $this->getId()); |
||||
385 | $logQuery->execute(); |
||||
386 | $logTime = $logQuery->fetchColumn(); |
||||
387 | $logQuery->closeCursor(); |
||||
388 | |||||
389 | return new DateTime($logTime); |
||||
390 | } |
||||
391 | |||||
392 | public function getLastUpdated() |
||||
393 | { |
||||
394 | $logQuery = $this->dbObject->prepare(<<<SQL |
||||
395 | SELECT max(d.ts) FROM ( |
||||
396 | SELECT r.date AS ts FROM request r WHERE r.id = :requestr |
||||
397 | UNION ALL |
||||
398 | SELECT l.timestamp AS ts FROM log l WHERE l.objectid = :requestl AND l.objecttype = 'Request' |
||||
399 | UNION ALL |
||||
400 | SELECT c.time AS ts FROM comment c WHERE c.request = :requestc |
||||
401 | ) d |
||||
402 | SQL |
||||
403 | ); |
||||
404 | |||||
405 | $logQuery->bindValue(":requestr", $this->getId()); |
||||
406 | $logQuery->bindValue(":requestl", $this->getId()); |
||||
407 | $logQuery->bindValue(":requestc", $this->getId()); |
||||
408 | $logQuery->execute(); |
||||
409 | $logTime = $logQuery->fetchColumn(); |
||||
410 | $logQuery->closeCursor(); |
||||
411 | |||||
412 | return new DateTime($logTime); |
||||
413 | } |
||||
414 | |||||
415 | /** |
||||
416 | * Returns a hash based on data within this request which can be generated easily from the data to be used to reveal |
||||
417 | * data to unauthorised* users. |
||||
418 | * |
||||
419 | * *:Not tool admins, check users, or the reserving user. |
||||
420 | * |
||||
421 | * @return string |
||||
422 | * |
||||
423 | * @todo future work to make invalidation better. Possibly move to the database and invalidate on relevant events? |
||||
424 | * Maybe depend on the last logged action timestamp? |
||||
425 | */ |
||||
426 | public function getRevealHash() |
||||
427 | { |
||||
428 | $data = $this->id // unique per request |
||||
429 | . '|' . $this->ip // } |
||||
430 | . '|' . $this->forwardedip // } private data not known to those without access |
||||
431 | . '|' . $this->useragent // } |
||||
432 | . '|' . $this->email // } |
||||
433 | . '|' . $this->status; // to rudimentarily invalidate the token on status change |
||||
434 | |||||
435 | return hash('sha256', $data); |
||||
436 | } |
||||
437 | |||||
438 | /** |
||||
439 | * @return int|null |
||||
440 | */ |
||||
441 | public function getQueue() : ?int |
||||
442 | { |
||||
443 | return $this->queue; |
||||
444 | } |
||||
445 | |||||
446 | /** |
||||
447 | * @return RequestQueue|null |
||||
448 | */ |
||||
449 | public function getQueueObject(): ?RequestQueue |
||||
450 | { |
||||
451 | /** @var RequestQueue $queue */ |
||||
452 | $queue = RequestQueue::getById($this->queue, $this->getDatabase()); |
||||
453 | |||||
454 | return $queue === false ? null : $queue; |
||||
0 ignored issues
–
show
|
|||||
455 | } |
||||
456 | |||||
457 | /** |
||||
458 | * @param int|null $queue |
||||
459 | */ |
||||
460 | public function setQueue(?int $queue): void |
||||
461 | { |
||||
462 | $this->queue = $queue; |
||||
463 | } |
||||
464 | |||||
465 | /** |
||||
466 | * @return int|null |
||||
467 | */ |
||||
468 | public function getOriginForm(): ?int |
||||
469 | { |
||||
470 | return $this->originform; |
||||
471 | } |
||||
472 | |||||
473 | public function getOriginFormObject(): ?RequestForm |
||||
474 | { |
||||
475 | if ($this->originform === null) { |
||||
476 | return null; |
||||
477 | } |
||||
478 | |||||
479 | /** @var RequestForm|bool $form */ |
||||
480 | $form = RequestForm::getById($this->originform, $this->getDatabase()); |
||||
481 | |||||
482 | return $form === false ? null : $form; |
||||
0 ignored issues
–
show
|
|||||
483 | } |
||||
484 | |||||
485 | /** |
||||
486 | * @param int|null $originForm |
||||
487 | */ |
||||
488 | public function setOriginForm(?int $originForm): void |
||||
489 | { |
||||
490 | $this->originform = $originForm; |
||||
491 | } |
||||
492 | |||||
493 | public function getDomain(): int |
||||
494 | { |
||||
495 | return $this->domain; |
||||
496 | } |
||||
497 | |||||
498 | public function setDomain(int $domain): void |
||||
499 | { |
||||
500 | $this->domain = $domain; |
||||
501 | } |
||||
502 | } |
||||
503 |