<?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
 */

/**
 * Module page class.
 */
class Module_groups
{
    /**
     * Find details of the module.
     *
     * @return ?array Map of module info (null: module is disabled)
     */
    public function info() : ?array
    {
        $info = [];
        $info['author'] = 'Chris Graham';
        $info['organisation'] = 'Composr';
        $info['hacked_by'] = null;
        $info['hack_version'] = null;
        $info['version'] = 2;
        $info['locked'] = false;
        $info['min_cms_version'] = 11.0;
        $info['addon'] = 'core_cns';
        return $info;
    }

    /**
     * Find entry-points available within this module.
     *
     * @param  boolean $check_perms Whether to check permissions
     * @param  ?MEMBER $member_id The member to check permissions as (null: current user)
     * @param  boolean $support_crosslinks Whether to allow cross links to other modules (identifiable via a full-page-link rather than a screen-name)
     * @param  boolean $be_deferential Whether to avoid any entry-point (or even return null to disable the page in the Sitemap) if we know another module, or page_group, is going to link to that entry-point. Note that "!" and "browse" entry points are automatically merged with container page nodes (likely called by page-groupings) as appropriate.
     * @return ?array A map of entry points (screen-name=>language-code/string or screen-name=>[language-code/string, icon-theme-image]) (null: disabled)
     */
    public function get_entry_points(bool $check_perms = true, ?int $member_id = null, bool $support_crosslinks = true, bool $be_deferential = false) : ?array
    {
        if (get_forum_type() != 'cns') {
            return null;
        }

        return [
            'browse' => ['USERGROUPS', 'menu/social/groups'],
        ];
    }

    public $title;
    public $id;
    public $group;
    public $group_name;
    public $club;

    /**
     * Module pre-run function. Allows us to know metadata for <head> before we start streaming output.
     *
     * @return ?Tempcode Tempcode indicating some kind of exceptional output (null: none)
     */
    public function pre_run() : ?object
    {
        if (get_forum_type() != 'cns') {
            warn_exit(do_lang_tempcode('NO_CNS'));
        }

        $type = get_param_string('type', 'browse');

        cns_require_all_forum_stuff();

        require_css('cns');
        require_lang('cns');

        if ($type == 'browse') {
            $this->title = get_screen_title('USERGROUPS');
        }

        if ($type == 'view') {
            $id = get_param_integer('id');

            if ($id == db_get_first_id()) {
                warn_exit(do_lang_tempcode('INTERNAL_ERROR'));
            }

            $members_groups = $GLOBALS['CNS_DRIVER']->get_members_groups(get_member());
            $map = ((has_privilege(get_member(), 'see_hidden_groups')) || (in_array($id, $members_groups))) ? ['id' => $id] : ['id' => $id, 'g_hidden' => 0];
            $groups = $GLOBALS['FORUM_DB']->query_select('f_groups', ['*'], $map, '', 1);
            if (!array_key_exists(0, $groups)) {
                warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'group'));
            }
            $group = $groups[0];

            $group_name = get_translated_text($group['g_name'], $GLOBALS['FORUM_DB']);
            $club = ($group['g_is_private_club'] == 1);

            breadcrumb_set_self(protect_from_escaping(escape_html($group_name)));
            breadcrumb_set_parents([['_SELF:_SELF:browse', do_lang_tempcode('USERGROUPS')]]);

            set_extra_request_metadata([
                'identifier' => '_SEARCH:groups:view:' . strval($id),
            ], $group, 'group', strval($id));

            $this->title = get_screen_title($club ? 'VIEW_CLUB' : 'VIEW_USERGROUP', true, [make_fractionable_editable('group', $id, $group_name)]);

