<?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_admin_cns_members
{
    /**
     * 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'] = 3;
        $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;
        }

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

        $ret = [
            'browse' => ['MEMBERS', 'menu/social/members'],
            'step1' => ['ADD_MEMBER', 'menu/adminzone/tools/users/member_add'],
        ];

        if (has_privilege(get_member(), 'mass_import')) {
            $ret['delurk'] = ['DELETE_LURKERS', 'menu/adminzone/tools/users/delete_lurkers'];
            $ret['import_spreadsheet'] = ['IMPORT_MEMBERS', 'admin/import_spreadsheet'];
        }

        $ret['export_spreadsheet'] = ['EXPORT_MEMBERS', 'admin/export_spreadsheet'];

        if ($support_crosslinks) {
            if (has_privilege($member_id, 'member_maintenance')) {
                $ret['_SEARCH:members:browse:include_non_confirmed=include'] = ['MEMBER_DIRECTORY', 'menu/adminzone/tools/users/member_edit'];
            }
            $ret['_SEARCH:admin_cns_members:merge'] = ['MERGE_MEMBERS', 'menu/adminzone/tools/users/merge_members'];
            if (addon_installed('cns_cpfs')) {
                $ret['_SEARCH:admin_cns_customprofilefields:browse'] = ['CUSTOM_PROFILE_FIELDS', 'menu/adminzone/tools/users/custom_profile_fields'];
            }
            if (addon_installed('welcome_emails')) {
                $ret['_SEARCH:admin_cns_welcome_emails:browse'] = ['WELCOME_EMAILS', 'menu/adminzone/setup/welcome_emails'];
            }
            if (addon_installed('securitylogging')) {
                $ret['_SEARCH:admin_lookup:browse'] = ['INVESTIGATE_USER', 'menu/adminzone/tools/users/investigate_user'];
            }
            $ret['_SEARCH:admin_privacy:browse'] = ['PRIVACY', 'menu/pages/privacy_policy'];
            /*
            We'll link these from elsewhere instead
            if (addon_installed('ecommerce')) {
                $ret['_SEARCH:admin_ecommerce:browse'] = ['ECOMMERCE', 'menu/adminzone/audit/ecommerce/ecommerce'];
            }
            $ret['_SEARCH:admin_cns_groups:browse'] = ['USERGROUPS', 'menu/social/groups'];
            */
            if (addon_installed('cns_warnings')) {
                $ret['_SEARCH:warnings:edit'] = ['WARNINGS', 'menu/social/warnings'];
            }
        }

        return $ret;
    }

    public $title;

    /**
     * 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'));
        }

        require_code('form_templates'); // Needs to run high so that the anti-click-hacking header is sent

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

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

        set_helper_panel_tutorial('tut_adv_members');

        if ($type == 'step1') {
            set_helper_panel_tutorial('tut_members');

            breadcrumb_set_parents([['_SEARCH:admin_cns_members:browse', do_lang_tempcode('MEMBERS')]]);
            breadcrumb_set_self(do_lang_tempcode('ADD_MEMBER'));
        }

        if ($type == 'step2') {
            breadcrumb_set_parents([['_SEARCH:admin_cns_members:browse', do_lang_tempcode('MEMBERS')], ['_SELF:_SELF:step1', do_lang_tempcode('ADD_MEMBER')]]);
            breadcrumb_set_self(do_lang_tempcode('DETAILS'));
        }

        if ($type == 'delurk') {
            breadcrumb_set_parents([['_SEARCH:admin_cns_members:browse', do_lang_tempcode('MEMBERS')]]);
        }

        if ($type == '_delurk') {
            breadcrumb_set_parents([['_SEARCH:admin_cns_members:browse', do_lang_tempcode('MEMBERS')], ['_SEARCH:admin_cns_members:delurk', do_lang_tempcode('DELETE_LURKERS')]]);
            breadcrumb_set_self(do_lang_tempcode('CONFIRM'));
        }

        if ($type == '__delurk') {
            breadcrumb_set_parents([['_SEARCH:admin_cns_members:browse', do_lang_tempcode('MEMBERS')], ['_SEARCH:admin_cns_members:delurk', do_lang_tempcode('DELETE_LURKERS')]]);
        }

        if ($type == 'import_spreadsheet') {
            breadcrumb_set_parents([['_SEARCH:admin_cns_members:browse', do_lang_tempcode('MEMBERS')]]);
        }

        if ($type == '_import_spreadsheet') {
            breadcrumb_set_parents([['_SEARCH:admin_cns_members:browse', do_lang_tempcode('MEMBERS')], ['_SEARCH:admin_cns_members:import_spreadsheet', do_lang_tempcode('IMPORT_MEMBERS')]]);
            breadcrumb_set_self(do_lang_tempcode('DONE'));
        }

        if ($type == 'export_spreadsheet') {
            breadcrumb_set_parents([['_SEARCH:admin_cns_members:browse', do_lang_tempcode('MEMBERS')]]);
        }

        if ($type == '_export_spreadsheet') {
            breadcrumb_set_parents([['_SEARCH:admin_cns_members:browse', do_lang_tempcode('MEMBERS')], ['_SEARCH:admin_cns_members:export_spreadsheet', do_lang_tempcode('EXPORT_MEMBERS')]]);
            breadcrumb_set_self(do_lang_tempcode('DONE'));
        }

        if ($type == 'step1' || $type == 'step2') {
            $this->title = get_screen_title('ADD_MEMBER');
        }

        if ($type == 'delurk' || $type == '_delurk' || $type == '__delurk') {
            $this->title = get_screen_title('DELETE_LURKERS');
        }

        if ($type == 'import_spreadsheet' || $type == '_import_spreadsheet') {
            $this->title = get_screen_title('IMPORT_MEMBERS');
        }

        if ($type == 'export_spreadsheet' || $type == '_export_spreadsheet') {
            $this->title = get_screen_title('EXPORT_MEMBERS');

            set_helper_panel_text(comcode_lang_string('DOC_EXPORT_MEMBERS'));
        }

        if ($type == 'merge') {
            $this->title = get_screen_title('MERGE_MEMBERS');

            inform_non_canonical_parameter('from');
            inform_non_canonical_parameter('to');

            breadcrumb_set_parents([['_SEARCH:admin_cns_members:browse', do_lang_tempcode('MEMBERS')]]);
        }

        if ($type == '_merge') {
            $this->title = get_screen_title('MERGE_MEMBERS');

            breadcrumb_set_parents([['_SEARCH:admin_cns_members:browse', do_lang_tempcode('MEMBERS')], ['_SELF:_SELF:browse', do_lang_tempcode('MERGE_MEMBERS')]]);
            breadcrumb_set_self(do_lang_tempcode('DONE'));
        }

        return null;
    }

    /**
     * Execute the module.
     *
     * @return Tempcode The result of execution
     */
    public function run() : object
    {
        cns_require_all_forum_stuff();

        require_code('cns_members_action');
        require_code('cns_members_action2');

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

        if ($type == 'browse') {
            return $this->browse();
        }
        if ($type == 'step1') {
            return $this->step1();
        }
        if ($type == 'step2') {
            return $this->step2();
        }
        if ($type == 'delurk') {
            return $this->delurk();
        }
        if ($type == '_delurk') {
            return $this->_delurk();
        }
        if ($type == '__delurk') {
            return $this->__delurk();
        }
        if ($type == 'export_spreadsheet') {
            return $this->export_spreadsheet();
        }
        if ($type == '_export_spreadsheet') {
            return $this->_export_spreadsheet();
        }
        if ($type == 'import_spreadsheet') {
            return $this->import_spreadsheet();
        }
        if ($type == '_import_spreadsheet') {
            return $this->_import_spreadsheet();
        }
        if ($type == 'merge') {
            return $this->merge();
        }
        if ($type == '_merge') {
            return $this->_merge();
        }

        return new Tempcode();
    }

    /**
     * The do-next manager for choosing what to do.
     *
     * @return Tempcode The UI
     */
    public function browse() : object
    {
        if (addon_installed('securitylogging')) {
            require_lang('lookup');
        }
        if (addon_installed('welcome_emails')) {
            require_lang('cns_welcome_emails');
        }
        if (addon_installed('ecommerce')) {
            require_lang('ecommerce');
        }
        require_lang('group_member_timeouts');

        require_code('templates_donext');
        return do_next_manager(
            get_screen_title('MEMBERS'),
            comcode_lang_string('DOC_MEMBERS'),
            [
                ['menu/adminzone/tools/users/member_add', ['admin_cns_members', ['type' => 'step1'], get_module_zone('admin_cns_members')], do_lang_tempcode('ADD_MEMBER'), 'DOC_ADD_MEMBER'],
                (!has_privilege(get_member(), 'member_maintenance')) ? null : ['menu/adminzone/tools/users/member_edit', ['members', ['type' => 'browse', 'include_non_confirmed' => 'include'], get_module_zone('members'), do_lang_tempcode('SWITCH_ZONE_WARNING')], do_lang_tempcode('MEMBER_DIRECTORY'), 'DOC_EDIT_MEMBER'],
                ['menu/adminzone/tools/users/merge_members', ['admin_cns_members', ['type' => 'merge'], get_module_zone('admin_cns_members')], do_lang_tempcode('MERGE_MEMBERS'), 'DOC_MERGE_MEMBERS'],
                (!has_privilege(get_member(), 'mass_import')) ? null : ['menu/adminzone/tools/users/delete_lurkers', ['admin_cns_members', ['type' => 'delurk'], get_module_zone('admin_cns_members')], do_lang_tempcode('DELETE_LURKERS'), 'DOC_DELETE_LURKERS'],
                (!has_privilege(get_member(), 'mass_import')) ? null : ['admin/import_spreadsheet', ['admin_cns_members', ['type' => 'import_spreadsheet'], get_module_zone('admin_cns_members')], do_lang_tempcode('IMPORT_MEMBERS'), 'DOC_IMPORT_MEMBERS'],
                ['admin/export_spreadsheet', ['admin_cns_members', ['type' => 'export_spreadsheet'], get_module_zone('admin_cns_members')], do_lang_tempcode('EXPORT_MEMBERS'), 'DOC_EXPORT_MEMBERS'],
                addon_installed('cns_cpfs') ? ['menu/adminzone/tools/users/custom_profile_fields', ['admin_cns_customprofilefields', ['type' => 'browse'], get_module_zone('admin_cns_customprofilefields')], do_lang_tempcode('CUSTOM_PROFILE_FIELDS'), 'DOC_CUSTOM_PROFILE_FIELDS'] : null,
                addon_installed('welcome_emails') ? ['menu/adminzone/setup/welcome_emails', ['admin_cns_welcome_emails', ['type' => 'browse'], get_module_zone('admin_cns_welcome_emails')], do_lang_tempcode('WELCOME_EMAILS'), 'DOC_WELCOME_EMAILS'] : null,
                addon_installed('securitylogging') ? ['menu/adminzone/tools/users/investigate_user', ['admin_lookup', [], get_module_zone('admin_lookup')], do_lang_tempcode('INVESTIGATE_USER'), 'DOC_INVESTIGATE_USER'] : null,
                addon_installed('cns_warnings') ? ['menu/social/warnings', ['warnings', ['type' => 'edit'], get_module_zone('warnings')], do_lang_tempcode('WARNINGS')] : null,
                ['menu/adminzone/security/usergroups_temp', ['admin_group_member_timeouts', ['type' => 'browse'], get_module_zone('admin_group_member_timeouts')], do_lang_tempcode('GROUP_MEMBER_TIMEOUTS'), 'DOC_GROUP_MEMBER_TIMEOUTS'],
                addon_installed('ecommerce') ? ['menu/adminzone/audit/ecommerce/ecommerce', ['admin_ecommerce', ['type' => 'browse'], get_module_zone('admin_ecommerce')], do_lang_tempcode('CUSTOM_PRODUCT_USERGROUP'), 'DOC_ECOMMERCE'] : null,
                ['menu/social/groups', ['admin_cns_groups', ['type' => 'browse'], get_module_zone('admin_cns_groups'), do_lang_tempcode('SWITCH_SECTION_WARNING')], do_lang_tempcode('USERGROUPS'), 'DOC_GROUPS'],
                ['menu/pages/privacy_policy', ['admin_privacy', ['type' => 'browse'], get_module_zone('admin_privacy'), do_lang_tempcode('SWITCH_SECTION_WARNING')], do_lang_tempcode('PRIVACY'), 'DOC_GDPR'],
            ],
            do_lang('MEMBERS')
        );
    }

    /**
     * The UI for adding a member.
     *
     * @return Tempcode The UI
     */
    public function step1() : object
    {
        url_default_parameters__enable();
        list($fields, $hidden) = cns_get_member_fields(false);
        url_default_parameters__disable();

        $default_temporary_password = (get_option('new_member_default_temporary_password') == '1');
        $default_email_subject = get_option('new_member_default_email_subject');
        $default_email_message = get_option('new_member_default_email_message');

        $fields->attach(do_template('FORM_SCREEN_FIELD_SPACER', [
            '_GUID' => 'a26588ac13dfd13d279be15b3c28ae50',
            'SECTION_HIDDEN' => ($default_email_subject == '') && ($default_email_message == ''),
            'TITLE' => do_lang_tempcode('config:_EMAIL'),
            'HELP' => do_lang_tempcode('ADD_MEMBER_EMAIL_HELP'),
        ]));
        $fields->attach(form_input_line(do_lang_tempcode('SUBJECT'), '', 'email_subject', $default_email_subject, false));
        $fields->attach(form_input_text(do_lang_tempcode('MESSAGE'), '', 'email_message', $default_email_message, false, false));

        $fields->attach(do_template('FORM_SCREEN_FIELD_SPACER', [
            '_GUID' => '101063c817a45c10bca5c384e1f32bf1',
            'SECTION_HIDDEN' => $default_temporary_password,
            'TITLE' => do_lang_tempcode('OPTIONS'),
        ]));
        $fields->attach(form_input_tick(do_lang_tempcode('FORCE_TEMPORARY_PASSWORD'), do_lang_tempcode('DESCRIPTION_FORCE_TEMPORARY_PASSWORD'), 'temporary_password', $default_temporary_password));

        $text = do_lang_tempcode('_ENTER_PROFILE_DETAILS');

        $submit_name = do_lang_tempcode('ADD');
        $url = build_url(['page' => '_SELF', 'type' => 'step2'], '_SELF');
        return do_template('FORM_SCREEN', [
            '_GUID' => '3724dec184e27bb1bfebc5712e8faec2',
            'HIDDEN' => $hidden,
            'TITLE' => $this->title,
            'FIELDS' => $fields,
            'TEXT' => $text,
            'SUBMIT_ICON' => 'menu/site_meta/user_actions/join',
            'SUBMIT_NAME' => $submit_name,
            'URL' => $url,
        ]);
    }

    /**
     * The actualiser for adding a member.
     *
     * @return Tempcode The UI
     */
    public function step2() : object
    {
        cms_extend_time_limit(TIME_LIMIT_EXTEND__MODEST);

        // Read in data...

        $username = post_param_string('username', false, INPUT_FILTER_POST_IDENTIFIER);
        $password = post_param_string('password', false, INPUT_FILTER_PASSWORD);
        $email_address = post_param_string('email', member_field_is_required(null, 'email_address') ? false : '', INPUT_FILTER_POST_IDENTIFIER | INPUT_FILTER_EMAIL_ADDRESS);

        $primary_group = (has_privilege(get_member(), 'assume_any_member')) ? post_param_integer('primary_group') : null;
        if (!array_key_exists('secondary_groups', $_POST)) {
            $_POST['secondary_groups'] = [];
        }

        require_code('temporal2');
        list($dob_year, $dob_month, $dob_day) = post_param_date_components('birthday');
        if (($dob_year === null) || ($dob_month === null) || ($dob_day === null)) {
            if (member_field_is_required(null, 'dob', null, null)) {
                warn_exit(do_lang_tempcode('NO_PARAMETER_SENT', escape_html('birthday')));
            }

            $dob_day = null;
            $dob_month = null;
            $dob_year = null;
        }

        $custom_fields = cns_get_all_custom_fields_match(cns_get_all_default_groups(true));
        $actual_custom_fields = cns_read_in_custom_fields($custom_fields);

        require_code('locations');
        $region = post_param_region('region', '');

        $timezone = post_param_string('timezone', get_site_timezone());
        $language = post_param_string('language', get_site_default_lang());
        $theme = post_param_string('theme', '');

        $preview_posts = post_param_integer('preview_posts', 0);
        $reveal_age = post_param_integer('reveal_age', 0);
        $views_signatures = post_param_integer('views_signatures', 0);
        $auto_monitor_contrib_content = post_param_integer('auto_monitor_contrib_content', 0);
        $smart_topic_notification = post_param_integer('smart_topic_notification', 0);
        $mailing_list_style = post_param_integer('mailing_list_style', 0);
        $auto_mark_read = post_param_integer('auto_mark_read', 0);
        $sound_enabled = post_param_integer('sound_enabled', 0);
        $allow_emails = post_param_integer('allow_emails', 0);
        $allow_emails_from_staff = post_param_integer('allow_emails_from_staff', 0);

        $all_pt_allow = '';
        $tmp_groups = $GLOBALS['CNS_DRIVER']->get_usergroup_list(true, true);
        foreach (array_keys($tmp_groups) as $key) {
            if ($key != db_get_first_id()) {
                if ($all_pt_allow != '') {
                    $all_pt_allow .= ',';
                }
                $all_pt_allow .= strval($key);
            }
        }
        $pt_allow = array_key_exists('pt_allow', $_POST) ? implode(',', $_POST['pt_allow']) : '';
        if ($pt_allow == $all_pt_allow) {
            $pt_allow = '*';
        }
        $pt_rules_text = post_param_string('pt_rules_text', '');

        $validated = post_param_integer('validated', 0);

        // Secondary groups
        $secondary_groups = [];
        if (array_key_exists('secondary_groups', $_POST)) {
            $group_count = $GLOBALS['FORUM_DB']->query_select_value('f_groups', 'COUNT(*)');
            $groups = list_to_map('id', $GLOBALS['FORUM_DB']->query_select('f_groups', ['*'], ($group_count > 200) ? ['g_is_private_club' => 0] : []));
            foreach ($_POST['secondary_groups'] as $group_id) {
                $group = $groups[intval($group_id)];

                if ((!has_privilege(get_member(), 'see_hidden_groups')) && ($group['g_hidden'] == 1)) {
                    continue;
                }

                if ((!has_privilege(get_member(), 'assume_any_member')) && ($group['g_open_membership'] == 0)) {
                    continue;
                }

                $secondary_groups[] = $group['id'];
            }
        }

        // Add member
        $password_compat_scheme = ((post_param_integer('temporary_password', 0) == 1) ? 'bcrypt_temporary' : 'bcrypt');
        $id = cns_make_member(
            $username, // username
            $password, // password
            $email_address, // email_address
            $primary_group, // primary_group
            $secondary_groups, // secondary_groups
            $dob_day, // dob_day
            $dob_month, // dob_month
            $dob_year, // dob_year
            $actual_custom_fields, // custom_fields
            $timezone, // timezone
            $region, // Region
            $language, // language
            $theme, // theme
            '', // title
            '', // photo_url
            null, // avatar_url
            '', // signature
            $preview_posts, // preview_posts
            $reveal_age, // reveal_age
            $views_signatures, // views_signatures
            $auto_monitor_contrib_content, // auto_monitor_contrib_content
            $smart_topic_notification, // smart_topic_notification
            $mailing_list_style, // mailing_list_style
            $auto_mark_read, // auto_mark_read
            $sound_enabled, // sound_enabled
            $allow_emails, // allow_emails
            $allow_emails_from_staff, // allow_emails_from_staff
            post_param_integer('highlighted_name', 0), // highlighted_name
            $pt_allow, // pt_allow
            $pt_rules_text, // pt_rules_text
            $validated, // validated
            '', // validated_email_confirm_code
            null, // probation_expiration_time
            '0', // is_perm_banned
            true, // check_correctness
            '', // ip_address
            $password_compat_scheme // password_compatibility_scheme
        );

        if (addon_installed('content_reviews')) {
            require_code('content_reviews2');
            content_review_set('member', strval($id));
        }

        log_it('ADD_MEMBER', strval($id), $username);

        // Send e-mail, if requested
        $email_subject = post_param_string('email_subject', '');
        $email_message = post_param_string('email_message', '');
        if ($email_message != '') {
            $to_name = $GLOBALS['FORUM_DRIVER']->get_displayname($username);

            if (addon_installed('newsletter')) {
                // Newsletter-style variable substitution
                require_code('newsletter');
                $extra_mappings = $GLOBALS['FORUM_DRIVER']->get_member_row($id);
                require_code('cns_members');
                $extra_mappings += cns_get_custom_field_mappings($id);
                $extra_mappings['username'] = $username;
                $extra_mappings['password'] = $password;
                $email_message = newsletter_prepare($email_message, $email_subject, $language, '', '', $to_name, $email_address, 'w' . strval($id), '', @array_map('strval', $extra_mappings), null/*no wrapper*/);
            }

            require_code('mail');
            dispatch_mail($email_subject, $email_message, '', ($email_address == '') ? null : [$email_address], $to_name, '', '', ['bypass_queue' => true]);
        }

        $special_links = [];

        if (addon_installed('galleries')) {
            require_lang('galleries');
            $special_links[] = ['menu/rich_content/galleries', ['cms_galleries', ['type' => 'import', 'member_id' => $id], get_module_zone('cms_galleries')], do_lang('ADD_GALLERY')];
        }

        require_code('templates_donext');
        return do_next_manager(
            $this->title,
            do_lang_tempcode('SUCCESS'),
            [],
            null,
            /* TYPED-ORDERED LIST OF 'LINKS' */
            ['_SELF', ['type' => 'step1'], '_SELF'], // Add one
            null, // Edit this
            null, // Edit one
            ['members', ['type' => 'view', 'id' => $id], get_module_zone('members')], // View this
            ['members', ['type' => 'browse'], get_module_zone('members'), do_lang_tempcode('MEMBERS')], // View archive
            null, // Add one category
            null, // Edit one category
            null, // Edit this category
            null, // View this category
            /* SPECIALLY TYPED 'LINKS' */
            $special_links,
            [],
            [],
            null,
            null,
            do_lang_tempcode('MEMBERS'),
            null,
            'member'
        );
    }

    /**
     * The UI for choosing delurk criteria.
     *
     * @return Tempcode The UI
     */
    public function delurk() : object
    {
        require_lang('cns_lurkers');

        check_privilege('mass_import');

        $hidden = new Tempcode();

        url_default_parameters__enable();

        $_max_posts = get_value('delurk__max_posts', null, true);
        $_max_points = get_value('delurk__max_points', null, true);
        $_max_logged_actions = get_value('delurk__max_logged_actions', null, true);
        $_min_days_since_login = get_value('delurk__min_days_since_login', null, true);
        $_min_days_since_join = get_value('delurk__min_days_since_join', null, true);
        $_non_confirmed = get_value('delurk__non_confirmed', null, true);
        $_usergroups = get_value('delurk__usergroups', null, true);
        if ($_max_posts === null) {
            $max_posts = 2;
        } else {
            $max_posts = intval($_max_posts);
        }
        if ($_max_points === null) {
            $max_points = 150;
        } else {
            $max_points = intval($_max_points);
        }
        if ($_max_logged_actions === null) {
            $max_logged_actions = 2;
        } else {
            $max_logged_actions = intval($_max_logged_actions);
        }
        if ($_min_days_since_login === null) {
            $min_days_since_login = 60;
        } else {
            $min_days_since_login = intval($_min_days_since_login);
        }
        if ($_min_days_since_join === null) {
            $min_days_since_join = 90;
        } else {
            $min_days_since_join = intval($_min_days_since_join);
        }
        if ($_non_confirmed === null) {
            $non_confirmed = false;
        } else {
            $non_confirmed = ($_non_confirmed == '1');
        }
        if ($_usergroups === null) {
            $usergroups = [];
        } else {
            $temp = explode(',', $_usergroups);
            $usergroups = [];
            foreach ($temp as $t) {
                if ((is_numeric($t))) {
                    $usergroups[] = intval($t);
                }
            }
        }

        $fields = new Tempcode();
        $fields->attach(form_input_integer(do_lang_tempcode('DELURK_MAX_POSTS'), do_lang_tempcode('DELURK_MAX_POSTS_DESCRIPTION'), 'max_posts', $max_posts, false));
        if (addon_installed('points')) {
            $fields->attach(form_input_integer(do_lang_tempcode('DELURK_MAX_POINTS'), do_lang_tempcode('DELURK_MAX_POINTS_DESCRIPTION'), 'max_points', $max_points, false));
        } else {
            $hidden->attach(form_input_hidden('max_points', ''));
        }
        $fields->attach(form_input_integer(do_lang_tempcode('DELURK_MAX_LOGGED_ACTIONS'), do_lang_tempcode('DELURK_MAX_LOGGED_ACTIONS_DESCRIPTION'), 'max_logged_actions', $max_logged_actions, false));
        $fields->attach(form_input_integer(do_lang_tempcode('DELURK_MIN_DAYS_SINCE_LOGIN'), do_lang_tempcode('DELURK_MIN_DAYS_SINCE_LOGIN_DESCRIPTION'), 'min_days_since_login', $min_days_since_login, false));
        $fields->attach(form_input_integer(do_lang_tempcode('DELURK_MIN_DAYS_SINCE_JOIN'), do_lang_tempcode('DELURK_MIN_DAYS_SINCE_JOIN_DESCRIPTION'), 'min_days_since_join', $min_days_since_join, false));
        $fields->attach(form_input_tick(do_lang_tempcode('DELURK_NON_CONFIRMED'), do_lang_tempcode('DELURK_NON_CONFIRMED_DESCRIPTION'), 'non_confirmed', $non_confirmed));
        $groups = new Tempcode();
        $group_count = $GLOBALS['FORUM_DB']->query_select_value('f_groups', 'COUNT(*)');
        $rows = $GLOBALS['FORUM_DB']->query_select('f_groups', ['id', 'g_name'], ($group_count > 200) ? ['g_is_private_club' => 0] : []);
        foreach ($rows as $row) {
            if ($row['id'] != db_get_first_id()) {
                $groups->attach(form_input_list_entry(strval($row['id']), in_array($row['id'], $usergroups), get_translated_text($row['g_name'], $GLOBALS['FORUM_DB'])));
            }
        }
        $fields->attach(form_input_multi_list(do_lang_tempcode('EXCEPT_IN_USERGROUPS'), do_lang_tempcode('DELURK_USERGROUPS_DESCRIPTION'), 'usergroups', $groups));

        url_default_parameters__disable();

        $submit_name = do_lang_tempcode('PROCEED');

        $post_url = build_url(['page' => '_SELF', 'type' => '_delurk'], '_SELF');

        $text = do_lang_tempcode('CHOOSE_DELURK_CRITERIA');

        $hidden->attach(form_input_hidden('csrf_token_preserve', '1'));

        require_code('form_templates');
        list($warning_details, $ping_url) = handle_conflict_resolution(false, 'delurk');

        return do_template('FORM_SCREEN', [
            '_GUID' => 'f911fc5be2865bdd065abf7c636530d4',
            'TITLE' => $this->title,
            'HIDDEN' => $hidden,
            'FIELDS' => $fields,
            'URL' => $post_url,
            'TEXT' => $text,
            'SUBMIT_ICON' => 'buttons/proceed',
            'SUBMIT_NAME' => $submit_name,
            'WARNING_DETAILS' => $warning_details,
            'PING_URL' => $ping_url,
        ]);
    }

    /**
     * Find a mapping of member IDs to usernames, of those who'll get delurked.
     *
     * @param  integer $max_posts Maximum forum posts
     * @param  integer $max_points Maximum points
     * @param  integer $max_logged_actions Maximum logged actions
     * @param  integer $min_days_since_login Minimum days since last login
     * @param  integer $min_days_since_join Minimum days since joining
     * @param  boolean $non_confirmed Non-confirmed only
     * @param  array $usergroups List of usergroups
     * @return array Mapping of lurkers
     */
    public function find_lurkers(int $max_posts, int $max_points, int $max_logged_actions, int $min_days_since_login, int $min_days_since_join, bool $non_confirmed, array $usergroups) : array
    {
        if (addon_installed('points')) {
            require_code('points');
        }

        $out = [];
        $start = 0;
        do {
            cms_extend_time_limit(10);

            send_http_output_ping();

            $sql = 'SELECT id,m_username FROM ' . $GLOBALS['FORUM_DB']->get_table_prefix() . 'f_members WHERE';
            $sql .= ' id<>' . strval($GLOBALS['FORUM_DRIVER']->get_guest_id());
            $sql .= ' AND id<>' . strval(get_member());
            if ($max_posts > 0) {
                $sql .= ' AND m_cache_num_posts<=' . strval($max_posts);
            }
            if ($min_days_since_login > 0) {
                $sql .= ' AND m_last_visit_time<' . strval(time() - $min_days_since_login * 60 * 60 * 24);
            }
            if ($min_days_since_join > 0) {
                $sql .= ' AND m_join_time<' . strval(time() - $min_days_since_join * 60 * 60 * 24);
            }
            if ($non_confirmed) {
                $sql .= ' AND ' . db_string_equal_to('m_validated_email_confirm_code', '');
            }
            $rows = $GLOBALS['FORUM_DB']->query($sql, 500, $start);
            foreach ($rows as $row) {
                if (addon_installed('points') && ($max_points > 0)) {
                    if (points_rank($row['id']) > $max_points) {
                        continue;
                    }
                }
                $_usergroups = $GLOBALS['FORUM_DRIVER']->get_members_groups($row['id']);
                foreach ($_usergroups as $g_id) {
                    if (in_array($g_id, $usergroups)) {
                        continue 2;
                    }
                }
                $num_actions = $GLOBALS['SITE_DB']->query_select_value('actionlogs', 'COUNT(*)', ['member_id' => $row['id']]);
                if (($max_logged_actions > 0) && ($num_actions > $max_logged_actions)) {
                    continue;
                }

                if (count($out) == 500) {
                    attach_message(do_lang_tempcode('TOO_MANY_LURKERS'), 'notice');
                    return $out;
                }

                $out[$row['id']] = $row['m_username'];
            }
            $start += 500;
        } while (!empty($rows));

        return $out;
    }

    /**
     * The UI for confirming the deletion results of delurk criteria.
     *
     * @return Tempcode The UI
     */
    public function _delurk() : object
    {
        check_privilege('mass_import');

        require_lang('cns_lurkers');

        $max_posts = post_param_integer('max_posts', 0);
        $max_points = post_param_integer('max_points', 0);
        $max_logged_actions = post_param_integer('max_logged_actions', 0);
        $min_days_since_login = post_param_integer('min_days_since_login', 0);
        $min_days_since_join = post_param_integer('min_days_since_join', 0);
        $non_confirmed = (post_param_integer('non_confirmed', 0) == 1);
        $usergroups = [];
        if (array_key_exists('usergroups', $_POST)) {
            foreach ($_POST['usergroups'] as $g_id) {
                if ((is_numeric($g_id))) {
                    $usergroups[] = intval($g_id);
                }
            }
        }

        set_value('delurk__max_posts', strval($max_posts), true);
        set_value('delurk__max_points', strval($max_points), true);
        set_value('delurk__max_logged_actions', strval($max_logged_actions), true);
        set_value('delurk__min_days_since_login', strval($min_days_since_login), true);
        set_value('delurk__min_days_since_join', strval($min_days_since_join), true);
        set_value('delurk__non_confirmed', $non_confirmed ? '1' : '0', true);
        set_value('delurk__usergroups', implode(',', array_map('strval', $usergroups)), true);

        $lurkers = $this->find_lurkers($max_posts, $max_points, $max_logged_actions, $min_days_since_login, $min_days_since_join, $non_confirmed, $usergroups);

        if (empty($lurkers)) {
            inform_exit(do_lang_tempcode('NO_LURKERS_FOUND'));
        }

        $_lurkers = [];
        foreach ($lurkers as $id => $username) {
            if (is_guest($id)) {
                warn_exit(do_lang_tempcode('INTERNAL_ERROR', escape_html('069cc2a06f1451f993876ab9c3d89539')));
            }

            $_lurkers[] = ['ID' => strval($id), 'USERNAME' => $username, 'PROFILE_URL' => $GLOBALS['FORUM_DRIVER']->member_profile_url($id, true)];
        }

        $url = build_url(['page' => '_SELF', 'type' => '__delurk'], '_SELF');

        require_code('form_templates');
        list($warning_details, $ping_url) = handle_conflict_resolution(false, 'delurk');

        return do_template('CNS_DELURK_CONFIRM_SCREEN', [
            '_GUID' => '52870b8546653782e354533602531970',
            'TITLE' => $this->title,
            'LURKERS' => $_lurkers,
            'URL' => $url,
            'WARNING_DETAILS' => $warning_details,
            'PING_URL' => $ping_url,
        ]);
    }

    /**
     * The actualiser for deletion members according to delurk criteria.
     *
     * @return Tempcode The UI
     */
    public function __delurk() : object
    {
        require_lang('cns_lurkers');
        require_code('tasks');
        require_lang('cns');

        check_privilege('mass_import');

        log_it('DELETE_LURKERS');

        foreach ($_POST as $key => $val) {
            if ((is_string($key)) && (substr($key, 0, 7) == 'lurker_')) {
                $member_id = intval(substr($key, 7));

                // Queue the task
                call_user_func_array__long_task(do_lang('DELETE_MEMBER'), null, 'cns_delete_member', [$member_id, get_member(), false], true, false, false); // No notification because the member will be deleted

                // Security: Invalidate the session and prevent the member from logging in
                require_code('users_active_actions');
                delete_session_by_member_id($member_id);
                $GLOBALS['FORUM_DB']->query_update('f_members', ['m_password_compat_scheme' => 'pending_deletion'], ['id' => $member_id]);
            }
        }

        return inform_screen($this->title, do_lang_tempcode('SUCCESS'));
    }

    /**
     * The UI to export a spreadsheet file of members.
     *
     * @return Tempcode The UI
     */
    public function export_spreadsheet() : object
    {
        check_privilege('view_any_profile_field');

        $hidden = new Tempcode();
        $fields = new Tempcode();

        $js_function_calls = [];

        // Option to filter by whether members allow e-mails
        $fields->attach(form_input_tick(do_lang_tempcode('FILTER_BY_ALLOW'), do_lang_tempcode('DESCRIPTION_FILTER_BY_ALLOW'), 'filter_by_allow', get_param_integer('filter_by_allow', 0) == 1));

        // Select fields
        $fields_to_use = new Tempcode();
        require_code('cns_members_action2');
        list($headings) = member_get_spreadsheet_headings_extended();
        foreach ($headings as $field_label => $field_name) {
            $fields_to_use->attach(form_input_list_entry($field_label, true));
        }
        $fields->attach(form_input_multi_list(do_lang_tempcode('COLUMNS'), do_lang_tempcode('SELECT_COLUMNS_TO_INCLUDE'), 'fields_to_use', $fields_to_use, null, 10, true));

        // Order by
        $fields_to_order_by = new Tempcode();
        foreach ($headings as $field_label => $field_name) {
            $fields_to_order_by->attach(form_input_list_entry($field_label, $field_name === 'id'));
        }
        $fields->attach(form_input_list(do_lang_tempcode('ORDER'), do_lang_tempcode('MEMBER_EXPORT_ORDER'), 'order_by', $fields_to_order_by, null, false, true));

        // Usergroups
        $groups = cns_create_selection_list_usergroups();
        $fields->attach(form_input_multi_list(do_lang_tempcode('USERGROUPS'), do_lang_tempcode('SELECT_USERGROUPS_TO_FILTER'), 'usergroups', $groups, null, 10, false));

        // File name
        $fields->attach(do_template('FORM_SCREEN_FIELD_SPACER', ['_GUID' => '16d396d2357684d2dbfebefbd20776a3', 'SECTION_HIDDEN' => false, 'TITLE' => do_lang_tempcode('FILENAME')]));
        $filename = cms_mb_strtolower(do_lang('MEMBERS')) . '-' . date('Y-m-d');
        $fields->attach(form_input_line(do_lang_tempcode('NAME'), '', 'filename', $filename, true));

        // ...

        $submit_name = do_lang_tempcode('EXPORT_MEMBERS');
        $post_url = build_url(['page' => '_SELF', 'type' => '_export_spreadsheet'], '_SELF');
        $text = '';

        return do_template('FORM_SCREEN', [
            '_GUID' => '24cae29bc329a307a94c8b3f1e087708',
            'TITLE' => $this->title,
            'HIDDEN' => $hidden,
            'FIELDS' => $fields,
            'URL' => $post_url,
            'TEXT' => $text,
            'SUBMIT_ICON' => 'admin/export_spreadsheet',
            'SUBMIT_NAME' => $submit_name,
            'TARGET' => '_blank',
            'JS_FUNCTION_CALLS' => $js_function_calls,
        ]);
    }

    /**
     * The actualiser to export a spreadsheet of members.
     *
     * @return Tempcode The UI
     */
    public function _export_spreadsheet() : object
    {
        check_privilege('view_any_profile_field');

        $filter_by_allow = post_param_integer('filter_by_allow', 0);
        $fields_to_use = isset($_POST['fields_to_use']) ? $_POST['fields_to_use'] : [];
        $usergroups = isset($_POST['usergroups']) ? $_POST['usergroups'] : [];
        $order_by = post_param_string('order_by');

        log_it('EXPORT_MEMBERS');

        require_code('tasks');
        return call_user_func_array__long_task(do_lang('EXPORT_MEMBERS'), $this->title, 'export_members', [$filter_by_allow == 1, $fields_to_use, $usergroups, $order_by]);
    }

    /**
     * The UI for importing a spreadsheet file.
     *
     * @return Tempcode The UI
     */
    public function import_spreadsheet() : object
    {
        check_privilege('mass_import');
        check_privilege('member_maintenance');

        $hidden = new Tempcode();

        $fields = new Tempcode();
        handle_max_file_size($hidden);
        require_code('files_spreadsheets_read');
        $fields->attach(form_input_upload(do_lang_tempcode('UPLOAD'), do_lang_tempcode('DESCRIPTION_IMPORT_SPREADSHEET'), 'file', true, null, null, true, spreadsheet_read_file_types()));
        $fields->attach(form_input_line(do_lang_tempcode('DEFAULT_PASSWORD'), do_lang_tempcode('DESCRIPTION_DEFAULT_PASSWORD'), 'default_password', '', false));
        $fields->attach(form_input_tick(do_lang_tempcode('FORCE_TEMPORARY_PASSWORD'), do_lang_tempcode('DESCRIPTION_FORCE_TEMPORARY_PASSWORD'), 'temporary_password', false));

        $submit_name = do_lang_tempcode('IMPORT_MEMBERS');
        $post_url = build_url(['page' => '_SELF', 'type' => '_import_spreadsheet'], '_SELF');
        $text = '';

        require_code('form_templates');
        list($warning_details, $ping_url) = handle_conflict_resolution(false, false);

        return do_template('FORM_SCREEN', [
            '_GUID' => '9196652a093d7f3a0e5dd0922f74cc51',
            'TITLE' => $this->title,
            'HIDDEN' => $hidden,
            'FIELDS' => $fields,
            'URL' => $post_url,
            'TEXT' => $text,
            'SUBMIT_ICON' => 'admin/import_spreadsheet',
            'SUBMIT_NAME' => $submit_name,
            'WARNING_DETAILS' => $warning_details,
            'PING_URL' => $ping_url,
        ]);
    }

    /**
     * The actualiser for importing a spreadsheet file.
     *
     * @return Tempcode The UI
     */
    public function _import_spreadsheet() : object
    {
        check_privilege('mass_import');
        check_privilege('member_maintenance');

        $default_password = post_param_string('default_password', false, INPUT_FILTER_NONE);

        $use_temporary_passwords = (post_param_integer('temporary_password', 0) == 1);

        require_code('uploads');
        $target_path = get_temporary_upload_path('file');

        require_code('files_spreadsheets_read');
        if (!is_spreadsheet_readable($_FILES['file']['name'])) {
            unlink($target_path);
            sync_file($target_path);
            warn_exit(do_lang_tempcode('UNKNOWN_FORMAT', escape_html(get_file_extension($_FILES['file']['name']))));
        }

        log_it('IMPORT_MEMBERS');

        require_code('tasks');
        return call_user_func_array__long_task(do_lang('IMPORT_MEMBERS'), $this->title, 'import_members', [$default_password, $use_temporary_passwords, $target_path, basename($target_path)]);
    }

    /**
     * The UI for choosing members to merge.
     *
     * @return Tempcode The UI
     */
    public function merge() : object
    {
        $fields = new Tempcode();

        $from = get_param_string('from', '', INPUT_FILTER_GET_COMPLEX);
        $to = get_param_string('to', '', INPUT_FILTER_GET_COMPLEX);

        $fields->attach(form_input_username(do_lang_tempcode('FROM'), do_lang_tempcode('DESCRIPTION_MEMBER_FROM'), 'from', $from, true));
        $fields->attach(form_input_username(do_lang_tempcode('TO'), do_lang_tempcode('DESCRIPTION_MEMBER_TO'), 'to', $to, true));

        if (is_on_multi_site_network()) {
            $fields->attach(form_input_tick(do_lang_tempcode('MERGING_ON_MSN'), do_lang_tempcode('DESCRIPTION_MERGING_ON_MSN'), 'keep', true));
        }

        $submit_name = do_lang_tempcode('MERGE_MEMBERS');
        $post_url = build_url(['page' => '_SELF', 'type' => '_merge'], '_SELF');
        $text = do_lang_tempcode('MERGE_MEMBERS_TEXT');

        require_code('form_templates');
        list($warning_details, $ping_url) = handle_conflict_resolution(false, false);

        return do_template('FORM_SCREEN', [
            '_GUID' => '6f6b18d90bbe9550303ab41be0a26dcb',
            'SKIP_WEBSTANDARDS' => true,
            'TITLE' => $this->title,
            'URL' => $post_url,
            'FIELDS' => $fields,
            'HIDDEN' => '',
            'TEXT' => $text,
            'SUBMIT_ICON' => 'admin/merge',
            'SUBMIT_NAME' => $submit_name,
            'WARNING_DETAILS' => $warning_details,
            'PING_URL' => $ping_url,
        ]);
    }

    /**
     * The actualiser for merging members.
     *
     * @return Tempcode The UI
     */
    public function _merge() : object
    {
        $to_username = post_param_string('to');
        $to_id = $GLOBALS['FORUM_DRIVER']->get_member_from_username($to_username);
        if (($to_id === null) || (is_guest($to_id))) {
            warn_exit(do_lang_tempcode('_MEMBER_NO_EXIST', escape_html($to_username)), false, false, 404);
        }
        $from_username = post_param_string('from');
        $from_id = $GLOBALS['FORUM_DRIVER']->get_member_from_username($from_username);

        if (($from_id === null) || (is_guest($from_id))) {
            warn_exit(do_lang_tempcode('_MEMBER_NO_EXIST', escape_html($from_username)), false, false, 404);
        }

        require_code('cns_members2');

        cns_merge_members($from_id, $to_id, (post_param_integer('keep', 0) != 1));

        $user_link = $GLOBALS['FORUM_DRIVER']->member_profile_hyperlink($to_id, '', false);
        return inform_screen($this->title, do_lang_tempcode('MERGED_MEMBERS', $user_link));
    }
}
