<?php /*

 Composr
 Copyright (c) Christopher Graham, 2004-2024

 See docs/LICENSE.md for full licensing information.


 NOTE TO PROGRAMMERS:
   Do not edit this file. If you need to make changes, save your changed file to the appropriate *_custom folder
   **** If you ignore this advice, then your website upgrades (e.g. for bug fixes) will likely kill your changes ****

*/

/**
 * @license    http://opensource.org/licenses/cpal_1.0 Common Public Attribution License
 * @copyright  Christopher Graham
 * @package    core_cns
 */

/**
 * Find whether a certain member may control a certain usergroup.
 *
 * @param  GROUP $group_id The usergroup
 * @param  MEMBER $member_id The member
 * @param  ?array $group_row Database row for usergroup, passed for performance optimisation (null: lookup)
 * @return boolean The answer
 */
function cns_may_control_group(int $group_id, int $member_id, ?array $group_row = null) : bool
{
    if ($group_row === null) {
        $leader = cns_get_group_property($group_id, 'group_lead_member');
        $is_super_admin = cns_get_group_property($group_id, 'is_super_admin');
    } else {
        $leader = $group_row['g_group_lead_member'];
        $is_super_admin = $group_row['g_is_super_admin'];
    }
    return (($member_id === $leader) || ($GLOBALS['CNS_DRIVER']->is_super_admin($member_id)) || ((has_privilege($member_id, 'control_usergroups')) && ($is_super_admin == 0)));
}

/**
 * Edit a usergroup.
 *
 * @param  AUTO_LINK $group_id The ID of the usergroup to edit
 * @param  ?SHORT_TEXT $name The name of the usergroup. (null: do not change)
 * @param  ?BINARY $is_default Whether members are automatically put into the when they join. (null: do not change)
 * @param  ?BINARY $is_super_admin Whether members of this usergroup are all super-administrators. (null: do not change)
 * @param  ?BINARY $is_super_moderator Whether members of this usergroup are all super-moderators. (null: do not change)
 * @param  ?SHORT_TEXT $title The title for primary members of this usergroup that don't have their own title. (null: do not change)
 * @param  ?URLPATH $rank_image The rank image for this. (null: do not change)
 * @param  ?GROUP $promotion_target The that members of this usergroup get promoted to at point threshold (null: no promotion prospects)
 * @param  ?integer $promotion_threshold The point threshold for promotion (null: no promotion prospects)
 * @param  ?BINARY $promotion_approval Whether usergroup promotion should require manual approval (null: do not change)
 * @param  ?MEMBER $group_leader The leader of this usergroup (null: none)
 * @param  ?integer $flood_control_submit_secs The number of seconds that members of this usergroup must endure between submits (group 'best of' applies). (null: do not change)
 * @param  ?integer $flood_control_access_secs The number of seconds that members of this usergroup must endure between accesses (group 'best of' applies). (null: do not change)
 * @param  ?integer $max_daily_upload_mb The number of megabytes that members of this usergroup may attach per day (group 'best of' applies). (null: do not change)
 * @param  ?integer $max_attachments_per_post The number of attachments that members of this usergroup may attach to something (group 'best of' applies). (null: do not change)
 * @param  ?integer $max_avatar_width The maximum avatar width that members of this usergroup may have (group 'best of' applies). (null: do not change)
 * @param  ?integer $max_avatar_height The maximum avatar height that members of this usergroup may have (group 'best of' applies). (null: do not change)
 * @param  ?integer $max_post_length_comcode The maximum post length that members of this usergroup may make (group 'best of' applies). (null: do not change)
 * @param  ?integer $max_sig_length_comcode The maximum signature length that members of this usergroup may make (group 'best of' applies). (null: do not change)
 * @param  ?integer $gift_points_base The number of gift points that members of this usergroup start with (group 'best of' applies). (null: do not change)
 * @param  ?integer $gift_points_per_day The number of gift points that members of this usergroup get per day (group 'best of' applies). (null: do not change)
 * @param  ?BINARY $enquire_on_new_ips Whether e-mail confirmation is needed for new IP addresses seen for any member of this usergroup (group 'best of' applies). (null: do not change)
 * @param  ?BINARY $is_presented_at_install Whether the is presented for joining at joining (implies anyone may be in the, but only choosable at joining) (null: do not change)
 * @param  ?BINARY $hidden Whether the name and membership of the is hidden (null: do not change)
 * @param  ?integer $order The display order this will be given, relative to other usergroups. Lower numbered usergroups display before higher numbered usergroups. (null: do not change)
 * @param  ?BINARY $rank_image_pri_only Whether the rank image will not be shown for secondary membership (null: do not change)
 * @param  ?BINARY $open_membership Whether members may join this usergroup without requiring any special permission (null: do not change)
 * @param  ?BINARY $is_private_club Whether this usergroup is a private club. Private clubs may be managed in the CMS zone, and do not have any special permissions - except over their own associated forum. (null: do not change)
 * @param  boolean $uniqify Whether to force the title as unique, if there's a conflict
 */
