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

/**
 * Find the URL to a post.
 *
 * @param  AUTO_LINK $post_id The post ID
 * @return URLPATH The URL
 */
function find_post_id_url(int $post_id) : string
{
    $max = intval(get_option('forum_posts_per_page'));
    if ($max == 0) {
        $max = 1;
    }

    $post_details = $GLOBALS['FORUM_DB']->query_select('f_posts', ['p_topic_id', 'p_time'], ['id' => $post_id]);
    if (empty($post_details)) {
        warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'post'));
    }

    $post_time = $post_details[0]['p_time'];
    $topic_id = $post_details[0]['p_topic_id'];

    // What page is it on?
    $before = $GLOBALS['FORUM_DB']->query_value_if_there('SELECT COUNT(*) FROM ' . $GLOBALS['FORUM_DB']->get_table_prefix() . 'f_posts WHERE p_time<=' . strval($post_time) . ' AND id<>' . strval($post_id) . ' AND ' . cns_get_topic_where($topic_id), false, true);
    $start = intval(floor(floatval($before) / floatval($max))) * $max;

    // Now redirect accordingly
    $map = ['page' => 'topicview', 'type' => null, 'id' => $topic_id, 'topic_start' => ($start == 0) ? null : $start, 'post_id' => $post_id, 'threaded' => get_param_integer('threaded', null)];
    foreach ($_GET as $key => $val) {
        if (is_integer($key)) {
            $key = strval($key);
        }

        if ((substr($key, 0, 3) == 'kfs') || (in_array($key, ['topic_start', 'topic_max']))) {
            $map[$key] = $val;
        }
    }
    $test_threaded = get_param_integer('threaded', null);
    if ($test_threaded !== null) {
        $map['threaded'] = $test_threaded;
    }
    $_redirect = build_url($map, '_SELF', [], false, false, false, 'post_' . strval($post_id));
    $redirect = $_redirect->evaluate();

    return $redirect;
}

/**
 * Find the URL to the latest unread post in a topic.
 *
 * @param  AUTO_LINK $topic_id The topic ID
 * @return URLPATH The URL
 */
function find_first_unread_url(int $topic_id) : string
{
    $max = intval(get_option('forum_posts_per_page'));
    if ($max == 0) {
        $max = 1;
    }

    $first_unread_id = null;

    $last_read_time = $GLOBALS['FORUM_DB']->query_select_value_if_there('f_read_logs', 'l_time', ['l_member_id' => get_member(), 'l_topic_id' => $topic_id]);
    if ($last_read_time === null) {
        // Assumes that everything made in the last two weeks has not been read
        $unread_details = $GLOBALS['FORUM_DB']->query('SELECT id,p_time FROM ' . $GLOBALS['FORUM_DB']->get_table_prefix() . 'f_posts WHERE p_topic_id=' . strval($topic_id) . ' AND p_time>' . strval(time() - 60 * 60 * 24 * intval(get_option('post_read_history_days'))) . ' ORDER BY p_time', 1);
        if (array_key_exists(0, $unread_details)) {
            $last_read_time = $unread_details[0]['p_time'] - 1;
        } else {
            $last_read_time = 0;
        }
    }
    $post_details = $GLOBALS['FORUM_DB']->query('SELECT p_time,id FROM ' . $GLOBALS['FORUM_DB']->get_table_prefix() . 'f_posts WHERE p_topic_id=' . strval($topic_id) . ' AND p_time>=' . strval($last_read_time) . ' ORDER BY p_time');
    if (!empty($post_details)) {
        $first_unread_id = $post_details[0]['id'];

        // What page is it on?
        $first_unread_id_post_time = $post_details[0]['p_time'];
        $before = $GLOBALS['FORUM_DB']->query_value_if_there('SELECT COUNT(*) FROM ' . $GLOBALS['FORUM_DB']->get_table_prefix() . 'f_posts WHERE p_time<' . strval($first_unread_id_post_time) . ' AND ' . cns_get_topic_where($topic_id), false, true);
        $start = intval(floor(floatval($before) / floatval($max))) * $max;
    } else {
        // What page is it on?
        $before = $GLOBALS['FORUM_DB']->query_value_if_there('SELECT COUNT(*) FROM ' . $GLOBALS['FORUM_DB']->get_table_prefix() . 'f_posts WHERE ' . cns_get_topic_where($topic_id), false, true);
        $start = intval(floor(floatval($before) / floatval($max))) * $max;
        if ($start == $before) {
            $start = max(0, $before - $max);
        }
    }

    // Now redirect accordingly
    $map = ['page' => 'topicview', 'id' => $topic_id, 'type' => null, 'topic_start' => ($start == 0) ? null : $start, 'post_id' => $first_unread_id];
    foreach ($_GET as $key => $val) {
        if (is_integer($key)) {
            $key = strval($key);
        }

        if ((substr($key, 0, 3) == 'kfs') || (in_array($key, ['topic_start', 'topic_max']))) {
            $map[$key] = $val;
        }
    }
    if ($first_unread_id !== null) {
        $hash = 'post-' . strval($first_unread_id);
    } else {
        $hash = 'first-unread';
    }
    $_redirect = build_url($map, '_SELF', [], false, false, false, $hash);
    $redirect = $_redirect->evaluate();

    return $redirect;
}

/**
 * Turn a post row, into a detailed map of information that is suitable for use as display parameters for that post.
 *
 * @param  array $_postdetails The post row
 * @param  array $topic_info The topic row
 * @param  boolean $only_post Whether the post is the only post in the topic
 * @return array The detailed map
 */
