<?php /*

 Composr
 Copyright (c) Christopher Graham, 2004-2024

 See docs/LICENSE.md for full licensing information.


 NOTE TO PROGRAMMERS:
   Do not edit this file. If you need to make changes, save your changed file to the appropriate *_custom folder
   **** If you ignore this advice, then your website upgrades (e.g. for bug fixes) will likely kill your changes ****

*/

/**
 * @license    http://opensource.org/licenses/cpal_1.0 Common Public Attribution License
 * @copyright  Christopher Graham
 * @package    core_cns
 */

/**
 * Pick a default avatar for a member.
 * Does not support gravatar, that is supported by get_member_avatar_url.
 *
 * @param  EMAIL $email_address E-mail address is used to create a bias (or perhaps something else if this function is overridden)
 * @param  string $algorithm Algorithm to use (if possible). 'avoid_conflict' tries to avoid conflict with existing members. 'dynamic' uses the $email_address as a bias
 * @set avoid_conflict dynamic
 * @return URLPATH Avatar (blank: none)
 * @ignore
 */
function cns_choose_default_avatar(string $email_address = '', string $algorithm = 'avoid_conflict') : string
{
    $avatar_url = '';

    if (get_option('random_avatars', true) === '1') {
        require_code('themes2');
        $codes = get_all_image_ids_type('cns_default_avatars/default_set', false, $GLOBALS['FORUM_DB']);
        if ($algorithm == 'avoid_conflict') {
            shuffle($codes);
        }
        $results = [];
        foreach ($codes as $code) {
            if ($code == 'system') {
                continue;
            }

            if ($algorithm == 'avoid_conflict') {
                $count = @intval($GLOBALS['FORUM_DB']->query_select_value('f_members', 'SUM(m_cache_num_posts)', ['m_avatar_url' => find_theme_image($code, false, true)]));
            } else {
                $count = null;
            }
            $results[$code] = $count;
        }
        $avatar_id = null;
        if ($algorithm == 'avoid_conflict') {
            @asort($results); // @'d as type checker fails for some odd reason
            $_found_avatars = array_keys($results);
            $avatar_id = array_shift($_found_avatars);
        } else {
            $_found_avatars = array_keys($results);
            $found_avatars = count($_found_avatars);
            if ($found_avatars > 0) {
                $crc = crc32($email_address);
                $avatar_id = $_found_avatars[$crc % $found_avatars];
            }
        }
        if ($avatar_id !== null) {
            $avatar_url = find_theme_image($avatar_id, true, true);
        }
    }

    if ($avatar_url == '') {
        $GLOBALS['SITE_DB']->query_delete('theme_images', ['id' => 'cns_default_avatars/default', 'url' => '']); // In case failure cached, gets very confusing
        $avatar_url = find_theme_image('cns_default_avatars/default', true, true);
    }
    return $avatar_url;
}

/**
 * Make sure we are doing necessary join to be able to access the given field.
 *
 * @param  object $db Database connector
 * @param  array $info Content type info
 * @param  ?ID_TEXT $context Context (unused) (null: none)
 * @param  array $extra_join List of joins (passed as reference)
 * @param  ID_TEXT $filter_key The field to get
 * @param  string $field_val The field value for this
 * @param  array $db_fields Database field data
 * @param  string $table_join_code What the database will join the table with
 * @return ?array A triple: Proper database field name to access with, The fields API table type (blank: no special table), The new filter value (null: error)
 * @ignore
 */
function _members_filtercode(object $db, array $info, ?string $context, array &$extra_join, string $filter_key, string $field_val, array $db_fields, string $table_join_code) : ?array
{
    // If it's trivial
    if (($filter_key == 'id') || (preg_match('#^m_\w+$#', $filter_key) != 0)) {
        if (!array_key_exists($filter_key, $db_fields)) {
            return null;
        }
        return [$table_join_code . '.' . $filter_key, '', $field_val];
    }

    // Mostly CPFS...
    // --------------

    $join_sql = ' LEFT JOIN ' . $db->get_table_prefix() . 'f_member_custom_fields f ON f.mf_member_id=r.id';

    if (!in_array($join_sql, $extra_join)) {
        $extra_join[$filter_key] = $join_sql;
    }

    $new_filter_key = $filter_key;
    if (is_numeric($filter_key)) {
        // CPF by just ID
        $new_filter_key = 'field_' . $new_filter_key;
    } elseif ($filter_key == 'gm_group_id') {
        // Secondary usergroup membership

        $join_sql = ' LEFT JOIN ' . $db->get_table_prefix() . 'f_group_members gm ON gm.gm_member_id=r.id';

        if (!in_array($join_sql, $extra_join)) {
            $extra_join[$filter_key] = $join_sql;
        }

        return [$new_filter_key, '', $field_val];
    } elseif (preg_match('#^field_\d+$#', $filter_key) == 0) { // If it's not already correct
        // CPF by title
        require_code('cns_members');
        $cpf_id = find_cpf_field_id($filter_key);
        if ($cpf_id === null) {
            return null;
        }
        $new_filter_key = 'field_' . strval($cpf_id);
    } else {
        // CPF like field_xxx
        $new_filter_key = $filter_key;
    }

    return [$new_filter_key, '', $field_val];
}