function cns_edit_group(int $group_id, ?string $name, ?int $is_default, ?int $is_super_admin, ?int $is_super_moderator, ?string $title, ?string $rank_image, ?int $promotion_target, ?int $promotion_threshold, ?int $promotion_approval, ?int $group_leader, ?int $flood_control_submit_secs, ?int $flood_control_access_secs, ?int $max_daily_upload_mb, ?int $max_attachments_per_post, ?int $max_avatar_width, ?int $max_avatar_height, ?int $max_post_length_comcode, ?int $max_sig_length_comcode, ?int $gift_points_base, ?int $gift_points_per_day, ?int $enquire_on_new_ips, ?int $is_presented_at_install, ?int $hidden, ?int $order, ?int $rank_image_pri_only, ?int $open_membership, ?int $is_private_club, bool $uniqify = false)
{
    $test = $GLOBALS['FORUM_DB']->query_select_value_if_there('f_groups', 'id', [$GLOBALS['FORUM_DB']->translate_field_ref('g_name') => $name]);
    if (($test !== null) && ($test != $group_id)) {
        if ($uniqify) {
            $name .= '_' . uniqid('', false);
        } else {
            warn_exit(do_lang_tempcode('ALREADY_EXISTS', escape_html($name)));
        }
    }

    $_group_info = $GLOBALS['FORUM_DB']->query_select('f_groups', ['g_name', 'g_title', 'g_rank_image'], ['id' => $group_id], '', 1);
    if (!array_key_exists(0, $_group_info)) {
        warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'group'));
    }
    $_name = $_group_info[0]['g_name'];
    $_title = $_group_info[0]['g_title'];

    if (get_translated_text($_name) == get_option('probation_usergroup')) {
        require_code('config2');
        set_option('probation_usergroup', $name);
    }

    $map = [];
    if ($name !== null) {
        $map += lang_remap('g_name', $_name, $name, $GLOBALS['FORUM_DB']);
    }
    if ($is_default !== null) {
        $map['g_is_default'] = $is_default;
    }
    if ($is_presented_at_install !== null) {
        $map['g_is_presented_at_install'] = $is_presented_at_install;
    }
    if ($is_super_admin !== null) {
        $map['g_is_super_admin'] = $is_super_admin;
    }
    if ($is_super_moderator !== null) {
        $map['g_is_super_moderator'] = $is_super_moderator;
    }
    $map['g_group_lead_member'] = $group_leader;
    if ($title !== null) {
        $map += lang_remap('g_title', $_title, $title, $GLOBALS['FORUM_DB']);
    }
    if (addon_installed('points')) {
        $map['g_promotion_target_group'] = $promotion_target;
        $map['g_promotion_threshold'] = $promotion_threshold;
        $map['g_promotion_approval'] = $promotion_approval;
    }
    if ($flood_control_submit_secs !== null) {
        $map['g_flood_control_submit_secs'] = $flood_control_submit_secs;
    }
    if ($flood_control_access_secs !== null) {
        $map['g_flood_control_access_secs'] = $flood_control_access_secs;
    }
    if ($max_daily_upload_mb !== null) {
        $map['g_max_daily_upload_mb'] = $max_daily_upload_mb;
    }
    if ($max_attachments_per_post !== null) {
        $map['g_max_attachments_per_post'] = $max_attachments_per_post;
    }
    if ($max_avatar_width !== null) {
        $map['g_max_avatar_width'] = $max_avatar_width;
    }
    if ($max_avatar_height !== null) {
        $map['g_max_avatar_height'] = $max_avatar_height;
    }
    if ($max_post_length_comcode !== null) {
        $map['g_max_post_length_comcode'] = $max_post_length_comcode;
    }
    if ($max_sig_length_comcode !== null) {
        $map['g_max_sig_length_comcode'] = $max_sig_length_comcode;
    }
    if (addon_installed('points') && get_option('enable_gift_points') == '1') {
        if ($gift_points_base !== null) {
            $map['g_gift_points_base'] = $gift_points_base;
        }
        if ($gift_points_per_day !== null) {
            $map['g_gift_points_per_day'] = $gift_points_per_day;
        }
    }
    if ($enquire_on_new_ips !== null) {
        $map['g_enquire_on_new_ips'] = $enquire_on_new_ips;
    }
    if ($rank_image !== null) {
        $map['g_rank_image'] = $rank_image;
    }
    if ($hidden !== null) {
        $map['g_hidden'] = $hidden;
    }
    if ($order !== null) {
        $map['g_order'] = $order;
    }
    if ($rank_image_pri_only !== null) {
        $map['g_rank_image_pri_only'] = $rank_image_pri_only;
    }
    if ($open_membership !== null) {
        $map['g_open_membership'] = $open_membership;
    }
    if ($is_private_club !== null) {
        $map['g_is_private_club'] = $is_private_club;
    }

    $GLOBALS['FORUM_DB']->query_update('f_groups', $map, ['id' => $group_id], '', 1);

    require_code('urls2');
    suggest_new_idmoniker_for('groups', 'view', strval($group_id), '', $name);

    require_code('themes2');
    tidy_theme_img_code($rank_image, $_group_info[0]['g_rank_image'], 'f_groups', 'g_rank_image', $GLOBALS['FORUM_DB']);

    log_it('EDIT_GROUP', strval($group_id), $name);

    if ((addon_installed('commandr')) && (!running_script('install')) && (!get_mass_import_mode())) {
        require_code('resource_fs');
        generate_resource_fs_moniker('group', strval($group_id));
    }

    persistent_cache_delete('GROUPS');
    persistent_cache_delete('GROUPS_PO');
    persistent_cache_delete('SUPER_ADMIN_GROUPS');
    persistent_cache_delete('SUPER_MODERATOR_GROUPS');
    persistent_cache_delete('OPEN_GROUPS');

    require_code('sitemap_xml');
    notify_sitemap_node_edit('_SEARCH:groups:view:' . strval($group_id), $hidden == 0);
}

