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

/**
 * Get a config option, with override support.
 *
 * @param  ID_TEXT $option_name The config option
 * @param  ?array $overrides Map of config option codenames and overridden values (null: no overrides)
 * @return ?ID_TEXT Config option value (null: config option disabled or not found)
 */
function get_option_with_overrides(string $option_name, ?array $overrides) : ?string
{
    if ((isset($overrides[$option_name])) && ($overrides[$option_name] != '')) {
        return $overrides[$option_name];
    }
    return get_option($option_name);
}

/**
 * Find whether a member's field must be filled in.
 *
 * @param  ?MEMBER $member_id The member being edited (null: new member)
 * @param  string $field_class Special code representing what kind of field it is
 * @set email_address dob required_cpfs
 * @param  ?mixed $current_value The value the field has now (null: lookup from member record; cannot do this for a CPF)
 * @param  ?MEMBER $editing_member The member doing the adding/editing operation (null: current member)
 * @param  array $adjusted_config_options A map of adjusted config options
 * @return boolean Whether the field must be filled in
 */
function member_field_is_required(?int $member_id, string $field_class, $current_value = null, ?int $editing_member = null, array $adjusted_config_options = []) : bool
{
    if (($field_class == 'dob') && ((get_option_with_overrides('dobs', $adjusted_config_options) == '0') || ((get_option_with_overrides('dobs', $adjusted_config_options) == '1') && ($member_id === null)))) {
        return false;
    }

    // E-mail is technically not required within the software, but lost-password etc won't work without it, so we're not going to allow it to be left blank unless bypass_email_address_if_already_empty if there and it's already blank

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

    if (has_privilege($editing_member, 'bypass_' . $field_class)) {
        return false;
    }

    // Existing member, allow blank to persist if such a privilege
    if ($member_id !== null) {
        if ($current_value === null) {
            $current_value = $GLOBALS['FORUM_DRIVER']->get_member_row_field($member_id, ($field_class == 'dob') ? ('m_' . $field_class . '_day') : ('m_' . $field_class));
        }

        if (is_string($current_value)) {
            $current_value = trim($current_value);
        }
        if ((cms_empty_safe($current_value)) && (has_privilege($editing_member, 'bypass_' . $field_class . '_if_already_empty'))) {
            return false;
        }
    }

    return true;
}