function cns_get_details_to_show_post(array $_postdetails, array $topic_info, bool $only_post = false) : array
{
    $forum_id = $_postdetails['p_cache_forum_id'];

    $primary_group = cns_get_member_primary_group($_postdetails['p_posting_member']);
    if ($primary_group === null) {
        $_postdetails['p_posting_member'] = db_get_first_id();
        $primary_group = db_get_first_id();
    }

    $post = [
        'id' => $_postdetails['id'],
        'topic_id' => $_postdetails['p_topic_id'],
        'title' => $_postdetails['p_title'],
        'post' => $_postdetails['message'],
        'time' => $_postdetails['p_time'],
        'date' => get_timezoned_date_time($_postdetails['p_time']),
        'validated' => $_postdetails['p_validated'],
        'is_emphasised' => $_postdetails['p_is_emphasised'],
        'poster_username' => $_postdetails['p_poster_name_if_guest'],
        'poster' => $_postdetails['p_posting_member'],
    ];

    $post['has_revisions'] = false;
    if ((addon_installed('actionlog')) && (has_actual_page_access(get_member(), 'admin_revisions'))) {
        require_code('revisions_engine_database');
        $revision_engine = new RevisionEngineDatabase(true);
        if ($revision_engine->has_revisions(['post'], strval($_postdetails['id']))) {
            $post['has_revisions'] = true;
        }
    }

    if (array_key_exists('message_comcode', $_postdetails)) {
        $post['message_comcode'] = $_postdetails['message_comcode'];
    }

    // Edited?
    if ($_postdetails['p_last_edit_member'] !== null) {
        $post['last_edit_member'] = $_postdetails['p_last_edit_member'];
        $post['last_edit_time'] = $_postdetails['p_last_edit_time'];
        $post['last_edit_date'] = get_timezoned_date_time($_postdetails['p_last_edit_time']);
        $post['last_edit_username'] = $GLOBALS['CNS_DRIVER']->get_username($_postdetails['p_last_edit_member']);
    }

    $is_banned = ($GLOBALS['CNS_DRIVER']->get_member_row_field($_postdetails['p_posting_member'], 'm_is_perm_banned') != '0');

    // Find title
    $title = addon_installed('cns_member_titles') ? $GLOBALS['CNS_DRIVER']->get_member_row_field($_postdetails['p_posting_member'], 'm_title') : '';
    if ($title == '') {
        $title = get_translated_text(cns_get_group_property($primary_group, 'title'), $GLOBALS['FORUM_DB']);
    }
    if ($is_banned) {
        $title = do_lang('BANNED');
    }
    $post['poster_title'] = $title;

    // If this isn't guest posted, we can put some member details in
    if (($_postdetails['p_posting_member'] !== null) && ($_postdetails['p_posting_member'] != $GLOBALS['CNS_DRIVER']->get_guest_id())) {
        require_code('cns_general');
        $need = [
            'highlighted_name',
            'online',
            'avatar',
            'ip_address',
            'signature',
        ];
        $post += cns_read_in_member_profile($_postdetails['p_posting_member'], $need, false, false);
    } elseif ($_postdetails['p_posting_member'] == $GLOBALS['CNS_DRIVER']->get_guest_id()) {
        if ($_postdetails['p_poster_name_if_guest'] == do_lang('SYSTEM')) {
            $post['avatar'] = find_theme_image('cns_default_avatars/system', true);
            $post['poster_title'] = '';
        }
    }

    // Do we have any special controls over this post?
    require_code('cns_posts');
    $reason = null;
    $may_edit = cns_may_edit_post_by($_postdetails['id'], $_postdetails['p_time'], $_postdetails['p_posting_member'], $forum_id, get_member(), $topic_info['t_is_open'] == 0, $reason);
    if ($may_edit || $reason !== null/*Interesting reason, let them find it out when they click*/) {
        $post['may_edit'] = true;
    }
    if (!$only_post) {
        $may_delete = cns_may_delete_post_by($_postdetails['id'], $_postdetails['p_time'], $_postdetails['p_posting_member'], $forum_id, get_member(), $reason);
        if ($may_delete || $reason !== null/*Interesting reason, let them find it out when they click*/) {
            $post['may_delete'] = true;
        }
    }

    // More
    if (has_privilege(get_member(), 'see_ip')) {
        $post['ip_address'] = $_postdetails['p_ip_address'];
    }
    if ($_postdetails['p_whisper_to_member'] !== null) {
        $post['whisper_to_member'] = $_postdetails['p_whisper_to_member'];
    }

    return $post;
}

/**
 * Read in a great big map of details relating to a topic.
 *
 * @param  ?AUTO_LINK $topic_id The ID of the topic we are getting details of (null: whispers)
 * @param  integer $start The start row for getting details of posts in the topic (i.e. 0 is start of topic, higher is further through).
 * @param  integer $max The maximum number of posts to get detail of
 * @param  boolean $view_poll_results Whether we are viewing poll results for the topic (if there is no poll for the topic, this is irrelevant)
 * @param  boolean $check_perms Whether to check permissions
 * @return array The map of details
 */
