PageUserManagement   C
last analyzed

Complexity

Total Complexity 56

Size/Duplication

Total Lines 588
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 56
eloc 309
c 2
b 0
f 0
dl 0
loc 588
rs 5.5199

10 Methods

Rating   Name   Duplication   Size   Complexity  
B suspend() 0 58 7
B rename() 0 70 6
A validateUnusedEmail() 0 8 1
C editRoles() 0 89 14
B decline() 0 53 6
A getRoleData() 0 30 5
A main() 0 67 4
A sendStatusChangeEmail() 0 15 1
A approve() 0 47 4
B editUser() 0 66 8

How to fix   Complexity   

Complex Class

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

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

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

1
<?php
2
/******************************************************************************
3
 * Wikipedia Account Creation Assistance tool                                 *
4
 *                                                                            *
5
 * All code in this file is released into the public domain by the ACC        *
6
 * Development Team. Please see team.json for a list of contributors.         *
7
 ******************************************************************************/
8
9
namespace Waca\Pages;
10
11
use Waca\DataObjects\Domain;
12
use Waca\DataObjects\User;
13
use Waca\DataObjects\UserRole;
14
use Waca\Exceptions\ApplicationLogicException;
15
use Waca\Helpers\Logger;
16
use Waca\Helpers\OAuthUserHelper;
17
use Waca\Helpers\SearchHelpers\UserSearchHelper;
18
use Waca\SessionAlert;
19
use Waca\Tasks\InternalPageBase;
20
use Waca\WebRequest;
21
22
/**
23
 * Class PageUserManagement
24
 * @package Waca\Pages
25
 */