            $this->id = $id;
            $this->group = $group;
            $this->group_name = $group_name;
            $this->club = $club;
        }

        if ($type == 'resign') {
            $this->title = get_screen_title('RESIGN_FROM_GROUP');
        }

        if ($type == 'remove_from') {
            $this->title = get_screen_title('REMOVE_MEMBER_FROM_GROUP');
        }

        if ($type == 'apply') {
            $id = post_param_integer('id', null);
            if ($id === null) {
                $id = get_param_integer('id');
                if ($id == db_get_first_id()) {
                    warn_exit(do_lang_tempcode('INTERNAL_ERROR'));
                }

                $group_name = cns_get_group_name($id);

                breadcrumb_set_self(do_lang_tempcode('DONE'));
                breadcrumb_set_parents([['_SELF:_SELF:browse', do_lang_tempcode('USERGROUPS')], ['_SELF:_SELF:view:' . strval($id), do_lang_tempcode('VIEW_USERGROUP', escape_html($group_name))]]);
            } else {
                $group_name = cns_get_group_name($id);
            }

            $this->title = get_screen_title('_APPLY_TO_GROUP', true, [escape_html($group_name)]);

            $this->id = $id;
            $this->group_name = $group_name;
        }

        if ($type == 'accept') {
            $this->title = get_screen_title('ACCEPT_INTO_GROUP');
        }

        if ($type == 'add_to') {
            $this->title = get_screen_title('ADD_MEMBER_TO_GROUP');
        }

        if ($type == 'decline') {
            $this->title = get_screen_title('DECLINE_FROM_GROUP');
        }

        return null;
    }

    /**
     * Execute the module.
     *
     * @return Tempcode The result of execution
     */
    public function run() : object
    {
        require_code('cns_groups_action');
        require_code('cns_groups_action2');
        require_code('cns_groups2');

        $type = get_param_string('type', 'browse');

        if ($type == 'browse') {
            return $this->directory();
        }
        if ($type == 'view') {
            return $this->usergroup();
        }
        if ($type == 'resign') {
            return $this->resign();
        }
        if ($type == 'remove_from') {
            return $this->remove_from();
        }
        if ($type == 'apply') {
            return $this->apply();
        }
        if ($type == 'accept') {
            return $this->accept();
        }
        if ($type == 'add_to') {
            return $this->add_to();
        }
        if ($type == 'decline') {
            return $this->decline();
        }

        return new Tempcode();
    }

    /**
     * The UI to show the usergroup directory.
     *
     * @return Tempcode The UI
     */
    public function directory() : object
    {
        // Calculate the number of pending promotions (approvals) and join requests (validations) for each usergroup
        $_pending_approvals = $GLOBALS['FORUM_DB']->query_select('f_group_approvals', ['ga_new_group_id'], ['ga_status' => 0]);
        $pending_approvals = [];
        foreach ($_pending_approvals as $row) {
            if (!array_key_exists($row['ga_new_group_id'], $pending_approvals)) {
                $pending_approvals[$row['ga_new_group_id']] = 0;
            }
            $pending_approvals[$row['ga_new_group_id']]++;
        }

        $staff_groups = array_merge($GLOBALS['FORUM_DRIVER']->get_super_admin_groups(), $GLOBALS['FORUM_DRIVER']->get_moderator_groups());

        $members_groups = $GLOBALS['CNS_DRIVER']->get_members_groups(get_member());

        $sql = 'SELECT g.* FROM ' . $GLOBALS['FORUM_DB']->get_table_prefix() . 'f_groups g WHERE ';
        if (!has_privilege(get_member(), 'see_hidden_groups')) {
            $sql .= '(g_hidden=0 OR g.id IN (' . implode(',', array_map('strval', $members_groups)) . ')) AND ';
        }
        $sql .= '(g_promotion_target_group IS NOT NULL OR EXISTS(SELECT id FROM ' . $GLOBALS['FORUM_DB']->get_table_prefix() . 'f_groups h WHERE h.g_promotion_target_group=g.id)';
        foreach ($staff_groups as $g_id) {
            $sql .= ' OR g.id=' . strval($g_id);
        }
        $sql .= ')';
        $sql .= ' ORDER BY g_order,' . $GLOBALS['FORUM_DB']->translate_field_ref('g_name');
        $lang_fields = find_lang_fields('f_groups', 'g');
        $groups = $GLOBALS['FORUM_DB']->query($sql, null, 0, false, false, $lang_fields);

        foreach ($groups as $g_id => $row) {
            $groups[$g_id]['_name'] = get_translated_text($row['g_name'], $GLOBALS['FORUM_DB']);
            $groups[$g_id]['_approvals'] = 0;
        }

        // Categorise
        $_staff = [];
        $_ranks = [];
        $_others = [];
        foreach ($groups as $group) {
            if ($group['id'] == 1) {
                continue; // Don't show guest usergroup
            }

            if (in_array($group['id'], $staff_groups)) {
                $_staff[$group['id']] = $group;
            } else {
                if ($group['g_promotion_target_group'] !== null) {
                    // Are we at the start of a usergroup?
                    $found = false;
                    foreach ($groups as $group2) {
                        if ($group2['g_promotion_target_group'] == $group['id']) {
                            $found = true;
                            break;
                        }
                    }
                    if (!$found) {
                        $_ranks[$group['id']] = [$group['id'] => $group];
                        $next = $group['g_promotion_target_group'];
                        while ($next !== null) {
                            $found = false;
                            foreach ($groups as $group2) {
                                if ($group2['id'] == $next) {
                                    $next = $group2['g_promotion_target_group'];
                                    $_ranks[$group['id']][$group2['id']] = $group2;
                                    if (array_key_exists($next, $_ranks[$group['id']])) {
                                        break; // uhoh- loop
                                    }
                                    $found = true;
                                    break;
                                }
                            }
                            if (!$found) {
                                break; // uhoh- either loop, or unfound usergroup
                            }
                        }
                    }
                }
            }
        }

        // Generate usergroup result browsers
        require_code('templates_results_table');
        $sortables = [];
        list($sortable, $sort_order) = ['foo', 'ASC'];

        //-Staff
        $start = get_param_integer('staff_start', 0);
        $max = get_param_integer('staff_max', intval(get_option('important_groups_per_page')));
        $max_rows = count($_staff);
        $has_images = false;
        $show_pending_applications = false;
        foreach ($_staff as $row) {
            if ($row['g_rank_image'] != '') {
                $has_images = true;
            }
            if ((cns_may_control_group($row['id'], get_member(), $row)) && (array_key_exists($row['id'], $pending_approvals))) {
                $show_pending_applications = true;
            }
        }
        list($col_widths, $titles) = $this->find_table_headings($has_images, false, $show_pending_applications);
        $header_row = results_header_row($titles, $sortables);
        $staff_entries = new Tempcode();
        $i = 0;
        foreach ($_staff as $row) {
            if ($i < $start) {
                $i++;
                continue;
            }
            if ($i > $start + $max) {
                break;
            }

            $num_members = integer_format(cns_get_group_members_raw_count($row['id'], true, false, true, false));

            $entry = [];
            $entry[] = $this->display_group_link($row);
            if ($has_images) {
                $rank_image_tpl = $this->display_rank_image($row);
                $entry[] = $rank_image_tpl;
            }
            $entry[] = escape_html($num_members);
            if ($show_pending_applications) {
                if (cns_may_control_group($row['id'], get_member(), $row)) {
                    $entry[] = escape_html(integer_format(array_key_exists($row['id'], $pending_approvals) ? $pending_approvals[$row['id']] : 0));
                } else {
                    $entry[] = do_lang_tempcode('NA_EM');
                }
            }

            $staff_entries->attach(results_entry($entry, false));
            $i++;
        }
        $staff = results_table(do_lang_tempcode('STAFF'), $start, 'staff_start', $max, 'staff_max', $max_rows, $header_row, $staff_entries, $sortables, $sortable, $sort_order, 'staff_sort', null, $col_widths);

        //-Ranks
        $ranks = [];
        foreach ($_ranks as $g_id => $_rank) {
            $start = get_param_integer('rank_start_' . strval($g_id), 0);
            $max = get_param_integer('rank_max_' . strval($g_id), intval(get_option('important_groups_per_page')));
            $max_rows = count($_rank);
            $has_images = false;
            $show_pending_applications = false;
            foreach ($_rank as $row) {
                if ($row['g_rank_image'] != '') {
                    $has_images = true;
                }
                if ((cns_may_control_group($row['id'], get_member(), $row)) && (array_key_exists($row['id'], $pending_approvals))) {
                    $show_pending_applications = true;
                }
            }
            list($col_widths, $titles) = $this->find_table_headings($has_images, true, $show_pending_applications);
            $header_row = results_header_row($titles, $sortables);
            $rank_entries = new Tempcode();
            $i = 0;
            foreach ($_rank as $row) {
                if ($i < $start) {
                    $i++;
                    continue;
                }
                if ($i > $start + $max) {
                    break;
                }

                $num_members = integer_format(cns_get_group_members_raw_count($row['id'], true, false, true, false));

                $_p_t = $row['g_promotion_threshold'];
                $p_t = new Tempcode();
                if (($_p_t !== null) && (array_key_exists($row['g_promotion_target_group'], $_rank))) {
                    $p_t = do_lang_tempcode('PROMOTION_TO', escape_html(integer_format($_p_t)), escape_html($_rank[$row['g_promotion_target_group']]['_name']));
                }

                $entry = [];
                $entry[] = $this->display_group_link($row);
                $entry[] = $p_t;
                if ($has_images) {
                    $rank_image_tpl = $this->display_rank_image($row);
                    $entry[] = $rank_image_tpl;
                }
                $entry[] = escape_html($num_members);
                if ($show_pending_applications) {
                    if (cns_may_control_group($row['id'], get_member(), $row)) {
                        $entry[] = escape_html(integer_format(array_key_exists($row['id'], $pending_approvals) ? $pending_approvals[$row['id']] : 0));
                    } else {
                        $entry[] = do_lang_tempcode('NA_EM');
                    }
                }
                $rank_entries->attach(results_entry($entry, false));
            }
            $rank = results_table(do_lang_tempcode('RANK_SETS'), $start, 'rank_start_' . strval($g_id), $max, 'rank_max_' . strval($g_id), $max_rows, $header_row, $rank_entries, $sortables, $sortable, $sort_order, 'rank_sort_' . strval($g_id), null, $col_widths);
            $ranks[] = $rank;
        }

        //-Others / Clubs
        $others = new Tempcode();
        $clubs = new Tempcode();
        foreach ([0 => 'others', 1 => 'clubs'] as $is_private_club => $prefix) {
            $start = get_param_integer($prefix . '_start', 0);
            $max = get_param_integer($prefix . '_max', intval(get_option('normal_groups_per_page')));
            $sql = 'SELECT g.* FROM ' . $GLOBALS['FORUM_DB']->get_table_prefix() . 'f_groups g WHERE ';
            if (!has_privilege(get_member(), 'see_hidden_groups')) {
                $sql .= '(g_hidden=0 OR g.id IN (' . implode(',', array_map('strval', $members_groups)) . ')) AND ';
            }
            $sql .= 'g_is_private_club=' . strval($is_private_club) . ' AND ';
            $sql .= '(g_promotion_target_group IS NULL AND NOT EXISTS(SELECT id FROM ' . $GLOBALS['FORUM_DB']->get_table_prefix() . 'f_groups h WHERE h.g_promotion_target_group=g.id)';
            foreach ($staff_groups as $g_id) {
                $sql .= ' AND g.id<>' . strval($g_id);
            }
            $sql .= ' AND g.id<>' . strval(db_get_first_id());
            $sql .= ')';
            $sql .= ' ORDER BY g_order,' . $GLOBALS['FORUM_DB']->translate_field_ref('g_name');
            $_rows = $GLOBALS['FORUM_DB']->query($sql, $max, $start, false, false, $lang_fields);
            $max_rows = $GLOBALS['FORUM_DB']->query_value_if_there(str_replace('SELECT * ', 'SELECT COUNT(*) ', $sql), false, false, $lang_fields);
            $has_images = false;
            $show_pending_applications = false;
            foreach ($_rows as $row) {
                if ($row['g_rank_image'] != '') {
                    $has_images = true;
                }
                if ((cns_may_control_group($row['id'], get_member(), $row)) && (array_key_exists($row['id'], $pending_approvals))) {
                    $show_pending_applications = true;
                }
            }
            list($col_widths, $titles) = $this->find_table_headings($has_images, false, $show_pending_applications);
            $header_row = results_header_row($titles, $sortables);
            $entries = new Tempcode();
            foreach ($_rows as $row) {
                $num_members = integer_format(cns_get_group_members_raw_count($row['id'], true, false, true, false));

                $entry = [];
                $entry[] = $this->display_group_link($row, false);
                if ($has_images) {
                    $rank_image_tpl = $this->display_rank_image($row);
                    $entry[] = $rank_image_tpl;
                }
                $entry[] = escape_html($num_members);
                if ($show_pending_applications) {
                    if (cns_may_control_group($row['id'], get_member(), $row)) {
                        $entry[] = escape_html(integer_format(array_key_exists($row['id'], $pending_approvals) ? $pending_approvals[$row['id']] : 0));
                    } else {
                        $entry[] = do_lang_tempcode('NA_EM');
                    }
                }
                $entries->attach(results_entry($entry, false));
            }
            if (!$entries->is_empty()) {
                if ($is_private_club == 0) {
                    $pagination_label = empty($ranks) ? 'OTHER' : 'OTHER_USERGROUPS';
                } else {
                    $pagination_label = 'CLUBS';
                }
                $results_table = results_table(do_lang_tempcode($pagination_label), $start, $prefix . '_start', $max, $prefix . '_max', $max_rows, $header_row, $entries, $sortables, $sortable, $sort_order, $prefix . '_sort', null, $col_widths);
                if ($is_private_club == 0) {
                    $others = $results_table;
                } else {
                    $clubs = $results_table;
                }
            }
        }

        $tpl = do_template('CNS_GROUP_DIRECTORY_SCREEN', ['_GUID' => '39aebd8fcb618c2ae45e867d0c96a4cf', 'TITLE' => $this->title, 'STAFF' => $staff, 'OTHERS' => $others, 'CLUBS' => $clubs, 'RANKS' => $ranks]);

        require_code('templates_internalise_screen');
        return internalise_own_screen($tpl);
    }

    /**
     * Display a link to a rank image for the directory.
     *
     * @param  array $row The group row
     * @return Tempcode The rank image
     */
    protected function display_rank_image(array $row) : object
    {
        $rank_image = $row['g_rank_image'];
        if ($rank_image != '') {
            $group_name = get_translated_text($row['g_name'], $GLOBALS['FORUM_DB']);
            $rank_image_tpl = do_template('CNS_RANK_IMAGE', ['_GUID' => '3753739ac2bebcfb9fff8b80e4bd71d0', 'GROUP_NAME' => $group_name, 'IMG' => $rank_image, 'IS_LEADER' => false]);
        } else {
            $rank_image_tpl = new Tempcode();
        }
        return $rank_image_tpl;
    }

    /**
     * Display a link to a group from the directory.
     *
     * @param  array $row The group row
     * @param  boolean $include_club_labelling Include special labelling for clubs
     * @return Tempcode The link
     */
    protected function display_group_link(array $row, bool $include_club_labelling = true) : object
    {
        $url = build_url(['page' => '_SELF', 'type' => 'view', 'id' => $row['id']], '_SELF');

        $group_name = get_translated_text($row['g_name'], $GLOBALS['FORUM_DB']);

        $club = ($row['g_is_private_club'] == 1);

        $title = new Tempcode();
        if ($club) {
            $title = do_lang_tempcode('CLUB_EXPLANATION');
        }

        $link = hyperlink($url, make_fractionable_editable('group', $row['id'], $group_name), false, true, $title);

        if ($include_club_labelling) {
            return do_lang_tempcode($club ? 'VIEW_CLUB' : 'VIEW_USERGROUP', $link);
        }
        return $link;
    }

    /**
     * Find table column widths and headings.
     *
     * @param  boolean $has_images Whether there are rank images
     * @param  boolean $has_rank Whether there are rank promotions
     * @param  boolean $pending_applications Whether to show the pending applications column
     * @return array A pair: column widths, table headings
     */
    protected function find_table_headings(bool $has_images, bool $has_rank, bool $pending_applications = false) : array
    {
        $col_widths = [''];
        $titles = [do_lang_tempcode('NAME')];
        if ($has_rank) {
            $col_widths[] = '250';
            $titles[] = do_lang_tempcode('PROMOTION_THRESHOLD');
        }
        if ($has_images) {
            $col_widths[] = '175';
            $titles[] = do_lang_tempcode('IMAGE');
        }
        $col_widths[] = '100';
        $titles[] = do_lang_tempcode('COUNT_MEMBERS');
        if ($pending_applications) {
            $col_widths[] = '175';
            $titles[] = do_lang_tempcode('PENDING_APPLICATIONS');
        }

        return [$col_widths, $titles];
    }

    /**
     * The UI to show a usergroup.
     *
     * @return Tempcode The UI
     */
    public function usergroup() : object
    {
        $id = $this->id;
        $group = $this->group;
        $group_name = $this->group_name;
        $club = $this->club;

        // Came from a notification for approving a promotion
        $approval_id = get_param_integer('approval_id', null);
        if ($approval_id !== null) {
            $rows = $GLOBALS['FORUM_DB']->query_select('f_group_approvals', ['*'], ['id' => $approval_id]);
            if (count($rows) == 0) {
                attach_message(do_lang_tempcode('GROUP_REQUEST_INVALID'), 'warn');
            } else {
                if ($rows[0]['ga_old_group_id'] === null) {
                    $type = do_lang_tempcode('GROUP_REQUEST_JOIN_L');
                } else {
                    $type = do_lang_tempcode('GROUP_REQUEST_PROMOTION_L');
                }
                $reason = get_translated_text($rows[0]['ga_reason']);
                $username = ($rows[0]['ga_status_member_id'] !== null) ? $GLOBALS['FORUM_DRIVER']->get_username($rows[0]['ga_status_member_id'], false, USERNAME_DEFAULT_DELETED) : do_lang('UNKNOWN');
                $m_username = $GLOBALS['FORUM_DRIVER']->get_username($rows[0]['ga_member_id'], false, USERNAME_DEFAULT_NULL);
                if ($m_username === null) {
                    $m_username = $rows[0]['ga_member_username'];
                }

                if ($rows[0]['ga_status'] == -1) {
                    attach_message(do_lang_tempcode('GROUP_REQUEST_ALREADY_DECLINED', escape_html($username), $type, [escape_html($m_username), escape_html($reason)]), 'warn');
                } elseif ($rows[0]['ga_status'] == 1) {
                    attach_message(do_lang_tempcode('GROUP_REQUEST_ALREADY_ACCEPTED', escape_html($username), $type, [escape_html($m_username), escape_html($reason)]), 'warn');
                }
            }
        }

        // Check for pending approval into this usergroup
        $pending = $GLOBALS['FORUM_DB']->query_select_value_if_there('f_group_approvals', 'id', ['ga_new_group_id' => $id, 'ga_member_id' => get_member(), 'ga_status' => 0]);
        if ($pending !== null) {
            attach_message(do_lang_tempcode('GROUP_REQUEST_PENDING'), 'inform');
        }

        // Leadership
        if (($group['g_group_lead_member'] !== null) && ($GLOBALS['FORUM_DRIVER']->get_username($group['g_group_lead_member'], false, USERNAME_DEFAULT_NULL) !== null)) {
            $leader_name = $GLOBALS['FORUM_DRIVER']->get_username($group['g_group_lead_member'], true);
            $leader_url = $GLOBALS['FORUM_DRIVER']->member_profile_url($group['g_group_lead_member']);
            $leader_link = hyperlink($leader_url, $leader_name, false, true);
            $leader = paragraph(do_lang_tempcode('GROUP_LED_BY', $leader_link), 'gfgdfggdf');
        } else {
            $leader = new Tempcode();
        }

        // Promotion
        if ((addon_installed('points')) && ($group['g_promotion_threshold'] !== null) && ($group['g_promotion_target_group'] !== null)) {
            $promote_link = cns_get_group_link($group['g_promotion_target_group']);
            $requires_approval = $GLOBALS['FORUM_DB']->query_select_value_if_there('f_groups', 'id', ['id' => $group['g_promotion_target_group'], 'g_promotion_approval' => 1]);
            $promotion_info = do_lang_tempcode(($requires_approval === null) ? 'CNS_PROMOTION_INFO_AUTO' : 'CNS_PROMOTION_INFO_MANUAL', escape_html(integer_format($group['g_promotion_threshold'])), $promote_link->evaluate());
        } else {
            $promotion_info = new Tempcode();
        }

        $may_control_group = cns_may_control_group($id, get_member());

        // To add
        if ($may_control_group) {
            $add_url = build_url(['page' => '_SELF', 'type' => 'add_to', 'id' => $id], '_SELF');
        } else {
            $add_url = new Tempcode();
        }

        // To apply
        $my_groups = $GLOBALS['FORUM_DRIVER']->get_members_groups(get_member(), false, false);
        if (is_guest()) {
            $apply_url = new Tempcode();
            $apply_text = new Tempcode();
        } else {
            if (!in_array($id, $my_groups)) {
                $apply_url = build_url(['page' => '_SELF', 'type' => 'apply', 'id' => $id], '_SELF');
                $apply_text = do_lang_tempcode('APPLY_TO_GROUP');
            } elseif (cns_get_member_primary_group(get_member()) != $id) {
                $apply_url = build_url(['page' => '_SELF', 'type' => 'resign', 'id' => $id], '_SELF');
                $apply_text = do_lang_tempcode('RESIGN_FROM_GROUP');
            } else {
                $apply_url = new Tempcode();
                $apply_text = new Tempcode();
            }
        }

        require_code('templates_results_table');
        $sortables = [];
        list($sortable, $sort_order) = explode(' ', get_param_string('p_sort', 'date_and_time DESC', INPUT_FILTER_GET_COMPLEX));

        // Primary members
        $start = get_param_integer('p_start', 0);
        $max = get_param_integer('p_max', intval(get_option('primary_members_per_page')));
        $_primary_members = cns_get_group_members_raw($id, true, true, false, false, $max, $start);
        if (!empty($_primary_members)) {
            $max_rows = cns_get_group_members_raw_count($id, true, true, false, false);
            $primary_members = new Tempcode();
            $may_control_any = false;
            foreach ($_primary_members as $i => $primary_member) {
                if ($this->filter_out($primary_member)) {
                    continue;
                }
                $url = $GLOBALS['FORUM_DRIVER']->member_profile_url($primary_member['gm_member_id'], true);

                $temp = do_template('CNS_VIEW_GROUP_MEMBER', [
                    '_GUID' => 'b96b674ac713e9790ecb78c15af1baab',
                    'ID' => strval($primary_member['gm_member_id']),
                    'NAME' => $primary_member['m_username'],
                    'URL' => $url,
                ]);

                $primary_members->attach(results_entry([$temp], false));
            }
            $header_row = results_header_row([do_lang_tempcode('PRIMARY_MEMBERS')], $sortables, 'p_sort', $sortable . ' ' . $sort_order);
            $primary_members = results_table(do_lang_tempcode('PRIMARY_MEMBERS'), $start, 'p_start', $max, 'p_max', $max_rows, $header_row, $primary_members, $sortables, $sortable, $sort_order, 'p_sort', null, [], null, 6);
        } else {
            $primary_members = new Tempcode();
        }

        // Secondary members
        $s_start = get_param_integer('s_start', 0);
        $s_max = get_param_integer('s_max', intval(get_option('secondary_members_per_page')));
        $_secondary_members = cns_get_group_members_raw($id, false, true, true, false, $s_max, $s_start);
        $secondary_members = new Tempcode();
        $prospective_members = new Tempcode();
        $s_max_rows = cns_get_group_members_raw_count($id, false, false, true, false);
        $may_control_any = false;
        foreach ($_secondary_members as $secondary_member) {
            if ((!$secondary_member['validated']) || ($this->filter_out($GLOBALS['FORUM_DRIVER']->get_member_row($secondary_member['gm_member_id'])))) {
                continue;
            }

            $m_username = $GLOBALS['FORUM_DRIVER']->get_member_row_field($secondary_member['gm_member_id'], 'm_username');
            if ($m_username === null) {
                continue;
            }

            $url = $GLOBALS['FORUM_DRIVER']->member_profile_url($secondary_member['gm_member_id'], true);
            $temp = do_template('CNS_VIEW_GROUP_MEMBER', [
                '_GUID' => 'ea5556b36e132c02b299f17a9893ac5e',
                'ID' => strval($secondary_member['gm_member_id']),
                'NAME' => $m_username,
                'URL' => $url,
            ]);

            $remove_url = build_url(['page' => '_SELF', 'type' => 'remove_from', 'id' => $id, 'member_id' => $secondary_member['gm_member_id']], '_SELF');
            $results_map = [$temp];

            $may_control = ($may_control_group && (!$secondary_member['implicit']));
            if ($may_control) {
                $may_control_any = true;
                $actions = new Tempcode();
                $actions->attach(do_template('COLUMNED_TABLE_ACTION', [
                    '_GUID' => '122cd1957122a58bbcc7984ff7eaa865',
                    'NAME' => $m_username,
                    'URL' => $remove_url,
                    'ACTION_TITLE' => do_lang_tempcode('REMOVE_MEMBER_FROM_GROUP'),
                    'ICON' => 'buttons/cancel',
                    'GET' => true,
                ]));
                $results_map[] = $actions;
            }
            $secondary_members->attach(results_entry($results_map, false));
        }
        if (!$secondary_members->is_empty()) {
            $header_map = [do_lang_tempcode('SECONDARY_MEMBERS')];
            if ($may_control_any) {
                $header_map[] = do_lang_tempcode('ACTIONS');
            }
            $header_row = results_header_row($header_map, $sortables, 'p_sort', $sortable . ' ' . $sort_order);
            $secondary_members = results_table(do_lang_tempcode('SECONDARY_MEMBERS'), $s_start, 's_start', $s_max, 's_max', $s_max_rows, $header_row, $secondary_members, $sortables, $sortable, $sort_order, 's_sort', null, [], null, 6);
        }

        // Prospective members
        $_prospective_members = new Tempcode();
        $prospective_members = new Tempcode();
        if ($may_control_group) {
            $start = get_param_integer('pp_start', 0);
            $max = get_param_integer('pp_max', 50);
            $max_rows = $GLOBALS['FORUM_DB']->query_select_value('f_group_approvals', 'COUNT(*)', ['ga_new_group_id' => $id, 'ga_status' => 0]);
            $rows = $GLOBALS['FORUM_DB']->query_select('f_group_approvals', ['*'], ['ga_new_group_id' => $id, 'ga_status' => 0], '', $max, $start);
            $can_be_promoted_to = ($GLOBALS['FORUM_DB']->query_select_value_if_there('f_groups', 'id', ['g_promotion_target_group' => $id]) !== null);
            if (!empty($rows)) {
                foreach ($rows as $row) {
                    if ($this->filter_out($GLOBALS['FORUM_DRIVER']->get_member_row($row['ga_member_id']))) {
                        continue;
                    }

                    $username = $GLOBALS['FORUM_DRIVER']->get_username($row['ga_member_id'], true, USERNAME_DEFAULT_DELETED);
                    $profile_url = hyperlink($GLOBALS['FORUM_DRIVER']->member_profile_url($row['ga_member_id'], true), $username, false, true);

                    $promotion_url = do_lang_tempcode('NA_EM');
                    if ($row['ga_old_group_id'] !== null) {
                        $promotion_url = cns_get_group_link($row['ga_old_group_id']);
                    }

                    $actions = new Tempcode();

                    $accept_url = build_url(['page' => '_SELF', 'type' => 'accept', 'id' => $id, 'member_id' => $row['ga_member_id']], '_SELF');
                    $actions->attach(do_template('COLUMNED_TABLE_ACTION', [
                        '_GUID' => '9a3bb787ec6ac41d6491c2c0975baabd',
                        'NAME' => '#' . strval($row['id']),
                        'URL' => $accept_url,
                        'ACTION_TITLE' => do_lang_tempcode('ACCEPT_INTO_GROUP'),
                        'ICON' => 'buttons/yes',
                        'GET' => true,
                    ]));

                    $decline_url = build_url(['page' => '_SELF', 'type' => 'decline', 'id' => $id, 'member_id' => $row['ga_member_id']], '_SELF');
                    $actions->attach(do_template('COLUMNED_TABLE_ACTION', [
                        '_GUID' => 'f8e96b5c1420201a571b9b4c6bf59653',
                        'NAME' => '#' . strval($row['id']),
                        'URL' => $decline_url,
                        'ACTION_TITLE' => do_lang_tempcode('DECLINE_FROM_GROUP'),
                        'ICON' => 'buttons/cancel',
                        'GET' => true,
                    ]));

                    $map = [$profile_url];
                    if ($can_be_promoted_to) {
                        $map[] = $promotion_url;
                    }
                    $map[] = $actions;
                    $_prospective_members->attach(results_entry($map, false));
                }

                $header_map = [do_lang_tempcode('PROSPECTIVE_MEMBERS')];
                if ($can_be_promoted_to) {
                    $header_map[] = do_lang_tempcode('PROMOTION_FROM');
                }
                $header_map[] = do_lang_tempcode('ACTIONS');
                $header_row = results_header_row($header_map, $sortables, 'pp_sort', $sortable . ' ' . $sort_order);
                $prospective_members = results_table(do_lang_tempcode('PROSPECTIVE_MEMBERS'), $start, 'pp_start', $max, 'pp_max', $max_rows, $header_row, $_prospective_members, $sortables, $sortable, $sort_order, 'pp_sort', null, [], null, 6);
            }
        }

        $edit_url = new Tempcode();
        if ((!$club) && (has_actual_page_access(get_member(), 'admin_cns_groups', get_module_zone('admin_cns_groups')))) {
            $edit_url = build_url(['page' => 'admin_cns_groups', 'type' => '_edit', 'id' => $id], get_module_zone('admin_cns_groups'));
        } elseif (($club) && (has_actual_page_access(get_member(), 'cms_cns_groups', get_module_zone('cms_cns_groups')))) {
            $is_super_admin = $group['g_is_super_admin'];
            if (cns_may_control_group($id, get_member(), $group)) {
                $edit_url = build_url(['page' => 'cms_cns_groups', 'type' => '_edit', 'id' => $id], get_module_zone('cms_cns_groups'));
            }
        }

        $club_forum = null;
        if ($group['g_is_private_club'] == 1) {
            $club_forum = $GLOBALS['FORUM_DB']->query_select_value_if_there('f_forums', 'id', [$GLOBALS['FORUM_DB']->translate_field_ref('f_description') => do_lang('FORUM_FOR_CLUB', $group_name)]);
        }

        require_javascript('ajax_people_lists');

        $forum_id = null;
        if ($club && addon_installed('cns_clubs')) {
            $forum_where = ['f_name' => $group_name, 'f_forum_grouping_id' => intval(get_option('club_forum_parent_forum_grouping')), 'f_parent_forum_id' => intval(get_option('club_forum_parent_forum'))];
            $forum_id = $GLOBALS['FORUM_DB']->query_select_value_if_there('f_forums', 'id', $forum_where);
        }

        $tpl = do_template('CNS_VIEW_GROUP_SCREEN', [
            '_GUID' => 'fc6cac5c73f92ab4410b492d58976dbe',
            'GROUP_NAME' => $group_name,
            'ID' => strval($id),
            'FORUM' => ($forum_id === null) ? '' : strval($forum_id),
            'CLUB' => $club,
            'EDIT_URL' => $edit_url,
            'TITLE' => $this->title,
            'LEADER' => $leader,
            'PROMOTION_INFO' => $promotion_info,
            'ADD_URL' => $add_url,
            'APPLY_URL' => $apply_url,
            'APPLY_TEXT' => $apply_text,
            'PRIMARY_MEMBERS' => $primary_members,
            'SECONDARY_MEMBERS' => $secondary_members,
            'PROSPECTIVE_MEMBERS' => $prospective_members,
        ]);

        require_code('templates_internalise_screen');
        return internalise_own_screen($tpl);
    }

    /**
     * Whether to filter out a member row from those displayed.
     *
     * @param  array $member_row The member row
     * @return boolean Filter out
     */
    public function filter_out(array $member_row) : bool
    {
        // Some special non-documented (FUDGE) filters to help find likely spammers
        if ((get_param_integer('with_sig', 0) == 1) && ($member_row['m_signature'] == '')) {
            return true;
        }
        if ((get_param_integer('with_posts', 0) == 1) && ($member_row['m_cache_num_posts'] == 0)) {
            return true;
        }
        if ((get_param_integer('without_posts', 0) == 1) && ($member_row['m_cache_num_posts'] > 0)) {
            return true;
        }
        if ((get_param_integer('is_banned', 0) == 1) && ($member_row['m_is_perm_banned'] == '0')) {
            return true;
        }
        if ((get_param_integer('is_not_banned', 0) == 1) && ($member_row['m_is_perm_banned'] != '0')) {
            return true;
        }
        if ((get_param_integer('with_links', 0) == 1) && (strpos(serialize($member_row), '<a') === false) && (strpos(serialize($member_row), '[url') === false)) {
            return true;
        }
        return false;
    }

    /**
     * The actualiser to add a member to a usergroup.
     *
     * @param  boolean $special_permission Whether to skip checking permission for usergroup control
     * @param  ?string $username Username to add (null: read from environment)
     * @return Tempcode The UI
     */
    public function add_to(bool $special_permission = false, ?string $username = null) : object
    {
        $id = get_param_integer('id');

        if ($id == db_get_first_id()) {
            warn_exit(do_lang_tempcode('INTERNAL_ERROR'));
        }

        if ($username === null) {
            $username = post_param_string('username', false, INPUT_FILTER_POST_IDENTIFIER);
        }

        if ($username == '') {
            warn_exit(do_lang_tempcode('NO_PARAMETER_SENT', 'username'));
        }

        if ((!$special_permission) && (!cns_may_control_group($id, get_member()))) {
            access_denied('I_ERROR');
        }

        $member_id = $GLOBALS['FORUM_DRIVER']->get_member_from_username($username);
        if (($member_id === null) || (is_guest($member_id))) {
            warn_exit(do_lang_tempcode('_MEMBER_NO_EXIST', escape_html($username)), false, false, 404);
        }

        $test = $GLOBALS['FORUM_DRIVER']->get_members_groups($member_id, false, false);
        if (in_array($id, $test)) {
            warn_exit(do_lang_tempcode('ALREADY_IN_GROUP'));
        }

        cns_add_member_to_secondary_group($member_id, $id);
        cns_update_group_approvals($member_id, get_member());

        $url = build_url(['page' => '_SELF', 'type' => 'view', 'id' => $id], '_SELF');
        return redirect_screen($this->title, $url, do_lang_tempcode('SUCCESS'));
    }

    /**
     * The actualiser to remove a member from a usergroup.
     *
     * @return Tempcode The UI
     */
    public function remove_from() : object
    {
        $member_id = get_param_integer('member_id');
        $username = $GLOBALS['FORUM_DRIVER']->get_username($member_id, true);

        $id = post_param_integer('id', null);
        if ($id === null) {
            $id = get_param_integer('id');

            $post_url = build_url(['page' => '_SELF', 'type' => get_param_string('type')], '_SELF', [], true);
            $hidden = form_input_hidden('id', strval($id));

            return do_template('CONFIRM_SCREEN', [
                '_GUID' => 'f98ab98f130646f6fd33fbf85ae3f972',
                'TITLE' => $this->title,
                'TEXT' => do_lang_tempcode('Q_SURE_REMOVE_FROM_GROUP', escape_html($username)),
                'URL' => $post_url,
                'HIDDEN' => $hidden,
                'FIELDS' => '',
                'PREVIEW' => '',
            ]);
        }

        if (!cns_may_control_group($id, get_member())) {
            access_denied('I_ERROR');
        }

        cns_member_leave_secondary_group($id, $member_id);
        cns_update_group_approvals($member_id, get_member(), [$id]);

        $url = build_url(['page' => '_SELF', 'type' => 'view', 'id' => $id], '_SELF');
        return redirect_screen($this->title, $url, do_lang_tempcode('SUCCESS'));
    }

    /**
     * The actualiser to apply to join a usergroup.
     *
     * @return Tempcode The UI
     */
    public function apply() : object
    {
        $group_name = $this->group_name;

        $id = post_param_integer('id', null);
        if ($id === null) {
            $id = $this->id;

            $_leader = cns_get_group_property($id, 'group_lead_member');
            $free_access = (cns_get_group_property($id, 'open_membership') == 1);

            $post_url = build_url(['page' => '_SELF', 'type' => get_param_string('type')], '_SELF', [], true);
            $hidden = form_input_hidden('id', strval($id));

            if ($free_access) {
                $text = do_lang_tempcode('ABOUT_TO_APPLY_FREE_ACCESS', escape_html($group_name));
            } else {
                if (($_leader === null) || ($GLOBALS['FORUM_DRIVER']->get_username($_leader, false, USERNAME_DEFAULT_NULL) === null)) {
                    $text = do_lang_tempcode('ABOUT_TO_APPLY_STAFF', escape_html($group_name), escape_html(get_site_name()));
                } else {
                    $leader_username = $GLOBALS['FORUM_DRIVER']->get_username($_leader, true);
                    $leader_url = $GLOBALS['FORUM_DRIVER']->member_profile_url($_leader, true);
                    $text = do_lang_tempcode('ABOUT_TO_APPLY_LEADER', escape_html($group_name), escape_html($leader_username), escape_html($leader_url));
                }
            }

            return do_template('CONFIRM_SCREEN', [
                '_GUID' => 'ceafde00ade4492c65ed2e6e2309a0e7',
                'TITLE' => $this->title,
                'TEXT' => $text,
                'URL' => $post_url,
                'HIDDEN' => $hidden,
                'FIELDS' => '',
                'PREVIEW' => '',
            ]);
        }
        if ($id == db_get_first_id()) {
            warn_exit(do_lang_tempcode('INTERNAL_ERROR'));
        }

        $free_access = (cns_get_group_property($id, 'open_membership') == 1);

        if (is_guest()) {
            access_denied('I_ERROR');
        }

        require_code('cns_groups');
        if (cns_get_group_property($id, 'open_membership') == 1) {
            return $this->add_to(true, $GLOBALS['FORUM_DRIVER']->get_username(get_member()));
        }

        cns_member_ask_join_group($id, get_member());

        $url = build_url(['page' => '_SELF', 'type' => 'view', 'id' => $id], '_SELF');
        return redirect_screen($this->title, $url, do_lang_tempcode('AWAITING_GROUP_LEADER'));
    }

    /**
     * The actualiser to accept a member into a usergroup.
     *
     * @return Tempcode The UI
     */
    public function accept() : object
    {
        $id = post_param_integer('id', null);
        if ($id === null) {
            $id = get_param_integer('id');

            $post_url = build_url(['page' => '_SELF', 'type' => get_param_string('type')], '_SELF', [], true);
            $hidden = form_input_hidden('id', strval($id));

            return do_template('CONFIRM_SCREEN', [
                '_GUID' => 'ebc562534bceb3161a21307633bc229e',
                'TITLE' => $this->title,
                'TEXT' => do_lang_tempcode('Q_SURE'),
                'URL' => $post_url,
                'HIDDEN' => $hidden,
                'FIELDS' => '',
                'PREVIEW' => '',
            ]);
        }

        if (!cns_may_control_group($id, get_member())) {
            access_denied('I_ERROR');
        }

        cns_member_validate_into_group($id, get_param_integer('member_id'));

        $url = build_url(['page' => '_SELF', 'type' => 'view', 'id' => $id], '_SELF');
        return redirect_screen($this->title, $url, do_lang_tempcode('SUCCESS'));
    }

    /**
     * The actualiser to decline a members joining of a usergroup.
     *
     * @return Tempcode The UI
     */
    public function decline() : object
    {
        $id = post_param_integer('id', null);
        if ($id === null) {
            $id = get_param_integer('id');

            require_code('form_templates');

            $text = paragraph(do_lang_tempcode('OPTIONAL_REASON'));
            $submit_name = do_lang_tempcode('DECLINE_FROM_GROUP');
            $post_url = build_url(['page' => '_SELF', 'type' => get_param_string('type')], '_SELF', [], true);
            $fields = new Tempcode();
            $hidden = form_input_hidden('id', strval($id));
            $fields->attach(form_input_line(do_lang_tempcode('REASON'), '', 'reason', '', false));

            return do_template('FORM_SCREEN', [
                '_GUID' => 'ebec84204dee305a8db1a57e5a95c774',
                'SKIP_WEBSTANDARDS' => true,
                'HIDDEN' => $hidden,
                'TITLE' => $this->title,
                'TEXT' => $text,
                'URL' => $post_url,
                'FIELDS' => $fields,
                'SUBMIT_ICON' => 'buttons/no',
                'SUBMIT_NAME' => $submit_name,
            ]);
        }

        if (!cns_may_control_group($id, get_member())) {
            access_denied('I_ERROR');
        }

        $member_id = get_param_integer('member_id');

        cns_member_validate_into_group($id, $member_id, true, post_param_string('reason'));

        $url = build_url(['page' => '_SELF', 'type' => 'view', 'id' => $id], '_SELF');
        return redirect_screen($this->title, $url, do_lang_tempcode('SUCCESS'));
    }

    /**
     * The actualiser to resign from a usergroup.
     *
     * @return Tempcode The UI
     */
    public function resign() : object
    {
        $id = post_param_integer('id', null);
        if ($id === null) {
            $id = get_param_integer('id');

            $post_url = build_url(['page' => '_SELF', 'type' => get_param_string('type')], '_SELF', [], true);
            $hidden = form_input_hidden('id', strval($id));

            return do_template('CONFIRM_SCREEN', [
                '_GUID' => 'd9524899fbc243247a9d253cf93c8aa2',
                'TITLE' => $this->title,
                'TEXT' => do_lang_tempcode('Q_SURE'),
                'URL' => $post_url,
                'HIDDEN' => $hidden,
                'FIELDS' => '',
                'PREVIEW' => '',
            ]);
        }

        cns_member_leave_secondary_group($id, get_member());
        cns_update_group_approvals(get_member(), get_member(), [$id]);

        $url = build_url(['page' => '_SELF', 'type' => 'view', 'id' => $id], '_SELF');
        return redirect_screen($this->title, $url, do_lang_tempcode('SUCCESS'));
    }
}