/**
 * Delete a usergroup.
 *
 * @param  AUTO_LINK $group_id The ID of the usergroup to delete
 * @param  ?GROUP $target_group The usergroup to move primary members to (null: main members)
 */
function cns_delete_group(int $group_id, ?int $target_group = null)
{
    $orig_target_group = $target_group;
    require_code('cns_groups');
    if ($target_group === null) {
        $target_group = get_first_default_group();
    }

    require_code('cns_groups2');
    if (in_array($group_id, get_all_preserved_groups(true))) {
        fatal_exit(do_lang_tempcode('INTERNAL_ERROR'));
    }

    $_group_info = $GLOBALS['FORUM_DB']->query_select('f_groups', ['g_name', 'g_title', 'g_rank_image'], ['id' => $group_id], '', 1);
    if (!array_key_exists(0, $_group_info)) {
        warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'group'));
    }
    $_name = $_group_info[0]['g_name'];
    $_title = $_group_info[0]['g_title'];
    $name = get_translated_text($_name, $GLOBALS['FORUM_DB']);
    delete_lang($_name, $GLOBALS['FORUM_DB']);
    delete_lang($_title, $GLOBALS['FORUM_DB']);

    $GLOBALS['FORUM_DB']->query_update('f_groups', ['g_promotion_target_group' => null], ['g_promotion_target_group' => $group_id]);
    $GLOBALS['FORUM_DB']->query_update('f_members', ['m_primary_group' => $target_group], ['m_primary_group' => $group_id]);
    if ($orig_target_group !== null) {
        $GLOBALS['FORUM_DB']->query_update('f_group_members', ['gm_group_id' => $target_group], ['gm_group_id' => $group_id], '', null, 0, false, true); // Errors suppressed in case rows conflict with existing
    }
    $GLOBALS['FORUM_DB']->query_delete('f_group_members', ['gm_group_id' => $group_id]);
    $GLOBALS['FORUM_DB']->query_delete('f_groups', ['id' => $group_id], '', 1);

    // No need to delete permission stuff, as it could be on any MSN site, and the software is coded with a tolerance due to the forum driver system. However, to be tidy...
    $GLOBALS['SITE_DB']->query_delete('group_privileges', ['group_id' => $group_id]);
    if (is_on_multi_site_network() && (get_forum_type() == 'cns')) {
        $GLOBALS['FORUM_DB']->query_delete('group_privileges', ['group_id' => $group_id]);
    }
    $GLOBALS['SITE_DB']->query_delete('group_zone_access', ['group_id' => $group_id]);
    $GLOBALS['SITE_DB']->query_delete('group_category_access', ['group_id' => $group_id]);
    if (is_on_multi_site_network() && (get_forum_type() == 'cns')) {
        $GLOBALS['SITE_DB']->query_delete('group_category_access', ['group_id' => $group_id]);
    }
    $GLOBALS['SITE_DB']->query_delete('group_page_access', ['group_id' => $group_id]);
    if (addon_installed('ecommerce')) {
        $GLOBALS['FORUM_DB']->query_delete('f_usergroup_subs', ['s_group_id' => $group_id]);
    }
    $GLOBALS['FORUM_DB']->query_delete('f_group_member_timeouts', ['group_id' => $group_id]);

    require_code('themes2');
    tidy_theme_img_code(null, $_group_info[0]['g_rank_image'], 'f_groups', 'g_rank_image', $GLOBALS['FORUM_DB']);

    if (addon_installed('catalogues')) {
        update_catalogue_content_ref('group', strval($group_id), '');
    }

    $GLOBALS['SITE_DB']->query_update('url_id_monikers', ['m_deprecated' => 1], ['m_resource_page' => 'groups', 'm_resource_type' => 'view', 'm_resource_id' => strval($group_id)]);

    // Clean up approval records
    $GLOBALS['FORUM_DB']->query_delete('f_group_approvals', ['ga_new_group_id' => $group_id]);
    $map = ['ga_status' => -1, 'ga_status_member_id' => $GLOBALS['FORUM_DRIVER']->get_guest_id()];
    $map += insert_lang_comcode('ga_reason', do_lang('ORIGINAL_USERGROUP_DELETED'), 4, $GLOBALS['FORUM_DB']);
    $GLOBALS['FORUM_DB']->query_update('f_group_approvals', $map, ['ga_old_group_id' => $group_id, 'ga_status' => 0]);

    log_it('DELETE_GROUP', strval($group_id), $name);

    if ((addon_installed('commandr')) && (!running_script('install')) && (!get_mass_import_mode())) {
        require_code('resource_fs');
        expunge_resource_fs_moniker('group', strval($group_id));
    }

    persistent_cache_delete('GROUPS_COUNT');
    persistent_cache_delete('GROUPS_COUNT_PO');
    persistent_cache_delete('GROUPS');
    persistent_cache_delete('GROUPS_PO');
    persistent_cache_delete('SUPER_ADMIN_GROUPS');
    persistent_cache_delete('SUPER_MODERATOR_GROUPS');
    persistent_cache_delete('OPEN_GROUPS');

    require_code('sitemap_xml');
    notify_sitemap_node_delete('_SEARCH:groups:view:' . strval($group_id));
}

