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

require_code('crud_module');

/**
 * Module page class.
 */
class Module_warnings extends Standard_crud_module
{
    public $lang_type = 'WARNING';
    public $select_name = 'SUBMITTER';
    public $select_name_description = '';
    public $redirect_type = '!';
    public $menu_label = 'WARNINGS';
    public $table = 'f_warnings';
    public $orderer = 'w_time';

    /**
     * Find icon for this module, specifically to find an icon for the module's main sitemap node. Defined when there is no entry-point for a default page call.
     *
     * @return string Icon
     */
    public function get_wrapper_icon() : string
    {
        return 'menu/social/warnings';
    }

    /**
     * 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 (!addon_installed('cns_warnings')) {
            return null;
        }

        if (get_forum_type() != 'cns') {
            return null;
        }

        if ($be_deferential) {
            return null;
        }

        if ($check_perms) {
            cns_require_all_forum_stuff();
            if (!cns_may_warn_members()) {
                return null;
            }
        }

        return (!$check_perms || !is_guest($member_id)) ? (parent::get_entry_points()) : [];
    }

    public $title;

    /**
     * Module pre-run function. Allows us to know metadata for <head> before we start streaming output.
     *
     * @param  boolean $top_level Whether this is running at the top level, prior to having sub-objects called
     * @param  ?ID_TEXT $type The screen type to consider for metadata purposes (null: read from environment)
     * @return ?Tempcode Tempcode indicating some kind of exceptional output (null: none)
     */
    public function pre_run(bool $top_level = true, ?string $type = null) : ?object
    {
        require_code('cns_warnings');
        require_lang('cns_warnings');

        $error_msg = new Tempcode();
        if (!addon_installed__messaged('cns_warnings', $error_msg)) {
            return $error_msg;
        }

        if (get_forum_type() != 'cns') {
            warn_exit(do_lang_tempcode('NO_CNS'));
        }

        $type = get_param_string('type', 'history');
        $member_id = get_param_integer('id', null);

        cns_require_all_forum_stuff();

        if (addon_installed('securitylogging')) {
            require_lang('submitban');
        }

        if ($type == 'history') {
            $this->title = get_screen_title('REVIEW_WARNING_HISTORY');
            if ($member_id !== null) {
                $breadcrumbs = [['_SEARCH:members', do_lang_tempcode('MEMBERS')], ['_SEARCH:members:view:' . strval($member_id), $GLOBALS['FORUM_DRIVER']->get_username($member_id)]];
                breadcrumb_set_parents($breadcrumbs);
            }
            breadcrumb_set_self(do_lang_tempcode('WARNINGS'));
        }

        if ($type == 'view') {
            $this->title = get_screen_title('WARNING_NUMBER', true, [strval(get_param_integer('id'))]);
            $member_id = get_param_integer('member_id', null);
            breadcrumb_set_self(do_lang_tempcode('WARNING_NUMBER', strval(get_param_integer('id'))));
            if ($member_id !== null) {
                $breadcrumbs = [['_SEARCH:members', do_lang_tempcode('MEMBERS')], ['_SEARCH:members:view:' . strval($member_id), $GLOBALS['FORUM_DRIVER']->get_username($member_id)], ['_SEARCH:warnings:history:' . strval($member_id), do_lang_tempcode('WARNINGS')]];
                breadcrumb_set_parents($breadcrumbs);
            } else {
                $breadcrumbs = [['_SEARCH:warnings:history', do_lang_tempcode('WARNINGS')]];
                breadcrumb_set_parents($breadcrumbs);
            }
        }

        if ($type == 'undo_punitive_action') {
            $this->title = get_screen_title('UNDO_PUNITIVE_ACTION', true, [strval(post_param_integer('id'))]);
            breadcrumb_set_self(do_lang_tempcode('PUNITIVE_ACTION_NUMBER', strval(post_param_integer('id'))));

            $warning_id = $GLOBALS['FORUM_DB']->query_select_value_if_there('f_warnings_punitive', 'p_warning_id', ['id' => post_param_integer('id')]);
            if ($warning_id !== null) {
                $member_id = $GLOBALS['FORUM_DB']->query_select_value_if_there('f_warnings', 'w_member_id', ['id' => $warning_id]);
                if ($member_id !== null) {
                    $breadcrumbs = [['_SEARCH:members', do_lang_tempcode('MEMBERS')], ['_SEARCH:members:view:' . strval($member_id), $GLOBALS['FORUM_DRIVER']->get_username($member_id)], ['_SEARCH:warnings:history:' . strval($member_id), do_lang_tempcode('WARNINGS')], ['_SEARCH:warnings:view:' . strval($warning_id), do_lang_tempcode('WARNING_NUMBER', strval($warning_id))]];
                } else {
                    $breadcrumbs = [['_SEARCH:warnings:history', do_lang_tempcode('WARNINGS')]];
                }
                breadcrumb_set_parents($breadcrumbs);
            }
        }

        return parent::pre_run($top_level);
    }

