Failed Conditions
Pull Request — newinternal-bugfixing (#286)
by Simon
06:57 queued 03:32
created

includes/Pages/PageUserManagement.php (2 issues)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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\User;
12
use Waca\DataObjects\UserRole;
13
use Waca\Exceptions\ApplicationLogicException;
14
use Waca\Helpers\Logger;
15
use Waca\Helpers\SearchHelpers\UserSearchHelper;
16
use Waca\SessionAlert;
17
use Waca\Tasks\InternalPageBase;
18
use Waca\WebRequest;
19
20
/**
21
 * Class PageUserManagement
22
 * @package Waca\Pages
23
 */
24
class PageUserManagement extends InternalPageBase
25
{
26
    /** @var string */
27
    private $adminMailingList = '[email protected]';
28
29
    /**
30
     * Main function for this page, when no specific actions are called.
31
     */
32
    protected function main()
33
    {
34
        $this->setHtmlTitle('User Management');
35
36
        $database = $this->getDatabase();
37
        $currentUser = User::getCurrent($database);
38
39
        if (WebRequest::getBoolean("showAll")) {
40
            $this->assign("showAll", true);
41
42
            $this->assign("suspendedUsers",
43
                UserSearchHelper::get($database)->byStatus(User::STATUS_SUSPENDED)->fetch());
44
            $this->assign("declinedUsers", UserSearchHelper::get($database)->byStatus(User::STATUS_DECLINED)->fetch());
45
46
            UserSearchHelper::get($database)->getRoleMap($roleMap);
47
        }
48
        else {
49
            $this->assign("showAll", false);
50
            $this->assign("suspendedUsers", array());
51
            $this->assign("declinedUsers", array());
52
53
            UserSearchHelper::get($database)->statusIn(array('New', 'Active'))->getRoleMap($roleMap);
54
        }
55
56
        $this->assign('newUsers', UserSearchHelper::get($database)->byStatus(User::STATUS_NEW)->fetch());
57
        $this->assign('normalUsers',
58
            UserSearchHelper::get($database)->byStatus(User::STATUS_ACTIVE)->byRole('user')->fetch());
59
        $this->assign('adminUsers',
60
            UserSearchHelper::get($database)->byStatus(User::STATUS_ACTIVE)->byRole('admin')->fetch());
61
        $this->assign('checkUsers',
62
            UserSearchHelper::get($database)->byStatus(User::STATUS_ACTIVE)->byRole('checkuser')->fetch());
63
        $this->assign('toolRoots',
64
            UserSearchHelper::get($database)->byStatus(User::STATUS_ACTIVE)->byRole('toolRoot')->fetch());
65
66
        $this->assign('roles', $roleMap);
67
68
        $this->getTypeAheadHelper()->defineTypeAheadSource('username-typeahead', function() use ($database) {
69
            return UserSearchHelper::get($database)->fetchColumn('username');
70
        });
71
72
        $this->assign('canApprove', $this->barrierTest('approve', $currentUser));
73
        $this->assign('canDecline', $this->barrierTest('decline', $currentUser));
74
        $this->assign('canRename', $this->barrierTest('rename', $currentUser));
75
        $this->assign('canEditUser', $this->barrierTest('editUser', $currentUser));
76
        $this->assign('canSuspend', $this->barrierTest('suspend', $currentUser));
77
        $this->assign('canEditRoles', $this->barrierTest('editRoles', $currentUser));
78
79
        $this->setTemplate("usermanagement/main.tpl");
80
    }
81
82
    #region Access control
83
84
    /**
85
     * Action target for editing the roles assigned to a user
86
     */
87
    protected function editRoles()
88
    {
89
        $this->setHtmlTitle('User Management');
90
        $database = $this->getDatabase();
91
        $userId = WebRequest::getInt('user');
92
93
        /** @var User $user */
94
        $user = User::getById($userId, $database);
95
96
        if ($user === false) {
97
            throw new ApplicationLogicException('Sorry, the user you are trying to edit could not be found.');
98
        }
99
100
        $roleData = $this->getRoleData(UserRole::getForUser($user->getId(), $database));
101
102
        // Dual-mode action
103
        if (WebRequest::wasPosted()) {
104
            $this->validateCSRFToken();
105
106
            $reason = WebRequest::postString('reason');
107
            if ($reason === false || trim($reason) === '') {
108
                throw new ApplicationLogicException('No reason specified for roles change');
109
            }
110
111
            /** @var UserRole[] $delete */
112
            $delete = array();
113
            /** @var string[] $delete */
114
            $add = array();
115
116
            foreach ($roleData as $name => $r) {
117
                if ($r['allowEdit'] !== 1) {
118
                    // not allowed, to touch this, so ignore it
119
                    continue;
120
                }
121
122
                $newValue = WebRequest::postBoolean('role-' . $name) ? 1 : 0;
123
                if ($newValue !== $r['active']) {
124
                    if ($newValue === 0) {
125
                        $delete[] = $r['object'];
126
                    }
127
128
                    if ($newValue === 1) {
129
                        $add[] = $name;
130
                    }
131
                }
132
            }
133
134
            // Check there's something to do
135
            if ((count($add) + count($delete)) === 0) {
136
                $this->redirect('statistics/users', 'detail', array('user' => $user->getId()));
137
                SessionAlert::warning('No changes made to roles.');
138
139
                return;
140
            }
141
142
            $removed = array();
143
144
            /** @var UserRole $d */
145
            foreach ($delete as $d) {
146
                $removed[] = $d->getRole();
0 ignored issues
show
The method getRole cannot be called on $d (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
147
                $d->delete();
0 ignored issues
show
The method delete cannot be called on $d (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
148
            }
149
150 View Code Duplication
            foreach ($add as $x) {
151
                $a = new UserRole();
152
                $a->setUser($user->getId());
153
                $a->setRole($x);
154
                $a->setDatabase($database);
155
                $a->save();
156
            }
157
158
            Logger::userRolesEdited($database, $user, $reason, $add, $removed);
159
160
            // dummy save for optimistic locking. If this fails, the entire txn will roll back.
161
            $user->setUpdateVersion(WebRequest::postInt('updateversion'));
162
            $user->save();
163
164
            $this->getNotificationHelper()->userRolesEdited($user, $reason);
165
            SessionAlert::quick('Roles changed for user ' . htmlentities($user->getUsername(), ENT_COMPAT, 'UTF-8'));
166
167
            $this->redirect('statistics/users', 'detail', array('user' => $user->getId()));
168
            return;
169
        }
170
        else {
171
            $this->assignCSRFToken();
172
            $this->setTemplate('usermanagement/roleedit.tpl');
173
            $this->assign('user', $user);
174
            $this->assign('roleData', $roleData);
175
        }
176
    }
177
178
    /**
179
     * Action target for suspending users
180
     *
181
     * @throws ApplicationLogicException
182
     */
183 View Code Duplication
    protected function suspend()
184
    {
185
        $this->setHtmlTitle('User Management');
186
187
        $database = $this->getDatabase();
188
189
        $userId = WebRequest::getInt('user');
190
191
        /** @var User $user */
192
        $user = User::getById($userId, $database);
193
194
        if ($user === false) {
195
            throw new ApplicationLogicException('Sorry, the user you are trying to suspend could not be found.');
196
        }
197
198
        if ($user->isSuspended()) {
199
            throw new ApplicationLogicException('Sorry, the user you are trying to suspend is already suspended.');
200
        }
201
202
        // Dual-mode action
203
        if (WebRequest::wasPosted()) {
204
            $this->validateCSRFToken();
205
            $reason = WebRequest::postString('reason');
206
207
            if ($reason === null || trim($reason) === "") {
208
                throw new ApplicationLogicException('No reason provided');
209
            }
210
211
            $user->setStatus(User::STATUS_SUSPENDED);
212
            $user->setUpdateVersion(WebRequest::postInt('updateversion'));
213
            $user->save();
214
            Logger::suspendedUser($database, $user, $reason);
215
216
            $this->getNotificationHelper()->userSuspended($user, $reason);
217
            SessionAlert::quick('Suspended user ' . htmlentities($user->getUsername(), ENT_COMPAT, 'UTF-8'));
218
219
            // send email
220
            $this->sendStatusChangeEmail(
221
                'Your WP:ACC account has been suspended',
222
                'usermanagement/emails/suspended.tpl',
223
                $reason,
224
                $user,
225
                User::getCurrent($database)->getUsername()
226
            );
227
228
            $this->redirect('userManagement');
229
230
            return;
231
        }
232
        else {
233
            $this->assignCSRFToken();
234
            $this->setTemplate('usermanagement/changelevel-reason.tpl');
235
            $this->assign('user', $user);
236
            $this->assign('status', 'Suspended');
237
            $this->assign("showReason", true);
238
        }
239
    }
240
241
    /**
242
     * Entry point for the decline action
243
     *
244
     * @throws ApplicationLogicException
245
     */
246 View Code Duplication
    protected function decline()
247
    {
248
        $this->setHtmlTitle('User Management');
249
250
        $database = $this->getDatabase();
251
252
        $userId = WebRequest::getInt('user');
253
        $user = User::getById($userId, $database);
254
255
        if ($user === false) {
256
            throw new ApplicationLogicException('Sorry, the user you are trying to decline could not be found.');
257
        }
258
259
        if (!$user->isNewUser()) {
260
            throw new ApplicationLogicException('Sorry, the user you are trying to decline is not new.');
261
        }
262
263
        // Dual-mode action
264
        if (WebRequest::wasPosted()) {
265
            $this->validateCSRFToken();
266
            $reason = WebRequest::postString('reason');
267
268
            if ($reason === null || trim($reason) === "") {
269
                throw new ApplicationLogicException('No reason provided');
270
            }
271
272
            $user->setStatus(User::STATUS_DECLINED);
273
            $user->setUpdateVersion(WebRequest::postInt('updateversion'));
274
            $user->save();
275
            Logger::declinedUser($database, $user, $reason);
276
277
            $this->getNotificationHelper()->userDeclined($user, $reason);
278
            SessionAlert::quick('Declined user ' . htmlentities($user->getUsername(), ENT_COMPAT, 'UTF-8'));
279
280
            // send email
281
            $this->sendStatusChangeEmail(
282
                'Your WP:ACC account has been declined',
283
                'usermanagement/emails/declined.tpl',
284
                $reason,
285
                $user,
286
                User::getCurrent($database)->getUsername()
287
            );
288
289
            $this->redirect('userManagement');
290
291
            return;
292
        }
293
        else {
294
            $this->assignCSRFToken();
295
            $this->setTemplate('usermanagement/changelevel-reason.tpl');
296
            $this->assign('user', $user);
297
            $this->assign('status', 'Declined');
298
            $this->assign("showReason", true);
299
        }
300
    }
301
302
    /**
303
     * Entry point for the approve action
304
     *
305
     * @throws ApplicationLogicException
306
     */
307
    protected function approve()
308
    {
309
        $this->setHtmlTitle('User Management');
310
311
        $database = $this->getDatabase();
312
313
        $userId = WebRequest::getInt('user');
314
        $user = User::getById($userId, $database);
315
316
        if ($user === false) {
317
            throw new ApplicationLogicException('Sorry, the user you are trying to approve could not be found.');
318
        }
319
320
        if ($user->isActive()) {
321
            throw new ApplicationLogicException('Sorry, the user you are trying to approve is already an active user.');
322
        }
323
324
        // Dual-mode action
325
        if (WebRequest::wasPosted()) {
326
            $this->validateCSRFToken();
327
            $user->setStatus(User::STATUS_ACTIVE);
328
            $user->setUpdateVersion(WebRequest::postInt('updateversion'));
329
            $user->save();
330
            Logger::approvedUser($database, $user);
331
332
            $this->getNotificationHelper()->userApproved($user);
333
            SessionAlert::quick('Approved user ' . htmlentities($user->getUsername(), ENT_COMPAT, 'UTF-8'));
334
335
            // send email
336
            $this->sendStatusChangeEmail(
337
                'Your WP:ACC account has been approved',
338
                'usermanagement/emails/approved.tpl',
339
                null,
340
                $user,
341
                User::getCurrent($database)->getUsername()
342
            );
343
344
            $this->redirect("userManagement");
345
346
            return;
347
        }
348
        else {
349
            $this->assignCSRFToken();
350
            $this->setTemplate("usermanagement/changelevel-reason.tpl");
351
            $this->assign("user", $user);
352
            $this->assign("status", "User");
353
            $this->assign("showReason", false);
354
        }
355
    }
356
357
    #endregion
358
359
    #region Renaming / Editing
360
361
    /**
362
     * Entry point for the rename action
363
     *
364
     * @throws ApplicationLogicException
365
     */
366
    protected function rename()
367
    {
368
        $this->setHtmlTitle('User Management');
369
370
        $database = $this->getDatabase();
371
372
        $userId = WebRequest::getInt('user');
373
        $user = User::getById($userId, $database);
374
375
        if ($user === false) {
376
            throw new ApplicationLogicException('Sorry, the user you are trying to rename could not be found.');
377
        }
378
379
        // Dual-mode action
380
        if (WebRequest::wasPosted()) {
381
            $this->validateCSRFToken();
382
            $newUsername = WebRequest::postString('newname');
383
384
            if ($newUsername === null || trim($newUsername) === "") {
385
                throw new ApplicationLogicException('The new username cannot be empty');
386
            }
387
388
            if (User::getByUsername($newUsername, $database) != false) {
389
                throw new ApplicationLogicException('The new username already exists');
390
            }
391
392
            $oldUsername = $user->getUsername();
393
            $user->setUsername($newUsername);
394
            $user->setUpdateVersion(WebRequest::postInt('updateversion'));
395
396
            $user->save();
397
398
            $logEntryData = serialize(array(
399
                'old' => $oldUsername,
400
                'new' => $newUsername,
401
            ));
402
403
            Logger::renamedUser($database, $user, $logEntryData);
404
405
            SessionAlert::quick("Changed User "
406
                . htmlentities($oldUsername, ENT_COMPAT, 'UTF-8')
407
                . " name to "
408
                . htmlentities($newUsername, ENT_COMPAT, 'UTF-8'));
409
410
            $this->getNotificationHelper()->userRenamed($user, $oldUsername);
411
412
            // send an email to the user.
413
            $this->assign('targetUsername', $user->getUsername());
414
            $this->assign('toolAdmin', User::getCurrent($database)->getUsername());
415
            $this->assign('oldUsername', $oldUsername);
416
            $this->assign('mailingList', $this->adminMailingList);
417
418
            $this->getEmailHelper()->sendMail(
419
                $user->getEmail(),
420
                'Your username on WP:ACC has been changed',
421
                $this->fetchTemplate('usermanagement/emails/renamed.tpl'),
422
                array('Reply-To' => $this->adminMailingList)
423
            );
424
425
            $this->redirect("userManagement");
426
427
            return;
428
        }
429
        else {
430
            $this->assignCSRFToken();
431
            $this->setTemplate('usermanagement/renameuser.tpl');
432
            $this->assign('user', $user);
433
        }
434
    }
435
436
    /**
437
     * Entry point for the edit action
438
     *
439
     * @throws ApplicationLogicException
440
     */
441
    protected function editUser()
442
    {
443
        $this->setHtmlTitle('User Management');
444
445
        $database = $this->getDatabase();
446
447
        $userId = WebRequest::getInt('user');
448
        $user = User::getById($userId, $database);
449
450
        if ($user === false) {
451
            throw new ApplicationLogicException('Sorry, the user you are trying to edit could not be found.');
452
        }
453
454
        // Dual-mode action
455
        if (WebRequest::wasPosted()) {
456
            $this->validateCSRFToken();
457
            $newEmail = WebRequest::postEmail('user_email');
458
            $newOnWikiName = WebRequest::postString('user_onwikiname');
459
460
            if ($newEmail === null) {
461
                throw new ApplicationLogicException('Invalid email address');
462
            }
463
464
            if (!$user->isOAuthLinked()) {
465
                if (trim($newOnWikiName) == "") {
466
                    throw new ApplicationLogicException('New on-wiki username cannot be blank');
467
                }
468
469
                $user->setOnWikiName($newOnWikiName);
470
            }
471
472
            $user->setEmail($newEmail);
473
474
            $user->setUpdateVersion(WebRequest::postInt('updateversion'));
475
476
            $user->save();
477
478
            Logger::userPreferencesChange($database, $user);
479
            $this->getNotificationHelper()->userPrefChange($user);
480
            SessionAlert::quick('Changes to user\'s preferences have been saved');
481
482
            $this->redirect("userManagement");
483
484
            return;
485
        }
486
        else {
487
            $this->assignCSRFToken();
488
            $this->setTemplate('usermanagement/edituser.tpl');
489
            $this->assign('user', $user);
490
        }
491
    }
492
493
    #endregion
494
495
    /**
496
     * Sends a status change email to the user.
497
     *
498
     * @param string      $subject           The subject of the email
499
     * @param string      $template          The smarty template to use
500
     * @param string|null $reason            The reason for performing the status change
501
     * @param User        $user              The user affected
502
     * @param string      $toolAdminUsername The tool admin's username who is making the edit
503
     */
504
    private function sendStatusChangeEmail($subject, $template, $reason, $user, $toolAdminUsername)
505
    {
506
        $this->assign('targetUsername', $user->getUsername());
507
        $this->assign('toolAdmin', $toolAdminUsername);
508
        $this->assign('actionReason', $reason);
509
        $this->assign('mailingList', $this->adminMailingList);
510
511
        $this->getEmailHelper()->sendMail(
512
            $user->getEmail(),
513
            $subject,
514
            $this->fetchTemplate($template),
515
            array('Reply-To' => $this->adminMailingList)
516
        );
517
    }
518
519
    /**
520
     * @param UserRole[] $activeRoles
521
     *
522
     * @return array
523
     */
524
    private function getRoleData($activeRoles)
525
    {
526
        $availableRoles = $this->getSecurityManager()->getRoleConfiguration()->getAvailableRoles();
527
528
        $currentUser = User::getCurrent($this->getDatabase());
529
        $this->getSecurityManager()->getActiveRoles($currentUser, $userRoles, $inactiveRoles);
530
531
        $initialValue = array('active' => 0, 'allowEdit' => 0, 'description' => '???', 'object' => null);
532
533
        $roleData = array();
534
        foreach ($availableRoles as $role => $data) {
535
            $intersection = array_intersect($data['editableBy'], $userRoles);
536
537
            $roleData[$role] = $initialValue;
538
            $roleData[$role]['allowEdit'] = count($intersection) > 0 ? 1 : 0;
539
            $roleData[$role]['description'] = $data['description'];
540
        }
541
542
        foreach ($activeRoles as $role) {
543
            if (!isset($roleData[$role->getRole()])) {
544
                // This value is no longer available in the configuration, allow changing (aka removing) it.
545
                $roleData[$role->getRole()] = $initialValue;
546
                $roleData[$role->getRole()]['allowEdit'] = 1;
547
            }
548
549
            $roleData[$role->getRole()]['object'] = $role;
550
            $roleData[$role->getRole()]['active'] = 1;
551
        }
552
553
        return $roleData;
554
    }
555
}
556