/**
 * Mark a member as applying to be in a certain group, and inform the leader.
 *
 * @param  GROUP $group_id The usergroup to apply to
 * @param  ?MEMBER $member_id The member applying (null: current member)
 */
function cns_member_ask_join_group(int $group_id, ?int $member_id = null)
{
    require_code('notifications');

    $group_info = $GLOBALS['FORUM_DB']->query_select('f_groups', ['g_name', 'g_group_lead_member'], ['id' => $group_id], '', 1);
    if (!array_key_exists(0, $group_info)) {
        warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'group'));
    }

    if ($member_id === null) {
        $member_id = get_member();
    }

    if (cns_is_ldap_member($member_id)) {
        return;
    }

    $their_username = $GLOBALS['CNS_DRIVER']->get_member_row_field($member_id, 'm_username');

    $test = $GLOBALS['FORUM_DB']->query_select_value_if_there('f_group_members', 'gm_member_id', ['gm_member_id' => $member_id, 'gm_group_id' => $group_id]);
    if ($test !== null) {
        warn_exit(do_lang_tempcode('ALREADY_IN_GROUP'));
    }
    $test = $GLOBALS['FORUM_DB']->query_select_value_if_there('f_group_approvals', 'id', ['ga_member_id' => $member_id, 'ga_new_group_id' => $group_id, 'ga_status' => 0]);
    if ($test !== null) {
        warn_exit(do_lang_tempcode('ALREADY_APPLIED_FOR_GROUP'));
    }

    $test = $GLOBALS['FORUM_DB']->query_select_value('f_groups', 'g_is_presented_at_install', ['id' => $group_id]);
    if ($test == 1) {
        $GLOBALS['FORUM_DB']->query_insert('f_group_members', [
            'gm_group_id' => $group_id,
            'gm_member_id' => $member_id,
        ]);
        $GLOBALS['FORUM_DB']->query_insert('f_group_join_log', [
            'member_id' => $member_id,
            'usergroup_id' => $group_id,
            'join_time' => time(),
        ]);
    } else {
        $map = [
            'ga_date_and_time' => time(),
            'ga_member_id' => $member_id,
            'ga_member_username' => $their_username,
            'ga_old_group_id' => null,
            'ga_new_group_id' => $group_id,
            'ga_status' => 0,
        ];
        $map += insert_lang_comcode('ga_reason', '', 4, $GLOBALS['FORUM_DB']);
        $id = $GLOBALS['FORUM_DB']->query_insert('f_group_approvals', $map, true);

        $group_name = get_translated_text($group_info[0]['g_name'], $GLOBALS['FORUM_DB']);
        $_url = build_url(['page' => 'groups', 'type' => 'view', 'id' => $group_id, 'approval_id' => $id], get_module_zone('groups'), [], false, false, true);
        $url = $_url->evaluate();

        $leader_id = $group_info[0]['g_group_lead_member'];
        if ($leader_id !== null) {
            $mail = do_notification_lang('GROUP_JOIN_REQUEST_MAIL', comcode_escape($their_username), comcode_escape($group_name), [$url], get_lang($leader_id));
            $subject = do_lang('GROUP_JOIN_REQUEST_MAIL_SUBJECT', null, null, null, get_lang($leader_id));
            dispatch_notification('cns_group_join_request', null, $subject, $mail, [$leader_id]);
        } else {
            $mail = do_notification_lang('GROUP_JOIN_REQUEST_MAIL', comcode_escape($their_username), comcode_escape($group_name), [$url], get_site_default_lang());
            $subject = do_lang('GROUP_JOIN_REQUEST_MAIL_SUBJECT', null, null, null, get_site_default_lang());
            dispatch_notification('cns_group_join_request_staff', null, $subject, $mail, null, get_member(), ['use_real_from' => true]);
        }
    }
}