    /**
     * Standard crud_module run_start.
     *
     * @param  ID_TEXT $type The type of module execution
     * @return Tempcode The output of the run
     */
    public function run_start(string $type) : object
    {
        require_code('cns_moderation_action');
        require_code('cns_moderation_action2');

        if (!cns_may_warn_members()) {
            access_denied('PRIVILEGE', 'warn_members');
        }

        if ($type == 'history') {
            return $this->history();
        }

        if ($type == 'view') {
            return $this->view();
        }

        if ($type == 'undo_punitive_action') {
            return $this->undo_punitive_action();
        }

        return new Tempcode();
    }

    /**
     * Get Tempcode for an adding form.
     *
     * @return mixed Either Tempcode; or a tuple of: (fields, hidden-fields[, delete-fields][, edit-text][, whether all delete fields are specified][, posting form text, more fields][, parsed WYSIWYG editable text])
     */
    public function get_form_fields_for_add()
    {
        return $this->get_form_fields();
    }

    /**
     * Get Tempcode for a warning adding/editing form.
     *
     * @param  boolean $new Whether it is a new warning/punishment record
     * @param  LONG_TEXT $explanation The explanation for the warning/punishment record
     * @param  BINARY $is_warning Whether to make this a formal warning
     * @param  ?MEMBER $member_id The member the warning is for (null: get from environment)
     * @return array A pair: The input fields, Hidden fields
     */
    public function get_form_fields(bool $new = true, string $explanation = '', int $is_warning = 1, ?int $member_id = null) : array
    {
        require_code('form_templates');

        if ($member_id === null) {
            $member_id = get_param_integer('member_id', get_member());
        }

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

        $post_id = get_param_integer('post_id', null);
        $spam_mode = get_param_integer('spam', 0);
        $ip_address = ($post_id === null) ? null : $GLOBALS['FORUM_DB']->query_select_value_if_there('f_posts', 'p_ip_address', ['id' => $post_id]);

        $username = $GLOBALS['FORUM_DRIVER']->get_username($member_id);
        $num_warnings = $GLOBALS['FORUM_DRIVER']->get_member_row_field($member_id, 'm_cache_warnings');
        $_rules_url = build_url(['page' => 'rules']);
        $rules_url = $_rules_url->evaluate();
        $_history_url = build_url(['page' => '_SELF', 'type' => 'history', 'id' => $member_id], '_SELF');
        $history_url = $_history_url->evaluate();
        if (((!is_guest($member_id)) || ($ip_address !== null)) && (addon_installed('securitylogging'))) {
            $_lookup_url = build_url(['page' => 'admin_lookup', 'type' => 'results', 'param' => is_guest($member_id) ? $ip_address : $member_id], get_module_zone('admin_lookup'));
        } else {
            $_lookup_url = $GLOBALS['FORUM_DRIVER']->member_profile_url($member_id, true);
        }
        if (is_object($_lookup_url)) {
            $lookup_url = $_lookup_url->evaluate();
        } else {
            $lookup_url = $_lookup_url;
        }

        $this->add_text = new Tempcode();

        // Make sure if multiple staff are trying to warn the same member at the same time, they know about it
        list($warning_details, $ping_url) = handle_conflict_resolution(strval($member_id));
        if ($ping_url !== null) {
            $this->add_text->attach(do_template('HANDLE_CONFLICT_RESOLUTION', [
                '_GUID' => 'TODO',
                'PING_URL' => $ping_url,
            ]));
        }
        if ($warning_details !== null) {
            $this->add_text->attach($warning_details);
        }

        // Information about their history, and the rules - to educate the warner/punisher
        if ($new) {
            $hidden->attach(form_input_hidden('member_id', strval($member_id)));
            $this->add_text->attach(do_lang_tempcode('WARNINGS_FORM', escape_html($username), escape_html(integer_format($num_warnings, 0)), [escape_html(get_site_name()), escape_html($rules_url), escape_html($history_url), escape_html($lookup_url)]));
        }

        $fields->attach(do_template('FORM_SCREEN_FIELD_SPACER', [
            '_GUID' => '740461f4702d617aee5ecf46eb7d1af6',
            'TITLE' => do_lang_tempcode('WARNINGS'),
            'HELP' => do_lang_tempcode('HAS_ALREADY_X_WARNINGS', escape_html($username), escape_html(integer_format($num_warnings, 0)), [escape_html(get_site_name()), escape_html($rules_url), escape_html($history_url), escape_html($lookup_url)]),
        ]));

        // Add warning type field
        $warning_radios = new Tempcode();
        $warning_radios->attach(form_input_radio_entry('is_warning', '1', $is_warning === 1, do_lang_tempcode('WARNING_FORMAL'), 0));
        $warning_radios->attach(form_input_radio_entry('is_warning', '0', $is_warning === 0, do_lang_tempcode('WARNING_OFF_THE_BOOK'), 0));
        $fields->attach(form_input_radio(do_lang_tempcode('WARNING_TYPE'), do_lang_tempcode('DESCRIPTION_WARNING_TYPE'), 'is_warning', $warning_radios, true));

        // Add separator for punitive actions
        $fields->attach(do_template('FORM_SCREEN_FIELD_SPACER', ['_GUID' => '322a026b7a56a3e4e9ac58e4979add35', 'TITLE' => do_lang_tempcode('PUNITIVE_ACTIONS')]));

        $hooks = find_all_hook_obs('systems', 'cns_warnings', 'Hook_cns_warnings_');

        // Order our hooks so fields display in the order we want
        $ordered_hooks = [];
        $unordered_hooks = [];
        foreach ($hooks as $hook_ob) {
            if (method_exists($hook_ob, 'get_form_fields')) {
                $info = $hook_ob->get_details();
                if ($info === null) {
                    continue;
                }
                if (isset($info['order'])) {
                    $ordered_hooks[$info['order']] = $hook_ob;
                } else {
                    $unordered_hooks[] = $hook_ob;
                }
            }
        }
        ksort($ordered_hooks, SORT_NUMERIC);
        $ordered_hooks = array_merge($ordered_hooks, $unordered_hooks);

        // Iterate over each ordered hook to render their fields
        foreach ($ordered_hooks as $i => $hook) {
            $hook->get_form_fields($this->add_text, $fields, $hidden, $new, $explanation, $is_warning, $member_id, $spam_mode, $post_id, $ip_address);
        }

        // Explanatory text...

        $keep = symbol_tempcode('KEEP');
        $load_url = find_script('warnings_browse') . '?type=load' . $keep->evaluate();
        $fields->attach(do_template('FORM_SCREEN_FIELD_SPACER', ['_GUID' => 'c7eb70b13be74d8f3bd1f1c5e739d9aa', 'TITLE' => do_lang_tempcode('EXPLANATORY_TEXT'), 'HELP' => do_lang_tempcode('LOAD_SAVED_WARNING', escape_html($load_url))]));
        if (($explanation == '') && ($spam_mode == 1)) {
            $explanation = do_lang('SPAM');
        }

        // Use defined defaults as dropdown options
        $combo_options = new Tempcode();
        $selection_items = do_lang('EXPLANATORY_TEXT_DEFAULT_LIST');
        foreach (explode('|', $selection_items) as $selection_item) {
            $combo_options->attach(form_input_list_entry($selection_item, ($explanation == $selection_item), protect_from_escaping($selection_item)));
        }
        $fields->attach(form_input_combo(do_lang_tempcode('REASON'), do_lang_tempcode('DESCRIPTION_REASON_WARNING'), 'explanation', $explanation, $combo_options));

        if ($new) {
            $message = '';
            if ($post_id !== null) {
                $_postdetails_text = $GLOBALS['FORUM_DB']->query_select_value_if_there('f_posts', 'p_post', ['id' => $post_id]);
                if ($_postdetails_text !== null) {
                    $message = '[quote="' . $username . '"]' . "\n" . get_translated_text($_postdetails_text, $GLOBALS['FORUM_DB']) . "\n" . '[/quote]';
                    $message .= "\n\n" . do_lang('MODERATED_AS_SPAM', get_site_name());
                }
            }
            $fields->attach(form_input_text_comcode(do_lang_tempcode('MESSAGE'), do_lang_tempcode('DESCRIPTION_PP_MESSAGE'), 'message', $message, false));

            $fields->attach(form_input_line(do_lang_tempcode('SAVE_WARNING_DETAILS'), do_lang_tempcode('DESCRIPTION_SAVE_WARNING_DETAILS'), 'save', '', false));

            $fields->attach(form_input_tick(do_lang_tempcode('INCLUDE_PUNITIVE_TEXT'), do_lang_tempcode('DESCRIPTION_INCLUDE_PUNITIVE_TEXT'), 'include_punitive_text', false));

            require_javascript('cns_warnings');
        }

        if (!$new) {
            $this->do_preview = false;
        }

        return [$fields, $hidden];
    }

