diff --git a/administrator/components/com_admin/sql/updates/mysql/6.3.0-2026-11-02.sql b/administrator/components/com_admin/sql/updates/mysql/6.3.0-2026-11-02.sql new file mode 100644 index 0000000000000..b420bf4b0cd86 --- /dev/null +++ b/administrator/components/com_admin/sql/updates/mysql/6.3.0-2026-11-02.sql @@ -0,0 +1,2 @@ +INSERT IGNORE INTO `#__mail_templates` (`template_id`, `extension`, `language`, `subject`, `body`, `htmlbody`, `attachments`, `params`) VALUES +('com_users.new_credentials', 'com_users', '', 'COM_USERS_EMAIL_NEW_CREDENTIALS_SUBJECT', 'COM_USERS_EMAIL_NEW_CREDENTIALS_BODY', '', '', '{"tags":["name","sitename","url","username","password","email"]}'); diff --git a/administrator/components/com_admin/sql/updates/postgresql/6.3.0-2026-11-02.sql b/administrator/components/com_admin/sql/updates/postgresql/6.3.0-2026-11-02.sql new file mode 100644 index 0000000000000..5d58b7b636cf2 --- /dev/null +++ b/administrator/components/com_admin/sql/updates/postgresql/6.3.0-2026-11-02.sql @@ -0,0 +1,3 @@ +INSERT INTO "#__mail_templates" ("template_id", "extension", "language", "subject", "body", "htmlbody", "attachments", "params") VALUES +('com_users.new_credentials', 'com_users', '', 'COM_USERS_EMAIL_NEW_CREDENTIALS_SUBJECT', 'COM_USERS_EMAIL_NEW_CREDENTIALS_BODY', '', '', '{"tags":["name","sitename","url","username","password","email"]}') +ON CONFLICT DO NOTHING; diff --git a/administrator/components/com_users/src/Controller/UsersController.php b/administrator/components/com_users/src/Controller/UsersController.php index 5024b3de5bdb8..588d0ceaf86b1 100644 --- a/administrator/components/com_users/src/Controller/UsersController.php +++ b/administrator/components/com_users/src/Controller/UsersController.php @@ -147,6 +147,33 @@ public function activate() $this->setRedirect('index.php?option=com_users&view=users'); } + /** + * Method to generate a new password for the selected users and send it to them by email. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function sendNewPassword(): void + { + $this->checkToken(); + + $ids = array_filter((array) $this->input->get('cid', [], 'int')); + + if (empty($ids)) { + $this->setMessage(Text::_('COM_USERS_USERS_NO_ITEM_SELECTED'), 'warning'); + } else { + try { + $this->getModel()->sendNewPassword($ids, $this->input->getBool('requireReset', false)); + $this->setMessage(Text::plural('COM_USERS_N_USERS_NEW_PASSWORD_SENT', \count($ids))); + } catch (\Exception $exception) { + $this->setMessage($exception->getMessage(), 'error'); + } + } + + $this->setRedirect('index.php?option=com_users&view=users'); + } + /** * Method to get the number of active users * diff --git a/administrator/components/com_users/src/Model/UserModel.php b/administrator/components/com_users/src/Model/UserModel.php index 85c7b9af45b52..e595b133b913f 100644 --- a/administrator/components/com_users/src/Model/UserModel.php +++ b/administrator/components/com_users/src/Model/UserModel.php @@ -11,6 +11,7 @@ namespace Joomla\Component\Users\Administrator\Model; use Joomla\CMS\Access\Access; +use Joomla\CMS\Access\Exception\NotAllowed; use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\Event\User\AfterDeleteEvent; use Joomla\CMS\Event\User\AfterSaveEvent; @@ -22,6 +23,7 @@ use Joomla\CMS\Language\LanguageFactoryAwareTrait; use Joomla\CMS\Language\Multilanguage; use Joomla\CMS\Language\Text; +use Joomla\CMS\Log\Log; use Joomla\CMS\Mail\MailerFactoryAwareInterface; use Joomla\CMS\Mail\MailerFactoryAwareTrait; use Joomla\CMS\Mail\MailTemplate; @@ -29,6 +31,7 @@ use Joomla\CMS\MVC\Model\AdminModel; use Joomla\CMS\Plugin\PluginHelper; use Joomla\CMS\Table\Table; +use Joomla\CMS\Uri\Uri; use Joomla\CMS\User\UserFactoryAwareInterface; use Joomla\CMS\User\UserFactoryAwareTrait; use Joomla\CMS\User\UserHelper; @@ -663,6 +666,105 @@ public function activate(&$pks) return true; } + /** + * Method to generate a new password for user records and send it to the users by email. + * + * @param array &$pks The ids of the users. + * @param bool $requireReset Whether the users have to change the new password after logging in. + * + * @return void + * + * @since __DEPLOY_VERSION__ + * + * @throws NotAllowed If the current user is not allowed to edit users. + * @throws \RuntimeException If a user could not be saved. + */ + public function sendNewPassword(array &$pks, bool $requireReset): void + { + $app = Factory::getApplication(); + $my = $this->getCurrentUser(); + $iAmSuperAdmin = $my->authorise('core.admin'); + $passwordLength = (int) ComponentHelper::getParams('com_users')->get('minimum_length', 12); + + if (!$my->authorise('core.edit', 'com_users')) { + throw new NotAllowed(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED'), 403); + } + + $app->getLanguage()->load('com_users', JPATH_SITE); + + foreach ($pks as $i => $pk) { + if ((int) $pk === (int) $my->id) { + unset($pks[$i]); + $app->enqueueMessage(Text::_('COM_USERS_USERS_ERROR_CANNOT_SEND_NEW_PASSWORD_SELF'), 'error'); + + continue; + } + + // Prevent new password from non-super user for super user accounts + if (!$iAmSuperAdmin && Access::check($pk, 'core.admin')) { + unset($pks[$i]); + $app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED'), 'error'); + + continue; + } + + $user = $this->getUserFactory()->loadUserById((int) $pk); + + if ((int) $user->id !== (int) $pk) { + unset($pks[$i]); + + continue; + } + + $password = UserHelper::genRandomPassword($passwordLength); + $data = ['password' => $password, 'password2' => $password]; + + if (!$user->bind($data)) { + throw new \RuntimeException(Text::sprintf('COM_USERS_USERS_ERROR_NEW_PASSWORD_SAVE_FAILED', $user->username)); + } + + // Set after bind(), which clears requireReset whenever a new password is set + $user->requireReset = (int) $requireReset; + + if (!$user->save()) { + throw new \RuntimeException(Text::sprintf('COM_USERS_USERS_ERROR_NEW_PASSWORD_SAVE_FAILED', $user->username)); + } + + UserHelper::destroyUserSessions($user->id, true); + + $mailer = new MailTemplate( + 'com_users.new_credentials', + $user->getParam('language', $user->getParam('admin_language', $app->getLanguage()->getTag())), + $this->getMailerFactory()->createMailer(), + $this->getLanguageFactory(), + $this->getDatabase() + ); + $mailer->addTemplateData([ + 'name' => $user->name, + 'sitename' => $app->get('sitename'), + 'url' => Uri::root(), + 'username' => $user->username, + 'password' => $password, + 'email' => $user->email, + ]); + $mailer->addUnsafeTags(['username', 'password', 'name', 'email']); + $mailer->addRecipient($user->email, $user->name); + + try { + $sent = $mailer->send(); + } catch (\Exception $exception) { + Log::add($exception->getMessage(), Log::WARNING, 'jerror'); + + $sent = false; + } + + if ($sent !== true) { + unset($pks[$i]); + $app->enqueueMessage(Text::sprintf('COM_USERS_USERS_ERROR_NEW_PASSWORD_MAIL_FAILED', $user->username), 'warning'); + } + } + } + /** * Method to perform batch operations on an item or a set of items. * diff --git a/administrator/components/com_users/src/View/Users/HtmlView.php b/administrator/components/com_users/src/View/Users/HtmlView.php index 31e64fcfa86a8..6bc4aae3e86e0 100644 --- a/administrator/components/com_users/src/View/Users/HtmlView.php +++ b/administrator/components/com_users/src/View/Users/HtmlView.php @@ -142,6 +142,17 @@ protected function addToolbar() $childBar->standardButton('unblock', 'COM_USERS_TOOLBAR_UNBLOCK', 'users.unblock') ->listCheck(true); + if ($canDo->get('core.edit')) { + $childBar->popupButton('key', 'COM_USERS_TOOLBAR_SEND_NEW_PASSWORD') + ->popupType('inline') + ->textHeader(Text::_('COM_USERS_TOOLBAR_SEND_NEW_PASSWORD')) + ->url('#joomla-dialog-newpassword') + ->modalWidth('800px') + ->modalHeight('fit-content') + ->icon('icon-key') + ->listCheck(true); + } + // Add a batch button if ( $user->authorise('core.create', 'com_users') diff --git a/administrator/components/com_users/tmpl/users/default.php b/administrator/components/com_users/tmpl/users/default.php index 3d2c8400f2d9c..e895ac8f08b49 100644 --- a/administrator/components/com_users/tmpl/users/default.php +++ b/administrator/components/com_users/tmpl/users/default.php @@ -218,6 +218,10 @@ ) : ?> + + authorise('core.edit', 'com_users')) : ?> + + filterForm->renderControlFields(); ?> diff --git a/administrator/components/com_users/tmpl/users/default_newpassword_body.php b/administrator/components/com_users/tmpl/users/default_newpassword_body.php new file mode 100644 index 0000000000000..c9d06a897d20f --- /dev/null +++ b/administrator/components/com_users/tmpl/users/default_newpassword_body.php @@ -0,0 +1,32 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\Language\Text; + +/** @var \Joomla\Component\Users\Administrator\View\Users\HtmlView $this */ + +?> + +
+