/**
 * Remove a usergroup from a member's secondary usergroup memberships. Be sure to call cns_update_group_approvals at some point after this.
 *
 * @param  GROUP $group_id The usergroup to remove from
 * @param  ?MEMBER $member_id The member leaving (null: current member)
 */
function cns_member_leave_secondary_group(int $group_id, ?int $member_id = null)
{
    if ($member_id === null) {
        $member_id = get_member();
    }

    if (cns_is_ldap_member($member_id)) {
        return;
    }

    $group_leader = $GLOBALS['FORUM_DB']->query_select_value('f_groups', 'g_group_lead_member', ['id' => $group_id]);
    if ($group_leader == $member_id) {
        $GLOBALS['FORUM_DB']->query_update('f_groups', ['g_group_lead_member' => null], ['id' => $group_id], '', 1);
    }

    $test = $GLOBALS['FORUM_DB']->query_select_value_if_there('f_group_members', 'gm_group_id', ['gm_group_id' => $group_id, 'gm_member_id' => $member_id]);
    if ($test !== null) {
        $GLOBALS['FORUM_DB']->query_delete('f_group_members', ['gm_group_id' => $group_id, 'gm_member_id' => $member_id], '', 1);

        $GLOBALS['FORUM_DB']->query_delete('f_group_join_log', [
            'member_id' => $member_id,
            'usergroup_id' => $group_id,
        ]);

        log_it('MEMBER_REMOVED_FROM_GROUP', strval($member_id), strval($group_id));
    }
}

