<?php
/**
 * SeekQuarry/Yioop --
 * Open Source Pure PHP Search Engine, Crawler, and Indexer
 *
 * Copyright (C) 2009 - 2026  Chris Pollett chris@pollett.org
 *
 * LICENSE:
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * END LICENSE
 *
 * @author Chris Pollett chris@pollett.org
 * @license https://www.gnu.org/licenses/ GPL3
 * @link https://www.seekquarry.com/
 * @copyright 2009 - 2026
 * @filesource
 */
namespace seekquarry\yioop\controllers\components;

use seekquarry\yioop\configs as C;
use seekquarry\yioop\library as L;
use seekquarry\yioop\library\mail as ML;

/**
 * Component of the Yioop control panel used to handle activitys for
 * managing accounts, users, roles, and groups. i.e., Settings of users
 * and groups, what roles and groups a user has, what roles and users
 * a group has, and what activities make up a role. It is used by
 * AdminController
 *
 * @author Chris Pollett
 */
class AccountaccessComponent extends Component
{
    /**
     * Used to handle the change current user password admin activity
     *
     * @return array $data SCRIPT field contains success or failure message
     */
    public function manageAccount()
    {
        $parent = $this->parent;
        $signin_model = $parent->model("signin");
        $group_model = $parent->model("group");
        $user_model = $parent->model("user");
        $impression_model = $parent->model("impression");
        $crawl_model = $parent->model("crawl");
        $role_model = $parent->model("role");
        $profile_model = $parent->model("profile");
        $locale_model = $parent->model("locale");
        $cron_model = $parent->model("cron");
        $credit_model = $parent->model("credit");
        $profile = $profile_model->getProfile(C\WORK_DIRECTORY);
        $data["ELEMENT"] = "manageaccount";
        $data['SCRIPT'] = "";
        $data["INCLUDE_SCRIPTS"] ??= [];
        $data["INCLUDE_SCRIPTS"][] = "register_validator";
        $data['MESSAGE'] = "";
        $changed_settings_flag = false;
        $changed_locale_flag = true;
        $changed_user = false;
        $data['yioop_bot_configuration'] = $profile['CONFIGURE_BOT'];
        if ($profile['CONFIGURE_BOT'] == 'enable_bot_users') {
            $data['yioop_bot_configuration'] = true;
        } else {
            $data['yioop_bot_configuration'] = false;
        }
        $user_id = $_SESSION['USER_ID'];
        if (isset($_REQUEST['arg']) &&
            $_REQUEST['arg'] == "stopactingas" &&
            !empty($_SESSION['IMPERSONATOR_RETURN_ID'])) {
            $return_id = $_SESSION['IMPERSONATOR_RETURN_ID'];
            $return_name = $signin_model->getUserName($return_id);
            $supplied_password =
                $_REQUEST['impersonator_password'] ?? "";
            $check_name = $return_name;
            $reauth = $signin_model->checkValidSignin($check_name,
                $supplied_password);
            if (!$reauth) {
                return $parent->redirectWithMessage(
                    tl('accountaccess_component_reauth_failed'),
                    false, false, true);
            }
            $acting_as = $_SESSION['USER_NAME'] ?? '';
            unset($_SESSION['IMPERSONATOR_RETURN_ID']);
            unset($_SESSION['IMPERSONATOR_RETURN_NAME']);
            $_SESSION['USER_ID'] = $return_id;
            $_SESSION['USER_NAME'] = $return_name;
            $_REQUEST[C\p('CSRF_TOKEN')] =
                $parent->generateCSRFToken($return_id);
            L\crawlLog("ACT AS: user " . $return_name .
                " (id $return_id) stopped acting as user " .
                $acting_as);
            unset($_REQUEST['route']['c'], $_REQUEST['route']['a']);
            $_REQUEST['c'] = 'admin';
            $_REQUEST['a'] = 'manageUsers';
            return $parent->redirectWithMessage(
                tl('accountaccess_component_stopped_acting'),
                false, false, true);
        }
        /**
         * Preparing data for recommending users with threads and groups
         */
        $cron_timestamp =
            $cron_model->getCronTime("item_group_recommendations");
        $data['THREAD_RECOMMENDATIONS'] =
            $user_model->getRecommendations($cron_timestamp,
                $user_id, C\THREAD_RECOMMENDATION);
        $data['GROUP_RECOMMENDATIONS'] =
            $user_model->getRecommendations($cron_timestamp,
            $user_id, C\GROUP_RECOMMENDATION);
        $data['RESOURCE_RECOMMENDATIONS'] =
            $user_model->getResourceRecommendations($user_id);
        $username = $signin_model->getUserName($user_id);
        $data["USER"] = $user_model->getUser($username);
        $data["CRAWL_MANAGER"] = false;
        if ($user_model->isAllowedUserActivity($user_id, "manageCrawls")) {
            $data['NO_TOPMARGIN'] = true;
            $this->initCrawlBadges($user_id, $data);
        }
        if (isset($_REQUEST['edit']) && $_REQUEST['edit'] == "true") {
            $data['EDIT_USER'] = true;
        }
        if (isset($_REQUEST['edit_pass'])) {
            if ($_REQUEST['edit_pass'] == "true") {
                $data['EDIT_USER'] = true;
                $data['EDIT_PASSWORD'] = true;
            } else {
                $data['EDIT_USER'] = true;
            }
        }
        if (isset($_REQUEST['edit_recovery'])) {
            if ($_REQUEST['edit_recovery'] == "true") {
                $data['EDIT_USER'] = true;
                $data['EDIT_RECOVERY'] = true;
            } else {
                $data['EDIT_USER'] = true;
            }
        }
        if (isset($_REQUEST['edit_purchase']) &&
            $_REQUEST['edit_purchase'] == "true") {
            $data['EDIT_USER'] = true;
        }
        if (!empty($data['EDIT_USER'])) {
            $user_session = $user_model->getUserSession($user_id);
            if (C\p('RECOVERY_MODE') == C\QUESTIONS_RECOVERY &&
                !empty($data['EDIT_RECOVERY'])) {
                $recovery_user = $user_model->getUserById($user_id);
                $data['RECOVERY_QUESTION'] =
                    empty($recovery_user['RECOVERY_QUESTION']) ? "" :
                    $parent->clean($recovery_user['RECOVERY_QUESTION'],
                    "string");
                $data['RECOVERY_ANSWER'] =
                    empty($recovery_user['RECOVERY_ANSWER']) ? "" :
                    C\RECOVERY_ANSWER_PLACEHOLDER;
            }
        }
        $data['USERNAME'] = $username;
        $data['NUM_SHOWN'] = 5;
        $personal_group_name = C\PERSONAL_GROUP_PREFIX . $user_id;
        if (($personal_group_id =
            $group_model->getGroupId($personal_group_name)) < 0) {
            $personal_group_id = $group_model->addGroup($personal_group_name,
                $user_id, C\INVITE_ONLY_JOIN, C\ACTIVE_STATUS,
                C\NON_VOTING_GROUP, C\FOREVER, 0);
        }
        $this->initSocialBadges($user_id, $data);
        $groups = $group_model->getUserGroups($user_id, "", [], 0,
            min($data['NUM_GROUPS'], 100));
        $data['GROUPS'] = [];
        $i = 0;
        $len = strlen(C\PERSONAL_GROUP_PREFIX);
        $group_ids = [];
        foreach ($groups as $group) {
            $group_ids[] = $group['GROUP_ID'];
        }
        $most_recent_views = $impression_model->mostRecentGroupViews($user_id,
            $group_ids);
        foreach ($groups as $group) {
            $group_id = $group['GROUP_ID'];
            $data['GROUPS'][$i] = $group;
            $data['GROUPS'][$i]['MEMBER_STATUS'] =
                $data['GROUPS'][$i]['STATUS'];
            $data['GROUPS'][$i]["MOST_RECENT_VIEW"] =
                $most_recent_views[$group_id] ?? 0;
            $item = $group_model->getMostRecentGroupPost($group_id);
            $data['GROUPS'][$i]['NUM_POSTS'] =
                $group_model->getGroupPostCount($group_id);
            $data['GROUPS'][$i]['NEW_POSTS'] =
                $group_model->getGroupPostCount($group_id,
                $data['GROUPS'][$i]["MOST_RECENT_VIEW"]);
            $data['GROUPS'][$i]['NUM_THREADS'] =
                $group_model->getGroupThreadCount($group_id);
            $data['GROUPS'][$i]['NUM_PAGES'] =
                $group_model->getGroupPageCount($group_id);
            if (isset($item['TITLE'])) {
                $data['GROUPS'][$i]["ITEM_TITLE"] = $item['TITLE'];
                $data['GROUPS'][$i]["THREAD_ID"] = $item['PARENT_ID'];
            } else {
                $data['GROUPS'][$i]["ITEM_TITLE"] =
                    tl('accountaccess_component_no_posts_yet');
                $data['GROUPS'][$i]["THREAD_ID"] = -1;
            }
            $i++;
        }
        usort($data['GROUPS'], function($group1, $group2) {
            if ($group1["MOST_RECENT_VIEW"] == $group2["MOST_RECENT_VIEW"]) {
                return 0;
            }
            return ($group1["MOST_RECENT_VIEW"] > $group2["MOST_RECENT_VIEW"]) ?
                -1 : 1;
         });
        $data['GROUPS'] = array_slice($data['GROUPS'], 0, $data['NUM_SHOWN']);
        $data['NUM_SHOWN'] = count($data['GROUPS']);
        $languages = $locale_model->getLocaleList();
        foreach ($languages as $language) {
            $data['LANGUAGES'][$language['LOCALE_TAG']] =
                $language['LOCALE_NAME'];
        }
        if (isset($_REQUEST['lang']) &&
            in_array($_REQUEST['lang'],
            array_keys($data['LANGUAGES']))) {
            $old_value = isset($_SESSION['l']) ?
                $_SESSION['l'] : L\getLocaleTag();
            $_SESSION['l'] = $_REQUEST['lang'];
            L\setLocaleObject($_SESSION['l']);
            if ($old_value != $_SESSION['l'] || empty($user_session['l'])
                || $user_session['l'] != $_SESSION['l']) {
                $changed_locale_flag = true;
                $user_session['l'] = $_SESSION['l'];
            }
        }
        $data['LANGUAGES_TO_SHOW'] = 1;
        $data['LOCALE_TAG'] = L\getLocaleTag();
        $data['NUM_MIXES'] = count($crawl_model->getMixList($user_id));
        $arg = (isset($_REQUEST['arg'])) ? $_REQUEST['arg'] : "";
        switch ($arg) {
            case "updateuser":
                if (isset($data['EDIT_PASSWORD'])) {
                    if (isset($_REQUEST['new_password']) ) {
                        $pass_len = strlen($_REQUEST['new_password']);
                        if ($pass_len > C\LONG_NAME_LEN) {
                            return $parent->redirectWithMessage(
                            tl('accountaccess_component_passwords_too_long'),
                            ["edit", "edit_pass"]);
                        }
                    }
                    if (!isset($_REQUEST['retype_password']) ||
                        !isset($_REQUEST['new_password']) ||
                        $_REQUEST['retype_password'] !=
                        $_REQUEST['new_password']) {
                            return $parent->redirectWithMessage(
                            tl('accountaccess_component_passwords_dont_match'),
                            ["edit", "edit_pass"]);
                    }
                    if (!empty(L\passwordPolicyViolations(
                        $_REQUEST['new_password']))) {
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_password_requirements'),
                            ["edit", "edit_pass"]);
                    }
                    $result = $signin_model->checkValidSignin($username,
                        $parent->clean($_REQUEST['password'], "password") );
                    if (!$result) {
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_invalid_password'),
                            ["edit", "edit_pass"]);
                    }
                    $signin_model->changePassword($username,
                        $parent->clean($_REQUEST['new_password'],
                        "password"));
                    if ($changed_settings_flag) {
                        $user_model->setUserSession($user_id, $user_session);
                    }
                    $_REQUEST['edit'] = "true";
                    return $parent->redirectWithMessage(
                        tl('accountaccess_component_password_updated'),
                        ["edit"]);
                }
                if (!empty($data['EDIT_RECOVERY']) &&
                    C\p('RECOVERY_MODE') == C\QUESTIONS_RECOVERY &&
                    !empty($_REQUEST['recovery_question']) &&
                    !empty($_REQUEST['recovery_answer'])) {
                    $result = $signin_model->checkValidSignin($username,
                        $parent->clean($_REQUEST['password'] ?? "",
                        "password") );
                    if (!$result) {
                        $_REQUEST['edit_recovery'] = "true";
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_invalid_password'),
                            ["edit", "edit_recovery"]);
                    }
                    $question = mb_substr($_REQUEST['recovery_question'], 0,
                        C\RECOVERY_QUESTION_LEN);
                    $answer = $_REQUEST['recovery_answer'];
                    if ($answer === C\RECOVERY_ANSWER_PLACEHOLDER) {
                        $answer = null;
                    }
                    $user_model->setRecoveryQuestion($user_id, $question,
                        $answer);
                    $_REQUEST['edit'] = "true";
                    return $parent->redirectWithMessage(
                        tl('accountaccess_component_answers_updated'),
                        ["edit"]);
                }
                $user = [];
                $user['USER_ID'] = $user_id;
                $fields = ["EMAIL" => C\LONG_NAME_LEN,
                    "FIRST_NAME" => C\NAME_LEN, "LAST_NAME" => C\NAME_LEN];
                foreach ($fields as $field => $len) {
                    if (isset($_REQUEST[$field])) {
                        $user[$field] = substr($parent->clean(
                            $_REQUEST[$field], "string"), 0, $len);
                        if ($data["USER"][$field] != $user[$field]) {
                            $data['USER'][$field] =  $user[$field];
                            $changed_user = true;
                        }
                    }
                }
                if (isset($user['EMAIL']) &&
                    ML\MailSiteFactory::isForbiddenAccountEmail(
                    $user['EMAIL'])) {
                    return $parent->redirectWithMessage(
                        tl('accountaccess_component_email_system_address'),
                        ["edit"]);
                }
                $is_bot_updated = false;
                $is_bot_toggle = false;
                $bot_role_id = $role_model->getRoleId('Bot User');
                if (isset($_REQUEST['IS_BOT_USER']) && $bot_role_id) {
                    $is_bot_toggle = 1;
                    $is_bot_updated = true;
                } else if ($bot_role_id) {
                    $is_bot_toggle = 0;
                }
                if (isset($_REQUEST['BOT_TOKEN'])) {
                    $user['BOT_TOKEN'] = $_REQUEST['BOT_TOKEN'];
                    $data['USER']['BOT_TOKEN'] = $user['BOT_TOKEN'];
                    $is_bot_updated = true;
                }
                if (isset($_REQUEST['BOT_CALLBACK_URL'])){
                    $user['CALLBACK_URL'] = $_REQUEST['BOT_CALLBACK_URL'];
                    $data['USER']['CALLBACK_URL'] = $user['CALLBACK_URL'];
                    $is_bot_updated = true;
                }
                if (isset($_FILES['user_icon']['name']) &&
                    $_FILES['user_icon']['name'] != "") {
                    if (!in_array($_FILES['user_icon']['type'],
                        ['image/png', 'image/gif', 'image/jpeg',
                            'image/webp', 'image/x-icon'])) {
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_unknown_imagetype'),
                            ["edit", "edit_pass"]);
                    }
                    if ($_FILES['user_icon']['size'] > C\THUMB_SIZE) {
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_icon_too_big'),
                             ["edit", "edit_pass"]);
                    }
                    if (empty($_FILES['user_icon']['data'])) {
                        $user['IMAGE_STRING'] = file_get_contents(
                            $_FILES['user_icon']['tmp_name']);
                        $changed_user = true;
                    } else {
                        $user['IMAGE_STRING'] = $_FILES['user_icon']['data'];
                        $changed_user = true;
                    }
                    $folder = $user_model->getUserIconFolder(
                        $user['USER_ID']);
                    if (!$folder) {
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_no_user_folder'),
                            ["edit", "edit_pass"]);
                    }
                }
                if ($changed_user || $is_bot_updated) {
                    $result = $signin_model->checkValidSignin($username,
                        $parent->clean($_REQUEST['password'] ?? "",
                        "password") );
                    if (!$result) {
                        return $parent->redirectWithMessage(
                        tl('accountaccess_component_valid_password_required'),
                        ["edit"]);
                    }
                }
                if ($changed_user) {
                    $user_model->updateUser($user);
                }
                if ($changed_settings_flag || $changed_locale_flag) {
                    $user_model->setUserSession($user_id, $user_session);
                }
                if ($is_bot_updated) {
                    if ($is_bot_toggle === 1) {
                        $data['USER']['IS_BOT_USER'] = 1;
                        $role_model->addUserRole($user_id, $bot_role_id);
                    } else if ($is_bot_toggle === 0) {
                        $data['USER']['IS_BOT_USER'] = 0;
                        $role_model->deleteUserRole($user_id, $bot_role_id);
                    }
                    $user_model->updateBot($user);
                }
                $data['USER']['USER_ICON'] = $user_model->getUserIconUrl(
                    $user['USER_ID']);
                unset($user['IMAGE_STRING']);
                return $parent->redirectWithMessage(
                    tl('accountaccess_component_user_updated'),
                    ["edit", "edit_pass"]);
            case "purchaserole":
                $message = $this->purchaseRole($role_model,
                    $credit_model, $user_id,
                    intval($_REQUEST['purchase_role_id'] ?? 0));
                return $parent->redirectWithMessage($message,
                    ["edit", "edit_purchase"]);
            case "unsubscriberole":
                $message = $this->unsubscribeRole($role_model, $user_id,
                    intval($_REQUEST['unsubscribe_role_id'] ?? 0));
                return $parent->redirectWithMessage($message, ["edit"]);
        }
        $this->renewExpiredRoleSubscriptions($role_model, $credit_model,
            $user_id);
        $sellable_roles = $role_model->getSellableRoles();
        $data['SELLABLE_ROLES_EXIST'] = !empty($sellable_roles);
        if ($data['SELLABLE_ROLES_EXIST']) {
            $data['USER_ROLE_LIST'] =
                $role_model->getUserRoles($user_id, "");
            $data['USER_SELLABLE_ROLES'] =
                $role_model->getUserSellableRoles($user_id);
            $data['CREDIT_BALANCE'] =
                $credit_model->getCreditBalance($user_id);
            $held_ids = [];
            foreach ($data['USER_ROLE_LIST'] as $role) {
                $held_ids[(int)$role['ROLE_ID']] = true;
            }
            $purchasable = [];
            foreach ($sellable_roles as $role) {
                if (empty($held_ids[(int)$role['ROLE_ID']])) {
                    $purchasable[] = $role;
                }
            }
            $data['PURCHASABLE_ROLES'] = $purchasable;
            if (isset($_REQUEST['edit_purchase']) &&
                $_REQUEST['edit_purchase'] == "true") {
                $data['EDIT_PURCHASE'] = true;
            }
        }
        return $data;
    }
    /**
     * Converts a role's charge frequency into the number of seconds a
     * purchase of it lasts. A one-time ("once") purchase never lapses and
     * returns the FOREVER sentinel; the recurring frequencies return
     * their length in seconds, so a grant's expiry can be advanced by
     * that amount.
     *
     * @param string $frequency one of the CHARGE_FREQUENCY values
     * @return int seconds the purchase lasts, or FOREVER when permanent
     */
    private function chargeFrequency($frequency)
    {
        $months_per_semi_annual = 6;
        $seconds = [
            'once' => C\FOREVER,
            'monthly' => C\ONE_MONTH,
            'semi_annual' => $months_per_semi_annual * C\ONE_MONTH,
            'yearly' => C\ONE_YEAR,
            'test_minute' => C\ONE_MINUTE];
        return $seconds[$frequency] ?? C\FOREVER;
    }
    /**
     * Buys a sellable role for a user, or resumes one they cancelled.
     * If the user still holds the role and it has not lapsed, buying it
     * again only turns renewing back on and charges nothing until the
     * current paid period ends. Otherwise it checks the role is on sale
     * and the balance covers the cost, then debits the cost and grants
     * (or re-dates) the role with an expiry one billing period out, or a
     * never-lapsing grant for a one-time purchase.
     *
     * @param object $role_model model used to read and grant roles
     * @param object $credit_model model used to read and debit credits
     * @param int $user_id id of the buyer
     * @param int $role_id id of the role to buy
     * @return string the status message to show the buyer
     */
    private function purchaseRole($role_model, $credit_model, $user_id,
        $role_id)
    {
        $for_sale = null;
        foreach ($role_model->getSellableRoles() as $role) {
            if ((int)$role['ROLE_ID'] === (int)$role_id) {
                $for_sale = $role;
                break;
            }
        }
        if (empty($for_sale)) {
            return tl('accountaccess_component_role_not_for_sale');
        }
        $now = time();
        $held = null;
        foreach ($role_model->getUserSellableRoles($user_id) as $row) {
            if ((int)$row['ROLE_ID'] === (int)$role_id) {
                $held = $row;
                break;
            }
        }
        if ($held !== null) {
            $held_expires = (int)$held['EXPIRES'];
            if ($held_expires === C\FOREVER || $held_expires > $now) {
                if ((int)$held['WILL_RENEW'] === 1) {
                    return tl('accountaccess_component_role_already_held');
                }
                $role_model->setUserRoleRenew($user_id, $role_id, 1);
                return tl('accountaccess_component_role_resubscribed');
            }
        }
        $cost = (int)$for_sale['ROLE_COST'];
        if ($credit_model->getCreditBalance($user_id) < $cost) {
            return tl('accountaccess_component_role_no_credits');
        }
        $period = $this->chargeFrequency($for_sale['CHARGE_FREQUENCY']);
        $expires = ($period === C\FOREVER) ? C\FOREVER : $now + $period;
        $credit_model->updateCredits($user_id, -$cost,
            'accountaccess_component_role_purchase');
        if ($held !== null) {
            $role_model->updateUserRoleExpires($user_id, $role_id,
                $expires);
            $role_model->setUserRoleRenew($user_id, $role_id, 1);
        } else {
            $role_model->addUserRole($user_id, $role_id, $expires, 1);
        }
        return tl('accountaccess_component_role_purchased');
    }
    /**
     * Cancels a user's subscription to a sellable role. If the grant is
     * still within its paid period it is kept but stops renewing, so the
     * user keeps the role until it lapses and it is removed then; the
     * account page shows the expiry date meanwhile. A grant that has
     * already lapsed, or a one-time permanent grant, is removed at once.
     *
     * @param object $role_model model used to read and update grants
     * @param int $user_id id of the user cancelling
     * @param int $role_id id of the role to cancel
     * @return string the status message to show the user
     */
    private function unsubscribeRole($role_model, $user_id, $role_id)
    {
        $held = null;
        foreach ($role_model->getUserSellableRoles($user_id) as $row) {
            if ((int)$row['ROLE_ID'] === (int)$role_id) {
                $held = $row;
                break;
            }
        }
        if ($held === null) {
            return tl('accountaccess_component_role_unsubscribed');
        }
        $expires = (int)$held['EXPIRES'];
        if ($expires !== C\FOREVER && $expires > time()) {
            $role_model->setUserRoleRenew($user_id, $role_id, 0);
            return tl('accountaccess_component_role_cancelled');
        }
        $role_model->deleteUserRole($user_id, $role_id);
        return tl('accountaccess_component_role_unsubscribed');
    }
    /**
     * Settles a user's lapsed sellable roles when the account page is
     * viewed. A grant the user cancelled is removed once it lapses. A
     * grant still set to renew has its expiry moved forward one billing
     * period at a time, spending credits, for as many periods as the
     * balance can cover. A one-time ("once") grant never lapses and is
     * left alone.
     *
     * @param object $role_model model used to read and update grants
     * @param object $credit_model model used to read and debit credits
     * @param int $user_id id of the user whose subscriptions to settle
     */
    private function renewExpiredRoleSubscriptions($role_model,
        $credit_model, $user_id)
    {
        $balance = $credit_model->getCreditBalance($user_id);
        $now = time();
        foreach ($role_model->getUserSellableRoles($user_id) as $role) {
            $expires = (int)$role['EXPIRES'];
            if ($expires === C\FOREVER || $expires > $now) {
                continue;
            }
            if ((int)$role['WILL_RENEW'] === 0) {
                $role_model->deleteUserRole($user_id, $role['ROLE_ID']);
                continue;
            }
            $period = $this->chargeFrequency($role['CHARGE_FREQUENCY']);
            $cost = (int)$role['ROLE_COST'];
            if ($period === C\FOREVER || $cost <= 0) {
                continue;
            }
            $new_expires = $expires;
            while ($new_expires <= $now && $balance >= $cost) {
                $credit_model->updateCredits($user_id, -$cost,
                    'accountaccess_component_role_renewal');
                $balance -= $cost;
                $new_expires += $period;
            }
            if ($new_expires !== $expires) {
                $role_model->updateUserRoleExpires($user_id,
                    $role['ROLE_ID'], $new_expires);
            }
        }
    }
    /**
     * Used to handle the manage user activity.
     *
     * This activity allows new users to be added, old users to be
     * deleted and allows roles to be added to/deleted from a user
     *
     * @return array $data infomartion about users of the system, roles, etc.
     *     as well as status messages on performing a given sub activity
     */
    public function manageUsers()
    {
        $parent = $this->parent;
        $request_fields = ['start_row', 'num_show', 'end_row',
            'visible_roles', 'visible_groups', 'role_filter', 'role_sorts',
            'group_filter', 'group_sorts', 'role_limit', 'group_limit',
            'context'];
        $signin_model = $parent->model("signin");
        $user_model = $parent->model("user");
        $group_model = $parent->model("group");
        $role_model = $parent->model("role");
        $possible_arguments = ["adduser", 'edituser', 'search',
            "deleteuser", "adduserrole", "deleteuserrole",
            "addusergroup", "deleteusergroup", "updatestatus",
            "actasuser"];
        $data["ELEMENT"] = "manageusers";
        $data['SCRIPT'] = "";
        $data["INCLUDE_SCRIPTS"] ??= [];
        $data["INCLUDE_SCRIPTS"][] = "register_validator";
        $data['STATUS_CODES'] = [
            C\ACTIVE_STATUS => tl('accountaccess_component_active_status'),
            C\INACTIVE_STATUS => tl('accountaccess_component_inactive_status'),
            C\EDITOR_STATUS => tl('accountaccess_component_editor_status'),
            C\SUSPENDED_STATUS =>
                tl('accountaccess_component_suspended_status'),
        ];
        $data['MEMBERSHIP_CODES'] = [
            C\INACTIVE_STATUS => tl('accountaccess_component_request_join'),
            C\INVITED_STATUS => tl('accountaccess_component_invited'),
            C\ACTIVE_STATUS => tl('accountaccess_component_active_status'),
            C\EDITOR_STATUS => tl('accountaccess_component_editor_status'),
            C\SUSPENDED_STATUS =>
                tl('accountaccess_component_suspended_status')
        ];
        $data['FORM_TYPE'] = "adduser";
        $search_array = [];
        $username = "";
        if (isset($_REQUEST['user_name'])) {
            $username = substr($parent->clean($_REQUEST['user_name'], "string"),
                0, C\NAME_LEN);
        }
        if ($username == "" && isset($_REQUEST['arg']) && $_REQUEST['arg']
            != "search") {
            unset($_REQUEST['arg']);
        }
        $select_group = isset($_REQUEST['selectgroup']) ?
            $parent->clean($_REQUEST['selectgroup'],"string") : "";
        $select_role = isset($_REQUEST['selectrole']) ?
            $parent->clean($_REQUEST['selectrole'],"string") : "";
        if (isset($_REQUEST['arg']) && $_REQUEST['arg'] == 'edituser') {
            if ($select_role != "") {
                $_REQUEST['arg'] = "adduserrole";
            } else if ($select_group != ""){
                $_REQUEST['arg'] = "addusergroup";
            }
        }
        $user_id = -1;
        $data['visible_roles'] = 'false';
        $data['visible_groups'] = 'false';
        if ($username != "") {
            $user_id = $signin_model->getUserId($username);
            if ($user_id) {
                $this->getUserRolesData($data, $user_id);
                $this->getUserGroupsData($data, $user_id);
            }
        }
        $data['CURRENT_USER'] = ["user_name" => "", "first_name" => "",
            "last_name" => "", "email" => "", "status" => "", "password" => "",
            "repassword" => ""];
        $data['CAN_SET_PASSWORD'] = true;
        $data['PAGING'] = "";
        if (isset($_REQUEST['arg']) &&
            in_array($_REQUEST['arg'], $possible_arguments)) {
            $arg = $_REQUEST['arg'];
            $pass_len = (isset($_REQUEST['new_password'])) ?
                strlen($_REQUEST['new_password']) : 0;
            switch ($arg) {
                case "adduser":
                    if ($pass_len > C\LONG_NAME_LEN ) {
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_passwords_too_long'),
                            $request_fields);
                    } else if ($_REQUEST['retypepassword'] !=
                        $_REQUEST['password']) {
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_passwords_dont_match'),
                            $request_fields);
                    } else if (!empty(L\passwordPolicyViolations(
                        $_REQUEST['password']))) {
                        return $parent->redirectWithMessage(tl(
                            'accountaccess_component_password_requirements'),
                            $request_fields);
                    } else if (trim($username) == "") {
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_invalid_username'),
                            $request_fields);
                    } else if ($signin_model->getUserId($username) > 0) {
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_user_exists'),
                            $request_fields);
                    } else if (!empty($_REQUEST['email']) &&
                        ML\MailSiteFactory::isForbiddenAccountEmail(
                        substr(trim($parent->clean($_REQUEST['email'],
                        "string")), 0, C\LONG_NAME_LEN))) {
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_email_system_address'),
                            $request_fields);
                    } else if (!isset($data['STATUS_CODES'][
                        $_REQUEST['status']])) {
                        $_REQUEST['status'] = C\INACTIVE_STATUS;
                    } else {
                        $norm_password =
                            substr($parent->clean($_REQUEST['password'],
                            "password"), 0, C\LONG_NAME_LEN);
                        $username = trim($username);

                        $user_id = $user_model->addUser($username,
                            $norm_password,
                            substr(trim($parent->clean($_REQUEST['first_name'],
                                "string")), 0, C\NAME_LEN),
                            substr(trim($parent->clean($_REQUEST['last_name'],
                                "string")), 0, C\NAME_LEN),
                            substr(trim($parent->clean($_REQUEST['email'],
                                "string")), 0, C\LONG_NAME_LEN),
                            $_REQUEST['status']
                        );
                        $data['USER_NAMES'][$username] = $username;
                        $personal_group_name = C\PERSONAL_GROUP_PREFIX .
                            $user_id;
                        if ($group_model->getGroupId($personal_group_name) < 0){
                            $group_model->addGroup($personal_group_name,
                                $user_id, C\INVITE_ONLY_JOIN, C\ACTIVE_STATUS,
                                C\NON_VOTING_GROUP, C\FOREVER, 0);
                        }
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_user_added'),
                            $request_fields);
                    }
                    break;
                case "edituser":
                    $data['FORM_TYPE'] = "edituser";
                    if (!empty($_REQUEST['context'])) {
                        $data['context'] = "search";
                    }
                    $user = $user_model->getUser($username);
                    if (!$user) {
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_user_doesnt_exist'),
                            $request_fields);
                    }
                    $update = false;
                    $error = false;
                    if ($user["USER_ID"] == C\PUBLIC_USER_ID) {
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_cant_edit_public_user'),
                            $request_fields);
                    }
                    $editing_root = ($user["USER_ID"] == C\ROOT_ID);
                    $actor_is_root =
                        ($_SESSION['USER_ID'] == C\ROOT_ID);
                    $can_set_password =
                        (!$editing_root || $actor_is_root);
                    $data['CAN_SET_PASSWORD'] = $can_set_password;
                    if (!$can_set_password) {
                        unset($_REQUEST['password']);
                        unset($_REQUEST['retypepassword']);
                    }
                    foreach ($data['CURRENT_USER'] as $field => $value) {
                        $upper_field = strtoupper($field);
                        if (isset($_REQUEST[$field]) && $field != 'user_name') {
                            if ($field != "password" || ($_REQUEST["password"]
                                != md5("password") && $_REQUEST["password"] ==
                                $_REQUEST["retypepassword"])) {
                                if ($field == "password" && !empty(
                                    L\passwordPolicyViolations(
                                    $_REQUEST["password"]))) {
                                    return $parent->redirectWithMessage(tl(
                                'accountaccess_component_password_requirements'
                                    ), array_merge(array('arg', 'user_name'),
                                    $request_fields));
                                }
                                $clean_type =
                                    (!str_contains($field, 'password')) ?
                                    "string" : "password";
                                $tmp = $parent->clean(
                                    $_REQUEST[$field], $clean_type);
                                if ($tmp != $user[$upper_field]) {
                                    $user[$upper_field] = $tmp;
                                    if (!isset($_REQUEST['change_filter'])) {
                                        $update = true;
                                    }
                                }
                                $data['CURRENT_USER'][$field] =
                                    $user[$upper_field];
                            } else if ($_REQUEST["password"] !=
                                $_REQUEST["retypepassword"]) {
                                $error = true;
                                break;
                            }
                        } else if (isset($user[$upper_field])){
                            if ($field != "password" &&
                                $field != "retypepassword") {
                                $data['CURRENT_USER'][$field] =
                                    $user[$upper_field];
                            }
                        }
                    }
                    $data['CURRENT_USER']['password'] = md5("password");
                    $data['CURRENT_USER']['retypepassword'] = md5("password");
                    if ($error) {
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_passwords_dont_match'),
                            array_merge(array('arg', 'user_name'),
                            $request_fields));
                    } else if (isset($user['EMAIL']) &&
                        ML\MailSiteFactory::isForbiddenAccountEmail(
                        $user['EMAIL'])) {
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_email_system_address'),
                            array_merge(array('arg', 'user_name'),
                            $request_fields));
                    } else if ($update) {
                        $user_model->updateUser($user);
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_user_updated'),
                            array_merge(array('arg', 'user_name'),
                            $request_fields));
                    } else if (isset($_REQUEST['change_filter'])) {
                        if ($_REQUEST['change_filter'] == "group") {
                            $data['SCRIPT'] .= "doMessage('<h1 class=\"red\" >".
                                tl('accountaccess_component_user_filter_group').
                                "</h1>');";
                        } else {
                            $data['SCRIPT'] .= "doMessage('<h1 class=\"red\" >".
                                tl('accountaccess_component_user_filter_role').
                                "</h1>');";
                        }
                    }
                    $data['CURRENT_USER']['id'] = $user_id;
                    $actor_may_act_as = $user_model->
                        isAllowedUserActivity($_SESSION['USER_ID'],
                        'manageUsers');
                    $data['CAN_ACT_AS_USER'] = ($actor_may_act_as &&
                        !$editing_root &&
                        $user["USER_ID"] != C\PUBLIC_USER_ID &&
                        empty($_SESSION['IMPERSONATOR_RETURN_ID']));
                    $data['SCRIPT'] .= "elt('focus-button').focus();";
                    break;
                case "actasuser":
                    $target_id = $signin_model->getUserId($username);
                    $actor_may_act_as = $user_model->
                        isAllowedUserActivity($_SESSION['USER_ID'],
                        'manageUsers');
                    if (!$actor_may_act_as ||
                        !empty($_SESSION['IMPERSONATOR_RETURN_ID']) ||
                        $target_id <= 0 || in_array($target_id,
                        [C\ROOT_ID, C\PUBLIC_USER_ID])) {
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_no_act_as'),
                            $request_fields);
                    }
                    $_SESSION['IMPERSONATOR_RETURN_ID'] =
                        $_SESSION['USER_ID'];
                    $_SESSION['IMPERSONATOR_RETURN_NAME'] =
                        $_SESSION['USER_NAME'] ?? '';
                    L\crawlLog("ACT AS: user " .
                        ($_SESSION['USER_NAME'] ?? '') .
                        " (id " . $_SESSION['IMPERSONATOR_RETURN_ID'] .
                        ") began acting as user " . $username);
                    $_SESSION['USER_ID'] = $target_id;
                    $_SESSION['USER_NAME'] = $username;
                    $_REQUEST[C\p('CSRF_TOKEN')] =
                        $parent->generateCSRFToken($target_id);
                    unset($_REQUEST['route']['c'],
                        $_REQUEST['route']['a']);
                    $_REQUEST['c'] = 'admin';
                    $_REQUEST['a'] = 'manageAccount';
                    return $parent->redirectWithMessage(
                        tl('accountaccess_component_now_acting_as',
                        $username), false, false, true);
                    break;
                case "deleteuser":
                    if (!empty($_REQUEST['context'])) {
                        $_REQUEST['arg'] = 'search';
                        $request_fields[] = 'arg';
                    }
                    $user_id =
                        $signin_model->getUserId($username);
                    if ($user_id <= 0) {
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_username_doesnt_exists'
                            ), $request_fields);
                    } else if (in_array(
                        $user_id, [C\ROOT_ID, C\PUBLIC_USER_ID])) {
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_cant_delete_builtin'),
                            $request_fields);
                    } else {
                        $user_model->deleteUser($username);
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_user_deleted'),
                            $request_fields);
                    }
                    break;
                case "adduserrole":
                    $_REQUEST['arg'] = 'edituser';
                    if ($user_id <= 0 ) {
                        $_REQUEST['arg'] = 'adduser';
                        return $parent->redirectWithMessage(
                        tl('accountaccess_component_username_doesnt_exists'),
                            array_merge(['arg'], $request_fields));
                    } else  if (!($role_id = $role_model->getRoleId(
                        $select_role))) {
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_rolename_doesnt_exists'
                            ), array_merge(['arg', 'user_name'],
                            $request_fields));
                    } else if ($role_model->checkUserRole($user_id,
                        $role_id)) {
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_rolename_already_added'
                            ), array_merge(['arg', 'user_name'],
                            $request_fields));
                    } else {
                        $role_model->addUserRole($user_id, $role_id);
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_rolename_added'),
                            array_merge(['arg', 'user_name'],
                            $request_fields));
                    }
                    break;
                case "addusergroup":
                    $_REQUEST['arg'] = 'edituser';
                    if ( $user_id <= 0 ) {
                        $_REQUEST['arg'] = 'adduser';
                        return $parent->redirectWithMessage(
                        tl('accountaccess_component_username_doesnt_exists'),
                            array_merge(['arg'], $request_fields));
                    } else if (!($group_id = $group_model->getGroupId(
                        $select_group))) {
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_groupname_doesnt_exists'
                            ), array_merge(['arg', 'user_name'],
                            $request_fields));
                    } else if ($group_model->checkUserGroup($user_id,
                        $group_id)){
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_groupname_already_added'
                            ), array_merge(['arg', 'user_name'],
                            $request_fields));
                    } else {
                        $group_model->addUserGroup($user_id,
                            $group_id);
                        $this->getUserGroupsData($data, $user_id);
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_groupname_added'),
                            array_merge(['arg', 'user_name'],
                            $request_fields));
                    }
                    break;
                case "deleteuserrole":
                    $_REQUEST['arg'] = 'edituser';
                    if ($user_id <= 0) {
                        $_REQUEST['arg'] = 'adduser';
                        return $parent->redirectWithMessage(
                        tl('accountaccess_component_username_doesnt_exists'),
                            array_merge(['arg'],
                            $request_fields));
                    }
                    $deleted = 0;
                    if (!empty($_REQUEST['role_ids'])) {
                        $role_ids = $_REQUEST['role_ids'];
                        $ids = explode("*", $role_ids);
                        foreach ($ids as $role_id) {
                            $role_id = (!empty($role_id)) ?
                                $parent->clean($role_id, 'int'): 0;
                            if ($role_model->checkUserRole($user_id,
                                $role_id)) {
                                $role_model->deleteUserRole(
                                    $user_id, $role_id);
                                $deleted++;
                            }
                        }
                    }
                    if ($deleted == 1) {
                        $this->getUserRolesData($data, $user_id);
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_rolename_deleted'
                            ), array_merge(['arg', 'user_name'],
                            $request_fields));
                    } else if ($deleted > 1) {
                        $this->getUserRolesData($data, $user_id);
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_rolenames_deleted'
                            ), array_merge(['arg', 'user_name'],
                            $request_fields));
                    }
                    return $parent->redirectWithMessage(
                        tl('accountaccess_component_rolename_doesnt_exists'
                        ), array_merge(['arg', 'user_name'],
                        $request_fields));
                case "deleteusergroup":
                    $_REQUEST['arg'] = 'edituser';
                    if (!empty($_REQUEST['context'])) {
                        $_REQUEST['c'] = 'search';
                        $request_fields[] = 'arg';
                    }
                    if ($user_id <= 0) {
                        $_REQUEST['arg'] = 'adduser';
                        return $parent->redirectWithMessage(
                        tl('accountaccess_component_username_doesnt_exists'),
                            $request_fields);
                    }
                    $deleted = 0;
                    if (!empty($_REQUEST['group_ids'])) {
                        $group_ids = $_REQUEST['group_ids'];
                        $ids = explode("*", $group_ids);
                        foreach ($ids as $group_id) {
                            $group_id = (!empty($group_id)) ?
                                $parent->clean($group_id, 'int'): 0;
                            if ($group_model->checkUserGroup($user_id,
                                $group_id)) {
                                $group_model->deleteUserGroup(
                                    $user_id, $group_id);
                                $deleted++;
                            }
                        }
                    }
                    if ($deleted == 1) {
                        $this->getUserGroupsData($data, $user_id);
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_group_deleted'
                            ), array_merge(['arg', 'user_name'],
                            $request_fields));
                    } else if ($deleted > 1) {
                        $this->getUserGroupsData($data, $user_id);
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_groups_deleted'
                            ), array_merge(['arg', 'user_name'],
                            $request_fields));
                    }
                    return $parent->redirectWithMessage(
                        tl('accountaccess_component_groupname_doesnt_exists'
                        ), array_merge(['arg', 'user_name'],
                        $request_fields));
                case "search":
                    $data["FORM_TYPE"] = "search";
                    $search_array =
                        $parent->tableSearchRequestHandler($data,
                        "manageUsers",
                        ['ALL_FIELDS' =>
                            ['user', 'first', 'last', 'email', 'status'],
                         'EQUAL_COMPARISON_TYPES' => ['status']], "_name");
                    if (empty($_SESSION['LAST_SEARCH']['manageUsers_name']) ||
                        (!empty($_SESSION['LAST_SEARCH']['manageUsers_name']) &&
                        isset($_REQUEST['user_name'])) ) {
                        $_SESSION['LAST_SEARCH']['manageUsers_name'] =
                            $_SESSION['SEARCH']['manageUsers_name'];
                        unset($_SESSION['SEARCH']['manageUsers_name']);
                    } else {
                        $default_search = true;
                    }
                    break;
                case "updatestatus":
                    if (!empty($_REQUEST['context'])) {
                        $_REQUEST['arg'] = 'search';
                        $request_fields[] = 'arg';
                    }
                    $user_id = $signin_model->getUserId($username);
                    if (!isset($data['STATUS_CODES'][$_REQUEST['userstatus']])||
                        $user_id == 1) {
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_username_doesnt_exists'
                            ), $request_fields);
                    } else {
                        $user_model->updateUserStatus($user_id,
                            $_REQUEST['userstatus']);
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_userstatus_updated'),
                            $request_fields);
                    }
                    break;
            }
        }
        if ($search_array == [] || !empty($default_search)) {
            if (!empty($_SESSION['LAST_SEARCH']['manageUsers_name'])) {
                if (!empty($_REQUEST['arg']) && $_REQUEST['arg'] == 'search') {
                    $search_array =
                        $parent->restoreLastSearchFromSession($data,
                        'manageUsers_name');
                } else if (!empty($_REQUEST['context'])) {
                    $search_array = $_SESSION['LAST_SEARCH'][
                        'manageUsers_name']['SEARCH_ARRAY'];
                    $data['PAGING'] =
                        $_SESSION['LAST_SEARCH']['manageUsers_name']['PAGING'];
                }
            }
            if ($search_array == []) {
                $search_array[] = ["user", "", "", "ASC"];
            }
        }
        $parent->pagingLogic($data, $user_model, "USERS",
            C\DEFAULT_ADMIN_PAGING_NUM, $search_array, "");
        $num_users = count($data['USERS']);
        for ($i = 0; $i < $num_users; $i++) {
            $data['USERS'][$i]['NUM_GROUPS'] =
                $group_model->countUserGroups($data['USERS'][$i]['USER_ID']);
        }
        if ($data['FORM_TYPE'] == 'adduser') {
            $data['SCRIPT'] .= "setDisplay('admin-form-row', false);";
        }
        return $data;
    }
    /**
     * Uses $_REQUEST and $user_id to look up all the roles that a user
     * has subject to $_REQUEST['role_limit'], $_REQUEST['role_filter'],
     * and $_REQUEST['role_sorts']. Information about these roles is added as
     * fields to $data[NUM_USER_ROLES'] and $data['USER_ROLES']
     *
     * @param array &$data data for the manageUsers view.
     * @param int $user_id user to look up roles for
     */
    public function getUserRolesData(&$data, $user_id)
    {
        $parent = $this->parent;
        $role_model = $parent->model("role");
        $data['visible_roles'] = (isset($_REQUEST['visible_roles']) &&
            $_REQUEST['visible_roles']=='true') ? 'true' : 'false';
        if ($data['visible_roles'] == 'false') {
            unset($_REQUEST['role_filter']);
            unset($_REQUEST['role_limit']);
        }
        if (isset($_REQUEST['role_filter'])) {
            $role_filter = substr($parent->clean(
                $_REQUEST['role_filter'], 'string'), 0, C\NAME_LEN);
        } else {
            $role_filter = "";
        }
        $data['ROLE_FILTER'] = $role_filter;
        $data['ROLE_SORTS'] = (empty($_REQUEST['role_sorts'])) ? [] :
            json_decode(urldecode($_REQUEST['role_sorts']), true);
        if ($data['ROLE_SORTS'] === null) {
            $data['ROLE_SORTS'] = json_decode(html_entity_decode(
                urldecode($_REQUEST['role_sorts'])), true);
            $data['ROLE_SORTS'] = ($data['ROLE_SORTS']) ? $data['ROLE_SORTS'] :
                [];
        }
        $data['NUM_USER_ROLES'] =
            $role_model->countUserRoles($user_id, $role_filter);
        if (isset($_REQUEST['role_limit'])) {
            $role_limit = min($parent->clean(
                $_REQUEST['role_limit'], 'int'),
                $data['NUM_USER_ROLES']);
            if ($role_limit == $data['NUM_USER_ROLES']) {
                $role_limit = $data['NUM_USER_ROLES']- C\NUM_RESULTS_PER_PAGE;
            }
            $role_limit = max($role_limit, 0);
        } else {
            $role_limit = 0;
        }
        $data['ROLE_LIMIT'] = $role_limit;
        $data['USER_ROLES'] =
            $role_model->getUserRoles($user_id, $role_filter,
            $data['ROLE_SORTS'], $role_limit);
        $data['SCRIPT'] .= "listenAll('input.role-id', 'click',".
            " function(event){ updateCheckedIds(role_ids, event.target); });";
    }
    /**
     * Uses $_REQUEST and $user_id to look up all the groups that a user
     * belongs to subject to $_REQUEST['group_limit'],
     * $_REQUEST['group_filter'], and $_REQUEST['group_sorts']. Information
     * about these roles is added as
     * fields to $data[NUM_USER_GROUPS'] and $data['USER_GROUPS']
     *
     * @param array &$data data for the manageUsers view.
     * @param int $user_id user to look up roles for
     */
    public function getUserGroupsData(&$data, $user_id)
    {
        $parent = $this->parent;
        $group_model = $parent->model("group");
        $data['visible_groups'] = (isset($_REQUEST['visible_groups']) &&
            $_REQUEST['visible_groups']=='true') ? 'true' : 'false';
        if ($data['visible_groups'] == 'false') {
            unset($_REQUEST['group_filter']);
            unset($_REQUEST['group_limit']);
        }
        if (isset($_REQUEST['group_filter'])) {
            $group_filter = substr($parent->clean(
                $_REQUEST['group_filter'], 'string'), 0, C\SHORT_TITLE_LEN);
        } else {
            $group_filter = "";
        }
        $data['GROUP_FILTER'] = $group_filter;
        $data['GROUP_SORTS'] = (empty($_REQUEST['group_sorts'])) ? [] :
            json_decode(urldecode($_REQUEST['group_sorts']), true);
        if ($data['GROUP_SORTS'] === null) {
            $data['GROUP_SORTS'] = json_decode(html_entity_decode(
                urldecode($_REQUEST['group_sorts'])), true);
            $data['GROUP_SORTS'] = ($data['GROUP_SORTS']) ?
                $data['GROUP_SORTS'] : [];
        }
        $data['NUM_USER_GROUPS'] =
            $group_model->countUserGroups($user_id, $group_filter);
        if (isset($_REQUEST['group_limit'])) {
            $group_limit = min($parent->clean(
                $_REQUEST['group_limit'], 'int'),
                $data['NUM_USER_GROUPS']);
            $group_limit = max($group_limit, 0);
        } else {
            $group_limit = 0;
        }
        $data['GROUP_LIMIT'] = $group_limit;
        $data['USER_GROUPS'] =
            $group_model->getUserGroups($user_id, $group_filter,
            $data['GROUP_SORTS'], $group_limit);
        $data['SCRIPT'] .= "listenAll('input.group-id', 'click',".
            " function(event){ updateCheckedIds(group_ids, event.target); });";
    }
    /**
     * Used to handle the manage role activity.
     *
     * This activity allows new roles to be added, old roles to be
     * deleted and allows activities to be added to/deleted from a role
     *
     * @return array $data information about roles in the system, activities,
     *     etc. as well as status messages on performing a given sub activity
     */
    public function manageRoles()
    {
        $parent = $this->parent;
        $role_model = $parent->model("role");
        $possible_arguments = ["addactivity", "addmodifier", "addrole",
                "deleteactivity", "deletemodifier", "deleterole", "editrole",
                "search"];
        $data["ELEMENT"] = "manageroles";
        $data['SCRIPT'] = "";
        $data['FORM_TYPE'] = "addrole";
        $search_array = [];
        $data['CURRENT_ROLE'] = ["name" => ""];
        $data['PAGING'] = "";
        $modifier_activity_id = -1;
        if (isset($_REQUEST['arg']) && $_REQUEST['arg'] == 'editrole') {
            if (isset($_REQUEST['selectactivity']) &&
                $_REQUEST['selectactivity'] >= 0) {
                $_REQUEST['arg'] = "addactivity";
            }
            if (!empty($_REQUEST['selectmodifier']) &&
                is_array($_REQUEST['selectmodifier']) &&
                !in_array("-1", $_REQUEST['selectmodifier'])) {
                $modifier_activity_id =
                    array_keys($_REQUEST['selectmodifier'])[0];
                if ($modifier_activity_id > 0) {
                    $_REQUEST['arg'] = "addmodifier";
                }
            }
        }
        if (isset($_REQUEST['name'])) {
            $name = substr($parent->clean($_REQUEST['name'], "string"),
                0, C\NAME_LEN);
             $data['CURRENT_ROLE']['name'] = $name;
        } else {
            $name = "";
        }
        if ($name != "") {
            $role_id = $role_model->getRoleId($name);
            $data['ROLE_ACTIVITIES'] =
                $role_model->getRoleActivities($role_id);
            $all_activities = $parent->model("activity")->getActivityList();
            $activity_ids = [];
            $activity_names = [];
            $data['AVAILABLE_MODIFIERS'] = [];
            foreach ($all_activities as $activity) {
                $activity_ids[] = $activity['ACTIVITY_ID'];
                $activity_names[$activity['ACTIVITY_ID']] =
                    $activity['ACTIVITY_NAME'];
                $data['AVAILABLE_MODIFIERS'][$activity['ACTIVITY_ID']] =
                    $parent->getAccessModifiers($activity['METHOD_NAME']);
            }
            $available_activities = [];
            $role_activity_ids = [];
            $activities_by_id = [];
            foreach ($data['ROLE_ACTIVITIES'] as $activity) {
                $role_activity_ids[] = $activity["ACTIVITY_ID"];
                $activities_by_id[$activity["ACTIVITY_ID"]] = $activity;
            }
            $tmp = [];
            foreach ($all_activities as $activity) {
                if (!in_array($activity["ACTIVITY_ID"], $role_activity_ids) &&
                    !isset($tmp[$activity["ACTIVITY_ID"]])) {
                    $tmp[$activity["ACTIVITY_ID"]] = true;
                    $available_activities[] = $activity;
                }
            }
            $data['AVAILABLE_ACTIVITIES'][-1] =
                tl('accountaccess_component_add_roleactivity');
            foreach ($available_activities as $activity) {
                $data['AVAILABLE_ACTIVITIES'][$activity['ACTIVITY_ID']] =
                    $activity['ACTIVITY_NAME'];
            }
            if (isset($_REQUEST['selectactivity'])) {
                $select_activity =
                    $parent->clean($_REQUEST['selectactivity'], "int" );
            } else {
                $select_activity = "";
            }
            if ($select_activity != "") {
                $data['SELECT_ACTIVITY'] = $select_activity;
            } else {
                $data['SELECT_ACTIVITY'] = -1;
            }
        }
        if (isset($_REQUEST['arg']) &&
            in_array($_REQUEST['arg'], $possible_arguments)) {
            switch ($_REQUEST['arg']) {
                case "addactivity":
                    $_REQUEST['arg'] = "editrole";
                    if (($role_id = $role_model->getRoleId($name)) <= 0) {
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_rolename_doesnt_exists'
                            ), ["arg", "start_row", "end_row", "num_show",
                            "name", "context"]);
                    } else if (!in_array($select_activity, $activity_ids)) {
                        return $parent->redirectWithMessage(
                            tl(
                            'accountaccess_component_activityname_doesnt_exists'
                            ), ["arg", "start_row", "end_row", "num_show",
                            "name", "context"]);
                    } else {
                        $role_model->addActivityRole(
                            $role_id, $select_activity);
                        unset($data['AVAILABLE_ACTIVITIES'][$select_activity]);
                        $data['ROLE_ACTIVITIES'] =
                            $role_model->getRoleActivities($role_id);
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_activity_added'),
                            ["arg", "start_row", "end_row", "num_show", "name",
                            "context"]);
                    }
                    break;
                case "addmodifier":
                    $_REQUEST['arg'] = "editrole";
                    $modifier =
                        $_REQUEST['selectmodifier'][$modifier_activity_id] ??
                        "none";
                    $available_modifiers = $data['AVAILABLE_MODIFIERS'][
                        $modifier_activity_id] ?? [];
                    if (($role_id = $role_model->getRoleId($name)) <= 0) {
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_rolename_doesnt_exists'
                            ), ["arg", "start_row", "end_row", "num_show",
                            "name", "context"]);
                    } else if (!in_array($modifier_activity_id,
                        $activity_ids)) {
                        return $parent->redirectWithMessage(
                            tl(
                            'accountaccess_component_activityname_doesnt_exists'
                            ), ["arg", "start_row", "end_row", "num_show",
                            "name", "context"]);
                    } else if (!isset($available_modifiers[$modifier])) {
                        return $parent->redirectWithMessage(
                            tl(
                            'accountaccess_component_modifier_doesnt_exists'
                            ), ["arg", "start_row", "end_row", "num_show",
                            "name", "context"]);
                    } else {
                        $activity =
                            $activities_by_id[$modifier_activity_id];
                        $allowed_arguments = $activity["ALLOWED_ARGUMENTS"];
                        if ($modifier == "all") {
                            $allowed_arguments = "all";
                        } else {
                            if ($allowed_arguments == "all") {
                                $allowed_arguments = "";
                            }
                            $comma = (trim($allowed_arguments)) ?
                                "," : "";
                            if (!str_contains($allowed_arguments, $modifier)) {
                                $allowed_arguments .= $comma . $modifier;
                            } else {
                                return $parent->redirectWithMessage(
                                    tl('accountaccess_component_modifier_exists'
                                    ), ["arg", "start_row", "end_row",
                                    "num_show","name", "context"]);
                            }
                        }
                        $role_model->updateActivityRoleArguments($role_id,
                            $modifier_activity_id,
                            $allowed_arguments);
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_modifier_added'
                            ), ["arg", "start_row", "end_row", "num_show",
                            "name", "context"]);
                    }
                    break;
                case "addrole":
                    $name = trim($name);
                    if ($name != "" && $role_model->getRoleId($name) > 0) {
                        $data['SCRIPT'] .= "doMessage('<h1 class=\"red\" >".
                            tl('accountaccess_component_rolename_exists').
                            "</h1>');";
                    } else if ($name != "") {
                        $role_model->addRole($name);
                        $data['CURRENT_ROLE']['name'] = "";
                        $data['SCRIPT'] .= "doMessage('<h1 class=\"red\" >".
                            tl('accountaccess_component_rolename_added').
                            "</h1>');";
                   } else {
                        $data['SCRIPT'] .= "doMessage('<h1 class=\"red\" >".
                            tl('accountaccess_component_rolename_blank').
                            "</h1>');";
                   }
                   $data['CURRENT_ROLE']['name'] = "";
                   break;
                case "deleteactivity":
                   $_REQUEST['arg'] = "editrole";
                   if (($role_id = $role_model->getRoleId($name)) <= 0) {
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_rolename_doesnt_exists'
                            ), ["arg", "start_row", "end_row", "num_show",
                            "name", "context"]);
                    } else if (!in_array($select_activity, $activity_ids)) {
                        return $parent->redirectWithMessage(
                            tl(
                            'accountaccess_component_activityname_doesnt_exists'
                            ), ["arg", "start_row", "end_row", "num_show",
                            "name", "context"]);
                    } else {
                        $role_model->deleteActivityRole(
                            $role_id, $select_activity);
                        $data['ROLE_ACTIVITIES'] =
                            $role_model->getRoleActivities($role_id);
                        $data['AVAILABLE_ACTIVITIES'][$select_activity] =
                            $activity_names[$select_activity];
                        $data['SELECT_ACTIVITY'] = -1;
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_activity_deleted'),
                            ["arg", "start_row", "end_row", "num_show",
                            "name", "context"]);
                    }
                    break;
                case "deletemodifier":
                    $_REQUEST['arg'] = "editrole";
                    $modifier = $parent->clean($_REQUEST['modifier'], "string")
                        ?? "none";
                    $available_modifiers = $data['AVAILABLE_MODIFIERS'][
                        $select_activity] ?? [];
                    if (!isset($available_modifiers[$modifier])) {
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_invalid_modifier'
                            ), ["arg", "start_row", "end_row", "num_show",
                            "name", "context"]);
                    } else if (($role_id = $role_model->getRoleId($name)) <= 0){
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_rolename_doesnt_exists'
                            ), ["arg", "start_row", "end_row", "num_show",
                            "name", "context"]);
                    } else if (!in_array($select_activity, $activity_ids)) {
                        return $parent->redirectWithMessage(
                            tl(
                            'accountaccess_component_activityname_doesnt_exists'
                            ), ["arg", "start_row", "end_row", "num_show",
                            "name", "context"]);
                    }  else {
                        $activity =
                            $activities_by_id[$select_activity];
                        $allowed_arguments = $activity["ALLOWED_ARGUMENTS"];
                        if (!str_contains($allowed_arguments, $modifier)) {
                            return $parent->redirectWithMessage(
                                tl('accountaccess_component_mod_doesnt_exists'
                                ), ["arg", "start_row", "end_row",
                                "num_show","name", "context"]);
                        }
                        $allowed_arguments = preg_replace(
                            "/".preg_quote($modifier)."\s*\,?\s*/", "",
                            $allowed_arguments);
                        if (empty($allowed_arguments)) {
                            $allowed_arguments = "all";
                        }
                        $role_model->updateActivityRoleArguments($role_id,
                            $select_activity,
                            $allowed_arguments);
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_modifier_deleted'
                            ), ["arg", "start_row", "end_row",
                            "num_show","name", "context"]);
                    }
                    break;
                case "deleterole":
                    $preserve = [];
                    if (!empty($_REQUEST['context'])) {
                        $_REQUEST['arg'] = 'search';
                        $preserve[] = 'arg';
                    }
                    if (($role_id = $role_model->getRoleId($name)) <= 0) {
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_rolename_doesnt_exists'
                            ),array_merge($preserve, ["start_row", "end_row",
                            "num_show"]));
                    } else {
                        $role_model->deleteRole($role_id);
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_rolename_deleted'),
                            array_merge($preserve, ["start_row", "end_row",
                            "num_show"]));
                    }
                    break;
                case "editrole":
                    $data['FORM_TYPE'] = "editrole";
                    $role = false;
                    if ($name) {
                        $role = $role_model->getRole($name);
                    }
                    if ($role === false) {
                        $data['FORM_TYPE'] = "addrole";
                        break;
                    }
                    if (!empty($_REQUEST['context'])) {
                        $data['context'] = 'search';
                    }
                    $num_activities = count($data['ROLE_ACTIVITIES']);
                    $update_activities = $_REQUEST['activities'] ?? [];
                    if (!$update_activities) {
                        break;
                    }
                    $update = false;
                    for ($i  = 0; $i < $num_activities; $i++) {
                        $role_activity = $data['ROLE_ACTIVITIES'][$i];
                        $activity_id = $role_activity['ACTIVITY_ID'];
                        if (isset($update_activities[$activity_id]) &&
                            $update_activities[$activity_id] !=
                            $role_activity['ALLOWED_ARGUMENTS']) {
                            $role_model->updateActivityRoleArguments($role_id,
                                $activity_id, $update_activities[$activity_id]);
                            $update = true;
                        }
                    }
                    if ($update) {
                        return $parent->redirectWithMessage(
                            tl('accountaccess_component_role_updated'),
                            ["arg", "name", "start_row", "end_row", "num_show",
                            "context"]);
                    }
                    break;
                case "search":
                    $search_array = $parent->tableSearchRequestHandler($data,
                        "manageRoles", ['ALL_FIELDS' => ['name']]);
                    if (empty($_SESSION['LAST_SEARCH']['manageRoles']) ||
                        isset($_REQUEST['name'])) {
                        $_SESSION['LAST_SEARCH']['manageRoles'] =
                            $_SESSION['SEARCH']['manageRoles'];
                        unset($_SESSION['SEARCH']['manageRoles']);
                    } else {
                        $default_search = true;
                    }
                    break;
            }
        }
        if ($search_array == [] || !empty($default_search)) {
            if (!empty($_SESSION['LAST_SEARCH']['manageRoles'])) {
                if (!empty($_REQUEST['arg']) && $_REQUEST['arg'] == 'search') {
                    $search_array =
                        $parent->restoreLastSearchFromSession($data,
                        'manageRoles');
                } else if (!empty($_REQUEST['context'])) {
                    $search_array =
                        $_SESSION['LAST_SEARCH']['manageRoles']['SEARCH_ARRAY'];
                    $data['PAGING'] =
                        $_SESSION['LAST_SEARCH']['manageRoles']['PAGING'];
                }
            }
            if ($search_array == []) {
                $search_array[] = ["name", "", "", "ASC"];
            }
        }
        if ($data['FORM_TYPE'] == 'addrole') {
            $data['SCRIPT'] .= "setDisplay('admin-form-row', false);";
        }
        $parent->pagingLogic($data, $role_model, "ROLES",
            C\DEFAULT_ADMIN_PAGING_NUM, $search_array, "");
        return $data;
    }
}
