Passed
Push — master ( 8b5481...138166 )
by Simon
04:27
created

Request   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 464
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 47
eloc 178
c 0
b 0
f 0
dl 0
loc 464
ccs 0
cts 163
cp 0
rs 8.64

33 Methods

Rating   Name   Duplication   Size   Complexity  
A setOriginForm() 0 3 1
A getWasCreated() 0 29 3
A getQueueObject() 0 6 2
A getEmailConfirm() 0 3 1
A getClosureDate() 0 14 1
A setEmail() 0 3 1
A getStatus() 0 3 1
A generateEmailConfirmationHash() 0 3 1
A setUserAgent() 0 3 1
A getForwardedIp() 0 3 1
A getRevealHash() 0 10 1
A setReserved() 0 3 1
A getLastUpdated() 0 21 1
A getEmailSent() 0 3 1
A getDate() 0 3 1
A setQueue() 0 3 1
A getReserved() 0 3 1
A getIp() 0 3 1
A getQueue() 0 3 1
A getUserAgent() 0 3 1
A setEmailConfirm() 0 3 1
A setStatus() 0 3 1
A getEmail() 0 3 1
A getOriginForm() 0 3 1
A hasComments() 0 15 2
A getOriginFormObject() 0 10 3
B save() 0 67 5
A setForwardedIp() 0 14 3
A setName() 0 3 1
A setIp() 0 3 1
A getClosureReason() 0 23 2
A setEmailSent() 0 3 2
A getName() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like Request often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Request, and based on these observations, apply Extract Interface, too.

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
    private $originform;
41
42
    /**
43
     * @throws Exception
44
     * @throws OptimisticLockFailedException
45
     */
46
    public function save()
47
    {
48
        if ($this->isNew()) {
49
            // insert
50
            $statement = $this->dbObject->prepare(<<<SQL
51
INSERT INTO `request` (
52
	email, ip, name, status, date, emailsent,
53
	emailconfirm, reserved, useragent, forwardedip,
54
    queue, originform
55
) VALUES (
56
	:email, :ip, :name, :status, CURRENT_TIMESTAMP(), :emailsent,
57
	:emailconfirm, :reserved, :useragent, :forwardedip,
58
    :queue, :originform
59
);
60
SQL
61
            );
62
            $statement->bindValue(':email', $this->email);
63
            $statement->bindValue(':ip', $this->ip);
64
            $statement->bindValue(':name', $this->name);
65
            $statement->bindValue(':status', $this->status);
66
            $statement->bindValue(':emailsent', $this->emailsent);
67
            $statement->bindValue(':emailconfirm', $this->emailconfirm);
68
            $statement->bindValue(':reserved', $this->reserved);
69
            $statement->bindValue(':useragent', $this->useragent);
70
            $statement->bindValue(':forwardedip', $this->forwardedip);
71
            $statement->bindValue(':queue', $this->queue);
72
            $statement->bindValue(':originform', $this->originform);
73
74
            if ($statement->execute()) {
75
                $this->id = (int)$this->dbObject->lastInsertId();
76
            }
77
            else {
78
                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

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

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