26
class PageUserManagement extends InternalPageBase
27
{
28
    // FIXME: domains
29
    /** @var string */
30
    private $adminMailingList = '[email protected]';
31
32
    /**
33
     * Main function for this page, when no specific actions are called.
34
     */
35
    protected function main()
36
    {
37
        $this->setHtmlTitle('User Management');
38
39
        $database = $this->getDatabase();
40
        $currentUser = User::getCurrent($database);
41
42
        $userSearchRequest = WebRequest::getString('usersearch');
43
        if ($userSearchRequest !== null) {
44
            $searchedUser = User::getByUsername($userSearchRequest, $database);
45
            if ($searchedUser !== false) {
46
                $this->redirect('statistics/users', 'detail', ['user' => $searchedUser->getId()]);
47
                return;
48
            }
49
        }
50
51
        // A bit hacky, but it's better than my last solution of creating an object for each user and passing that to
52
        // the template. I still don't have a particularly good way of handling this.
53
        OAuthUserHelper::prepareTokenCountStatement($database);
54
55
        if (WebRequest::getBoolean("showAll")) {
56
            $this->assign("showAll", true);
57
58
            $suspendedUsers = UserSearchHelper::get($database)->byStatus(User::STATUS_SUSPENDED)->fetch();
59
            $this->assign("suspendedUsers", $suspendedUsers);
60
61
            $declinedUsers = UserSearchHelper::get($database)->byStatus(User::STATUS_DECLINED)->fetch();
62
            $this->assign("declinedUsers", $declinedUsers);
63
64
            UserSearchHelper::get($database)->getRoleMap($roleMap);
65
        }
66
        else {
67
            $this->assign("showAll", false);
68
            $this->assign("suspendedUsers", array());
69
            $this->assign("declinedUsers", array());
70
71
            UserSearchHelper::get($database)->statusIn(array('New', 'Active'))->getRoleMap($roleMap);
72
        }
73
74
        $newUsers = UserSearchHelper::get($database)->byStatus(User::STATUS_NEW)->fetch();
75
        $normalUsers = UserSearchHelper::get($database)->byStatus(User::STATUS_ACTIVE)->byRole('user')->fetch();
76
        $adminUsers = UserSearchHelper::get($database)->byStatus(User::STATUS_ACTIVE)->byRole('admin')->fetch();
77
        $checkUsers = UserSearchHelper::get($database)->byStatus(User::STATUS_ACTIVE)->byRole('checkuser')->fetch();
78
        $toolRoots = UserSearchHelper::get($database)->byStatus(User::STATUS_ACTIVE)->byRole('toolRoot')->fetch();
79
        $this->assign('newUsers', $newUsers);
80
        $this->assign('normalUsers', $normalUsers);
81
        $this->assign('adminUsers', $adminUsers);
82
        $this->assign('checkUsers', $checkUsers);
83
        $this->assign('toolRoots', $toolRoots);
84
85
        $this->assign('roles', $roleMap);
86
87
        $this->addJs("/api.php?action=users&all=true&targetVariable=typeaheaddata");
88
89
        $this->assign('canApprove', $this->barrierTest('approve', $currentUser));
90
        $this->assign('canDecline', $this->barrierTest('decline', $currentUser));
91
        $this->assign('canRename', $this->barrierTest('rename', $currentUser));
92
        $this->assign('canEditUser', $this->barrierTest('editUser', $currentUser));
93
        $this->assign('canSuspend', $this->barrierTest('suspend', $currentUser));
94
        $this->assign('canEditRoles', $this->barrierTest('editRoles', $currentUser));
95
96
        // FIXME: domains!
97
        /** @var Domain $domain */
98
        $domain = Domain::getById(1, $this->getDatabase());
99
        $this->assign('mediawikiScriptPath', $domain->getWikiArticlePath());
100
101
        $this->setTemplate("usermanagement/main.tpl");
102
    }
103
104
    #region Access control
105
106
    /**
107
     * Action target for editing the roles assigned to a user
108
     */
109
    protected function editRoles()
110
    {
111
        $this->setHtmlTitle('User Management');
112
        $database = $this->getDatabase();
113
        $userId = WebRequest::getInt('user');
114
115
        /** @var User $user */
116
        $user = User::getById($userId, $database);
117
118
        if ($user === false) {
0 ignored issues
show
introduced by
The condition $user === false is always false.
Loading history...
119
            throw new ApplicationLogicException('Sorry, the user you are trying to edit could not be found.');
120
        }
121
122
        $roleData = $this->getRoleData(UserRole::getForUser($user->getId(), $database));
123
124
        // Dual-mode action
125
        if (WebRequest::wasPosted()) {
126
            $this->validateCSRFToken();
127
128
            $reason = WebRequest::postString('reason');
129
            if ($reason === false || trim($reason) === '') {
130
                throw new ApplicationLogicException('No reason specified for roles change');
131
            }
132
133
            /** @var UserRole[] $delete */
134
            $delete = array();
135
            /** @var string[] $delete */
136
            $add = array();
137
138
            foreach ($roleData as $name => $r) {
139
                if ($r['allowEdit'] !== 1) {
140
                    // not allowed, to touch this, so ignore it
141
                    continue;
142
                }
143
144
                $newValue = WebRequest::postBoolean('role-' . $name) ? 1 : 0;
145
                if ($newValue !== $r['active']) {
146
                    if ($newValue === 0) {
147
                        $delete[] = $r['object'];
148
                    }
149
150
                    if ($newValue === 1) {
151
                        $add[] = $name;
152
                    }
153
                }
154
            }
155
156
            // Check there's something to do
157
            if ((count($add) + count($delete)) === 0) {
158
                $this->redirect('statistics/users', 'detail', array('user' => $user->getId()));
159
                SessionAlert::warning('No changes made to roles.');
160
161
                return;
162
            }
163
164
            $removed = array();
165
166
            /** @var UserRole $d */
167
            foreach ($delete as $d) {
168
                $removed[] = $d->getRole();
169
                $d->delete();
170
            }
171
172
            foreach ($add as $x) {
173
                $a = new UserRole();
174
                $a->setUser($user->getId());
175
                $a->setRole($x);
176
                $a->setDatabase($database);
177
                $a->save();
178
            }
179
180
            Logger::userRolesEdited($database, $user, $reason, $add, $removed);
181
182
            // dummy save for optimistic locking. If this fails, the entire txn will roll back.
183
            $user->setUpdateVersion(WebRequest::postInt('updateversion'));
184
            $user->save();
185
186
            $this->getNotificationHelper()->userRolesEdited($user, $reason);
187
            SessionAlert::quick('Roles changed for user ' . htmlentities($user->getUsername(), ENT_COMPAT, 'UTF-8'));
188
189
            $this->redirect('statistics/users', 'detail', array('user' => $user->getId()));
190
191
            return;
192
        }
193
        else {
194
            $this->assignCSRFToken();
195
            $this->setTemplate('usermanagement/roleedit.tpl');
196
            $this->assign('user', $user);
197
            $this->assign('roleData', $roleData);
198
        }
199
    }
200
201
    /**
202
     * Action target for suspending users
203
     *
204
     * @throws ApplicationLogicException
205
     */
206
    protected function suspend()
207
    {
208
        $this->setHtmlTitle('User Management');
209
210
        $database = $this->getDatabase();
211
212
        $userId = WebRequest::getInt('user');
213
214
        /** @var User $user */
215
        $user = User::getById($userId, $database);
216
217
        if ($user === false) {
0 ignored issues
show
introduced by
The condition $user === false is always false.
Loading history...
218
            throw new ApplicationLogicException('Sorry, the user you are trying to suspend could not be found.');
219
        }
220
221
        if ($user->isSuspended()) {
222
            throw new ApplicationLogicException('Sorry, the user you are trying to suspend is already suspended.');
223
        }
224
225
        // Dual-mode action
226
        if (WebRequest::wasPosted()) {
227
            $this->validateCSRFToken();
228
            $reason = WebRequest::postString('reason');
229
230
            if ($reason === null || trim($reason) === "") {
231
                throw new ApplicationLogicException('No reason provided');
232
            }
233
234
            $user->setStatus(User::STATUS_SUSPENDED);
235
            $user->setUpdateVersion(WebRequest::postInt('updateversion'));
236
            $user->save();
237
            Logger::suspendedUser($database, $user, $reason);
238
239
            $this->getNotificationHelper()->userSuspended($user, $reason);
240
            SessionAlert::quick('Suspended user ' . htmlentities($user->getUsername(), ENT_COMPAT, 'UTF-8'));
241
242
            // send email
243
            $this->sendStatusChangeEmail(
244
                'Your WP:ACC account has been suspended',
245
                'usermanagement/emails/suspended.tpl',
246
                $reason,
247
                $user,
248
                User::getCurrent($database)->getUsername()
249
            );
250
251
            $this->redirect('userManagement');
252
253
            return;
254
        }
255
        else {
256
            $this->assignCSRFToken();
257
            $this->setTemplate('usermanagement/changelevel-reason.tpl');
258
            $this->assign('user', $user);
259
            $this->assign('status', 'Suspended');
260
            $this->assign("showReason", true);
261
262
            if (WebRequest::getString('preload')) {
263
                $this->assign('preload', WebRequest::getString('preload'));
264
            }
265
        }
266
    }
267
268
    /**
269
     * Entry point for the decline action
270
     *
271
     * @throws ApplicationLogicException
272
     */
273
    protected function decline()
274
    {
275
        $this->setHtmlTitle('User Management');
276
277
        $database = $this->getDatabase();
278
279
        $userId = WebRequest::getInt('user');
280
        $user = User::getById($userId, $database);
281
282
        if ($user === false) {
0 ignored issues
show
introduced by
The condition $user === false is always false.
Loading history...
283
            throw new ApplicationLogicException('Sorry, the user you are trying to decline could not be found.');
284
        }
285
286
        if (!$user->isNewUser()) {
287
            throw new ApplicationLogicException('Sorry, the user you are trying to decline is not new.');
288
        }
289
290
        // Dual-mode action
291
        if (WebRequest::wasPosted()) {
292
            $this->validateCSRFToken();
293
            $reason = WebRequest::postString('reason');
294
295
            if ($reason === null || trim($reason) === "") {
296
                throw new ApplicationLogicException('No reason provided');
297
            }
298
299
            $user->setStatus(User::STATUS_DECLINED);
300
            $user->setUpdateVersion(WebRequest::postInt('updateversion'));
301
            $user->save();
302
            Logger::declinedUser($database, $user, $reason);
303
304
            $this->getNotificationHelper()->userDeclined($user, $reason);
305
            SessionAlert::quick('Declined user ' . htmlentities($user->getUsername(), ENT_COMPAT, 'UTF-8'));
306
307
            // send email
308
            $this->sendStatusChangeEmail(
309
                'Your WP:ACC account has been declined',
310
                'usermanagement/emails/declined.tpl',
311
                $reason,
312
                $user,
313
                User::getCurrent($database)->getUsername()
314
            );
315
316
            $this->redirect('userManagement');
317
318
            return;
319
        }
320
        else {
321
            $this->assignCSRFToken();
322
            $this->setTemplate('usermanagement/changelevel-reason.tpl');
323
            $this->assign('user', $user);
324
            $this->assign('status', 'Declined');
325
            $this->assign("showReason", true);
326
        }
327
    }
328
329
    /**
330
     * Entry point for the approve action
331
     *
332
     * @throws ApplicationLogicException
333
     */
334
    protected function approve()
335
    {
336
        $this->setHtmlTitle('User Management');
337
338
        $database = $this->getDatabase();
339
340
        $userId = WebRequest::getInt('user');
341
        $user = User::getById($userId, $database);
342
343
        if ($user === false) {
0 ignored issues
show
introduced by
The condition $user === false is always false.
Loading history...
344
            throw new ApplicationLogicException('Sorry, the user you are trying to approve could not be found.');
345
        }
346
347
        if ($user->isActive()) {
348
            throw new ApplicationLogicException('Sorry, the user you are trying to approve is already an active user.');
349
        }
350
351
        // Dual-mode action
352
        if (WebRequest::wasPosted()) {
353
            $this->validateCSRFToken();
354
            $user->setStatus(User::STATUS_ACTIVE);
355
            $user->setUpdateVersion(WebRequest::postInt('updateversion'));
356
            $user->save();
357
            Logger::approvedUser($database, $user);
358
359
            $this->getNotificationHelper()->userApproved($user);
360
            SessionAlert::quick('Approved user ' . htmlentities($user->getUsername(), ENT_COMPAT, 'UTF-8'));
361
362
            // send email
363
            $this->sendStatusChangeEmail(
364
                'Your WP:ACC account has been approved',
365
                'usermanagement/emails/approved.tpl',
366
                null,
367
                $user,
368
                User::getCurrent($database)->getUsername()
369
            );
370
371
            $this->redirect("userManagement");
372
373
            return;
374
        }
375
        else {
376
            $this->assignCSRFToken();
377
            $this->setTemplate("usermanagement/changelevel-reason.tpl");
378
            $this->assign("user", $user);
379
            $this->assign("status", "Active");
380
            $this->assign("showReason", false);
381
        }
382
    }
383
384
    #endregion
385
386
    #region Renaming / Editing
387
388
    /**
389
     * Entry point for the rename action
390
     *
391
     * @throws ApplicationLogicException
392
     */
393
    protected function rename()
394
    {
395
        $this->setHtmlTitle('User Management');
396
397
        $database = $this->getDatabase();
398
399
        $userId = WebRequest::getInt('user');
400
        $user = User::getById($userId, $database);
401
402
        if ($user === false) {
0 ignored issues
show
introduced by
The condition $user === false is always false.
Loading history...
403
            throw new ApplicationLogicException('Sorry, the user you are trying to rename could not be found.');
404
        }
405
406
        // Dual-mode action
407
        if (WebRequest::wasPosted()) {
408
            $this->validateCSRFToken();
409
            $newUsername = WebRequest::postString('newname');
410
411
            if ($newUsername === null || trim($newUsername) === "") {
412
                throw new ApplicationLogicException('The new username cannot be empty');
413
            }
414
415
            if (User::getByUsername($newUsername, $database) != false) {
416
                throw new ApplicationLogicException('The new username already exists');
417
            }
418
419
            $oldUsername = $user->getUsername();
420
            $user->setUsername($newUsername);
421
            $user->setUpdateVersion(WebRequest::postInt('updateversion'));
422
423
            $user->save();
424
425
            $logEntryData = serialize(array(
426
                'old' => $oldUsername,
427
                'new' => $newUsername,
428
            ));
429
430
            Logger::renamedUser($database, $user, $logEntryData);
431
432
            SessionAlert::quick("Changed User "
433
                . htmlentities($oldUsername, ENT_COMPAT, 'UTF-8')
434
                . " name to "
435
                . htmlentities($newUsername, ENT_COMPAT, 'UTF-8'));
436
437
            $this->getNotificationHelper()->userRenamed($user, $oldUsername);
438
439
            // send an email to the user.
440
            $this->assign('targetUsername', $user->getUsername());
441
            $this->assign('toolAdmin', User::getCurrent($database)->getUsername());
442
            $this->assign('oldUsername', $oldUsername);
443
            $this->assign('mailingList', $this->adminMailingList);
444
445
            // FIXME: domains!
446
            /** @var Domain $domain */
447
            $domain = Domain::getById(1, $database);
0 ignored issues
show
Unused Code introduced by
The assignment to $domain is dead and can be removed.
Loading history...
448
            $this->getEmailHelper()->sendMail(
449
                $this->adminMailingList,
450
                $user->getEmail(),
451
                'Your username on WP:ACC has been changed',
452
                $this->fetchTemplate('usermanagement/emails/renamed.tpl')
453
            );
454
455
            $this->redirect("userManagement");
456
457
            return;
458
        }
459
        else {
460
            $this->assignCSRFToken();
461
            $this->setTemplate('usermanagement/renameuser.tpl');
462
            $this->assign('user', $user);
463
        }
464
    }
465
466
    /**
467
     * Entry point for the edit action
468
     *
469
     * @throws ApplicationLogicException
470
     */
471
    protected function editUser()
472
    {
473
        $this->setHtmlTitle('User Management');
474
475
        $database = $this->getDatabase();
476
477
        $userId = WebRequest::getInt('user');
478
        $user = User::getById($userId, $database);
479
        $oauth = new OAuthUserHelper($user, $database, $this->getOAuthProtocolHelper(), $this->getSiteConfiguration());
480
481
        if ($user === false) {
0 ignored issues
show
introduced by
The condition $user === false is always false.
Loading history...
482
            throw new ApplicationLogicException('Sorry, the user you are trying to edit could not be found.');
483
        }
484
485
        // Dual-mode action
486
        if (WebRequest::wasPosted()) {
487
            $this->validateCSRFToken();
488
            $newEmail = WebRequest::postEmail('user_email');
489
            $newOnWikiName = WebRequest::postString('user_onwikiname');
490
491
            if ($newEmail === null) {
492
                throw new ApplicationLogicException('Invalid email address');
493
            }
494
495
            if ($this->validateUnusedEmail($newEmail, $userId)) {
0 ignored issues
show
Bug introduced by
It seems like $userId can also be of type null; however, parameter $userId of Waca\Pages\PageUserManag...::validateUnusedEmail() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

495
            if ($this->validateUnusedEmail($newEmail, /** @scrutinizer ignore-type */ $userId)) {
Loading history...
496
                throw new ApplicationLogicException('The specified email address is already in use.');
497
            }
498
499
            if (!($oauth->isFullyLinked() || $oauth->isPartiallyLinked())) {
500
                if (trim($newOnWikiName) == "") {
0 ignored issues
show
Bug introduced by
It seems like $newOnWikiName can also be of type null; however, parameter $string of trim() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

500
                if (trim(/** @scrutinizer ignore-type */ $newOnWikiName) == "") {
Loading history...
501
                    throw new ApplicationLogicException('New on-wiki username cannot be blank');
502
                }
503
504
                $user->setOnWikiName($newOnWikiName);
505
                $user->setWelcomeSig(WebRequest::postString('sig'));
506
            }
507
508
            $user->setEmail($newEmail);
509
            $user->setCreationMode(WebRequest::postInt('creationmode'));
510
511
            $user->setUpdateVersion(WebRequest::postInt('updateversion'));
512
513
            $user->save();
514
515
            Logger::userPreferencesChange($database, $user);
516
            $this->getNotificationHelper()->userPrefChange($user);
517
            SessionAlert::quick('Changes to user\'s preferences have been saved');
518
519
            $this->redirect("userManagement");
520
521
            return;
522
        }
523
        else {
524
            $this->assignCSRFToken();
525
            $oauth = new OAuthUserHelper($user, $database, $this->getOAuthProtocolHelper(),
526
                $this->getSiteConfiguration());
527
            $this->setTemplate('usermanagement/edituser.tpl');
528
            $this->assign('user', $user);
529
            $this->assign('oauth', $oauth);
530
531
            $this->assign('canManualCreate',
532
                $this->barrierTest(User::CREATION_MANUAL, $user, 'RequestCreation'));
533
            $this->assign('canOauthCreate',
534
                $this->barrierTest(User::CREATION_OAUTH, $user, 'RequestCreation'));
535
            $this->assign('canBotCreate',
536
                $this->barrierTest(User::CREATION_BOT, $user, 'RequestCreation'));
537
        }
538
    }
539
540
    #endregion
541
542
    private function validateUnusedEmail(string $email, int $userId) : bool {
543
        $query = 'SELECT COUNT(id) FROM user WHERE email = :email AND id <> :uid';
544
        $statement = $this->getDatabase()->prepare($query);
545
        $statement->execute(array(':email' => $email, ':uid' => $userId));
546
        $inUse = $statement->fetchColumn() > 0;
547
        $statement->closeCursor();
548
549
        return $inUse;
550
    }
551
552
    /**
553
     * Sends a status change email to the user.
554
     *
555
     * @param string      $subject           The subject of the email
556
     * @param string      $template          The smarty template to use
557
     * @param string|null $reason            The reason for performing the status change
558
     * @param User        $user              The user affected
559
     * @param string      $toolAdminUsername The tool admin's username who is making the edit
560
     */
561
    private function sendStatusChangeEmail($subject, $template, $reason, $user, $toolAdminUsername)
562
    {
563
        $this->assign('targetUsername', $user->getUsername());
564
        $this->assign('toolAdmin', $toolAdminUsername);
565
        $this->assign('actionReason', $reason);
566
        $this->assign('mailingList', $this->adminMailingList);
567
568
        // FIXME: domains!
569
        /** @var Domain $domain */
570
        $domain = Domain::getById(1, $this->getDatabase());
0 ignored issues
show
Unused Code introduced by
The assignment to $domain is dead and can be removed.
Loading history...
571
        $this->getEmailHelper()->sendMail(
572
            $this->adminMailingList,
573
            $user->getEmail(),
574
            $subject,
575
            $this->fetchTemplate($template)
576
        );
577
    }
578
579
    /**
580
     * @param UserRole[] $activeRoles
581
     *
582
     * @return array
583
     */
584
    private function getRoleData($activeRoles)
585
    {
586
        $availableRoles = $this->getSecurityManager()->getRoleConfiguration()->getAvailableRoles();
587
588
        $currentUser = User::getCurrent($this->getDatabase());
589
        $this->getSecurityManager()->getActiveRoles($currentUser, $userRoles, $inactiveRoles);
590
591
        $initialValue = array('active' => 0, 'allowEdit' => 0, 'description' => '???', 'object' => null);
592
593
        $roleData = array();
594
        foreach ($availableRoles as $role => $data) {
595
            $intersection = array_intersect($data['editableBy'], $userRoles);
596
597
            $roleData[$role] = $initialValue;
598
            $roleData[$role]['allowEdit'] = count($intersection) > 0 ? 1 : 0;
599
            $roleData[$role]['description'] = $data['description'];
600
        }
601
602
        foreach ($activeRoles as $role) {
603
            if (!isset($roleData[$role->getRole()])) {
604
                // This value is no longer available in the configuration, allow changing (aka removing) it.
605
                $roleData[$role->getRole()] = $initialValue;
606
                $roleData[$role->getRole()]['allowEdit'] = 1;
607
            }
608
609
            $roleData[$role->getRole()]['object'] = $role;
610
            $roleData[$role->getRole()]['active'] = 1;
611
        }
612
613
        return $roleData;
614
    }
615
}
616