/**
 * Get a member display box.
 *
 * @param  MEMBER $member_id Member ID
 * @param  boolean $preview Whether only to show 'preview' details
 * @param  boolean $show_avatar Whether to show the avatar
 * @param  array $extra_fields Map of extra fields to show
 * @param  boolean $give_context Whether to include context (i.e. say WHAT this is, not just show the actual content)
 * @param  ID_TEXT $guid Overridden GUID to send to templates (blank: none)
 * @return Tempcode The member box
 */
function render_member_box(int $member_id, bool $preview = false, bool $show_avatar = true, array $extra_fields = [], bool $give_context = true, string $guid = '') : object
{
    if ($member_id === null) { // Should never happen, but we need to be defensive
        return new Tempcode();
    }

    if (is_guest($member_id)) {
        return new Tempcode();
    }

    require_lang('cns');
    require_css('cns');
    require_code('cns_general');

    $need = [
        'custom_fields',
        'custom_data',
        'username',
        'posts',
        'points',
        'join_date',
        'primary_group_name',
        'secondary_groups_named',
        'online',
        'avatar',
        'ip_address',
        'num_warnings',
        'galleries',
        'dob_label',
        'dob',
    ];
    $member_info = cns_read_in_member_profile($member_id, $need, false, $preview);

    if ($member_info === null) {
        return paragraph(do_lang_tempcode('MISSING_RESOURCE'), 'j5wlj4zc0l1lffaru0e76ue3j6sflq4q', 'nothing-here');
    }

    $custom_fields = new Tempcode();
    foreach ($member_info['custom_fields'] as $name => $value) {
        if (($value !== null) && ($value !== '')) {
            if (is_integer($name)) {
                $name = strval($name);
            }
            $custom_fields->attach(do_template('CNS_MEMBER_BOX_CUSTOM_FIELD', [
                '_GUID' => ($guid != '') ? $guid : '10b72cd1ec240c315e56bc8a0f3a92a1',
                'NAME' => $name,
                'RAW' => $value['RAW'],
                'VALUE' => is_object($value['RENDERED']) ? protect_from_escaping($value['RENDERED']) : $value['RENDERED'],
                'MEMBER_ID' => strval($member_id),
            ]));
        }
    }

    foreach ($extra_fields as $key => $val) {
        if (is_integer($key)) {
            $key = strval($key);
        }

        $custom_fields->attach(do_template('CNS_MEMBER_BOX_CUSTOM_FIELD', [
            '_GUID' => ($guid != '') ? $guid : '530f049d3b3065df2d1b69270aa93491',
            'NAME' => $key,
            'RAW' => $val,
            'VALUE' => $val,
            'MEMBER_ID' => strval($member_id),
        ]));
    }

    foreach ($member_info['custom_data'] as $field_name => $field_value) {
        $custom_fields->attach(do_template('CNS_MEMBER_BOX_CUSTOM_FIELD', [
            '_GUID' => ($guid != '') ? $guid : '630f049d3b3065df2d1b69270aa93490',
            'NAME' => $field_name,
            'RAW' => $field_value,
            'VALUE' => $field_value,
            'MEMBER_ID' => strval($member_id),
        ]));
    }

    $map = [
        '_GUID' => ($guid != '') ? $guid : 'dfskfdsf9',
        'GIVE_CONTEXT' => $give_context,
        'MEMBER_ID' => strval($member_id),
        'USERNAME' => $member_info['username'],
        '_POSTS' => strval($member_info['posts']),
        'POSTS' => integer_format($member_info['posts'], 0),
        '_POINTS' => isset($member_info['points']) ? strval($member_info['points']) : '',
        'POINTS' => isset($member_info['points']) ? integer_format($member_info['points'], 0) : '',
        'JOIN_DATE_RAW' => strval($member_info['join_time']),
        'JOIN_DATE' => $member_info['join_date'],
        'PRIMARY_GROUP_NAME' => $member_info['primary_group_name'],
        'SECONDARY_GROUPS' => array_values($member_info['secondary_groups_named']),
        'CUSTOM_FIELDS' => $custom_fields,
        'ONLINE' => $member_info['online'],
        'AVATAR_URL' => ($show_avatar && isset($member_info['avatar'])) ? $member_info['avatar'] : '',
        'IP_ADDRESS' => ((has_privilege(get_member(), 'see_ip')) && (isset($member_info['ip_address']))) ? $member_info['ip_address'] : null,
        'GALLERIES' => isset($member_info['galleries']) ? $member_info['galleries'] : null,
        'DOB_LABEL' => isset($member_info['dob_label']) ? $member_info['dob_label'] : '',
        'DOB' => isset($member_info['dob']) ? $member_info['dob'] : '',
        '_DOB' => isset($member_info['_dob']) ? strval($member_info['_dob']) : '',
        '_DOB_CENSORED' => isset($member_info['_dob_censored']) ? strval($member_info['_dob_censored']) : '',
    ];
    if (addon_installed('cns_warnings')) {
        $map['_NUM_WARNINGS'] = strval($member_info['num_warnings']);
        $map['NUM_WARNINGS'] = integer_format($member_info['num_warnings'], 0);
    }
    return do_template('CNS_MEMBER_BOX', $map);
}

