uawdijnntqw1x1x1
IP : 216.73.216.107
Hostname : toronto-dev2
Kernel : Linux toronto-dev2 4.15.0-213-generic #224-Ubuntu SMP Mon Jun 19 13:30:12 UTC 2023 x86_64
Disable Function : None :)
OS : Linux
PATH:
/
srv
/
users
/
craft4
/
apps
/
craft4-newsite-space
/
vendor
/
craftcms
/
cms
/
src
/
controllers
/
UsersController.php
/
/
<?php /** * @link https://craftcms.com/ * @copyright Copyright (c) Pixel & Tonic, Inc. * @license https://craftcms.github.io/license/ */ namespace craft\controllers; use Craft; use craft\base\Element; use craft\base\ModelInterface; use craft\base\NameTrait; use craft\elements\Address; use craft\elements\Asset; use craft\elements\Entry; use craft\elements\User; use craft\errors\InvalidElementException; use craft\errors\UploadFailedException; use craft\errors\UserLockedException; use craft\events\DefineUserContentSummaryEvent; use craft\events\FindLoginUserEvent; use craft\events\InvalidUserTokenEvent; use craft\events\LoginFailureEvent; use craft\events\RegisterUserActionsEvent; use craft\events\UserEvent; use craft\helpers\ArrayHelper; use craft\helpers\Assets; use craft\helpers\Db; use craft\helpers\FileHelper; use craft\helpers\Html; use craft\helpers\Image; use craft\helpers\Session; use craft\helpers\UrlHelper; use craft\helpers\User as UserHelper; use craft\i18n\Locale; use craft\models\UserGroup; use craft\services\Users; use craft\web\Application; use craft\web\assets\edituser\EditUserAsset; use craft\web\Controller; use craft\web\Request; use craft\web\ServiceUnavailableHttpException; use craft\web\UploadedFile; use craft\web\View; use DateTime; use Throwable; use yii\base\Exception; use yii\base\InvalidArgumentException; use yii\base\Model; use yii\web\BadRequestHttpException; use yii\web\ForbiddenHttpException; use yii\web\HttpException; use yii\web\NotFoundHttpException; use yii\web\Response; use yii\web\ServerErrorHttpException; /** @noinspection ClassOverridesFieldOfSuperClassInspection */ /** * The UsersController class is a controller that handles various user account related tasks such as logging-in, * impersonating a user, logging out, forgetting passwords, setting passwords, validating accounts, activating * accounts, creating users, saving users, processing user avatars, deleting, suspending and unsuspending users. * Note that all actions in the controller, except [[actionLogin]], [[actionLogout]], [[actionGetRemainingSessionTime]], * [[actionSendPasswordResetEmail]], [[actionSetPassword]], [[actionVerifyEmail]] and [[actionSaveUser]] require an * authenticated Craft session via [[allowAnonymous]]. * * @author Pixel & Tonic, Inc. <support@pixelandtonic.com> * @since 3.0.0 */ class UsersController extends Controller { /** * @event FindLoginUserEvent The event that is triggered before attempting to find a user to sign in * * ```php * use Craft; * use craft\controllers\UsersController; * use craft\elements\User; * use craft\events\FindLoginUserEvent; * use yii\base\Event; * * Event::on( * UsersController::class, * UsersController::EVENT_BEFORE_FIND_LOGIN_USER, * function(FindLoginUserEvent $event) { * // force username-based login * $event->user = User::find() * ->username($event->loginName) * ->addSelect(['users.password', 'users.passwordResetRequired']) * ->one(); * } * ); * * @since 4.2.0 */ public const EVENT_BEFORE_FIND_LOGIN_USER = 'beforeFindLoginUser'; /** * @event FindLoginUserEvent The event that is triggered after attempting to find a user to sign in * @since 4.2.0 */ public const EVENT_AFTER_FIND_LOGIN_USER = 'afterFindLoginUser'; /** * @event LoginFailureEvent The event that is triggered when a failed login attempt was made */ public const EVENT_LOGIN_FAILURE = 'loginFailure'; /** * @event RegisterUserActionsEvent The event that is triggered when a user’s available actions are being registered */ public const EVENT_REGISTER_USER_ACTIONS = 'registerUserActions'; /** * @event UserEvent The event that is triggered BEFORE user groups and permissions ARE assigned to the user getting saved * @since 3.5.13 */ public const EVENT_BEFORE_ASSIGN_GROUPS_AND_PERMISSIONS = 'afterBeforeGroupsAndPermissions'; /** * @event UserEvent The event that is triggered after user groups and permissions have been assigned to the user getting saved * @since 3.5.13 */ public const EVENT_AFTER_ASSIGN_GROUPS_AND_PERMISSIONS = 'afterAssignGroupsAndPermissions'; /** * @event DefineUserContentSummaryEvent The event that is triggered when defining a summary of content owned by a user(s), before they are deleted * * --- * ```php * use craft\controllers\UsersController; * use craft\events\DefineUserContentSummaryEvent; * use yii\base\Event; * * Event::on(UsersController::class, UsersController::EVENT_DEFINE_CONTENT_SUMMARY, function(DefineUserContentSummaryEvent $e) { * $e->contentSummary[] = 'A pair of sneakers'; * }); * ``` * * @since 3.0.13 */ public const EVENT_DEFINE_CONTENT_SUMMARY = 'defineContentSummary'; /** * @event InvalidUserTokenEvent The event that is triggered when an invalid user token is sent. * @since 3.6.5 */ public const EVENT_INVALID_USER_TOKEN = 'invalidUserToken'; /** * @inheritdoc */ protected array|bool|int $allowAnonymous = [ 'get-remaining-session-time' => self::ALLOW_ANONYMOUS_LIVE | self::ALLOW_ANONYMOUS_OFFLINE, 'session-info' => self::ALLOW_ANONYMOUS_LIVE | self::ALLOW_ANONYMOUS_OFFLINE, 'login' => self::ALLOW_ANONYMOUS_LIVE | self::ALLOW_ANONYMOUS_OFFLINE, 'logout' => self::ALLOW_ANONYMOUS_LIVE | self::ALLOW_ANONYMOUS_OFFLINE, 'impersonate-with-token' => self::ALLOW_ANONYMOUS_LIVE | self::ALLOW_ANONYMOUS_OFFLINE, 'save-user' => self::ALLOW_ANONYMOUS_LIVE, 'send-activation-email' => self::ALLOW_ANONYMOUS_LIVE | self::ALLOW_ANONYMOUS_OFFLINE, 'send-password-reset-email' => self::ALLOW_ANONYMOUS_LIVE | self::ALLOW_ANONYMOUS_OFFLINE, 'set-password' => self::ALLOW_ANONYMOUS_LIVE | self::ALLOW_ANONYMOUS_OFFLINE, 'verify-email' => self::ALLOW_ANONYMOUS_LIVE | self::ALLOW_ANONYMOUS_OFFLINE, ]; /** * @inheritdoc */ public function beforeAction($action): bool { // Don't enable CSRF validation for login requests if the user is already logged-in. // (Guards against double-clicking a Login button.) if ($action->id === 'login' && !Craft::$app->getUser()->getIsGuest()) { $this->enableCsrfValidation = false; } return parent::beforeAction($action); } /** * Displays the login template, and handles login post requests. * * @return Response|null * @throws BadRequestHttpException */ public function actionLogin(): ?Response { $userSession = Craft::$app->getUser(); if (!$userSession->getIsGuest()) { // Too easy. return $this->_handleSuccessfulLogin($userSession->getIdentity()); } if (!$this->request->getIsPost()) { return null; } $loginName = $this->request->getRequiredBodyParam('loginName'); $password = $this->request->getRequiredBodyParam('password'); $rememberMe = (bool)$this->request->getBodyParam('rememberMe'); $user = $this->_findLoginUser($loginName); if (!$user || $user->password === null) { // Match $user->authenticate()'s delay $this->_hashCheck(); return $this->_handleLoginFailure(User::AUTH_INVALID_CREDENTIALS); } // Did they submit a valid password, and is the user capable of being logged-in? if (!$user->authenticate($password)) { return $this->_handleLoginFailure($user->authError, $user); } // Get the session duration $generalConfig = Craft::$app->getConfig()->getGeneral(); if ($rememberMe && $generalConfig->rememberedUserSessionDuration !== 0) { $duration = $generalConfig->rememberedUserSessionDuration; } else { $duration = $generalConfig->userSessionDuration; } // Try logging them in if (!$userSession->login($user, $duration)) { // Unknown error return $this->_handleLoginFailure(null, $user); } return $this->_handleSuccessfulLogin($user); } private function _findLoginUser(string $loginName): ?User { $event = new FindLoginUserEvent([ 'loginName' => $loginName, ]); $this->trigger(self::EVENT_BEFORE_FIND_LOGIN_USER, $event); $user = $event->user ?? Craft::$app->getUsers()->getUserByUsernameOrEmail($loginName); $event = new FindLoginUserEvent([ 'loginName' => $loginName, 'user' => $user, ]); $this->trigger(self::EVENT_AFTER_FIND_LOGIN_USER, $event); return $event->user; } /** * Logs a user in for impersonation. * * @return Response|null * @throws BadRequestHttpException * @throws ForbiddenHttpException */ public function actionImpersonate(): ?Response { $this->requirePostRequest(); $userSession = Craft::$app->getUser(); $userId = $this->request->getRequiredBodyParam('userId'); $user = Craft::$app->getUsers()->getUserById($userId); if (!$user) { throw new BadRequestHttpException("Invalid user ID: $userId"); } // Make sure they're allowed to impersonate this user $this->_enforceImpersonatePermission($user); // Save the original user ID to the session now so User::findIdentity() // knows not to worry if the user isn't active yet Session::set(User::IMPERSONATE_KEY, $userSession->getId()); if (!$userSession->loginByUserId($userId)) { Session::remove(User::IMPERSONATE_KEY); $this->setFailFlash(Craft::t('app', 'There was a problem impersonating this user.')); Craft::error($userSession->getIdentity()->username . ' tried to impersonate userId: ' . $userId . ' but something went wrong.', __METHOD__); return null; } return $this->_handleSuccessfulLogin($user); } /** * Generates and returns a new impersonation URL * * @return Response * @throws BadRequestHttpException * @throws ForbiddenHttpException * @throws ServerErrorHttpException * @since 3.6.0 */ public function actionGetImpersonationUrl(): Response { $this->requirePostRequest(); $userId = $this->request->getBodyParam('userId'); $user = Craft::$app->getUsers()->getUserById($userId); if (!$user) { throw new BadRequestHttpException("Invalid user ID: $userId"); } // Make sure they're allowed to impersonate this user $this->_enforceImpersonatePermission($user); // Create a single-use token that expires in an hour $token = Craft::$app->getTokens()->createToken([ 'users/impersonate-with-token', [ 'userId' => $userId, 'prevUserId' => Craft::$app->getUser()->getId(), ], ], 1, new DateTime('+1 hour')); if (!$token) { throw new ServerErrorHttpException('Unable to create the invalidation token.'); } $url = $user->can('accessCp') ? UrlHelper::cpUrl() : UrlHelper::siteUrl(); $url = UrlHelper::urlWithToken($url, $token); return $this->asJson(compact('url')); } /** * Logs a user in for impersonation via an impersonation token. * * @param int $userId * @param int $prevUserId * @return Response|null * @throws BadRequestHttpException * @throws ForbiddenHttpException * @since 3.6.0 */ public function actionImpersonateWithToken(int $userId, int $prevUserId): ?Response { $this->requireToken(); $userSession = Craft::$app->getUser(); $user = Craft::$app->getUsers()->getUserById($userId); $success = false; if ($user) { // Save the original user ID to the session now so User::findIdentity() // knows not to worry if the user isn't active yet Session::set(User::IMPERSONATE_KEY, $prevUserId); $success = $userSession->login($user); if (!$success) { Session::remove(User::IMPERSONATE_KEY); } } if (!$success) { $this->setFailFlash(Craft::t('app', 'There was a problem impersonating this user.')); Craft::error(sprintf('%s tried to impersonate userId: %s but something went wrong.', $userSession->getIdentity()->username, $userId), __METHOD__); return null; } return $this->_handleSuccessfulLogin($user); } /** * Ensures that the current user has permission to impersonate the given user. * * @param User $user * @throws ForbiddenHttpException */ private function _enforceImpersonatePermission(User $user): void { if (!Craft::$app->getUsers()->canImpersonate(static::currentUser(), $user)) { throw new ForbiddenHttpException('You do not have sufficient permissions to impersonate this user'); } } /** * Returns information about the current user session, if any. * * @return Response * @since 3.4.0 */ public function actionSessionInfo(): Response { $this->requireAcceptsJson(); $userSession = Craft::$app->getUser(); /** @var User|null $user */ $user = $userSession->getIdentity(); $return = [ 'isGuest' => $user === null, 'timeout' => $userSession->getRemainingSessionTime(), ]; if (Craft::$app->getConfig()->getGeneral()->enableCsrfProtection) { $return['csrfTokenName'] = Craft::$app->getConfig()->getGeneral()->csrfTokenName; $return['csrfTokenValue'] = $this->request->getCsrfToken(); } if ($user !== null) { $return['id'] = $user->id; $return['uid'] = $user->uid; $return['username'] = $user->username; $return['email'] = $user->email; } return $this->asJson($return); } /** * Returns how many seconds are left in the current elevated user session. * * @return Response */ public function actionGetElevatedSessionTimeout(): Response { $timeout = Craft::$app->getUser()->getElevatedSessionTimeout(); return $this->asJson([ 'timeout' => $timeout, ]); } /** * Starts an elevated user session. * * @return Response|null */ public function actionStartElevatedSession(): ?Response { $password = $this->request->getBodyParam('currentPassword') ?? $this->request->getBodyParam('password'); try { $success = Craft::$app->getUser()->startElevatedSession($password); } catch (UserLockedException $e) { $authError = Craft::$app->getConfig()->getGeneral()->cooldownDuration ? User::AUTH_ACCOUNT_COOLDOWN : User::AUTH_ACCOUNT_LOCKED; $message = UserHelper::getLoginFailureMessage($authError, $e->user); return $this->asFailure($message); } if (!$success) { return $this->asFailure(); } return $this->asSuccess(); } /** * @return Response */ public function actionLogout(): Response { // Passing false here for reasons. Craft::$app->getUser()->logout(false); $data = []; if ($this->request->getAcceptsJson()) { if (Craft::$app->getConfig()->getGeneral()->enableCsrfProtection) { $data['csrfTokenValue'] = $this->request->getCsrfToken(); } return $this->asSuccess( data: $data, ); } // Redirect to the login page if this is a control panel request if ($this->request->getIsCpRequest()) { return $this->redirect(Request::CP_PATH_LOGIN); } return $this->asSuccess( data: $data, redirect: Craft::$app->getConfig()->getGeneral()->getPostLogoutRedirect() ); } /** * Sends a password reset email. * * @return Response|null * @throws NotFoundHttpException if the requested user cannot be found */ public function actionSendPasswordResetEmail(): ?Response { $this->requirePostRequest(); $errors = []; $loginName = null; // If someone's logged in and they're allowed to edit other users, then see if a userId was submitted if (Craft::$app->getUser()->checkPermission('editUsers')) { $userId = $this->request->getBodyParam('userId'); if ($userId) { $user = Craft::$app->getUsers()->getUserById($userId); if (!$user) { throw new NotFoundHttpException('User not found'); } } } /** @noinspection UnSafeIsSetOverArrayInspection - FP */ if (!isset($user)) { $loginName = $this->request->getBodyParam('loginName'); if (!$loginName) { // If they didn't even enter a username/email, just bail now. $errors[] = Craft::$app->getConfig()->getGeneral()->useEmailAsUsername ? Craft::t('app', 'Email is required.') : Craft::t('app', 'Username or email is required.'); return $this->_handleSendPasswordResetError($errors); } $user = Craft::$app->getUsers()->getUserByUsernameOrEmail($loginName); if (!$user || !$user->getIsCredentialed()) { $errors[] = Craft::$app->getConfig()->getGeneral()->useEmailAsUsername ? Craft::t('app', 'Invalid email.') : Craft::t('app', 'Invalid username or email.'); } } // keep track of how long email sending takes $time = microtime(true); // Don't try to send the email if there are already error or there is no user try { if (empty($errors) && !empty($user) && !Craft::$app->getUsers()->sendPasswordResetEmail($user)) { throw new Exception(); } } catch (Exception) { $errors[] = Craft::t('app', 'There was a problem sending the password reset email.'); } if (Craft::$app->getConfig()->getGeneral()->preventUserEnumeration) { // Randomly delay the response $this->_randomlyDelayResponse(microtime(true) - $time); if (!empty($errors)) { $list = implode("\n", array_map(function(string $error) { return sprintf('- %s', $error); }, $errors)); Craft::warning(sprintf("Password reset email not sent:\n%s", $list), __METHOD__); $errors = []; } } if (empty($errors)) { return $this->asSuccess(Craft::t('app', 'Password reset email sent.')); } // Handle the errors. return $this->_handleSendPasswordResetError($errors, $loginName); } /** * Generates a new verification code for a given user, and returns its URL. * * @return Response * @throws BadRequestHttpException if the existing password submitted with the request is invalid */ public function actionGetPasswordResetUrl(): Response { $this->requirePermission('administrateUsers'); if (!$this->_verifyElevatedSession()) { throw new BadRequestHttpException('Existing password verification failed'); } $userId = $this->request->getRequiredParam('userId'); $user = Craft::$app->getUsers()->getUserById($userId); if (!$user) { $this->_noUserExists(); } try { $url = Craft::$app->getUsers()->getPasswordResetUrl($user); } catch (InvalidElementException) { $errors = $user->getFirstErrors(); throw new BadRequestHttpException(reset($errors)); } return $this->asJson([ 'url' => $url, ]); } /** * Sets a user’s password once they’ve verified they have access to their email. * * @return Response */ public function actionSetPassword(): Response { // Have they just submitted a password, or are we just displaying the page? if (!$this->request->getIsPost()) { if (!is_array($info = $this->_processTokenRequest())) { return $info; } /** @var User $user */ /** @var string $uid */ /** @var string $code */ [$user, $uid, $code] = $info; Craft::$app->getUser()->sendUsernameCookie($user); // Send them to the set password template. return $this->_renderSetPasswordTemplate([ 'code' => $code, 'id' => $uid, 'newUser' => !$user->password, ]); } // POST request. They've just set the password. $code = $this->request->getRequiredBodyParam('code'); $uid = $this->request->getRequiredParam('id'); $user = Craft::$app->getUsers()->getUserByUid($uid); if (!$user) { throw new BadRequestHttpException("Invalid user UID: $uid"); } // Make sure we still have a valid token. if (!Craft::$app->getUsers()->isVerificationCodeValidForUser($user, $code)) { return $this->_processInvalidToken($user); } $user->newPassword = $this->request->getRequiredBodyParam('newPassword'); $user->setScenario(User::SCENARIO_PASSWORD); if (!Craft::$app->getElements()->saveElement($user)) { return $this->asFailure( Craft::t('app', 'Couldn’t update password.'), $user->getErrors('newPassword'), ) ?? $this->_renderSetPasswordTemplate([ 'errors' => $user->getErrors('newPassword'), 'code' => $code, 'id' => $uid, 'newUser' => !$user->password, ]); } // If they're pending, try to activate them, and maybe treat this as an activation request if ( $user->getStatus() == User::STATUS_PENDING && Craft::$app->getUsers()->activateUser($user) && ($response = $this->_onAfterActivateUser($user)) !== null ) { return $response; } // Maybe automatically log them in $loggedIn = $this->_maybeLoginUserAfterAccountActivation($user); if ($this->request->getAcceptsJson()) { $return = [ 'status' => $user->getStatus(), ]; if ($loggedIn && Craft::$app->getConfig()->getGeneral()->enableCsrfProtection) { $return['csrfTokenValue'] = $this->request->getCsrfToken(); } return $this->asSuccess(data: $return); } // Can they access the control panel? if ($user->can('accessCp')) { // Send them to the control panel login page by default $url = UrlHelper::cpUrl(Request::CP_PATH_LOGIN); } else { // Send them to the 'setPasswordSuccessPath' by default $setPasswordSuccessPath = Craft::$app->getConfig()->getGeneral()->getSetPasswordSuccessPath(); $url = UrlHelper::siteUrl($setPasswordSuccessPath); } return $this->redirectToPostedUrl($user, $url); } /** * Verifies that a user has access to an email address. * * @return Response */ public function actionVerifyEmail(): Response { if (!is_array($info = $this->_processTokenRequest())) { return $info; } /** @var User $user */ [$user] = $info; $pending = $user->pending; $usersService = Craft::$app->getUsers(); // Do they have an unverified email? if ($user->unverifiedEmail) { if (!$usersService->verifyEmailForUser($user)) { return $this->renderTemplate('_special/emailtaken.twig', [ 'email' => $user->unverifiedEmail, ]); } } elseif ($pending) { // No unverified email so just get on with activating their account $usersService->activateUser($user); } // If they're logged in, give them a success notice if (!Craft::$app->getUser()->getIsGuest()) { $this->setSuccessFlash(Craft::t('app', 'Email verified')); } // Were they just activated? if ($pending && ($response = $this->_onAfterActivateUser($user)) !== null) { return $response; } return $this->_redirectUserToCp($user) ?? $this->_redirectUserAfterEmailVerification($user); } /** * Enables a user that is currently disabled or archived. * * @return Response|null * @since 4.3.2 */ public function actionEnableUser(): ?Response { $this->requirePostRequest(); $userId = $this->request->getRequiredBodyParam('userId'); $user = Craft::$app->getUsers()->getUserById($userId); if (!$user) { $this->_noUserExists(); } $elementsService = Craft::$app->getElements(); if (!$elementsService->canSave($user)) { throw new ForbiddenHttpException('User is not authorized to perform this action.'); } $user->enabled = true; $user->enabledForSite = true; $user->archived = false; if (!$elementsService->saveElement($user, false)) { return $this->asFailure(Craft::t('app', 'Couldn’t save {type}.', [ 'type' => User::lowerDisplayName(), ])); } return $this->asSuccess(Craft::t('app', '{type} saved.', [ 'type' => User::displayName(), ])); } /** * Manually activates a user account. Only admins have access. * * @return Response */ public function actionActivateUser(): ?Response { $this->requirePermission('administrateUsers'); $this->requirePostRequest(); $userVariable = $this->request->getValidatedBodyParam('userVariable') ?? 'user'; $userId = $this->request->getRequiredBodyParam('userId'); $user = Craft::$app->getUsers()->getUserById($userId); if (!$user) { $this->_noUserExists(); } try { if (!Craft::$app->getUsers()->activateUser($user)) { throw new InvalidElementException($user); } } catch (InvalidElementException) { return $this->asModelFailure( $user, Craft::t('app', 'There was a problem activating the user.'), $userVariable, ); } return $this->asModelSuccess( $user, Craft::t('app', 'Successfully activated the user.'), $userVariable, ); } /** * User index * * @param string|null $source * @return Response * @since 4.11.0 */ public function actionIndex(?string $source = null): Response { $this->requirePermission('editUsers'); return $this->renderTemplate('users/_index.twig', [ 'title' => Craft::t('app', 'Users'), 'buttonLabel' => Craft::t('app', 'New {type}', [ 'type' => User::lowerDisplayName(), ]), 'source' => $source, ]); } /** * Edit a user account. * * @param int|string|null $userId The user’s ID, if any, or a string that indicates the user to be edited ('current' or 'client'). * @param User|null $user The user being edited, if there were any validation errors. * @param array|null $errors Any errors that occurred as a result of the previous action. * @return Response * @throws NotFoundHttpException if the requested user cannot be found * @throws BadRequestHttpException if there’s a mismatch between|null $userId and|null $user */ public function actionEditUser(mixed $userId = null, ?User $user = null, ?array $errors = null): Response { if (!empty($errors)) { $firstError = reset($errors); if (is_array($firstError)) { $this->setFailFlash(implode(', ', $firstError)); } else { $this->setFailFlash($firstError); } } // Determine which user account we're editing // --------------------------------------------------------------------- $edition = Craft::$app->getEdition(); $currentUser = static::currentUser(); if ($user === null) { // Are we editing a specific user account? if ($userId !== null) { /** @var User|null $user */ $user = User::find() ->addSelect(['users.password', 'users.passwordResetRequired']) ->id($userId === 'current' ? $currentUser->id : $userId) ->status(null) ->one(); } elseif ($edition === Craft::Pro) { // Registering a new user $user = new User(); } if (!$user) { throw new NotFoundHttpException('User not found'); } } /** @var User $user */ $isNewUser = !$user->id; // Make sure they have permission to edit this user // --------------------------------------------------------------------- $isCurrentUser = $user->getIsCurrent(); if (!$isCurrentUser) { if ($isNewUser) { $this->requirePermission('registerUsers'); } else { $this->requirePermission('editUsers'); } } $canAdministrateUsers = $currentUser->can('administrateUsers'); $canModerateUsers = $currentUser->can('moderateUsers'); $name = trim($user->getName()); // Determine which actions should be available // --------------------------------------------------------------------- $statusLabel = null; $statusActions = []; $sessionActions = []; $destructiveActions = []; $miscActions = []; if ($edition === Craft::Pro && !$isNewUser) { switch ($user->getStatus()) { case Element::STATUS_ARCHIVED: case Element::STATUS_DISABLED: $statusLabel = $user->archived ? Craft::t('app', 'Archived') : Craft::t('app', 'Disabled'); if (Craft::$app->getElements()->canSave($user)) { $statusActions[] = [ 'action' => 'users/enable-user', 'label' => Craft::t('app', 'Enable'), ]; } break; case User::STATUS_INACTIVE: case User::STATUS_PENDING: $statusLabel = $user->pending ? Craft::t('app', 'Pending') : Craft::t('app', 'Inactive'); // Only provide activation actions if they have an email address if ($user->email) { if ($user->pending || $canAdministrateUsers) { $statusActions[] = [ 'action' => 'users/send-activation-email', 'label' => Craft::t('app', 'Send activation email'), ]; } if ($canAdministrateUsers) { // Only need to show the "Copy activation URL" option if they don't have a password if (!$user->password) { $statusActions[] = [ 'id' => 'copy-passwordreset-url', 'label' => Craft::t('app', 'Copy activation URL…'), ]; } $statusActions[] = [ 'action' => 'users/activate-user', 'label' => Craft::t('app', 'Activate account'), ]; } } break; case User::STATUS_SUSPENDED: $statusLabel = Craft::t('app', 'Suspended'); if (Craft::$app->getUsers()->canSuspend($currentUser, $user)) { $statusActions[] = [ 'action' => 'users/unsuspend-user', 'label' => Craft::t('app', 'Unsuspend'), ]; } break; case User::STATUS_ACTIVE: if ($user->locked) { $statusLabel = Craft::t('app', 'Locked'); if ( !$isCurrentUser && ($currentUser->admin || !$user->admin) && $canModerateUsers && ( ($previousUserId = Session::get(User::IMPERSONATE_KEY)) === null || $user->id != $previousUserId ) ) { $statusActions[] = [ 'action' => 'users/unlock-user', 'label' => Craft::t('app', 'Unlock'), ]; } } else { $statusLabel = Craft::t('app', 'Active'); } if (!$isCurrentUser) { $statusActions[] = [ 'action' => 'users/send-password-reset-email', 'label' => Craft::t('app', 'Send password reset email'), ]; if ($canAdministrateUsers) { $statusActions[] = [ 'id' => 'copy-passwordreset-url', 'label' => Craft::t('app', 'Copy password reset URL…'), ]; } } break; } if (!$isCurrentUser) { if (Craft::$app->getUsers()->canImpersonate($currentUser, $user)) { $sessionActions[] = [ 'action' => 'users/impersonate', 'label' => $name ? Craft::t('app', 'Sign in as {user}', ['user' => $user->getName()]) : Craft::t('app', 'Sign in as user'), ]; $sessionActions[] = [ 'id' => 'copy-impersonation-url', 'label' => Craft::t('app', 'Copy impersonation URL…'), ]; } if (Craft::$app->getUsers()->canSuspend($currentUser, $user) && $user->active && !$user->suspended) { $destructiveActions[] = [ 'action' => 'users/suspend-user', 'label' => Craft::t('app', 'Suspend'), ]; } } // Destructive actions that should only be performed on non-admins, unless the current user is also an admin if (!$user->admin || $currentUser->admin) { if (($isCurrentUser || $canAdministrateUsers) && ($user->active || $user->pending)) { $destructiveActions[] = [ 'action' => 'users/deactivate-user', 'label' => Craft::t('app', 'Deactivate…'), 'confirm' => Craft::t('app', 'Deactivating a user revokes their ability to sign in. Are you sure you want to continue?'), ]; } if ($isCurrentUser || $currentUser->can('deleteUsers')) { $destructiveActions[] = [ 'id' => 'delete-btn', 'label' => Craft::t('app', 'Delete…'), ]; } } } // Give plugins a chance to modify these, or add new ones $event = new RegisterUserActionsEvent([ 'user' => $user, 'statusActions' => $statusActions, 'sessionActions' => $sessionActions, 'destructiveActions' => $destructiveActions, 'miscActions' => $miscActions, ]); $this->trigger(self::EVENT_REGISTER_USER_ACTIONS, $event); $actions = array_filter([ $event->statusActions, $event->miscActions, $event->sessionActions, array_map(function(array $action): array { $action['destructive'] = true; return $action; }, $event->destructiveActions), ]); // Set the appropriate page title // --------------------------------------------------------------------- if (!$isNewUser) { if ($isCurrentUser) { $title = Craft::t('app', 'My Account'); } elseif ($name) { $title = Craft::t('app', '{user}’s Account', ['user' => $name]); } else { $title = Craft::t('app', 'Edit {type}', [ 'type' => User::displayName(), ]); } } else { $title = Craft::t('app', 'Create a new {type}', [ 'type' => User::lowerDisplayName(), ]); } // Trigger EVENT_DEFINE_ADDITIONAL_BUTTONS // --------------------------------------------------------------------- $additionalButtons = $user->getAdditionalButtons(); // Prep the form tabs & content // --------------------------------------------------------------------- $form = $user->getFieldLayout()->createForm($user, false, [ 'tabIdPrefix' => 'profile', 'registerDeltas' => true, ]); $selectedTab = 'account'; $tabs = [ 'account' => [ 'label' => Craft::t('app', 'Account'), 'url' => '#account', ], ]; $tabs += $form->getTabMenu(); // Show the permission tab for the users that can change them on Craft Pro editions $canAssignUserGroups = $currentUser->canAssignUserGroups(); $showPermissionsTab = ( $edition === Craft::Pro && ($currentUser->can('assignUserPermissions') || $canAssignUserGroups) ); if ($showPermissionsTab) { $tabs['perms'] = [ 'label' => Craft::t('app', 'Permissions'), 'url' => '#perms', ]; } // Show the preferences tab if it's the current user if ($isCurrentUser) { $tabs['prefs'] = [ 'label' => Craft::t('app', 'Preferences'), 'url' => '#prefs', ]; } // Just one tab looks awkward, so just don't show them at all then. if (count($tabs) == 1) { $tabs = []; } else { if ($user->hasErrors()) { // Add the 'error' class to any tabs that have errors $errors = $user->getErrors(); $accountFields = [ 'username', 'fullName', 'email', 'password', 'newPassword', 'currentPassword', 'passwordResetRequired', 'preferredLanguage', ]; foreach ($errors as $attribute => $error) { if (isset($tabs['account']) && in_array($attribute, $accountFields, true)) { $tabs['account']['class'] = 'error'; } else { if (isset($tabs['profile'])) { $tabs['profile']['class'] = 'error'; } } } } } $fieldsHtml = $form->render(false); // Prepare the language/locale options // --------------------------------------------------------------------- if ($isCurrentUser) { $i18n = Craft::$app->getI18n(); // Language $appLocales = $i18n->getAppLocales(); ArrayHelper::multisort($appLocales, fn(Locale $locale) => $locale->getDisplayName()); $languageId = Craft::$app->getLocale()->getLanguageID(); $languageOptions = array_map(fn(Locale $locale) => [ 'label' => $locale->getDisplayName(Craft::$app->language), 'value' => $locale->id, 'data' => [ 'data' => [ 'hint' => $locale->getLanguageID() !== $languageId ? $locale->getDisplayName() : '', 'hintLang' => $locale->id, ], ], ], $appLocales); $userLanguage = $user->getPreferredLanguage(); if ( !$userLanguage || !ArrayHelper::contains($appLocales, fn(Locale $locale) => $locale->id === $userLanguage) ) { $userLanguage = Craft::$app->language; } // Formatting Locale $allLocales = $i18n->getAllLocales(); ArrayHelper::multisort($allLocales, fn(Locale $locale) => $locale->getDisplayName()); $localeOptions = [ ['label' => Craft::t('app', 'Same as language'), 'value' => '__blank__'], ]; array_push($localeOptions, ...array_map(fn(Locale $locale) => [ 'label' => $locale->getDisplayName(Craft::$app->language), 'value' => $locale->id, 'data' => [ 'data' => [ 'hint' => $locale->getLanguageID() !== $languageId ? $locale->getDisplayName() : false, 'hintLang' => $locale->id, ], ], ], $allLocales)); $userLocale = $user->getPreferredLocale(); if ( !$userLocale || !ArrayHelper::contains($allLocales, fn(Locale $locale) => $locale->id === $userLocale) ) { $userLocale = Craft::$app->getConfig()->getGeneral()->defaultCpLocale; } } else { $languageOptions = $localeOptions = $userLanguage = $userLocale = null; } // Determine whether user photo uploading should be possible $volumeUid = Craft::$app->getProjectConfig()->get('users.photoVolumeUid'); $showPhotoField = $volumeUid && Craft::$app->getVolumes()->getVolumeByUid($volumeUid); // Load the resources and render the page // --------------------------------------------------------------------- // Body class $bodyClass = 'edit-user'; $this->getView()->registerAssetBundle(EditUserAsset::class); $deleteModalRedirect = Craft::$app->getSecurity()->hashData(Craft::$app->getEdition() === Craft::Pro ? 'users' : 'dashboard'); $this->getView()->registerJsWithVars( fn($userId, $isCurrent, $deleteModalRedirect) => <<<JS new Craft.AccountSettingsForm($userId, $isCurrent, { deleteModalRedirect: $deleteModalRedirect, }) JS, [$user->id, $isCurrentUser, $deleteModalRedirect], View::POS_END ); return $this->renderTemplate('users/_edit.twig', compact( 'user', 'isNewUser', 'isCurrentUser', 'statusLabel', 'actions', 'languageOptions', 'localeOptions', 'userLanguage', 'userLocale', 'bodyClass', 'title', 'tabs', 'selectedTab', 'showPhotoField', 'showPermissionsTab', 'canAssignUserGroups', 'fieldsHtml', 'additionalButtons', )); } /** * Provides an endpoint for saving a user account. * * This action accounts for the following scenarios: * - An admin registering a new user account. * - An admin editing an existing user account. * - A normal user with user-administration permissions registering a new user account. * - A normal user with user-administration permissions editing an existing user account. * - A guest registering a new user account ("public registration"). * This action behaves the same regardless of whether it was requested from the control panel or the front-end site. * * @return Response|null * @throws NotFoundHttpException if the requested user cannot be found * @throws BadRequestHttpException if attempting to create a client account, and one already exists * @throws ForbiddenHttpException if attempting public registration but public registration is not allowed */ public function actionSaveUser(): ?Response { $this->requirePostRequest(); $userSession = Craft::$app->getUser(); $currentUser = $userSession->getIdentity(); $canAdministrateUsers = $currentUser && $currentUser->can('administrateUsers'); $generalConfig = Craft::$app->getConfig()->getGeneral(); $userSettings = Craft::$app->getProjectConfig()->get('users') ?? []; $requireEmailVerification = $userSettings['requireEmailVerification'] ?? true; $deactivateByDefault = $userSettings['deactivateByDefault'] ?? false; $userVariable = $this->request->getValidatedBodyParam('userVariable') ?? 'user'; $returnCsrfToken = false; // Get the user being edited // --------------------------------------------------------------------- $userId = $this->request->getBodyParam('userId'); $isNewUser = !$userId; $newEmail = trim($this->request->getBodyParam('email') ?? '') ?: null; $isPublicRegistration = false; // Are we editing an existing user? if ($userId) { /** @var User|null $user */ $user = User::find() ->id($userId) ->status(null) ->addSelect(['users.password', 'users.passwordResetRequired']) ->one(); if (!$user) { throw new NotFoundHttpException('User not found'); } /** @var User $user */ if (!$user->getIsCurrent()) { // Make sure they have permission to edit other users $this->requirePermission('editUsers'); } } else { // Make sure this is Craft Pro, since that's required for having multiple user accounts Craft::$app->requireEdition(Craft::Pro); // Is someone logged in? if ($currentUser) { // Make sure they have permission to register users $this->requirePermission('registerUsers'); } else { // Make sure public registration is allowed $allowPublicRegistration = $userSettings['allowPublicRegistration'] ?? false; if (!$allowPublicRegistration) { throw new ForbiddenHttpException('Public registration is not allowed'); } $isPublicRegistration = true; // See if there's an inactive user with the same email if ($newEmail) { $user = User::find() ->email(Db::escapeParam($newEmail)) ->status(User::STATUS_INACTIVE) ->one(); } } $user = $user ?? new User(); } $isCurrentUser = $user->getIsCurrent(); if ($isCurrentUser) { // Remember the old username in case it changes $oldUsername = $user->username; } // Handle secure properties (email and password) // --------------------------------------------------------------------- $sendActivationEmail = false; // Are they allowed to set the email address? if ($isNewUser || $isCurrentUser || $canAdministrateUsers) { // Make sure it actually changed if (!$isNewUser && $newEmail && $newEmail === $user->email) { $newEmail = null; } if ($newEmail) { // Should we be sending a verification email now? // Even if verification isn't required, send one out on account creation if we don't have a password yet $sendActivationEmail = (!$isPublicRegistration || !$deactivateByDefault) && ( ( $requireEmailVerification && ( $isPublicRegistration || ($isCurrentUser && !$canAdministrateUsers) || ($this->request->getBodyParam('sendActivationEmail') ?? $this->request->getBodyParam('sendVerificationEmail')) ) ) || ( !$requireEmailVerification && $isNewUser && ( ($isPublicRegistration && $generalConfig->deferPublicRegistrationPassword) || ($this->request->getBodyParam('sendActivationEmail') ?? $this->request->getBodyParam('sendVerificationEmail')) ) ) ); if ($sendActivationEmail) { $user->unverifiedEmail = $newEmail; // Mark them as pending if (!$user->active) { $user->pending = true; } } else { // Clear out the unverified email if there is one, // so it doesn't overwrite the new email later on $user->unverifiedEmail = null; } if (!$sendActivationEmail || $isNewUser) { $user->email = $newEmail; } } } else { // Discard the new email if it was posted $newEmail = null; } // Are they allowed to set a new password? if ($isPublicRegistration) { if (!$generalConfig->deferPublicRegistrationPassword) { $user->newPassword = $this->request->getBodyParam('password', ''); } } else { if ($isCurrentUser) { // If there was a newPassword input but it was empty, pretend it didn't exist $user->newPassword = $this->request->getBodyParam('newPassword') ?: null; $returnCsrfToken = $user->newPassword !== null; } } // If editing an existing user and either of these properties are being changed, // require the user’s current password for additional security if ( !$isNewUser && (!empty($newEmail) || $user->newPassword !== null) && !$this->_verifyElevatedSession() ) { Craft::warning('Tried to change the email or password for userId: ' . $user->id . ', but the current password does not match what the user supplied.', __METHOD__); $user->addError('currentPassword', Craft::t('app', 'Incorrect current password.')); } // Handle the rest of the user properties // --------------------------------------------------------------------- // Is the site set to use email addresses as usernames? if ($generalConfig->useEmailAsUsername) { $user->username = $user->email; } elseif ($isNewUser || $currentUser->admin || $isCurrentUser) { $user->username = $this->request->getBodyParam('username', ($user->username ?: $user->email)); } $this->populateNameAttributes($user); // New users should always be initially saved in a pending state, // even if an admin is doing this and opted to not send the verification email if ($isNewUser && !$deactivateByDefault) { $user->pending = true; } if ($canAdministrateUsers) { $user->passwordResetRequired = (bool)$this->request->getBodyParam('passwordResetRequired', $user->passwordResetRequired); } // Is their admin status changing? if ( $currentUser && $currentUser->admin && ($adminParam = $this->request->getBodyParam('admin', $user->admin)) != $user->admin ) { if ($adminParam) { $this->requireElevatedSession(); $user->admin = true; } else { $user->admin = false; } } // If this is public registration and it's a Pro version, // set the default group on the user, so that any content // based on user group condition can be validated and saved against them if ($isPublicRegistration) { $groups = Craft::$app->getUsers()->getDefaultUserGroups($user); if (!empty($groups)) { $user->setGroups($groups); } } // If this is Craft Pro, grab any profile content from post $fieldsLocation = $this->request->getParam('fieldsLocation', 'fields'); $user->setFieldValuesFromRequest($fieldsLocation); // Validate and save! // --------------------------------------------------------------------- $photo = UploadedFile::getInstanceByName('photo'); if ($photo && !Image::canManipulateAsImage($photo->getExtension())) { $user->addError('photo', Craft::t('app', 'The user photo provided is not an image.')); } // Don't validate required custom fields if it's public registration if (!$isPublicRegistration || ($userSettings['validateOnPublicRegistration'] ?? false)) { $user->setScenario(Element::SCENARIO_LIVE); } elseif ($isPublicRegistration) { $user->setScenario(User::SCENARIO_REGISTRATION); } // Manually validate the user so we can pass $clearErrors=false $success = $user->validate(null, false) && Craft::$app->getElements()->saveElement($user, false); if (!$success) { Craft::info('User not saved due to validation error.', __METHOD__); if ($isPublicRegistration) { // Move any 'newPassword' errors over to 'password' $user->addErrors(['password' => $user->getErrors('newPassword')]); $user->clearErrors('newPassword'); } // Copy any 'unverifiedEmail' errors to 'email' if (!$user->hasErrors('email')) { $user->addErrors(['email' => $user->getErrors('unverifiedEmail')]); $user->clearErrors('unverifiedEmail'); } return $this->asModelFailure( $user, Craft::t('app', 'Couldn’t save {type}.', [ 'type' => User::lowerDisplayName(), ]), $userVariable ); } // If this is a new user and email verification isn't required, // go ahead and activate them now. if ($isNewUser && !$requireEmailVerification && !$deactivateByDefault) { Craft::$app->getUsers()->activateUser($user); } if ($isCurrentUser) { // Save their preferences too $preferredLocale = $this->request->getBodyParam('preferredLocale', $user->getPreference('locale')) ?: null; if ($preferredLocale === '__blank__') { $preferredLocale = null; } $preferences = [ 'language' => $this->request->getBodyParam('preferredLanguage', $user->getPreference('language')), 'locale' => $preferredLocale, 'weekStartDay' => $this->request->getBodyParam('weekStartDay', $user->getPreference('weekStartDay')), 'alwaysShowFocusRings' => (bool)$this->request->getBodyParam('alwaysShowFocusRings', $user->getPreference('alwaysShowFocusRings')), 'useShapes' => (bool)$this->request->getBodyParam('useShapes', $user->getPreference('useShapes')), 'underlineLinks' => (bool)$this->request->getBodyParam('underlineLinks', $user->getPreference('underlineLinks')), 'notificationDuration' => $this->request->getBodyParam('notificationDuration', $user->getPreference('notificationDuration')), ]; if ($user->admin) { $preferences = array_merge($preferences, [ 'showFieldHandles' => (bool)$this->request->getBodyParam('showFieldHandles', $user->getPreference('showFieldHandles')), 'enableDebugToolbarForSite' => (bool)$this->request->getBodyParam('enableDebugToolbarForSite', $user->getPreference('enableDebugToolbarForSite')), 'enableDebugToolbarForCp' => (bool)$this->request->getBodyParam('enableDebugToolbarForCp', $user->getPreference('enableDebugToolbarForCp')), 'showExceptionView' => (bool)$this->request->getBodyParam('showExceptionView', $user->getPreference('showExceptionView')), 'profileTemplates' => (bool)$this->request->getBodyParam('profileTemplates', $user->getPreference('profileTemplates')), ]); } Craft::$app->getUsers()->saveUserPreferences($user, $preferences); Craft::$app->updateTargetLanguage(); } // Is this the current user, and did their username just change? // todo: remove comment when WI-51866 is fixed /** @noinspection PhpUndefinedVariableInspection */ if ($isCurrentUser && $user->username !== $oldUsername) { // Update the username cookie $userSession->sendUsernameCookie($user); } // Save the user’s photo, if it was submitted $this->_processUserPhoto($user); if (Craft::$app->getEdition() === Craft::Pro) { // If this is public registration, assign the user to the default user group if ($isPublicRegistration) { // Assign them to the default user group Craft::$app->getUsers()->assignUserToDefaultGroup($user); } elseif ($currentUser) { // Fire an 'afterBeforeGroupsAndPermissions' event if ($this->hasEventHandlers(self::EVENT_BEFORE_ASSIGN_GROUPS_AND_PERMISSIONS)) { $this->trigger(self::EVENT_BEFORE_ASSIGN_GROUPS_AND_PERMISSIONS, new UserEvent([ 'user' => $user, ])); } // Assign user groups and permissions if the current user is allowed to do that $this->_saveUserGroups($user, $currentUser); $this->_saveUserPermissions($user, $currentUser); // Fire an 'afterAssignGroupsAndPermissions' event if ($this->hasEventHandlers(self::EVENT_AFTER_ASSIGN_GROUPS_AND_PERMISSIONS)) { $this->trigger(self::EVENT_AFTER_ASSIGN_GROUPS_AND_PERMISSIONS, new UserEvent([ 'user' => $user, ])); } } } // Do we need to send a verification email out? if ($sendActivationEmail) { // Temporarily set the unverified email on the User so the verification email goes to the // right place $originalEmail = $user->email; $user->email = $user->unverifiedEmail; if ($isNewUser) { // Send the activation email Craft::$app->getUsers()->sendActivationEmail($user); } else { // Send the standard verification email Craft::$app->getUsers()->sendNewEmailVerifyEmail($user); } // Put the original email back into place $user->email = $originalEmail; } // Is this public registration, and was the user going to be activated automatically? $publicActivation = $isPublicRegistration && $user->getStatus() === User::STATUS_ACTIVE; $loggedIn = $publicActivation && $this->_maybeLoginUserAfterAccountActivation($user); $returnCsrfToken = $returnCsrfToken || $loggedIn; if ($this->request->getAcceptsJson()) { return $this->asModelSuccess( $user, Craft::t('app', '{type} saved.', ['type' => User::displayName()]), $userVariable, array_filter([ 'id' => $user->id, // todo: remove 'csrfTokenValue' => $returnCsrfToken && $generalConfig->enableCsrfProtection ? $this->request->getCsrfToken() : null, ]), ); } if ($isPublicRegistration) { if (($message = $this->request->getParam('userRegisteredNotice')) !== null) { $default = Html::encode($message); Craft::$app->getDeprecator()->log('userRegisteredNotice', 'The `userRegisteredNotice` param has been deprecated for `users/save-user` requests. Use a hashed `successMessage` param instead.'); } else { $default = Craft::t('app', 'User registered.'); } $this->setSuccessFlash($default); } else { $this->setSuccessFlash(Craft::t('app', '{type} saved.', [ 'type' => User::displayName(), ])); } // Is this public registration, and is the user going to be activated automatically? if ($publicActivation) { return $this->_redirectUserToCp($user) ?? $this->_redirectUserAfterAccountActivation($user); } if (!$this->request->getAcceptsJson()) { // Tell all browser windows about the draft deletion Craft::$app->getSession()->broadcastToJs([ 'event' => 'saveElement', 'id' => $user->id, ]); } return $this->redirectToPostedUrl($user); } /** * Upload a user photo. * * @return Response|null * @throws BadRequestHttpException if the uploaded file is not an image */ public function actionUploadUserPhoto(): ?Response { $this->requireAcceptsJson(); $userId = $this->request->getRequiredBodyParam('userId'); if ($userId != static::currentUser()->id) { $this->requirePermission('editUsers'); } if (($file = UploadedFile::getInstanceByName('photo')) === null) { return null; } try { if ($file->getHasError()) { throw new UploadFailedException($file->error); } $users = Craft::$app->getUsers(); $user = $users->getUserById($userId); // Move to our own temp location $fileLocation = Assets::tempFilePath($file->getExtension()); move_uploaded_file($file->tempName, $fileLocation); $users->saveUserPhoto($fileLocation, $user, $file->name); return $this->asJson([ 'html' => $this->_renderPhotoTemplate($user), 'photoId' => $user->photoId, ]); } catch (Throwable $exception) { if (isset($fileLocation) && file_exists($fileLocation)) { FileHelper::unlink($fileLocation); } Craft::error('There was an error uploading the photo: ' . $exception->getMessage(), __METHOD__); return $this->asFailure(Craft::t('app', 'There was an error uploading your photo: {error}', [ 'error' => $exception->getMessage(), ])); } } /** * Delete all the photos for current user. * * @return Response * @throws BadRequestHttpException */ public function actionDeleteUserPhoto(): Response { $this->requireAcceptsJson(); $userId = $this->request->getRequiredBodyParam('userId'); if ($userId != static::currentUser()->id) { $this->requirePermission('editUsers'); } $user = Craft::$app->getUsers()->getUserById($userId); if ($user->photoId) { Craft::$app->getElements()->deleteElementById($user->photoId, Asset::class); } $user->photoId = null; Craft::$app->getElements()->saveElement($user, false); return $this->asJson([ 'html' => $this->_renderPhotoTemplate($user), ]); } /** * Sends a new activation email to a user. * * @return Response|null * @throws BadRequestHttpException if the user is not pending */ public function actionSendActivationEmail(): ?Response { $this->requirePostRequest(); $userId = $this->request->getRequiredBodyParam('userId'); /** @var User|null $user */ $user = User::find() ->id($userId) ->status(null) ->addSelect(['users.password']) ->one(); if (!$user) { $this->_noUserExists(); } // Only allow activation emails to be sent to inactive/pending users. /** @var User $user */ $status = $user->getStatus(); if (!in_array($status, [User::STATUS_INACTIVE, User::STATUS_PENDING])) { throw new BadRequestHttpException('Activation emails can only be sent to inactive or pending users'); } if (!$user->pending) { $this->requirePermission('administrateUsers'); } try { $emailSent = Craft::$app->getUsers()->sendActivationEmail($user); } catch (InvalidElementException) { $emailSent = false; } $userVariable = $this->request->getValidatedBodyParam('userVariable') ?? 'user'; if ($user->hasErrors()) { return $this->asModelFailure( $user, Craft::t('app', 'Couldn’t send activation email.'), $userVariable, ); } return $emailSent ? $this->asSuccess(Craft::t('app', 'Activation email sent.')) : $this->asFailure(Craft::t('app', 'Couldn’t send activation email. Check your email settings.')); } /** * Unlocks a user, bypassing the cooldown phase. * * @return Response * @throws ForbiddenHttpException if a non-admin is attempting to unlock an admin */ public function actionUnlockUser(): Response { $this->requirePostRequest(); $this->requirePermission('moderateUsers'); $userId = $this->request->getRequiredBodyParam('userId'); $user = Craft::$app->getUsers()->getUserById($userId); if (!$user) { $this->_noUserExists(); } // Even if you have moderateUsers permissions, only and admin should be able to unlock another admin. if ($user->admin) { $currentUser = static::currentUser(); if (!$currentUser->admin) { throw new ForbiddenHttpException('Only admins can unlock other admins.'); } // And admins can't unlock themselves by impersonating another admin $previousUserId = Session::get(User::IMPERSONATE_KEY); if ($previousUserId && $user->id == $previousUserId) { throw new ForbiddenHttpException('You can’t unlock yourself via impersonation.'); } } Craft::$app->getUsers()->unlockUser($user); $this->setSuccessFlash(Craft::t('app', 'User unlocked.')); return $this->redirectToPostedUrl(); } /** * Suspends a user. * * @return Response|null * @throws ForbiddenHttpException if a non-admin is attempting to suspend an admin */ public function actionSuspendUser(): ?Response { $this->requirePostRequest(); $this->requirePermission('moderateUsers'); $userId = $this->request->getRequiredBodyParam('userId'); $user = Craft::$app->getUsers()->getUserById($userId); if (!$user) { $this->_noUserExists(); } $usersService = Craft::$app->getUsers(); $currentUser = static::currentUser(); if (!$usersService->canSuspend($currentUser, $user) || !$usersService->suspendUser($user)) { $this->setFailFlash(Craft::t('app', 'Couldn’t suspend user.')); return null; } $this->setSuccessFlash(Craft::t('app', 'User suspended.')); return $this->redirectToPostedUrl(); } /** * Returns a summary of the content that is owned by a given user ID(s). * * @return Response * @since 3.0.13 */ public function actionUserContentSummary(): Response { $this->requirePostRequest(); $userId = $this->request->getRequiredBodyParam('userId'); if (is_array($userId)) { $userId = array_map(fn($id) => (int)$id, $userId); } else { $userId = (int)$userId; } if ($userId !== static::currentUser()?->id) { $this->requirePermission('deleteUsers'); } $summary = []; foreach (Craft::$app->getSections()->getAllSections() as $section) { $entryCount = Entry::find() ->sectionId($section->id) ->authorId($userId) ->site('*') ->unique() ->status(null) ->count(); if ($entryCount) { $summary[] = Craft::t('app', '{num, number} {section} {num, plural, =1{entry} other{entries}}', [ 'num' => $entryCount, 'section' => Craft::t('site', $section->name), ]); } } // Fire a 'defineUserContentSummary' event $event = new DefineUserContentSummaryEvent([ 'userId' => $userId, 'contentSummary' => $summary, ]); $this->trigger(self::EVENT_DEFINE_CONTENT_SUMMARY, $event); return $this->asJson($event->contentSummary); } /** * Deactivates a user. * * @return Response|null * @since 4.0.0 */ public function actionDeactivateUser(): ?Response { $this->requirePostRequest(); $userId = $this->request->getRequiredBodyParam('userId'); $user = Craft::$app->getUsers()->getUserById($userId); if (!$user) { $this->_noUserExists(); } if (!$user->getIsCurrent()) { $this->requirePermission('administrateUsers'); // Even if you have administrateUsers permissions, only and admin should be able to deactivate another admin. if ($user->admin) { $this->requireAdmin(false); } } // Deactivate the user if (Craft::$app->getUsers()->deactivateUser($user)) { $this->setSuccessFlash(Craft::t('app', 'Successfully deactivated the user.')); } else { $this->setFailFlash(Craft::t('app', 'There was a problem deactivating the user.')); } return $this->redirectToPostedUrl(); } /** * Deletes a user. * * @return Response|null */ public function actionDeleteUser(): ?Response { $this->requirePostRequest(); $userId = $this->request->getRequiredBodyParam('userId'); $user = Craft::$app->getUsers()->getUserById($userId); if (!$user) { $this->_noUserExists(); } if (!$user->getIsCurrent()) { $this->requirePermission('deleteUsers'); // Even if you have deleteUser permissions, only and admin should be able to delete another admin. if ($user->admin) { $this->requireAdmin(false); } } // Are we transferring the user’s content to a different user? $transferContentToId = $this->request->getBodyParam('transferContentTo'); if (is_array($transferContentToId) && isset($transferContentToId[0])) { $transferContentToId = $transferContentToId[0]; } if ($transferContentToId) { $transferContentTo = Craft::$app->getUsers()->getUserById($transferContentToId); if (!$transferContentTo) { $this->_noUserExists(); } } else { $transferContentTo = null; } // Delete the user $user->inheritorOnDelete = $transferContentTo; if (!Craft::$app->getElements()->deleteElement($user)) { $this->setFailFlash(Craft::t('app', 'Couldn’t delete {type}.', [ 'type' => User::lowerDisplayName(), ])); return null; } $this->setSuccessFlash(Craft::t('app', '{type} deleted.', [ 'type' => User::displayName(), ])); return $this->redirectToPostedUrl(); } /** * Unsuspends a user. * * @return Response|null * @throws ForbiddenHttpException if a non-admin is attempting to unsuspend an admin */ public function actionUnsuspendUser(): ?Response { $this->requirePostRequest(); $this->requirePermission('moderateUsers'); $userId = $this->request->getRequiredBodyParam('userId'); $user = Craft::$app->getUsers()->getUserById($userId); if (!$user) { $this->_noUserExists(); } // Even if you have moderateUsers permissions, only and admin should be able to unsuspend another admin. $usersService = Craft::$app->getUsers(); $currentUser = static::currentUser(); if (!$usersService->canSuspend($currentUser, $user) || !$usersService->unsuspendUser($user)) { $this->setFailFlash(Craft::t('app', 'Couldn’t unsuspend user.')); return null; } $this->setSuccessFlash(Craft::t('app', 'User unsuspended.')); return $this->redirectToPostedUrl(); } /** * Saves a user’s address. * * @return Response|null * @throws BadRequestHttpException * @throws ForbiddenHttpException * @since 4.0.0 */ public function actionSaveAddress(): ?Response { $elementsService = Craft::$app->getElements(); $user = static::currentUser(); $userId = (int)($this->request->getBodyParam('userId') ?? $user->id); $addressId = $this->request->getBodyParam('addressId'); if ($addressId) { $address = Address::findOne($addressId); if (!$address) { throw new BadRequestHttpException("Invalid address ID: $addressId"); } if ($address->ownerId !== $userId) { throw new BadRequestHttpException("Address $addressId is not owned by user $userId"); } } else { $address = new Address([ 'ownerId' => $userId, ]); } if (!$elementsService->canSave($address, $user)) { throw new ForbiddenHttpException('User is not permitted to edit this address.'); } // Addresses have no status, and the default element save controller also sets the address scenario to live $address->setScenario(Element::SCENARIO_LIVE); // Name attributes $this->populateNameAttributes($address); // All safe attributes $safeAttributes = []; foreach ($address->safeAttributes() as $name) { $value = $this->request->getBodyParam($name); if ($value !== null) { $safeAttributes[$name] = $value; } } $address->setAttributes($safeAttributes); // Custom fields $fieldsLocation = $this->request->getParam('fieldsLocation') ?? 'fields'; $address->setFieldValuesFromRequest($fieldsLocation); if (!$elementsService->saveElement($address)) { return $this->asModelFailure($address, Craft::t('app', 'Couldn’t save {type}.', [ 'type' => Address::lowerDisplayName(), ]), 'address'); } return $this->asModelSuccess($address, Craft::t('app', '{type} saved.', [ 'type' => Address::displayName(), ])); } /** * Deletes a user’s address. * * @return Response|null * @since 4.0.0 */ public function actionDeleteAddress(): ?Response { $addressId = $this->request->getRequiredBodyParam('addressId'); $address = Address::findOne($addressId); if (!$address) { throw new BadRequestHttpException("Invalid address ID: $addressId"); } $elementsService = Craft::$app->getElements(); if (!$elementsService->canDelete($address)) { throw new ForbiddenHttpException('User is not permitted to delete this address.'); } if (!$elementsService->deleteElement($address)) { return $this->asModelFailure($address, Craft::t('app', 'Couldn’t delete {type}.', [ 'type' => Address::lowerDisplayName(), ]), 'address'); } return $this->asModelSuccess($address, Craft::t('app', '{type} deleted.', [ 'type' => Address::displayName(), ])); } /** * Saves the user field layout. * * @return Response|null */ public function actionSaveFieldLayout(): ?Response { $this->requirePostRequest(); $this->requireAdmin(); // Set the field layout $fieldLayout = Craft::$app->getFields()->assembleLayoutFromPost(); $fieldLayout->type = User::class; $fieldLayout->reservedFieldHandles = [ 'active', 'addresses', 'admin', 'email', 'firstName', 'friendlyName', 'groups', 'lastName', 'locked', 'name', 'password', 'pending', 'photo', 'suspended', 'username', ]; if (!Craft::$app->getUsers()->saveLayout($fieldLayout)) { Craft::$app->getUrlManager()->setRouteParams([ 'variables' => [ 'fieldLayout' => $fieldLayout, ], ]); $this->setFailFlash(Craft::t('app', 'Couldn’t save user fields.')); return null; } $this->setSuccessFlash(Craft::t('app', 'User fields saved.')); return $this->redirectToPostedUrl(); } /** * Verifies a password for a user. * * @return Response|null */ public function actionVerifyPassword(): ?Response { $this->requireAcceptsJson(); if ($this->_verifyExistingPassword()) { return $this->asSuccess(); } return $this->asFailure(Craft::t('app', 'Invalid password.')); } /** * Handles a failed login attempt. * * @param string|null $authError * @param User|null $user * @return Response|null * @throws ServiceUnavailableHttpException */ private function _handleLoginFailure(?string $authError, ?User $user = null): ?Response { $message = UserHelper::getLoginFailureMessage($authError, $user); // Fire a 'loginFailure' event $event = new LoginFailureEvent([ 'authError' => $authError, 'message' => $message, 'user' => $user, ]); $this->trigger(self::EVENT_LOGIN_FAILURE, $event); return $this->asFailure( $event->message, data: [ 'errorCode' => $authError, ], routeParams: [ 'loginName' => $this->request->getBodyParam('loginName'), 'rememberMe' => (bool)$this->request->getBodyParam('rememberMe'), 'errorCode' => $authError, 'errorMessage' => $event->message, ] ); } /** * Redirects the user after a successful login attempt, or if they visited the Login page while they were already * logged in. * * @param User $user * @return Response */ private function _handleSuccessfulLogin(User $user): Response { // Get the return URL $userSession = Craft::$app->getUser(); $returnUrl = $userSession->getReturnUrl(); // Clear it out $userSession->removeReturnUrl(); // If this was an Ajax request, just return success:true if ($this->request->getAcceptsJson()) { $return = [ 'returnUrl' => $returnUrl, ]; if (Craft::$app->getConfig()->getGeneral()->enableCsrfProtection) { $return['csrfTokenValue'] = $this->request->getCsrfToken(); } return $this->asModelSuccess($user, modelName: 'user', data: $return); } return $this->redirectToPostedUrl($userSession->getIdentity(), $returnUrl); } /** * Renders the Set Password template for a given user. * * @param array $variables * @return Response */ private function _renderSetPasswordTemplate(array $variables): Response { // If this is a site request, try handling the request like normal if ($this->request->getIsSiteRequest()) { try { Craft::$app->getUrlManager()->setRouteParams([ 'variables' => $variables, ]); // Avoid re-routing to the same action again $this->request->checkIfActionRequest(true, true, false); if ($this->request->getActionSegments() === ['users', 'set-password']) { $this->request->setIsActionRequest(false); } /** @var Application $app */ $app = Craft::$app; return $app->handleRequest($this->request, true); } catch (NotFoundHttpException) { // Just go with the control panel template } } // Otherwise go with the control panel’s template return $this->renderTemplate('setpassword.twig', $variables, View::TEMPLATE_MODE_CP); } /** * Throws a "no user exists" exception * * @throws BadRequestHttpException */ private function _noUserExists(): void { throw new BadRequestHttpException('User not found'); } /** * Verifies that the user has an elevated session, or that their current password was submitted with the request. * * @return bool */ private function _verifyElevatedSession(): bool { return (Craft::$app->getUser()->getHasElevatedSession() || $this->_verifyExistingPassword()); } /** * Verifies that the current user’s password was submitted with the request. * * @return bool */ private function _verifyExistingPassword(): bool { $currentUser = static::currentUser(); if (!$currentUser) { return false; } $currentPassword = $this->request->getParam('currentPassword') ?? $this->request->getParam('password'); if ($currentPassword === null) { return false; } $currentHashedPassword = $currentUser->password; try { return Craft::$app->getSecurity()->validatePassword($currentPassword, $currentHashedPassword); } catch (InvalidArgumentException) { return false; } } /** * @param User $user * @throws Throwable if reasons */ private function _processUserPhoto(User $user): void { // Delete their photo? $users = Craft::$app->getUsers(); if ($this->request->getBodyParam('deletePhoto')) { $users->deleteUserPhoto($user); $user->photoId = null; Craft::$app->getElements()->saveElement($user); } $newPhoto = false; $fileLocation = null; $filename = null; // Did they upload a new one? if ($photo = UploadedFile::getInstanceByName('photo')) { $fileLocation = Assets::tempFilePath($photo->getExtension()); move_uploaded_file($photo->tempName, $fileLocation); $filename = $photo->name; $newPhoto = true; } elseif (($photo = $this->request->getBodyParam('photo')) && is_array($photo)) { // base64-encoded photo $matches = []; if (preg_match('/^data:((?<type>[a-z0-9]+\/[a-z0-9\+]+);)?base64,(?<data>.+)/i', $photo['data'] ?? '', $matches)) { $filename = $photo['filename'] ?? null; $extension = $filename ? pathinfo($filename, PATHINFO_EXTENSION) : null; if (!$extension && !empty($matches['type'])) { try { $extension = FileHelper::getExtensionByMimeType($matches['type']); } catch (InvalidArgumentException) { } } if (!$extension) { Craft::warning('Could not determine file extension for user photo.', __METHOD__); return; } $fileLocation = Assets::tempFilePath($extension); $data = base64_decode($matches['data']); FileHelper::writeToFile($fileLocation, $data); $newPhoto = true; } } if ($newPhoto) { try { $users->saveUserPhoto($fileLocation, $user, $filename); } catch (Throwable $e) { if (file_exists($fileLocation)) { FileHelper::unlink($fileLocation); } throw $e; } } } /** * Saves new permissions on the user * * @param User $user * @param User $currentUser * @throws ForbiddenHttpException if the user account doesn't have permission to assign the attempted permissions */ private function _saveUserPermissions(User $user, User $currentUser): void { if (!$currentUser->can('assignUserPermissions')) { return; } // Save any user permissions if ($user->admin) { $permissions = []; } else { $permissions = $this->request->getBodyParam('permissions'); if ($permissions === null) { return; } // it will be an empty string if no permissions were assigned during user saving. if ($permissions === '') { $permissions = []; } } // See if there are any new permissions in here $hasNewPermissions = false; foreach ($permissions as $permission) { if (!$user->can($permission)) { $hasNewPermissions = true; // Make sure the current user even has permission to grant it if (!$currentUser->can($permission)) { throw new ForbiddenHttpException("Your account doesn't have permission to assign the $permission permission to a user."); } } } if ($hasNewPermissions) { $this->requireElevatedSession(); } Craft::$app->getUserPermissions()->saveUserPermissions($user->id, $permissions); } /** * Saves user groups on a user. * * @param User $user * @param User $currentUser * @throws ForbiddenHttpException if the user account doesn't have permission to assign the attempted groups */ private function _saveUserGroups(User $user, User $currentUser): void { $groupIds = $this->request->getBodyParam('groups'); if ($groupIds === null) { return; } if ($groupIds === '') { $groupIds = []; } /** @var UserGroup[] $allGroups */ $allGroups = ArrayHelper::index(Craft::$app->getUserGroups()->getAllGroups(), 'id'); // See if there are any new groups in here $oldGroupIds = array_map(fn(UserGroup $group) => $group->id, $user->getGroups()); $hasNewGroups = false; $newGroups = []; foreach ($groupIds as $groupId) { $group = $newGroups[] = $allGroups[$groupId]; if (!in_array($groupId, $oldGroupIds, false)) { $hasNewGroups = true; // Make sure the current user is in the group or has permission to assign it if (!$currentUser->can("assignUserGroup:$group->uid")) { throw new ForbiddenHttpException("Your account doesn't have permission to assign user group “{$group->name}” to a user."); } } } if ($hasNewGroups) { $this->requireElevatedSession(); } Craft::$app->getUsers()->assignUserToGroups($user->id, $groupIds); $user->setGroups($newGroups); } /** * @return array|Response */ private function _processTokenRequest(): Response|array { $uid = $this->request->getRequiredParam('id'); $code = $this->request->getRequiredParam('code'); /** @var User|null $user */ $user = User::find() ->uid($uid) ->status(null) ->addSelect(['users.password']) ->one(); if (!$user) { return $this->_processInvalidToken(); } // If someone is logged in and it’s not this person, log them out $userSession = Craft::$app->getUser(); if (!$userSession->getIsGuest() && $userSession->getId() != $user->id) { $userSession->logout(); } // Fire a 'beforeVerifyUser' event $usersService = Craft::$app->getUsers(); if ($usersService->hasEventHandlers(Users::EVENT_BEFORE_VERIFY_EMAIL)) { $usersService->trigger(Users::EVENT_BEFORE_VERIFY_EMAIL, new UserEvent([ 'user' => $user, ])); } if (!Craft::$app->getUsers()->isVerificationCodeValidForUser($user, $code)) { return $this->_processInvalidToken($user); } // Fire an 'afterVerifyUser' event if ($usersService->hasEventHandlers(Users::EVENT_AFTER_VERIFY_EMAIL)) { $usersService->trigger(Users::EVENT_AFTER_VERIFY_EMAIL, new UserEvent([ 'user' => $user, ])); } return [$user, $uid, $code]; } /** * @param User|null $user * @return Response * @throws HttpException if the verification code is invalid */ private function _processInvalidToken(?User $user = null): Response { $this->trigger(self::EVENT_INVALID_USER_TOKEN, new InvalidUserTokenEvent([ 'user' => $user, ])); if ($this->request->getAcceptsJson()) { return $this->asFailure('InvalidVerificationCode'); } // If they don't have a verification code at all, and they're already logged-in, just send them to the post-login URL if ($user && !$user->verificationCode) { $userSession = Craft::$app->getUser(); if (!$userSession->getIsGuest()) { $returnUrl = $userSession->getReturnUrl(); $userSession->removeReturnUrl(); return $this->redirect($returnUrl); } } // If the invalidUserTokenPath config setting is set, send them there if ($this->request->getIsSiteRequest()) { $url = Craft::$app->getConfig()->getGeneral()->getInvalidUserTokenPath(); return $this->redirect(UrlHelper::siteUrl($url)); } throw new BadRequestHttpException(Craft::t('app', 'Invalid verification code. Please sign in or reset your password.')); } /** * Takes over after a user has been activated. * * @param User $user The user that was just activated * @return Response|null */ private function _onAfterActivateUser(User $user): ?Response { $this->_maybeLoginUserAfterAccountActivation($user); if (!$this->request->getAcceptsJson()) { return $this->_redirectUserToCp($user) ?? $this->_redirectUserAfterAccountActivation($user); } return null; } /** * Possibly log a user in right after they were activated or reset their password, if Craft is configured to do so. * * @param User $user The user that was just activated or reset their password * @return bool Whether the user was logged in */ private function _maybeLoginUserAfterAccountActivation(User $user): bool { $generalConfig = Craft::$app->getConfig()->getGeneral(); if (!$generalConfig->autoLoginAfterAccountActivation) { return false; } return Craft::$app->getUser()->login($user, $generalConfig->userSessionDuration); } /** * Redirects a user to the `postCpLoginRedirect` location, if they have access to the control panel. * * @param User $user The user to redirect * @return Response|null */ private function _redirectUserToCp(User $user): ?Response { // Can they access the control panel? if ($user->can('accessCp')) { $postCpLoginRedirect = Craft::$app->getConfig()->getGeneral()->getPostCpLoginRedirect(); $url = UrlHelper::cpUrl($postCpLoginRedirect); return $this->redirect($url); } return null; } /** * Redirect the browser after a user’s account has been activated. * * @param User $user The user that was just activated * @return Response */ private function _redirectUserAfterAccountActivation(User $user): Response { $activateAccountSuccessPath = Craft::$app->getConfig()->getGeneral()->getActivateAccountSuccessPath(); $url = UrlHelper::siteUrl($activateAccountSuccessPath); return $this->redirectToPostedUrl($user, $url); } /** * Redirect the browser after a user has verified their new email address * * @param User $user The user that just verified their email * @return Response */ private function _redirectUserAfterEmailVerification(User $user): Response { $verifyEmailSuccessPath = Craft::$app->getConfig()->getGeneral()->getVerifyEmailSuccessPath(); $url = UrlHelper::siteUrl($verifyEmailSuccessPath); return $this->redirectToPostedUrl($user, $url); } /** * @param string[] $errors * @param string|null $loginName * @return Response|null * @throws \Exception */ private function _handleSendPasswordResetError(array $errors, ?string $loginName = null): ?Response { $errorString = implode(', ', $errors); return $this->asFailure( $errorString, [ 'errors' => $errors, ], [ 'loginName' => $loginName, 'errors' => $errors, ] ); } /** * Renders the user photo template. * * @param User $user * @return string The rendered HTML */ private function _renderPhotoTemplate(User $user): string { $view = $this->getView(); $templateMode = $view->getTemplateMode(); if ($templateMode === View::TEMPLATE_MODE_SITE && !$view->doesTemplateExist('users/_photo.twig')) { $templateMode = View::TEMPLATE_MODE_CP; } return $view->renderTemplate('users/_photo.twig', [ 'user' => $user, ], $templateMode); } private function _hashCheck() { Craft::$app->getSecurity()->validatePassword('p@ss1w0rd', '$2y$13$nj9aiBeb7RfEfYP3Cum6Revyu14QelGGxwcnFUKXIrQUitSodEPRi'); } private function _randomlyDelayResponse(float $maxOffset = 0) { // Delay randomly between 0.5 and 1.5 seconds. $max = 1500000 - (int)($maxOffset * 1000000); if ($max > 500000) { usleep(random_int(500000, $max)); } } /** * Marks the user’s feature announcements as read. * * @return Response */ public function actionMarkAnnouncementsAsRead(): Response { $this->requirePostRequest(); $ids = $this->request->getRequiredBodyParam('ids'); Craft::$app->getAnnouncements()->markAsRead($ids); return $this->asSuccess(); } private function populateNameAttributes(object $model): void { /** @var object|NameTrait $model */ $fullName = $this->request->getBodyParam('fullName'); if ($fullName !== null) { $model->fullName = $fullName; // Unset firstName and lastName so NameTrait::prepareNamesForSave() can set them $model->firstName = $model->lastName = null; } else { // Still check for firstName/lastName in case a front-end form is still posting them $firstName = $this->request->getBodyParam('firstName'); $lastName = $this->request->getBodyParam('lastName'); if ($firstName !== null || $lastName !== null) { $model->firstName = $firstName ?? $model->firstName; $model->lastName = $lastName ?? $model->lastName; // Unset fullName so NameTrait::prepareNamesForSave() can set it $model->fullName = null; } } } public function asModelSuccess( ModelInterface|Model $model, ?string $message = null, ?string $modelName = null, array $data = [], ?string $redirect = null, ): Response { $this->clearPassword($model); return parent::asModelSuccess($model, $message, $modelName, $data, $redirect); } public function asModelFailure( ModelInterface|Model $model, ?string $message = null, ?string $modelName = null, array $data = [], array $routeParams = [], ): ?Response { $this->clearPassword($model); return parent::asModelFailure($model, $message, $modelName, $data, $routeParams); } private function clearPassword(ModelInterface|Model $model): void { if ($model instanceof User) { $model->password = null; $model->newPassword = null; $model->currentPassword = null; } } }
/srv/users/craft4/apps/craft4-newsite-space/vendor/craftcms/cms/src/controllers/UsersController.php