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

/**
 * Module page class.
 */
class Module_topicview
{
    public $title;
    public $breadcrumbs;
    public $id;
    public $forum_id;
    public $type;
    public $topic_info;
    public $posts;
    public $serialized_options;
    public $hash;
    public $may_reply;
    public $start;
    public $default_max;
    public $max;
    public $threaded_topic_ob;

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

        if (get_forum_type() != 'cns') {
            return null;
        }
        if ($be_deferential) {
            return [];
        }
        if ($check_perms && is_guest($member_id)) {
            return [];
        }

        return ['!' => ['INLINE_PERSONAL_POSTS', 'menu/social/forum/inline_personal_posts']];
    }

    /**
     * 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
    {
        $error_msg = new Tempcode();
        if (!addon_installed__messaged('cns_forum', $error_msg)) {
            return $error_msg;
        }

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

        cns_require_all_forum_stuff();

        require_code('cns_topicview');

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

        require_lang('cns');

        $id = get_param_integer('id', null);
        if ((is_guest()) && ($id === null)) {
            access_denied('NOT_AS_GUEST');
        }

        if ($id !== null) {
            $notification_where_map = ['d_notification_code' => 'cns_topic', 'd_code_category' => strval($id), 'd_to_member_id' => get_member(), 'd_read' => 0];
            $notification_ids = $GLOBALS['SITE_DB']->query_select('digestives_tin', ['id'], $notification_where_map);
            if (!empty($notification_ids)) {
                foreach ($notification_ids as $notification_id) {
                    $GLOBALS['SITE_DB']->query_update('digestives_tin', ['d_read' => 1], ['id' => $notification_id['id']]);
                }
                delete_cache_entry('_get_notifications', null, get_member());
            }
        }

        if ($type == 'findpost') {
            $post_id = get_param_integer('id');
            $redirect = find_post_id_url($post_id);
            return redirect_screen(get_screen_title('VIEW_TOPIC'), $redirect);
        } elseif ($type == 'first_unread') {
            $redirect = find_first_unread_url($id);
            return redirect_screen(get_screen_title('VIEW_TOPIC'), $redirect);
        }

        $start = get_param_integer('topic_start', 0);
        $default_max = intval(get_option('forum_posts_per_page'));
        $max = get_param_integer('topic_max', $default_max);
        if ($max == 0) {
            $max = $default_max;
        }
        if ($max == 0) {
            $max = 1;
        }
        if (($max > 30) && (!has_privilege(get_member(), 'remove_page_split'))) {
            $max = $default_max;
        }

        $view_poll_results = post_param_integer('view_poll_results', 0);
        $revoke_vote = post_param_integer('revoke_vote', 0);

        $topic_info = cns_read_in_topic($id, $start, $max, $view_poll_results == 1);

        if ($revoke_vote == 1 && array_key_exists('poll', $topic_info)) {
            require_code('cns_polls_action2');
            cns_revoke_vote_in_poll($topic_info);

            // Success
            attach_message(do_lang_tempcode('SUCCESS'), 'inform');

            // We must re-read topic info with updated poll stuff
            $topic_info = cns_read_in_topic($id, $start, $max, $view_poll_results == 1);
        }

        $may_reply = array_key_exists('may_reply', $topic_info);

        inform_non_canonical_parameter('threaded');
        inform_non_canonical_parameter('post_id');
        inform_non_canonical_parameter('#^kfs.*$#');

        if ($id !== null) {
            set_extra_request_metadata($topic_info['metadata'], $topic_info['row'], 'topic', strval($id));
        }

        global $SEO_TITLE;
        $SEO_TITLE = do_lang('_VIEW_TOPIC', $topic_info['title']);

        if ($id !== null) {
            inject_feed_url('?mode=cns_topicview&select=' . strval($id), do_lang('FORUM_TOPIC'));
        }

        if ($id === null) { // Just inline personal posts
            $title = get_screen_title('INLINE_PERSONAL_POSTS');
        } else {
            if ($topic_info['forum_id'] === null) {
                if (($topic_info['pt_from'] !== null) && ($topic_info['pt_to'] !== null)) {
                    $subtitle = do_lang_tempcode('PRIVATE_TOPIC_WITH', escape_html($GLOBALS['FORUM_DRIVER']->get_username($topic_info['pt_from'])), escape_html($GLOBALS['FORUM_DRIVER']->get_username($topic_info['pt_to'])));
                } else {
                    $subtitle = null;
                }
                $title = get_screen_title(do_lang_tempcode('NAMED_PRIVATE_TOPIC', escape_html($topic_info['title'])), false, [], do_lang_tempcode('READING_PRIVATE_TOPIC'), [], true, $subtitle);
            } else {
                if ((get_value('disable_awards_in_titles') !== '1') && (addon_installed('awards'))) {
                    require_code('awards');
                    $awards = find_awards_for('topic', strval($id));
                } else {
                    $awards = [];
                }

                $title = get_screen_title(do_lang_tempcode('NAMED_TOPIC', make_fractionable_editable('topic', $id, $topic_info['title'])), false, [], null, $awards);
            }
        }

        // Forum breadcrumbs
        if ($topic_info['forum_id'] !== null) {
            $breadcrumbs = cns_forum_breadcrumbs($topic_info['forum_id'], null, null, false);
        } else {
            $breadcrumbs = [];
            $breadcrumbs[] = [build_page_link(['page' => 'members'], get_module_zone('members')), do_lang_tempcode('MEMBERS')];
            if (has_privilege(get_member(), 'view_other_pt')) {
                $of_member = ($topic_info['pt_from'] == get_member()) ? $topic_info['pt_from'] : $topic_info['pt_to'];
            } else {
                $of_member = get_member();
            }
            if ($of_member !== null) {
                $of_username = $GLOBALS['FORUM_DRIVER']->get_username($of_member, true);
            } else {
                $of_username = do_lang('INTERNAL_ERROR');
            }
            $private_topic_page_link = build_page_link(['page' => 'members', 'type' => 'view', 'id' => $of_member], get_module_zone('members'), [], 'tab--pts');
            $breadcrumbs[] = [$private_topic_page_link, do_lang_tempcode('MEMBER_ACCOUNT', escape_html($of_username))];
        }

        if ($id !== null) {
            $breadcrumbs[] = ['', do_lang_tempcode(($topic_info['forum_id'] === null) ? 'VIEW_PRIVATE_TOPIC' : 'VIEW_TOPIC')];
            breadcrumb_set_parents($breadcrumbs);
        }

        if ($id === null) { // Just inline personal posts
            $root_forum_name = $GLOBALS['FORUM_DB']->query_select_value('f_forums', 'f_name', ['id' => db_get_first_id()]);
            $breadcrumbs = [];
            $breadcrumbs[] = [build_page_link(['page' => 'forumview', 'id' => db_get_first_id()], get_module_zone('forumview')), $root_forum_name];
            $breadcrumbs[] = ['', do_lang_tempcode('INLINE_PERSONAL_POSTS')];
            breadcrumb_set_parents($breadcrumbs);
        }

        $threaded = ($topic_info['is_threaded'] == 1);
        if (!$threaded) {
            $jump_post_id = get_param_integer('post_id', null);

            set_extra_request_metadata([
                'description' => $topic_info['description'],
            ]);
            foreach ($topic_info['posts'] as $array_id => $_postdetails) {
                if (((!isset($GLOBALS['METADATA']['description'])) || ($GLOBALS['METADATA']['description'] == '')) && (($_postdetails['id'] === $jump_post_id) || (($array_id == 0) && ($jump_post_id === null)))) {
                    // NB: A side-effect of this is that the Tempcode is evaluated, causing the 'image' metadata for an attachment (in MEDIA_WEBSAFE.tpl) to fill. We want this.
                    $truncated = generate_truncation($_postdetails['post'], 'left', 200, false, true, true, 0.2); // FUDGE: Ideally wouldn't hard-code '200', but adding an option would be bloat.
                    set_extra_request_metadata([
                        'description' => strip_html($truncated),
                    ]);
                }
            }
        } else {
            require_code('topics');
            $threaded_topic_ob = new CMS_Topic();

            // Load some settings into the renderer
            $threaded_topic_ob->first_post_id = $topic_info['first_post_id'];
            $threaded_topic_ob->topic_description = $topic_info['description'];
            $threaded_topic_ob->topic_description_link = $topic_info['description_link'];
            $threaded_topic_ob->topic_title = $topic_info['title'];
            $threaded_topic_ob->topic_info = $topic_info;

            // Other settings we need
            $max_thread_depth = intval(get_option('max_thread_depth'));
            $num_to_show_limit = get_param_integer('max_comments', intval(get_option('comments_to_show_in_thread')));

            // Load posts
            $threaded_topic_ob->load_from_topic($id, $num_to_show_limit, $start, false, null, true);
            $threaded_topic_ob->is_threaded = true;

            // Render posts
            list($posts, $serialized_options, $hash) = $threaded_topic_ob->render_posts($num_to_show_limit, $max_thread_depth, $may_reply, $topic_info['first_poster'], [], $topic_info['forum_id'], $topic_info['row'], null, false);

            set_extra_request_metadata([
                'description' => $threaded_topic_ob->topic_description,
            ]);

            $this->posts = $posts;
            $this->serialized_options = $serialized_options;
            $this->hash = $hash;
            $this->threaded_topic_ob = $threaded_topic_ob;
        }

        $this->title = $title;
        $this->breadcrumbs = $breadcrumbs;
        $this->type = $type;
        $this->id = $id;
        $this->topic_info = $topic_info;
        $this->forum_id = $topic_info['forum_id'];
        $this->may_reply = $may_reply;
        $this->start = $start;
        $this->default_max = $default_max;
        $this->max = $max;

        return null;
    }

    /**
     * Execute the module.
     *
     * @return Tempcode The result of execution
     */
    public function run() : object
    {
        if (get_param_integer('clear_autosave', 0) == 1) {
            require_code('autosave');
            clear_cms_autosave();
        }

        $title = $this->title;
        $breadcrumbs = $this->breadcrumbs;
        $type = $this->type;
        $id = $this->id;
        $topic_info = $this->topic_info;
        $may_reply = $this->may_reply;
        $start = $this->start;
        $default_max = $this->default_max;
        $max = $this->max;

        require_css('cns');

        $first_unread_id = -1;

        require_code('users');

        // Mark as read
        if (($id !== null) && ($GLOBALS['FORUM_DRIVER']->get_member_row_field(get_member(), 'm_auto_mark_read') == 1)) {
            if ($topic_info['forum_id'] === null) {
                $this->_update_read_status(); // Done early because we need to have updated read status set when the pt_notifications show up
            } else {
                cms_register_shutdown_function_safe([$this, '_update_read_status']); // done at end after output in case of locking (don't make the user wait)
            }
        }

        // Render posts according to whether threaded or not
        $threaded = ($topic_info['is_threaded'] == 1);
        if (!$threaded) {
            $jump_post_id = get_param_integer('post_id', null);
            $jump_post_found = false;

            // Render non-threaded
            $posts = new Tempcode();
            $replied = false;
            if ($topic_info['forum_id'] === null) {
                // Has now read
                decache_private_topics(get_member());
            }
            $second_poster = $topic_info['first_poster'];
            $poster_details_cache = [];
            foreach ($topic_info['posts'] as $array_id => $_postdetails) {
                if ($_postdetails['id'] === $jump_post_id) {
                    $jump_post_found = true;
                }

                if ($array_id == 0) {
                    $description = $topic_info['description'];
                } else {
                    $description = null;
                }

                if ($_postdetails['poster'] == get_member()) {
                    $replied = true;
                }

                if (($array_id == 1 && $start == 0) || ($array_id == 0 && $start != 0)) {
                    $second_poster = $_postdetails['poster'];
                }

                if (array_key_exists('last_edit_time', $_postdetails)) {
                    $last_edited = do_template('CNS_TOPIC_POST_LAST_EDITED', [
                        '_GUID' => '77a28e8bc3cf2ec2211aafdb5ba192bf',
                        'LAST_EDIT_DATE_RAW' => ($_postdetails['last_edit_time'] === null) ? '' : strval($_postdetails['last_edit_time']),
                        'LAST_EDIT_DATE' => $_postdetails['last_edit_date'],
                        'LAST_EDIT_PROFILE_URL' => $GLOBALS['FORUM_DRIVER']->member_profile_url($_postdetails['last_edit_member'], true),
                        'LAST_EDIT_USERNAME' => $_postdetails['last_edit_username'],
                    ]);
                } else {
                    $last_edited = new Tempcode();
                }
                $last_edited_raw = (array_key_exists('last_edit_time', $_postdetails)) ? (($_postdetails['last_edit_time'] === null) ? '' : strval($_postdetails['last_edit_time'])) : '0';

                $is_spacer_post = $_postdetails['is_spacer_post'];

                // Post buttons
                $buttons = new Tempcode();
                if (!$is_spacer_post) {
                    $buttons = cns_render_post_buttons($topic_info, $_postdetails, $may_reply);
                }

                if (isset($poster_details_cache[$_postdetails['poster']][$_postdetails['poster_username']])) { // The cache avoids a lot of potentially duplicated Tempcode structure
                    list($poster_online, $post_avatar, $rank_images, $poster_details, $poster, $signature) = $poster_details_cache[$_postdetails['poster']][$_postdetails['poster_username']];
                } else {
                    // User online status
                    $poster_online = null;
                    if ((get_option('is_on_show_online') == '1') && (!is_guest($_postdetails['poster']))) {
                        require_code('users2');
                        $poster_online = $_postdetails['online'];
                    }

                    // Avatar
                    if ((array_key_exists('avatar', $_postdetails)) && ($_postdetails['avatar'] != '')) {
                        $post_avatar = do_template('CNS_TOPIC_POST_AVATAR', ['_GUID' => 'd647ada9c11d56eedc0ff7894d33e83c', 'AVATAR' => $_postdetails['avatar']]);
                    } else {
                        $post_avatar = new Tempcode();
                    }

                    // Rank images
                    $rank_images = new Tempcode();
                    if (!$is_spacer_post) {
                        $posters_groups = $GLOBALS['FORUM_DRIVER']->get_members_groups($_postdetails['poster'], true);
                        foreach ($posters_groups as $group) {
                            $rank_image = cns_get_group_property($group, 'rank_image');
                            $group_leader = cns_get_group_property($group, 'group_lead_member');
                            $group_name = cns_get_group_name($group);
                            $rank_image_pri_only = cns_get_group_property($group, 'rank_image_pri_only');
                            if (($rank_image != '') && (($rank_image_pri_only == 0) || ($group == $GLOBALS['FORUM_DRIVER']->get_member_row_field($_postdetails['poster'], 'm_primary_group')))) {
                                $rank_images->attach(do_template('CNS_RANK_IMAGE', [
                                    '_GUID' => '0ff7855482b901be95591964d4212c44',
                                    'GROUP_NAME' => $group_name,
                                    'USERNAME' => $GLOBALS['FORUM_DRIVER']->get_username($_postdetails['poster']),
                                    'IMG' => $rank_image,
                                    'IS_LEADER' => $group_leader == $_postdetails['poster'],
                                ]));
                            }
                        }
                    }

                    // Poster details
                    if (!$is_spacer_post) {
                        if (!is_guest($_postdetails['poster'])) {
                            require_code('cns_members2');
                            $poster_details = render_member_box($_postdetails['poster'], false, false, [], false);
                        } else {
                            $custom_fields = new Tempcode();
                            if (array_key_exists('ip_address', $_postdetails)) {
                                $custom_fields->attach(do_template('CNS_MEMBER_BOX_CUSTOM_FIELD', [
                                    '_GUID' => 'd85be094dff0d039a64120d6f8f381bb',
                                    'NAME' => do_lang_tempcode('IP_ADDRESS'),
                                    'RAW' => $_postdetails['ip_address'],
                                    'VALUE' => $_postdetails['ip_address'],
                                    'MEMBER_ID' => strval($_postdetails['poster']),
                                ]));
                                $poster_details = do_template('CNS_GUEST_DETAILS', ['_GUID' => 'e43534acaf598008602e8da8f9725f38', 'CUSTOM_FIELDS' => $custom_fields]);
                            } else {
                                $poster_details = new Tempcode();
                            }
                        }
                    } else {
                        $poster_details = new Tempcode();
                    }

                    if (!is_guest($_postdetails['poster'])) {
                        require_code('users2');
                        $poster = do_template('CNS_POSTER_MEMBER', [
                            '_GUID' => 'dbbed1850b6c01a6c9601d85c6aee43f',
                            'ONLINE' => $_postdetails['online'],
                            'ID' => strval($_postdetails['poster']),
                            'POSTER_DETAILS' => $poster_details,
                            'PROFILE_URL' => $GLOBALS['FORUM_DRIVER']->member_profile_url($_postdetails['poster'], true),
                            'POSTER_USERNAME' => $_postdetails['poster_username'],
                            'HIGHLIGHT_NAME' => array_key_exists('highlighted_name', $_postdetails) ? ($_postdetails['highlighted_name'] ? '1' : '0') : null,
                        ]);
                    } else {
                        if ((addon_installed('securitylogging')) && (array_key_exists('ip_address', $_postdetails)) && (has_actual_page_access(get_member(), 'admin_lookup'))) {
                            $ip_url = build_url(['page' => 'admin_lookup', 'type' => 'results', 'param' => $_postdetails['ip_address']], get_module_zone('admin_lookup'));
                        } else {
                            $ip_url = new Tempcode();
                        }
                        $poster = do_template('CNS_POSTER_GUEST', [
                            '_GUID' => '36a8e550222cdac5165ef8f722be3def',
                            'LOOKUP_IP_URL' => $ip_url,
                            'POSTER_DETAILS' => $poster_details,
                            'POSTER_USERNAME' => $_postdetails['poster_username'],
                        ]);
                    }

                    // Signature
                    $signature = new Tempcode();
                    if ((array_key_exists('signature', $_postdetails)) && (!$_postdetails['signature']->is_empty())) {
                        $signature = $_postdetails['signature'];
                    }

                    $poster_details_cache[$_postdetails['poster']][$_postdetails['poster_username']] = [$poster_online, $post_avatar, $rank_images, $poster_details, $poster, $signature];
                }

                $post_title = $_postdetails['title'];

                $first_unread = (($_postdetails['id'] == $first_unread_id) || (($first_unread_id < 0) && ($array_id == count($topic_info['posts']) - 1))) ? do_template('CNS_TOPIC_FIRST_UNREAD') : new Tempcode();

                $validation = (($_postdetails['validated'] == 0) && (addon_installed('validation'))) ? do_lang_tempcode('NOT_VALIDATED') : new Tempcode();

                $post_url = $GLOBALS['FORUM_DRIVER']->post_url($_postdetails['id'], ($topic_info['forum_id'] === null) ? '' : strval($topic_info['forum_id']), true);

                if ((array_key_exists('whisper_to_member', $_postdetails)) && ($_postdetails['whisper_to_member'] == get_member())) {
                    // Has now read
                    decache_private_topics(get_member());
                }

                list($post_class, $emphasis) = cns_get_post_emphasis($_postdetails, $topic_info);

                require_code('feedback');
                if (!array_key_exists('whisper_to_member', $_postdetails) && get_value('disable_post_rating', '0', true) !== '1') {
                    actualise_rating(true, 'post', strval($_postdetails['id']), get_self_url(), $_postdetails['title']);
                    $rating = display_rating(get_self_url(), $_postdetails['title'], 'post', strval($_postdetails['id']), 'RATING_INLINE_DYNAMIC', $_postdetails['poster']);
                } else {
                    $rating = new Tempcode();
                }

                if ((isset($GLOBALS['METADATA']['description'])) && ($GLOBALS['METADATA']['description'] == '') && (($_postdetails['id'] === $jump_post_id) || (($array_id == 0) && ($jump_post_id === null)))) {
                    set_extra_request_metadata([
                        'description' => strip_html(generate_truncation($_postdetails['post'], 'left', 200, false, true, true, 0.2)),
                    ]);
                }

                $rendered_post = do_template('CNS_TOPIC_POST', [
                    '_GUID' => 'sacd09wekfofpw2f',
                    'GIVE_CONTEXT' => false,
                    'ID' => $is_spacer_post ? '' : strval($_postdetails['id']),
                    'TOPIC_FIRST_POST_ID' => ($topic_info['first_post_id'] === null) ? '' : strval($topic_info['first_post_id']),
                    'TOPIC_FIRST_POSTER' => ($topic_info['first_poster'] === null) ? '' : strval($topic_info['first_poster']),
                    'POST_ID' => $is_spacer_post ? '' : ((get_option('seq_post_ids') == '1') ? strval($start + $array_id + 1) : strval($_postdetails['id'])),
                    'URL' => $post_url,
                    'CLASS' => $post_class,
                    'EMPHASIS' => $emphasis,
                    'FIRST_UNREAD' => $first_unread,
                    'POSTER_TITLE' => $is_spacer_post ? '' : $_postdetails['poster_title'],
                    'POST_TITLE' => $post_title,
                    'POST_DATE_RAW' => strval($_postdetails['time']),
                    'POST_DATE' => $_postdetails['date'],
                    'POST' => $_postdetails['post'],
                    'TOPIC_ID' => ($id === null) ? '' : strval($id),
                    'LAST_EDITED_RAW' => $last_edited_raw,
                    'LAST_EDITED' => $last_edited,
                    'POSTER_ID' => strval($_postdetails['poster']),
                    'POSTER' => $is_spacer_post ? '' : $poster,
                    'POSTER_DETAILS' => $poster_details,
                    'POST_AVATAR' => $post_avatar,
                    'RANK_IMAGES' => $rank_images,
                    'BUTTONS' => $buttons,
                    'SIGNATURE' => $signature,
                    'NOT_VALIDATED' => $validation,
                    'DESCRIPTION' => $description,
                    'RATING' => $rating,
                    'POSTER_ONLINE' => $poster_online,
                    'FORUM_ID' => ($topic_info['forum_id'] === null) ? '' : strval($topic_info['forum_id']),
                ]);
                $posts->attach($rendered_post);
            }

            $serialized_options = null;
            $hash = null;

            if (($jump_post_id !== null) && (!$jump_post_found)) {
                attach_message(do_lang_tempcode('_MISSING_RESOURCE', strval($jump_post_id), 'post'), 'warn');
            }
        } else { // Threaded
            $posts = $this->posts;
            $serialized_options = $this->serialized_options;
            $hash = $this->hash;
            $threaded_topic_ob = $this->threaded_topic_ob;

            // Get other gathered details
            $replied = $threaded_topic_ob->replied;
            if ($threaded_topic_ob->topic_title !== null) { // Updated topic title
                $topic_info['title'] = $threaded_topic_ob->topic_title;
            }
            $topic_info['max_rows'] = $threaded_topic_ob->total_posts;

            $second_poster = $GLOBALS['FORUM_DRIVER']->get_guest_id(); // No definitive post orders
        }

        // Buttons for topic as whole
        $button_array = [];
        if ($id !== null) {
            if (get_option('threaded_buttons') == '1') {
                if ($threaded) {
                    $view_as_linear_url = get_self_url(false, false, ['threaded' => 0]);
                    $button_array[] = ['immediate' => true, 'title' => do_lang_tempcode('_VIEW_AS_LINEAR'), 'url' => $view_as_linear_url, 'img' => 'buttons/linear'];
                } else {
                    $view_as_threaded_url = get_self_url(false, false, ['threaded' => 1]);
                    $button_array[] = ['immediate' => true, 'title' => do_lang_tempcode('_VIEW_AS_THREADED'), 'url' => $view_as_threaded_url, 'img' => 'buttons/threaded'];
                }
            }

            if (!is_guest()) {
                $too_old = $topic_info['last_time'] < time() - 60 * 60 * 24 * intval(get_option('post_read_history_days'));

                require_code('users');

                if (($GLOBALS['FORUM_DRIVER']->get_member_row_field(get_member(), 'm_auto_mark_read') != 1) && (get_option('enable_mark_topic_unread') === '1') && !cns_has_read_topic($id)) {
                    $redirect_map = ['page' => 'topicview', 'id' => $id];
                    $test_threaded = get_param_integer('threaded', null);
                    if ($test_threaded !== null) {
                        $redirect_map['threaded'] = $test_threaded;
                    }
                    $redirect = build_url($redirect_map, get_module_zone('topicview'));
                    $map = ['page' => 'topics', 'type' => 'mark_read_topic', 'id' => $id, 'redirect' => protect_url_parameter($redirect->evaluate())];
                    if ($test_threaded !== null) {
                        $map['threaded'] = $test_threaded;
                    }
                    $mark_read_url = build_url($map, get_module_zone('topics'));
                    $button_array[] = ['immediate' => true, 'title' => do_lang_tempcode('MARK_READ'), 'url' => $mark_read_url, 'img' => 'buttons/mark_read'];
                } else {
                    if ((get_option('enable_mark_topic_unread') === '1') && !$too_old) {
                        $map = ['page' => 'topics', 'type' => 'mark_unread_topic', 'id' => $id];
                        $test = get_param_string('kfs' . (($topic_info['forum_id'] === null) ? '' : strval($topic_info['forum_id'])), null, INPUT_FILTER_GET_COMPLEX);
                        if (($test !== null) && ($test !== '0')) {
                            $map['kfs' . (($topic_info['forum_id'] === null) ? '' : strval($topic_info['forum_id']))] = $test;
                        }
                        $test_threaded = get_param_integer('threaded', null);
                        if ($test_threaded !== null) {
                            $map['threaded'] = $test_threaded;
                        }
                        $mark_unread_url = build_url($map, get_module_zone('topics'));
                        $button_array[] = ['immediate' => true, 'title' => do_lang_tempcode('MARK_UNREAD'), 'url' => $mark_unread_url, 'img' => 'buttons/mark_unread'];
                    }
                }
            }

            if (($may_reply) && (get_bot_type() === null)) {
                $reply_prevented = false;

                // "Staff-only" reply for support tickets
                if (($GLOBALS['FORUM_DRIVER']->is_staff(get_member())) && (addon_installed('tickets'))) {
                    require_code('tickets');
                    if (is_ticket_forum($topic_info['forum_id'])) {
                        if (is_guest($second_poster)) {
                            $reply_prevented = true;
                        }

                        require_lang('tickets');
                        require_css('tickets');
                        $map = ['page' => 'topics', 'type' => 'new_post', 'id' => $id, 'whisper_to_member' => $GLOBALS['FORUM_DRIVER']->get_guest_id()];
                        $test = get_param_string('kfs' . (($topic_info['forum_id'] === null) ? '' : strval($topic_info['forum_id'])), null, INPUT_FILTER_GET_COMPLEX);
                        if (($test !== null) && ($test !== '0')) {
                            $map['kfs' . (($topic_info['forum_id'] === null) ? '' : strval($topic_info['forum_id']))] = $test;
                        }
                        $test_threaded = get_param_integer('threaded', null);
                        if ($test_threaded !== null) {
                            $map['threaded'] = $test_threaded;
                        }
                        $new_post_url = build_url($map, get_module_zone('topics'));
                        $button_array[] = ['immediate' => false, 'rel' => 'add', 'title' => do_lang_tempcode('TICKET_STAFF_ONLY_REPLY'), 'url' => $new_post_url, 'img' => 'buttons/new_reply_staff_only'];
                    }
                }

                if (!$reply_prevented) {
                    if ($topic_info['is_threaded'] == 0) { // For threaded ones (i.e. not this) we want to encourage people to click the reply button by a post
                        $map = ['page' => 'topics', 'type' => 'new_post', 'id' => $id];
                        $test = get_param_string('kfs' . (($topic_info['forum_id'] === null) ? '' : strval($topic_info['forum_id'])), null, INPUT_FILTER_GET_COMPLEX);
                        if (($test !== null) && ($test !== '0')) {
                            $map['kfs' . (($topic_info['forum_id'] === null) ? '' : strval($topic_info['forum_id']))] = $test;
                        }
                        $test_threaded = get_param_integer('threaded', null);
                        if ($test_threaded !== null) {
                            $map['threaded'] = $test_threaded;
                        }
                        $new_post_url = build_url($map, get_module_zone('topics'));
                        $button_array[] = ['immediate' => false, 'rel' => 'add', 'title' => do_lang_tempcode($topic_info['is_open'] ? '_REPLY' : 'CLOSED'), 'url' => $new_post_url, 'img' => $topic_info['is_open'] ? 'buttons/new_reply' : 'buttons/closed'];
                    }
                } else {
                    unset($topic_info['may_use_quick_reply']);
                }
            } elseif ((($topic_info['forum_id'] === null) || (has_privilege(get_member(), 'submit_lowrange_content', 'topics', ['forums', $topic_info['forum_id']]))) && ($topic_info['last_poster'] == get_member()) && (!is_guest()) && (cns_may_edit_post_by($topic_info['last_post_id'], $topic_info['last_time'], get_member(), $topic_info['forum_id'], null, $topic_info['is_open'] == 0))) {
                // Maybe we can let them edit their last post instead?
                $map = ['page' => 'topics', 'type' => 'edit_post', 'id' => $topic_info['last_post_id']];
                $test = get_param_string('kfs' . strval($topic_info['forum_id']), null, INPUT_FILTER_GET_COMPLEX);
                if (($test !== null) && ($test !== '0')) {
                    $map['kfs' . strval($topic_info['forum_id'])] = $test;
                }
                $test_threaded = get_param_integer('threaded', null);
                if ($test_threaded !== null) {
                    $map['threaded'] = $test_threaded;
                }
                $new_post_url = build_url($map, get_module_zone('topics'));
                $button_array[] = ['immediate' => false, 'rel' => 'edit', 'title' => do_lang_tempcode('LAST_POST'), 'url' => $new_post_url, 'img' => 'admin/edit'];
            }

            if ($topic_info['forum_id'] !== null) {
                if (get_option('enable_add_topic_btn_in_topic') == '1') {
                    if (cns_may_post_topic($topic_info['forum_id'], get_member())) {
                        $new_topic_url = build_url(['page' => 'topics', 'type' => 'new_topic', 'id' => $topic_info['forum_id']], get_module_zone('topics'));
                        $button_array[] = ['immediate' => false, 'rel' => 'add', 'title' => do_lang_tempcode('ADD_TOPIC'), 'url' => $new_topic_url, 'img' => 'buttons/add_topic'];
                    }
                }
            } else {
                $invite_url = build_url(['page' => 'topics', 'type' => 'invite_member', 'id' => $id], get_module_zone('topics'));
                $button_array[] = ['immediate' => false, 'title' => do_lang_tempcode('_INVITE_MEMBER_TO_PT'), 'url' => $invite_url, 'img' => 'admin/add_to_category'];
            }
        }
        $buttons = cns_button_screen_wrap($button_array);

        // Poll
        if ((array_key_exists('poll', $topic_info))) {
            require_lang('cns_polls');
            require_code('cns_polls_action2');

            $_poll = $topic_info['poll'];
            $poll_is_open = cns_is_poll_open($_poll);
            $voted_already = $_poll['voted_already'];
            $is_private = $_poll['is_private'] == 1;
            $poll_results = (array_key_exists(0, $_poll['answers'])) && (array_key_exists('num_votes', $_poll['answers'][0]));
            $answers = new Tempcode();
            $show_buttons = false;
            $disable_answers = false;
            $poll_buttons = new Tempcode();
            $footer_message = new Tempcode();
            $revoke_url = new Tempcode();
            $can_view_results = false;
            $vote_will_forfeight = false;

            // Controls the visibility of the "View Results" button (clicking to go from the voting interface to the results, without actually voting)
            if (
                (!is_guest()) || // Members can always view results as the system can reliably forfeit their vote against the member ID
                (has_privilege(get_member(), 'view_poll_results_before_voting')) || // Privilege to view results regardless of current vote status
                (is_guest()) && ($_poll['guests_can_vote'] == 1) && ($_poll['vote_revocation'] == 1) // Guests can only view results if they are allowed to vote (i.e. we're not trying to encourage them to log in to participate) and if revocation is enabled (i.e. we do not mind users changing their mind, which is necessarily the case for guest voting as it is impossible to enforce a forfeit)
            ) {
                $can_view_results = true;
            }

            // For controlling the warning overlay when someone clicks "View Results"
            if (!$voted_already && $_poll['vote_revocation'] != 0 && !has_privilege(get_member(), 'view_poll_results_before_voting')) {
                $vote_will_forfeight = true;
            }

            // Work out vote URL
            $map = ['page' => 'topics', 'type' => 'vote_poll', 'id' => $id, 'topic_start' => ($start == 0) ? null : $start, 'topic_max' => ($max == $default_max) ? null : $max];
            $test = get_param_string('kfs' . (($topic_info['forum_id'] === null) ? '' : strval($topic_info['forum_id'])), null, INPUT_FILTER_GET_COMPLEX);
            if (($test !== null) && ($test !== '0')) {
                $map['kfs' . (($topic_info['forum_id'] === null) ? '' : strval($topic_info['forum_id']))] = $test;
            }
            $test_threaded = get_param_integer('threaded', null);
            if ($test_threaded !== null) {
                $map['threaded'] = $test_threaded;
            }
            $vote_url = build_url($map, get_module_zone('topics'));

            // Work out buttons / error messages
            if ($poll_is_open) {
                if (($poll_results || $voted_already !== null)) {
                    $disable_answers = true;
                    if ($voted_already) {
                        $footer_message = do_lang_tempcode('ALREADY_VOTED');
                        if ($_poll['vote_revocation'] == 1 && !is_guest()) {
                            $map = ['page' => 'topicview', 'id' => $id, 'topic_start' => ($start == 0) ? null : $start, 'topic_max' => ($max == $default_max) ? null : $max];
                            $test = get_param_string('kfs' . (($topic_info['forum_id'] === null) ? '' : strval($topic_info['forum_id'])), null, INPUT_FILTER_GET_COMPLEX);
                            if (($test !== null) && ($test !== '0')) {
                                $map['kfs' . (($topic_info['forum_id'] === null) ? '' : strval($topic_info['forum_id']))] = $test;
                            }
                            $test_threaded = get_param_integer('threaded', null);
                            if ($test_threaded !== null) {
                                $map['threaded'] = $test_threaded;
                            }
                            $revoke_url = build_url($map, get_module_zone('topics'));
                        }
                        if (($_poll['view_member_votes'] == 1) || ($_poll['vote_revocation'] == 1 && !is_guest())) {
                            $show_buttons = true;
                            $footer_message = new Tempcode();
                            $map = ['GUID' => '00662ec1c9d84b96a5e1d8e197ef6de4'];
                            if (($_poll['view_member_votes'] == 1) && ($_poll['is_private'] != 1)) {
                                $all_votes_url = build_url(['page' => 'topics', 'type' => 'view_poll_voters', 'id' => $_poll['id']], '_SELF');
                                $map['ALL_VOTES_URL'] = $all_votes_url;
                            }
                            if ($_poll['vote_revocation'] == 1 && !is_guest()) {
                                $map['REVOKE_URL'] = $revoke_url;
                            }
                            $poll_buttons = do_template('CNS_TOPIC_POLL_BUTTON_RESULTS', $map);
                        }
                    }
                } elseif (($_poll['requires_reply']) && (!$replied)) {
                    $disable_answers = true;
                    $footer_message = do_lang_tempcode('POLL_REQUIRES_REPLY');
                } else {
                    if (!has_privilege(get_member(), 'vote_in_polls')) {
                        $disable_answers = true;
                        $footer_message = do_lang_tempcode(is_guest() ? 'GUESTS_CANT_VOTE_IN_POLLS' : 'VOTE_DENIED');
                    } else {
                        require_lang('cns_polls');
                        $map = ['page' => 'topicview', 'id' => $id, 'topic_start' => ($start == 0) ? null : $start, 'topic_max' => ($max == $default_max) ? null : $max];
                        $test = get_param_string('kfs' . (($topic_info['forum_id'] === null) ? '' : strval($topic_info['forum_id'])), null, INPUT_FILTER_GET_COMPLEX);
                        if (($test !== null) && ($test !== '0')) {
                            $map['kfs' . (($topic_info['forum_id'] === null) ? '' : strval($topic_info['forum_id']))] = $test;
                        }
                        $test_threaded = get_param_integer('threaded', null);
                        if ($test_threaded !== null) {
                            $map['threaded'] = $test_threaded;
                        }
                        $results_url = build_url($map, get_module_zone('topics'));
                        $poll_buttons = do_template('CNS_TOPIC_POLL_BUTTON', [
                            '_GUID' => '94b932fd01028df8f67bb5864d9235f9',
                            'RESULTS_URL' => $results_url,
                            'VOTE_URL' => $vote_url,
                            'CAN_VIEW_RESULTS' => $can_view_results,
                            'VOTE_WILL_FORFEIGHT' => $vote_will_forfeight
                        ]);
                        $show_buttons = true;
                        $footer_message = new Tempcode();
                    }
                }
            } else {
                $footer_message = do_lang_tempcode('TOPIC_POLL_CLOSED');
                $disable_answers = true;
            }

            // Work out results / voting UI
            $point_weighting = (get_option('enable_poll_point_weighting') == '1') && ($_poll['point_weighting'] == 1);
            $total_votes = $_poll['total_votes'];
            $total_voting_power = $_poll['total_voting_power'];
            foreach ($_poll['answers'] as $answer) {
                if (($poll_results) && (($_poll['requires_reply'] == 0) || ($replied))) {
                    $num_votes = $answer['num_votes'];
                    $voting_power = $answer['voting_power'];
                    if ($total_votes != 0) {
                        if ($point_weighting) {
                            $width = intval(round(100.0 * floatval($voting_power) / floatval($total_voting_power)));
                        } else {
                            $width = intval(round(100.0 * floatval($num_votes) / floatval($total_votes)));
                        }
                    } else {
                        $width = 0;
                    }
                    $answer_tpl = do_template('CNS_TOPIC_POLL_ANSWER_RESULTS', [
                        '_GUID' => 'b32f4c526e147abf20ca0d668e40d515',
                        'ID' => strval($_poll['id']),
                        '_NUM_VOTES' => strval($num_votes),
                        '_TOTAL_VOTES' => strval($total_votes),
                        'NUM_VOTES' => integer_format($num_votes),
                        'VOTING_POWER' => ($voting_power !== null) ? float_format($voting_power, 2) : null,
                        'TOTAL_VOTES' => integer_format($total_votes),
                        'TOTAL_VOTING_POWER' => ($total_voting_power !== null) ? float_format($total_voting_power, 2) : null,
                        'POINT_WEIGHTING' => $point_weighting,
                        'WIDTH' => strval($width),
                        'ANSWER' => $answer['answer'],
                        'I' => strval($answer['id']),
                        'VOTERS_URL' => $_poll['view_member_votes'] ? build_url(['page' => 'topics', 'type' => 'view_poll_voters', 'id' => $_poll['id'], 'answer_id' => $answer['id']], '_SELF') : null
                    ]);
                } else {
                    $answer_tpl = do_template('CNS_TOPIC_POLL_ANSWER' . (($_poll['maximum_selections'] == 1) ? '_RADIO' : '_TICK'), ['DISABLE_ANSWERS' => $disable_answers, 'ID' => strval($_poll['id']), 'ANSWER' => $answer['answer'], 'I' => strval($answer['id'])]);
                }
                $answers->attach($answer_tpl);
            }

            // Main poll template
            $poll = do_template('CNS_TOPIC_POLL' . ($poll_results ? '_VIEW_RESULTS' : ''), [
                'ID' => strval($_poll['id']),
                'PRIVATE' => $is_private,
                'VOTES_REVEALED' => $_poll['view_member_votes'] == 1,
                'QUESTION' => $_poll['question'],
                'ANSWERS' => $answers,
                'SHOW_BUTTONS' => $show_buttons,
                'BUTTONS' => $poll_buttons,
                'FOOTER_MESSAGE' => $footer_message,
                'VOTE_URL' => $vote_url,
                'REVOKE_URL' => $revoke_url,
                'NOT_VOTED' => $voted_already == 0,
                'IS_OPEN' => $poll_is_open,
                '_MINIMUM_SELECTIONS' => strval($_poll['minimum_selections']),
                '_MAXIMUM_SELECTIONS' => strval($_poll['maximum_selections']),
                '_TOTAL_VOTES' => strval($total_votes),
                'MINIMUM_SELECTIONS' => integer_format($_poll['minimum_selections']),
                'MAXIMUM_SELECTIONS' => integer_format($_poll['maximum_selections']),
                'CLOSING_TIME' => ($poll_is_open && $_poll['closing_time'] !== null) ? strval($_poll['closing_time']) : '0',
                'TOTAL_VOTES' => integer_format($total_votes),
                'TOTAL_VOTING_POWER' => ($total_voting_power !== null) ? float_format($total_voting_power, 2) : null,
                'POINT_WEIGHTING' => $point_weighting,
            ]);

            // Add polls RSS feed
            inject_feed_url('?mode=cns_polls&select=' . strval($topic_info['id']), do_lang('TOPIC_POLL_VOTES'));
        } else {
            $poll = new Tempcode();
        }

        // Quick reply
        if ((array_key_exists('may_use_quick_reply', $topic_info)) && ($may_reply) && ($id !== null)) {
            $map = ['page' => 'topics', 'type' => '_add_reply', 'topic_id' => $id, 'timestamp' => time()];
            $test = get_param_string('kfs' . (($topic_info['forum_id'] === null) ? '' : strval($topic_info['forum_id'])), null, INPUT_FILTER_GET_COMPLEX);
            if (($test !== null) && ($test !== '0')) {
                $map['kfs' . (($topic_info['forum_id'] === null) ? '' : strval($topic_info['forum_id']))] = $test;
            }
            $test_threaded = get_param_integer('threaded', null);
            if ($test_threaded !== null) {
                $map['threaded'] = $test_threaded;
            }
            $map['overlay'] = get_param_integer('overlay', null);
            $map['wide_high'] = get_param_integer('wide_high', null);
            $_post_url = build_url($map, get_module_zone('topics'));
            $post_url = $_post_url->evaluate();
            $map = ['page' => 'topics', 'type' => 'new_post', 'id' => $id, 'timestamp' => time()];
            if (($test !== null) && ($test !== '0')) {
                $map['kfs' . (($topic_info['forum_id'] === null) ? '' : strval($topic_info['forum_id']))] = $test;
            }
            $more_url = build_url($map, get_module_zone('topics'));
            if (isset($topic_info['first_post'])) {
                $post_row = db_map_restrict($topic_info['row'], ['id', 'p_post'], ['id' => 't_cache_first_post_id']);
                $_postdetails = get_translated_tempcode('f_posts', $post_row, 'p_post', $GLOBALS['FORUM_DB']);
            } else {
                $_postdetails = new Tempcode();
            }
            $first_post = $_postdetails;
            if (strpos($first_post->evaluate(), '<script') !== false) {
                $first_post = new Tempcode(); // Rendering twice could cause issues
            }
            if ($topic_info['first_post_id'] !== null) {
                $first_post_url = $GLOBALS['FORUM_DRIVER']->post_url($topic_info['first_post_id'], ($topic_info['forum_id'] === null) ? '' : strval($topic_info['forum_id']), true);
            } else {
                $first_post_url = new Tempcode();
            }
            $display = 'block';
            $expand_type = 'contract';
            if ($topic_info['max_rows'] > $start + $max) {
                $display = 'none';
                $expand_type = 'expand';
            }
            $emoticons = $GLOBALS['FORUM_DRIVER']->get_emoticon_chooser();
            require_javascript('editing');
            require_javascript('checking');
            if (addon_installed('captcha')) {
                require_code('captcha');
                $use_captcha = use_captcha();
                if ($use_captcha) {
                    generate_captcha();
                }
            } else {
                $use_captcha = false;
            }

            $post_warning = '';

            if ($topic_info['is_really_threaded'] == 1) {
                $post_warning = do_lang('THREADED_REPLY_NOTICE', $post_warning);
            }

            require_code('form_templates');
            list($attachments, $attach_size_field) = get_attachments('post');

            $quick_reply = do_template('COMMENTS_POSTING_FORM', [
                '_GUID' => '4c532620f3eb68d9cc820b18265792d7',
                'TITLE' => do_lang_tempcode('QUICK_REPLY'),
                'USE_CAPTCHA' => $use_captcha,
                'GET_EMAIL' => false,
                'EMAIL_OPTIONAL' => true,
                'GET_TITLE' => false,
                'TITLE_OPTIONAL' => true,
                'DEFAULT_TITLE' => '',
                'POST_WARNING' => $post_warning,
                'RULES_TEXT' => '',
                'ATTACHMENTS' => $attachments,
                'ATTACH_SIZE_FIELD' => $attach_size_field,
                'TRUE_ATTACHMENT_UI' => true,
                'EMOTICONS' => $emoticons,
                'EXPAND_TYPE' => $expand_type,
                'DISPLAY' => $display,
                'FIRST_POST_URL' => $first_post_url,
                'FIRST_POST' => $first_post,
                'COMMENT_URL' => $post_url,
                'SUBMIT_NAME' => do_lang_tempcode('REPLY'),
                'MORE_URL' => $more_url,
                'REVIEWS' => false,
                'ANALYTIC_EVENT_CATEGORY' => null,
            ]);
        } else {
            $quick_reply = new Tempcode();
        }

        $action_url = build_url(['page' => 'topics', 'id' => $id], get_module_zone('topics'));
        if ($id !== null) {
            $default_poll_options = [];
            require_code('cns_polls_action3');
            $default_poll_options = cns_get_default_poll_options($topic_info['forum_id']);

            // Moderation options
            $moderator_actions = ''; // XHTMLXHTML
            if ($topic_info['forum_id'] === null) {
                $moderator_actions .= '<option value="categorise_pts">' . do_lang('_CATEGORISE_PTS') . '</option>';
                $filter_cats = cns_get_filter_cats();
                foreach ($filter_cats as $filter_cat) {
                    if ($filter_cat != '') {
                        $moderator_actions .= '<option value="categorise_pts__' . escape_html($filter_cat) . '">' . do_lang('CATEGORISE_PTS_AS', escape_html($filter_cat)) . '</option>';
                    }
                }
            }
            if ((array_key_exists('may_multi_moderate', $topic_info)) && (array_key_exists('forum_id', $topic_info)) && (addon_installed('cns_multi_moderations'))) {
                $multi_moderations = cns_list_multi_moderations($topic_info['forum_id']);
                if (!empty($multi_moderations)) {
                    require_lang('cns_multi_moderations');
                    $moderator_actions .= '<optgroup label="' . do_lang('MULTI_MODERATIONS') . '">';
                    foreach ($multi_moderations as $mm_id => $mm_name) {
                        $moderator_actions .= '<option value="mm_' . strval($mm_id) . '">' . escape_html($mm_name) . '</option>';
                    }
                    $moderator_actions .= '</optgroup>';
                }
            }
            if (array_key_exists('may_move_topic', $topic_info)) {
                $moderator_actions .= '<option value="move_topic">' . do_lang('MOVE_TOPIC') . '</option>';
            }
            if (array_key_exists('may_edit_topic', $topic_info)) {
                $moderator_actions .= '<option value="edit_topic">' . do_lang('EDIT_THIS_TOPIC') . '</option>';
            }
            if (array_key_exists('may_delete_topic', $topic_info)) {
                $moderator_actions .= '<option value="delete_topic">' . do_lang('DELETE_THIS_TOPIC') . '</option>';
            }
            if (array_key_exists('may_pin_topic', $topic_info)) {
                $moderator_actions .= '<option value="pin_topic">' . do_lang('PIN_TOPIC') . '</option>';
            }
            if (array_key_exists('may_unpin_topic', $topic_info)) {
                $moderator_actions .= '<option value="unpin_topic">' . do_lang('UNPIN_TOPIC') . '</option>';
            }
            if (array_key_exists('may_cascade_topic', $topic_info)) {
                $moderator_actions .= '<option value="cascade_topic">' . do_lang('CASCADE_TOPIC') . '</option>';
            }
            if (array_key_exists('may_uncascade_topic', $topic_info)) {
                $moderator_actions .= '<option value="uncascade_topic">' . do_lang('UNCASCADE_TOPIC') . '</option>';
            }
            if (array_key_exists('may_open_topic', $topic_info)) {
                $moderator_actions .= '<option value="open_topic">' . do_lang('OPEN_TOPIC') . '</option>';
            }
            if (array_key_exists('may_close_topic', $topic_info)) {
                $moderator_actions .= '<option value="close_topic">' . do_lang('CLOSE_TOPIC') . '</option>';
            }
            if ((array_key_exists('may_edit_poll', $topic_info))) {
                $moderator_actions .= '<option value="edit_poll">' . do_lang('EDIT_TOPIC_POLL') . '</option>';
            }
            if ((array_key_exists('may_delete_poll', $topic_info)) && (!$default_poll_options['requireTopicPoll'])) {
                $moderator_actions .= '<option value="delete_poll">' . do_lang('DELETE_TOPIC_POLL') . '</option>';
            }
            if ((array_key_exists('may_attach_poll', $topic_info))) {
                $moderator_actions .= '<option value="add_poll">' . do_lang('ADD_TOPIC_POLL') . '</option>';
            }
            if (addon_installed('actionlog')) {
                require_code('revisions_engine_database');
                $revision_engine = new RevisionEngineDatabase(true);
                if ($revision_engine->has_revisions(['post'], null, strval($id))) {
                    $moderator_actions .= '<option value="topic_history">' . do_lang('actionlog:REVISIONS') . '</option>';
                }
            }
            if ((array_key_exists('may_make_private', $topic_info)) && ($topic_info['forum_id'] !== null)) {
                $moderator_actions .= '<option value="make_private">' . do_lang('MAKE_PERSONAL') . '</option>';
            }

            if ($GLOBALS['XSS_DETECT']) {
                ocp_mark_as_escaped($moderator_actions);
            }

            // Marked post actions
            $map = ['page' => 'topics', 'id' => $id];
            $test = get_param_string('kfs' . (($topic_info['forum_id'] === null) ? '' : strval($topic_info['forum_id'])), null, INPUT_FILTER_GET_COMPLEX);
            if (($test !== null) && ($test !== '0')) {
                $map['kfs' . (($topic_info['forum_id'] === null) ? '' : strval($topic_info['forum_id']))] = $test;
            }
            $test_threaded = get_param_integer('threaded', null);
            if ($test_threaded !== null) {
                $map['threaded'] = $test_threaded;
            }
            $action_url = build_url($map, get_module_zone('topics'), [], false, true);
            $marked_post_actions = ''; // XHTMLXHTML
            if (array_key_exists('may_move_posts', $topic_info)) {
                $marked_post_actions .= '<option value="move_posts_a">' . do_lang('MERGE_POSTS') . '</option>';
                $marked_post_actions .= '<option value="move_posts_b">' . do_lang('SPLIT_POSTS') . '</option>';
            }
            if (array_key_exists('may_delete_posts', $topic_info)) {
                $marked_post_actions .= '<option value="delete_posts">' . do_lang('DELETE_POSTS') . '</option>';
            }
            if ((array_key_exists('may_validate_posts', $topic_info)) && (addon_installed('validation'))) {
                $marked_post_actions .= '<option value="validate_posts">' . do_lang('VALIDATE_POSTS') . '</option>';
            }
            if (get_option('enable_multi_quote') == '1') {
                if ($may_reply) {
                    $marked_post_actions .= '<option value="new_post">' . do_lang('QUOTE_POSTS') . '</option>';
                }
            }

            if ($GLOBALS['XSS_DETECT']) {
                ocp_mark_as_escaped($marked_post_actions);
            }
        } else {
            $moderator_actions = '';
            $marked_post_actions = '';
        }

        $max_rows = $topic_info['max_rows'];
        if (($max_rows > $max) && (!$threaded)) {
            require_code('templates_pagination');
            $pagination = pagination(do_lang_tempcode('FORUM_POSTS'), $start, 'topic_start', $max, 'topic_max', $max_rows, false, 7, [10, 20, 30]);
        } else {
            $pagination = new Tempcode();
        }

        // Members viewing this topic
        if ($topic_info['forum_id'] !== null) {
            member_tracking_update('forumview', '', strval($topic_info['forum_id'])); // If we are viewing topic say we are viewing the forum too
        }
        require_code('users2');
        if ($id === null) {
            $num_guests = null;
            $num_members = null;
            $members_viewing = new Tempcode();
        } else {
            list($num_guests, $num_members, $members_viewing) = get_members_viewing_wrap('topicview', '', strval($id), true); // This does a member_tracking_update to the topic internally
        }

        if (($topic_info['validated'] == 0) && (addon_installed('validation'))) {
            $warning_details = do_template('WARNING_BOX', [
                '_GUID' => '313de370c1aeab9545c4bee4e35e7f84',
                'WARNING' => do_lang_tempcode((get_param_integer('redirected', 0) == 1) ? 'NOT_VALIDATED_TEXT_NON_DIRECT' : 'NOT_VALIDATED_TEXT', 'topic'),
            ]);
        } else {
            $warning_details = new Tempcode();
        }

        if ($topic_info['forum_id'] !== null) {
            require_code('cns_general');
            cns_set_context_forum($topic_info['forum_id']);
        }

        if (addon_installed('tickets')) {
            require_code('tickets');
            $is_ticket_forum = is_ticket_forum($topic_info['forum_id']);
        } else {
            $is_ticket_forum = false;
        }

        $topic_tpl = do_template('CNS_TOPIC_SCREEN', [
            '_GUID' => 'bb201d5d59559e5e2bd60e7cf2e6f7e9',
            'TITLE' => $this->title,
            'SERIALIZED_OPTIONS' => $serialized_options,
            'HASH' => $hash,
            'ID' => ($id === null) ? '' : strval($id),
            '_TITLE' => $topic_info['title'],
            'MAY_DOUBLE_POST' => has_privilege(get_member(), 'double_post'),
            'LAST_POSTER' => array_key_exists('last_poster', $topic_info) ? (($topic_info['last_poster'] === null) ? '' : strval($topic_info['last_poster'])) : '',
            'WARNING_DETAILS' => $warning_details,
            'MAX' => strval($max),
            'MAY_CHANGE_MAX' => array_key_exists('may_change_max', $topic_info),
            'ACTION_URL' => $action_url,
            '_NUM_GUESTS' => ($num_guests === null) ? '' : strval($num_guests),
            '_NUM_MEMBERS' => ($num_members === null) ? '' : strval($num_members),
            'NUM_GUESTS' => ($num_guests === null) ? '' : integer_format($num_guests, 0),
            'NUM_MEMBERS' => ($num_members === null) ? '' : integer_format($num_members, 0),
            'MEMBERS_VIEWING' => $members_viewing,
            'PAGINATION' => $pagination,
            'MODERATOR_ACTIONS' => $moderator_actions,
            'MARKED_POST_ACTIONS' => $marked_post_actions,
            'QUICK_REPLY' => $quick_reply,
            'BREADCRUMBS' => breadcrumbs_get_default_stub(),
            'POLL' => $poll,
            'BUTTON_SCREENS' => $buttons,
            'POSTS' => $posts,
            'THREADED' => $threaded,
            'FORUM_ID' => ($topic_info['forum_id'] === null) ? '' : strval($topic_info['forum_id']),
            'IS_ALREADY_READ' => ($id === null) ? true : cns_has_read_topic($id),
            'TICKET_FORUM' => $is_ticket_forum,
        ]);

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

    /**
     * Update the read status for a topic.
     */
    public function _update_read_status()
    {
        if (!is_guest()) {
            if ((get_option('post_read_history_days') != '0') && ((get_value('disable_normal_topic_read_history') !== '1') || ($this->forum_id === null))) {
                cns_ping_topic_read($this->id);
                if ($GLOBALS['IS_ACTUALLY'] !== null) { // If posting with SU, mark the SUing user as read too, otherwise it is annoying
                    cns_ping_topic_read($this->id, $GLOBALS['IS_ACTUALLY']);
                }
            }
        }
        $increment = statistical_update_model('f_topics', $this->topic_info['num_views']);
        if ($increment != 0) {
            $GLOBALS['FORUM_DB']->query('UPDATE ' . $GLOBALS['FORUM_DB']->get_table_prefix() . 'f_topics SET t_num_views=(t_num_views+1) WHERE id=' . strval($this->id), 1, 0, true); // Errors suppressed in case DB write access broken
        }
    }
}