/**
 * Add a member.
 *
 * @param  SHORT_TEXT $username The username
 * @param  SHORT_TEXT $password The password
 * @param  SHORT_TEXT $email_address The e-mail address
 * @param  ?GROUP $primary_group The member's primary usergroup. Note there are no security checks, so make sure anything you pass is already available for this user (null: default)
 * @param  ?array $secondary_groups A list of secondary usergroups. Note there are no security checks, so make sure anything you pass is already available for this user (null: default/current usergroups)
 * @param  ?integer $dob_day Day of date of birth (null: unknown)
 * @param  ?integer $dob_month Month of date of birth (null: unknown)
 * @param  ?integer $dob_year Year of date of birth (null: unknown)
 * @param  array $custom_fields A map of custom field values (fieldID=>value)
 * @param  ?ID_TEXT $timezone The member timezone, should not be blank (null: auto-detect)
 * @param  ?LANGUAGE_NAME $language The member's language (null: auto detect) (blank: site default)
 * @param  ID_TEXT $theme The member's default theme (blank: default)
 * @param  SHORT_TEXT $title The member's title (blank: get from primary)
 * @param  URLPATH $photo_url The URL to the member's photo (blank: none)
 * @param  ?URLPATH $avatar_url The URL to the member's avatar (blank: none) (null: choose one automatically)
 * @param  LONG_TEXT $signature The member's signature (blank: none)
 * @param  ?BINARY $preview_posts Whether posts are previewed before they are made (null: global configured default)
 * @param  BINARY $reveal_age Whether the member's age may be shown
 * @param  BINARY $views_signatures Whether the member sees signatures in posts
 * @param  ?BINARY $auto_monitor_contrib_content Whether the member automatically is enabled for notifications for content they contribute to (null: get default from config)
 * @param  ?BINARY $smart_topic_notification Whether to do smart topic notification [i.e. avoid sending so many notifications] (null: global configured default)
 * @param  ?BINARY $mailing_list_style Whether to send mailing-list style notifications (null: global configured default)
 * @param  BINARY $auto_mark_read Mark topics as read automatically
 * @param  ?BINARY $sound_enabled Whether sound is enabled (null: global configured default)
 * @param  BINARY $allow_emails Whether the member allows e-mails via the site
 * @param  BINARY $allow_emails_from_staff Whether the member allows e-mails from staff via the site
 * @param  BINARY $highlighted_name Whether the member username will be highlighted
 * @param  SHORT_TEXT $pt_allow Usergroups that may PT the member
 * @param  LONG_TEXT $pt_rules_text Rules that other members must agree to before they may start a PT with the member
 * @param  BINARY $validated Whether the account has been validated
 * @param  SHORT_TEXT $validated_email_confirm_code The code required before the account becomes active (blank: already entered)
 * @param  ?TIME $probation_expiration_time When the member is on probation until (null: not on probation)
 * @param  ID_TEXT $is_perm_banned Whether the member is permanently banned
 * @param  boolean $check_correctness Whether to check details for correctness
 * @param  ?IP $ip_address The member's IP address (blank: unknown) (null: IP address of current user)
 * @param  ?ID_TEXT $password_compatibility_scheme The compatibility scheme that the password operates in (blank: none) (null: none [meaning normal bcrypt salted style] or plain, depending on whether passwords are encrypted)
 * @param  SHORT_TEXT $salt The password salt (blank: password compatibility scheme does not use a salt / auto-generate)
 * @param  ?TIME $join_time When the member joined (null: now)
 * @param  ?TIME $last_visit_time When the member last visited (null: now)
 * @param  ?TIME $last_submit_time The time the member last made a submission (null: set to now)
 * @param  BINARY $profile_views Total number of views to the profile
 * @param  BINARY $total_sessions Total number of sessions (basically, visits)
 * @param  ?AUTO_LINK $id Force an ID (null: don't force an ID)
 * @return AUTO_LINK The ID of the new member
 */