function cns_read_in_topic(?int $topic_id, int $start, int $max, bool $view_poll_results = false, bool $check_perms = true) : array
{
    if ($topic_id !== null) {
        $table = 'f_topics t LEFT JOIN ' . $GLOBALS['FORUM_DB']->get_table_prefix() . 'f_forums f ON f.id=t.t_forum_id';
        $select = ['t.*', 'f.f_is_threaded'];
        if (multi_lang_content()) {
            $select[] = 't_cache_first_post AS p_post';
        } else {
            $table .= ' LEFT JOIN ' . $GLOBALS['FORUM_DB']->get_table_prefix() . 'f_posts p ON p.id=t.t_cache_first_post_id';
            $select[] = 'p_post,p_post__text_parsed,p_post__source_user';
        }
        $_topic_info = $GLOBALS['FORUM_DB']->query_select($table, $select, ['t.id' => $topic_id], '', 1);
        if (!array_key_exists(0, $_topic_info)) {
            warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'topic'));
        }
        $topic_info = $_topic_info[0];

        // Are we allowed into here?
        //  Check forum
        $forum_id = $topic_info['t_forum_id'];
        if ($forum_id !== null) {
            if ($check_perms) {
                if (!cns_may_access_topic($topic_id, get_member(), $topic_info, false)) {
                    access_denied('CATEGORY_ACCESS_LEVEL');
                }
            }
        } else {
            // It must be a private topic. Do we have access?
            if (!cns_may_access_topic($topic_id, get_member(), $topic_info, false)) {
                access_denied('PRIVILEGE', 'view_other_pt');
            }

            decache_private_topics(get_member());
        }
        // Check validated
        if (($topic_info['t_validated'] == 0) && (addon_installed('validation') && ($check_perms))) {
            require_code('validation');
            check_jump_to_not_validated('topic', strval($topic_id), get_member(), [$topic_info['t_cache_first_member_id']]);
        }

        if (get_param_integer('threaded', null) === null) {
            if ($start > 0) {
                if ($topic_info['f_is_threaded'] == 1) {
                    $_GET['threaded'] = '0';
                }
            }
        }
        $is_threaded = get_param_integer('threaded', (($topic_info['f_is_threaded'] === null) ? 0 : $topic_info['f_is_threaded']));
        if ($is_threaded != 1) {
            $is_threaded = 0; // In case of invalid URLs causing inconsistent handling
        }

        // Some general info
        if ($topic_info['t_description'] == '') {
            require_code('content2');
            list(, $good_description) = _seo_meta_find_data([], get_translated_text($topic_info['p_post'], $GLOBALS['FORUM_DB']));
        } else {
            $good_description = $topic_info['t_description'];
        }
        $out = [
            'id' => $topic_info['id'],
            'num_views' => $topic_info['t_num_views'],
            'num_posts' => $topic_info['t_cache_num_posts'],
            'validated' => $topic_info['t_validated'],
            'title' => $topic_info['t_cache_first_title'],
            'description' => $topic_info['t_description'],
            'description_link' => $topic_info['t_description_link'],
            'emoticon' => $topic_info['t_emoticon'],
            'forum_id' => $topic_info['t_forum_id'],
            'first_post' => $topic_info['p_post'],
            'first_poster' => $topic_info['t_cache_first_member_id'],
            'first_post_id' => $topic_info['t_cache_first_post_id'],
            'pt_from' => $topic_info['t_pt_from_member'],
            'pt_to' => $topic_info['t_pt_to_member'],
            'is_open' => $topic_info['t_is_open'],
            'is_threaded' => $is_threaded,
            'is_really_threaded' => ($topic_info['f_is_threaded'] === null) ? 0 : $topic_info['f_is_threaded'],
            'last_time' => $topic_info['t_cache_last_time'],
            'metadata' => [
                'identifier' => '_SEARCH:topicview:browse:' . strval($topic_id),
                'numcomments' => strval($topic_info['t_cache_num_posts']),
                'description' => $good_description, // There's no meta description, so we'll take this as a description, which will feed through
            ],
            'row' => $topic_info,
        ];

        // Poll?
        if ($topic_info['t_poll_id'] !== null) {
            require_code('cns_polls');
            if (is_guest()) {
                $voted_already_map = ['pv_poll_id' => $topic_info['t_poll_id'], 'pv_ip_address' => get_ip_address(), 'pv_member_id' => $GLOBALS['FORUM_DRIVER']->get_guest_id(), 'pv_revoked' => 0];
            } else {
                $voted_already_map = ['pv_poll_id' => $topic_info['t_poll_id'], 'pv_member_id' => get_member(), 'pv_revoked' => 0];
            }
            $voted_already = $GLOBALS['FORUM_DB']->query_select_value_if_there('f_poll_votes', 'pv_member_id', $voted_already_map);
            $test = cns_poll_get_results($topic_info['t_poll_id'], $view_poll_results || ($voted_already !== null));
            if ($test !== null) {
                $out['poll'] = $test;
                $out['poll']['voted_already'] = $voted_already;
                $out['poll_id'] = $topic_info['t_poll_id'];
            }
        }

        // Post query
        $where = cns_get_topic_where($topic_id);
        $query = 'SELECT p.* FROM ' . $GLOBALS['FORUM_DB']->get_table_prefix() . 'f_posts p WHERE ' . $where . ' ORDER BY p_time,p.id';
    } else {
        $out = [
            'num_views' => 0,
            'num_posts' => 0,
            'validated' => 1,
            'title' => do_lang('INLINE_PERSONAL_POSTS'),
            'description' => '',
            'description_link' => '',
            'emoticon' => '',
            'forum_id' => null,
            'first_post' => null,
            'first_poster' => null,
            'first_post_id' => null,
            'pt_from' => null,
            'pt_to' => null,
            'is_open' => 1,
            'is_threaded' => 0,
            'last_time' => time(),
            'metadata' => [],
            'row' => [],
        ];

        // Post query
        $where = 'p_whisper_to_member=' . strval(get_member());
        $query = 'SELECT p.* FROM ' . $GLOBALS['FORUM_DB']->get_table_prefix() . 'f_posts p WHERE ' . $where . ' ORDER BY p_time,p.id';

        $topic_info = [
            't_is_open' => 1,
        ];
    }

    // Posts
    if ($out['is_threaded'] == 0) {
        if ($start < 200) {
            $_postdetailss = list_to_map('id', $GLOBALS['FORUM_DB']->query($query, $max, $start, false, false, ['p_post' => 'LONG_TRANS__COMCODE']));
        } else { // deep search, so we need to make offset more efficient, trade-off is more queries
            $_postdetailss = list_to_map('id', $GLOBALS['FORUM_DB']->query($query, $max, $start));
        }
        if (($start == 0) && (count($_postdetailss) < $max)) {
            $out['max_rows'] = $max; // We know that they're all on this screen
        } else {
            $out['max_rows'] = (($topic_id === null) || $topic_info['t_cache_num_posts'] < 500) ? $GLOBALS['FORUM_DB']->query_value_if_there('SELECT COUNT(*) FROM ' . $GLOBALS['FORUM_DB']->get_table_prefix() . 'f_posts WHERE ' . $where) : $topic_info['t_cache_num_posts']/*for performance reasons*/;
        }
        $posts = [];
        // Precache member/group details in one fell swoop
        $members = [];
        foreach ($_postdetailss as $_postdetails) {
            $members[$_postdetails['p_posting_member']] = 1;
            if ($out['title'] == '') {
                $out['title'] = $_postdetails['p_title'];
            }
        }
        cns_cache_member_details(array_keys($members));

        $i = 0;
        foreach ($_postdetailss as $_postdetails) {
            $_postdetails['message_comcode'] = get_translated_text($_postdetails['p_post'], $GLOBALS['FORUM_DB']);

            $linked_type = '';
            $linked_id = '';
            $linked_url = '';

            // If it's a spacer post, see if we can detect it better
            list($is_spacer_post, $spacer_post_lang) = is_spacer_post($_postdetails['message_comcode']);
            if ($is_spacer_post) {
                $c_prefix = do_lang('COMMENT', null, null, null, $spacer_post_lang) . ': #';
                if ((substr($out['description'], 0, strlen($c_prefix)) == $c_prefix) && ($out['description_link'] != '')) {
                    list($linked_type, $linked_id) = explode('_', substr($out['description'], strlen($c_prefix)), 2);
                    $linked_url = $out['description_link'];
                    $out['description'] = '';
                }
            }

            // Load post
            $post_row = db_map_restrict($_postdetails, ['id', 'p_post']);
            $_postdetails['message'] = get_translated_tempcode('f_posts', $post_row, 'p_post', $GLOBALS['FORUM_DB']);

            // Fake a quoted post? (kind of a nice 'tidy up' feature if a forum's threading has been turned off, leaving things for flat display)
            if (($_postdetails['p_parent_id'] !== null) && (strpos($_postdetails['message_comcode'], '[quote') === false)) {
                $p = null; // null
                if (array_key_exists($_postdetails['p_parent_id'], $_postdetailss)) { // Ah, we're already loading it on this page
                    $p = $_postdetailss[$_postdetails['p_parent_id']];

                    // Load post
                    $_p = db_map_restrict($p, ['id', 'p_post', 'p_poster_name_if_guest']);
                    $p['message'] = get_translated_tempcode('f_posts', $_p, 'p_post', $GLOBALS['FORUM_DB']);
                } else { // Drat, we need to load it
                    $_p = $GLOBALS['FORUM_DB']->query_select('f_posts', ['*'], ['id' => $_postdetails['p_parent_id']], '', 1);
                    if (array_key_exists(0, $_p)) {
                        $p = db_map_restrict($_p[0], ['id', 'p_post', 'p_poster_name_if_guest']);
                        $p['message'] = get_translated_tempcode('f_posts', $p, 'p_post', $GLOBALS['FORUM_DB']);
                    }
                }
                $temp = $_postdetails['message'];
                $_postdetails['message'] = new Tempcode();
                $_postdetails['message'] = do_template('COMCODE_QUOTE_BY', ['_GUID' => '4521bfe295b1834460f498df488ee7cb', 'SAIDLESS' => false, 'BY' => $p['p_poster_name_if_guest'], 'CONTENT' => $p['message']]);
                $_postdetails['message']->attach($temp);
            }

            // Spacer posts may have a better first post put in place
            if ($is_spacer_post) {
                require_code('cns_posts');
                list($new_description, $new_post) = cns_display_spacer_post($linked_type, $linked_id);
                //if ($new_description !== null) $out['description']=$new_description;   Actually, it's a bit redundant
                if ($new_post !== null) {
                    $_postdetails['message'] = $new_post;
                }

                $is_ticket = false;
                if (addon_installed('tickets')) {
                    require_code('tickets');
                    if (is_ticket_forum($forum_id)) {
                        $is_ticket = true;

                        require_lang('tickets');
                        require_code('feedback');
                        $ticket_id = extract_topic_identifier($out['description']);
                        $ticket_type_id = $GLOBALS['SITE_DB']->query_select_value_if_there('tickets', 'ticket_type', ['ticket_id' => $ticket_id]);
                        $ticket_type_name = null;
                        if ($ticket_type_id !== null) {
                            $ticket_type_name = $GLOBALS['SITE_DB']->query_select_value_if_there('ticket_types', 'ticket_type_name', ['id' => $ticket_id]);
                        }
                        $out['title'] = do_lang_tempcode('_VIEW_SUPPORT_TICKET', escape_html($out['title']), ($ticket_type_name === null) ? do_lang('UNKNOWN') : escape_html(get_translated_text($ticket_type_name)));
                        $_postdetails['p_title'] = '';
                    }
                }
                if (!$is_ticket) {
                    $out['title'] = protect_from_escaping(do_lang('SPACER_TOPIC_TITLE_WRAP', escape_html($out['title']), '', '', $spacer_post_lang));
                    $_postdetails['p_title'] = do_lang('SPACER_TOPIC_TITLE_WRAP', $_postdetails['p_title'], '', '', $spacer_post_lang);
                }
            }

            // Put together
            $collated_post_details = cns_get_details_to_show_post($_postdetails, $topic_info, ($start == 0) && (count($_postdetailss) == 1));
            $collated_post_details['is_spacer_post'] = $is_spacer_post;
            $posts[] = $collated_post_details;

            $i++;
        }

        $out['posts'] = $posts;
    }

    // Any special topic/for-any-post-in-topic controls?
    if ($topic_id !== null) {
        $out['last_poster'] = $topic_info['t_cache_last_member_id'];
        $out['last_post_id'] = $topic_info['t_cache_last_post_id'];
        if (cns_may_post_in_topic($forum_id, $topic_id, $topic_info['t_cache_last_member_id'], $topic_info['t_is_open'] == 0)) {
            $out['may_reply'] = true;
        }
        if (cns_may_post_in_topic($forum_id, $topic_id, $topic_info['t_cache_last_member_id'], $topic_info['t_is_open'] == 0, null, true)) {
            $out['may_reply_private_post'] = true;
        }
        if ((has_privilege(get_member(), 'may_report_content')) && (addon_installed('tickets'))) {
            $out['may_report_content'] = true;
        }
        if (cns_may_make_private_topic()) {
            $out['may_pt_members'] = true;
        }
        if (cns_may_edit_topics_by($forum_id, get_member(), $topic_info['t_cache_first_member_id'])) {
            $out['may_edit_topic'] = true;
        }
        require_code('cns_moderation');
        require_code('cns_forums');
        if (cns_may_warn_members()) {
            $out['may_warn_members'] = true;
        }
        if (cns_may_delete_topics_by($forum_id, get_member(), $topic_info['t_cache_first_member_id'])) {
            $out['may_delete_topic'] = true;
        }
        if (cns_may_perform_multi_moderation($forum_id)) {
            $out['may_multi_moderate'] = true;
        }
        if (has_privilege(get_member(), 'use_quick_reply')) {
            $out['may_use_quick_reply'] = true;
        }
        $may_moderate_forum = cns_may_moderate_forum($forum_id);
        if ($may_moderate_forum) {
            if ($topic_info['t_is_open'] == 0) {
                $out['may_open_topic'] = true;
            } else {
                $out['may_close_topic'] = true;
            }
            if ($topic_info['t_pinned'] == 0) {
                $out['may_pin_topic'] = true;
            } else {
                $out['may_unpin_topic'] = true;
            }
            if ($topic_info['t_cascading'] == 0) {
                $out['may_cascade_topic'] = true;
            } else {
                $out['may_uncascade_topic'] = true;
            }
            $out['may_move_topic'] = true;
            $out['may_move_posts'] = true;
            $out['may_delete_posts'] = true;
            $out['may_validate_posts'] = true;
            $out['may_make_private'] = true;
            $out['may_change_max'] = true;
        } else {
            if (($topic_info['t_cache_first_member_id'] == get_member()) && (has_privilege(get_member(), 'close_own_topics')) && ($topic_info['t_is_open'] == 1)) {
                $out['may_close_topic'] = true;
            }
        }
        if ($topic_info['t_poll_id'] !== null) {
            require_code('cns_polls');

            if (cns_may_edit_poll_by($forum_id, $topic_info['t_cache_first_member_id'])) {
                $out['may_edit_poll'] = true;
            }
            if (cns_may_delete_poll_by($forum_id, $topic_info['t_cache_first_member_id'])) {
                $out['may_delete_poll'] = true;
            }
        } else {
            require_code('cns_polls');

            if (cns_may_attach_poll($topic_id, $topic_info['t_cache_first_member_id'], ($topic_info['t_poll_id'] !== null), $forum_id)) {
                $out['may_attach_poll'] = true;
            }
        }
    } else {
        $out['last_poster'] = null;
        $out['last_post_id'] = null;
    }

    return $out;
}

