Passed
Push — php8 ( a4466e...6a268a )
by Michael
06:33 queued 02:53
created
includes/Pages/PageBan.php 1 patch
Indentation   +657 added lines, -657 removed lines patch added patch discarded remove patch
@@ -26,663 +26,663 @@
 block discarded – undo
26 26
 
27 27
 class PageBan extends InternalPageBase
28 28
 {
29
-    /**
30
-     * Main function for this page, when no specific actions are called.
31
-     */
32
-    protected function main(): void
33
-    {
34
-        $this->assignCSRFToken();
35
-        $this->setHtmlTitle('Bans');
36
-
37
-        $database = $this->getDatabase();
38
-        $currentDomain = Domain::getCurrent($database);
39
-        $bans = Ban::getActiveBans($database, $currentDomain->getId());
40
-
41
-        $this->setupBanList($bans);
42
-
43
-        $this->assign('isFiltered', false);
44
-        $this->setTemplate('bans/main.tpl');
45
-    }
46
-
47
-    protected function show(): void
48
-    {
49
-        $this->assignCSRFToken();
50
-        $this->setHtmlTitle('Bans');
51
-
52
-        $rawIdList = WebRequest::getString('id');
53
-        if ($rawIdList === null) {
54
-            $this->redirect('bans');
55
-
56
-            return;
57
-        }
58
-
59
-        $idList = explode(',', $rawIdList);
60
-
61
-        $database = $this->getDatabase();
62
-        $currentDomain = Domain::getCurrent($database);
63
-        $bans = Ban::getByIdList($idList, $database, $currentDomain->getId());
64
-
65
-        $this->setupBanList($bans);
66
-        $this->assign('isFiltered', true);
67
-        $this->setTemplate('bans/main.tpl');
68
-    }
69
-
70
-    /**
71
-     * Entry point for the ban set action
72
-     * @throws Smarty\Exception
73
-     * @throws Exception
74
-     */
75
-    protected function set(): void
76
-    {
77
-        $this->setHtmlTitle('Bans');
78
-
79
-        // dual-mode action
80
-        if (WebRequest::wasPosted()) {
81
-            try {
82
-                $this->handlePostMethodForSetBan();
83
-            }
84
-            catch (ApplicationLogicException $ex) {
85
-                SessionAlert::error($ex->getMessage());
86
-                $this->redirect("bans", "set");
87
-            }
88
-        }
89
-        else {
90
-            $this->handleGetMethodForSetBan();
91
-
92
-            $user = User::getCurrent($this->getDatabase());
93
-            $banType = WebRequest::getString('type');
94
-            $banRequest = WebRequest::getInt('request');
95
-
96
-            // if the parameters are null, skip loading a request.
97
-            if ($banType !== null && $banRequest !== null && $banRequest !== 0) {
98
-                $this->preloadFormForRequest($banRequest, $banType, $user);
99
-            }
100
-        }
101
-    }
102
-
103
-    protected function replace(): void
104
-    {
105
-        $this->setHtmlTitle('Bans');
106
-
107
-        $database = $this->getDatabase();
108
-        $domain = Domain::getCurrent($database);
109
-
110
-        // dual-mode action
111
-        if (WebRequest::wasPosted()) {
112
-            try {
113
-                $originalBanId = WebRequest::postInt('replaceBanId');
114
-                $originalBanUpdateVersion = WebRequest::postInt('replaceBanUpdateVersion');
115
-
116
-                $originalBan = Ban::getActiveId($originalBanId, $database, $domain->getId());
117
-
118
-                if ($originalBan === false) {
119
-                    throw new ApplicationLogicException("The specified ban is not currently active, or doesn't exist.");
120
-                }
121
-
122
-                // Discard original ban; we're replacing it.
123
-                $originalBan->setUpdateVersion($originalBanUpdateVersion);
124
-                $originalBan->setActive(false);
125
-                $originalBan->save();
126
-
127
-                Logger::banReplaced($database, $originalBan);
128
-
129
-                // Proceed as normal to save the new ban.
130
-                $this->handlePostMethodForSetBan();
131
-            }
132
-            catch (ApplicationLogicException $ex) {
133
-                $database->rollback();
134
-                SessionAlert::error($ex->getMessage());
135
-                $this->redirect("bans", "set");
136
-            }
137
-        }
138
-        else {
139
-            $this->handleGetMethodForSetBan();
140
-
141
-            $user = User::getCurrent($database);
142
-            $originalBanId = WebRequest::getString('id');
143
-
144
-            $originalBan = Ban::getActiveId($originalBanId, $database, $domain->getId());
145
-
146
-            if ($originalBan === false) {
147
-                throw new ApplicationLogicException("The specified ban is not currently active, or doesn't exist.");
148
-            }
149
-
150
-            if ($originalBan->getName() !== null) {
151
-                if (!$this->barrierTest('name', $user, 'BanType')) {
152
-                    SessionAlert::error("You are not allowed to set this type of ban.");
153
-                    $this->redirect("bans", "set");
154
-                    return;
155
-                }
156
-
157
-                $this->assign('banName', $originalBan->getName());
158
-            }
159
-
160
-            if ($originalBan->getEmail() !== null) {
161
-                if (!$this->barrierTest('email', $user, 'BanType')) {
162
-                    SessionAlert::error("You are not allowed to set this type of ban.");
163
-                    $this->redirect("bans", "set");
164
-                    return;
165
-                }
166
-
167
-                $this->assign('banEmail', $originalBan->getEmail());
168
-            }
169
-
170
-            if ($originalBan->getUseragent() !== null) {
171
-                if (!$this->barrierTest('useragent', $user, 'BanType')) {
172
-                    SessionAlert::error("You are not allowed to set this type of ban.");
173
-                    $this->redirect("bans", "set");
174
-                    return;
175
-                }
176
-
177
-                $this->assign('banUseragent', $originalBan->getUseragent());
178
-            }
179
-
180
-            if ($originalBan->getIp() !== null) {
181
-                if (!$this->barrierTest('ip', $user, 'BanType')) {
182
-                    SessionAlert::error("You are not allowed to set this type of ban.");
183
-                    $this->redirect("bans", "set");
184
-                    return;
185
-                }
186
-
187
-                $this->assign('banIP', $originalBan->getIp() . '/' . $originalBan->getIpMask());
188
-            }
189
-
190
-            $banIsGlobal = $originalBan->getDomain() === null;
191
-            if ($banIsGlobal) {
192
-                if (!$this->barrierTest('global', $user, 'BanType')) {
193
-                    SessionAlert::error("You are not allowed to set this type of ban.");
194
-                    $this->redirect("bans", "set");
195
-                    return;
196
-                }
197
-            }
198
-
199
-            if (!$this->barrierTest($originalBan->getVisibility(), $user, 'BanVisibility')) {
200
-                SessionAlert::error("You are not allowed to set this type of ban.");
201
-                $this->redirect("bans", "set");
202
-                return;
203
-            }
204
-
205
-            $this->assign('banGlobal', $banIsGlobal);
206
-            $this->assign('banVisibility', $originalBan->getVisibility());
207
-
208
-            if ($originalBan->getDuration() !== null) {
209
-                $this->assign('banDuration', date('c', $originalBan->getDuration()));
210
-            }
211
-
212
-            $this->assign('banReason', $originalBan->getReason());
213
-            $this->assign('banAction', $originalBan->getAction());
214
-            $this->assign('banQueue', $originalBan->getTargetQueue());
215
-
216
-            $this->assign('replaceBanId', $originalBan->getId());
217
-            $this->assign('replaceBanUpdateVersion', $originalBan->getUpdateVersion());
218
-        }
219
-    }
220
-
221
-    /**
222
-     * Entry point for the ban remove action
223
-     *
224
-     * @throws AccessDeniedException
225
-     * @throws ApplicationLogicException
226
-     * @throws Smarty\Exception
227
-     */
228
-    protected function remove(): void
229
-    {
230
-        $this->setHtmlTitle('Bans');
231
-
232
-        $ban = $this->getBanForUnban();
233
-
234
-        $banHelper = new BanHelper($this->getDatabase(), $this->getXffTrustProvider(), $this->getSecurityManager());
235
-        if (!$banHelper->canUnban($ban)) {
236
-            // triggered when a user tries to unban a ban they can't see the entirety of.
237
-            // there's no UI way to get to this, so a raw exception is fine.
238
-            throw new AccessDeniedException($this->getSecurityManager(), $this->getDomainAccessManager());
239
-        }
240
-
241
-        // dual mode
242
-        if (WebRequest::wasPosted()) {
243
-            $this->validateCSRFToken();
244
-            $unbanReason = WebRequest::postString('unbanreason');
245
-
246
-            if ($unbanReason === null || trim($unbanReason) === "") {
247
-                SessionAlert::error('No unban reason specified');
248
-                $this->redirect("bans", "remove", array('id' => $ban->getId()));
249
-            }
250
-
251
-            // set optimistic locking from delete form page load
252
-            $updateVersion = WebRequest::postInt('updateversion');
253
-            $ban->setUpdateVersion($updateVersion);
254
-
255
-            $database = $this->getDatabase();
256
-            $ban->setActive(false);
257
-            $ban->save();
258
-
259
-            Logger::unbanned($database, $ban, $unbanReason);
260
-
261
-            SessionAlert::quick('Disabled ban.');
262
-            $this->getNotificationHelper()->unbanned($ban, $unbanReason);
263
-
264
-            $this->redirect('bans');
265
-        }
266
-        else {
267
-            $this->assignCSRFToken();
268
-            $this->assign('ban', $ban);
269
-            $this->setTemplate('bans/unban.tpl');
270
-        }
271
-    }
272
-
273
-    /**
274
-     * Retrieves the requested ban duration from the WebRequest
275
-     *
276
-     * @throws ApplicationLogicException
277
-     */
278
-    private function getBanDuration(): ?int
279
-    {
280
-        $duration = WebRequest::postString('duration');
281
-        if ($duration === "other") {
282
-            $duration = strtotime(WebRequest::postString('otherduration'));
283
-
284
-            if (!$duration) {
285
-                throw new ApplicationLogicException('Invalid ban time');
286
-            }
287
-            elseif (time() > $duration) {
288
-                throw new ApplicationLogicException('Ban time has already expired!');
289
-            }
290
-
291
-            return $duration;
292
-        }
293
-        elseif ($duration === "-1") {
294
-            return null;
295
-        }
296
-        else {
297
-            return WebRequest::postInt('duration') + time();
298
-        }
299
-    }
300
-
301
-    /**
302
-     * Handles the POST method on the set action
303
-     *
304
-     * @throws ApplicationLogicException
305
-     * @throws Exception
306
-     */
307
-    private function handlePostMethodForSetBan()
308
-    {
309
-        $this->validateCSRFToken();
310
-        $database = $this->getDatabase();
311
-        $user = User::getCurrent($database);
312
-        $currentDomain = Domain::getCurrent($database);
313
-
314
-        // Checks whether there is a reason entered for ban.
315
-        $reason = WebRequest::postString('banreason');
316
-        if ($reason === null || trim($reason) === "") {
317
-            throw new ApplicationLogicException('You must specify a ban reason');
318
-        }
319
-
320
-        // ban targets
321
-        list($targetName, $targetIp, $targetEmail, $targetUseragent) = $this->getRawBanTargets($user);
322
-
323
-        $visibility = $this->getBanVisibility();
324
-
325
-        // Validate ban duration
326
-        $duration = $this->getBanDuration();
327
-
328
-        $action = WebRequest::postString('banAction') ?? Ban::ACTION_NONE;
329
-
330
-        $global = WebRequest::postBoolean('banGlobal');
331
-        if (!$this->barrierTest('global', $user, 'BanType')) {
332
-            $global = false;
333
-        }
334
-
335
-        if ($action === Ban::ACTION_DEFER && $global) {
336
-            throw new ApplicationLogicException("Cannot set a global ban in defer-to-queue mode.");
337
-        }
338
-
339
-        // handle CIDR ranges
340
-        $targetMask = null;
341
-        if ($targetIp !== null) {
342
-            list($targetIp, $targetMask) = $this->splitCidrRange($targetIp);
343
-            $this->validateIpBan($targetIp, $targetMask, $user, $action);
344
-        }
345
-
346
-        $banHelper = new BanHelper($this->getDatabase(), $this->getXffTrustProvider(), $this->getSecurityManager());
347
-
348
-        $bansByTarget = $banHelper->getBansByTarget(
349
-            $targetName,
350
-            $targetEmail,
351
-            $targetIp,
352
-            $targetMask,
353
-            $targetUseragent,
354
-            $currentDomain->getId());
355
-
356
-        if (count($bansByTarget) > 0) {
357
-            throw new ApplicationLogicException('This target is already banned!');
358
-        }
359
-
360
-        $ban = new Ban();
361
-        $ban->setDatabase($database);
362
-        $ban->setActive(true);
363
-
364
-        $ban->setName($targetName);
365
-        $ban->setIp($targetIp, $targetMask);
366
-        $ban->setEmail($targetEmail);
367
-        $ban->setUseragent($targetUseragent);
368
-
369
-        $ban->setUser($user->getId());
370
-        $ban->setReason($reason);
371
-        $ban->setDuration($duration);
372
-        $ban->setVisibility($visibility);
373
-
374
-        $ban->setDomain($global ? null : $currentDomain->getId());
375
-
376
-        $ban->setAction($action);
377
-        if ($ban->getAction() === Ban::ACTION_DEFER) {
378
-            //FIXME: domains
379
-            $queue = RequestQueue::getByApiName($database, WebRequest::postString('banActionTarget'), 1);
380
-            if ($queue === false) {
381
-                throw new ApplicationLogicException("Unknown target queue");
382
-            }
383
-
384
-            if (!$queue->isEnabled()) {
385
-                throw new ApplicationLogicException("Target queue is not enabled");
386
-            }
387
-
388
-            $ban->setTargetQueue($queue->getId());
389
-        }
390
-
391
-        $ban->save();
392
-
393
-        Logger::banned($database, $ban, $reason);
394
-
395
-        $this->getNotificationHelper()->banned($ban);
396
-        SessionAlert::quick('Ban has been set.');
397
-
398
-        $this->redirect('bans');
399
-    }
400
-
401
-    /**
402
-     * Handles the GET method on the set action
403
-     * @throws Exception
404
-     */
405
-    private function handleGetMethodForSetBan()
406
-    {
407
-        $this->setTemplate('bans/banform.tpl');
408
-        $this->assignCSRFToken();
409
-
410
-        $this->assign('maxIpRange', $this->getSiteConfiguration()->getBanMaxIpRange());
411
-        $this->assign('maxIpBlockRange', $this->getSiteConfiguration()->getBanMaxIpBlockRange());
412
-
413
-        $this->assign('banVisibility', 'user');
414
-        $this->assign('banGlobal', false);
415
-        $this->assign('banQueue', false);
416
-        $this->assign('banAction', Ban::ACTION_BLOCK);
417
-        $this->assign('banDuration', '');
418
-        $this->assign('banReason', '');
419
-
420
-        $this->assign('banEmail', '');
421
-        $this->assign('banIP', '');
422
-        $this->assign('banName', '');
423
-        $this->assign('banUseragent', '');
424
-
425
-        $this->assign('replaceBanId', null);
426
-
427
-
428
-
429
-        $database = $this->getDatabase();
430
-
431
-        $user = User::getCurrent($database);
432
-        $this->setupSecurity($user);
433
-
434
-        $queues = RequestQueue::getEnabledQueues($database);
435
-
436
-        $this->assign('requestQueues', $queues);
437
-    }
438
-
439
-    /**
440
-     * Finds the Ban object referenced in the WebRequest if it is valid
441
-     *
442
-     * @return Ban
443
-     * @throws ApplicationLogicException
444
-     */
445
-    private function getBanForUnban(): Ban
446
-    {
447
-        $banId = WebRequest::getInt('id');
448
-        if ($banId === null || $banId === 0) {
449
-            throw new ApplicationLogicException("The ban ID appears to be missing. This is probably a bug.");
450
-        }
451
-
452
-        $database = $this->getDatabase();
453
-        $this->setupSecurity(User::getCurrent($database));
454
-        $currentDomain = Domain::getCurrent($database);
455
-        $ban = Ban::getActiveId($banId, $database, $currentDomain->getId());
456
-
457
-        if ($ban === false) {
458
-            throw new ApplicationLogicException("The specified ban is not currently active, or doesn't exist.");
459
-        }
460
-
461
-        return $ban;
462
-    }
463
-
464
-    /**
465
-     * Sets up Smarty variables for access control
466
-     */
467
-    private function setupSecurity(User $user): void
468
-    {
469
-        $this->assign('canSeeIpBan', $this->barrierTest('ip', $user, 'BanType'));
470
-        $this->assign('canSeeNameBan', $this->barrierTest('name', $user, 'BanType'));
471
-        $this->assign('canSeeEmailBan', $this->barrierTest('email', $user, 'BanType'));
472
-        $this->assign('canSeeUseragentBan', $this->barrierTest('useragent', $user, 'BanType'));
473
-
474
-        $this->assign('canGlobalBan', $this->barrierTest('global', $user, 'BanType'));
475
-
476
-        $this->assign('canSeeUserVisibility', $this->barrierTest('user', $user, 'BanVisibility'));
477
-        $this->assign('canSeeAdminVisibility', $this->barrierTest('admin', $user, 'BanVisibility'));
478
-        $this->assign('canSeeCheckuserVisibility', $this->barrierTest('checkuser', $user, 'BanVisibility'));
479
-    }
480
-
481
-    /**
482
-     * Validates that the provided IP is acceptable for a ban of this type
483
-     *
484
-     * @param string $targetIp   IP address
485
-     * @param int    $targetMask CIDR prefix length
486
-     * @param User   $user       User performing the ban
487
-     * @param string $action     Ban action to take
488
-     *
489
-     * @throws ApplicationLogicException
490
-     */
491
-    private function validateIpBan(string $targetIp, int $targetMask, User $user, string $action): void
492
-    {
493
-        // validate this is an IP
494
-        if (!filter_var($targetIp, FILTER_VALIDATE_IP)) {
495
-            throw new ApplicationLogicException("Not a valid IP address");
496
-        }
497
-
498
-        $canLargeIpBan = $this->barrierTest('ip-largerange', $user, 'BanType');
499
-        $maxIpBlockRange = $this->getSiteConfiguration()->getBanMaxIpBlockRange();
500
-        $maxIpRange = $this->getSiteConfiguration()->getBanMaxIpRange();
501
-
502
-        // validate CIDR ranges
503
-        if (filter_var($targetIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
504
-            if ($targetMask < 0 || $targetMask > 128) {
505
-                throw new ApplicationLogicException("CIDR mask out of range for IPv6");
506
-            }
507
-
508
-            // prevent setting the ban if:
509
-            //  * the user isn't allowed to set large bans, AND
510
-            //  * the ban is a drop or a block (preventing human review of the request), AND
511
-            //  * the mask is too wide-reaching
512
-            if (!$canLargeIpBan && ($action == Ban::ACTION_BLOCK || $action == Ban::ACTION_DROP) && $targetMask < $maxIpBlockRange[6]) {
513
-                throw new ApplicationLogicException("The requested IP range for this ban is too wide for the block/drop action.");
514
-            }
515
-
516
-            if (!$canLargeIpBan && $targetMask < $maxIpRange[6]) {
517
-                throw new ApplicationLogicException("The requested IP range for this ban is too wide.");
518
-            }
519
-        }
520
-
521
-        if (filter_var($targetIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
522
-            if ($targetMask < 0 || $targetMask > 32) {
523
-                throw new ApplicationLogicException("CIDR mask out of range for IPv4");
524
-            }
525
-
526
-            if (!$canLargeIpBan && ($action == Ban::ACTION_BLOCK || $action == Ban::ACTION_DROP) && $targetMask < $maxIpBlockRange[4]) {
527
-                throw new ApplicationLogicException("The IP range for this ban is too wide for the block/drop action.");
528
-            }
529
-
530
-            if (!$canLargeIpBan && $targetMask < $maxIpRange[4]) {
531
-                throw new ApplicationLogicException("The requested IP range for this ban is too wide.");
532
-            }
533
-        }
534
-
535
-        $squidIpList = $this->getSiteConfiguration()->getSquidList();
536
-        if (in_array($targetIp, $squidIpList)) {
537
-            throw new ApplicationLogicException("This IP address is on the protected list of proxies, and cannot be banned.");
538
-        }
539
-    }
540
-
541
-    /**
542
-     * Configures a ban list template for display
543
-     *
544
-     * @param Ban[] $bans
545
-     */
546
-    private function setupBanList(array $bans): void
547
-    {
548
-        $userIds = array_map(fn(Ban $entry) => $entry->getUser(), $bans);
549
-        $userList = UserSearchHelper::get($this->getDatabase())->inIds($userIds)->fetchMap('username');
550
-
551
-        $domainIds = array_filter(array_unique(array_map(fn(Ban $entry) => $entry->getDomain(), $bans)));
552
-        $domains = [];
553
-        foreach ($domainIds as $d) {
554
-            if ($d === null) {
555
-                continue;
556
-            }
557
-            $domains[$d] = Domain::getById($d, $this->getDatabase());
558
-        }
559
-
560
-        $this->assign('domains', $domains);
561
-
562
-        $user = User::getCurrent($this->getDatabase());
563
-        $this->assign('canSet', $this->barrierTest('set', $user));
564
-        $this->assign('canRemove', $this->barrierTest('remove', $user));
565
-
566
-        $this->setupSecurity($user);
567
-
568
-        $this->assign('usernames', $userList);
569
-        $this->assign('activebans', $bans);
570
-
571
-        $banHelper = new BanHelper($this->getDatabase(), $this->getXffTrustProvider(), $this->getSecurityManager());
572
-        $this->assign('banHelper', $banHelper);
573
-    }
574
-
575
-    /**
576
-     * Converts a plain IP or CIDR mask into an IP and a CIDR suffix
577
-     *
578
-     * @param string $targetIp IP or CIDR range
579
-     *
580
-     * @return array
581
-     */
582
-    private function splitCidrRange(string $targetIp): array
583
-    {
584
-        if (strpos($targetIp, '/') !== false) {
585
-            $ipParts = explode('/', $targetIp, 2);
586
-            $targetIp = $ipParts[0];
587
-            $targetMask = (int)$ipParts[1];
588
-        }
589
-        else {
590
-            // Default the CIDR range based on the IP type
591
-            $targetMask = filter_var($targetIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) ? 128 : 32;
592
-        }
593
-
594
-        return array($targetIp, $targetMask);
29
+	/**
30
+	 * Main function for this page, when no specific actions are called.
31
+	 */
32
+	protected function main(): void
33
+	{
34
+		$this->assignCSRFToken();
35
+		$this->setHtmlTitle('Bans');
36
+
37
+		$database = $this->getDatabase();
38
+		$currentDomain = Domain::getCurrent($database);
39
+		$bans = Ban::getActiveBans($database, $currentDomain->getId());
40
+
41
+		$this->setupBanList($bans);
42
+
43
+		$this->assign('isFiltered', false);
44
+		$this->setTemplate('bans/main.tpl');
45
+	}
46
+
47
+	protected function show(): void
48
+	{
49
+		$this->assignCSRFToken();
50
+		$this->setHtmlTitle('Bans');
51
+
52
+		$rawIdList = WebRequest::getString('id');
53
+		if ($rawIdList === null) {
54
+			$this->redirect('bans');
55
+
56
+			return;
57
+		}
58
+
59
+		$idList = explode(',', $rawIdList);
60
+
61
+		$database = $this->getDatabase();
62
+		$currentDomain = Domain::getCurrent($database);
63
+		$bans = Ban::getByIdList($idList, $database, $currentDomain->getId());
64
+
65
+		$this->setupBanList($bans);
66
+		$this->assign('isFiltered', true);
67
+		$this->setTemplate('bans/main.tpl');
68
+	}
69
+
70
+	/**
71
+	 * Entry point for the ban set action
72
+	 * @throws Smarty\Exception
73
+	 * @throws Exception
74
+	 */
75
+	protected function set(): void
76
+	{
77
+		$this->setHtmlTitle('Bans');
78
+
79
+		// dual-mode action
80
+		if (WebRequest::wasPosted()) {
81
+			try {
82
+				$this->handlePostMethodForSetBan();
83
+			}
84
+			catch (ApplicationLogicException $ex) {
85
+				SessionAlert::error($ex->getMessage());
86
+				$this->redirect("bans", "set");
87
+			}
88
+		}
89
+		else {
90
+			$this->handleGetMethodForSetBan();
91
+
92
+			$user = User::getCurrent($this->getDatabase());
93
+			$banType = WebRequest::getString('type');
94
+			$banRequest = WebRequest::getInt('request');
95
+
96
+			// if the parameters are null, skip loading a request.
97
+			if ($banType !== null && $banRequest !== null && $banRequest !== 0) {
98
+				$this->preloadFormForRequest($banRequest, $banType, $user);
99
+			}
100
+		}
101
+	}
102
+
103
+	protected function replace(): void
104
+	{
105
+		$this->setHtmlTitle('Bans');
106
+
107
+		$database = $this->getDatabase();
108
+		$domain = Domain::getCurrent($database);
109
+
110
+		// dual-mode action
111
+		if (WebRequest::wasPosted()) {
112
+			try {
113
+				$originalBanId = WebRequest::postInt('replaceBanId');
114
+				$originalBanUpdateVersion = WebRequest::postInt('replaceBanUpdateVersion');
115
+
116
+				$originalBan = Ban::getActiveId($originalBanId, $database, $domain->getId());
117
+
118
+				if ($originalBan === false) {
119
+					throw new ApplicationLogicException("The specified ban is not currently active, or doesn't exist.");
120
+				}
121
+
122
+				// Discard original ban; we're replacing it.
123
+				$originalBan->setUpdateVersion($originalBanUpdateVersion);
124
+				$originalBan->setActive(false);
125
+				$originalBan->save();
126
+
127
+				Logger::banReplaced($database, $originalBan);
128
+
129
+				// Proceed as normal to save the new ban.
130
+				$this->handlePostMethodForSetBan();
131
+			}
132
+			catch (ApplicationLogicException $ex) {
133
+				$database->rollback();
134
+				SessionAlert::error($ex->getMessage());
135
+				$this->redirect("bans", "set");
136
+			}
137
+		}
138
+		else {
139
+			$this->handleGetMethodForSetBan();
140
+
141
+			$user = User::getCurrent($database);
142
+			$originalBanId = WebRequest::getString('id');
143
+
144
+			$originalBan = Ban::getActiveId($originalBanId, $database, $domain->getId());
145
+
146
+			if ($originalBan === false) {
147
+				throw new ApplicationLogicException("The specified ban is not currently active, or doesn't exist.");
148
+			}
149
+
150
+			if ($originalBan->getName() !== null) {
151
+				if (!$this->barrierTest('name', $user, 'BanType')) {
152
+					SessionAlert::error("You are not allowed to set this type of ban.");
153
+					$this->redirect("bans", "set");
154
+					return;
155
+				}
156
+
157
+				$this->assign('banName', $originalBan->getName());
158
+			}
159
+
160
+			if ($originalBan->getEmail() !== null) {
161
+				if (!$this->barrierTest('email', $user, 'BanType')) {
162
+					SessionAlert::error("You are not allowed to set this type of ban.");
163
+					$this->redirect("bans", "set");
164
+					return;
165
+				}
166
+
167
+				$this->assign('banEmail', $originalBan->getEmail());
168
+			}
169
+
170
+			if ($originalBan->getUseragent() !== null) {
171
+				if (!$this->barrierTest('useragent', $user, 'BanType')) {
172
+					SessionAlert::error("You are not allowed to set this type of ban.");
173
+					$this->redirect("bans", "set");
174
+					return;
175
+				}
176
+
177
+				$this->assign('banUseragent', $originalBan->getUseragent());
178
+			}
179
+
180
+			if ($originalBan->getIp() !== null) {
181
+				if (!$this->barrierTest('ip', $user, 'BanType')) {
182
+					SessionAlert::error("You are not allowed to set this type of ban.");
183
+					$this->redirect("bans", "set");
184
+					return;
185
+				}
186
+
187
+				$this->assign('banIP', $originalBan->getIp() . '/' . $originalBan->getIpMask());
188
+			}
189
+
190
+			$banIsGlobal = $originalBan->getDomain() === null;
191
+			if ($banIsGlobal) {
192
+				if (!$this->barrierTest('global', $user, 'BanType')) {
193
+					SessionAlert::error("You are not allowed to set this type of ban.");
194
+					$this->redirect("bans", "set");
195
+					return;
196
+				}
197
+			}
198
+
199
+			if (!$this->barrierTest($originalBan->getVisibility(), $user, 'BanVisibility')) {
200
+				SessionAlert::error("You are not allowed to set this type of ban.");
201
+				$this->redirect("bans", "set");
202
+				return;
203
+			}
204
+
205
+			$this->assign('banGlobal', $banIsGlobal);
206
+			$this->assign('banVisibility', $originalBan->getVisibility());
207
+
208
+			if ($originalBan->getDuration() !== null) {
209
+				$this->assign('banDuration', date('c', $originalBan->getDuration()));
210
+			}
211
+
212
+			$this->assign('banReason', $originalBan->getReason());
213
+			$this->assign('banAction', $originalBan->getAction());
214
+			$this->assign('banQueue', $originalBan->getTargetQueue());
215
+
216
+			$this->assign('replaceBanId', $originalBan->getId());
217
+			$this->assign('replaceBanUpdateVersion', $originalBan->getUpdateVersion());
218
+		}
219
+	}
220
+
221
+	/**
222
+	 * Entry point for the ban remove action
223
+	 *
224
+	 * @throws AccessDeniedException
225
+	 * @throws ApplicationLogicException
226
+	 * @throws Smarty\Exception
227
+	 */
228
+	protected function remove(): void
229
+	{
230
+		$this->setHtmlTitle('Bans');
231
+
232
+		$ban = $this->getBanForUnban();
233
+
234
+		$banHelper = new BanHelper($this->getDatabase(), $this->getXffTrustProvider(), $this->getSecurityManager());
235
+		if (!$banHelper->canUnban($ban)) {
236
+			// triggered when a user tries to unban a ban they can't see the entirety of.
237
+			// there's no UI way to get to this, so a raw exception is fine.
238
+			throw new AccessDeniedException($this->getSecurityManager(), $this->getDomainAccessManager());
239
+		}
240
+
241
+		// dual mode
242
+		if (WebRequest::wasPosted()) {
243
+			$this->validateCSRFToken();
244
+			$unbanReason = WebRequest::postString('unbanreason');
245
+
246
+			if ($unbanReason === null || trim($unbanReason) === "") {
247
+				SessionAlert::error('No unban reason specified');
248
+				$this->redirect("bans", "remove", array('id' => $ban->getId()));
249
+			}
250
+
251
+			// set optimistic locking from delete form page load
252
+			$updateVersion = WebRequest::postInt('updateversion');
253
+			$ban->setUpdateVersion($updateVersion);
254
+
255
+			$database = $this->getDatabase();
256
+			$ban->setActive(false);
257
+			$ban->save();
258
+
259
+			Logger::unbanned($database, $ban, $unbanReason);
260
+
261
+			SessionAlert::quick('Disabled ban.');
262
+			$this->getNotificationHelper()->unbanned($ban, $unbanReason);
263
+
264
+			$this->redirect('bans');
265
+		}
266
+		else {
267
+			$this->assignCSRFToken();
268
+			$this->assign('ban', $ban);
269
+			$this->setTemplate('bans/unban.tpl');
270
+		}
271
+	}
272
+
273
+	/**
274
+	 * Retrieves the requested ban duration from the WebRequest
275
+	 *
276
+	 * @throws ApplicationLogicException
277
+	 */
278
+	private function getBanDuration(): ?int
279
+	{
280
+		$duration = WebRequest::postString('duration');
281
+		if ($duration === "other") {
282
+			$duration = strtotime(WebRequest::postString('otherduration'));
283
+
284
+			if (!$duration) {
285
+				throw new ApplicationLogicException('Invalid ban time');
286
+			}
287
+			elseif (time() > $duration) {
288
+				throw new ApplicationLogicException('Ban time has already expired!');
289
+			}
290
+
291
+			return $duration;
292
+		}
293
+		elseif ($duration === "-1") {
294
+			return null;
295
+		}
296
+		else {
297
+			return WebRequest::postInt('duration') + time();
298
+		}
299
+	}
300
+
301
+	/**
302
+	 * Handles the POST method on the set action
303
+	 *
304
+	 * @throws ApplicationLogicException
305
+	 * @throws Exception
306
+	 */
307
+	private function handlePostMethodForSetBan()
308
+	{
309
+		$this->validateCSRFToken();
310
+		$database = $this->getDatabase();
311
+		$user = User::getCurrent($database);
312
+		$currentDomain = Domain::getCurrent($database);
313
+
314
+		// Checks whether there is a reason entered for ban.
315
+		$reason = WebRequest::postString('banreason');
316
+		if ($reason === null || trim($reason) === "") {
317
+			throw new ApplicationLogicException('You must specify a ban reason');
318
+		}
319
+
320
+		// ban targets
321
+		list($targetName, $targetIp, $targetEmail, $targetUseragent) = $this->getRawBanTargets($user);
322
+
323
+		$visibility = $this->getBanVisibility();
324
+
325
+		// Validate ban duration
326
+		$duration = $this->getBanDuration();
327
+
328
+		$action = WebRequest::postString('banAction') ?? Ban::ACTION_NONE;
329
+
330
+		$global = WebRequest::postBoolean('banGlobal');
331
+		if (!$this->barrierTest('global', $user, 'BanType')) {
332
+			$global = false;
333
+		}
334
+
335
+		if ($action === Ban::ACTION_DEFER && $global) {
336
+			throw new ApplicationLogicException("Cannot set a global ban in defer-to-queue mode.");
337
+		}
338
+
339
+		// handle CIDR ranges
340
+		$targetMask = null;
341
+		if ($targetIp !== null) {
342
+			list($targetIp, $targetMask) = $this->splitCidrRange($targetIp);
343
+			$this->validateIpBan($targetIp, $targetMask, $user, $action);
344
+		}
345
+
346
+		$banHelper = new BanHelper($this->getDatabase(), $this->getXffTrustProvider(), $this->getSecurityManager());
347
+
348
+		$bansByTarget = $banHelper->getBansByTarget(
349
+			$targetName,
350
+			$targetEmail,
351
+			$targetIp,
352
+			$targetMask,
353
+			$targetUseragent,
354
+			$currentDomain->getId());
355
+
356
+		if (count($bansByTarget) > 0) {
357
+			throw new ApplicationLogicException('This target is already banned!');
358
+		}
359
+
360
+		$ban = new Ban();
361
+		$ban->setDatabase($database);
362
+		$ban->setActive(true);
363
+
364
+		$ban->setName($targetName);
365
+		$ban->setIp($targetIp, $targetMask);
366
+		$ban->setEmail($targetEmail);
367
+		$ban->setUseragent($targetUseragent);
368
+
369
+		$ban->setUser($user->getId());
370
+		$ban->setReason($reason);
371
+		$ban->setDuration($duration);
372
+		$ban->setVisibility($visibility);
373
+
374
+		$ban->setDomain($global ? null : $currentDomain->getId());
375
+
376
+		$ban->setAction($action);
377
+		if ($ban->getAction() === Ban::ACTION_DEFER) {
378
+			//FIXME: domains
379
+			$queue = RequestQueue::getByApiName($database, WebRequest::postString('banActionTarget'), 1);
380
+			if ($queue === false) {
381
+				throw new ApplicationLogicException("Unknown target queue");
382
+			}
383
+
384
+			if (!$queue->isEnabled()) {
385
+				throw new ApplicationLogicException("Target queue is not enabled");
386
+			}
387
+
388
+			$ban->setTargetQueue($queue->getId());
389
+		}
390
+
391
+		$ban->save();
392
+
393
+		Logger::banned($database, $ban, $reason);
394
+
395
+		$this->getNotificationHelper()->banned($ban);
396
+		SessionAlert::quick('Ban has been set.');
397
+
398
+		$this->redirect('bans');
399
+	}
400
+
401
+	/**
402
+	 * Handles the GET method on the set action
403
+	 * @throws Exception
404
+	 */
405
+	private function handleGetMethodForSetBan()
406
+	{
407
+		$this->setTemplate('bans/banform.tpl');
408
+		$this->assignCSRFToken();
409
+
410
+		$this->assign('maxIpRange', $this->getSiteConfiguration()->getBanMaxIpRange());
411
+		$this->assign('maxIpBlockRange', $this->getSiteConfiguration()->getBanMaxIpBlockRange());
412
+
413
+		$this->assign('banVisibility', 'user');
414
+		$this->assign('banGlobal', false);
415
+		$this->assign('banQueue', false);
416
+		$this->assign('banAction', Ban::ACTION_BLOCK);
417
+		$this->assign('banDuration', '');
418
+		$this->assign('banReason', '');
419
+
420
+		$this->assign('banEmail', '');
421
+		$this->assign('banIP', '');
422
+		$this->assign('banName', '');
423
+		$this->assign('banUseragent', '');
424
+
425
+		$this->assign('replaceBanId', null);
426
+
427
+
428
+
429
+		$database = $this->getDatabase();
430
+
431
+		$user = User::getCurrent($database);
432
+		$this->setupSecurity($user);
433
+
434
+		$queues = RequestQueue::getEnabledQueues($database);
435
+
436
+		$this->assign('requestQueues', $queues);
437
+	}
438
+
439
+	/**
440
+	 * Finds the Ban object referenced in the WebRequest if it is valid
441
+	 *
442
+	 * @return Ban
443
+	 * @throws ApplicationLogicException
444
+	 */
445
+	private function getBanForUnban(): Ban
446
+	{
447
+		$banId = WebRequest::getInt('id');
448
+		if ($banId === null || $banId === 0) {
449
+			throw new ApplicationLogicException("The ban ID appears to be missing. This is probably a bug.");
450
+		}
451
+
452
+		$database = $this->getDatabase();
453
+		$this->setupSecurity(User::getCurrent($database));
454
+		$currentDomain = Domain::getCurrent($database);
455
+		$ban = Ban::getActiveId($banId, $database, $currentDomain->getId());
456
+
457
+		if ($ban === false) {
458
+			throw new ApplicationLogicException("The specified ban is not currently active, or doesn't exist.");
459
+		}
460
+
461
+		return $ban;
462
+	}
463
+
464
+	/**
465
+	 * Sets up Smarty variables for access control
466
+	 */
467
+	private function setupSecurity(User $user): void
468
+	{
469
+		$this->assign('canSeeIpBan', $this->barrierTest('ip', $user, 'BanType'));
470
+		$this->assign('canSeeNameBan', $this->barrierTest('name', $user, 'BanType'));
471
+		$this->assign('canSeeEmailBan', $this->barrierTest('email', $user, 'BanType'));
472
+		$this->assign('canSeeUseragentBan', $this->barrierTest('useragent', $user, 'BanType'));
473
+
474
+		$this->assign('canGlobalBan', $this->barrierTest('global', $user, 'BanType'));
475
+
476
+		$this->assign('canSeeUserVisibility', $this->barrierTest('user', $user, 'BanVisibility'));
477
+		$this->assign('canSeeAdminVisibility', $this->barrierTest('admin', $user, 'BanVisibility'));
478
+		$this->assign('canSeeCheckuserVisibility', $this->barrierTest('checkuser', $user, 'BanVisibility'));
479
+	}
480
+
481
+	/**
482
+	 * Validates that the provided IP is acceptable for a ban of this type
483
+	 *
484
+	 * @param string $targetIp   IP address
485
+	 * @param int    $targetMask CIDR prefix length
486
+	 * @param User   $user       User performing the ban
487
+	 * @param string $action     Ban action to take
488
+	 *
489
+	 * @throws ApplicationLogicException
490
+	 */
491
+	private function validateIpBan(string $targetIp, int $targetMask, User $user, string $action): void
492
+	{
493
+		// validate this is an IP
494
+		if (!filter_var($targetIp, FILTER_VALIDATE_IP)) {
495
+			throw new ApplicationLogicException("Not a valid IP address");
496
+		}
497
+
498
+		$canLargeIpBan = $this->barrierTest('ip-largerange', $user, 'BanType');
499
+		$maxIpBlockRange = $this->getSiteConfiguration()->getBanMaxIpBlockRange();
500
+		$maxIpRange = $this->getSiteConfiguration()->getBanMaxIpRange();
501
+
502
+		// validate CIDR ranges
503
+		if (filter_var($targetIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
504
+			if ($targetMask < 0 || $targetMask > 128) {
505
+				throw new ApplicationLogicException("CIDR mask out of range for IPv6");
506
+			}
507
+
508
+			// prevent setting the ban if:
509
+			//  * the user isn't allowed to set large bans, AND
510
+			//  * the ban is a drop or a block (preventing human review of the request), AND
511
+			//  * the mask is too wide-reaching
512
+			if (!$canLargeIpBan && ($action == Ban::ACTION_BLOCK || $action == Ban::ACTION_DROP) && $targetMask < $maxIpBlockRange[6]) {
513
+				throw new ApplicationLogicException("The requested IP range for this ban is too wide for the block/drop action.");
514
+			}
515
+
516
+			if (!$canLargeIpBan && $targetMask < $maxIpRange[6]) {
517
+				throw new ApplicationLogicException("The requested IP range for this ban is too wide.");
518
+			}
519
+		}
520
+
521
+		if (filter_var($targetIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
522
+			if ($targetMask < 0 || $targetMask > 32) {
523
+				throw new ApplicationLogicException("CIDR mask out of range for IPv4");
524
+			}
525
+
526
+			if (!$canLargeIpBan && ($action == Ban::ACTION_BLOCK || $action == Ban::ACTION_DROP) && $targetMask < $maxIpBlockRange[4]) {
527
+				throw new ApplicationLogicException("The IP range for this ban is too wide for the block/drop action.");
528
+			}
529
+
530
+			if (!$canLargeIpBan && $targetMask < $maxIpRange[4]) {
531
+				throw new ApplicationLogicException("The requested IP range for this ban is too wide.");
532
+			}
533
+		}
534
+
535
+		$squidIpList = $this->getSiteConfiguration()->getSquidList();
536
+		if (in_array($targetIp, $squidIpList)) {
537
+			throw new ApplicationLogicException("This IP address is on the protected list of proxies, and cannot be banned.");
538
+		}
539
+	}
540
+
541
+	/**
542
+	 * Configures a ban list template for display
543
+	 *
544
+	 * @param Ban[] $bans
545
+	 */
546
+	private function setupBanList(array $bans): void
547
+	{
548
+		$userIds = array_map(fn(Ban $entry) => $entry->getUser(), $bans);
549
+		$userList = UserSearchHelper::get($this->getDatabase())->inIds($userIds)->fetchMap('username');
550
+
551
+		$domainIds = array_filter(array_unique(array_map(fn(Ban $entry) => $entry->getDomain(), $bans)));
552
+		$domains = [];
553
+		foreach ($domainIds as $d) {
554
+			if ($d === null) {
555
+				continue;
556
+			}
557
+			$domains[$d] = Domain::getById($d, $this->getDatabase());
558
+		}
559
+
560
+		$this->assign('domains', $domains);
561
+
562
+		$user = User::getCurrent($this->getDatabase());
563
+		$this->assign('canSet', $this->barrierTest('set', $user));
564
+		$this->assign('canRemove', $this->barrierTest('remove', $user));
565
+
566
+		$this->setupSecurity($user);
567
+
568
+		$this->assign('usernames', $userList);
569
+		$this->assign('activebans', $bans);
570
+
571
+		$banHelper = new BanHelper($this->getDatabase(), $this->getXffTrustProvider(), $this->getSecurityManager());
572
+		$this->assign('banHelper', $banHelper);
573
+	}
574
+
575
+	/**
576
+	 * Converts a plain IP or CIDR mask into an IP and a CIDR suffix
577
+	 *
578
+	 * @param string $targetIp IP or CIDR range
579
+	 *
580
+	 * @return array
581
+	 */
582
+	private function splitCidrRange(string $targetIp): array
583
+	{
584
+		if (strpos($targetIp, '/') !== false) {
585
+			$ipParts = explode('/', $targetIp, 2);
586
+			$targetIp = $ipParts[0];
587
+			$targetMask = (int)$ipParts[1];
588
+		}
589
+		else {
590
+			// Default the CIDR range based on the IP type
591
+			$targetMask = filter_var($targetIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) ? 128 : 32;
592
+		}
593
+
594
+		return array($targetIp, $targetMask);
595 595
 }
596 596
 
597
-    /**
598
-     * Returns the validated ban visibility from WebRequest
599
-     *
600
-     * @throws ApplicationLogicException
601
-     */
602
-    private function getBanVisibility(): string
603
-    {
604
-        $visibility = WebRequest::postString('banVisibility');
605
-        if ($visibility !== 'user' && $visibility !== 'admin' && $visibility !== 'checkuser') {
606
-            throw new ApplicationLogicException('Invalid ban visibility');
607
-        }
608
-
609
-        return $visibility;
610
-    }
611
-
612
-    /**
613
-     * Returns array of [username, ip, email, ua] as ban targets from WebRequest,
614
-     * filtered for whether the user is allowed to set bans including those types.
615
-     *
616
-     * @return string[]
617
-     * @throws ApplicationLogicException
618
-     */
619
-    private function getRawBanTargets(User $user): array
620
-    {
621
-        $targetName = WebRequest::postString('banName');
622
-        $targetIp = WebRequest::postString('banIP');
623
-        $targetEmail = WebRequest::postString('banEmail');
624
-        $targetUseragent = WebRequest::postString('banUseragent');
625
-
626
-        // check the user is allowed to use provided targets
627
-        if (!$this->barrierTest('name', $user, 'BanType')) {
628
-            $targetName = null;
629
-        }
630
-        if (!$this->barrierTest('ip', $user, 'BanType')) {
631
-            $targetIp = null;
632
-        }
633
-        if (!$this->barrierTest('email', $user, 'BanType')) {
634
-            $targetEmail = null;
635
-        }
636
-        if (!$this->barrierTest('useragent', $user, 'BanType')) {
637
-            $targetUseragent = null;
638
-        }
639
-
640
-        // Checks whether there is a target entered to ban.
641
-        if ($targetName === null && $targetIp === null && $targetEmail === null && $targetUseragent === null) {
642
-            throw new ApplicationLogicException('You must specify a target to be banned');
643
-        }
644
-
645
-        return array($targetName, $targetIp, $targetEmail, $targetUseragent);
646
-    }
647
-
648
-    private function preloadFormForRequest(int $banRequest, string $banType, User $user): void
649
-    {
650
-        $database = $this->getDatabase();
651
-
652
-        // Attempt to resolve the correct target
653
-        /** @var Request|false $request */
654
-        $request = Request::getById($banRequest, $database);
655
-        if ($request === false) {
656
-            $this->assign('bantarget', '');
657
-
658
-            return;
659
-        }
660
-
661
-        switch ($banType) {
662
-            case 'EMail':
663
-                if ($this->barrierTest('email', $user, 'BanType')) {
664
-                    $this->assign('banEmail', $request->getEmail());
665
-                }
666
-                break;
667
-            case 'IP':
668
-                if ($this->barrierTest('ip', $user, 'BanType')) {
669
-                    $trustedIp = $this->getXffTrustProvider()->getTrustedClientIp(
670
-                        $request->getIp(),
671
-                        $request->getForwardedIp());
672
-
673
-                    $this->assign('banIP', $trustedIp);
674
-                }
675
-                break;
676
-            case 'Name':
677
-                if ($this->barrierTest('name', $user, 'BanType')) {
678
-                    $this->assign('banName', $request->getName());
679
-                }
680
-                break;
681
-            case 'UA':
682
-                if ($this->barrierTest('useragent', $user, 'BanType')) {
683
-                    $this->assign('banUseragent', $request->getEmail());
684
-                }
685
-                break;
686
-        }
687
-    }
597
+	/**
598
+	 * Returns the validated ban visibility from WebRequest
599
+	 *
600
+	 * @throws ApplicationLogicException
601
+	 */
602
+	private function getBanVisibility(): string
603
+	{
604
+		$visibility = WebRequest::postString('banVisibility');
605
+		if ($visibility !== 'user' && $visibility !== 'admin' && $visibility !== 'checkuser') {
606
+			throw new ApplicationLogicException('Invalid ban visibility');
607
+		}
608
+
609
+		return $visibility;
610
+	}
611
+
612
+	/**
613
+	 * Returns array of [username, ip, email, ua] as ban targets from WebRequest,
614
+	 * filtered for whether the user is allowed to set bans including those types.
615
+	 *
616
+	 * @return string[]
617
+	 * @throws ApplicationLogicException
618
+	 */
619
+	private function getRawBanTargets(User $user): array
620
+	{
621
+		$targetName = WebRequest::postString('banName');
622
+		$targetIp = WebRequest::postString('banIP');
623
+		$targetEmail = WebRequest::postString('banEmail');
624
+		$targetUseragent = WebRequest::postString('banUseragent');
625
+
626
+		// check the user is allowed to use provided targets
627
+		if (!$this->barrierTest('name', $user, 'BanType')) {
628
+			$targetName = null;
629
+		}
630
+		if (!$this->barrierTest('ip', $user, 'BanType')) {
631
+			$targetIp = null;
632
+		}
633
+		if (!$this->barrierTest('email', $user, 'BanType')) {
634
+			$targetEmail = null;
635
+		}
636
+		if (!$this->barrierTest('useragent', $user, 'BanType')) {
637
+			$targetUseragent = null;
638
+		}
639
+
640
+		// Checks whether there is a target entered to ban.
641
+		if ($targetName === null && $targetIp === null && $targetEmail === null && $targetUseragent === null) {
642
+			throw new ApplicationLogicException('You must specify a target to be banned');
643
+		}
644
+
645
+		return array($targetName, $targetIp, $targetEmail, $targetUseragent);
646
+	}
647
+
648
+	private function preloadFormForRequest(int $banRequest, string $banType, User $user): void
649
+	{
650
+		$database = $this->getDatabase();
651
+
652
+		// Attempt to resolve the correct target
653
+		/** @var Request|false $request */
654
+		$request = Request::getById($banRequest, $database);
655
+		if ($request === false) {
656
+			$this->assign('bantarget', '');
657
+
658
+			return;
659
+		}
660
+
661
+		switch ($banType) {
662
+			case 'EMail':
663
+				if ($this->barrierTest('email', $user, 'BanType')) {
664
+					$this->assign('banEmail', $request->getEmail());
665
+				}
666
+				break;
667
+			case 'IP':
668
+				if ($this->barrierTest('ip', $user, 'BanType')) {
669
+					$trustedIp = $this->getXffTrustProvider()->getTrustedClientIp(
670
+						$request->getIp(),
671
+						$request->getForwardedIp());
672
+
673
+					$this->assign('banIP', $trustedIp);
674
+				}
675
+				break;
676
+			case 'Name':
677
+				if ($this->barrierTest('name', $user, 'BanType')) {
678
+					$this->assign('banName', $request->getName());
679
+				}
680
+				break;
681
+			case 'UA':
682
+				if ($this->barrierTest('useragent', $user, 'BanType')) {
683
+					$this->assign('banUseragent', $request->getEmail());
684
+				}
685
+				break;
686
+		}
687
+	}
688 688
 }
Please login to merge, or discard this patch.
includes/Pages/PageUserManagement.php 1 patch
Indentation   +636 added lines, -636 removed lines patch added patch discarded remove patch
@@ -29,640 +29,640 @@
 block discarded – undo
29 29
  */
30 30
 class PageUserManagement extends InternalPageBase
31 31
 {
32
-    // FIXME: domains
33
-    /** @var string */
34
-    private $adminMailingList = '[email protected]';
35
-
36
-    /**
37
-     * Main function for this page, when no specific actions are called.
38
-     */
39
-    protected function main()
40
-    {
41
-        $this->setHtmlTitle('User Management');
42
-
43
-        $database = $this->getDatabase();
44
-        $currentUser = User::getCurrent($database);
45
-
46
-        $userSearchRequest = WebRequest::getString('usersearch');
47
-        if ($userSearchRequest !== null) {
48
-            $searchedUser = User::getByUsername($userSearchRequest, $database);
49
-            if ($searchedUser !== false) {
50
-                $this->redirect('statistics/users', 'detail', ['user' => $searchedUser->getId()]);
51
-                return;
52
-            }
53
-        }
54
-
55
-        // A bit hacky, but it's better than my last solution of creating an object for each user and passing that to
56
-        // the template. I still don't have a particularly good way of handling this.
57
-        OAuthUserHelper::prepareTokenCountStatement($database);
58
-
59
-        if (WebRequest::getBoolean("showAll")) {
60
-            $this->assign("showAll", true);
61
-
62
-            $suspendedUsers = UserSearchHelper::get($database)->byStatus(User::STATUS_SUSPENDED)->fetch();
63
-            $this->assign("suspendedUsers", $suspendedUsers);
64
-
65
-            $declinedUsers = UserSearchHelper::get($database)->byStatus(User::STATUS_DECLINED)->fetch();
66
-            $this->assign("declinedUsers", $declinedUsers);
67
-
68
-            UserSearchHelper::get($database)->getRoleMap($roleMap);
69
-        }
70
-        else {
71
-            $this->assign("showAll", false);
72
-            $this->assign("suspendedUsers", array());
73
-            $this->assign("declinedUsers", array());
74
-
75
-            UserSearchHelper::get($database)->statusIn(array('New', 'Active'))->getRoleMap($roleMap);
76
-        }
77
-
78
-        $newUsers = UserSearchHelper::get($database)->byStatus(User::STATUS_NEW)->fetch();
79
-        $normalUsers = UserSearchHelper::get($database)->byStatus(User::STATUS_ACTIVE)->byRole('user')->fetch();
80
-        $adminUsers = UserSearchHelper::get($database)->byStatus(User::STATUS_ACTIVE)->byRole('admin')->fetch();
81
-        $checkUsers = UserSearchHelper::get($database)->byStatus(User::STATUS_ACTIVE)->byRole('checkuser')->fetch();
82
-        $stewards = UserSearchHelper::get($database)->byStatus(User::STATUS_ACTIVE)->byRole('steward')->fetch();
83
-        $toolRoots = UserSearchHelper::get($database)->byStatus(User::STATUS_ACTIVE)->byRole('toolRoot')->fetch();
84
-        $this->assign('newUsers', $newUsers);
85
-        $this->assign('normalUsers', $normalUsers);
86
-        $this->assign('adminUsers', $adminUsers);
87
-        $this->assign('checkUsers', $checkUsers);
88
-        $this->assign('stewards', $stewards);
89
-        $this->assign('toolRoots', $toolRoots);
90
-
91
-        $this->assign('roles', $roleMap);
92
-
93
-        $this->addJs("/api.php?action=users&all=true&targetVariable=typeaheaddata");
94
-
95
-        $this->assign('canApprove', $this->barrierTest('approve', $currentUser));
96
-        $this->assign('canDecline', $this->barrierTest('decline', $currentUser));
97
-        $this->assign('canRename', $this->barrierTest('rename', $currentUser));
98
-        $this->assign('canEditUser', $this->barrierTest('editUser', $currentUser));
99
-        $this->assign('canSuspend', $this->barrierTest('suspend', $currentUser));
100
-        $this->assign('canEditRoles', $this->barrierTest('editRoles', $currentUser));
101
-
102
-        // FIXME: domains!
103
-        /** @var Domain $domain */
104
-        $domain = Domain::getById(1, $this->getDatabase());
105
-        $this->assign('mediawikiScriptPath', $domain->getWikiArticlePath());
106
-
107
-        $this->setTemplate("usermanagement/main.tpl");
108
-    }
109
-
110
-    #region Access control
111
-
112
-    /**
113
-     * Action target for editing the roles assigned to a user
114
-     *
115
-     * @throws ApplicationLogicException
116
-     * @throws Smarty\Exception
117
-     * @throws OptimisticLockFailedException
118
-     * @throws Exception
119
-     */
120
-    protected function editRoles(): void
121
-    {
122
-        $this->setHtmlTitle('User Management');
123
-        $database = $this->getDatabase();
124
-        $domain = Domain::getCurrent($database);
125
-        $userId = WebRequest::getInt('user');
126
-
127
-        /** @var User|false $user */
128
-        $user = User::getById($userId, $database);
129
-
130
-        if ($user === false || $user->isCommunityUser()) {
131
-            throw new ApplicationLogicException('Sorry, the user you are trying to edit could not be found.');
132
-        }
133
-
134
-        $roleData = $this->getRoleData(UserRole::getForUser($user->getId(), $database, $domain->getId()));
135
-
136
-        // Dual-mode action
137
-        if (WebRequest::wasPosted()) {
138
-            $this->validateCSRFToken();
139
-
140
-            $reason = WebRequest::postString('reason');
141
-            if ($reason === false || trim($reason) === '') {
142
-                throw new ApplicationLogicException('No reason specified for roles change');
143
-            }
144
-
145
-            /** @var UserRole[] $delete */
146
-            $delete = array();
147
-            /** @var string[] $add */
148
-            $add = array();
149
-
150
-            /** @var UserRole[] $globalDelete */
151
-            $globalDelete = array();
152
-            /** @var string[] $globalAdd */
153
-            $globalAdd = array();
154
-
155
-            foreach ($roleData as $name => $r) {
156
-                if ($r['allowEdit'] !== 1) {
157
-                    // not allowed, to touch this, so ignore it
158
-                    continue;
159
-                }
160
-
161
-                $newValue = WebRequest::postBoolean('role-' . $name) ? 1 : 0;
162
-                if ($newValue !== $r['active']) {
163
-                    if ($newValue === 0) {
164
-                        if ($r['globalOnly']) {
165
-                            $globalDelete[] = $r['object'];
166
-                        }
167
-                        else {
168
-                            $delete[] = $r['object'];
169
-                        }
170
-                    }
171
-
172
-                    if ($newValue === 1) {
173
-                        if ($r['globalOnly']) {
174
-                            $globalAdd[] = $name;
175
-                        }
176
-                        else {
177
-                            $add[] = $name;
178
-                        }
179
-                    }
180
-                }
181
-            }
182
-
183
-            // Check there's something to do
184
-            if ((count($add) + count($delete) + count($globalAdd) + count($globalDelete)) === 0) {
185
-                $this->redirect('statistics/users', 'detail', array('user' => $user->getId()));
186
-                SessionAlert::warning('No changes made to roles.');
187
-
188
-                return;
189
-            }
190
-
191
-            $removed = array();
192
-            $globalRemoved = array();
193
-
194
-            foreach ($delete as $d) {
195
-                $removed[] = $d->getRole();
196
-                $d->delete();
197
-            }
198
-
199
-            foreach ($globalDelete as $d) {
200
-                $globalRemoved[] = $d->getRole();
201
-                $d->delete();
202
-            }
203
-
204
-            foreach ($add as $x) {
205
-                $a = new UserRole();
206
-                $a->setUser($user->getId());
207
-                $a->setRole($x);
208
-                $a->setDomain($domain->getId());
209
-                $a->setDatabase($database);
210
-                $a->save();
211
-            }
212
-
213
-            foreach ($globalAdd as $x) {
214
-                $a = new UserRole();
215
-                $a->setUser($user->getId());
216
-                $a->setRole($x);
217
-                $a->setDomain(null);
218
-                $a->setDatabase($database);
219
-                $a->save();
220
-            }
221
-
222
-            if ((count($add) + count($delete)) > 0) {
223
-                Logger::userRolesEdited($database, $user, $reason, $add, $removed, $domain->getId());
224
-            }
225
-
226
-            if ((count($globalAdd) + count($globalDelete)) > 0) {
227
-                Logger::userGlobalRolesEdited($database, $user, $reason, $globalAdd, $globalRemoved);
228
-            }
229
-
230
-            // dummy save for optimistic locking. If this fails, the entire txn will roll back.
231
-            $user->setUpdateVersion(WebRequest::postInt('updateversion'));
232
-            $user->save();
233
-
234
-            $this->getNotificationHelper()->userRolesEdited($user, $reason);
235
-            SessionAlert::quick('Roles changed for user ' . htmlentities($user->getUsername(), ENT_COMPAT, 'UTF-8'));
236
-
237
-            $this->redirect('statistics/users', 'detail', array('user' => $user->getId()));
238
-        }
239
-        else {
240
-            $this->assignCSRFToken();
241
-            $this->setTemplate('usermanagement/roleedit.tpl');
242
-            $this->assign('user', $user);
243
-            $this->assign('roleData', $roleData);
244
-        }
245
-    }
246
-
247
-    /**
248
-     * Action target for suspending users
249
-     *
250
-     * @throws ApplicationLogicException
251
-     */
252
-    protected function suspend()
253
-    {
254
-        $this->setHtmlTitle('User Management');
255
-
256
-        $database = $this->getDatabase();
257
-
258
-        $userId = WebRequest::getInt('user');
259
-
260
-        /** @var User $user */
261
-        $user = User::getById($userId, $database);
262
-
263
-        if ($user === false || $user->isCommunityUser()) {
264
-            throw new ApplicationLogicException('Sorry, the user you are trying to suspend could not be found.');
265
-        }
266
-
267
-        if ($user->isSuspended()) {
268
-            throw new ApplicationLogicException('Sorry, the user you are trying to suspend is already suspended.');
269
-        }
270
-
271
-        // Dual-mode action
272
-        if (WebRequest::wasPosted()) {
273
-            $this->validateCSRFToken();
274
-            $reason = WebRequest::postString('reason');
275
-
276
-            if ($reason === null || trim($reason) === "") {
277
-                throw new ApplicationLogicException('No reason provided');
278
-            }
279
-
280
-            $user->setStatus(User::STATUS_SUSPENDED);
281
-            $user->setUpdateVersion(WebRequest::postInt('updateversion'));
282
-            $user->save();
283
-            Logger::suspendedUser($database, $user, $reason);
284
-
285
-            $this->getNotificationHelper()->userSuspended($user, $reason);
286
-            SessionAlert::quick('Suspended user ' . htmlentities($user->getUsername(), ENT_COMPAT, 'UTF-8'));
287
-
288
-            // send email
289
-            $this->sendStatusChangeEmail(
290
-                'Your WP:ACC account has been suspended',
291
-                'usermanagement/emails/suspended.tpl',
292
-                $reason,
293
-                $user,
294
-                User::getCurrent($database)->getUsername()
295
-            );
296
-
297
-            $this->redirect('userManagement');
298
-
299
-            return;
300
-        }
301
-        else {
302
-            $this->assignCSRFToken();
303
-            $this->setTemplate('usermanagement/changelevel-reason.tpl');
304
-            $this->assign('user', $user);
305
-            $this->assign('status', 'Suspended');
306
-            $this->assign("showReason", true);
307
-
308
-            if (WebRequest::getString('preload')) {
309
-                $this->assign('preload', WebRequest::getString('preload'));
310
-            }
311
-        }
312
-    }
313
-
314
-    /**
315
-     * Entry point for the decline action
316
-     *
317
-     * @throws ApplicationLogicException
318
-     */
319
-    protected function decline()
320
-    {
321
-        $this->setHtmlTitle('User Management');
322
-
323
-        $database = $this->getDatabase();
324
-
325
-        $userId = WebRequest::getInt('user');
326
-        $user = User::getById($userId, $database);
327
-
328
-        if ($user === false || $user->isCommunityUser()) {
329
-            throw new ApplicationLogicException('Sorry, the user you are trying to decline could not be found.');
330
-        }
331
-
332
-        if (!$user->isNewUser()) {
333
-            throw new ApplicationLogicException('Sorry, the user you are trying to decline is not new.');
334
-        }
335
-
336
-        // Dual-mode action
337
-        if (WebRequest::wasPosted()) {
338
-            $this->validateCSRFToken();
339
-            $reason = WebRequest::postString('reason');
340
-
341
-            if ($reason === null || trim($reason) === "") {
342
-                throw new ApplicationLogicException('No reason provided');
343
-            }
344
-
345
-            $user->setStatus(User::STATUS_DECLINED);
346
-            $user->setUpdateVersion(WebRequest::postInt('updateversion'));
347
-            $user->save();
348
-            Logger::declinedUser($database, $user, $reason);
349
-
350
-            $this->getNotificationHelper()->userDeclined($user, $reason);
351
-            SessionAlert::quick('Declined user ' . htmlentities($user->getUsername(), ENT_COMPAT, 'UTF-8'));
352
-
353
-            // send email
354
-            $this->sendStatusChangeEmail(
355
-                'Your WP:ACC account has been declined',
356
-                'usermanagement/emails/declined.tpl',
357
-                $reason,
358
-                $user,
359
-                User::getCurrent($database)->getUsername()
360
-            );
361
-
362
-            $this->redirect('userManagement');
363
-
364
-            return;
365
-        }
366
-        else {
367
-            $this->assignCSRFToken();
368
-            $this->setTemplate('usermanagement/changelevel-reason.tpl');
369
-            $this->assign('user', $user);
370
-            $this->assign('status', 'Declined');
371
-            $this->assign("showReason", true);
372
-        }
373
-    }
374
-
375
-    /**
376
-     * Entry point for the approve action
377
-     *
378
-     * @throws ApplicationLogicException
379
-     */
380
-    protected function approve()
381
-    {
382
-        $this->setHtmlTitle('User Management');
383
-
384
-        $database = $this->getDatabase();
385
-
386
-        $userId = WebRequest::getInt('user');
387
-        $user = User::getById($userId, $database);
388
-
389
-        if ($user === false || $user->isCommunityUser()) {
390
-            throw new ApplicationLogicException('Sorry, the user you are trying to approve could not be found.');
391
-        }
392
-
393
-        if ($user->isActive()) {
394
-            throw new ApplicationLogicException('Sorry, the user you are trying to approve is already an active user.');
395
-        }
396
-
397
-        // Dual-mode action
398
-        if (WebRequest::wasPosted()) {
399
-            $this->validateCSRFToken();
400
-            $user->setStatus(User::STATUS_ACTIVE);
401
-            $user->setUpdateVersion(WebRequest::postInt('updateversion'));
402
-            $user->save();
403
-            Logger::approvedUser($database, $user);
404
-
405
-            $this->getNotificationHelper()->userApproved($user);
406
-            SessionAlert::quick('Approved user ' . htmlentities($user->getUsername(), ENT_COMPAT, 'UTF-8'));
407
-
408
-            // send email
409
-            $this->sendStatusChangeEmail(
410
-                'Your WP:ACC account has been approved',
411
-                'usermanagement/emails/approved.tpl',
412
-                null,
413
-                $user,
414
-                User::getCurrent($database)->getUsername()
415
-            );
416
-
417
-            $this->redirect("userManagement");
418
-
419
-            return;
420
-        }
421
-        else {
422
-            $this->assignCSRFToken();
423
-            $this->setTemplate("usermanagement/changelevel-reason.tpl");
424
-            $this->assign("user", $user);
425
-            $this->assign("status", "Active");
426
-            $this->assign("showReason", false);
427
-        }
428
-    }
429
-
430
-    #endregion
431
-
432
-    #region Renaming / Editing
433
-
434
-    /**
435
-     * Entry point for the rename action
436
-     *
437
-     * @throws ApplicationLogicException
438
-     */
439
-    protected function rename()
440
-    {
441
-        $this->setHtmlTitle('User Management');
442
-
443
-        $database = $this->getDatabase();
444
-
445
-        $userId = WebRequest::getInt('user');
446
-        $user = User::getById($userId, $database);
447
-
448
-        if ($user === false || $user->isCommunityUser()) {
449
-            throw new ApplicationLogicException('Sorry, the user you are trying to rename could not be found.');
450
-        }
451
-
452
-        // Dual-mode action
453
-        if (WebRequest::wasPosted()) {
454
-            $this->validateCSRFToken();
455
-            $newUsername = WebRequest::postString('newname');
456
-
457
-            if ($newUsername === null || trim($newUsername) === "") {
458
-                throw new ApplicationLogicException('The new username cannot be empty');
459
-            }
460
-
461
-            if (User::getByUsername($newUsername, $database) != false) {
462
-                throw new ApplicationLogicException('The new username already exists');
463
-            }
464
-
465
-            $oldUsername = $user->getUsername();
466
-            $user->setUsername($newUsername);
467
-            $user->setUpdateVersion(WebRequest::postInt('updateversion'));
468
-
469
-            $user->save();
470
-
471
-            $logEntryData = serialize(array(
472
-                'old' => $oldUsername,
473
-                'new' => $newUsername,
474
-            ));
475
-
476
-            Logger::renamedUser($database, $user, $logEntryData);
477
-
478
-            SessionAlert::quick("Changed User "
479
-                . htmlentities($oldUsername, ENT_COMPAT, 'UTF-8')
480
-                . " name to "
481
-                . htmlentities($newUsername, ENT_COMPAT, 'UTF-8'));
482
-
483
-            $this->getNotificationHelper()->userRenamed($user, $oldUsername);
484
-
485
-            // send an email to the user.
486
-            $this->assign('targetUsername', $user->getUsername());
487
-            $this->assign('toolAdmin', User::getCurrent($database)->getUsername());
488
-            $this->assign('oldUsername', $oldUsername);
489
-            $this->assign('mailingList', $this->adminMailingList);
490
-
491
-            // FIXME: domains!
492
-            /** @var Domain $domain */
493
-            $domain = Domain::getById(1, $database);
494
-            $this->getEmailHelper()->sendMail(
495
-                $this->adminMailingList,
496
-                $user->getEmail(),
497
-                'Your username on WP:ACC has been changed',
498
-                $this->fetchTemplate('usermanagement/emails/renamed.tpl')
499
-            );
500
-
501
-            $this->redirect("userManagement");
502
-
503
-            return;
504
-        }
505
-        else {
506
-            $this->assignCSRFToken();
507
-            $this->setTemplate('usermanagement/renameuser.tpl');
508
-            $this->assign('user', $user);
509
-        }
510
-    }
511
-
512
-    /**
513
-     * Entry point for the edit action
514
-     *
515
-     * @throws ApplicationLogicException
516
-     */
517
-    protected function editUser()
518
-    {
519
-        $this->setHtmlTitle('User Management');
520
-
521
-        $database = $this->getDatabase();
522
-
523
-        $userId = WebRequest::getInt('user');
524
-        $user = User::getById($userId, $database);
525
-        $oauth = new OAuthUserHelper($user, $database, $this->getOAuthProtocolHelper(), $this->getSiteConfiguration());
526
-
527
-        if ($user === false || $user->isCommunityUser()) {
528
-            throw new ApplicationLogicException('Sorry, the user you are trying to edit could not be found.');
529
-        }
530
-
531
-        // FIXME: domains
532
-        $prefs = new PreferenceManager($database, $user->getId(), 1);
533
-
534
-        // Dual-mode action
535
-        if (WebRequest::wasPosted()) {
536
-            $this->validateCSRFToken();
537
-            $newEmail = WebRequest::postEmail('user_email');
538
-            $newOnWikiName = WebRequest::postString('user_onwikiname');
539
-
540
-            if ($newEmail === null) {
541
-                throw new ApplicationLogicException('Invalid email address');
542
-            }
543
-
544
-            if ($this->validateUnusedEmail($newEmail, $userId)) {
545
-                throw new ApplicationLogicException('The specified email address is already in use.');
546
-            }
547
-
548
-            if (!($oauth->isFullyLinked() || $oauth->isPartiallyLinked())) {
549
-                if (trim($newOnWikiName) == "") {
550
-                    throw new ApplicationLogicException('New on-wiki username cannot be blank');
551
-                }
552
-
553
-                $user->setOnWikiName($newOnWikiName);
554
-            }
555
-
556
-            $user->setEmail($newEmail);
557
-
558
-            $prefs->setLocalPreference(PreferenceManager::PREF_CREATION_MODE, WebRequest::postInt('creationmode'));
559
-
560
-            $user->setUpdateVersion(WebRequest::postInt('updateversion'));
561
-
562
-            $user->save();
563
-
564
-            Logger::userPreferencesChange($database, $user);
565
-            $this->getNotificationHelper()->userPrefChange($user);
566
-            SessionAlert::quick('Changes to user\'s preferences have been saved');
567
-
568
-            $this->redirect("userManagement");
569
-
570
-            return;
571
-        }
572
-        else {
573
-            $this->assignCSRFToken();
574
-            $oauth = new OAuthUserHelper($user, $database, $this->getOAuthProtocolHelper(),
575
-                $this->getSiteConfiguration());
576
-            $this->setTemplate('usermanagement/edituser.tpl');
577
-            $this->assign('user', $user);
578
-            $this->assign('oauth', $oauth);
579
-
580
-            $this->assign('preferredCreationMode', (int)$prefs->getPreference(PreferenceManager::PREF_CREATION_MODE));
581
-            $this->assign('emailSignature', $prefs->getPreference(PreferenceManager::PREF_EMAIL_SIGNATURE));
582
-
583
-            $this->assign('canManualCreate',
584
-                $this->barrierTest(PreferenceManager::CREATION_MANUAL, $user, 'RequestCreation'));
585
-            $this->assign('canOauthCreate',
586
-                $this->barrierTest(PreferenceManager::CREATION_OAUTH, $user, 'RequestCreation'));
587
-            $this->assign('canBotCreate',
588
-                $this->barrierTest(PreferenceManager::CREATION_BOT, $user, 'RequestCreation'));
589
-        }
590
-    }
591
-
592
-    #endregion
593
-
594
-    private function validateUnusedEmail(string $email, int $userId) : bool {
595
-        $query = 'SELECT COUNT(id) FROM user WHERE email = :email AND id <> :uid';
596
-        $statement = $this->getDatabase()->prepare($query);
597
-        $statement->execute(array(':email' => $email, ':uid' => $userId));
598
-        $inUse = $statement->fetchColumn() > 0;
599
-        $statement->closeCursor();
600
-
601
-        return $inUse;
602
-    }
603
-
604
-    /**
605
-     * Sends a status change email to the user.
606
-     *
607
-     * @param string      $subject           The subject of the email
608
-     * @param string      $template          The smarty template to use
609
-     * @param string|null $reason            The reason for performing the status change
610
-     * @param User        $user              The user affected
611
-     * @param string      $toolAdminUsername The tool admin's username who is making the edit
612
-     */
613
-    private function sendStatusChangeEmail($subject, $template, $reason, $user, $toolAdminUsername)
614
-    {
615
-        $this->assign('targetUsername', $user->getUsername());
616
-        $this->assign('toolAdmin', $toolAdminUsername);
617
-        $this->assign('actionReason', $reason);
618
-        $this->assign('mailingList', $this->adminMailingList);
619
-
620
-        // FIXME: domains!
621
-        /** @var Domain $domain */
622
-        $domain = Domain::getById(1, $this->getDatabase());
623
-        $this->getEmailHelper()->sendMail(
624
-            $this->adminMailingList,
625
-            $user->getEmail(),
626
-            $subject,
627
-            $this->fetchTemplate($template)
628
-        );
629
-    }
630
-
631
-    /**
632
-     * @param UserRole[] $activeRoles
633
-     *
634
-     * @return array
635
-     */
636
-    private function getRoleData($activeRoles)
637
-    {
638
-        $availableRoles = $this->getSecurityManager()->getRoleConfiguration()->getAvailableRoles();
639
-
640
-        $currentUser = User::getCurrent($this->getDatabase());
641
-        $this->getSecurityManager()->getActiveRoles($currentUser, $userRoles, $inactiveRoles);
642
-
643
-        $initialValue = array('active' => 0, 'allowEdit' => 0, 'description' => '???', 'object' => null);
644
-
645
-        $roleData = array();
646
-        foreach ($availableRoles as $role => $data) {
647
-            $intersection = array_intersect($data['editableBy'], $userRoles);
648
-
649
-            $roleData[$role] = $initialValue;
650
-            $roleData[$role]['allowEdit'] = count($intersection) > 0 ? 1 : 0;
651
-            $roleData[$role]['description'] = $data['description'];
652
-            $roleData[$role]['globalOnly'] = $data['globalOnly'];
653
-        }
654
-
655
-        foreach ($activeRoles as $role) {
656
-            if (!isset($roleData[$role->getRole()])) {
657
-                // This value is no longer available in the configuration, allow changing (aka removing) it.
658
-                $roleData[$role->getRole()] = $initialValue;
659
-                $roleData[$role->getRole()]['allowEdit'] = 1;
660
-            }
661
-
662
-            $roleData[$role->getRole()]['object'] = $role;
663
-            $roleData[$role->getRole()]['active'] = 1;
664
-        }
665
-
666
-        return $roleData;
667
-    }
32
+	// FIXME: domains
33
+	/** @var string */
34
+	private $adminMailingList = '[email protected]';
35
+
36
+	/**
37
+	 * Main function for this page, when no specific actions are called.
38
+	 */
39
+	protected function main()
40
+	{
41
+		$this->setHtmlTitle('User Management');
42
+
43
+		$database = $this->getDatabase();
44
+		$currentUser = User::getCurrent($database);
45
+
46
+		$userSearchRequest = WebRequest::getString('usersearch');
47
+		if ($userSearchRequest !== null) {
48
+			$searchedUser = User::getByUsername($userSearchRequest, $database);
49
+			if ($searchedUser !== false) {
50
+				$this->redirect('statistics/users', 'detail', ['user' => $searchedUser->getId()]);
51
+				return;
52
+			}
53
+		}
54
+
55
+		// A bit hacky, but it's better than my last solution of creating an object for each user and passing that to
56
+		// the template. I still don't have a particularly good way of handling this.
57
+		OAuthUserHelper::prepareTokenCountStatement($database);
58
+
59
+		if (WebRequest::getBoolean("showAll")) {
60
+			$this->assign("showAll", true);
61
+
62
+			$suspendedUsers = UserSearchHelper::get($database)->byStatus(User::STATUS_SUSPENDED)->fetch();
63
+			$this->assign("suspendedUsers", $suspendedUsers);
64
+
65
+			$declinedUsers = UserSearchHelper::get($database)->byStatus(User::STATUS_DECLINED)->fetch();
66
+			$this->assign("declinedUsers", $declinedUsers);
67
+
68
+			UserSearchHelper::get($database)->getRoleMap($roleMap);
69
+		}
70
+		else {
71
+			$this->assign("showAll", false);
72
+			$this->assign("suspendedUsers", array());
73
+			$this->assign("declinedUsers", array());
74
+
75
+			UserSearchHelper::get($database)->statusIn(array('New', 'Active'))->getRoleMap($roleMap);
76
+		}
77
+
78
+		$newUsers = UserSearchHelper::get($database)->byStatus(User::STATUS_NEW)->fetch();
79
+		$normalUsers = UserSearchHelper::get($database)->byStatus(User::STATUS_ACTIVE)->byRole('user')->fetch();
80
+		$adminUsers = UserSearchHelper::get($database)->byStatus(User::STATUS_ACTIVE)->byRole('admin')->fetch();
81
+		$checkUsers = UserSearchHelper::get($database)->byStatus(User::STATUS_ACTIVE)->byRole('checkuser')->fetch();
82
+		$stewards = UserSearchHelper::get($database)->byStatus(User::STATUS_ACTIVE)->byRole('steward')->fetch();
83
+		$toolRoots = UserSearchHelper::get($database)->byStatus(User::STATUS_ACTIVE)->byRole('toolRoot')->fetch();
84
+		$this->assign('newUsers', $newUsers);
85
+		$this->assign('normalUsers', $normalUsers);
86
+		$this->assign('adminUsers', $adminUsers);
87
+		$this->assign('checkUsers', $checkUsers);
88
+		$this->assign('stewards', $stewards);
89
+		$this->assign('toolRoots', $toolRoots);
90
+
91
+		$this->assign('roles', $roleMap);
92
+
93
+		$this->addJs("/api.php?action=users&all=true&targetVariable=typeaheaddata");
94
+
95
+		$this->assign('canApprove', $this->barrierTest('approve', $currentUser));
96
+		$this->assign('canDecline', $this->barrierTest('decline', $currentUser));
97
+		$this->assign('canRename', $this->barrierTest('rename', $currentUser));
98
+		$this->assign('canEditUser', $this->barrierTest('editUser', $currentUser));
99
+		$this->assign('canSuspend', $this->barrierTest('suspend', $currentUser));
100
+		$this->assign('canEditRoles', $this->barrierTest('editRoles', $currentUser));
101
+
102
+		// FIXME: domains!
103
+		/** @var Domain $domain */
104
+		$domain = Domain::getById(1, $this->getDatabase());
105
+		$this->assign('mediawikiScriptPath', $domain->getWikiArticlePath());
106
+
107
+		$this->setTemplate("usermanagement/main.tpl");
108
+	}
109
+
110
+	#region Access control
111
+
112
+	/**
113
+	 * Action target for editing the roles assigned to a user
114
+	 *
115
+	 * @throws ApplicationLogicException
116
+	 * @throws Smarty\Exception
117
+	 * @throws OptimisticLockFailedException
118
+	 * @throws Exception
119
+	 */
120
+	protected function editRoles(): void
121
+	{
122
+		$this->setHtmlTitle('User Management');
123
+		$database = $this->getDatabase();
124
+		$domain = Domain::getCurrent($database);
125
+		$userId = WebRequest::getInt('user');
126
+
127
+		/** @var User|false $user */
128
+		$user = User::getById($userId, $database);
129
+
130
+		if ($user === false || $user->isCommunityUser()) {
131
+			throw new ApplicationLogicException('Sorry, the user you are trying to edit could not be found.');
132
+		}
133
+
134
+		$roleData = $this->getRoleData(UserRole::getForUser($user->getId(), $database, $domain->getId()));
135
+
136
+		// Dual-mode action
137
+		if (WebRequest::wasPosted()) {
138
+			$this->validateCSRFToken();
139
+
140
+			$reason = WebRequest::postString('reason');
141
+			if ($reason === false || trim($reason) === '') {
142
+				throw new ApplicationLogicException('No reason specified for roles change');
143
+			}
144
+
145
+			/** @var UserRole[] $delete */
146
+			$delete = array();
147
+			/** @var string[] $add */
148
+			$add = array();
149
+
150
+			/** @var UserRole[] $globalDelete */
151
+			$globalDelete = array();
152
+			/** @var string[] $globalAdd */
153
+			$globalAdd = array();
154
+
155
+			foreach ($roleData as $name => $r) {
156
+				if ($r['allowEdit'] !== 1) {
157
+					// not allowed, to touch this, so ignore it
158
+					continue;
159
+				}
160
+
161
+				$newValue = WebRequest::postBoolean('role-' . $name) ? 1 : 0;
162
+				if ($newValue !== $r['active']) {
163
+					if ($newValue === 0) {
164
+						if ($r['globalOnly']) {
165
+							$globalDelete[] = $r['object'];
166
+						}
167
+						else {
168
+							$delete[] = $r['object'];
169
+						}
170
+					}
171
+
172
+					if ($newValue === 1) {
173
+						if ($r['globalOnly']) {
174
+							$globalAdd[] = $name;
175
+						}
176
+						else {
177
+							$add[] = $name;
178
+						}
179
+					}
180
+				}
181
+			}
182
+
183
+			// Check there's something to do
184
+			if ((count($add) + count($delete) + count($globalAdd) + count($globalDelete)) === 0) {
185
+				$this->redirect('statistics/users', 'detail', array('user' => $user->getId()));
186
+				SessionAlert::warning('No changes made to roles.');
187
+
188
+				return;
189
+			}
190
+
191
+			$removed = array();
192
+			$globalRemoved = array();
193
+
194
+			foreach ($delete as $d) {
195
+				$removed[] = $d->getRole();
196
+				$d->delete();
197
+			}
198
+
199
+			foreach ($globalDelete as $d) {
200
+				$globalRemoved[] = $d->getRole();
201
+				$d->delete();
202
+			}
203
+
204
+			foreach ($add as $x) {
205
+				$a = new UserRole();
206
+				$a->setUser($user->getId());
207
+				$a->setRole($x);
208
+				$a->setDomain($domain->getId());
209
+				$a->setDatabase($database);
210
+				$a->save();
211
+			}
212
+
213
+			foreach ($globalAdd as $x) {
214
+				$a = new UserRole();
215
+				$a->setUser($user->getId());
216
+				$a->setRole($x);
217
+				$a->setDomain(null);
218
+				$a->setDatabase($database);
219
+				$a->save();
220
+			}
221
+
222
+			if ((count($add) + count($delete)) > 0) {
223
+				Logger::userRolesEdited($database, $user, $reason, $add, $removed, $domain->getId());
224
+			}
225
+
226
+			if ((count($globalAdd) + count($globalDelete)) > 0) {
227
+				Logger::userGlobalRolesEdited($database, $user, $reason, $globalAdd, $globalRemoved);
228
+			}
229
+
230
+			// dummy save for optimistic locking. If this fails, the entire txn will roll back.
231
+			$user->setUpdateVersion(WebRequest::postInt('updateversion'));
232
+			$user->save();
233
+
234
+			$this->getNotificationHelper()->userRolesEdited($user, $reason);
235
+			SessionAlert::quick('Roles changed for user ' . htmlentities($user->getUsername(), ENT_COMPAT, 'UTF-8'));
236
+
237
+			$this->redirect('statistics/users', 'detail', array('user' => $user->getId()));
238
+		}
239
+		else {
240
+			$this->assignCSRFToken();
241
+			$this->setTemplate('usermanagement/roleedit.tpl');
242
+			$this->assign('user', $user);
243
+			$this->assign('roleData', $roleData);
244
+		}
245
+	}
246
+
247
+	/**
248
+	 * Action target for suspending users
249
+	 *
250
+	 * @throws ApplicationLogicException
251
+	 */
252
+	protected function suspend()
253
+	{
254
+		$this->setHtmlTitle('User Management');
255
+
256
+		$database = $this->getDatabase();
257
+
258
+		$userId = WebRequest::getInt('user');
259
+
260
+		/** @var User $user */
261
+		$user = User::getById($userId, $database);
262
+
263
+		if ($user === false || $user->isCommunityUser()) {
264
+			throw new ApplicationLogicException('Sorry, the user you are trying to suspend could not be found.');
265
+		}
266
+
267
+		if ($user->isSuspended()) {
268
+			throw new ApplicationLogicException('Sorry, the user you are trying to suspend is already suspended.');
269
+		}
270
+
271
+		// Dual-mode action
272
+		if (WebRequest::wasPosted()) {
273
+			$this->validateCSRFToken();
274
+			$reason = WebRequest::postString('reason');
275
+
276
+			if ($reason === null || trim($reason) === "") {
277
+				throw new ApplicationLogicException('No reason provided');
278
+			}
279
+
280
+			$user->setStatus(User::STATUS_SUSPENDED);
281
+			$user->setUpdateVersion(WebRequest::postInt('updateversion'));
282
+			$user->save();
283
+			Logger::suspendedUser($database, $user, $reason);
284
+
285
+			$this->getNotificationHelper()->userSuspended($user, $reason);
286
+			SessionAlert::quick('Suspended user ' . htmlentities($user->getUsername(), ENT_COMPAT, 'UTF-8'));
287
+
288
+			// send email
289
+			$this->sendStatusChangeEmail(
290
+				'Your WP:ACC account has been suspended',
291
+				'usermanagement/emails/suspended.tpl',
292
+				$reason,
293
+				$user,
294
+				User::getCurrent($database)->getUsername()
295
+			);
296
+
297
+			$this->redirect('userManagement');
298
+
299
+			return;
300
+		}
301
+		else {
302
+			$this->assignCSRFToken();
303
+			$this->setTemplate('usermanagement/changelevel-reason.tpl');
304
+			$this->assign('user', $user);
305
+			$this->assign('status', 'Suspended');
306
+			$this->assign("showReason", true);
307
+
308
+			if (WebRequest::getString('preload')) {
309
+				$this->assign('preload', WebRequest::getString('preload'));
310
+			}
311
+		}
312
+	}
313
+
314
+	/**
315
+	 * Entry point for the decline action
316
+	 *
317
+	 * @throws ApplicationLogicException
318
+	 */
319
+	protected function decline()
320
+	{
321
+		$this->setHtmlTitle('User Management');
322
+
323
+		$database = $this->getDatabase();
324
+
325
+		$userId = WebRequest::getInt('user');
326
+		$user = User::getById($userId, $database);
327
+
328
+		if ($user === false || $user->isCommunityUser()) {
329
+			throw new ApplicationLogicException('Sorry, the user you are trying to decline could not be found.');
330
+		}
331
+
332
+		if (!$user->isNewUser()) {
333
+			throw new ApplicationLogicException('Sorry, the user you are trying to decline is not new.');
334
+		}
335
+
336
+		// Dual-mode action
337
+		if (WebRequest::wasPosted()) {
338
+			$this->validateCSRFToken();
339
+			$reason = WebRequest::postString('reason');
340
+
341
+			if ($reason === null || trim($reason) === "") {
342
+				throw new ApplicationLogicException('No reason provided');
343
+			}
344
+
345
+			$user->setStatus(User::STATUS_DECLINED);
346
+			$user->setUpdateVersion(WebRequest::postInt('updateversion'));
347
+			$user->save();
348
+			Logger::declinedUser($database, $user, $reason);
349
+
350
+			$this->getNotificationHelper()->userDeclined($user, $reason);
351
+			SessionAlert::quick('Declined user ' . htmlentities($user->getUsername(), ENT_COMPAT, 'UTF-8'));
352
+
353
+			// send email
354
+			$this->sendStatusChangeEmail(
355
+				'Your WP:ACC account has been declined',
356
+				'usermanagement/emails/declined.tpl',
357
+				$reason,
358
+				$user,
359
+				User::getCurrent($database)->getUsername()
360
+			);
361
+
362
+			$this->redirect('userManagement');
363
+
364
+			return;
365
+		}
366
+		else {
367
+			$this->assignCSRFToken();
368
+			$this->setTemplate('usermanagement/changelevel-reason.tpl');
369
+			$this->assign('user', $user);
370
+			$this->assign('status', 'Declined');
371
+			$this->assign("showReason", true);
372
+		}
373
+	}
374
+
375
+	/**
376
+	 * Entry point for the approve action
377
+	 *
378
+	 * @throws ApplicationLogicException
379
+	 */
380
+	protected function approve()
381
+	{
382
+		$this->setHtmlTitle('User Management');
383
+
384
+		$database = $this->getDatabase();
385
+
386
+		$userId = WebRequest::getInt('user');
387
+		$user = User::getById($userId, $database);
388
+
389
+		if ($user === false || $user->isCommunityUser()) {
390
+			throw new ApplicationLogicException('Sorry, the user you are trying to approve could not be found.');
391
+		}
392
+
393
+		if ($user->isActive()) {
394
+			throw new ApplicationLogicException('Sorry, the user you are trying to approve is already an active user.');
395
+		}
396
+
397
+		// Dual-mode action
398
+		if (WebRequest::wasPosted()) {
399
+			$this->validateCSRFToken();
400
+			$user->setStatus(User::STATUS_ACTIVE);
401
+			$user->setUpdateVersion(WebRequest::postInt('updateversion'));
402
+			$user->save();
403
+			Logger::approvedUser($database, $user);
404
+
405
+			$this->getNotificationHelper()->userApproved($user);
406
+			SessionAlert::quick('Approved user ' . htmlentities($user->getUsername(), ENT_COMPAT, 'UTF-8'));
407
+
408
+			// send email
409
+			$this->sendStatusChangeEmail(
410
+				'Your WP:ACC account has been approved',
411
+				'usermanagement/emails/approved.tpl',
412
+				null,
413
+				$user,
414
+				User::getCurrent($database)->getUsername()
415
+			);
416
+
417
+			$this->redirect("userManagement");
418
+
419
+			return;
420
+		}
421
+		else {
422
+			$this->assignCSRFToken();
423
+			$this->setTemplate("usermanagement/changelevel-reason.tpl");
424
+			$this->assign("user", $user);
425
+			$this->assign("status", "Active");
426
+			$this->assign("showReason", false);
427
+		}
428
+	}
429
+
430
+	#endregion
431
+
432
+	#region Renaming / Editing
433
+
434
+	/**
435
+	 * Entry point for the rename action
436
+	 *
437
+	 * @throws ApplicationLogicException
438
+	 */
439
+	protected function rename()
440
+	{
441
+		$this->setHtmlTitle('User Management');
442
+
443
+		$database = $this->getDatabase();
444
+
445
+		$userId = WebRequest::getInt('user');
446
+		$user = User::getById($userId, $database);
447
+
448
+		if ($user === false || $user->isCommunityUser()) {
449
+			throw new ApplicationLogicException('Sorry, the user you are trying to rename could not be found.');
450
+		}
451
+
452
+		// Dual-mode action
453
+		if (WebRequest::wasPosted()) {
454
+			$this->validateCSRFToken();
455
+			$newUsername = WebRequest::postString('newname');
456
+
457
+			if ($newUsername === null || trim($newUsername) === "") {
458
+				throw new ApplicationLogicException('The new username cannot be empty');
459
+			}
460
+
461
+			if (User::getByUsername($newUsername, $database) != false) {
462
+				throw new ApplicationLogicException('The new username already exists');
463
+			}
464
+
465
+			$oldUsername = $user->getUsername();
466
+			$user->setUsername($newUsername);
467
+			$user->setUpdateVersion(WebRequest::postInt('updateversion'));
468
+
469
+			$user->save();
470
+
471
+			$logEntryData = serialize(array(
472
+				'old' => $oldUsername,
473
+				'new' => $newUsername,
474
+			));
475
+
476
+			Logger::renamedUser($database, $user, $logEntryData);
477
+
478
+			SessionAlert::quick("Changed User "
479
+				. htmlentities($oldUsername, ENT_COMPAT, 'UTF-8')
480
+				. " name to "
481
+				. htmlentities($newUsername, ENT_COMPAT, 'UTF-8'));
482
+
483
+			$this->getNotificationHelper()->userRenamed($user, $oldUsername);
484
+
485
+			// send an email to the user.
486
+			$this->assign('targetUsername', $user->getUsername());
487
+			$this->assign('toolAdmin', User::getCurrent($database)->getUsername());
488
+			$this->assign('oldUsername', $oldUsername);
489
+			$this->assign('mailingList', $this->adminMailingList);
490
+
491
+			// FIXME: domains!
492
+			/** @var Domain $domain */
493
+			$domain = Domain::getById(1, $database);
494
+			$this->getEmailHelper()->sendMail(
495
+				$this->adminMailingList,
496
+				$user->getEmail(),
497
+				'Your username on WP:ACC has been changed',
498
+				$this->fetchTemplate('usermanagement/emails/renamed.tpl')
499
+			);
500
+
501
+			$this->redirect("userManagement");
502
+
503
+			return;
504
+		}
505
+		else {
506
+			$this->assignCSRFToken();
507
+			$this->setTemplate('usermanagement/renameuser.tpl');
508
+			$this->assign('user', $user);
509
+		}
510
+	}
511
+
512
+	/**
513
+	 * Entry point for the edit action
514
+	 *
515
+	 * @throws ApplicationLogicException
516
+	 */
517
+	protected function editUser()
518
+	{
519
+		$this->setHtmlTitle('User Management');
520
+
521
+		$database = $this->getDatabase();
522
+
523
+		$userId = WebRequest::getInt('user');
524
+		$user = User::getById($userId, $database);
525
+		$oauth = new OAuthUserHelper($user, $database, $this->getOAuthProtocolHelper(), $this->getSiteConfiguration());
526
+
527
+		if ($user === false || $user->isCommunityUser()) {
528
+			throw new ApplicationLogicException('Sorry, the user you are trying to edit could not be found.');
529
+		}
530
+
531
+		// FIXME: domains
532
+		$prefs = new PreferenceManager($database, $user->getId(), 1);
533
+
534
+		// Dual-mode action
535
+		if (WebRequest::wasPosted()) {
536
+			$this->validateCSRFToken();
537
+			$newEmail = WebRequest::postEmail('user_email');
538
+			$newOnWikiName = WebRequest::postString('user_onwikiname');
539
+
540
+			if ($newEmail === null) {
541
+				throw new ApplicationLogicException('Invalid email address');
542
+			}
543
+
544
+			if ($this->validateUnusedEmail($newEmail, $userId)) {
545
+				throw new ApplicationLogicException('The specified email address is already in use.');
546
+			}
547
+
548
+			if (!($oauth->isFullyLinked() || $oauth->isPartiallyLinked())) {
549
+				if (trim($newOnWikiName) == "") {
550
+					throw new ApplicationLogicException('New on-wiki username cannot be blank');
551
+				}
552
+
553
+				$user->setOnWikiName($newOnWikiName);
554
+			}
555
+
556
+			$user->setEmail($newEmail);
557
+
558
+			$prefs->setLocalPreference(PreferenceManager::PREF_CREATION_MODE, WebRequest::postInt('creationmode'));
559
+
560
+			$user->setUpdateVersion(WebRequest::postInt('updateversion'));
561
+
562
+			$user->save();
563
+
564
+			Logger::userPreferencesChange($database, $user);
565
+			$this->getNotificationHelper()->userPrefChange($user);
566
+			SessionAlert::quick('Changes to user\'s preferences have been saved');
567
+
568
+			$this->redirect("userManagement");
569
+
570
+			return;
571
+		}
572
+		else {
573
+			$this->assignCSRFToken();
574
+			$oauth = new OAuthUserHelper($user, $database, $this->getOAuthProtocolHelper(),
575
+				$this->getSiteConfiguration());
576
+			$this->setTemplate('usermanagement/edituser.tpl');
577
+			$this->assign('user', $user);
578
+			$this->assign('oauth', $oauth);
579
+
580
+			$this->assign('preferredCreationMode', (int)$prefs->getPreference(PreferenceManager::PREF_CREATION_MODE));
581
+			$this->assign('emailSignature', $prefs->getPreference(PreferenceManager::PREF_EMAIL_SIGNATURE));
582
+
583
+			$this->assign('canManualCreate',
584
+				$this->barrierTest(PreferenceManager::CREATION_MANUAL, $user, 'RequestCreation'));
585
+			$this->assign('canOauthCreate',
586
+				$this->barrierTest(PreferenceManager::CREATION_OAUTH, $user, 'RequestCreation'));
587
+			$this->assign('canBotCreate',
588
+				$this->barrierTest(PreferenceManager::CREATION_BOT, $user, 'RequestCreation'));
589
+		}
590
+	}
591
+
592
+	#endregion
593
+
594
+	private function validateUnusedEmail(string $email, int $userId) : bool {
595
+		$query = 'SELECT COUNT(id) FROM user WHERE email = :email AND id <> :uid';
596
+		$statement = $this->getDatabase()->prepare($query);
597
+		$statement->execute(array(':email' => $email, ':uid' => $userId));
598
+		$inUse = $statement->fetchColumn() > 0;
599
+		$statement->closeCursor();
600
+
601
+		return $inUse;
602
+	}
603
+
604
+	/**
605
+	 * Sends a status change email to the user.
606
+	 *
607
+	 * @param string      $subject           The subject of the email
608
+	 * @param string      $template          The smarty template to use
609
+	 * @param string|null $reason            The reason for performing the status change
610
+	 * @param User        $user              The user affected
611
+	 * @param string      $toolAdminUsername The tool admin's username who is making the edit
612
+	 */
613
+	private function sendStatusChangeEmail($subject, $template, $reason, $user, $toolAdminUsername)
614
+	{
615
+		$this->assign('targetUsername', $user->getUsername());
616
+		$this->assign('toolAdmin', $toolAdminUsername);
617
+		$this->assign('actionReason', $reason);
618
+		$this->assign('mailingList', $this->adminMailingList);
619
+
620
+		// FIXME: domains!
621
+		/** @var Domain $domain */
622
+		$domain = Domain::getById(1, $this->getDatabase());
623
+		$this->getEmailHelper()->sendMail(
624
+			$this->adminMailingList,
625
+			$user->getEmail(),
626
+			$subject,
627
+			$this->fetchTemplate($template)
628
+		);
629
+	}
630
+
631
+	/**
632
+	 * @param UserRole[] $activeRoles
633
+	 *
634
+	 * @return array
635
+	 */
636
+	private function getRoleData($activeRoles)
637
+	{
638
+		$availableRoles = $this->getSecurityManager()->getRoleConfiguration()->getAvailableRoles();
639
+
640
+		$currentUser = User::getCurrent($this->getDatabase());
641
+		$this->getSecurityManager()->getActiveRoles($currentUser, $userRoles, $inactiveRoles);
642
+
643
+		$initialValue = array('active' => 0, 'allowEdit' => 0, 'description' => '???', 'object' => null);
644
+
645
+		$roleData = array();
646
+		foreach ($availableRoles as $role => $data) {
647
+			$intersection = array_intersect($data['editableBy'], $userRoles);
648
+
649
+			$roleData[$role] = $initialValue;
650
+			$roleData[$role]['allowEdit'] = count($intersection) > 0 ? 1 : 0;
651
+			$roleData[$role]['description'] = $data['description'];
652
+			$roleData[$role]['globalOnly'] = $data['globalOnly'];
653
+		}
654
+
655
+		foreach ($activeRoles as $role) {
656
+			if (!isset($roleData[$role->getRole()])) {
657
+				// This value is no longer available in the configuration, allow changing (aka removing) it.
658
+				$roleData[$role->getRole()] = $initialValue;
659
+				$roleData[$role->getRole()]['allowEdit'] = 1;
660
+			}
661
+
662
+			$roleData[$role->getRole()]['object'] = $role;
663
+			$roleData[$role->getRole()]['active'] = 1;
664
+		}
665
+
666
+		return $roleData;
667
+	}
668 668
 }
Please login to merge, or discard this patch.
includes/Tasks/PageBase.php 1 patch
Indentation   +374 added lines, -374 removed lines patch added patch discarded remove patch
@@ -24,378 +24,378 @@
 block discarded – undo
24 24
 
25 25
 abstract class PageBase extends TaskBase implements IRoutedTask
26 26
 {
27
-    use TemplateOutput;
28
-    /** @var string Smarty template to display */
29
-    protected $template = "base.tpl";
30
-    /** @var string HTML title. Currently unused. */
31
-    protected $htmlTitle;
32
-    /** @var bool Determines if the page is a redirect or not */
33
-    protected $isRedirecting = false;
34
-    /** @var array Queue of headers to be sent on successful completion */
35
-    protected $headerQueue = array();
36
-    /** @var string The name of the route to use, as determined by the request router. */
37
-    private $routeName = null;
38
-    /** @var TokenManager */
39
-    protected $tokenManager;
40
-    /** @var ContentSecurityPolicyManager */
41
-    private $cspManager;
42
-    /** @var string[] Extra JS files to include */
43
-    private $extraJs = array();
44
-    /** @var bool Don't show (and hence clear) session alerts when this page is displayed  */
45
-    private $hideAlerts = false;
46
-
47
-    /**
48
-     * Sets the route the request will take. Only should be called from the request router or barrier test.
49
-     *
50
-     * @param string $routeName        The name of the route
51
-     * @param bool   $skipCallableTest Don't use this unless you know what you're doing, and what the implications are.
52
-     *
53
-     * @throws Exception
54
-     * @category Security-Critical
55
-     */
56
-    final public function setRoute($routeName, $skipCallableTest = false)
57
-    {
58
-        // Test the new route is callable before adopting it.
59
-        if (!$skipCallableTest && !is_callable(array($this, $routeName))) {
60
-            throw new Exception("Proposed route '$routeName' is not callable.");
61
-        }
62
-
63
-        // Adopt the new route
64
-        $this->routeName = $routeName;
65
-    }
66
-
67
-    /**
68
-     * Gets the name of the route that has been passed from the request router.
69
-     * @return string
70
-     */
71
-    final public function getRouteName()
72
-    {
73
-        return $this->routeName;
74
-    }
75
-
76
-    /**
77
-     * Performs generic page setup actions
78
-     */
79
-    final protected function setupPage()
80
-    {
81
-        $this->setUpSmarty();
82
-
83
-        $database = $this->getDatabase();
84
-        $currentUser = User::getCurrent($database);
85
-        $this->assign('currentUser', $currentUser);
86
-        $this->assign('skin', PreferenceManager::getForCurrent($database)->getPreference(PreferenceManager::PREF_SKIN));
87
-        $this->assign('currentDomain', Domain::getCurrent($database));
88
-        $this->assign('loggedIn', (!$currentUser->isCommunityUser()));
89
-    }
90
-
91
-    /**
92
-     * Runs the page logic as routed by the RequestRouter
93
-     *
94
-     * Only should be called after a security barrier! That means only from execute().
95
-     */
96
-    final protected function runPage()
97
-    {
98
-        $database = $this->getDatabase();
99
-
100
-        // initialise a database transaction
101
-        if (!$database->beginTransaction()) {
102
-            throw new Exception('Failed to start transaction on primary database.');
103
-        }
104
-
105
-        try {
106
-            // run the page code
107
-            $this->{$this->getRouteName()}();
108
-
109
-            $database->commit();
110
-        }
111
-        /** @noinspection PhpRedundantCatchClauseInspection */
112
-        catch (ApplicationLogicException $ex) {
113
-            // it's an application logic exception, so nothing went seriously wrong with the site. We can use the
114
-            // standard templating system for this.
115
-
116
-            // Firstly, let's undo anything that happened to the database.
117
-            $database->rollBack();
118
-
119
-            // Reset smarty
120
-            $this->setupPage();
121
-
122
-            $this->skipAlerts();
123
-
124
-            // Set the template
125
-            $this->setTemplate('exception/application-logic.tpl');
126
-            $this->assign('message', $ex->getMessage());
127
-
128
-            // Force this back to false
129
-            $this->isRedirecting = false;
130
-            $this->headerQueue = array();
131
-        }
132
-        /** @noinspection PhpRedundantCatchClauseInspection */
133
-        catch (OptimisticLockFailedException $ex) {
134
-            // it's an optimistic lock failure exception, so nothing went seriously wrong with the site. We can use the
135
-            // standard templating system for this.
136
-
137
-            // Firstly, let's undo anything that happened to the database.
138
-            $database->rollBack();
139
-
140
-            // Reset smarty
141
-            $this->setupPage();
142
-
143
-            // Set the template
144
-            $this->skipAlerts();
145
-            $this->setTemplate('exception/optimistic-lock-failure.tpl');
146
-            $this->assign('message', $ex->getMessage());
147
-
148
-            $this->assign('debugTrace', false);
149
-
150
-            if ($this->getSiteConfiguration()->getDebuggingTraceEnabled()) {
151
-                ob_start();
152
-                var_dump(ExceptionHandler::getExceptionData($ex));
153
-                $textErrorData = ob_get_contents();
154
-                ob_end_clean();
155
-
156
-                $this->assign('exceptionData', $textErrorData);
157
-                $this->assign('debugTrace', true);
158
-            }
159
-
160
-            // Force this back to false
161
-            $this->isRedirecting = false;
162
-            $this->headerQueue = array();
163
-        }
164
-        finally {
165
-            // Catch any hanging on transactions
166
-            if ($database->hasActiveTransaction()) {
167
-                $database->rollBack();
168
-            }
169
-        }
170
-
171
-        // run any finalisation code needed before we send the output to the browser.
172
-        $this->finalisePage();
173
-
174
-        // Send the headers
175
-        $this->sendResponseHeaders();
176
-
177
-        // Check we have a template to use!
178
-        if ($this->template !== null) {
179
-            $content = $this->fetchTemplate($this->template);
180
-            ob_clean();
181
-            print($content);
182
-            ob_flush();
183
-
184
-            return;
185
-        }
186
-    }
187
-
188
-    /**
189
-     * Performs final tasks needed before rendering the page.
190
-     */
191
-    protected function finalisePage()
192
-    {
193
-        if ($this->isRedirecting) {
194
-            $this->template = null;
195
-
196
-            return;
197
-        }
198
-
199
-        $this->assign('extraJs', $this->extraJs);
200
-
201
-        if (!$this->hideAlerts) {
202
-            // If we're actually displaying content, we want to add the session alerts here!
203
-            $this->assign('alerts', SessionAlert::getAlerts());
204
-            SessionAlert::clearAlerts();
205
-        }
206
-
207
-        $this->assign('htmlTitle', $this->htmlTitle);
208
-    }
209
-
210
-    /**
211
-     * @return TokenManager
212
-     */
213
-    public function getTokenManager()
214
-    {
215
-        return $this->tokenManager;
216
-    }
217
-
218
-    /**
219
-     * @param TokenManager $tokenManager
220
-     */
221
-    public function setTokenManager($tokenManager)
222
-    {
223
-        $this->tokenManager = $tokenManager;
224
-    }
225
-
226
-    /**
227
-     * @return ContentSecurityPolicyManager
228
-     */
229
-    public function getCspManager(): ContentSecurityPolicyManager
230
-    {
231
-        return $this->cspManager;
232
-    }
233
-
234
-    /**
235
-     * @param ContentSecurityPolicyManager $cspManager
236
-     */
237
-    public function setCspManager(ContentSecurityPolicyManager $cspManager): void
238
-    {
239
-        $this->cspManager = $cspManager;
240
-    }
241
-
242
-    /**
243
-     * Skip the display of session alerts in this page
244
-     */
245
-    public function skipAlerts(): void
246
-    {
247
-        $this->hideAlerts = true;
248
-    }
249
-
250
-    /**
251
-     * Sends the redirect headers to perform a GET at the destination page.
252
-     *
253
-     * Also nullifies the set template so Smarty does not render it.
254
-     *
255
-     * @param string      $page   The page to redirect requests to (as used in the UR)
256
-     * @param null|string $action The action to use on the page.
257
-     * @param null|array  $parameters
258
-     * @param null|string $script The script (relative to index.php) to redirect to
259
-     */
260
-    final protected function redirect($page = '', $action = null, $parameters = null, $script = null)
261
-    {
262
-        $currentScriptName = WebRequest::scriptName();
263
-
264
-        // Are we changing script?
265
-        if ($script === null || substr($currentScriptName, -1 * count($script)) === $script) {
266
-            $targetScriptName = $currentScriptName;
267
-        }
268
-        else {
269
-            $targetScriptName = $this->getSiteConfiguration()->getBaseUrl() . '/' . $script;
270
-        }
271
-
272
-        $pathInfo = array($targetScriptName);
273
-
274
-        $pathInfo[1] = $page;
275
-
276
-        if ($action !== null) {
277
-            $pathInfo[2] = $action;
278
-        }
279
-
280
-        $url = implode('/', $pathInfo);
281
-
282
-        if (is_array($parameters) && count($parameters) > 0) {
283
-            $url .= '?' . http_build_query($parameters);
284
-        }
285
-
286
-        $this->redirectUrl($url);
287
-    }
288
-
289
-    /**
290
-     * Sends the redirect headers to perform a GET at the new address.
291
-     *
292
-     * Also nullifies the set template so Smarty does not render it.
293
-     *
294
-     * @param string $path URL to redirect to
295
-     */
296
-    final protected function redirectUrl($path)
297
-    {
298
-        // 303 See Other = re-request at new address with a GET.
299
-        $this->headerQueue[] = 'HTTP/1.1 303 See Other';
300
-        $this->headerQueue[] = "Location: $path";
301
-
302
-        $this->setTemplate(null);
303
-        $this->isRedirecting = true;
304
-    }
305
-
306
-    /**
307
-     * Sets the name of the template this page should display.
308
-     *
309
-     * @param string $name
310
-     *
311
-     * @throws Exception
312
-     */
313
-    final protected function setTemplate($name)
314
-    {
315
-        if ($this->isRedirecting) {
316
-            throw new Exception('This page has been set as a redirect, no template can be displayed!');
317
-        }
318
-
319
-        $this->template = $name;
320
-    }
321
-
322
-    /**
323
-     * Adds an extra JS file to to the page
324
-     *
325
-     * @param string $path The path (relative to the application root) of the file
326
-     */
327
-    final protected function addJs($path)
328
-    {
329
-        if (in_array($path, $this->extraJs)) {
330
-            // nothing to do
331
-            return;
332
-        }
333
-
334
-        $this->extraJs[] = $path;
335
-    }
336
-
337
-    /**
338
-     * Main function for this page, when no specific actions are called.
339
-     * @return void
340
-     */
341
-    abstract protected function main();
342
-
343
-    /**
344
-     * Takes a smarty template string and sets the HTML title to that value
345
-     *
346
-     * @param string $title
347
-     *
348
-     * @throws Smarty\Exception
349
-     */
350
-    final protected function setHtmlTitle($title)
351
-    {
352
-        $this->htmlTitle = $this->smarty->fetch('string:' . $title);
353
-    }
354
-
355
-    public function execute()
356
-    {
357
-        if ($this->getRouteName() === null) {
358
-            throw new Exception('Request is unrouted.');
359
-        }
360
-
361
-        if ($this->getSiteConfiguration() === null) {
362
-            throw new Exception('Page has no configuration!');
363
-        }
364
-
365
-        $this->setupPage();
366
-
367
-        $this->runPage();
368
-    }
369
-
370
-    public function assignCSRFToken()
371
-    {
372
-        $token = $this->tokenManager->getNewToken();
373
-        $this->assign('csrfTokenData', $token->getTokenData());
374
-    }
375
-
376
-    public function validateCSRFToken()
377
-    {
378
-        if (!$this->tokenManager->validateToken(WebRequest::postString('csrfTokenData'))) {
379
-            throw new ApplicationLogicException('Form token is not valid, please reload and try again');
380
-        }
381
-    }
382
-
383
-    protected function sendResponseHeaders()
384
-    {
385
-        if (headers_sent()) {
386
-            throw new ApplicationLogicException('Headers have already been sent! This is likely a bug in the application.');
387
-        }
388
-
389
-        // send the CSP headers now
390
-        header($this->getCspManager()->getHeader());
391
-
392
-        foreach ($this->headerQueue as $item) {
393
-            if (mb_strpos($item, "\r") !== false || mb_strpos($item, "\n") !== false) {
394
-                // Oops. We're not allowed to do this.
395
-                throw new Exception('Unable to split header');
396
-            }
397
-
398
-            header($item);
399
-        }
400
-    }
27
+	use TemplateOutput;
28
+	/** @var string Smarty template to display */
29
+	protected $template = "base.tpl";
30
+	/** @var string HTML title. Currently unused. */
31
+	protected $htmlTitle;
32
+	/** @var bool Determines if the page is a redirect or not */
33
+	protected $isRedirecting = false;
34
+	/** @var array Queue of headers to be sent on successful completion */
35
+	protected $headerQueue = array();
36
+	/** @var string The name of the route to use, as determined by the request router. */
37
+	private $routeName = null;
38
+	/** @var TokenManager */
39
+	protected $tokenManager;
40
+	/** @var ContentSecurityPolicyManager */
41
+	private $cspManager;
42
+	/** @var string[] Extra JS files to include */
43
+	private $extraJs = array();
44
+	/** @var bool Don't show (and hence clear) session alerts when this page is displayed  */
45
+	private $hideAlerts = false;
46
+
47
+	/**
48
+	 * Sets the route the request will take. Only should be called from the request router or barrier test.
49
+	 *
50
+	 * @param string $routeName        The name of the route
51
+	 * @param bool   $skipCallableTest Don't use this unless you know what you're doing, and what the implications are.
52
+	 *
53
+	 * @throws Exception
54
+	 * @category Security-Critical
55
+	 */
56
+	final public function setRoute($routeName, $skipCallableTest = false)
57
+	{
58
+		// Test the new route is callable before adopting it.
59
+		if (!$skipCallableTest && !is_callable(array($this, $routeName))) {
60
+			throw new Exception("Proposed route '$routeName' is not callable.");
61
+		}
62
+
63
+		// Adopt the new route
64
+		$this->routeName = $routeName;
65
+	}
66
+
67
+	/**
68
+	 * Gets the name of the route that has been passed from the request router.
69
+	 * @return string
70
+	 */
71
+	final public function getRouteName()
72
+	{
73
+		return $this->routeName;
74
+	}
75
+
76
+	/**
77
+	 * Performs generic page setup actions
78
+	 */
79
+	final protected function setupPage()
80
+	{
81
+		$this->setUpSmarty();
82
+
83
+		$database = $this->getDatabase();
84
+		$currentUser = User::getCurrent($database);
85
+		$this->assign('currentUser', $currentUser);
86
+		$this->assign('skin', PreferenceManager::getForCurrent($database)->getPreference(PreferenceManager::PREF_SKIN));
87
+		$this->assign('currentDomain', Domain::getCurrent($database));
88
+		$this->assign('loggedIn', (!$currentUser->isCommunityUser()));
89
+	}
90
+
91
+	/**
92
+	 * Runs the page logic as routed by the RequestRouter
93
+	 *
94
+	 * Only should be called after a security barrier! That means only from execute().
95
+	 */
96
+	final protected function runPage()
97
+	{
98
+		$database = $this->getDatabase();
99
+
100
+		// initialise a database transaction
101
+		if (!$database->beginTransaction()) {
102
+			throw new Exception('Failed to start transaction on primary database.');
103
+		}
104
+
105
+		try {
106
+			// run the page code
107
+			$this->{$this->getRouteName()}();
108
+
109
+			$database->commit();
110
+		}
111
+		/** @noinspection PhpRedundantCatchClauseInspection */
112
+		catch (ApplicationLogicException $ex) {
113
+			// it's an application logic exception, so nothing went seriously wrong with the site. We can use the
114
+			// standard templating system for this.
115
+
116
+			// Firstly, let's undo anything that happened to the database.
117
+			$database->rollBack();
118
+
119
+			// Reset smarty
120
+			$this->setupPage();
121
+
122
+			$this->skipAlerts();
123
+
124
+			// Set the template
125
+			$this->setTemplate('exception/application-logic.tpl');
126
+			$this->assign('message', $ex->getMessage());
127
+
128
+			// Force this back to false
129
+			$this->isRedirecting = false;
130
+			$this->headerQueue = array();
131
+		}
132
+		/** @noinspection PhpRedundantCatchClauseInspection */
133
+		catch (OptimisticLockFailedException $ex) {
134
+			// it's an optimistic lock failure exception, so nothing went seriously wrong with the site. We can use the
135
+			// standard templating system for this.
136
+
137
+			// Firstly, let's undo anything that happened to the database.
138
+			$database->rollBack();
139
+
140
+			// Reset smarty
141
+			$this->setupPage();
142
+
143
+			// Set the template
144
+			$this->skipAlerts();
145
+			$this->setTemplate('exception/optimistic-lock-failure.tpl');
146
+			$this->assign('message', $ex->getMessage());
147
+
148
+			$this->assign('debugTrace', false);
149
+
150
+			if ($this->getSiteConfiguration()->getDebuggingTraceEnabled()) {
151
+				ob_start();
152
+				var_dump(ExceptionHandler::getExceptionData($ex));
153
+				$textErrorData = ob_get_contents();
154
+				ob_end_clean();
155
+
156
+				$this->assign('exceptionData', $textErrorData);
157
+				$this->assign('debugTrace', true);
158
+			}
159
+
160
+			// Force this back to false
161
+			$this->isRedirecting = false;
162
+			$this->headerQueue = array();
163
+		}
164
+		finally {
165
+			// Catch any hanging on transactions
166
+			if ($database->hasActiveTransaction()) {
167
+				$database->rollBack();
168
+			}
169
+		}
170
+
171
+		// run any finalisation code needed before we send the output to the browser.
172
+		$this->finalisePage();
173
+
174
+		// Send the headers
175
+		$this->sendResponseHeaders();
176
+
177
+		// Check we have a template to use!
178
+		if ($this->template !== null) {
179
+			$content = $this->fetchTemplate($this->template);
180
+			ob_clean();
181
+			print($content);
182
+			ob_flush();
183
+
184
+			return;
185
+		}
186
+	}
187
+
188
+	/**
189
+	 * Performs final tasks needed before rendering the page.
190
+	 */
191
+	protected function finalisePage()
192
+	{
193
+		if ($this->isRedirecting) {
194
+			$this->template = null;
195
+
196
+			return;
197
+		}
198
+
199
+		$this->assign('extraJs', $this->extraJs);
200
+
201
+		if (!$this->hideAlerts) {
202
+			// If we're actually displaying content, we want to add the session alerts here!
203
+			$this->assign('alerts', SessionAlert::getAlerts());
204
+			SessionAlert::clearAlerts();
205
+		}
206
+
207
+		$this->assign('htmlTitle', $this->htmlTitle);
208
+	}
209
+
210
+	/**
211
+	 * @return TokenManager
212
+	 */
213
+	public function getTokenManager()
214
+	{
215
+		return $this->tokenManager;
216
+	}
217
+
218
+	/**
219
+	 * @param TokenManager $tokenManager
220
+	 */
221
+	public function setTokenManager($tokenManager)
222
+	{
223
+		$this->tokenManager = $tokenManager;
224
+	}
225
+
226
+	/**
227
+	 * @return ContentSecurityPolicyManager
228
+	 */
229
+	public function getCspManager(): ContentSecurityPolicyManager
230
+	{
231
+		return $this->cspManager;
232
+	}
233
+
234
+	/**
235
+	 * @param ContentSecurityPolicyManager $cspManager
236
+	 */
237
+	public function setCspManager(ContentSecurityPolicyManager $cspManager): void
238
+	{
239
+		$this->cspManager = $cspManager;
240
+	}
241
+
242
+	/**
243
+	 * Skip the display of session alerts in this page
244
+	 */
245
+	public function skipAlerts(): void
246
+	{
247
+		$this->hideAlerts = true;
248
+	}
249
+
250
+	/**
251
+	 * Sends the redirect headers to perform a GET at the destination page.
252
+	 *
253
+	 * Also nullifies the set template so Smarty does not render it.
254
+	 *
255
+	 * @param string      $page   The page to redirect requests to (as used in the UR)
256
+	 * @param null|string $action The action to use on the page.
257
+	 * @param null|array  $parameters
258
+	 * @param null|string $script The script (relative to index.php) to redirect to
259
+	 */
260
+	final protected function redirect($page = '', $action = null, $parameters = null, $script = null)
261
+	{
262
+		$currentScriptName = WebRequest::scriptName();
263
+
264
+		// Are we changing script?
265
+		if ($script === null || substr($currentScriptName, -1 * count($script)) === $script) {
266
+			$targetScriptName = $currentScriptName;
267
+		}
268
+		else {
269
+			$targetScriptName = $this->getSiteConfiguration()->getBaseUrl() . '/' . $script;
270
+		}
271
+
272
+		$pathInfo = array($targetScriptName);
273
+
274
+		$pathInfo[1] = $page;
275
+
276
+		if ($action !== null) {
277
+			$pathInfo[2] = $action;
278
+		}
279
+
280
+		$url = implode('/', $pathInfo);
281
+
282
+		if (is_array($parameters) && count($parameters) > 0) {
283
+			$url .= '?' . http_build_query($parameters);
284
+		}
285
+
286
+		$this->redirectUrl($url);
287
+	}
288
+
289
+	/**
290
+	 * Sends the redirect headers to perform a GET at the new address.
291
+	 *
292
+	 * Also nullifies the set template so Smarty does not render it.
293
+	 *
294
+	 * @param string $path URL to redirect to
295
+	 */
296
+	final protected function redirectUrl($path)
297
+	{
298
+		// 303 See Other = re-request at new address with a GET.
299
+		$this->headerQueue[] = 'HTTP/1.1 303 See Other';
300
+		$this->headerQueue[] = "Location: $path";
301
+
302
+		$this->setTemplate(null);
303
+		$this->isRedirecting = true;
304
+	}
305
+
306
+	/**
307
+	 * Sets the name of the template this page should display.
308
+	 *
309
+	 * @param string $name
310
+	 *
311
+	 * @throws Exception
312
+	 */
313
+	final protected function setTemplate($name)
314
+	{
315
+		if ($this->isRedirecting) {
316
+			throw new Exception('This page has been set as a redirect, no template can be displayed!');
317
+		}
318
+
319
+		$this->template = $name;
320
+	}
321
+
322
+	/**
323
+	 * Adds an extra JS file to to the page
324
+	 *
325
+	 * @param string $path The path (relative to the application root) of the file
326
+	 */
327
+	final protected function addJs($path)
328
+	{
329
+		if (in_array($path, $this->extraJs)) {
330
+			// nothing to do
331
+			return;
332
+		}
333
+
334
+		$this->extraJs[] = $path;
335
+	}
336
+
337
+	/**
338
+	 * Main function for this page, when no specific actions are called.
339
+	 * @return void
340
+	 */
341
+	abstract protected function main();
342
+
343
+	/**
344
+	 * Takes a smarty template string and sets the HTML title to that value
345
+	 *
346
+	 * @param string $title
347
+	 *
348
+	 * @throws Smarty\Exception
349
+	 */
350
+	final protected function setHtmlTitle($title)
351
+	{
352
+		$this->htmlTitle = $this->smarty->fetch('string:' . $title);
353
+	}
354
+
355
+	public function execute()
356
+	{
357
+		if ($this->getRouteName() === null) {
358
+			throw new Exception('Request is unrouted.');
359
+		}
360
+
361
+		if ($this->getSiteConfiguration() === null) {
362
+			throw new Exception('Page has no configuration!');
363
+		}
364
+
365
+		$this->setupPage();
366
+
367
+		$this->runPage();
368
+	}
369
+
370
+	public function assignCSRFToken()
371
+	{
372
+		$token = $this->tokenManager->getNewToken();
373
+		$this->assign('csrfTokenData', $token->getTokenData());
374
+	}
375
+
376
+	public function validateCSRFToken()
377
+	{
378
+		if (!$this->tokenManager->validateToken(WebRequest::postString('csrfTokenData'))) {
379
+			throw new ApplicationLogicException('Form token is not valid, please reload and try again');
380
+		}
381
+	}
382
+
383
+	protected function sendResponseHeaders()
384
+	{
385
+		if (headers_sent()) {
386
+			throw new ApplicationLogicException('Headers have already been sent! This is likely a bug in the application.');
387
+		}
388
+
389
+		// send the CSP headers now
390
+		header($this->getCspManager()->getHeader());
391
+
392
+		foreach ($this->headerQueue as $item) {
393
+			if (mb_strpos($item, "\r") !== false || mb_strpos($item, "\n") !== false) {
394
+				// Oops. We're not allowed to do this.
395
+				throw new Exception('Unable to split header');
396
+			}
397
+
398
+			header($item);
399
+		}
400
+	}
401 401
 }
Please login to merge, or discard this patch.
includes/Offline.php 1 patch
Indentation   +46 added lines, -46 removed lines patch added patch discarded remove patch
@@ -17,57 +17,57 @@
 block discarded – undo
17 17
  */
18 18
 class Offline
19 19
 {
20
-    /**
21
-     * Determines if the tool is offline
22
-     * @return bool
23
-     */
24
-    public static function isOffline(SiteConfiguration $configuration): bool
25
-    {
26
-        return (bool)$configuration->getOffline()['offline'];
27
-    }
20
+	/**
21
+	 * Determines if the tool is offline
22
+	 * @return bool
23
+	 */
24
+	public static function isOffline(SiteConfiguration $configuration): bool
25
+	{
26
+		return (bool)$configuration->getOffline()['offline'];
27
+	}
28 28
 
29
-    /**
30
-     * Gets the offline message
31
-     *
32
-     * @throws Smarty\Exception
33
-     */
34
-    public static function getOfflineMessage(bool $external, SiteConfiguration $configuration, ?string $message = null): string
35
-    {
36
-        $baseurl = $configuration->getBaseUrl();
37
-        $culprit = $configuration->getOffline()['culprit'];
38
-        $reason = $configuration->getOffline()['reason'];
29
+	/**
30
+	 * Gets the offline message
31
+	 *
32
+	 * @throws Smarty\Exception
33
+	 */
34
+	public static function getOfflineMessage(bool $external, SiteConfiguration $configuration, ?string $message = null): string
35
+	{
36
+		$baseurl = $configuration->getBaseUrl();
37
+		$culprit = $configuration->getOffline()['culprit'];
38
+		$reason = $configuration->getOffline()['reason'];
39 39
 
40
-        $smarty = new Smarty();
41
-        $smarty->assign("baseurl", $baseurl);
42
-        $smarty->assign("resourceCacheEpoch", 0);
43
-        $smarty->assign("alerts", []);
44
-        $smarty->assign("toolversion", Environment::getToolVersion());
40
+		$smarty = new Smarty();
41
+		$smarty->assign("baseurl", $baseurl);
42
+		$smarty->assign("resourceCacheEpoch", 0);
43
+		$smarty->assign("alerts", []);
44
+		$smarty->assign("toolversion", Environment::getToolVersion());
45 45
 
46
-        if (!headers_sent()) {
47
-            header("HTTP/1.1 503 Service Unavailable");
48
-        }
46
+		if (!headers_sent()) {
47
+			header("HTTP/1.1 503 Service Unavailable");
48
+		}
49 49
 
50
-        if ($external) {
51
-            return $smarty->fetch("offline/external.tpl");
52
-        }
53
-        else {
54
-            $hideCulprit = true;
50
+		if ($external) {
51
+			return $smarty->fetch("offline/external.tpl");
52
+		}
53
+		else {
54
+			$hideCulprit = true;
55 55
 
56
-            // Use the provided message if possible
57
-            if ($message === null) {
58
-                $hideCulprit = false;
59
-                $message = $reason;
60
-            }
56
+			// Use the provided message if possible
57
+			if ($message === null) {
58
+				$hideCulprit = false;
59
+				$message = $reason;
60
+			}
61 61
 
62
-            $smarty->assign("hideCulprit", $hideCulprit);
63
-            $smarty->assign("dontUseDbCulprit", $culprit);
64
-            $smarty->assign("dontUseDbReason", $message);
65
-            $smarty->assign("alerts", []);
66
-            $smarty->assign('currentUser', User::getCommunity());
67
-            $smarty->assign('skin', 'main');
68
-            $smarty->assign('currentDomain', null);
62
+			$smarty->assign("hideCulprit", $hideCulprit);
63
+			$smarty->assign("dontUseDbCulprit", $culprit);
64
+			$smarty->assign("dontUseDbReason", $message);
65
+			$smarty->assign("alerts", []);
66
+			$smarty->assign('currentUser', User::getCommunity());
67
+			$smarty->assign('skin', 'main');
68
+			$smarty->assign('currentDomain', null);
69 69
 
70
-            return $smarty->fetch("offline/internal.tpl");
71
-        }
72
-    }
70
+			return $smarty->fetch("offline/internal.tpl");
71
+		}
72
+	}
73 73
 }
Please login to merge, or discard this patch.
smarty-plugins/function.defaultsort.php 1 patch
Indentation   +15 added lines, -15 removed lines patch added patch discarded remove patch
@@ -17,24 +17,24 @@
 block discarded – undo
17 17
  */
18 18
 function smarty_function_defaultsort($params, Smarty\Template $template)
19 19
 {
20
-    if (empty($params['id'])) {
21
-        return "";
22
-    }
20
+	if (empty($params['id'])) {
21
+		return "";
22
+	}
23 23
 
24
-    $attr = 'data-sortname="' . htmlspecialchars($params['id'], ENT_QUOTES) . '"';
24
+	$attr = 'data-sortname="' . htmlspecialchars($params['id'], ENT_QUOTES) . '"';
25 25
 
26
-    if (empty($params['req'])) {
27
-        return $attr;
28
-    }
26
+	if (empty($params['req'])) {
27
+		return $attr;
28
+	}
29 29
 
30
-    if ($params['dir'] !== 'asc' && $params['dir'] !== 'desc') {
31
-        $params['dir'] = 'asc';
32
-    }
30
+	if ($params['dir'] !== 'asc' && $params['dir'] !== 'desc') {
31
+		$params['dir'] = 'asc';
32
+	}
33 33
 
34
-    $sort = '';
35
-    if ($params['req'] === $params['id']) {
36
-        $sort = ' data-defaultsort="' . htmlspecialchars($params['dir'], ENT_QUOTES) . '"';
37
-    }
34
+	$sort = '';
35
+	if ($params['req'] === $params['id']) {
36
+		$sort = ' data-defaultsort="' . htmlspecialchars($params['dir'], ENT_QUOTES) . '"';
37
+	}
38 38
 
39
-    return $attr . $sort;
39
+	return $attr . $sort;
40 40
 }
41 41
\ No newline at end of file
Please login to merge, or discard this patch.