function cns_make_member(string $username, string $password, string $email_address = '', ?int $primary_group = null, ?array $secondary_groups = null, ?int $dob_day = null, ?int $dob_month = null, ?int $dob_year = null, array $custom_fields = [], ?string $timezone = null, ?string $language = null, string $theme = '', string $title = '', string $photo_url = '', ?string $avatar_url = null, string $signature = '', ?int $preview_posts = null, int $reveal_age = 0, int $views_signatures = 1, ?int $auto_monitor_contrib_content = null, ?int $smart_topic_notification = null, ?int $mailing_list_style = null, int $auto_mark_read = 1, ?int $sound_enabled = null, int $allow_emails = 1, int $allow_emails_from_staff = 1, int $highlighted_name = 0, string $pt_allow = '*', string $pt_rules_text = '', int $validated = 1, string $validated_email_confirm_code = '', ?int $probation_expiration_time = null, string $is_perm_banned = '0', bool $check_correctness = true, ?string $ip_address = null, ?string $password_compatibility_scheme = null, string $salt = '', ?int $join_time = null, ?int $last_visit_time = null, ?int $last_submit_time = null, int $profile_views = 0, int $total_sessions = 0, ?int $id = null) : int
{
    require_code('form_templates');
    require_code('cns_members');
    require_code('cns_groups');

    if ($password_compatibility_scheme === null) {
        if (get_value('disable_password_hashing') === '1' || $password == ''/*Makes debugging easier or allows basic testing to work on PHP installs with broken OpenSSL*/) {
            $password_compatibility_scheme = 'plain';
        } else {
            $password_compatibility_scheme = '';
        }
    }
    if ((($password_compatibility_scheme == '') || ($password_compatibility_scheme == 'temporary')) && (get_value('disable_password_hashing') === '1')) {
        $password_compatibility_scheme = 'plain';
        $salt = '';
    }
    if (($salt == '') && (($password_compatibility_scheme == '') || ($password_compatibility_scheme == 'temporary'))) {
        require_code('crypt');
        $salt = get_secure_random_string();
        $password_salted = ratchet_hash($password, $salt);
    } else {
        $password_salted = $password;
    }

    if ($primary_group === null) {
        $primary_group = get_first_default_group(); // This is members
    }
    if ($secondary_groups === null) {
        $secondary_groups = cns_get_all_default_groups(false);
    }
    foreach ($secondary_groups as $_g_id => $g_id) {
        if ($g_id == $primary_group) {
            unset($secondary_groups[$_g_id]);
        }
    }

    if ($timezone === null) {
        $timezone = get_site_timezone();
    }
    if ($language === null) {
        $language = user_lang();
    }

    if ($avatar_url === null) {
        if (($GLOBALS['IN_MINIKERNEL_VERSION']) || (!addon_installed('cns_member_avatars')) || (get_option('gravatars') == '1') || (running_script('stress_test_loader'))) {
            $avatar_url = '';
        } else {
            require_code('cns_members2');
            $avatar_url = cns_choose_default_avatar($email_address);
        }
    }

    $preview_posts = (get_option('default_preview_guests') == '1') ? 1 : 0;
    if ($auto_monitor_contrib_content === null) {
        $auto_monitor_contrib_content = (get_option('allow_auto_notifications') == '0') ? 0 : 1;
    }
    if ($smart_topic_notification === null) {
        $smart_topic_notification = (get_option('smart_topic_notification_default') == '1') ? 1 : 0;
    }
    if ($mailing_list_style === null) {
        $mailing_list_style = (get_option('mailing_list_style_default') == '1') ? 1 : 0;
    }
    if ($sound_enabled === null) {
        $sound_enabled = ((addon_installed('cns_forum')) && (get_option('sound_enabled_default') == '1')) ? 1 : 0;
    }

    $doing_email_option = (get_option('member_email_receipt_configurability') == '2') && (addon_installed('cns_contact_member'));
    if (!$doing_email_option) {
        $allow_emails = 1;
    }
    $doing_email_from_staff_option = (get_option('staff_email_receipt_configurability') == '2');
    if (!$doing_email_from_staff_option) {
        $allow_emails_from_staff = 1;
    }

    if ($check_correctness) {
        if (!in_array($password_compatibility_scheme, ['ldap', 'httpauth'])) {
            require_code('cns_members_action2');
            require_code('temporal');
            cns_check_name_valid($username, null, ($password_compatibility_scheme == '') ? $password : null, $email_address, ($dob_year === null) ? null : cms_mktime(12, 0, 0, $dob_month, $dob_day, $dob_year));
        }
        if ((!function_exists('has_actual_page_access')) || (!has_actual_page_access(get_member(), 'admin_cns_members'))) {
            require_code('type_sanitisation');
            if ((!is_valid_email_address($email_address)) && ($email_address != '')) {
                warn_exit(do_lang_tempcode('_INVALID_EMAIL_ADDRESS', escape_html($email_address)));
            }
        }

        if ((get_option('one_per_email_address') != '0') && ($email_address != '')) {
            $test = $GLOBALS['FORUM_DB']->query_select_value_if_there('f_members', 'id', ['m_email_address' => $email_address]);
            if ($test !== null) {
                warn_exit(do_lang_tempcode('_EMAIL_ADDRESS_IN_USE'));
            }
        }
    }

    if ($ip_address === null) {
        $ip_address = get_ip_address();
    }

    if ($join_time === null) {
        $join_time = time();
    }
    if ($last_visit_time === null) {
        $last_visit_time = time();
    }
    if ($last_submit_time === null) {
        $last_submit_time = time();
    }

    // Supplement custom field values given with defaults, and check constraints
    $all_fields = list_to_map('id', cns_get_all_custom_fields_match(array_merge([$primary_group], $secondary_groups)));
    require_code('fields');
    foreach ($all_fields as $field) {
        $field_id = $field['id'];

        if (array_key_exists($field_id, $custom_fields)) {
            if (($check_correctness) && ($field[array_key_exists('cf_show_on_join_form', $field) ? 'cf_show_on_join_form' : 'cf_required'] == 0) && ($field['cf_owner_set'] == 0) && (!has_actual_page_access(get_member(), 'admin_cns_members'))) {
                access_denied('I_ERROR');
            }
        }
    }

    if (!addon_installed('validation')) {
        $validated = 1;
    }
    $map = [
        'm_username' => $username,
        'm_pass_hash_salted' => $password_salted,
        'm_password_compat_scheme' => $password_compatibility_scheme,
        'm_pass_salt' => $salt,
        'm_password_change_code' => '',
        'm_password_change_code_time' => null,
        'm_login_key_hash' => '',
        'm_email_address' => $email_address,
        'm_primary_group' => $primary_group,
        'm_dob_day' => $dob_day,
        'm_dob_month' => $dob_month,
        'm_dob_year' => $dob_year,
        'm_timezone_offset' => $timezone,
        'm_language' => $language,
        'm_theme' => $theme,
        'm_title' => $title,
        'm_photo_url' => $photo_url,
        'm_avatar_url' => $avatar_url,
        'm_preview_posts' => $preview_posts,
        'm_reveal_age' => $reveal_age,
        'm_views_signatures' => $views_signatures,
        'm_auto_monitor_contrib_content' => $auto_monitor_contrib_content,
        'm_smart_topic_notification' => $smart_topic_notification,
        'm_mailing_list_style' => $mailing_list_style,
        'm_auto_mark_read' => $auto_mark_read,
        'm_sound_enabled' => $sound_enabled,
        'm_allow_emails' => $allow_emails,
        'm_allow_emails_from_staff' => $allow_emails_from_staff,
        'm_highlighted_name' => $highlighted_name,
        'm_pt_allow' => $pt_allow,
        'm_validated' => $validated,
        'm_validated_email_confirm_code' => $validated_email_confirm_code,
        'm_probation_expiration_time' => $probation_expiration_time,
        'm_is_perm_banned' => $is_perm_banned,
        'm_ip_address' => $ip_address,
        'm_join_time' => $join_time,
        'm_last_visit_time' => $last_visit_time,
        'm_last_submit_time' => $last_submit_time,
        'm_profile_views' => $profile_views,
        'm_total_sessions' => $total_sessions,
        'm_cache_num_posts' => 0,
        'm_cache_warnings' => 0,
        'm_max_email_attach_size_mb' => 5,
    ];
    $map += insert_lang_comcode('m_signature', $signature, 4, $GLOBALS['FORUM_DB']);
    $map += insert_lang_comcode('m_pt_rules_text', $pt_rules_text, 4, $GLOBALS['FORUM_DB']);
    if ($id !== null) {
        $map['id'] = $id;
    }
    $member_id = $GLOBALS['FORUM_DB']->query_insert('f_members', $map, true);

    if ($check_correctness) {
        // If it was an invite/recommendation, award the referrer and set a friendship
        $tracking_codes = find_session_tracking_codes($email_address);
        foreach ($tracking_codes as $tracking_code) {
            if (is_numeric($tracking_code)) {
                $referrer = intval($tracking_code);

                if ((addon_installed('points')) && (addon_installed('recommend'))) {
                    $points_recommend_site = intval(get_option('points_RECOMMEND_SITE'));
                    if ($points_recommend_site > 0) {
                        require_code('points2');
                        require_lang('recommend');
                        points_credit_member($referrer, do_lang('RECOMMEND_SITE_TO', $username, get_site_name()), $points_recommend_site, 0, true, 0, 'recommend_site');
                    }
                }
                if (addon_installed('chat')) {
                    require_code('chat2');
                    friend_add($referrer, $member_id, null, false);
                    friend_add($member_id, $referrer, null, false);
                }

                break; // We only pick the first
            }
        }
    }

    // Make member custom fields
    _cns_make_member_cpfs($member_id, $custom_fields, $all_fields);

    // Any secondary work...

    foreach ($secondary_groups as $g) {
        if ($g != $primary_group) {
            $GLOBALS['FORUM_DB']->query_delete('f_group_members', ['gm_member_id' => $member_id, 'gm_group_id' => $g], '', 1);
            $GLOBALS['FORUM_DB']->query_insert('f_group_members', [
                'gm_group_id' => $g,
                'gm_member_id' => $member_id,
            ]);
        }
    }

    if (!get_mass_import_mode()) {
        $GLOBALS['FORUM_DB']->query_insert('f_group_join_log', [
            'member_id' => $member_id,
            'usergroup_id' => $primary_group,
            'join_time' => time()
        ]);
    }

    if ($check_correctness) {
        if (function_exists('delete_cache_entry')) {
            delete_cache_entry('side_stats');
        }
        delete_value('cns_newest_member_id');
        delete_value('cns_newest_member_username');
    }

    if ((addon_installed('commandr')) && (!running_script('install')) && (!get_mass_import_mode())) {
        require_code('resource_fs');
        generate_resource_fs_moniker('member', strval($member_id), null, null, true);
    }

    $password_change_days = get_option('password_change_days');
    if ($password_change_days == '') { // Issue when installing, option will not be set
        $password_change_days = '0';
    }
    if (intval($password_change_days) > 0) {
        if ($password_compatibility_scheme == '') {
            require_code('password_rules');
            bump_password_change_date($member_id, $password, $password_salted, $salt);
        }
    }

    // Copy notification defaults
    if (!$GLOBALS['IN_MINIKERNEL_VERSION']) {
        push_db_scope_check(false);
        $notification_settings = $GLOBALS['FORUM_DB']->query_select('notifications_enabled', ['*'], ['l_member_id' => $GLOBALS['FORUM_DRIVER']->get_guest_id(), 'l_code_category' => '']);
        foreach ($notification_settings as $notification_row) {
            unset($notification_row['id']);
            unset($notification_row['l_member_id']);
            $GLOBALS['FORUM_DB']->query_insert('notifications_enabled', ['l_member_id' => $member_id] + $notification_row);
        }
        push_db_scope_check(true);
    }

    // Award joining points
    if (addon_installed('points')) {
        $points_joining = intval(get_option('points_joining'));
        if ($points_joining > 0) {
            require_code('points2');
            points_credit_member($member_id, do_lang('JOINING'), $points_joining, 0, null, 0, 'member', 'join');
        }
    }

    require_code('member_mentions');
    dispatch_member_mention_notifications('member', strval($member_id));

    if (function_exists('delete_cache_entry')) {
        delete_cache_entry('main_members');
    }

    if (!get_mass_import_mode()) {
        set_value('cns_member_count', strval(intval(get_value('cns_member_count')) + 1));
    }

    if ($validated == 1) {
        require_code('sitemap_xml');
        notify_sitemap_node_add('_SEARCH:members:view:' . strval($member_id), $join_time, null, SITEMAP_IMPORTANCE_LOW, 'monthly', true);
    }

    return $member_id;
}