    /**
     * Standard crud_module table function.
     *
     * @param  array $url_map Details to go to build_url for link to the next screen
     * @return array A quintet: The choose table, Whether re-ordering is supported from this screen, Search URL, Archive URL, a Filtercode box block
     */
    public function create_selection_list_choose_table(array $url_map) : array
    {
        require_code('templates_results_table');

        $current_ordering = get_param_string('sort', 'w_time DESC', INPUT_FILTER_GET_COMPLEX);
        $sortables = [
            'w_time' => do_lang_tempcode('DATE'),
            'w_issuing_member' => do_lang_tempcode('BY'),
        ];
        list($sql_sort, $sort_order, $sortable) = process_sorting_params('warning', $current_ordering);

        $fh = [
            do_lang_tempcode('DATE'),
            do_lang_tempcode('USERNAME'),
            do_lang_tempcode('BY'),
        ];
        $fh[] = do_lang_tempcode('ACTIONS');

        $header_row = results_header_row($fh, $sortables, 'sort', $sortable . ' ' . $sort_order);

        $result_entries = new Tempcode();

        list($rows, $max_rows) = $this->get_entry_rows(false, $sql_sort);
        foreach ($rows as $row) {
            $edit_url = build_url($url_map + ['id' => $row['id'], 'redirect' => protect_url_parameter(SELF_REDIRECT)], '_SELF');

            $username = $GLOBALS['FORUM_DRIVER']->member_profile_hyperlink($row['w_member_id'], '', false);
            $by = $GLOBALS['FORUM_DRIVER']->member_profile_hyperlink($row['w_issuing_member']);

            $map = [
                hyperlink(build_url(['page' => '_SELF', 'type' => 'view', 'id' => $row['id'], 'member_id' => $row['w_member_id']], '_SELF'), get_timezoned_date_time($row['w_time']), false, true, $row['w_explanation']),
                protect_from_escaping($username),
                protect_from_escaping($by),
            ];

            $map[] = protect_from_escaping(hyperlink($edit_url, do_lang_tempcode('EDIT'), false, true, do_lang('EDIT') . ' #' . strval($row['id'])));

            $result_entries->attach(results_entry($map, true));
        }

        $search_url = null;
        $archive_url = null;

        return [results_table(do_lang($this->menu_label), get_param_integer('start', 0), 'start', get_param_integer('max', 20), 'max', $max_rows, $header_row, $result_entries, $sortables, $sortable, $sort_order), false, $search_url, $archive_url];
    }

