<?php

/**
 * @package     Joomla.Administrator
 * @subpackage  com_users
 *
 * @copyright   (C) 2008 Open Source Matters, Inc. <https://www.joomla.org>
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

namespace Joomla\Component\Users\Administrator\Model;

use Joomla\CMS\Access\Access;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\Form\Form;
use Joomla\CMS\Language\Multilanguage;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Mail\MailTemplate;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\CMS\MVC\Model\AdminModel;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Table\Table;
use Joomla\CMS\User\UserFactoryAwareInterface;
use Joomla\CMS\User\UserFactoryAwareTrait;
use Joomla\CMS\User\UserHelper;
use Joomla\Database\ParameterType;
use Joomla\Utilities\ArrayHelper;

// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects

/**
 * User model.
 *
 * @since  1.6
 */
class UserModel extends AdminModel implements UserFactoryAwareInterface
{
    use UserFactoryAwareTrait;

    /**
     * An item.
     *
     * @var    array
     */
    protected $_item = null;

    /**
     * Constructor.
     *
     * @param   array                 $config   An optional associative array of configuration settings.
     * @param   ?MVCFactoryInterface  $factory  The factory.
     *
     * @see     \Joomla\CMS\MVC\Model\BaseDatabaseModel
     * @since   3.2
     */
    public function __construct($config = [], ?MVCFactoryInterface $factory = null)
    {
        $config = array_merge(
            [
                'event_after_delete'  => 'onUserAfterDelete',
                'event_after_save'    => 'onUserAfterSave',
                'event_before_delete' => 'onUserBeforeDelete',
                'event_before_save'   => 'onUserBeforeSave',
                'events_map'          => ['save' => 'user', 'delete' => 'user', 'validate' => 'user'],
            ],
            $config
        );

        parent::__construct($config, $factory);
    }

    /**
     * Returns a reference to the a Table object, always creating it.
     *
     * @param   string  $type    The table type to instantiate
     * @param   string  $prefix  A prefix for the table class name. Optional.
     * @param   array   $config  Configuration array for model. Optional.
     *
     * @return  Table  A database object
     *
     * @since   1.6
     */
    public function getTable($type = 'User', $prefix = 'Joomla\\CMS\\Table\\', $config = [])
    {
        $table = Table::getInstance($type, $prefix, $config);

        return $table;
    }

    /**
     * Method to get a single record.
     *
     * @param   integer  $pk  The id of the primary key.
     *
     * @return  mixed  Object on success, false on failure.
     *
     * @since   1.6
     */
    public function getItem($pk = null)
    {
        $pk = (!empty($pk)) ? $pk : (int) $this->getState('user.id');

        if ($this->_item === null) {
            $this->_item = [];
        }

        if (!isset($this->_item[$pk])) {
            $this->_item[$pk] = parent::getItem($pk);
        }

        return $this->_item[$pk];
    }

    /**
     * Method to get the record form.
     *
     * @param   array    $data      An optional array of data for the form to interrogate.
     * @param   boolean  $loadData  True if the form is to load its own data (default case), false if not.
     *
     * @return  Form|bool  A Form object on success, false on failure
     *
     * @since   1.6
     */
    public function getForm($data = [], $loadData = true)
    {
        // Get the form.
        $form = $this->loadForm('com_users.user', 'user', ['control' => 'jform', 'load_data' => $loadData]);

        if (empty($form)) {
            return false;
        }

        $user = $this->getCurrentUser();

        // If the user needs to change their password, mark the password fields as required
        if ($user->requireReset) {
            $form->setFieldAttribute('password', 'required', 'true');
            $form->setFieldAttribute('password2', 'required', 'true');
        }

        // When multilanguage is set, a user's default site language should also be a Content Language
        if (Multilanguage::isEnabled()) {
            $form->setFieldAttribute('language', 'type', 'frontendlanguage', 'params');
        }

        $userId = (int) $form->getValue('id');

        // The user should not be able to set the requireReset value on their own account
        if ($userId === (int) $user->id) {
            $form->removeField('requireReset');
        }

        /**
         * If users without core.manage permission editing their own account, remove some fields which they should
         * not be allowed to change and prevent them to change user name if configured
         */
        if (!$user->authorise('core.manage', 'com_users') && (int) $user->id === $userId) {
            if (!ComponentHelper::getParams('com_users')->get('change_login_name')) {
                $form->setFieldAttribute('username', 'required', 'false');
                $form->setFieldAttribute('username', 'readonly', 'true');
                $form->setFieldAttribute('username', 'description', 'COM_USERS_USER_FIELD_NOCHANGE_USERNAME_DESC');
            }

            $form->removeField('lastResetTime');
            $form->removeField('resetCount');
            $form->removeField('sendEmail');
            $form->removeField('block');
        }

        return $form;
    }