/**
 * Make a member's custom fields row.
 * This function assumes the row does not yet exist.
 *
 * @param  MEMBER $member_id The member to which we are making the CPFs
 * @param  array $custom_fields A map of custom field values (fieldID=>value)
 * @param  array $all_fields A map of all member fields
 * @ignore
 */
function _cns_make_member_cpfs(int $member_id, array $custom_fields, array $all_fields)
{
    require_code('fields');

    $value = null;

    // Store custom fields
    $row = [];
    $all_fields_types = collapse_2d_complexity('id', 'cf_type', $all_fields);
    foreach ($custom_fields as $field_num => $value) {
        if (!array_key_exists($field_num, $all_fields_types)) {
            continue; // Trying to set a field we're not allowed to (doesn't apply to our group)
        }

        $row['field_' . strval($field_num)] = $value;
    }

    // Set custom field row
    $all_fields_regardless = $GLOBALS['FORUM_DB']->query_select('f_custom_fields', ['id', 'cf_type', 'cf_default', 'cf_required']);
    foreach ($all_fields_regardless as $field) {
        $ob = get_fields_hook($field['cf_type']);
        list(, $default, $storage_type) = $ob->get_field_value_row_bits($field, $field['cf_required'] == 1, $field['cf_default'], $GLOBALS['FORUM_DB']);

        if (array_key_exists('field_' . strval($field['id']), $row)) {
            $value = $row['field_' . strval($field['id'])];
        } else {
            $value = $default;
        }

        $row['field_' . strval($field['id'])] = $value;
        if (is_string($value)) { // Should not normally be needed, but the grabbing from cf_default further up is not converted yet
            switch ($storage_type) {
                case 'short_trans':
                case 'long_trans':
                    $row = insert_lang_comcode('field_' . strval($field['id']), $value, 3, $GLOBALS['FORUM_DB']) + $row;
                    break;
                case 'integer':
                    $row['field_' . strval($field['id'])] = ($value == '') ? null : intval($value);
                    break;
                case 'float':
                    $row['field_' . strval($field['id'])] = ($value == '') ? null : floatval($value);
                    break;
            }
        }
    }
    $GLOBALS['FORUM_DB']->query_insert('f_member_custom_fields', ['mf_member_id' => $member_id] + $row);

    require_code('locations_cpfs');
    autofill_geo_cpfs($member_id);
}