    /**
     * Standard crud_module list function.
     *
     * @return Tempcode The selection list
     */
    public function create_selection_list_entries() : object
    {
        $_m = $GLOBALS['FORUM_DB']->query_select('f_warnings', ['*'], [], 'ORDER BY w_time DESC');
        $entries = new Tempcode();
        foreach ($_m as $m) {
            $entries->attach(form_input_list_entry(strval($m['id']), false, $GLOBALS['FORUM_DRIVER']->get_username($m['w_member_id']) . ' (' . get_timezoned_date_time($m['w_time']) . ')'));
        }

        return $entries;
    }

    /**
     * Standard crud_module edit form filler.
     *
     * @param  ID_TEXT $id The entry being edited
     * @return mixed Either Tempcode; or a tuple of: (fields, hidden-fields[, delete-fields][, edit-text][, whether all delete fields are specified][, posting form text, more fields][, parsed WYSIWYG editable text])
     */
    public function fill_in_edit_form(string $id)
    {
        $warning = $GLOBALS['FORUM_DB']->query_select('f_warnings', ['w_explanation', 'w_issuing_member', 'w_member_id', 'w_is_warning'], ['id' => intval($id)], '', 1);
        if (!array_key_exists(0, $warning)) {
            warn_exit(do_lang_tempcode('MISSING_RESOURCE'));
        }

        return $this->get_form_fields(false, $warning[0]['w_explanation'], $warning[0]['w_is_warning'], $warning[0]['w_member_id']);
    }

