diff --git a/src/Api/Jobs/EditPostJob.php b/src/Api/Jobs/EditPostJob.php index ee3af857..b08ca696 100644 --- a/src/Api/Jobs/EditPostJob.php +++ b/src/Api/Jobs/EditPostJob.php @@ -5,6 +5,8 @@ class EditPostJob extends AbstractPostJob { $post = $this->post; + LogHelper::bufferChanges(); + $subJobs = [ new EditPostSafetyJob(), @@ -29,6 +31,7 @@ class EditPostJob extends AbstractPostJob } } + LogHelper::flush(); return $post; } } diff --git a/src/Api/Jobs/EditUserAccessRankJob.php b/src/Api/Jobs/EditUserAccessRankJob.php new file mode 100644 index 00000000..a94974c1 --- /dev/null +++ b/src/Api/Jobs/EditUserAccessRankJob.php @@ -0,0 +1,35 @@ +user; + $newAccessRank = UserModel::validateAccessRank($this->getArgument(self::NEW_ACCESS_RANK)); + + $oldAccessRank = $user->accessRank; + if ($oldAccessRank == $newAccessRank) + return $user; + + $user->accessRank = $newAccessRank; + + UserModel::save($user); + + LogHelper::log('{user} changed {subject}\'s access rank to {rank}', [ + 'user' => TextHelper::reprUser(Auth::getCurrentUser()), + 'subject' => TextHelper::reprUser($user), + 'rank' => AccessRank::toString($newAccessRank)]); + + return $user; + } + + public function requiresPrivilege() + { + return + [ + Privilege::ChangeUserEmail, + Access::getIdentity($this->user), + ]; + } +} diff --git a/src/Api/Jobs/EditUserEmailJob.php b/src/Api/Jobs/EditUserEmailJob.php new file mode 100644 index 00000000..fb71b718 --- /dev/null +++ b/src/Api/Jobs/EditUserEmailJob.php @@ -0,0 +1,74 @@ +user; + $newEmail = UserModel::validateEmail($this->getArgument(self::NEW_EMAIL)); + + $oldEmail = $user->emailConfirmed; + if ($oldEmail == $newEmail) + return $user; + + if (Auth::getCurrentUser()->id == $user->id) + { + $user->emailUnconfirmed = $newEmail; + $user->emailConfirmed = null; + + if (!empty($newEmail)) + { + $this->sendEmail($user); + } + } + else + { + $user->emailUnconfirmed = null; + $user->emailConfirmed = $newEmail; + } + + UserModel::save($user); + + LogHelper::log('{user} changed {subject}\'s e-mail to {mail}', [ + 'user' => TextHelper::reprUser(Auth::getCurrentUser()), + 'subject' => TextHelper::reprUser($user), + 'mail' => $newEmail]); + + return $user; + } + + public function requiresPrivilege() + { + return + [ + Privilege::ChangeUserAccessRank, + Access::getIdentity($this->user), + ]; + } + + //todo: change to private once finished refactors to UserController + public function sendEmail($user) + { + $regConfig = getConfig()->registration; + + if (!$regConfig->confirmationEmailEnabled) + { + $user->emailUnconfirmed = null; + $user->emailConfirmed = $user->emailUnconfirmed; + return; + } + + $mail = new Mail(); + $mail->body = $regConfig->confirmationEmailBody; + $mail->subject = $regConfig->confirmationEmailSubject; + $mail->senderName = $regConfig->confirmationEmailSenderName; + $mail->senderEmail = $regConfig->confirmationEmailSenderEmail; + $mail->recipientEmail = $user->emailUnconfirmed; + + return Mailer::sendMailWithTokenLink( + $user, + ['UserController', 'activationAction'], + $mail); + } +} diff --git a/src/Api/Jobs/EditUserJob.php b/src/Api/Jobs/EditUserJob.php new file mode 100644 index 00000000..d180db82 --- /dev/null +++ b/src/Api/Jobs/EditUserJob.php @@ -0,0 +1,34 @@ +user; + + LogHelper::bufferChanges(); + + $subJobs = + [ + new EditUserNameJob(), + new EditUserPasswordJob(), + new EditUserEmailJob(), + new EditUserAccessRankJob(), + ]; + + foreach ($subJobs as $subJob) + { + $args = $this->getArguments(); + $args[self::USER_NAME] = $user->name; + try + { + Api::run($subJob, $args); + } + catch (ApiMissingArgumentException $e) + { + } + } + + LogHelper::flush(); + return $user; + } +} diff --git a/src/Api/Jobs/EditUserNameJob.php b/src/Api/Jobs/EditUserNameJob.php new file mode 100644 index 00000000..81553550 --- /dev/null +++ b/src/Api/Jobs/EditUserNameJob.php @@ -0,0 +1,35 @@ +user; + $newName = UserModel::validateUserName($this->getArgument(self::NEW_USER_NAME)); + + $oldName = $user->name; + if ($oldName == $newName) + return $user; + + $user->name = $newName; + + UserModel::save($user); + + LogHelper::log('{user} renamed {old} to {new}', [ + 'user' => TextHelper::reprUser(Auth::getCurrentUser()), + 'old' => TextHelper::reprUser($oldName), + 'new' => TextHelper::reprUser($newName)]); + + return $user; + } + + public function requiresPrivilege() + { + return + [ + Privilege::ChangeUserName, + Access::getIdentity($this->user), + ]; + } +} diff --git a/src/Api/Jobs/EditUserPasswordJob.php b/src/Api/Jobs/EditUserPasswordJob.php new file mode 100644 index 00000000..d2c59d1b --- /dev/null +++ b/src/Api/Jobs/EditUserPasswordJob.php @@ -0,0 +1,35 @@ +user; + $newPassword = UserModel::validatePassword($this->getArgument(self::NEW_PASSWORD)); + + $newPasswordHash = UserModel::hashPassword($newPassword, $user->passSalt); + $oldPasswordHash = $user->passHash; + if ($oldPasswordHash == $newPasswordHash) + return $user; + + $user->passHash = $newPasswordHash; + + UserModel::save($user); + + LogHelper::log('{user} changed {subject}\'s password', [ + 'user' => TextHelper::reprUser(Auth::getCurrentUser()), + 'subject' => TextHelper::reprUser($user)]); + + return $user; + } + + public function requiresPrivilege() + { + return + [ + Privilege::ChangeUserPassword, + Access::getIdentity($this->user), + ]; + } +} diff --git a/src/Controllers/UserController.php b/src/Controllers/UserController.php index d8f02691..733ac03c 100644 --- a/src/Controllers/UserController.php +++ b/src/Controllers/UserController.php @@ -87,92 +87,26 @@ class UserController $this->genericView($name, 'edit'); $this->requirePasswordConfirmation(); - $user = getContext()->transport->user; + if (InputHelper::get('password1') != InputHelper::get('password2')) + throw new SimpleException('Specified passwords must be the same'); - $suppliedCurrentPassword = InputHelper::get('current-password'); - $suppliedName = InputHelper::get('name'); - $suppliedPassword1 = InputHelper::get('password1'); - $suppliedPassword2 = InputHelper::get('password2'); - $suppliedEmail = InputHelper::get('email'); - $suppliedAccessRank = InputHelper::get('access-rank'); + $args = + [ + EditUserNameJob::USER_NAME => $name, + EditUserNameJob::NEW_USER_NAME => InputHelper::get('name'), + EditUserPasswordJob::NEW_PASSWORD => InputHelper::get('password1'), + EditUserEmailJob::NEW_EMAIL => InputHelper::get('email'), + EditUserAccessRankJob::NEW_ACCESS_RANK => InputHelper::get('access-rank'), + ]; - $confirmMail = false; - LogHelper::bufferChanges(); + $args = array_filter($args); + $user = Api::run(new EditUserJob(), $args); - if ($suppliedName != '' and $suppliedName != $user->name) - { - Access::assert( - Privilege::ChangeUserName, - Access::getIdentity($user)); - - $suppliedName = UserModel::validateUserName($suppliedName); - $oldName = $user->name; - $user->name = $suppliedName; - LogHelper::log('{user} renamed {old} to {new}', [ - 'old' => TextHelper::reprUser($oldName), - 'new' => TextHelper::reprUser($suppliedName)]); - } - - if ($suppliedPassword1 != '') - { - Access::assert( - Privilege::ChangeUserPassword, - Access::getIdentity($user)); - - if ($suppliedPassword1 != $suppliedPassword2) - throw new SimpleException('Specified passwords must be the same'); - $suppliedPassword = UserModel::validatePassword($suppliedPassword1); - $user->passHash = UserModel::hashPassword($suppliedPassword, $user->passSalt); - LogHelper::log('{user} changed {subject}\'s password', ['subject' => TextHelper::reprUser($user)]); - } - - if ($suppliedEmail != '' and $suppliedEmail != $user->emailConfirmed) - { - Access::assert( - Privilege::ChangeUserEmail, - Access::getIdentity($user)); - - $suppliedEmail = UserModel::validateEmail($suppliedEmail); - if (Auth::getCurrentUser()->id == $user->id) - { - $user->emailUnconfirmed = $suppliedEmail; - if (!empty($user->emailUnconfirmed)) - $confirmMail = true; - LogHelper::log('{user} changed e-mail to {mail}', ['mail' => $suppliedEmail]); - } - else - { - $user->emailUnconfirmed = null; - $user->emailConfirmed = $suppliedEmail; - LogHelper::log('{user} changed {subject}\'s e-mail to {mail}', [ - 'subject' => TextHelper::reprUser($user), - 'mail' => $suppliedEmail]); - } - } - - if ($suppliedAccessRank != '' and $suppliedAccessRank != $user->accessRank) - { - Access::assert( - Privilege::ChangeUserAccessRank, - Access::getIdentity($user)); - - $suppliedAccessRank = UserModel::validateAccessRank($suppliedAccessRank); - $user->accessRank = $suppliedAccessRank; - LogHelper::log('{user} changed {subject}\'s access rank to {rank}', [ - 'subject' => TextHelper::reprUser($user), - 'rank' => AccessRank::toString($suppliedAccessRank)]); - } - - if ($confirmMail) - self::sendEmailChangeConfirmation($user); - - UserModel::save($user); if (Auth::getCurrentUser()->id == $user->id) Auth::setCurrentUser($user); - LogHelper::flush(); $message = 'Account settings updated!'; - if ($confirmMail) + if (Mailer::getMailCounter() > 0) $message .= ' You will be sent an e-mail address confirmation message soon.'; Messenger::message($message); @@ -297,10 +231,10 @@ class UserController UserModel::save($dbUser); if (!empty($dbUser->emailUnconfirmed)) - self::sendEmailChangeConfirmation($dbUser); + EditUserEmailJob::sendEmail($dbUser); $message = 'Congratulations, your account was created.'; - if (!empty($context->mailSent)) + if (Mailer::getMailCounter() > 0) { $message .= ' Please wait for activation e-mail.'; if (getConfig()->registration->staffActivation) @@ -410,97 +344,25 @@ class UserController else throw new SimpleException('This user has no e-mail specified; activation cannot proceed'); } - self::sendEmailChangeConfirmation($user); + EditUserEmailJob::sendEmail($user); Messenger::message('Activation e-mail resent.'); } - private static function sendTokenizedEmail( - $user, - $body, - $subject, - $senderName, - $senderEmail, - $recipientEmail, - $linkActionName) - { - //prepare unique user token - $token = TokenModel::spawn(); - $token->setUser($user); - $token->token = TokenModel::forgeUnusedToken(); - $token->used = false; - $token->expires = null; - TokenModel::save($token); - - getContext()->mailSent = true; - $tokens = []; - $tokens['host'] = $_SERVER['HTTP_HOST']; - $tokens['token'] = $token->token; //gosh this code looks so silly - $tokens['nl'] = PHP_EOL; - if ($linkActionName !== null) - $tokens['link'] = \Chibi\Router::linkTo(['UserController', $linkActionName], ['token' => $token->token]); - - $body = wordwrap(TextHelper::replaceTokens($body, $tokens), 70); - $subject = TextHelper::replaceTokens($subject, $tokens); - $senderName = TextHelper::replaceTokens($senderName, $tokens); - $senderEmail = TextHelper::replaceTokens($senderEmail, $tokens); - - if (empty($recipientEmail)) - throw new SimpleException('Destination e-mail address was not found'); - - $messageId = $_SERVER['REQUEST_TIME'] . md5($_SERVER['REQUEST_TIME']) . '@' . $_SERVER['HTTP_HOST']; - - $headers = []; - $headers []= sprintf('MIME-Version: 1.0'); - $headers []= sprintf('Content-Transfer-Encoding: 7bit'); - $headers []= sprintf('Date: %s', date('r', $_SERVER['REQUEST_TIME'])); - $headers []= sprintf('Message-ID: <%s>', $messageId); - $headers []= sprintf('From: %s <%s>', $senderName, $senderEmail); - $headers []= sprintf('Reply-To: %s', $senderEmail); - $headers []= sprintf('Return-Path: %s', $senderEmail); - $headers []= sprintf('Subject: %s', $subject); - $headers []= sprintf('Content-Type: text/plain; charset=utf-8', $subject); - $headers []= sprintf('X-Mailer: PHP/%s', phpversion()); - $headers []= sprintf('X-Originating-IP: %s', $_SERVER['SERVER_ADDR']); - $encodedSubject = '=?UTF-8?B?' . base64_encode($subject) . '?='; - mail($recipientEmail, $encodedSubject, $body, implode("\r\n", $headers), '-f' . $senderEmail); - - LogHelper::log('Sending e-mail with subject "{subject}" to {mail}', [ - 'subject' => $subject, - 'mail' => $recipientEmail]); - } - - private static function sendEmailChangeConfirmation($user) - { - $regConfig = getConfig()->registration; - if (!$regConfig->confirmationEmailEnabled) - { - $user->emailConfirmed = $user->emailUnconfirmed; - $user->emailUnconfirmed = null; - return; - } - - return self::sendTokenizedEmail( - $user, - $regConfig->confirmationEmailBody, - $regConfig->confirmationEmailSubject, - $regConfig->confirmationEmailSenderName, - $regConfig->confirmationEmailSenderEmail, - $user->emailUnconfirmed, - 'activationAction'); - } - private static function sendPasswordResetConfirmation($user) { $regConfig = getConfig()->registration; - return self::sendTokenizedEmail( + $mail = new Mail(); + $mail->body = $regConfig->passwordResetEmailBody; + $mail->subject = $regConfig->passwordResetEmailSubject; + $mail->senderName = $regConfig->passwordResetEmailSenderName; + $mail->senderEmail = $regConfig->passwordResetEmailSenderEmail; + $mail->recipientEmail = $user->emailConfirmed; + + return Mailer::sendMailWithTokenLink( $user, - $regConfig->passwordResetEmailBody, - $regConfig->passwordResetEmailSubject, - $regConfig->passwordResetEmailSenderName, - $regConfig->passwordResetEmailSenderEmail, - $user->emailConfirmed, - 'passwordResetAction'); + ['UserController', 'passwordResetAction'], + $mail); } private function requirePasswordConfirmation() diff --git a/src/Mail.php b/src/Mail.php new file mode 100644 index 00000000..503ffee7 --- /dev/null +++ b/src/Mail.php @@ -0,0 +1,9 @@ +body, $tokens), 70); + $subject = TextHelper::replaceTokens($mail->subject, $tokens); + $senderName = TextHelper::replaceTokens($mail->senderName, $tokens); + $senderEmail = TextHelper::replaceTokens($mail->senderEmail, $tokens); + $recipientEmail = $mail->recipientEmail; + + if (empty($recipientEmail)) + throw new SimpleException('Destination e-mail address was not found'); + + $messageId = $_SERVER['REQUEST_TIME'] . md5($_SERVER['REQUEST_TIME']) . '@' . $_SERVER['HTTP_HOST']; + + $headers = []; + $headers []= sprintf('MIME-Version: 1.0'); + $headers []= sprintf('Content-Transfer-Encoding: 7bit'); + $headers []= sprintf('Date: %s', date('r', $_SERVER['REQUEST_TIME'])); + $headers []= sprintf('Message-ID: <%s>', $messageId); + $headers []= sprintf('From: %s <%s>', $senderName, $senderEmail); + $headers []= sprintf('Reply-To: %s', $senderEmail); + $headers []= sprintf('Return-Path: %s', $senderEmail); + $headers []= sprintf('Subject: %s', $subject); + $headers []= sprintf('Content-Type: text/plain; charset=utf-8', $subject); + $headers []= sprintf('X-Mailer: PHP/%s', phpversion()); + $headers []= sprintf('X-Originating-IP: %s', $_SERVER['SERVER_ADDR']); + $encodedSubject = '=?UTF-8?B?' . base64_encode($subject) . '?='; + mail($recipientEmail, $encodedSubject, $body, implode("\r\n", $headers), '-f' . $senderEmail); + + self::$mailCounter ++; + + LogHelper::log('Sending e-mail with subject "{subject}" to {mail}', [ + 'subject' => $subject, + 'mail' => $recipientEmail]); + } + + public static function sendMailWithTokenLink( + UserEntity $user, + $linkDestination, + Mail $mail, + array $tokens = []) + { + //prepare unique user token + $token = TokenModel::spawn(); + $token->setUser($user); + $token->token = TokenModel::forgeUnusedToken(); + $token->used = false; + $token->expires = null; + TokenModel::save($token); + + $tokens['link'] = \Chibi\Router::linkTo($linkDestination, ['token' => $token->token]); + + return self::sendMail($mail, $tokens); + } +}