/**
 * Find how to store a field in the database.
 *
 * @param  ID_TEXT $type The field type
 * @param  BINARY $encrypted Whether the field is encrypted
 * @param  string $__default The default value to use
 * @return array A tuple: the DB field type, the default (in correct data type)
 */
function get_cpf_storage_for(string $type, int $encrypted = 0, string $__default = '') : array
{
    $default = null;

    require_code('fields');
    $ob = get_fields_hook($type);
    list(, $_default, $storage_type) = $ob->get_field_value_row_bits(['id' => null, 'cf_type' => $type, 'cf_default' => $__default], false, $__default);
    $db_type = ($encrypted == 1) ? 'LONG_TEXT' : 'SHORT_TEXT';
    switch ($storage_type) {
        case 'short_trans':
            $db_type = 'SHORT_TRANS__COMCODE';
            $default = $_default;
            break;
        case 'long_trans':
            $db_type = 'LONG_TRANS__COMCODE';
            $default = $_default;
            break;
        case 'long':
            $db_type = 'LONG_TEXT';
            $default = $_default;
            break;
        case 'integer':
            $db_type = '?INTEGER';
            $default = ($_default == '') ? null : intval($_default);
            break;
        case 'float':
            $db_type = '?REAL';
            $default = ($_default == '') ? null : floatval($_default);
            break;
        default:
            $default = $_default;
            break;
    }

    return [$db_type, $default];
}