+
+ + +
+
+
+ + + +
diff --git a/administrator/language/en-GB/com_users.ini b/administrator/language/en-GB/com_users.ini index 1cd983eb0de7f..58e945c496807 100644 --- a/administrator/language/en-GB/com_users.ini +++ b/administrator/language/en-GB/com_users.ini @@ -223,6 +223,8 @@ COM_USERS_MAIL_FIELD_SUBJECT_LABEL="Subject" COM_USERS_MAIL_FIELD_VALUE_ALL_USERS_GROUPS="All Users Groups" COM_USERS_MAIL_MASSMAIL_MAIL_DESC="The "Mass Mail Users" email." COM_USERS_MAIL_MASSMAIL_MAIL_TITLE="Users: Mass Mail Users" +COM_USERS_MAIL_NEW_CREDENTIALS_DESC="Sent to a user when an administrator generates a new password from the users list." +COM_USERS_MAIL_NEW_CREDENTIALS_TITLE="Users: New Password" COM_USERS_MAIL_NO_USERS_COULD_BE_FOUND_IN_THIS_GROUP="No users could be found in this group." COM_USERS_MAIL_ONLY_YOU_COULD_BE_FOUND_IN_THIS_GROUP="You are the only user in this group." COM_USERS_MAIL_PASSWORD_RESET_DESC="Sent to a user by the "Forgot your password?" link eg in a login form." @@ -331,6 +333,9 @@ COM_USERS_N_USERS_ACTIVATED_1="User activated." COM_USERS_N_USERS_BLOCKED="%d Users blocked." COM_USERS_N_USERS_BLOCKED_0="No User blocked." COM_USERS_N_USERS_BLOCKED_1="User blocked." +COM_USERS_N_USERS_NEW_PASSWORD_SENT="New password sent to %d users." +COM_USERS_N_USERS_NEW_PASSWORD_SENT_0="No new password sent." +COM_USERS_N_USERS_NEW_PASSWORD_SENT_1="New password sent to the user." COM_USERS_N_USERS_UNBLOCKED="%d Users unblocked." COM_USERS_N_USERS_UNBLOCKED_0="No User unblocked." COM_USERS_N_USERS_UNBLOCKED_1="User unblocked." @@ -383,15 +388,20 @@ COM_USERS_SUBMENU_USERS="Users" COM_USERS_TOOLBAR_ACTIVATE="Activate" COM_USERS_TOOLBAR_BLOCK="Block" COM_USERS_TOOLBAR_MAIL_SEND_MAIL="Send Email" +COM_USERS_TOOLBAR_SEND_NEW_PASSWORD="Send New Password" COM_USERS_TOOLBAR_UNBLOCK="Unblock" COM_USERS_UNACTIVATED="Unactivated" COM_USERS_USERGROUP_DETAILS="User Group Details" +COM_USERS_USERS_CONFIRM_SEND_NEW_PASSWORD="A new password will be generated for the selected users and sent to them by email. Their current passwords will stop working immediately. Are you sure?" COM_USERS_USERS_ERROR_CANNOT_BLOCK_SELF="You can't block yourself." COM_USERS_USERS_ERROR_CANNOT_DELETE_SELF="You can't delete yourself." COM_USERS_USERS_ERROR_CANNOT_DEMOTE_SELF="You can't remove your own Super User permissions." COM_USERS_USERS_ERROR_CANNOT_EDIT_OWN_GROUP="You can't edit your own user groups. User groups saving was skipped." COM_USERS_USERS_ERROR_CANNOT_REQUIRERESET_SELF="You can't require a Password reset for yourself." COM_USERS_USERS_ERROR_CANNOT_SAVE_ACCOUNT_WITHOUT_GROUPS="You can't save a user account without selecting at least one user group." +COM_USERS_USERS_ERROR_CANNOT_SEND_NEW_PASSWORD_SELF="You can't send a new password to yourself." +COM_USERS_USERS_ERROR_NEW_PASSWORD_MAIL_FAILED="The password of %s was changed, but the email with the new password could not be sent." +COM_USERS_USERS_ERROR_NEW_PASSWORD_SAVE_FAILED="The new password for %s could not be saved." COM_USERS_USERS_MULTIPLE_GROUPS="Multiple Groups" COM_USERS_USERS_NO_ITEM_SELECTED="No Users selected." COM_USERS_USERS_N_ITEMS_DELETED="%d users deleted." diff --git a/installation/sql/mysql/supports.sql b/installation/sql/mysql/supports.sql index f5f28b276cb1f..be1c340b98a59 100644 --- a/installation/sql/mysql/supports.sql +++ b/installation/sql/mysql/supports.sql @@ -419,6 +419,7 @@ INSERT INTO `#__mail_templates` (`template_id`, `extension`, `language`, `subjec ('com_users.massmail.mail', 'com_users', '', 'COM_USERS_MASSMAIL_MAIL_SUBJECT', 'COM_USERS_MASSMAIL_MAIL_BODY', '', '', '{"tags":["subject","body","subjectprefix","bodysuffix"]}'), ('com_users.password_reset', 'com_users', '', 'COM_USERS_EMAIL_PASSWORD_RESET_SUBJECT', 'COM_USERS_EMAIL_PASSWORD_RESET_BODY', '', '', '{"tags":["name","email","sitename","link_text","link_html","token"]}'), ('com_users.reminder', 'com_users','', 'COM_USERS_EMAIL_USERNAME_REMINDER_SUBJECT', 'COM_USERS_EMAIL_USERNAME_REMINDER_BODY', '', '', '{"tags":["name","username","sitename","email","link_text","link_html"]}'), +('com_users.new_credentials', 'com_users', '', 'COM_USERS_EMAIL_NEW_CREDENTIALS_SUBJECT', 'COM_USERS_EMAIL_NEW_CREDENTIALS_BODY', '', '', '{"tags":["name","sitename","url","username","password","email"]}'), ('com_joomlaupdate.update.success', 'com_joomlaupdate', '', 'COM_JOOMLAUPDATE_UPDATE_SUCCESS_MAIL_SUBJECT', 'COM_JOOMLAUPDATE_UPDATE_SUCCESS_MAIL_BODY', '', '', '{"tags":["newversion","oldversion","sitename","url"]}'), ('com_joomlaupdate.update.failed', 'com_joomlaupdate', '', 'COM_JOOMLAUPDATE_UPDATE_FAILED_MAIL_SUBJECT', 'COM_JOOMLAUPDATE_UPDATE_FAILED_MAIL_BODY', '', '', '{"tags":["newversion","oldversion","sitename","url"]}'), ('plg_task_updatenotification.mail', 'plg_task_updatenotification', '', 'PLG_TASK_UPDATENOTIFICATION_EMAIL_SUBJECT', 'PLG_TASK_UPDATENOTIFICATION_EMAIL_BODY', '', '', '{"tags":["newversion","curversion","sitename","url","link","releasenews"]}'), diff --git a/installation/sql/postgresql/supports.sql b/installation/sql/postgresql/supports.sql index c0688fa70a7be..805ae303cd100 100644 --- a/installation/sql/postgresql/supports.sql +++ b/installation/sql/postgresql/supports.sql @@ -429,6 +429,7 @@ INSERT INTO "#__mail_templates" ("template_id", "extension", "language", "subjec ('com_users.massmail.mail', 'com_users', '', 'COM_USERS_MASSMAIL_MAIL_SUBJECT', 'COM_USERS_MASSMAIL_MAIL_BODY', '', '', '{"tags":["subject","body","subjectprefix","bodysuffix"]}'), ('com_users.password_reset', 'com_users', '', 'COM_USERS_EMAIL_PASSWORD_RESET_SUBJECT', 'COM_USERS_EMAIL_PASSWORD_RESET_BODY', '', '', '{"tags":["name","email","sitename","link_text","link_html","token"]}'), ('com_users.reminder', 'com_users', '', 'COM_USERS_EMAIL_USERNAME_REMINDER_SUBJECT', 'COM_USERS_EMAIL_USERNAME_REMINDER_BODY', '', '', '{"tags":["name","username","sitename","email","link_text","link_html"]}'), +('com_users.new_credentials', 'com_users', '', 'COM_USERS_EMAIL_NEW_CREDENTIALS_SUBJECT', 'COM_USERS_EMAIL_NEW_CREDENTIALS_BODY', '', '', '{"tags":["name","sitename","url","username","password","email"]}'), ('com_joomlaupdate.update.success', 'com_joomlaupdate', '', 'COM_JOOMLAUPDATE_UPDATE_SUCCESS_MAIL_SUBJECT', 'COM_JOOMLAUPDATE_UPDATE_SUCCESS_MAIL_BODY', '', '', '{"tags":["newversion","oldversion","sitename","url"]}'), ('com_joomlaupdate.update.failed', 'com_joomlaupdate', '', 'COM_JOOMLAUPDATE_UPDATE_FAILED_MAIL_SUBJECT', 'COM_JOOMLAUPDATE_UPDATE_FAILED_MAIL_BODY', '', '', '{"tags":["newversion","oldversion","sitename","url"]}'), ('plg_task_updatenotification.mail', 'plg_task_updatenotification', '', 'PLG_TASK_UPDATENOTIFICATION_EMAIL_SUBJECT', 'PLG_TASK_UPDATENOTIFICATION_EMAIL_BODY', '', '', '{"tags":["newversion","curversion","sitename","url","link","releasenews"]}'), diff --git a/language/en-GB/com_users.ini b/language/en-GB/com_users.ini index 42479b546fb19..2412e248b8820 100644 --- a/language/en-GB/com_users.ini +++ b/language/en-GB/com_users.ini @@ -13,6 +13,8 @@ COM_USERS_EMAIL_ACTIVATED_BY_ADMIN_ACTIVATION_BODY="Hello {NAME},\n\nYour accoun COM_USERS_EMAIL_ACTIVATED_BY_ADMIN_ACTIVATION_SUBJECT="Account activated for {NAME} at {SITENAME}" COM_USERS_EMAIL_ACTIVATE_WITH_ADMIN_ACTIVATION_BODY="Hello administrator,\n\nA new user has registered at {SITENAME}.\nThe user has verified their email address and requests that you approve their account.\nThis email has their details:\n\n Name : {NAME} \n email: {EMAIL} \n Username: {USERNAME} \n\nYou can activate the user by selecting the link below:\n{ACTIVATE}" COM_USERS_EMAIL_ACTIVATE_WITH_ADMIN_ACTIVATION_SUBJECT="Registration approval required for account of {NAME} at {SITENAME}" +COM_USERS_EMAIL_NEW_CREDENTIALS_BODY="Hello {NAME},\n\nAn administrator has generated a new password for your account at {SITENAME}.\n\nYou may now log in to {URL} using the following username and password:\n\nUsername: {USERNAME}\nPassword: {PASSWORD}\n\nYour previous password is no longer valid." +COM_USERS_EMAIL_NEW_CREDENTIALS_SUBJECT="Your new login details for {SITENAME}" COM_USERS_EMAIL_PASSWORD_RESET_BODY="Hello,\n\nA request has been made to reset your {SITENAME} account password. To reset your password, you will need to submit this verification code to verify that the request was legitimate.\n\nThe verification code is {TOKEN}\n\nSelect the URL below and proceed with resetting your password.\n\n{LINK_TEXT} \n\nThank you." COM_USERS_EMAIL_PASSWORD_RESET_SUBJECT="Your {SITENAME} password reset request" COM_USERS_EMAIL_REGISTERED_BODY="Hello {NAME},\n\nThank you for registering at {SITENAME}.\n\nYou may now log in to {SITEURL} using the following username and password:\n\nUsername: {USERNAME}\nPassword: {PASSWORD_CLEAR}"