    /**
     * Standard crud_module add actualiser.
     *
     * @return array A pair: The entry added, description about usage
     */
    public function add_actualisation() : array
    {
        require_code('global4');

        $explanation = post_param_string('explanation');
        $member_id = post_param_integer('member_id');
        $message = post_param_string('message', '');
        $username = $GLOBALS['FORUM_DRIVER']->get_username($member_id, false, USERNAME_DEFAULT_ERROR);
        $message_punitive = post_param_integer('include_punitive_text', 0);

        // Save explanatory details if asked to
        $save = post_param_string('save');
        if ($save != '') {
            $GLOBALS['FORUM_DB']->query_delete('f_saved_warnings', ['s_title' => $save], '', 1);
            $GLOBALS['FORUM_DB']->query_insert('f_saved_warnings', [
                's_title' => $save,
                's_explanation' => $explanation,
                's_message' => $message,
            ]);
        }

        // Make the warning now so we can associate its warning ID with logs
        $warning_id = cns_make_warning($member_id, $explanation, null, null, post_param_integer('is_warning', 0));
        set_related_warning_id($warning_id);

        // Grab our warning hooks and order them (so explanatory text is in the defined order)
        $hooks = find_all_hook_obs('systems', 'cns_warnings', 'Hook_cns_warnings_');
        // Order our hooks so fields display in the order we want
        $ordered_hooks = [];
        $unordered_hooks = [];
        foreach ($hooks as $hook_ob) {
            $info = $hook_ob->get_details();
            if ($info === null) {
                continue;
            }
            if (isset($info['order'])) {
                $ordered_hooks[$info['order']] = $hook_ob;
            } else {
                $unordered_hooks[] = $hook_ob;
            }
        }
        ksort($ordered_hooks, SORT_NUMERIC);
        $ordered_hooks = array_merge($ordered_hooks, $unordered_hooks);

        // Execute each punitive action in our hooks, receiving default punitive text pushed on to an array
        $punitive_messages = [];
        foreach ($ordered_hooks as $i => $hook) {
            if (method_exists($hook_ob, 'actualise_punitive_action')) {
                $hook->actualise_punitive_action($punitive_messages, $warning_id, $member_id, $username, $explanation, $message);
            }
        }

        // Add punitive actions text to the bottom of the message if we requested it
        if ((count($punitive_messages) > 0) && ($message_punitive == 1)) {
            $message .= '[title="4" base="2"]' . do_lang('PUNITIVE_HEADER', null, null, null, null, false) . '[/title]';
            $message .= do_lang('PUNITIVE_HEADER_DESCRIPTION', null, null, null, null, false) . "\n";
            foreach ($punitive_messages as $value) {
                $message .= ' - ' . $value . "\n";
            }
        }

        // Send PT if there is a message to send
        if ($message != '') {
            require_code('cns_topics_action');
            require_code('cns_topics_action2');
            require_code('cns_posts_action');
            require_code('cns_posts_action2');

            $_title = do_lang('NEW_WARNING_TO_YOU');

            $pt_topic_id = cns_make_topic(null, '', '', 1, 1, 0, 0, get_member(), $member_id);
            $post_id = cns_make_post($pt_topic_id, $_title, $message, 0, true, 1, 1/*emphasised*/, null, null, null, null, null, null, null, false);

            send_pt_notification($post_id, $_title, $pt_topic_id, $member_id);

            // Update warning row with the new private topic ID
            $GLOBALS['FORUM_DB']->query_update('f_warnings', ['w_topic_id' => $pt_topic_id], ['id' => $warning_id], '', 1);
        }

        // Disable warning id association
        set_related_warning_id(null);

        // Redirect after issuing the warning if applicable
        if ((get_param_string('redirect', '', INPUT_FILTER_URL_INTERNAL) == '')) {
            require_code('site2');
            assign_refresh($GLOBALS['FORUM_DRIVER']->member_profile_url($member_id, true), 0.0); // redirect_screen not used because there is already a legitimate output screen happening
            unset($_GET['redirect']);
        }

        return [strval($warning_id), null];
    }

    /**
     * Standard crud_module edit actualiser.
     *
     * @param  ID_TEXT $id The entry being edited
     * @return ?Tempcode Description about usage (null: none)
     */
    public function edit_actualisation(string $id) : ?object
    {
        $member_id = cns_edit_warning(intval($id), post_param_string('explanation'), post_param_integer('is_warning', 0));

        if (get_param_string('redirect', '', INPUT_FILTER_URL_INTERNAL) == '') {
            require_code('site2');
            assign_refresh($GLOBALS['FORUM_DRIVER']->member_profile_url($member_id, true), 0.0); // redirect_screen not used because there is already a legitimate output screen happening
        }

        return null;
    }

    /**
     * Standard crud_module submitter getter.
     *
     * @param  ID_TEXT $id The entry for which the submitter is sought
     * @return array The submitter, and the time of submission (null submission time implies no known submission time)
     */
    public function get_submitter(string $id) : array
    {
        $rows = $GLOBALS['FORUM_DB']->query_select('f_warnings', ['w_issuing_member', 'w_time'], ['id' => intval($id)], '', 1);
        if (!array_key_exists(0, $rows)) {
            return [null, null];
        }
        return [$rows[0]['w_issuing_member'], $rows[0]['w_time']];
    }