/**
 * Add a usergroup to a member's secondary usergroup memberships. Be sure to call cns_update_group_approvals at some point after this.
 *
 * @param  MEMBER $member_id The member
 * @param  GROUP $id The usergroup
 * @param  BINARY $validated Whether the member is validated into the usergroup
 */
function cns_add_member_to_secondary_group(int $member_id, int $id, int $validated = 1)
{
    if (cns_is_ldap_member($member_id)) {
        return;
    }

    $test = $GLOBALS['FORUM_DB']->query_select_value_if_there('f_groups', 'g_is_presented_at_install', ['id' => $id]);
    if ($test === null) {
        warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'group'));
    }

    $username = $GLOBALS['FORUM_DRIVER']->get_username($member_id);

    $GLOBALS['FORUM_DB']->query_delete('f_group_members', [
        'gm_group_id' => $id,
        'gm_member_id' => $member_id,
    ], '', 1);

    if ($validated == 1) {
        $GLOBALS['FORUM_DB']->query_insert('f_group_members', [
            'gm_group_id' => $id,
            'gm_member_id' => $member_id,
        ], false, true);
    } else {
        $map = [
            'ga_date_and_time' => time(),
            'ga_member_id' => $member_id,
            'ga_member_username' => $username,
            'ga_old_group_id' => null,
            'ga_new_group_id' => $id,
            'ga_status' => 0,
        ];
        $map += insert_lang_comcode('ga_reason', '', 4, $GLOBALS['FORUM_DB']);
        $GLOBALS['FORUM_DB']->query_insert('f_group_approvals', $map);
    }

    log_it('MEMBER_ADDED_TO_GROUP', strval($member_id), strval($id));

    if (cns_get_group_property($id, 'hidden') == 0) {
        require_lang('cns');
        require_code('notifications');
        $displayname = $GLOBALS['FORUM_DRIVER']->get_username($member_id, true);
        $group_name = cns_get_group_name($id);
        $subject = do_lang('MJG_NOTIFICATION_MAIL_SUBJECT', get_site_name(), $username, $group_name);
        $group_url = build_url(['page' => 'groups', 'type' => 'view', 'id' => $id], get_module_zone('groups'), [], false, false, true);
        $mail = do_notification_lang('MJG_NOTIFICATION_MAIL', comcode_escape(get_site_name()), comcode_escape($username), [comcode_escape($group_name), $group_url->evaluate(), comcode_escape($displayname)]);
        dispatch_notification('cns_member_joined_group', strval($id), $subject, $mail);
    }

    if ($validated == 1) {
        $GLOBALS['FORUM_DB']->query_insert('f_group_join_log', [
            'member_id' => $member_id,
            'usergroup_id' => $id,
            'join_time' => time(),
        ]);
    }
}

/**
 * Set whether a member that has applied to be in a usergroup (or is pending a promotion approval) may be in it, and inform them of the decision.
 *
 * @param  GROUP $group_id The usergroup
 * @param  MEMBER $prospective_member_id The prospective member
 * @param  boolean $decline Whether the member is being declined membership
 * @param  string $reason The reason given for declining
 */