    /**
     * Method to get the data that should be injected in the form.
     *
     * @return  mixed  The data for the form.
     *
     * @since   1.6
     * @throws  \Exception
     */
    protected function loadFormData()
    {
        // Check the session for previously entered form data.
        $data = Factory::getApplication()->getUserState('com_users.edit.user.data', []);

        if (empty($data)) {
            $data = $this->getItem();
        }

        $this->preprocessData('com_users.profile', $data, 'user');

        return $data;
    }

    /**
     * Override Joomla\CMS\MVC\Model\AdminModel::preprocessForm to ensure the correct plugin group is loaded.
     *
     * @param   Form    $form   A Form object.
     * @param   mixed   $data   The data expected for the form.
     * @param   string  $group  The name of the plugin group to import (defaults to "content").
     *
     * @return  void
     *
     * @since   1.6
     *
     * @throws  \Exception if there is an error in the form event.
     */
    protected function preprocessForm(Form $form, $data, $group = 'user')
    {
        parent::preprocessForm($form, $data, $group);
    }

    /**
     * Method to save the form data.
     *
     * @param   array  $data  The form data.
     *
     * @return  boolean  True on success.
     *
     * @since   1.6
     * @throws  \Exception
     */
    public function save($data)
    {
        $pk            = (!empty($data['id'])) ? $data['id'] : (int) $this->getState('user.id');
        $user          = $this->getUserFactory()->loadUserById($pk);
        $my            = $this->getCurrentUser();
        $iAmSuperAdmin = $my->authorise('core.admin');

        // User cannot modify own user groups
        if ((int) $user->id == (int) $my->id && !$iAmSuperAdmin && isset($data['groups'])) {
            // Form was probably tampered with
            Factory::getApplication()->enqueueMessage(Text::_('COM_USERS_USERS_ERROR_CANNOT_EDIT_OWN_GROUP'), 'warning');

            $data['groups'] = null;
        }

        if ($data['block'] && $pk == $my->id && !$my->block) {
            $this->setError(Text::_('COM_USERS_USERS_ERROR_CANNOT_BLOCK_SELF'));

            return false;
        }

        // Make sure user groups is selected when add/edit an account
        if (empty($data['groups']) && ((int) $user->id != (int) $my->id || $iAmSuperAdmin)) {
            $this->setError(Text::_('COM_USERS_USERS_ERROR_CANNOT_SAVE_ACCOUNT_WITHOUT_GROUPS'));

            return false;
        }

        // Make sure that we are not removing ourself from Super Admin group
        if ($iAmSuperAdmin && $my->id == $pk) {
            // Check that at least one of our new groups is Super Admin
            $stillSuperAdmin = false;
            $myNewGroups     = $data['groups'];

            foreach ($myNewGroups as $group) {
                $stillSuperAdmin = $stillSuperAdmin ?: Access::checkGroup($group, 'core.admin');
            }

            if (!$stillSuperAdmin) {
                $this->setError(Text::_('COM_USERS_USERS_ERROR_CANNOT_DEMOTE_SELF'));

                return false;
            }
        }

        // Unset the username if it should not be overwritten
        if (
            !$my->authorise('core.manage', 'com_users')
            && (int) $user->id === (int) $my->id
            && !ComponentHelper::getParams('com_users')->get('change_login_name')
        ) {
            unset($data['username']);
        }

        // Bind the data.
        if (!$user->bind($data)) {
            $this->setError($user->getError());

            return false;
        }

        // Store the data.
        if (!$user->save()) {
            $this->setError($user->getError());

            return false;
        }

        // Destroy all active sessions for the user after changing the password or blocking him
        if (!empty($data['password2']) || !empty($data['block'])) {
            UserHelper::destroyUserSessions($user->id, true);
        }

        $this->setState('user.id', $user->id);

        return true;
    }