    /**
     * Standard crud_module delete actualiser.
     *
     * @param  ID_TEXT $id The entry being deleted
     */
    public function delete_actualisation(string $id)
    {
        $member_id = cns_delete_warning(intval($id));

        if (get_param_string('redirect', '', INPUT_FILTER_URL_INTERNAL) == '') {
            require_code('site2');
            assign_refresh($GLOBALS['FORUM_DRIVER']->member_profile_url($member_id, true), 0.0); // redirect_screen not used because there is already a legitimate output screen happening
        }
    }

    /**
     * View the warning/punishment history for a member.
     *
     * @return Tempcode The output of the run
     */
    public function history() : object
    {
        require_code('templates_results_table');

        $start = get_param_integer('start', 0);
        $max = get_param_integer('max', 50);

        $member_id = get_param_integer('id', null);

        $f = [do_lang_tempcode('DATE')];

        if ($member_id !== null) {
            $username = $GLOBALS['FORUM_DRIVER']->get_username($member_id);
            if ($member_id != get_member()) {
                log_it('REVIEW_WARNING_HISTORY', strval($member_id), $username);
            }
            $where = ['w_member_id' => $member_id];
        } else {
            $where = [];
            $f[] = do_lang_tempcode('MEMBER');
        }

        $rows = $GLOBALS['FORUM_DB']->query_select('f_warnings', ['*'], $where, 'ORDER BY w_time DESC', $max, $start);
        $max_rows = $GLOBALS['FORUM_DB']->query_select_value('f_warnings', 'COUNT(*)', $where);

        $f = array_merge($f, [do_lang_tempcode('BY'), do_lang_tempcode('IS_FORMAL_WARNING'), do_lang_tempcode('ACTIONS')]);

        if (empty($rows)) {
            inform_exit(do_lang_tempcode('NO_ENTRIES'));
        }

        $out = new Tempcode();
        $header_row = results_header_row($f, []);
        foreach ($rows as $row) {
            $date = hyperlink(build_url(['page' => '_SELF', 'type' => 'view', 'id' => $row['id'], 'member_id' => $member_id, 'redirect' => protect_url_parameter(SELF_REDIRECT)], '_SELF'), get_timezoned_date_time($row['w_time']), false, true, $row['w_explanation']);
            $member = $GLOBALS['FORUM_DRIVER']->member_profile_hyperlink($row['w_member_id']);
            $by = $GLOBALS['FORUM_DRIVER']->member_profile_hyperlink($row['w_issuing_member']);

            $is_warning = $row['w_is_warning'] ? do_lang_tempcode('YES') : do_lang_tempcode('NO');

            $action_edit = hyperlink(build_url(['page' => '_SELF', 'type' => '_edit', 'id' => $row['id'], 'redirect' => protect_url_parameter(SELF_REDIRECT)], '_SELF'), do_lang('EDIT'), false, true, '');
            $actions = new Tempcode();
            $actions->attach($action_edit);

            if ($member_id !== null) {
                $g = [$date, $by, $is_warning, $actions];
            } else {
                $g = [$date, $member, $by, $is_warning, $actions];
            }
            $out->attach(results_entry($g, false));
        }

        if ($member_id !== null) {
            $_username = protect_from_escaping(escape_html($username));
            $add_warning_url = build_url(['page' => '_SELF', 'type' => 'add', 'id' => $member_id, 'redirect' => protect_url_parameter(SELF_REDIRECT)], '_SELF');
            $view_profile_url = $GLOBALS['FORUM_DRIVER']->member_profile_url($member_id, true);
        } else {
            $_username = do_lang_tempcode('ALL_MEMBERS');
            $view_profile_url = null;
            $add_warning_url = null;
        }

        $results_table = results_table(do_lang_tempcode('REVIEW_WARNING_HISTORY'), $start, 'start', $max, 'max', $max_rows, $header_row, $out, [], null, null, null, paragraph(do_lang_tempcode('WARNINGS_HISTORY_TEXT', $_username), '4t4ygyerhrth4'));

        $tpl = do_template('CNS_WARNING_HISTORY_SCREEN', [
            '_GUID' => '4444beed9305f0460a6c00e6c87d4208',
            'TITLE' => $this->title,
            'MEMBER_ID' => strval($member_id),
            'VIEW_PROFILE_URL' => $view_profile_url,
            'ADD_WARNING_URL' => $add_warning_url,
            'RESULTS_TABLE' => $results_table,
        ]);

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

    /**
     * View details about a specific warning.
     *
     * @return Tempcode The output of the run
     */
    public function view() : object
    {
        require_code('cns_topics');
        require_code('templates_tooltip');
        require_code('templates_map_table');
        require_code('templates_columned_table');

        $fields = [];

        $id = get_param_integer('id');

        $rows = $GLOBALS['FORUM_DB']->query_select('f_warnings', ['*'], ['id' => $id], '', 1);
        if (!array_key_exists(0, $rows)) {
            warn_exit(do_lang_tempcode('MISSING_RESOURCE'));
        }

        $row = $rows[0];

        // Basic info
        $fields['MEMBER'] = $GLOBALS['FORUM_DRIVER']->member_profile_hyperlink($row['w_member_id']);
        $fields['BY'] = $GLOBALS['FORUM_DRIVER']->member_profile_hyperlink($row['w_issuing_member']);
        $fields['DATE_TIME'] = get_timezoned_date_time($row['w_time'], false, false, get_member());
        $fields['IS_FORMAL_WARNING'] = ($row['w_is_warning'] == 0) ? do_lang('NO') : do_lang('YES');
        $fields['EXPLANATION'] = $row['w_explanation'];

        // Warning private topic
        if ($row['w_topic_id'] !== null) {
            $topic_rows = $GLOBALS['FORUM_DB']->query_select('f_topics', ['*'], ['id' => $row['w_topic_id']], '', 1);
            if (array_key_exists(0, $topic_rows)) {
                $topic_row = $topic_rows[0];
                if (cns_may_access_topic($row['w_topic_id'])) {
                    $pt_url = build_url(['page' => 'topicview', 'id' => $row['w_topic_id']], get_module_zone('topicview'));
                    $fields['PRIVATE_TOPIC'] = hyperlink($pt_url, do_lang('VIEW_TOPIC'), false, false);
                }
            }
        }

        // Action logs
        if (addon_installed('actionlog')) {
            require_code('actionlog');
            require_lang('actionlog');

            $view_actionlogs = [];
            $_rows = $GLOBALS['SITE_DB']->query_select('actionlogs', ['*'], ['warning_id' => $id], ' ORDER BY date_and_time');
            $_rows2 = $GLOBALS['FORUM_DB']->query_select('f_moderator_logs', ['l_reason AS reason', 'id', 'l_by_member AS member_id', 'l_date_and_time AS date_and_time', 'l_the_type AS the_type', 'l_param_a AS param_a', 'l_param_b AS param_b', 'l_warning_id AS warning_id'], ['l_warning_id' => $id], ' ORDER BY date_and_time');
            $rows = array_merge($_rows, $_rows2);
            sort_maps_by($rows, 'date_and_time');

            foreach ($rows as $_row) {
                if ($_row['the_type'] === 'PRIVATE_TOPIC') {
                    continue;
                }

                $by = $GLOBALS['FORUM_DRIVER']->member_profile_hyperlink($_row['member_id'], '', false);

                $mode = array_key_exists('reason', $_row) ? 'cns' : 'cms';
                $url = build_url(['page' => 'admin_actionlog', 'type' => 'view', 'id' => $_row['id'], 'mode' => $mode], get_module_zone('admin_actionlog'));
                $date = hyperlink($url, get_timezoned_date_time($_row['date_and_time']), false, true, '#' . strval($_row['id']), null, null, null, '_top');

                $linkage = actionlog_linkage($_row, null, null, false);
                $action = do_lang($_row['the_type'], $_row['param_a'], $_row['param_b'], null, null, false);
                $view_actionlogs[] = [$by, $date, ($action !== null) ? $action : $_row['the_type'], ($linkage !== null) ? $linkage[0] : ''];
            }

            if (count($view_actionlogs) == 0) {
                $fields['VIEW_ACTIONLOGS'] = do_lang_tempcode('NA_EM');
            } else {
                $header_row = columned_table_header_row([
                    do_lang('BY'),
                    do_lang('DATE_TIME'),
                    do_lang('ACTION'),
                    do_lang('DETAILS')
                ]);

                $table_rows = new Tempcode();
                foreach ($view_actionlogs as $actionlogs) {
                    $table_rows->attach(columned_table_row($actionlogs, true));
                }

                $fields['VIEW_ACTIONLOGS'] = do_template('COLUMNED_TABLE', ['_GUID' => 'df009e51c7795ce634d76aef9eabe129', 'HEADER_ROW' => $header_row, 'ROWS' => $table_rows, 'NONRESPONSIVE' => false]);
            }
        }

        // Punitive actions
        $rows = $GLOBALS['FORUM_DB']->query_select('f_warnings_punitive', ['*'], ['p_warning_id' => $id]);
        $fields['PUNITIVE_ACTIONS'] = new Tempcode();
        foreach ($rows as $row) {
            $action = new Tempcode();

            $hook = get_hook_ob('systems', 'cns_warnings', $row['p_hook'], 'Hook_cns_warnings_');
            if (method_exists($hook, 'generate_text') && ($hook->get_details() !== null)) {
                $action->attach($hook->generate_text($row));
            }

            // Undo actions
            if ($row['p_reversed'] == 0) {
                if (method_exists($hook, 'undo_punitive_action') && ($hook->get_details() !== null)) {
                    $_undoing_url = build_url(['page' => '_SELF', 'type' => 'undo_punitive_action'], '_SELF');
                    $_undoing_link = hyperlink($_undoing_url, do_lang_tempcode('UNDO'), false, true, '', null, form_input_hidden('id', strval($row['id'])));
                    $action->attach(do_lang_tempcode('ACTION_LINK', protect_from_escaping($_undoing_link)));
                }
            } else {
                $action->attach(do_lang_tempcode('ACTION_LINK', do_lang('UNDONE')));
            }

            $fields['PUNITIVE_ACTIONS']->attach(div($action));
        }

        if ($fields['PUNITIVE_ACTIONS']->is_empty()) {
            $fields['PUNITIVE_ACTIONS'] = do_lang_tempcode('NA_EM');
        }

        $fields['ACTIONS'] = new Tempcode();
        // Edit action
        $fields['ACTIONS']->attach(hyperlink(build_url(['page' => '_SELF', 'type' => '_edit', 'id' => $row['id'], 'redirect' => protect_url_parameter(SELF_REDIRECT)], '_SELF'), do_lang('EDIT'), false, true, ''));

        return map_table_screen($this->title, $fields, true, null, null, true);
    }

    /**
     * Actualiser to undo a punitive action.
     *
     * @return Tempcode Either a redirect screen if successful, or an error if not successful
     */
    public function undo_punitive_action() : object
    {
        $id = post_param_integer('id');

        // Grab our punitive action
        $rows = $GLOBALS['FORUM_DB']->query_select('f_warnings_punitive', ['*'], ['id' => $id]);
        if (!array_key_exists(0, $rows)) {
            warn_exit(do_lang_tempcode('MISSING_RESOURCE'));
        }
        $punitive_action = $rows[0];

        // Do not allow reversing an already-reversed action
        if ($punitive_action['p_reversed'] != 0) {
            require_lang('cns_warnings');
            warn_exit(do_lang_tempcode('PUNITIVE_ACTION_ALREADY_UNDONE'));
        }

        // Get the associated warning
        $rows = $GLOBALS['FORUM_DB']->query_select('f_warnings', ['*'], ['id' => $punitive_action['p_warning_id']]);
        if (!array_key_exists(0, $rows)) {
            warn_exit(do_lang_tempcode('MISSING_RESOURCE'));
        }
        $warning = $rows[0];

        // Confirm the action
        $confirm = get_param_integer('confirm', 0);
        if ($confirm == 0) {
            $undo_action = strval($id);
            $hook = get_hook_ob('systems', 'cns_warnings', $punitive_action['p_hook'], 'Hook_cns_warnings_');
            if (method_exists($hook, 'generate_text') && ($hook->get_details() !== null)) {
                $undo_action = $hook->generate_text($punitive_action);
            }

            $member = $GLOBALS['FORUM_DRIVER']->get_username($warning['w_member_id']);

            $preview = do_lang_tempcode('ARE_YOU_SURE_UNDO_PUNITIVE_ACTION', escape_html($undo_action), escape_html($member));
            return do_template('CONFIRM_SCREEN', [
                '_GUID' => 'a8c48f5437f4258833577b2f785af755',
                'TITLE' => $this->title,
                'PREVIEW' => $preview,
                'URL' => get_self_url(false, false, ['confirm' => 1]),
                'FIELDS' => build_keep_post_fields(),
            ]);
        }

        require_code('global4');

        // Action log entries created from our hooks should be associated with this warning
        set_related_warning_id($warning['id']);

        // Run the actualiser from the appropriate warnings hook
        $hook = get_hook_ob('systems', 'cns_warnings', $punitive_action['p_hook'], 'Hook_cns_warnings_');
        if (method_exists($hook, 'undo_punitive_action') && ($hook->get_details() !== null)) {
            $hook->undo_punitive_action($punitive_action, $warning);
        } else {
            warn_exit(do_lang_tempcode('INTERNAL_ERROR', escape_html('e885739f6b295ac69e4f9a9321a16620')));
        }

        set_related_warning_id(null);

        // Mark punitive action as reversed
        $GLOBALS['FORUM_DB']->query_update('f_warnings_punitive', ['p_reversed' => 1], ['id' => $id]);

        // Show it worked / Refresh
        $url = build_url(['page' => '_SELF', 'type' => 'view', 'id' => $warning['id'], 'member_id' => $warning['w_member_id']], '_SELF');
        return redirect_screen($this->title, $url, do_lang_tempcode('SUCCESS'));
    }
}
