Failed Conditions
Pull Request — multiproject/requestqueue (#704)
by Simon
02:29
created

Request::getQueueObject()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 2
c 0
b 0
f 0
dl 0
loc 6
ccs 0
cts 3
cp 0
rs 10
cc 2
nc 2
nop 0
crap 6
1
<?php
2
/******************************************************************************
3
 * Wikipedia Account Creation Assistance tool                                 *
4
 *                                                                            *
5
 * All code in this file is released into the public domain by the ACC        *
6
 * Development Team. Please see team.json for a list of contributors.         *
7
 ******************************************************************************/
8
9
namespace Waca\DataObjects;
10
11
use DateTime;
12
use DateTimeImmutable;
13
use Exception;
14
use Waca\DataObject;
15
use Waca\Exceptions\OptimisticLockFailedException;
16
use Waca\RequestStatus;
17
18
/**
19
 * Request data object
20
 *
21
 * This data object is the main request object.
22
 */
23
class Request extends DataObject
24
{
25
    private $email;
26
    private $ip;
27
    private $name;
28
    /** @var string|null */
29
    private $status = RequestStatus::OPEN;
30
    private $queue;
31
    private $date;
32
    private $emailsent = 0;
33
    private $emailconfirm;
34
    /** @var int|null */
35
    private $reserved = null;
36
    private $useragent;
37
    private $forwardedip;
38
    private $hasComments = false;
39
    private $hasCommentsResolved = false;
40
41
    /**
42
     * @throws Exception
43
     * @throws OptimisticLockFailedException
44
     */
45
    public function save()
46
    {
47
        if ($this->isNew()) {
48
            // insert
49
            $statement = $this->dbObject->prepare(<<<SQL
50
INSERT INTO `request` (
51
	email, ip, name, status, date, emailsent,
52
	emailconfirm, reserved, useragent, forwardedip,
53
    queue
54
) VALUES (
55
	:email, :ip, :name, :status, CURRENT_TIMESTAMP(), :emailsent,
56
	:emailconfirm, :reserved, :useragent, :forwardedip,
57
    :queue
58
);
59
SQL
60
            );
61
            $statement->bindValue(':email', $this->email);
62
            $statement->bindValue(':ip', $this->ip);
63
            $statement->bindValue(':name', $this->name);
64
            $statement->bindValue(':status', $this->status);
65
            $statement->bindValue(':emailsent', $this->emailsent);
66
            $statement->bindValue(':emailconfirm', $this->emailconfirm);
67
            $statement->bindValue(':reserved', $this->reserved);
68
            $statement->bindValue(':useragent', $this->useragent);
69
            $statement->bindValue(':forwardedip', $this->forwardedip);
70
            $statement->bindValue(':queue', $this->queue);
71
72
            if ($statement->execute()) {
73
                $this->id = (int)$this->dbObject->lastInsertId();
74
            }
75
            else {
76
                throw new Exception($statement->errorInfo());
0 ignored issues
show
Bug introduced by
$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

76
                throw new Exception(/** @scrutinizer ignore-type */ $statement->errorInfo());
Loading history...
77
            }
78
        }
79
        else {
80
            // update
81
            $statement = $this->dbObject->prepare(<<<SQL
82
UPDATE `request` SET
83
	status = :status,
84
	emailsent = :emailsent,
85
	emailconfirm = :emailconfirm,
86
	reserved = :reserved,
87
    queue = :queue,
88
	updateversion = updateversion + 1
89
WHERE id = :id AND updateversion = :updateversion;
90
SQL
91
            );
92
93
            $statement->bindValue(':id', $this->id);
94
            $statement->bindValue(':updateversion', $this->updateversion);
95
96
            $statement->bindValue(':status', $this->status);
97
            $statement->bindValue(':emailsent', $this->emailsent);
98
            $statement->bindValue(':emailconfirm', $this->emailconfirm);
99
            $statement->bindValue(':reserved', $this->reserved);
100
            $statement->bindValue(':queue', $this->queue);
101
102
            if (!$statement->execute()) {
103
                throw new Exception($statement->errorInfo());
104
            }
105
106
            if ($statement->rowCount() !== 1) {
107
                throw new OptimisticLockFailedException();
108
            }
109
110
            $this->updateversion++;
111
        }
112
    }
113
114
    /**
115
     * @return string
116
     */
117
    public function getIp()
118
    {
119
        return $this->ip;
120
    }
121
122
    /**
123
     * @param string $ip
124
     */
125 1
    public function setIp($ip)
126
    {
127 1
        $this->ip = $ip;
128 1
    }
129
130
    /**
131
     * @return string
132
     */
133 1
    public function getName()
134
    {
135 1
        return $this->name;
136
    }
137
138
    /**
139
     * @param string $name
140
     */
141 1
    public function setName($name)
142
    {
143 1
        $this->name = $name;
144 1
    }
145
146
    /**
147
     * @return string
148
     */
149
    public function getStatus()
150
    {
151
        return $this->status;
152
    }
153
154
    /**
155
     * @param string $status
156
     */
157
    public function setStatus($status)
158
    {
159
        $this->status = $status;
160
    }
161
162
    /**
163
     * Returns the time the request was first submitted
164
     *
165
     * @return DateTimeImmutable
166
     */
167
    public function getDate()
168
    {
169
        return new DateTimeImmutable($this->date);
170
    }
171
172
    /**
173
     * @return bool
174
     */
175
    public function getEmailSent()
176
    {
177
        return $this->emailsent == "1";
178
    }
179
180
    /**
181
     * @param bool $emailSent
182
     */
183
    public function setEmailSent($emailSent)
184
    {
185
        $this->emailsent = $emailSent ? 1 : 0;
186
    }
187
188
    /**
189
     * @return int|null
190
     */
191
    public function getReserved()
192
    {
193
        return $this->reserved;
194
    }
195
196
    /**
197
     * @param int|null $reserved
198
     */
199
    public function setReserved($reserved)
200
    {
201
        $this->reserved = $reserved;
202
    }
203
204
    /**
205
     * @return string
206
     */
207
    public function getUserAgent()
208
    {
209
        return $this->useragent;
210
    }
211
212
    /**
213
     * @param string $useragent
214
     */
215
    public function setUserAgent($useragent)
216
    {
217
        $this->useragent = $useragent;
218
    }
219
220
    /**
221
     * @return string|null
222
     */
223
    public function getForwardedIp()
224
    {
225
        return $this->forwardedip;
226
    }
227
228
    /**
229
     * @param string|null $forwardedip
230
     */
231
    public function setForwardedIp($forwardedip)
232
    {
233
        // Verify that the XFF chain only contains valid IP addresses, and silently discard anything that isn't.
234
        
235
        $xff = explode(',', $forwardedip);
0 ignored issues
show
Bug introduced by
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

235
        $xff = explode(',', /** @scrutinizer ignore-type */ $forwardedip);
Loading history...
236
        $valid = array();
237
        
238
        foreach ($xff as $ip) {
239
            $ip = trim($ip);
240
            if (filter_var($ip, FILTER_VALIDATE_IP)) {
241
                $valid[] = $ip;
242
            }
243
        }
244
        $this->forwardedip = implode(", ", $valid);
245
    }
246
247
    /**
248
     * @return bool
249
     */
250
    public function hasComments()
251
    {
252
        if ($this->hasCommentsResolved) {
253
            return $this->hasComments;
254
        }
255
256
        $commentsQuery = $this->dbObject->prepare("SELECT COUNT(*) AS num FROM comment WHERE request = :id;");
257
        $commentsQuery->bindValue(":id", $this->id);
258
259
        $commentsQuery->execute();
260
261
        $this->hasComments = ($commentsQuery->fetchColumn() != 0);
262
        $this->hasCommentsResolved = true;
263
264
        return $this->hasComments;
265
    }
266
267
    /**
268
     * @return string
269
     */
270
    public function getEmailConfirm()
271
    {
272
        return $this->emailconfirm;
273
    }
274
275
    /**
276
     * @param string $emailconfirm
277
     */
278
    public function setEmailConfirm($emailconfirm)
279
    {
280
        $this->emailconfirm = $emailconfirm;
281
    }
282
283
    public function generateEmailConfirmationHash()
284
    {
285
        $this->emailconfirm = bin2hex(openssl_random_pseudo_bytes(16));
286
    }
287
288
    /**
289
     * @return string|null
290
     */
291
    public function getEmail()
292
    {
293
        return $this->email;
294
    }
295
296
    /**
297
     * @param string|null $email
298
     */
299 1
    public function setEmail($email)
300
    {
301 1
        $this->email = $email;
302 1
    }
303
304
    /**
305
     * @return string
306
     * @throws Exception
307
     */
308
    public function getClosureReason()
309
    {
310
        if ($this->status != 'Closed') {
311
            throw new Exception("Can't get closure reason for open request.");
312
        }
313
314
        $statement = $this->dbObject->prepare(<<<SQL
315
SELECT closes.mail_desc
316
FROM log
317
INNER JOIN closes ON log.action = closes.closes
318
WHERE log.objecttype = 'Request'
319
AND log.objectid = :requestId
320
AND log.action LIKE 'Closed%'
321
ORDER BY log.timestamp DESC
322
LIMIT 1;
323
SQL
324
        );
325
326
        $statement->bindValue(":requestId", $this->id);
327
        $statement->execute();
328
        $reason = $statement->fetchColumn();
329
330
        return $reason;
331
    }
332
333
    /**
334
     * Gets a value indicating whether the request was closed as created or not.
335
     */
336
    public function getWasCreated()
337
    {
338
        if ($this->status != 'Closed') {
339
            throw new Exception("Can't get closure reason for open request.");
340
        }
341
342
        $statement = $this->dbObject->prepare(<<<SQL
343
SELECT emailtemplate.defaultaction, log.action
344
FROM log
345
LEFT JOIN emailtemplate ON CONCAT('Closed ', emailtemplate.id) = log.action
346
WHERE log.objecttype = 'Request'
347
AND log.objectid = :requestId
348
AND log.action LIKE 'Closed%'
349
ORDER BY log.timestamp DESC
350
LIMIT 1;
351
SQL
352
        );
353
354
        $statement->bindValue(":requestId", $this->id);
355
        $statement->execute();
356
        $defaultAction = $statement->fetchColumn(0);
357
        $logAction = $statement->fetchColumn(1);
358
        $statement->closeCursor();
359
360
        if ($defaultAction === null) {
361
            return $logAction === "Closed custom-y";
362
        }
363
364
        return $defaultAction === EmailTemplate::ACTION_CREATED;
365
    }
366
367
    /**
368
     * @return DateTime
369
     */
370
    public function getClosureDate()
371
    {
372
        $logQuery = $this->dbObject->prepare(<<<SQL
373
SELECT timestamp FROM log
374
WHERE objectid = :request AND objecttype = 'Request' AND action LIKE 'Closed%'
375
ORDER BY timestamp DESC LIMIT 1;
376
SQL
377
        );
378
        $logQuery->bindValue(":request", $this->getId());
379
        $logQuery->execute();
380
        $logTime = $logQuery->fetchColumn();
381
        $logQuery->closeCursor();
382
383
        return new DateTime($logTime);
384
    }
385
386
    /**
387
     * Returns a hash based on data within this request which can be generated easily from the data to be used to reveal
388
     * data to unauthorised* users.
389
     *
390
     * *:Not tool admins, check users, or the reserving user.
391
     *
392
     * @return string
393
     *
394
     * @todo future work to make invalidation better. Possibly move to the database and invalidate on relevant events?
395
     *       Maybe depend on the last logged action timestamp?
396
     */
397
    public function getRevealHash()
398
    {
399
        $data = $this->id         // unique per request
400
            . '|' . $this->ip           // }
401
            . '|' . $this->forwardedip  // } private data not known to those without access
402
            . '|' . $this->useragent    // }
403
            . '|' . $this->email        // }
404
            . '|' . $this->status; // to rudimentarily invalidate the token on status change
405
406
        return hash('sha256', $data);
407
    }
408
409
    /**
410
     * @return int|null
411
     */
412
    public function getQueue() : ?int
413
    {
414
        return $this->queue;
415
    }
416
417
    /**
418
     * @return RequestQueue|null
419
     */
420
    public function getQueueObject(): ?RequestQueue
421
    {
422
        /** @var RequestQueue $queue */
423
        $queue = RequestQueue::getById($this->queue, $this->getDatabase());
424
425
        return $queue === false ? null : $queue;
0 ignored issues
show
introduced by
The condition $queue === false is always false.
Loading history...
426
    }
427
428
    /**
429
     * @param int|null $queue
430
     */
431
    public function setQueue(?int $queue): void
432
    {
433
        $this->queue = $queue;
434
    }
435
}
436