    /**
     * Method to delete rows.
     *
     * @param   array  &$pks  An array of item ids.
     *
     * @return  boolean  Returns true on success, false on failure.
     *
     * @since   1.6
     * @throws  \Exception
     */
    public function delete(&$pks)
    {
        $user  = $this->getCurrentUser();
        $table = $this->getTable();
        $pks   = (array) $pks;

        // Check if I am a Super Admin
        $iAmSuperAdmin = $user->authorise('core.admin');

        PluginHelper::importPlugin($this->events_map['delete']);

        if (\in_array($user->id, $pks)) {
            $this->setError(Text::_('COM_USERS_USERS_ERROR_CANNOT_DELETE_SELF'));

            return false;
        }

        // Iterate the items to delete each one.
        foreach ($pks as $i => $pk) {
            if ($table->load($pk)) {
                // Access checks.
                $allow = $user->authorise('core.delete', 'com_users');

                // Don't allow non-super-admin to delete a super admin
                $allow = (!$iAmSuperAdmin && Access::check($pk, 'core.admin')) ? false : $allow;

                if ($allow) {
                    // Get users data for the users to delete.
                    $user_to_delete = $this->getUserFactory()->loadUserById($pk);

                    // Fire the before delete event.
                    Factory::getApplication()->triggerEvent($this->event_before_delete, [$table->getProperties()]);

                    if (!$table->delete($pk)) {
                        $this->setError($table->getError());

                        return false;
                    }

                    // Trigger the after delete event.
                    Factory::getApplication()->triggerEvent($this->event_after_delete, [ArrayHelper::fromObject($user_to_delete, false), true, $this->getError()]);
                } else {
                    // Prune items that you can't change.
                    unset($pks[$i]);
                    Factory::getApplication()->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'error');
                }
            } else {
                $this->setError($table->getError());

                return false;
            }
        }

