Passed
Pull Request — multiproject/db (#703)
by Simon
02:48
created

Request::setEmail()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
ccs 0
cts 2
cp 0
rs 10
cc 1
nc 1
nop 1
crap 2
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
17
/**
18
 * Request data object
19
 *
20
 * This data object is the main request object.
21
 */
22
class Request extends DataObject
23
{
24
    private $email;
25
    private $ip;
26
    private $name;
27
    /** @var string|null */
28
    private $status = "Open";
29
    private $date;
30
    private $emailsent = 0;
31
    private $emailconfirm;
32
    /** @var int|null */
33
    private $reserved = null;
34
    private $useragent;
35
    private $forwardedip;
36
    private $hasComments = false;
37
    private $hasCommentsResolved = false;
38
39
    /**
40
     * @throws Exception
41
     * @throws OptimisticLockFailedException
42
     */
43
    public function save()
44
    {
45
        if ($this->isNew()) {
46
            // insert
47
            $statement = $this->dbObject->prepare(<<<SQL
48
INSERT INTO `request` (
49
	email, ip, name, status, date, emailsent,
50
	emailconfirm, reserved, useragent, forwardedip
51
) VALUES (
52
	:email, :ip, :name, :status, CURRENT_TIMESTAMP(), :emailsent,
53
	:emailconfirm, :reserved, :useragent, :forwardedip
54
);
55
SQL
56
            );
57
            $statement->bindValue(':email', $this->email);
58
            $statement->bindValue(':ip', $this->ip);
59
            $statement->bindValue(':name', $this->name);
60
            $statement->bindValue(':status', $this->status);
61
            $statement->bindValue(':emailsent', $this->emailsent);
62
            $statement->bindValue(':emailconfirm', $this->emailconfirm);
63
            $statement->bindValue(':reserved', $this->reserved);
64
            $statement->bindValue(':useragent', $this->useragent);
65
            $statement->bindValue(':forwardedip', $this->forwardedip);
66
67
            if ($statement->execute()) {
68
                $this->id = (int)$this->dbObject->lastInsertId();
69
            }
70
            else {
71
                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

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

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