/**
 * Mass-load details for a list of members into memory, to reduce queries when we access it later.
 *
 * @param  array $members List of members
 */
function cns_cache_member_details(array $members)
{
    require_code('cns_members');

    $member_or_list = '';
    foreach ($members as $member_id) {
        if ($member_or_list != '') {
            $member_or_list .= ' OR ';
        }
        $member_or_list .= 'm.id=' . strval($member_id);
    }
    if ($member_or_list != '') {
        $member_rows = $GLOBALS['FORUM_DB']->query('SELECT m.*,f.* FROM ' . $GLOBALS['FORUM_DB']->get_table_prefix() . 'f_members m LEFT JOIN ' . $GLOBALS['FORUM_DB']->get_table_prefix() . 'f_member_custom_fields f ON f.mf_member_id=m.id WHERE ' . $member_or_list, null, 0, false, true);
        $member_rows_g = $GLOBALS['FORUM_DB']->query('SELECT gm_group_id,gm_member_id FROM ' . $GLOBALS['FORUM_DB']->get_table_prefix() . 'f_group_members WHERE (' . str_replace('m.id', 'gm_member_id', $member_or_list) . ')', null, 0, false, true);
        global $MEMBER_CACHE_FIELD_MAPPINGS, $SIGNATURES_CACHE;
        $found_groups = [];
        foreach ($member_rows as $row) {
            $GLOBALS['CNS_DRIVER']->MEMBER_ROWS_CACHED[$row['id']] = $row;

            if (!cns_is_ldap_member($row['id'])) {
                // Primary
                $pg = $GLOBALS['CNS_DRIVER']->get_member_row_field($row['id'], 'm_primary_group');
                $found_groups[$pg] = true;
            }

            // Signature
            if ((get_page_name() != 'search') && ($row['m_signature'] !== null) && ($row['m_signature'] !== '') && ($row['m_signature'] !== 0)) {
                $just_row = db_map_restrict($row, ['id', 'm_signature']);
                $SIGNATURES_CACHE[$row['id']] = get_translated_tempcode('f_members', $just_row, 'm_signature', $GLOBALS['FORUM_DB']);
            }

            $MEMBER_CACHE_FIELD_MAPPINGS[$row['mf_member_id']] = $row;
        }
        foreach ($member_rows_g as $row) {
            if (!cns_is_ldap_member($row['gm_member_id'])) {
                $found_groups[$row['gm_group_id']] = true;
            }
        }

        require_code('cns_groups');
        cns_ensure_groups_cached(array_keys($found_groups));
    }
}