/**
 * Make a Custom Profile Field.
 *
 * @param  SHORT_TEXT $name Name of the field
 * @param  BINARY $locked Whether the field is locked (i.e. cannot be deleted from the system).
 * @param  SHORT_TEXT $description Description of the field
 * @param  LONG_TEXT $default The default value for the field
 * @param  BINARY $public_view Whether the field is publicly viewable
 * @param  BINARY $owner_view Whether the field is viewable by the owner
 * @param  BINARY $owner_set Whether the field may be set by the owner
 * @param  BINARY $encrypted Whether the field is encrypted
 * @param  ID_TEXT $type The type of the field
 * @set short_text long_text short_trans long_trans integer upload picture url list tick float
 * @param  BINARY $required Whether it is required that every member have this field filled in
 * @param  BINARY $show_in_posts Whether this field is shown in posts and places where member details are highlighted (such as an image in a member gallery)
 * @param  BINARY $show_in_post_previews Whether this field is shown in preview places, such as in the forum member tooltip
 * @param  ?integer $order The order of this field relative to other fields (null: next)
 * @param  LONG_TEXT $only_group The usergroups that this field is confined to (comma-separated list)
 * @param  BINARY $show_on_join_form Whether the field is to be shown on the join form
 * @param  SHORT_TEXT $options Field options
 * @param  BINARY $include_in_main_search Whether to include in main keyword search
 * @param  BINARY $allow_template_search Whether to allow template search
 * @param  ID_TEXT $icon An icon to show the CPF with on the member profiles
 * @param  ID_TEXT $section A section to show with on the member-links part of member profiles
 * @param  LONG_TEXT $tempcode This is Tempcode that is used for displaying the field. See the DESCRIPTION_CPF_CODE language string.
 * @param  boolean $no_name_dupe Whether to check that no field has this name already
 * @param  ID_TEXT $autofill_type Autofill field name from https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill-field
 * @param  ID_TEXT $autofill_hint Autofill hint: '' or 'shipping' or 'billing'
 * @return AUTO_LINK The ID of the new Custom Profile Field
 */
