diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 3176e5e5..b14ea64b 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -48,24 +48,6 @@ parameters: count: 1 path: src/bundle/Controller/PasswordChangeController.php - - - message: '#^Method Ibexa\\Bundle\\User\\Controller\\PasswordResetController\:\:sendNotification\(\) has parameter \$user with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: src/bundle/Controller/PasswordResetController.php - - - - message: '#^Parameter \#1 \$user of method Ibexa\\Bundle\\User\\Controller\\PasswordResetController\:\:sendResetPasswordMessage\(\) expects Ibexa\\Contracts\\Core\\Repository\\Values\\User\\User, Ibexa\\Contracts\\Core\\Repository\\Values\\User\\User\|false given\.$#' - identifier: argument.type - count: 1 - path: src/bundle/Controller/PasswordResetController.php - - - - message: '#^Parameter \#1 \$user of method Ibexa\\Bundle\\User\\Controller\\PasswordResetController\:\:updateUserToken\(\) expects Ibexa\\Contracts\\Core\\Repository\\Values\\User\\User, Ibexa\\Contracts\\Core\\Repository\\Values\\User\\User\|false given\.$#' - identifier: argument.type - count: 1 - path: src/bundle/Controller/PasswordResetController.php - - message: '#^Parameter \#1 \$email of class Ibexa\\Contracts\\User\\Invitation\\InvitationCreateStruct constructor expects string, string\|null given\.$#' identifier: argument.type @@ -1284,12 +1266,6 @@ parameters: count: 1 path: src/lib/Validator/Constraints/EmailInvitationValidator.php - - - message: '#^Method Ibexa\\User\\Validator\\Constraints\\Password\:\:getTargets\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/lib/Validator/Constraints/Password.php - - message: '#^Access to an undefined property Symfony\\Component\\Validator\\Constraint\:\:\$contentType\.$#' identifier: property.notFound diff --git a/src/bundle/Controller/PasswordResetController.php b/src/bundle/Controller/PasswordResetController.php index 01edfdac..933ee9e4 100644 --- a/src/bundle/Controller/PasswordResetController.php +++ b/src/bundle/Controller/PasswordResetController.php @@ -17,11 +17,7 @@ use Ibexa\Contracts\Core\Repository\Values\User\User; use Ibexa\Contracts\Core\Repository\Values\User\UserTokenUpdateStruct; use Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface; -use Ibexa\Contracts\Notifications\Service\NotificationServiceInterface; -use Ibexa\Contracts\Notifications\Value\Notification\SymfonyNotificationAdapter; -use Ibexa\Contracts\Notifications\Value\Recipent\SymfonyRecipientAdapter; -use Ibexa\Contracts\Notifications\Value\Recipent\UserRecipient; -use Ibexa\Contracts\User\Notification\UserPasswordReset; +use Ibexa\Contracts\User\PasswordReset\NotifierInterface; use Ibexa\User\ExceptionHandler\ActionResultHandler; use Ibexa\User\Form\Data\UserPasswordResetData; use Ibexa\User\Form\Factory\FormFactory; @@ -31,11 +27,8 @@ use Ibexa\User\View\ResetPassword\FormView as UserResetPasswordFormView; use Ibexa\User\View\ResetPassword\InvalidLinkView; use Ibexa\User\View\ResetPassword\SuccessView as UserResetPasswordSuccessView; -use Swift_Mailer; -use Swift_Message; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Twig\Environment; class PasswordResetController extends Controller { @@ -43,36 +36,28 @@ class PasswordResetController extends Controller private UserService $userService; - private Swift_Mailer $mailer; - - private Environment $twig; - private ActionResultHandler $actionResultHandler; private PermissionResolver $permissionResolver; private ConfigResolverInterface $configResolver; - private NotificationServiceInterface $notificationService; + private NotifierInterface $passwordResetMailer; public function __construct( FormFactory $formFactory, UserService $userService, - Swift_Mailer $mailer, - Environment $twig, ActionResultHandler $actionResultHandler, PermissionResolver $permissionResolver, ConfigResolverInterface $configResolver, - NotificationServiceInterface $notificationService + NotifierInterface $passwordResetMailer ) { $this->formFactory = $formFactory; $this->userService = $userService; - $this->mailer = $mailer; - $this->twig = $twig; $this->actionResultHandler = $actionResultHandler; $this->permissionResolver = $permissionResolver; $this->configResolver = $configResolver; - $this->notificationService = $notificationService; + $this->passwordResetMailer = $passwordResetMailer; } /** @@ -89,16 +74,17 @@ public function userForgotPasswordAction(Request $request, ?string $reason = nul $data = $form->getData(); $users = $this->userService->loadUsersByEmail($data->getEmail()); - /** Because is is possible to have multiple user accounts with same email address we must gain a user login. */ + /** Because it is possible to have multiple user accounts with same email address we must gain a user login. */ if (\count($users) > 1) { return $this->redirectToRoute('ibexa.user.forgot_password.login'); } if (!empty($users)) { + /** @var \Ibexa\Contracts\Core\Repository\Values\User\User $user */ $user = reset($users); $token = $this->updateUserToken($user); - $this->sendResetPasswordMessage($user, $token); + $this->passwordResetMailer->sendMessage($user, $token); } return new SuccessView(null); @@ -138,7 +124,7 @@ public function userForgotPasswordLoginAction(Request $request) } $token = $this->updateUserToken($user); - $this->sendResetPasswordMessage($user, $token); + $this->passwordResetMailer->sendMessage($user, $token); return new SuccessView(null); } @@ -231,55 +217,6 @@ private function updateUserToken(User $user): string return $struct->hashKey; } - - private function sendResetPasswordMessage(User $user, string $hashKey): void - { - if ($this->isNotifierConfigured()) { - $this->sendNotification($user, $hashKey); - - return; - } - - // Swiftmailer delivery has to be kept to maintain backwards compatibility - $template = $this->twig->load($this->configResolver->getParameter('user_forgot_password.templates.mail')); - - $senderAddress = $this->configResolver->hasParameter('sender_address', 'swiftmailer.mailer') - ? $this->configResolver->getParameter('sender_address', 'swiftmailer.mailer') - : ''; - - $subject = $template->renderBlock('subject', []); - $from = $template->renderBlock('from', []) ?: $senderAddress; - $body = $template->renderBlock('body', ['hash_key' => $hashKey]); - - $message = (new Swift_Message()) - ->setSubject($subject) - ->setTo($user->email) - ->setBody($body, 'text/html'); - - if (empty($from) === false) { - $message->setFrom($from); - } - - $this->mailer->send($message); - } - - private function sendNotification($user, string $token): void - { - $this->notificationService->send( - new SymfonyNotificationAdapter( - new UserPasswordReset($user, $token), - ), - [new SymfonyRecipientAdapter(new UserRecipient($user))], - ); - } - - private function isNotifierConfigured(): bool - { - $subscriptions = $this->configResolver->getParameter('notifications.subscriptions'); - - return array_key_exists(UserPasswordReset::class, $subscriptions) - && !empty($subscriptions[UserPasswordReset::class]['channels']); - } } class_alias(PasswordResetController::class, 'EzSystems\EzPlatformUserBundle\Controller\PasswordResetController'); diff --git a/src/bundle/Resources/config/services.yaml b/src/bundle/Resources/config/services.yaml index c7a85e47..6ba35eda 100644 --- a/src/bundle/Resources/config/services.yaml +++ b/src/bundle/Resources/config/services.yaml @@ -7,6 +7,7 @@ imports: - { resource: services/invitation.yaml } - { resource: services/installer.yaml } - { resource: services/twig.yaml } + - { resource: services/notifier.yaml } parameters: ibexa.user.content_type_identifier: user diff --git a/src/bundle/Resources/config/services/notifier.yaml b/src/bundle/Resources/config/services/notifier.yaml new file mode 100644 index 00000000..e838fe66 --- /dev/null +++ b/src/bundle/Resources/config/services/notifier.yaml @@ -0,0 +1,9 @@ +services: + _defaults: + autowire: true + autoconfigure: true + public: false + + Ibexa\User\PasswordReset\Notifier: ~ + + Ibexa\Contracts\User\PasswordReset\NotifierInterface: '@Ibexa\User\PasswordReset\Notifier' diff --git a/src/bundle/Resources/translations/ibexa_forgot_password.en.xliff b/src/bundle/Resources/translations/ibexa_forgot_password.en.xliff index a42560e0..8da71ee6 100644 --- a/src/bundle/Resources/translations/ibexa_forgot_password.en.xliff +++ b/src/bundle/Resources/translations/ibexa_forgot_password.en.xliff @@ -16,23 +16,6 @@ This email is connected with several accounts. Enter your username instead. key: ezplatform.forgot_password.login - -
- We have received a request to reset the password for your account. Click “reset password” below to choose a new password: -