/**
 * Get buttons for showing under a post.
 *
 * @param  array $topic_info Map of topic info
 * @param  array $_postdetails Map of post info
 * @param  boolean $may_reply Whether the current member may reply to the topic
 * @param  ID_TEXT $rendering_context Rendering context
 * @return Tempcode The buttons
 */
function cns_render_post_buttons(array $topic_info, array $_postdetails, bool $may_reply, string $rendering_context = 'cns') : object
{
    $may_reply_private_post = array_key_exists('may_reply_private_post', $topic_info);

    require_lang('cns');
    require_javascript('cns_forum');
    require_code('cns_members2');

    $buttons = new Tempcode();

    // Validate
    if ((array_key_exists('may_validate_posts', $topic_info)) && (addon_installed('validation')) && ((($topic_info['validated'] == 0) && ($_postdetails['id'] == $topic_info['first_post_id'])) || ($_postdetails['validated'] == 0))) {
        $map = ['page' => 'topics', 'type' => 'validate_post', 'id' => $_postdetails['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'));
        $_title = do_lang_tempcode('VALIDATE_POST');
        $_title_full = new Tempcode();
        $_title_full->attach($_title);
        $_title_full->attach(do_lang_tempcode('ID_NUM', strval($_postdetails['id'])));
        $buttons->attach(do_template('BUTTON_SCREEN_ITEM', ['_GUID' => '712fdaee35f378e37b007f3a73246690', 'REL' => 'validate nofollow', 'IMMEDIATE' => true, 'IMG' => 'menu/adminzone/audit/not_validated', 'TITLE' => $_title, 'FULL_TITLE' => $_title_full, 'URL' => $action_url]));
    }

    // Quote/reply
    if (($may_reply) && (get_bot_type() === null)) {
        $map = ['page' => 'topics', 'type' => 'new_post', 'id' => $_postdetails['topic_id'], 'parent_id' => $_postdetails['id']];
        if ($topic_info['is_threaded'] == 0) {
            $map['quote'] = $_postdetails['id'];
        }
        if (array_key_exists('whisper_to_member', $_postdetails)) {
            $map['whisper_to_member'] = $_postdetails['poster'];
        }
        $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'));

        $onclick_call_functions = null;
        $onclick_call_functions_explicit_quote = null;

        if ((array_key_exists('message_comcode', $_postdetails)) && ($_postdetails['message_comcode'] !== null) && (strlen($_postdetails['message_comcode']) < 1024 * 10/*10kb limit, for reasonable performance*/) && (array_key_exists('may_use_quick_reply', $topic_info)) && (!array_key_exists('whisper_to_member', $map))) {
            require_code('comcode_cleanup');
            $replying_to_post = comcode_censored_raw_code_access($_postdetails['message_comcode']);
            $replying_to_post_plain = ($topic_info['is_threaded'] == 0) ? '' : strip_comcode($_postdetails['message_comcode']);
            require_javascript('cns_forum');
            $onclick_call_functions = [['topicReply', $topic_info['is_threaded'], $_postdetails['id'], $_postdetails['poster_username'], $replying_to_post, $replying_to_post_plain, false]];
            $onclick_call_functions_explicit_quote = [['topicReply', 0, $_postdetails['id'], $_postdetails['poster_username'], $replying_to_post, $replying_to_post_plain, true]];
        }
        $_title = do_lang_tempcode(($topic_info['is_threaded'] == 1) ? '_REPLY' : '_QUOTE_POST');
        $_title_full = new Tempcode();
        $_title_full->attach(do_lang_tempcode(($topic_info['is_threaded'] == 1) ? 'REPLY' : 'QUOTE_POST'));
        $_title_full->attach(do_lang_tempcode('ID_NUM', strval($_postdetails['id'])));

        $buttons->attach(do_template('BUTTON_SCREEN_ITEM', ['_GUID' => '6813e0dd826933c6b69de7d558db43c8', 'REL' => 'add reply nofollow', 'IMMEDIATE' => false, 'IMG' => ($topic_info['is_threaded'] == 1) ? 'buttons/new_reply' : 'buttons/new_quote', 'TITLE' => $_title, 'FULL_TITLE' => $_title_full, 'URL' => $action_url, 'ONCLICK_CALL_FUNCTIONS' => $onclick_call_functions]));

        if ($topic_info['is_threaded'] == 1) { // Second button for replying with explicit quote
            $_title = do_lang_tempcode('_QUOTE_POST');
            $_title_full = new Tempcode();
            $_title_full->attach($_title);
            $_title_full->attach(do_lang_tempcode('ID_NUM', strval($_postdetails['id'])));
            $buttons->attach(do_template('BUTTON_SCREEN_ITEM', ['_GUID' => 'fc13d12cfe58324d78befec29a663b4f', 'REL' => 'add reply nofollow', 'IMMEDIATE' => false, 'IMG' => 'buttons/new_quote', 'TITLE' => $_title, 'FULL_TITLE' => $_title_full, 'URL' => $action_url, 'ONCLICK_CALL_FUNCTIONS' => $onclick_call_functions_explicit_quote]));
        }
    }

    // Quote to new ticket
    if ($rendering_context == 'tickets') {
        if ((array_key_exists('message_comcode', $_postdetails)) && ($_postdetails['message_comcode'] !== null)) {
            $ticket_id = get_param_string('id', null);
            if (($ticket_id !== null) && (addon_installed('tickets'))) {
                require_lang('tickets');
                require_code('tickets');
                $ticket_owner = check_ticket_access($ticket_id);

                if (($ticket_owner == get_member()) || (has_privilege(get_member(), 'support_operator'))) {
                    $_title = do_lang_tempcode('QUOTE_TO_NEW_TICKET');
                    $_title_full = new Tempcode();
                    $_title_full->attach($_title);
                    $_title_full->attach(do_lang_tempcode('ID_NUM', strval($_postdetails['id'])));

                    $action_url = build_url(['page' => 'tickets', 'type' => 'ticket', 'post_as' => ($ticket_owner == get_member()) ? null : $ticket_owner], get_module_zone('tickets'));

                    $ticket_url = ticket_url($ticket_id);
                    $quote_to_new_post = do_lang('POSTING_TICKET_AS', $GLOBALS['FORUM_DRIVER']->get_username(get_member()), $ticket_url, $_postdetails['message_comcode']);
                    $hidden = form_input_hidden('post', $quote_to_new_post);

                    $buttons->attach(do_template('BUTTON_SCREEN_ITEM', ['_GUID' => '927d758415a3358d6b69e1587cab1e8d', 'IMMEDIATE' => true, 'HIDDEN' => $hidden, 'IMG' => 'buttons/new_quote', 'TITLE' => $_title, 'FULL_TITLE' => $_title_full, 'URL' => $action_url, 'TARGET' => '_blank']));
                }
            }
        }
    }

    // Whisper
    $may_pt_members = array_key_exists('may_pt_members', $topic_info);
    $may_inline_pp = $may_reply_private_post;
    if (get_option('inline_pp_advertise') == '0') {
        $whisper_type = (get_option('inline_pp_advertise') == '0') ? 'new_pt' : 'whisper';
    } elseif (($may_inline_pp) && ($may_pt_members)) {
        $whisper_type = 'whisper';
    } elseif ($may_inline_pp) {
        $whisper_type = 'new_post';
    } elseif ($may_pt_members) {
        $whisper_type = 'new_pt';
    } else {
        $whisper_type = null;
    }
    if (
        ($whisper_type !== null) &&
        ($_postdetails['poster'] != get_member()) &&
        ($_postdetails['poster'] != $GLOBALS['CNS_DRIVER']->get_guest_id()) &&
        (cns_may_whisper($_postdetails['poster'])) &&
        (get_option('overt_whisper_suggestion') == '1')
    ) {
        $action_url = build_url(['page' => 'topics', 'type' => $whisper_type, 'id' => $_postdetails['topic_id'], 'quote' => $_postdetails['id'], 'whisper_to_member' => $_postdetails['poster']], get_module_zone('topics'));
        $_title = do_lang_tempcode('WHISPER');
        $_title_full = new Tempcode();
        $_title_full->attach($_title);
        $_title_full->attach(do_lang_tempcode('ID_NUM', strval($_postdetails['id'])));
        $buttons->attach(do_template('BUTTON_SCREEN_ITEM', ['_GUID' => 'fb1c74bae9c553dc160ade85adf289b5', 'REL' => 'add reply contact nofollow', 'IMMEDIATE' => false, 'IMG' => (get_option('inline_pp_advertise') == '0') ? 'buttons/send' : 'buttons/whisper', 'TITLE' => $_title, 'FULL_TITLE' => $_title_full, 'URL' => $action_url]));
    }

    // Edit
    if (array_key_exists('may_edit', $_postdetails)) {
        $map = ['page' => 'topics', 'type' => 'edit_post', 'id' => $_postdetails['id']];
        if ($rendering_context == 'tickets') {
            $map['redirect'] = protect_url_parameter(build_url(['page' => 'tickets', 'type' => 'ticket', 'id' => get_param_string('id')], get_module_zone('tickets'), [], false, false, false, '_top'));
        } else {
            $map['redirect'] = protect_url_parameter(SELF_REDIRECT);
        }
        $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;
        }
        $edit_url = build_url($map, get_module_zone('topics'));
        $_title = do_lang_tempcode('EDIT');
        $_title_full = do_lang_tempcode('EDIT_THIS_POST');
        $_title_full->attach($_title);
        $_title_full->attach(do_lang_tempcode('ID_NUM', strval($_postdetails['id'])));
        $buttons->attach(do_template('BUTTON_SCREEN_ITEM', ['_GUID' => 'f341cfc94b3d705437d43e89f572bff6', 'REL' => 'edit nofollow', 'IMMEDIATE' => false, 'IMG' => 'admin/edit', 'TITLE' => $_title, 'FULL_TITLE' => $_title_full, 'URL' => $edit_url]));
    }

    // Delete
    if (array_key_exists('may_delete', $_postdetails)) {
        $map = ['page' => 'topics', 'type' => 'delete_post', 'id' => $_postdetails['id']];
        if ($rendering_context == 'tickets') {
            $map['redirect'] = protect_url_parameter(build_url(['page' => 'tickets', 'type' => 'ticket', 'id' => get_param_string('id')], get_module_zone('tickets'), [], false, false, false, '_top'));
        } else {
            $map['redirect'] = protect_url_parameter($GLOBALS['FORUM_DRIVER']->topic_url($_postdetails['topic_id'], '', true));
        }
        $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;
        }
        $delete_url = build_url($map, get_module_zone('topics'));
        $_title = do_lang_tempcode('DELETE');
        $_title_full = new Tempcode();
        $_title_full->attach(do_lang_tempcode('DELETE_THIS_POST'));
        $_title_full->attach(do_lang_tempcode('ID_NUM', strval($_postdetails['id'])));
        $buttons->attach(do_template('BUTTON_SCREEN_ITEM', ['_GUID' => '8bf6d098ddc217eef75718464dc03d41', 'REL' => 'delete nofollow', 'IMMEDIATE' => false, 'IMG' => 'admin/delete3', 'TITLE' => $_title, 'FULL_TITLE' => $_title_full, 'URL' => $delete_url]));
    }

    // Report
    if ($rendering_context != 'tickets') {
        if ((array_key_exists('may_report_content', $topic_info)) && (addon_installed('tickets')) && (get_bot_type() === null)) {
            require_lang('report_content');
            $action_url = build_url(['page' => 'topics', 'type' => 'report_post', 'id' => $_postdetails['id']], get_module_zone('topics'));
            $_title = do_lang_tempcode('_REPORT_POST');
            $_title_full = new Tempcode();
            $_title_full->attach(do_lang_tempcode('REPORT_POST'));
            $_title_full->attach(do_lang_tempcode('ID_NUM', strval($_postdetails['id'])));
            $buttons->attach(do_template('BUTTON_SCREEN_ITEM', ['_GUID' => 'f81cbe84f524b4ed9e089c6e89a7c717', 'REL' => 'report nofollow', 'IMMEDIATE' => false, 'IMG' => 'buttons/report', 'TITLE' => $_title, 'FULL_TITLE' => $_title_full, 'URL' => $action_url, 'EXTRA_ATTRS' => 'data-open-as-overlay=\'{"height": "100%"}\' ']));
        }

        if ((array_key_exists('may_warn_members', $topic_info)) && ($_postdetails['poster'] != $GLOBALS['CNS_DRIVER']->get_guest_id()) && (addon_installed('cns_warnings'))) {
            $redir_url = get_self_url(true);
            $redir_url .= '#post_' . strval($_postdetails['id']);
            $action_url = build_url(['page' => 'warnings', 'type' => 'add', 'member_id' => $_postdetails['poster'], 'post_id' => $_postdetails['id'], 'redirect' => protect_url_parameter($redir_url)], get_module_zone('warnings'));
            $_title = do_lang_tempcode('__WARN_MEMBER');
            $_title_full = do_lang_tempcode('WARN_MEMBER');
            $_title_full->attach(do_lang_tempcode('ID_NUM', strval($_postdetails['id'])));
            $onmousedown_call_functions = [['spamWarning']];
            $buttons->attach(do_template('BUTTON_SCREEN_ITEM', ['_GUID' => '2698c51b06a72773ac7135bbfe791318', 'REL' => 'nofollow', 'IMMEDIATE' => false, 'IMG' => 'admin/warn', 'TITLE' => $_title, 'FULL_TITLE' => $_title_full, 'URL' => $action_url, 'ONMOUSEDOWN_CALL_FUNCTIONS' => $onmousedown_call_functions]));
        }
    }

    // Revisions
    if ($_postdetails['has_revisions']) {
        $action_url = build_url(['page' => 'admin_revisions', 'type' => 'browse', 'resource_types' => 'post', 'resource_id' => $_postdetails['id']], get_module_zone('admin_revisions'));
        $_title = do_lang_tempcode('actionlog:REVISIONS');
        $_title_full = new Tempcode();
        $_title_full->attach($_title);
        $_title_full->attach(do_lang_tempcode('ID_NUM', strval($_postdetails['id'])));
        $buttons->attach(do_template('BUTTON_SCREEN_ITEM', ['_GUID' => '6086b2ae226bf2a69d1e34641d22ae21', 'REL' => 'history nofollow', 'IMMEDIATE' => false, 'IMG' => 'admin/revisions', 'TITLE' => $_title, 'FULL_TITLE' => $_title_full, 'URL' => $action_url]));
    }

    // Send points as thanks
    if ($rendering_context != 'tickets') {
        if ((addon_installed('points')) && (!is_guest()) && (!is_guest($_postdetails['poster'])) && ($_postdetails['poster'] != get_member()) && (has_privilege($_postdetails['poster'], 'use_points')) && (has_privilege($_postdetails['poster'], 'send_points'))) {
            require_code('points');
            require_css('points');
            $action_url = points_url($_postdetails['poster']);
            $_title = do_lang_tempcode('__POINTS_THANKS');
            $_title_full = do_lang_tempcode('POINTS_THANKS');
            $buttons->attach(do_template('BUTTON_SCREEN_ITEM', ['_GUID' => 'a66f98cb4d56bd0d64e9ecc44d357141', 'IMMEDIATE' => false, 'IMG' => 'buttons/points', 'TITLE' => $_title, 'FULL_TITLE' => $_title_full, 'URL' => $action_url]));
        }
    }

    return $buttons;
}

