Merge pull request #5987 from WoltLab/acp-dahsboard-box-hight
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / acp / form / UserEditForm.class.php
CommitLineData
158bd3ca 1<?php
a9229942 2
158bd3ca 3namespace wcf\acp\form;
a9229942 4
f0f8e343 5use wcf\data\style\Style;
320f4a6d 6use wcf\data\user\avatar\UserAvatar;
fc69b61d 7use wcf\data\user\avatar\UserAvatarAction;
9fdd2995 8use wcf\data\user\cover\photo\UserCoverPhoto;
2bc9f31d 9use wcf\data\user\group\UserGroup;
158bd3ca
TD
10use wcf\data\user\User;
11use wcf\data\user\UserAction;
12use wcf\data\user\UserEditor;
320f4a6d 13use wcf\data\user\UserProfileAction;
158bd3ca 14use wcf\form\AbstractForm;
9fdd2995 15use wcf\system\cache\runtime\UserProfileRuntimeCache;
158bd3ca
TD
16use wcf\system\exception\IllegalLinkException;
17use wcf\system\exception\PermissionDeniedException;
fc69b61d 18use wcf\system\exception\UserInputException;
006b4d78 19use wcf\system\message\embedded\object\MessageEmbeddedObjectManager;
fc69b61d 20use wcf\system\moderation\queue\ModerationQueueManager;
54b237e0 21use wcf\system\option\user\UserOptionHandler;
f0f8e343 22use wcf\system\style\StyleHandler;
460bc6f1 23use wcf\system\user\command\SetColorScheme;
3fcadd19 24use wcf\system\user\multifactor\Setup;
2bc9f31d 25use wcf\system\WCF;
158bd3ca
TD
26use wcf\util\StringUtil;
27
28/**
29 * Shows the user edit form.
a9229942
TD
30 *
31 * @author Marcel Werk
32 * @copyright 2001-2020 WoltLab GmbH
33 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
158bd3ca 34 */
a9229942
TD
35class UserEditForm extends UserAddForm
36{
37 /**
38 * @inheritDoc
39 */
40 public $activeMenuItem = 'wcf.acp.menu.link.user.list';
41
42 /**
43 * @inheritDoc
44 */
45 public $neededPermissions = ['admin.user.canEditUser'];
46
47 /**
48 * user id
49 * @var int
50 */
51 public $userID = 0;
52
53 /**
54 * user editor object
55 * @var UserEditor
56 */
57 public $user;
58
59 /**
60 * ban status
61 * @var bool
62 */
63 public $banned = 0;
64
65 /**
66 * ban reason
67 * @var string
68 */
69 public $banReason = '';
70
71 /**
72 * date when the ban expires
73 * @var int
74 */
75 public $banExpires = 0;
76
77 /**
78 * user avatar object
79 * @var UserAvatar
80 */
81 public $userAvatar;
82
83 /**
84 * avatar type
85 * @var string
86 */
87 public $avatarType = 'none';
88
89 /**
90 * true to disable this avatar
91 * @var bool
92 */
93 public $disableAvatar = 0;
94
95 /**
96 * reason
97 * @var string
98 */
99 public $disableAvatarReason = '';
100
101 /**
102 * date when the avatar will be enabled again
103 * @var int
104 */
105 public $disableAvatarExpires = 0;
106
107 /**
108 * user cover photo object
109 * @var UserCoverPhoto
110 */
111 public $userCoverPhoto;
112
113 /**
114 * true to disable this cover photo
115 * @var bool
116 */
117 public $disableCoverPhoto = 0;
118
119 /**
120 * reason
121 * @var string
122 */
123 public $disableCoverPhotoReason = '';
124
125 /**
126 * date when the cover photo will be enabled again
127 * @var int
128 */
129 public $disableCoverPhotoExpires = 0;
130
131 /**
132 * true to delete the current cover photo
133 * @var bool
134 */
135 public $deleteCoverPhoto = 0;
136
137 /**
138 * true to delete the current auth data
139 * @var bool
140 */
141 public $disconnect3rdParty = 0;
142
143 /**
144 * true to disable multifactor authentication
145 * @var bool
146 */
147 public $multifactorDisable = 0;
148
149 /**
150 * list of available styles for the edited user
151 * @var Style[]
152 * @since 5.3
153 */
154 public $availableStyles = [];
155
156 /**
157 * id of the used style
158 * @var int
159 * @since 5.3
160 */
161 public $styleID = 0;
162
460bc6f1
AE
163 /**
164 * @since 6.0
165 */
166 public string $colorScheme = 'system';
167
a9229942
TD
168 /**
169 * @inheritDoc
170 */
171 public function readParameters()
172 {
173 if (isset($_REQUEST['id'])) {
174 $this->userID = \intval($_REQUEST['id']);
175 }
176 $user = new User($this->userID);
177 if (!$user->userID) {
178 throw new IllegalLinkException();
179 }
180
181 $this->user = new UserEditor($user);
182 if (!UserGroup::isAccessibleGroup($this->user->getGroupIDs())) {
183 throw new PermissionDeniedException();
184 }
006b4d78 185 $this->attachmentObjectID = $this->user->userID;
a9229942
TD
186
187 parent::readParameters();
188 }
189
190 /**
191 * @inheritDoc
192 */
193 protected function initOptionHandler()
194 {
54b237e0 195 \assert($this->optionHandler instanceof UserOptionHandler);
a9229942
TD
196 $this->optionHandler->setUser($this->user->getDecoratedObject());
197 }
198
199 /**
200 * @inheritDoc
201 */
202 public function readFormParameters()
203 {
204 parent::readFormParameters();
205
206 if (!WCF::getSession()->getPermission('admin.user.canEditPassword') || !empty($this->user->authData)) {
31f379b1 207 $this->password = '';
a9229942
TD
208 }
209 if (!WCF::getSession()->getPermission('admin.user.canEditMailAddress')) {
87b8b7b6 210 $this->email = $this->user->email;
a9229942
TD
211 }
212
213 if (!empty($_POST['banned'])) {
214 $this->banned = 1;
215 }
216 if (isset($_POST['banReason'])) {
217 $this->banReason = StringUtil::trim($_POST['banReason']);
218 }
219 if ($this->banned && !isset($_POST['banNeverExpires'])) {
220 if (isset($_POST['banExpires'])) {
619de66a 221 $this->banExpires = @\intval(\strtotime(StringUtil::trim($_POST['banExpires'])));
a9229942
TD
222 }
223 }
224
225 if (isset($_POST['avatarType'])) {
226 $this->avatarType = $_POST['avatarType'];
227 }
228 if (isset($_POST['styleID'])) {
229 $this->styleID = \intval($_POST['styleID']);
230 }
460bc6f1
AE
231 if (isset($_POST['colorScheme'])) {
232 $this->colorScheme = $_POST['colorScheme'];
233 }
a9229942
TD
234
235 if (WCF::getSession()->getPermission('admin.user.canDisableAvatar')) {
236 if (!empty($_POST['disableAvatar'])) {
237 $this->disableAvatar = 1;
238 }
239 if (isset($_POST['disableAvatarReason'])) {
240 $this->disableAvatarReason = StringUtil::trim($_POST['disableAvatarReason']);
241 }
242 if ($this->disableAvatar && !isset($_POST['disableAvatarNeverExpires'])) {
243 if (isset($_POST['disableAvatarExpires'])) {
78b6b34e 244 $this->disableAvatarExpires = @\intval(@\strtotime(StringUtil::trim($_POST['disableAvatarExpires'])));
a9229942
TD
245 }
246 }
247 }
248
249 if (WCF::getSession()->getPermission('admin.user.canDisableCoverPhoto')) {
250 if (isset($_POST['deleteCoverPhoto'])) {
251 $this->deleteCoverPhoto = 1;
252 }
253 if (!empty($_POST['disableCoverPhoto'])) {
254 $this->disableCoverPhoto = 1;
255 }
256 if (isset($_POST['disableCoverPhotoReason'])) {
257 $this->disableCoverPhotoReason = StringUtil::trim($_POST['disableCoverPhotoReason']);
258 }
259 if ($this->disableCoverPhoto && !isset($_POST['disableCoverPhotoNeverExpires'])) {
260 if (isset($_POST['disableCoverPhotoExpires'])) {
78b6b34e 261 $this->disableCoverPhotoExpires = @\intval(@\strtotime(StringUtil::trim($_POST['disableCoverPhotoExpires'])));
a9229942
TD
262 }
263 }
264 }
265
266 if (WCF::getSession()->getPermission('admin.user.canEditPassword') && isset($_POST['disconnect3rdParty'])) {
267 $this->disconnect3rdParty = 1;
268 }
269 if (WCF::getSession()->getPermission('admin.user.canEditPassword') && isset($_POST['multifactorDisable'])) {
270 $this->multifactorDisable = 1;
271 }
272 }
273
274 /**
275 * @inheritDoc
276 */
277 public function readData()
278 {
279 if (empty($_POST)) {
280 // get visible languages
281 $this->readVisibleLanguages();
282
283 // default values
284 $this->readDefaultValues();
285 }
286
287 $userProfile = UserProfileRuntimeCache::getInstance()->getObject($this->userID);
288 foreach (StyleHandler::getInstance()->getStyles() as $style) {
289 if (!$style->isDisabled || $userProfile->getPermission('admin.style.canUseDisabledStyle')) {
290 $this->availableStyles[$style->styleID] = $style;
291 }
292 }
293
294 parent::readData();
295
296 // get the avatar object
806bfe5d 297 if ($this->avatarType == 'custom' && $this->user->avatarID) {
a9229942
TD
298 $this->userAvatar = new UserAvatar($this->user->avatarID);
299 }
300
301 // get the user cover photo object
302 if ($this->user->coverPhotoHash) {
303 // If the editing user lacks the permissions to view the cover photo, the system
304 // will try to load the default cover photo. However, the default cover photo depends
305 // on the style, eventually triggering a change to the template group which will
306 // fail in the admin panel.
307 if ($userProfile->canSeeCoverPhoto()) {
308 $this->userCoverPhoto = UserProfileRuntimeCache::getInstance()
309 ->getObject($this->userID)
310 ->getCoverPhoto(true);
311 }
312 }
313 }
314
315 /**
316 * Sets the selected languages.
317 */
318 protected function readVisibleLanguages()
319 {
320 $this->visibleLanguages = $this->user->getLanguageIDs();
321 }
322
323 /**
324 * Sets the default values.
325 */
326 protected function readDefaultValues()
327 {
328 $this->username = $this->user->username;
87b8b7b6 329 $this->email = $this->user->email;
a9229942
TD
330 $this->groupIDs = $this->user->getGroupIDs(true);
331 $this->languageID = $this->user->languageID;
332 $this->banned = $this->user->banned;
333 $this->banReason = $this->user->banReason;
334 $this->banExpires = $this->user->banExpires;
335 $this->userTitle = $this->user->userTitle;
336 $this->styleID = $this->user->styleID;
337
338 $this->signature = $this->user->signature;
339 $this->disableSignature = $this->user->disableSignature;
340 $this->disableSignatureReason = $this->user->disableSignatureReason;
341 $this->disableSignatureExpires = $this->user->disableSignatureExpires;
342
343 $this->disableAvatar = $this->user->disableAvatar;
344 $this->disableAvatarReason = $this->user->disableAvatarReason;
345 $this->disableAvatarExpires = $this->user->disableAvatarExpires;
346
347 $this->disableCoverPhoto = $this->user->disableCoverPhoto;
348 $this->disableCoverPhotoReason = $this->user->disableCoverPhotoReason;
349 $this->disableCoverPhotoExpires = $this->user->disableCoverPhotoExpires;
350
351 if ($this->user->avatarID) {
352 $this->avatarType = 'custom';
a9229942 353 }
460bc6f1
AE
354
355 $this->colorScheme = $this->user->getUserOption('colorScheme');
a9229942
TD
356 }
357
358 /**
359 * @inheritDoc
360 */
361 public function assignVariables()
362 {
363 parent::assignVariables();
364
365 WCF::getTPL()->assign([
366 'userID' => $this->user->userID,
367 'action' => 'edit',
368 'url' => '',
369 'markedUsers' => 0,
370 'user' => $this->user,
371 'banned' => $this->banned,
372 'banReason' => $this->banReason,
373 'avatarType' => $this->avatarType,
374 'disableAvatar' => $this->disableAvatar,
375 'disableAvatarReason' => $this->disableAvatarReason,
376 'disableAvatarExpires' => $this->disableAvatarExpires,
377 'userAvatar' => $this->userAvatar,
378 'banExpires' => $this->banExpires,
379 'userCoverPhoto' => $this->userCoverPhoto,
380 'disableCoverPhoto' => $this->disableCoverPhoto,
381 'disableCoverPhotoReason' => $this->disableCoverPhotoReason,
382 'disableCoverPhotoExpires' => $this->disableCoverPhotoExpires,
383 'deleteCoverPhoto' => $this->deleteCoverPhoto,
384 'ownerGroupID' => UserGroup::getOwnerGroupID(),
385 'availableStyles' => $this->availableStyles,
386 'styleID' => $this->styleID,
460bc6f1 387 'colorScheme' => $this->colorScheme,
a9229942
TD
388 ]);
389 }
390
391 /**
392 * @inheritDoc
393 */
394 public function save()
395 {
396 AbstractForm::save();
006b4d78
C
397 $this->htmlInputProcessor->setObjectID($this->userID);
398 MessageEmbeddedObjectManager::getInstance()->registerObjects($this->htmlInputProcessor);
a9229942
TD
399
400 // handle avatar
401 if ($this->avatarType != 'custom') {
402 // delete custom avatar
403 if ($this->user->avatarID) {
404 $action = new UserAvatarAction([$this->user->avatarID], 'delete');
405 $action->executeAction();
406 }
407 }
408
409 $avatarData = [];
60014982
TD
410 if ($this->avatarType === 'none') {
411 $avatarData = [
412 'avatarID' => null,
413 ];
a9229942
TD
414 }
415
416 $this->additionalFields = \array_merge($this->additionalFields, $avatarData);
417
418 if ($this->disconnect3rdParty) {
419 $this->additionalFields['authData'] = '';
420 }
421
422 // add default groups
423 $defaultGroups = UserGroup::getAccessibleGroups([UserGroup::GUESTS, UserGroup::EVERYONE, UserGroup::USERS]);
424 $oldGroupIDs = $this->user->getGroupIDs(true);
425 foreach ($oldGroupIDs as $oldGroupID) {
426 if (isset($defaultGroups[$oldGroupID])) {
427 $this->groupIDs[] = $oldGroupID;
428 }
429 }
430 $this->groupIDs = \array_unique($this->groupIDs);
431
432 // save user
433 $saveOptions = $this->optionHandler->save();
434
435 $data = [
436 'data' => \array_merge($this->additionalFields, [
437 'username' => $this->username,
438 'email' => $this->email,
439 'password' => $this->password,
440 'languageID' => $this->languageID,
441 'userTitle' => $this->userTitle,
442 'signature' => $this->htmlInputProcessor->getHtml(),
006b4d78 443 'signatureEnableHtml' => 1,
a9229942
TD
444 'styleID' => $this->styleID,
445 ]),
446 'groups' => $this->groupIDs,
447 'languageIDs' => $this->visibleLanguages,
448 'options' => $saveOptions,
006b4d78 449 'signatureAttachmentHandler' => $this->attachmentHandler,
a9229942
TD
450 ];
451 // handle changed username
452 if (\mb_strtolower($this->username) != \mb_strtolower($this->user->username)) {
453 $data['data']['lastUsernameChange'] = TIME_NOW;
454 $data['data']['oldUsername'] = $this->user->username;
455 }
456
457 // handle ban
458 if (WCF::getSession()->getPermission('admin.user.canBanUser')) {
459 $data['data']['banned'] = $this->banned;
460 $data['data']['banReason'] = $this->banReason;
461 $data['data']['banExpires'] = $this->banExpires;
462 }
463
464 // handle disabled signature
465 if (WCF::getSession()->getPermission('admin.user.canDisableSignature')) {
466 $data['data']['disableSignature'] = $this->disableSignature;
467 $data['data']['disableSignatureReason'] = $this->disableSignatureReason;
468 $data['data']['disableSignatureExpires'] = $this->disableSignatureExpires;
469 }
470
471 // handle disabled avatar
472 if (WCF::getSession()->getPermission('admin.user.canDisableAvatar')) {
473 $data['data']['disableAvatar'] = $this->disableAvatar;
474 $data['data']['disableAvatarReason'] = $this->disableAvatarReason;
475 $data['data']['disableAvatarExpires'] = $this->disableAvatarExpires;
476 }
477
478 // handle disabled cover photo
479 if (WCF::getSession()->getPermission('admin.user.canDisableCoverPhoto')) {
480 $data['data']['disableCoverPhoto'] = $this->disableCoverPhoto;
481 $data['data']['disableCoverPhotoReason'] = $this->disableCoverPhotoReason;
482 $data['data']['disableCoverPhotoExpires'] = $this->disableCoverPhotoExpires;
483
484 if ($this->deleteCoverPhoto) {
485 UserProfileRuntimeCache::getInstance()->getObject($this->userID)->getCoverPhoto()->delete();
486
487 $data['data']['coverPhotoHash'] = null;
488 $data['data']['coverPhotoExtension'] = '';
489
490 UserProfileRuntimeCache::getInstance()->removeObject($this->userID);
491 }
492 }
493
494 $this->objectAction = new UserAction([$this->userID], 'update', $data);
495 $this->objectAction->executeAction();
496
497 // disable multifactor authentication
498 if (WCF::getSession()->getPermission('admin.user.canEditPassword') && $this->multifactorDisable) {
499 WCF::getDB()->beginTransaction();
500 $setups = Setup::getAllForUser($this->user->getDecoratedObject());
501 foreach ($setups as $setup) {
502 $setup->delete();
503 }
504
505 $this->user->update([
506 'multifactorActive' => 0,
507 ]);
508 WCF::getDB()->commitTransaction();
509 }
510
460bc6f1
AE
511 if ($this->user->getUserOption('colorScheme') !== $this->colorScheme) {
512 $command = new SetColorScheme($this->user->getDecoratedObject(), $this->colorScheme);
513 $command();
514 }
515
a9229942
TD
516 // reload user
517 $this->user = new UserEditor(new User($this->userID));
518
519 // update user rank
520 if (MODULE_USER_RANK) {
521 $action = new UserProfileAction([$this->user], 'updateUserRank');
522 $action->executeAction();
523 }
524 if (MODULE_USERS_ONLINE) {
525 $action = new UserProfileAction([$this->user], 'updateUserOnlineMarking');
526 $action->executeAction();
527 }
528
529 // remove assignments
530 $sql = "DELETE FROM wcf" . WCF_N . "_moderation_queue_to_user
531 WHERE userID = ?";
532 $statement = WCF::getDB()->prepareStatement($sql);
533 $statement->execute([$this->user->userID]);
534
535 // reset moderation count
536 ModerationQueueManager::getInstance()->resetModerationCount($this->user->userID);
537 $this->saved();
538
539 // reset password
31f379b1 540 $this->password = '';
a9229942
TD
541
542 // reload user
543 $this->user = new UserEditor(new User($this->userID));
544
545 // show success message
546 WCF::getTPL()->assign('success', true);
547 }
548
549 /**
550 * @inheritDoc
551 */
552 protected function validateUsername($username)
553 {
45db7bb7
TD
554 try {
555 if (\mb_strtolower($this->user->username) != \mb_strtolower($username)) {
556 parent::validateUsername($username);
557 }
558 } catch (UserInputException $e) {
559 if ($e->getField() === 'username' && $e->getType() === 'notUnique') {
560 $user2 = User::getUserByUsername($username);
561 if ($user2->userID != $this->user->userID) {
562 throw $e;
563 }
564 } else {
565 throw $e;
566 }
a9229942
TD
567 }
568 }
569
87b8b7b6
MW
570 #[\Override]
571 protected function validateEmail(string $email): void
a9229942 572 {
a9229942 573 if (\mb_strtolower($this->user->email) != \mb_strtolower($email)) {
87b8b7b6 574 parent::validateEmail($email);
a9229942
TD
575 }
576 }
577
31f379b1 578 #[\Override]
dda7fbce
TD
579 protected function validatePassword(
580 #[\SensitiveParameter]
31f379b1
MW
581 string $password
582 ): void {
583 if (!empty($password)) {
584 parent::validatePassword($password);
a9229942
TD
585 }
586 }
587
588 /**
589 * Validates the user avatar.
590 */
591 protected function validateAvatar()
592 {
60014982 593 if ($this->avatarType != 'custom') {
a9229942
TD
594 $this->avatarType = 'none';
595 }
596
597 try {
598 switch ($this->avatarType) {
599 case 'custom':
600 if (!$this->user->avatarID) {
601 throw new UserInputException('customAvatar');
602 }
603 break;
a9229942
TD
604 }
605 } catch (UserInputException $e) {
606 $this->errorType[$e->getField()] = $e->getType();
607 }
608 }
609
610 /**
611 * @inheritDoc
612 */
613 public function validate()
614 {
615 if ($this->user->userID == WCF::getUser()->userID && WCF::getUser()->hasOwnerAccess()) {
616 $ownerGroupID = UserGroup::getOwnerGroupID();
617 if ($ownerGroupID && !\in_array($ownerGroupID, $this->groupIDs)) {
618 // Members of the owner group cannot remove themselves.
619 throw new PermissionDeniedException();
620 }
621 }
622
623 $this->validateAvatar();
624
625 parent::validate();
626
627 if (!isset($this->availableStyles[$this->styleID])) {
628 $this->styleID = 0;
629 }
460bc6f1
AE
630
631 if ($this->colorScheme !== 'light' && $this->colorScheme !== 'dark') {
632 $this->colorScheme = 'system';
633 }
a9229942 634 }
158bd3ca 635}