Failed Conditions
Push — master ( 1b2d37...3f91db )
by Simon
13:21 queued 09:15
created

PageUserManagement   F

Complexity

Total Complexity 61

Size/Duplication

Total Lines 575
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 61
eloc 302
dl 0
loc 575
ccs 0
cts 310
cp 0
rs 3.52
c 1
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
B deactivate() 0 58 8
B rename() 0 70 7
A validateUnusedEmail() 0 8 1
F editRoles() 0 124 21
A getRoleData() 0 31 5
A main() 0 64 4
A sendStatusChangeEmail() 0 15 1
A approve() 0 47 5
B editUser() 0 76 9

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

479
            if ($this->validateUnusedEmail($newEmail, /** @scrutinizer ignore-type */ $userId)) {
Loading history...
480
                throw new ApplicationLogicException('The specified email address is already in use.');
481
            }
482
483
            if (!($oauth->isFullyLinked() || $oauth->isPartiallyLinked())) {
484
                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

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