/**
 * Get post emphasis Tempcode.
 *
 * @param  array $_postdetails Map of post info
 * @param  ?array $topic_info Map of topic info (null: not available or not running Conversr)
 * @return array A pair of emphasis class to use and the text Tempcode
 */
function cns_get_post_emphasis(array $_postdetails, ?array $topic_info) : array
{
    $emphasis = new Tempcode();
    $post_class = '';
    if ($_postdetails['is_emphasised']) {
        $emphasis = do_lang_tempcode('IMPORTANT');
        $post_class = 'cns-post-emphasis';
    } elseif (array_key_exists('whisper_to_member', $_postdetails)) {
        $pp_to_displayname = $GLOBALS['FORUM_DRIVER']->get_username($_postdetails['whisper_to_member'], true);
        $pp_to_username = $GLOBALS['FORUM_DRIVER']->get_username($_postdetails['whisper_to_member']);
        $emphasis = do_lang_tempcode('PP_TO', escape_html($pp_to_displayname), escape_html($pp_to_username));
        $post_class = 'cns-post-personal';
    } elseif (($topic_info !== null) && ($topic_info['forum_id'] === null)) {
        $emphasis = do_lang_tempcode('PRIVATE_TOPIC');
        $post_class = 'cns-post-private-topic';
    }
    return [$post_class, $emphasis];
}
