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 @@ ) : ?> loadTemplate('batch_body'); ?> + + authorise('core.edit', 'com_users')) : ?> + loadTemplate('newpassword_body'); ?> + 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 */ + +?> + +