function cns_make_custom_field(string $name, int $locked = 0, string $description = '', string $default = '', int $public_view = 0, int $owner_view = 0, int $owner_set = 0, int $encrypted = 0, string $type = 'long_text', int $required = 0, int $show_in_posts = 0, int $show_in_post_previews = 0, ?int $order = null, string $only_group = '', int $show_on_join_form = 0, string $options = '', int $include_in_main_search = 0, int $allow_template_search = 0, string $icon = '', string $section = '', string $tempcode = '', bool $no_name_dupe = false, string $autofill_type = '', string $autofill_hint = '') : int
{
    require_code('global4');
    prevent_double_submit('ADD_CUSTOM_PROFILE_FIELD', null, $name);

    if ($only_group == '-1') {
        $only_group = '';
    }

    // Can't have publicly-viewable encrypted fields
    require_code('encryption');
    if ($encrypted == 1) {
        $public_view = 0;
    }

    if ($no_name_dupe) {
        $test = $GLOBALS['FORUM_DB']->query_select_value_if_there('f_custom_fields', 'id', [$GLOBALS['FORUM_DB']->translate_field_ref('cf_name') => $name]);
        if ($test !== null) {
            pop_db_scope_check();
            return $test;
        }
    }

    if ($order === null) {
        $order = $GLOBALS['FORUM_DB']->query_select_value('f_custom_fields', 'MAX(cf_order)');
        if ($order === null) {
            $order = 0;
        } else {
            $order++;
        }
    }

    $map = [
        'cf_locked' => $locked,
        'cf_default' => $default,
        'cf_public_view' => $public_view,
        'cf_owner_view' => $owner_view,
        'cf_owner_set' => $owner_set,
        'cf_type' => $type,
        'cf_required' => $required,
        'cf_show_in_posts' => $show_in_posts,
        'cf_show_in_post_previews' => $show_in_post_previews,
        'cf_order' => $order,
        'cf_only_group' => $only_group,
        'cf_show_on_join_form' => $show_on_join_form,
    ];

    // LEGACY
    $_version_database = get_value('ocf_version');
    if ($_version_database === null) {
        $_version_database = get_value('cns_version');
    }
    if (running_script('install') || intval($_version_database) >= 10) {
        $map['cf_options'] = $options;
    }
    if (running_script('install') || running_script('upgrader') || intval($_version_database) >= 11) {
        $map += [
            'cf_include_in_main_search' => $include_in_main_search,
            'cf_allow_template_search' => $allow_template_search,
            'cf_icon' => $icon,
            'cf_section' => $section,
            'cf_tempcode' => $tempcode,
            'cf_autofill_type' => $autofill_type,
            'cf_autofill_hint' => $autofill_hint,
        ];
    }

    if (substr($name, 0, 4) == 'cms_') {
        require_code('lang3');
        $map += lang_code_to_static_content('cf_name', $name, false, 2, $GLOBALS['FORUM_DB']);
    } else {
        $map += insert_lang('cf_name', $name, 2, $GLOBALS['FORUM_DB']);
    }
    $map += insert_lang('cf_description', $description, 2, $GLOBALS['FORUM_DB']);
    $id = $GLOBALS['FORUM_DB']->query_insert('f_custom_fields', $map + ['cf_encrypted' => $encrypted], true);

    list($db_type, $_default) = get_cpf_storage_for($type, $encrypted, $default);

    $GLOBALS['FORUM_DB']->add_table_field('f_member_custom_fields', 'field_' . strval($id), $db_type, $_default);

    log_it('ADD_CUSTOM_PROFILE_FIELD', strval($id), $name);

    if ((addon_installed('commandr')) && (!running_script('install')) && (!get_mass_import_mode())) {
        require_code('resource_fs');
        generate_resource_fs_moniker('cpf', strval($id), null, null, true);
    }

    if (function_exists('delete_cache_entry')) {
        delete_cache_entry('main_members');
    }

    if (function_exists('persistent_cache_delete')) {
        persistent_cache_delete('CUSTOM_FIELD_CACHE');
        persistent_cache_delete('LIST_CPFS');
    }

    build_cpf_indices($id, $include_in_main_search == 1 || $allow_template_search == 1, $type, $db_type); // Should be done last, in case of timeout

    return $id;
}