- Reset password -

- If you did not request a password reset, please ignore this email, and your password will remain the same.]]> -
- We have received a request to reset the password for your account. Click “reset password” below to choose a new password: -

-
Reset password -

- If you did not request a password reset, please ignore this email, and your password will remain the same.]]> - key: ezplatform.forgot_password.message -
Reset your password Reset your password @@ -54,6 +37,31 @@

If you reset your password multiple times, only the most recent password reset link will be valid.

]]> key: ezplatform.forgot_password.success
+ + Reset your password + Reset your password + key: forgot_password.reset_your_password + + + We have received a request to reset the password for your account. Click "reset password" below to choose a new password: + We have received a request to reset the password for your account. Click "reset password" below to choose a new password: + key: forgot_user_password.mail.message + + + If you did not request a password reset, please ignore this email, and your password will remain the same. + If you did not request a password reset, please ignore this email, and your password will remain the same. + key: forgot_user_password.mail.message_footer + + + Hello, + Hello, + key: forgot_user_password.mail.message_title + + + Reset password + Reset password + key: forgot_user_password.mail.reset_password + diff --git a/src/bundle/Resources/translations/ibexa_user_invitation.en.xliff b/src/bundle/Resources/translations/ibexa_user_invitation.en.xliff index 0659726a..9f8bed05 100644 --- a/src/bundle/Resources/translations/ibexa_user_invitation.en.xliff +++ b/src/bundle/Resources/translations/ibexa_user_invitation.en.xliff @@ -6,6 +6,11 @@ The source node in most cases contains the sample message as written by the developer. If it looks like a dot-delimitted string such as "form.label.firstname", then the developer has not provided a default message. + + Join + Join + key: ibexa.user.invitation.mail.join + Hello, Join us at: %invite_link% @@ -15,6 +20,11 @@ key: ibexa.user.invitation.mail.message + + Hello, + Hello, + key: ibexa.user.invitation.mail.message_title + You are invited to join You are invited to join diff --git a/src/bundle/Resources/views/forgot_password/mail/forgot_user_password.html.twig b/src/bundle/Resources/views/forgot_password/mail/forgot_user_password.html.twig index 94fd30dc..1ae5bd77 100644 --- a/src/bundle/Resources/views/forgot_password/mail/forgot_user_password.html.twig +++ b/src/bundle/Resources/views/forgot_password/mail/forgot_user_password.html.twig @@ -1,21 +1,26 @@ -{% trans_default_domain 'ibexa_forgot_password' %} +{% extends '@ibexadesign/ui/mail/base_mail_template.html.twig' %} -{%- block from -%} -{%- endblock from -%} +{% trans_default_domain 'ibexa_forgot_password' %} {%- block subject -%} - {{ 'ezplatform.forgot_password.reset_your_password'|trans|desc('Reset your password') }} + {{ 'forgot_password.reset_your_password'|trans|desc('Reset your password') }} {%- endblock subject -%} -{%- block body -%} -