function cns_member_validate_into_group(int $group_id, int $prospective_member_id, bool $decline = false, string $reason = '')
{
    if (cns_is_ldap_member($prospective_member_id)) {
        return;
    }

    require_code('notifications');

    // Check if the member is actually pending to be added to the usergroup (if not, cns_add_member_to_secondary_group should be used instead)
    $pending = $GLOBALS['FORUM_DB']->query_select_value_if_there('f_group_approvals', 'id', ['ga_member_id' => $prospective_member_id, 'ga_new_group_id' => $group_id, 'ga_status' => 0]);
    if ($pending === null) {
        warn_exit(do_lang_tempcode('INTERNAL_ERROR'));
    }

    $GLOBALS['FORUM_DB']->query_delete('f_group_members', ['gm_member_id' => $prospective_member_id, 'gm_group_id' => $group_id], '', 1);

    $name = cns_get_group_name($group_id);

    if (!$decline) {
        // Remove member from old usergroups in pending promotion approvals
        $rows = $GLOBALS['FORUM_DB']->query_select('f_group_approvals', ['ga_old_group_id'], ['ga_member_id' => $prospective_member_id, 'ga_new_group_id' => $group_id, 'ga_status' => 0]);
        $is_primary = false;
        $removed_usergroups = [];
        foreach ($rows as $row) {
            if ($row['ga_old_group_id'] === null) {
                continue;
            }

            // If the old group is the member's current primary usergroup, change their primary usergroup to $group_id (and clean out approval records)
            $current_primary_usergroup = $GLOBALS['FORUM_DRIVER']->get_member_row_field($prospective_member_id, 'm_primary_group');
            if ($current_primary_usergroup == $row['ga_old_group_id']) {
                $GLOBALS['FORUM_DB']->query_update('f_members', ['m_primary_group' => $group_id], ['id' => $prospective_member_id], '', 1);
                $is_primary = true;
            }

            cns_member_leave_secondary_group($row['ga_old_group_id'], $prospective_member_id);
            $removed_usergroups[] = $row['ga_old_group_id'];
        }

        // If this usergroup did not become the member's primary usergroup, add it as a secondary usergroup.
        if (!$is_primary) {
            $GLOBALS['FORUM_DB']->query_insert('f_group_members', [
                'gm_group_id' => $group_id,
                'gm_member_id' => $prospective_member_id,
            ]);
        } else {
            // Decache and log
            unset($GLOBALS['FORUM_DRIVER']->MEMBER_ROWS_CACHED[$prospective_member_id]);
            unset($GLOBALS['MEMBER_CACHE_FIELD_MAPPINGS'][$prospective_member_id]);
            unset($GLOBALS['TIMEZONE_MEMBER_CACHE'][$prospective_member_id]);
            unset($GLOBALS['USER_NAME_CACHE'][$prospective_member_id]);
            log_it('MEMBER_PRIMARY_GROUP_CHANGED', strval($prospective_member_id), strval($group_id));
        }

        $GLOBALS['FORUM_DB']->query_insert('f_group_join_log', [
            'member_id' => $prospective_member_id,
            'usergroup_id' => $group_id,
            'join_time' => time(),
        ]);

        log_it('MEMBER_ADDED_TO_GROUP', strval($prospective_member_id), strval($group_id));

        $mail = do_notification_lang('GROUP_ACCEPTED_MAIL', get_site_name(), $name, null, get_lang($prospective_member_id));
        $subject = do_lang('GROUP_ACCEPTED_MAIL_SUBJECT', $name, null, null, get_lang($prospective_member_id));

        // This will also handle marking approved the approval record if applicable since the member was added to the usergroup
        cns_update_group_approvals($prospective_member_id, get_member(), $removed_usergroups);
    } else {
        // Decline the approval
        $map = ['ga_status' => -1, 'ga_status_member_id' => get_member()];
        $map += insert_lang_comcode('ga_reason', $reason, 4, $GLOBALS['FORUM_DB']);
        $GLOBALS['FORUM_DB']->query_update('f_group_approvals', $map, ['ga_member_id' => $prospective_member_id, 'ga_new_group_id' => $group_id, 'ga_status' => 0]);

        if ($reason != '') {
            $mail = do_notification_lang('GROUP_DECLINED_MAIL_REASON', comcode_escape(get_site_name()), comcode_escape($name), comcode_escape($reason), get_lang($prospective_member_id));
        } else {
            $mail = do_notification_lang('GROUP_DECLINED_MAIL', comcode_escape(get_site_name()), comcode_escape($name), null, get_lang($prospective_member_id));
        }
        $subject = do_lang('GROUP_DECLINED_MAIL_SUBJECT', $name, null, null, get_lang($prospective_member_id));
    }

    dispatch_notification('cns_group_status', null, $subject, $mail, [$prospective_member_id]);
}