        return true;
    }

    /**
     * Method to block user records.
     *
     * @param   array    &$pks   The ids of the items to publish.
     * @param   integer  $value  The value of the published state
     *
     * @return  boolean  True on success.
     *
     * @since   1.6
     * @throws  \Exception
     */
    public function block(&$pks, $value = 1)
    {
        $app  = Factory::getApplication();
        $user = $this->getCurrentUser();

        // Check if I am a Super Admin
        $iAmSuperAdmin = $user->authorise('core.admin');
        $table         = $this->getTable();
        $pks           = (array) $pks;

        PluginHelper::importPlugin($this->events_map['save']);

        // Prepare the logout options.
        $options = [
            'clientid' => $app->get('shared_session', '0') ? null : 0,
        ];

        // Access checks.
        foreach ($pks as $i => $pk) {
            if ($value == 1 && $pk == $user->id) {
                // Cannot block yourself.
                unset($pks[$i]);
                Factory::getApplication()->enqueueMessage(Text::_('COM_USERS_USERS_ERROR_CANNOT_BLOCK_SELF'), 'error');
            } elseif ($table->load($pk)) {
                $old   = $table->getProperties();
                $allow = $user->authorise('core.edit.state', 'com_users');

                // Don't allow non-super-admin to delete a super admin
                $allow = (!$iAmSuperAdmin && Access::check($pk, 'core.admin')) ? false : $allow;

                if ($allow) {
                    // Skip changing of same state
                    if ($table->block == $value) {
                        unset($pks[$i]);
                        continue;
                    }

                    $table->block = (int) $value;

                    // If unblocking, also change password reset count to zero to unblock reset
                    if ($table->block === 0) {
                        $table->resetCount = 0;
                    }

                    // Allow an exception to be thrown.
                    try {
                        if (!$table->check()) {
                            $this->setError($table->getError());

                            return false;
                        }

                        // Trigger the before save event.
                        $result = Factory::getApplication()->triggerEvent($this->event_before_save, [$old, false, $table->getProperties()]);

                        if (\in_array(false, $result, true)) {
                            // Plugin will have to raise its own error or throw an exception.
                            return false;
                        }

                        // Store the table.
                        if (!$table->store()) {
                            $this->setError($table->getError());

                            return false;
                        }

                        if ($table->block) {
                            UserHelper::destroyUserSessions($table->id);
                        }

                        // Trigger the after save event
                        Factory::getApplication()->triggerEvent($this->event_after_save, [$table->getProperties(), false, true, null]);
                    } catch (\Exception $e) {
                        $this->setError($e->getMessage());

                        return false;
                    }

                    // Log the user out.
                    if ($value) {
                        $app->logout($table->id, $options);
                    }
                } else {
                    // Prune items that you can't change.
                    unset($pks[$i]);
                    Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error');
                }
            }
        }

        return true;
    }

    /**
     * Method to activate user records.
     *
     * @param   array  &$pks  The ids of the items to activate.
     *
     * @return  boolean  True on success.
     *
     * @since   1.6
     * @throws  \Exception
     */
    public function activate(&$pks)
    {
        $app  = Factory::getApplication();
        $user = $app->getIdentity();

        // Check if I am a super admin
        $iAmSuperAdmin = $user->authorise('core.admin');

        // Load user table
        $table = $this->getTable();
        $pks   = (array) $pks;

        // Compile the user activated notification mail default values.
        $mailData             = [];
        $mailData['siteurl']  = \Joomla\CMS\Uri\Uri::root();
        $mailData['fromname'] = $app->get('fromname');
        $mailData['mailfrom'] = $app->get('mailfrom');
        $mailData['sitename'] = $app->get('sitename');

        // Load com_users site language strings, the mail template use it
        $app->getLanguage()->load('com_users', JPATH_SITE);

        $sendMailTo = function ($userData) use ($app, $mailData) {
            $mailData['name']     = $userData['name'];
            $mailData['username'] = $userData['username'];

            // Use the default language
            $langTag = ComponentHelper::getParams('com_languages')->get('site', 'en-GB');

            $mailer = new MailTemplate('com_users.registration.user.admin_activated', $langTag);
            $mailer->addTemplateData($mailData);
            $mailer->addRecipient($userData['email']);

            try {
                $return = $mailer->send();
            } catch (\Exception $exception) {
                try {
                    \Joomla\CMS\Log\Log::add(Text::_($exception->getMessage()), \Joomla\CMS\Log\Log::WARNING, 'jerror');

                    $return = false;
                } catch (\RuntimeException $exception) {
                    $app->enqueueMessage(Text::_($exception->errorMessage()), $app::MSG_WARNING);

                    $return = false;
                }
            }

            // Check for an error.
            if ($return !== true) {
                $app->enqueueMessage(Text::_('COM_USERS_REGISTRATION_ACTIVATION_NOTIFY_SEND_MAIL_FAILED'), $app::MSG_WARNING);

                return false;
            }

            $app->enqueueMessage(Text::_('COM_USERS_REGISTRATION_ACTIVATION_NOTIFY_SEND_MAIL_SUCCESS'), $app::MSG_INFO);
            return true;
        };

        PluginHelper::importPlugin($this->events_map['save']);

        // Activate and send the notification email
        foreach ($pks as $i => $pk) {
            if ($table->load($pk)) {
                $prevUserData = $table->getProperties();
                $allow        = $user->authorise('core.edit.state', 'com_users');

                // Don't allow non-super-admin to edit the active status of a super admin
                $allow = (!$iAmSuperAdmin && Access::check($pk, 'core.admin')) ? false : $allow;

                // Ignore activated accounts but check if we can still
                // resend the notification email
                if (empty($table->activation)) {
                    if (\is_null($table->lastvisitDate)) {
                        $sendMailTo($prevUserData);
                    }

                    unset($pks[$i]);
                } elseif ($allow) {
                    $table->block      = 0;
                    $table->activation = '';

                    // Allow an exception to be thrown.
                    try {
                        if (!$table->check()) {
                            $this->setError($table->getError());

                            return false;
                        }

                        // Trigger the before save event.
                        $result = Factory::getApplication()->triggerEvent($this->event_before_save, [$prevUserData, false, $table->getProperties()]);

                        if (\in_array(false, $result, true)) {
                            // Plugin will have to raise it's own error or throw an exception.
                            return false;
                        }

                        // Store the table.
                        if (!$table->store()) {
                            $this->setError($table->getError());

                            return false;
                        }

                        // Fire the after save event
                        Factory::getApplication()->triggerEvent($this->event_after_save, [$table->getProperties(), false, true, null]);

                        // Send the email
                        if (!$sendMailTo($prevUserData)) {
                            return false;
                        }
                    } catch (\Exception $e) {
                        $this->setError($e->getMessage());

                        return false;
                    }
                } else {
                    // Prune items that you can't change.
                    unset($pks[$i]);
                    $app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error');
                }
            }
        }

        return true;
    }

    /**
     * Method to perform batch operations on an item or a set of items.
     *
     * @param   array  $commands  An array of commands to perform.
     * @param   array  $pks       An array of item ids.
     * @param   array  $contexts  An array of item contexts.
     *
     * @return  boolean  Returns true on success, false on failure.
     *
     * @since   2.5
     */
    public function batch($commands, $pks, $contexts)
    {
        // Sanitize user ids.
        $pks = array_unique($pks);
        $pks = ArrayHelper::toInteger($pks);

        // Remove any values of zero.
        if (array_search(0, $pks, true)) {
            unset($pks[array_search(0, $pks, true)]);
        }

        if (empty($pks)) {
            $this->setError(Text::_('COM_USERS_USERS_NO_ITEM_SELECTED'));

            return false;
        }

        $done = false;

        if (!empty($commands['group_id'])) {
            $cmd = ArrayHelper::getValue($commands, 'group_action', 'add');

            if (!$this->batchUser((int) $commands['group_id'], $pks, $cmd)) {
                return false;
            }

            $done = true;
        }

        if (!empty($commands['reset_id'])) {
            if (!$this->batchReset($pks, $commands['reset_id'])) {
                return false;
            }

            $done = true;
        }

        if (!$done) {
            $this->setError(Text::_('JLIB_APPLICATION_ERROR_INSUFFICIENT_BATCH_INFORMATION'));

            return false;
        }

        // Clear the cache
        $this->cleanCache();

        return true;
    }

    /**
     * Batch flag users as being required to reset their passwords
     *
     * @param   array   $userIds  An array of user IDs on which to operate
     * @param   string  $action   The action to perform
     *
     * @return  boolean  True on success, false on failure
     *
     * @since   3.2
     */
    public function batchReset($userIds, $action)
    {
        $userIds = ArrayHelper::toInteger($userIds);

        // Check if I am a Super Admin
        $iAmSuperAdmin = $this->getCurrentUser()->authorise('core.admin');

        // Non-super super user cannot work with super-admin user.
        if (!$iAmSuperAdmin && UserHelper::checkSuperUserInUsers($userIds)) {
            $this->setError(Text::_('COM_USERS_ERROR_CANNOT_BATCH_SUPERUSER'));

            return false;
        }

        // Set the action to perform
        if ($action === 'yes') {
            $value = 1;
        } else {
            $value = 0;
        }

        // Prune out the current user if they are in the supplied user ID array
        $userIds = array_diff($userIds, [$this->getCurrentUser()->id]);

        if (empty($userIds)) {
            $this->setError(Text::_('COM_USERS_USERS_ERROR_CANNOT_REQUIRERESET_SELF'));

            return false;
        }

        // Get the DB object
        $db = $this->getDatabase();

        $userIds = ArrayHelper::toInteger($userIds);

        $query = $db->createQuery();

        // Update the reset flag
        $query->update($db->quoteName('#__users'))
            ->set($db->quoteName('requireReset') . ' = :requireReset')
            ->whereIn($db->quoteName('id'), $userIds)
            ->bind(':requireReset', $value, ParameterType::INTEGER);

        $db->setQuery($query);

        try {
            $db->execute();
        } catch (\RuntimeException $e) {
            $this->setError($e->getMessage());

            return false;
        }

        return true;
    }

    /**
     * Perform batch operations
     *
     * @param   integer  $groupId  The group ID which assignments are being edited
     * @param   array    $userIds  An array of user IDs on which to operate
     * @param   string   $action   The action to perform
     *
     * @return  boolean  True on success, false on failure
     *
     * @since   1.6
     */
    public function batchUser($groupId, $userIds, $action)
    {
        $userIds = ArrayHelper::toInteger($userIds);

        // Check if I am a Super Admin
        $iAmSuperAdmin = $this->getCurrentUser()->authorise('core.admin');

        // Non-super super user cannot work with super-admin user.
        if (!$iAmSuperAdmin && UserHelper::checkSuperUserInUsers($userIds)) {
            $this->setError(Text::_('COM_USERS_ERROR_CANNOT_BATCH_SUPERUSER'));

            return false;
        }

        // Non-super admin cannot work with super-admin group.
        if ((!$iAmSuperAdmin && Access::checkGroup($groupId, 'core.admin')) || $groupId < 1) {
            $this->setError(Text::_('COM_USERS_ERROR_INVALID_GROUP'));

            return false;
        }

        // Get the DB object
        $db = $this->getDatabase();

        switch ($action) {
            case 'set':
                // Sets users to a selected group
                $doDelete = 'all';
                $doAssign = true;
                break;

            case 'del':
                // Remove users from a selected group
                $doDelete = 'group';
                break;

            case 'add':
            default:
                // Add users to a selected group
                $doAssign = true;
                break;
        }

        // Remove the users from the group if requested.
        if (isset($doDelete)) {
            /*
             * First we need to check that the user is part of more than one group
             * otherwise we will end up with a user that is not part of any group
             * unless we are moving the user to a new group.
             */
            if ($doDelete === 'group') {
                $query = $db->createQuery();
                $query->select($db->quoteName('user_id'))
                    ->from($db->quoteName('#__user_usergroup_map'))
                    ->whereIn($db->quoteName('user_id'), $userIds);

                // Add the group by clause to remove users who are only in one group
                $query->group($db->quoteName('user_id'))
                    ->having('COUNT(user_id) > 1');
                $db->setQuery($query);
                $users = $db->loadColumn();

                // If we have no users to process, throw an error to notify the user
                if (empty($users)) {
                    $this->setError(Text::_('COM_USERS_ERROR_ONLY_ONE_GROUP'));

                    return false;
                }

                // Check to see if the users are in the group to be removed
                $query->clear()
                    ->select($db->quoteName('user_id'))
                    ->from($db->quoteName('#__user_usergroup_map'))
                    ->whereIn($db->quoteName('user_id'), $users)
                    ->where($db->quoteName('group_id') . ' = :group_id')
                    ->bind(':group_id', $groupId, ParameterType::INTEGER);
                $db->setQuery($query);
                $users = $db->loadColumn();

                // If we have no users to process, throw an error to notify the user
                if (empty($users)) {
                    $this->setError(Text::_('COM_USERS_ERROR_NOT_IN_GROUP'));

                    return false;
                }

                // Finally remove the users from the group
                $query->clear()
                    ->delete($db->quoteName('#__user_usergroup_map'))
                    ->whereIn($db->quoteName('user_id'), $users)
                    ->where($db->quoteName('group_id') . '= :group_id')
                    ->bind(':group_id', $groupId, ParameterType::INTEGER);
                $db->setQuery($query);
            } elseif ($doDelete === 'all') {
                $query = $db->createQuery();
                $query->delete($db->quoteName('#__user_usergroup_map'))
                    ->whereIn($db->quoteName('user_id'), $userIds);
            }
            $db->setQuery($query);

            try {
                $db->execute();
            } catch (\RuntimeException $e) {
                $this->setError($e->getMessage());

                return false;
            }
        }

        // Assign the users to the group if requested.
        if (isset($doAssign)) {
            $query = $db->createQuery();

            // First, we need to check if the user is already assigned to a group
            $query->select($db->quoteName('user_id'))
                ->from($db->quoteName('#__user_usergroup_map'))
                ->where($db->quoteName('group_id') . ' = :group_id')
                ->bind(':group_id', $groupId, ParameterType::INTEGER);
            $db->setQuery($query);
            $users = $db->loadColumn();

            // Build the values clause for the assignment query.
            $query->clear();
            $groups = false;

            foreach ($userIds as $id) {
                if (!\in_array($id, $users)) {
                    $query->values($id . ',' . $groupId);
                    $groups = true;
                }
            }

            // If we have no users to process, throw an error to notify the user
            if (!$groups) {
                $this->setError(Text::_('COM_USERS_ERROR_NO_ADDITIONS'));

                return false;
            }

            $query->insert($db->quoteName('#__user_usergroup_map'))
                ->columns([$db->quoteName('user_id'), $db->quoteName('group_id')]);
            $db->setQuery($query);

            try {
                $db->execute();
            } catch (\RuntimeException $e) {
                $this->setError($e->getMessage());

                return false;
            }
        }

        return true;
    }

    /**
     * Gets the available groups.
     *
     * @return  array  An array of groups
     *
     * @since   1.6
     */
    public function getGroups()
    {
        $user = $this->getCurrentUser();

        if ($user->authorise('core.edit', 'com_users') && $user->authorise('core.manage', 'com_users')) {
            $model = $this->bootComponent('com_users')
                ->getMVCFactory()->createModel('Groups', 'Administrator', ['ignore_request' => true]);

            return $model->getItems();
        }

        return null;
    }

    /**
     * Gets the groups this object is assigned to
     *
     * @param   integer  $userId  The user ID to retrieve the groups for
     *
     * @return  array  An array of assigned groups
     *
     * @since   1.6
     */
    public function getAssignedGroups($userId = null)
    {
        $userId = (!empty($userId)) ? $userId : (int) $this->getState('user.id');

        if (empty($userId)) {
            $result = [];
            $form   = $this->getForm();

            if ($form) {
                $groupsIDs = $form->getValue('groups');
            }

            if (!empty($groupsIDs)) {
                $result = $groupsIDs;
            } else {
                $params = ComponentHelper::getParams('com_users');

                if ($groupId = $params->get('new_usertype', $params->get('guest_usergroup', 1))) {
                    $result[] = $groupId;
                }
            }
        } else {
            $result = UserHelper::getUserGroups($userId);
        }

        return $result;
    }

    /**
     * No longer used
     *
     * @param   integer  $userId  Ignored
     *
     * @return  \stdClass
     *
     * @since   3.2
     *
     * @deprecated   4.2 will be removed in 6.0.
     *               Will be removed without replacement
     */
    public function getOtpConfig($userId = null)
    {
        @trigger_error(
            \sprintf(
                '%s() is deprecated. Use \Joomla\Component\Users\Administrator\Helper\Mfa::getUserMfaRecords() instead.',
                __METHOD__
            ),
            E_USER_DEPRECATED
        );

        // Return the configuration object
        return (object) [
            'method' => 'none',
            'config' => [],
            'otep'   => [],
        ];
    }

    /**
     * No longer used
     *
     * @param   integer    $userId     Ignored
     * @param   \stdClass  $otpConfig  Ignored
     *
     * @return  boolean  True on success
     *
     * @since   3.2
     *
     * @deprecated   4.2 will be removed in 5.0.
     *               Will be removed without replacement
     */
    public function setOtpConfig($userId, $otpConfig)
    {
        @trigger_error(
            \sprintf(
                '%s() is deprecated. Multi-factor Authentication actions are handled by plugins in the multifactorauth folder.',
                __METHOD__
            ),
            E_USER_DEPRECATED
        );

        return true;
    }

    /**
     * No longer used
     *
     * @return  string
     *
     * @since   3.2
     *
     * @deprecated   4.2 will be removed in 6.0.
     *               Use \Joomla\CMS\Factory::getApplication()->get('secret') instead'
     */
    public function getOtpConfigEncryptionKey()
    {
        @trigger_error(
            \sprintf(
                '%s() is deprecated. Use \Joomla\CMS\Factory::getApplication()->get(\'secret\') instead',
                __METHOD__
            ),
            E_USER_DEPRECATED
        );

        return Factory::getApplication()->get('secret');
    }

    /**
     * No longer used
     *
     * @param   integer  $userId  Ignored
     *
     * @return  array  Empty array
     *
     * @since   3.2
     * @throws  \Exception
     *
     * @deprecated   4.2 will be removed in 5.0.
     *               Will be removed without replacement
     */
    public function getTwofactorform($userId = null)
    {
        @trigger_error(
            \sprintf(
                '%s() is deprecated. Use \Joomla\Component\Users\Administrator\Helper\Mfa::getConfigurationInterface()',
                __METHOD__
            ),
            E_USER_DEPRECATED
        );

        return [];
    }

    /**
     * No longer used
     *
     * @param   integer  $userId  Ignored
     * @param   integer  $count   Ignored
     *
     * @return  array  Empty array
     *
     * @since   3.2
     *
     * @deprecated   4.2 will be removed in 5.0
     *               Will be removed without replacement
     */
    public function generateOteps($userId, $count = 10)
    {
        @trigger_error(
            \sprintf(
                '%s() is deprecated. See \Joomla\Component\Users\Administrator\Model\BackupcodesModel::saveBackupCodes()',
                __METHOD__
            ),
            E_USER_DEPRECATED
        );

        return [];
    }

    /**
     * No longer used. Always returns true.
     *
     * @param   integer  $userId     Ignored
     * @param   string   $secretKey  Ignored
     * @param   array    $options    Ignored
     *
     * @return  boolean  Always true
     *
     * @since   3.2
     * @throws  \Exception
     *
     * @deprecated   4.2 will be removed in 5.0
     *               Will be removed without replacement
     */
    public function isValidSecretKey($userId, $secretKey, $options = [])
    {
        @trigger_error(
            \sprintf(
                '%s() is deprecated. Multi-factor Authentication actions are handled by plugins in the multifactorauth folder.',
                __METHOD__
            ),
            E_USER_DEPRECATED
        );

        return true;
    }

    /**
     * No longer used
     *
     * @param   integer  $userId     Ignored
     * @param   string   $otep       Ignored
     * @param   object   $otpConfig  Ignored
     *
     * @return  boolean  Always true
     *
     * @since   3.2
     *
     * @deprecated   4.2 will be removed in 5.0
     *               Will be removed without replacement
     */
    public function isValidOtep($userId, $otep, $otpConfig = null)
    {
        @trigger_error(
            \sprintf(
                '%s() is deprecated. Multi-factor Authentication actions are handled by plugins in the multifactorauth folder.',
                __METHOD__
            ),
            E_USER_DEPRECATED
        );

        return true;
    }
}