- {{ 'ezplatform.forgot_password.message'|trans({ '%reset_password%': url('ibexa.user.reset_password', {'hashKey': hash_key}) })|raw - |desc('Hello, -

- We have received a request to reset the password for your account. Click “reset password” below to choose a new password: -

-
Reset password -

- If you did not request a password reset, please ignore this email, and your password will remain the same.') }} -

-{%- endblock body -%} +{%- block mail_message_title_content -%} + {{ 'forgot_user_password.mail.message_title'|trans()|desc('Hello,') }} +{%- endblock mail_message_title_content -%} + +{%- block mail_message_content %} + {{ 'forgot_user_password.mail.message'|trans()|desc('We have received a request to reset the password for your account. Click "reset password" below to choose a new password:') }} +{%- endblock mail_message_content %} + +{%- block mail_actions_content -%} + {% include '@ibexadesign/ui/mail/components/action_btn.html.twig' with { + url: url('ibexa.user.reset_password', {'hashKey': hash_key}), + label: 'forgot_user_password.mail.reset_password'|trans()|desc('Reset password') + } only %} +{%- endblock mail_actions_content -%} + +{%- block mail_footer_content -%} + {{ 'forgot_user_password.mail.message_footer'|trans()|desc('If you did not request a password reset, please ignore this email, and your password will remain the same.') }} +{%- endblock mail_footer_content -%} diff --git a/src/bundle/Resources/views/invitation/mail/user_invitation.html.twig b/src/bundle/Resources/views/invitation/mail/user_invitation.html.twig index 1b446870..d2ec0a2b 100644 --- a/src/bundle/Resources/views/invitation/mail/user_invitation.html.twig +++ b/src/bundle/Resources/views/invitation/mail/user_invitation.html.twig @@ -1,22 +1,27 @@ -{% trans_default_domain 'ibexa_user_invitation' %} +{% extends '@ibexadesign/ui/mail/base_mail_template.html.twig' %} -{%- block from -%} -{%- endblock from -%} +{% trans_default_domain 'ibexa_user_invitation' %} {%- block subject -%} {{ 'ibexa.user.invitation.mail.subject'|trans|desc('You are invited to join') }} {%- endblock subject -%} -{%- block body -%} -

- {{ 'ibexa.user.invitation.mail.message'|trans({ - '%invite_link%': url('ibexa.user.from_invite.register', { - 'inviteHash': invite_hash, - 'siteaccess': siteaccess - }) - })|raw - |desc('Hello, - Join us at: %invite_link% - ') }} -

-{%- endblock body -%} +{%- block mail_message_title_content -%} + {{ 'ibexa.user.invitation.mail.message_title'|trans()|desc('Hello,') }} +{%- endblock mail_message_title_content -%} + +{%- block mail_message_content -%} + {{ 'ibexa.user.invitation.mail.message'|trans()|desc('Join us at:') }} +{%- endblock mail_message_content -%} + +{%- block mail_actions_content -%} + {% include '@ibexadesign/ui/mail/components/action_btn.html.twig' with { + url: url('ibexa.user.from_invite.register', { + 'inviteHash': invite_hash, + 'siteaccess': siteaccess + }), + label: 'ibexa.user.invitation.mail.join'|trans()|desc('Join') + } %} +{%- endblock mail_actions_content -%} + +{%- block mail_footer -%}{%- endblock mail_footer -%} diff --git a/src/contracts/PasswordReset/NotifierInterface.php b/src/contracts/PasswordReset/NotifierInterface.php new file mode 100644 index 00000000..e30f19dc --- /dev/null +++ b/src/contracts/PasswordReset/NotifierInterface.php @@ -0,0 +1,16 @@ + + * @return array */ private function getTemplatesMap(): array { diff --git a/src/lib/Invitation/MailSender.php b/src/lib/Invitation/MailSender.php index 2714ea60..672ca53b 100644 --- a/src/lib/Invitation/MailSender.php +++ b/src/lib/Invitation/MailSender.php @@ -49,16 +49,18 @@ public function sendInvitation(Invitation $invitation): void $subject = $template->renderBlock('subject', []); $from = $template->renderBlock('from', []) ?: $senderAddress; + + $message = (new Swift_Message()) + ->setSubject($subject) + ->setTo($invitation->getEmail()); + $body = $template->renderBlock('body', [ 'invite_hash' => $invitation->getHash(), 'siteaccess' => $invitation->getSiteAccessIdentifier(), 'invitation' => $invitation, ]); - $message = (new Swift_Message()) - ->setSubject($subject) - ->setTo($invitation->getEmail()) - ->setBody($body, 'text/html'); + $message->setBody($body, 'text/html'); if (empty($from) === false) { $message->setFrom($from); diff --git a/src/lib/PasswordReset/Notifier.php b/src/lib/PasswordReset/Notifier.php new file mode 100644 index 00000000..d5e2ab07 --- /dev/null +++ b/src/lib/PasswordReset/Notifier.php @@ -0,0 +1,94 @@ +configResolver = $configResolver; + $this->mailer = $mailer; + $this->twig = $twig; + $this->notificationService = $notificationService; + } + + public function sendMessage(User $user, string $hashKey): void + { + if ($this->isNotifierConfigured()) { + $this->sendNotification($user, $hashKey); + + return; + } + + // Swiftmailer delivery has to be kept to maintain backwards compatibility + $template = $this->twig->load($this->configResolver->getParameter('user_forgot_password.templates.mail')); + + $senderAddress = $this->configResolver->hasParameter('sender_address', 'swiftmailer.mailer') + ? $this->configResolver->getParameter('sender_address', 'swiftmailer.mailer') + : ''; + + $subject = $template->renderBlock('subject'); + $from = $template->renderBlock('from') ?: $senderAddress; + + $message = (new Swift_Message()) + ->setSubject($subject) + ->setTo($user->email); + + $body = $template->renderBlock('body', ['hash_key' => $hashKey]); + $message->setBody($body, 'text/html'); + + if (empty($from) === false) { + $message->setFrom($from); + } + + $this->mailer->send($message); + } + + private function sendNotification(User $user, string $token): void + { + $this->notificationService->send( + new SymfonyNotificationAdapter( + new UserPasswordReset($user, $token), + ), + [new SymfonyRecipientAdapter(new UserRecipient($user))], + ); + } + + private function isNotifierConfigured(): bool + { + $subscriptions = $this->configResolver->getParameter('notifications.subscriptions'); + + return array_key_exists(UserPasswordReset::class, $subscriptions) + && !empty($subscriptions[UserPasswordReset::class]['channels']); + } +}