/**
 * Find if a certain member may be PTd by a certain member.
 *
 * @param  MEMBER $target Member to be PTd
 * @param  ?MEMBER $member_id Member to PT. (null: current member)
 * @return boolean Whether the PT may be created
 */
function cns_may_whisper(int $target, ?int $member_id = null) : bool
{
    if ($member_id === null) {
        $member_id = get_member();
    }

    if (addon_installed('cns_forum')) {
        if (get_option('enable_pt_restrict') == '0') {
            return true;
        }
    }

    if ($target == $member_id) {
        return true;
    }

    if (has_privilege($member_id, 'pt_anyone')) {
        return true;
    }
    $pt_allow = $GLOBALS['CNS_DRIVER']->get_member_row_field($target, 'm_pt_allow');
    if ($pt_allow == '*') {
        return true;
    }

    static $may_whisper_cache = [];
    $key = serialize([$target, $member_id]);
    if (array_key_exists($key, $may_whisper_cache)) {
        return $may_whisper_cache[$key];
    }

    require_code('selectcode');
    $groups = $GLOBALS['FORUM_DRIVER']->get_members_groups($member_id);
    $answer = !empty(array_intersect(selectcode_to_idlist_using_memory($pt_allow, $GLOBALS['FORUM_DRIVER']->get_usergroup_list(false, false, false, [], $member_id)), $groups));

    if ((!$answer) && (addon_installed('chat'))) {
        require_code('chat');
        if (member_befriended($member_id, $target)) {
            $answer = true;
        }
    }

    $may_whisper_cache[$key] = $answer;
    return $answer;
}

/**
 * Merge one member into another member.
 *
 * @param  MEMBER $from_id The member ID to merge from
 * @param  MEMBER $to_id The member ID to merge into
 * @param  boolean $keep Whether to keep the $from_id member (false: the member will be deleted)
 */