/**
 * Make Custom Profile Field indices.
 *
 * @param  AUTO_LINK $id CPF ID
 * @param  boolean $index Whether an index is needed for search purposes (there may be other reasons though)
 * @param  ID_TEXT $type CPF type
 * @param  ID_TEXT $db_type Underlying field type
 * @param  boolean $immediate Whether to create the indexes immediately (as opposed to doing it in a task)
 * @return boolean If the operation could succeed (false means limit hit)
 */
function build_cpf_indices(int $id, bool $index, string $type, string $db_type, bool $immediate = false) : bool
{
    $GLOBALS['FORUM_DB']->delete_index_if_exists('f_member_custom_fields', 'field_' . strval($id)); // LEGACY

    if ($index) {
        $create_regular_index = (strpos($db_type, 'LONG_') === false) && ((strpos($db_type, '_TRANS') === false) || (multi_lang_content()/*we need to be able to do efficient joins if any searching will happen via the translate table*/));
        $create_fulltext_index = (strpos($db_type, '_TEXT') !== false) || ($db_type == 'TEXT'); // Note we never create on a _TRANS field because the translate field already has fulltext and _helper_create_table automatically creates fulltext indexes on any _TRANS field if multi_lang_content is off

        if ($create_regular_index || $create_fulltext_index) {
            $indexes_remaining = $GLOBALS['SITE_DB']->driver->get_max_indexes();

            if ($indexes_remaining !== null) {
                $indexes_remaining--; // One for the primary key

                // Consider how many indexes currently exist
                $indexes_remaining -= $GLOBALS['FORUM_DB']->query_select_value('db_meta_indices', 'COUNT(*)', ['i_table' => 'f_member_custom_fields']);
                if (!running_script('install')) {
                    // Consider what is currently queued
                    $indexes_remaining -= $GLOBALS['FORUM_DB']->query_select_value('task_queue', 'COUNT(*)', ['t_hook' => 'create_index']);
                }
            }

            $space_for_indexes = ($indexes_remaining === null) || ($indexes_remaining >= (($create_regular_index && $create_fulltext_index) ? 2 : 1));

            if ($create_regular_index) {
                if ($space_for_indexes) {
                    if (running_script('install') || $immediate) {
                        $GLOBALS['FORUM_DB']->create_index('f_member_custom_fields', 'mcf' . strval($id), ['field_' . strval($id)], 'mf_member_id');
                    } else {
                        require_code('tasks');
                        call_user_func_array__long_task(do_lang('CREATE_INDEX'), null, 'create_index', ['FORUM_DB', ['f_member_custom_fields', 'mcf' . strval($id), ['field_' . strval($id)], 'mf_member_id']], false, $immediate, false);
                    }
                    if ($indexes_remaining !== null) {
                        $indexes_remaining--;
                    }
                }
            } else {
                $GLOBALS['FORUM_DB']->delete_index_if_exists('f_member_custom_fields', 'mcf' . strval($id));
            }

            if ($create_fulltext_index) {
                if ($space_for_indexes) {
                    if (running_script('install') || $immediate) {
                        $GLOBALS['FORUM_DB']->create_index('f_member_custom_fields', '#mcf_ft_' . strval($id), ['field_' . strval($id)]);
                    } else {
                        require_code('tasks');
                        call_user_func_array__long_task(do_lang('CREATE_INDEX'), null, 'create_index', ['FORUM_DB', ['f_member_custom_fields', '#mcf_ft_' . strval($id), ['field_' . strval($id)], 'mf_member_id']], false, $immediate, false);
                    }
                    if ($indexes_remaining !== null) {
                        $indexes_remaining--;
                    }
                }
            } else {
                $GLOBALS['FORUM_DB']->delete_index_if_exists('f_member_custom_fields', '#mcf_ft_' . strval($id));
            }

            if (!$space_for_indexes) {
                return false;
            }
        }
    } else {
        $GLOBALS['FORUM_DB']->delete_index_if_exists('f_member_custom_fields', 'mcf' . strval($id));
        $GLOBALS['FORUM_DB']->delete_index_if_exists('f_member_custom_fields', '#mcf_ft_' . strval($id));
    }

    return true;
}