/**
 * Call this after making changes to a member's usergroups to update approval records.
 *
 * @param  MEMBER $member_id The member whose usergroups we are checking
 * @param  ?MEMBER $status_member_id The member who made the changes to the usergroups (null: the system)
 * @param  array $groups_to_check Array of usergroups that the member could possibly have been removed from (e.g. all the groups they belonged in before changes were made, or the explicit groups from which they were removed)
 */
function cns_update_group_approvals(int $member_id, ?int $status_member_id = null, array $groups_to_check = []) : void
{
    require_lang('cns');

    if ($status_member_id === null) {
        $status_member_id = $GLOBALS['FORUM_DRIVER']->get_guest_id();
    }

    // Get all of the usergroups this member belongs in right now; skip cache in case changes were recently made
    $all_current_groups = $GLOBALS['FORUM_DRIVER']->get_members_groups($member_id, false, true, true);

    // Mark all approval records approved for current usergroups the member is in
    foreach ($all_current_groups as $group) {
        $map = ['ga_status' => 1, 'ga_status_member_id' => $status_member_id];
        $map += insert_lang_comcode('ga_reason', do_lang('UNCONVENTIONAL_MEMBER_ADDED_TO_GROUP'), 4, $GLOBALS['FORUM_DB']);
        $GLOBALS['FORUM_DB']->query_update('f_group_approvals', $map, ['ga_member_id' => $member_id, 'ga_new_group_id' => $group, 'ga_status' => 0]);
    }

    // Mark all approval records declined for usergroups which the member was supposed to promote from but is no longer in
    foreach ($groups_to_check as $group) {
        if (in_array($group, $all_current_groups)) { // Member is still in the usergroup, so skip it
            continue;
        }
        $map = ['ga_status' => -1, 'ga_status_member_id' => $status_member_id];
        $map += insert_lang_comcode('ga_reason', do_lang('ORIGINAL_USERGROUP_RESIGNED'), 4, $GLOBALS['FORUM_DB']);
        $GLOBALS['FORUM_DB']->query_update('f_group_approvals', $map, ['ga_member_id' => $member_id, 'ga_old_group_id' => $group, 'ga_status' => 0]);
    }
}

/**
 * Copy permissions relating to one, to another.
 *
 * @param  GROUP $to The that is having its permissions replaced
 * @param  GROUP $from The that the permissions are being drawn from
 */
function cns_group_absorb_privileges_of(int $to, int $from)
{
    _cns_group_absorb_privileges_of($to, $from, 'group_category_access');
    _cns_group_absorb_privileges_of($to, $from, 'group_zone_access');
    _cns_group_absorb_privileges_of($to, $from, 'group_page_access');
    _cns_group_absorb_privileges_of($to, $from, 'group_privileges');
}

/**
 * Helper function, for copy permissions relating to one, to another.
 *
 * @param  GROUP $to The record ID that is having its permissions replaced
 * @param  GROUP $from The record ID that the permissions are being drawn from
 * @param  ID_TEXT $table The table holding the permissions
 * @param  ID_TEXT $id The name of the field in the table that holds the ID
 * @ignore
 */
function _cns_group_absorb_privileges_of(int $to, int $from, string $table, string $id = 'group_id')
{
    if ($to == $from) {
        fatal_exit(do_lang_tempcode('INTERNAL_ERROR'));
    }

    $db = get_db_for($table);

    $db->query_delete($table, [$id => $to]);
    $rows = $db->query_select($table, ['*'], [$id => $from]);
    foreach ($rows as $row) {
        $row[$id] = $to;
        $db->query_insert($table, $row);
    }
}