function cns_merge_members(int $from_id, int $to_id, bool $keep = false)
{
    if ($to_id == $from_id) {
        warn_exit(do_lang_tempcode('MERGE_SAME'));
    }

    // Do not merge guests
    if (is_guest($from_id) || is_guest($to_id)) {
        warn_exit(do_lang_tempcode('INTERNAL_ERROR', escape_html('TODO')));
    }

    $from_username = $GLOBALS['FORUM_DRIVER']->get_username($from_id);
    $to_username = $GLOBALS['FORUM_DRIVER']->get_username($to_id);

    // Reassign submitter field values
    $meta = $GLOBALS['SITE_DB']->query('SELECT m_table,m_name FROM ' . get_table_prefix() . 'db_meta WHERE ' . db_string_equal_to('m_type', 'MEMBER') . ' OR ' . db_string_equal_to('m_type', '?MEMBER') . ' OR ' . db_string_equal_to('m_type', '*MEMBER'));
    foreach ($meta as $m) {
        $db = get_db_for($m['m_table']);
        $db->query_update($m['m_table'], [$m['m_name'] => $to_id], [$m['m_name'] => $from_id], '', null, 0, false, true); // Errors suppressed in case rows conflict with existing
    }

    // Reassign poster usernames
    $GLOBALS['FORUM_DB']->query_update('f_posts', ['p_poster_name_if_guest' => $to_username], ['p_posting_member' => $from_id]);

    // Merge in post count caching
    $new_post_count = $GLOBALS['FORUM_DRIVER']->get_member_row_field($from_id, 'm_cache_num_posts') + $GLOBALS['FORUM_DRIVER']->get_member_row_field($to_id, 'm_cache_num_posts');
    $GLOBALS['FORUM_DB']->query_update('f_members', ['m_cache_num_posts' => $new_post_count], ['id' => $to_id], '', 1);

    // Reassign personal galleries
    if (addon_installed('galleries')) {
        $personal_galleries = $GLOBALS['SITE_DB']->query('SELECT name FROM ' . get_table_prefix() . 'galleries WHERE name LIKE \'member_' . strval($from_id) . '\_%\'');
        foreach ($personal_galleries as $gallery) {
            $old_gallery_name = $gallery['name'];
            $new_gallery_name = preg_replace('#^member_\d+_#', 'member_' . strval($to_id) . '_', $old_gallery_name);

            $GLOBALS['SITE_DB']->query_update('galleries', ['parent_id' => $new_gallery_name], ['parent_id' => $old_gallery_name]);
            $GLOBALS['SITE_DB']->query_update('images', ['cat' => $new_gallery_name], ['cat' => $old_gallery_name]);
            $GLOBALS['SITE_DB']->query_update('videos', ['cat' => $new_gallery_name], ['cat' => $old_gallery_name]);

            $test = $GLOBALS['SITE_DB']->query_select_value_if_there('galleries', 'name', ['name' => $new_gallery_name]);
            if ($test === null) { // Rename
                $GLOBALS['SITE_DB']->query_update('galleries', ['name' => $new_gallery_name], ['name' => $old_gallery_name], '', 1);
            } else { // Delete
                require_code('galleries2');
                delete_gallery($old_gallery_name);
            }
        }
    }

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

    // Merge in CPFs
    $custom_fields = cns_get_all_custom_fields_match_member($from_id);
    foreach ($custom_fields as $details) {
        if ($details['RAW'] != '') {
            cns_set_custom_field($to_id, intval($details['FIELD_ID']), $details['RAW']);
        }
    }

    // Delete old member
    if (!$keep) {
        cns_delete_member($from_id);
    }

    // Cache emptying ...
    cns_require_all_forum_stuff();

    require_code('cns_posts_action');
    require_code('cns_posts_action2');
    require_code('cns_topics_action2');
    require_code('cns_forums_action2');

    // Members
    cns_force_update_member_post_count($to_id);
    $num_warnings = $GLOBALS['FORUM_DB']->query_select_value('f_warnings', 'COUNT(*)', ['w_member_id' => $to_id]);
    $GLOBALS['FORUM_DB']->query_update('f_members', ['m_cache_warnings' => $num_warnings], ['id' => $to_id], '', 1);

    // Topics and posts
    require_code('cns_topics_action');
    $topics = $GLOBALS['FORUM_DB']->query_select('f_topics', ['id', 't_forum_id'], ['t_cache_first_member_id' => $from_id]);
    foreach ($topics as $topic) {
        cns_force_update_topic_caching($topic['id'], null, true, true);
    }
    $topics = $GLOBALS['FORUM_DB']->query_select('f_topics', ['id', 't_forum_id'], ['t_cache_last_member_id' => $from_id]);
    foreach ($topics as $topic) {
        cns_force_update_topic_caching($topic['id'], null, true, true);
    }

    // Forums
    require_code('cns_posts_action2');
    $forums = $GLOBALS['FORUM_DB']->query_select('f_forums', ['id'], ['f_cache_last_member_id' => $from_id]);
    foreach ($forums as $forum) {
        cns_force_update_forum_caching($forum['id']);
    }

    // ---

    log_it('MERGE_MEMBERS', $from_username, $to_username);
}
