Issues (186)

Branch: oauth-creation-featureflag

includes/DataObjects/Request.php (4 issues)

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
$statement->errorInfo() of type array is incompatible with the type string expected by parameter $message of Exception::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

82
                throw new Exception(/** @scrutinizer ignore-type */ $statement->errorInfo());
Loading history...
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 ignore-type  annotation

241
        $xff = explode(',', /** @scrutinizer ignore-type */ $forwardedip);
Loading history...
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
The condition $queue === false is always false.
Loading history...
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
The condition $form === false is always false.
Loading history...
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