<?php /*

 Composr
 Copyright (c) ocProducts, 2004-2016

 See text/EN/licence.txt for full licencing 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  ocProducts Ltd
 * @package    core_cns
 */

/**
 * Standard code module initialisation function.
 *
 * @ignore
 */
function init__cns_members_action2()
{
    global $ANY_FIELD_ENCRYPTED;
    $ANY_FIELD_ENCRYPTED = null;
}

/**
 * Get extended field mapping data for CSV import/export.
 *
 * @return array Triple: headings, CPFs, subscription type data
 */
function member_get_csv_headings_extended()
{
    // Read CPFs
    $cpfs = list_to_map('id', $GLOBALS['FORUM_DB']->query_select('f_custom_fields', array('id', 'cf_type', 'cf_name'), null, 'ORDER BY cf_order,' . $GLOBALS['FORUM_DB']->translate_field_ref('cf_name')));

    // Headings
    $headings = member_get_csv_headings();
    foreach ($cpfs as $i => $c) { // CPFs take precedence over normal fields of the same name
        $cpfs[$i]['_cf_name'] = get_translated_text($c['cf_name'], $GLOBALS['FORUM_DB']);
        $headings[$cpfs[$i]['_cf_name']] = strval($i); // We specially recognise numeric names as a map back to a CPF ID
    }

    // Subscription types
    $subscription_types = array();
    if (addon_installed('ecommerce')) {
        require_lang('ecommerce');

        $usergroup_subscription_rows = $GLOBALS['FORUM_DB']->query_select('f_usergroup_subs', array('id', 's_title'));
        foreach ($usergroup_subscription_rows as $usergroup_subscription_row) {
            $item_name = get_translated_text($usergroup_subscription_row['s_title'], $GLOBALS['FORUM_DB']);
            $heading_lang_strings = array(
                'SUBSCRIPTION_START_TIME',
                'SUBSCRIPTION_TERM_START_TIME',
                'SUBSCRIPTION_TERM_END_TIME',
                'SUBSCRIPTION_EXPIRY_TIME',
                'PAYMENT_GATEWAY',
                'STATUS',
            );
            foreach ($heading_lang_strings as $heading_lang_string) {
                $headings[$item_name . ' (' . do_lang($heading_lang_string) . ')'] = ':' . str_replace('/', '\\', $item_name . ' (' . do_lang($heading_lang_string) . ')'); // Forward slashes are assumed as delimiters
            }
            $subscription_types['USERGROUP' . strval($usergroup_subscription_row['id'])] = $item_name;
        }
    }

    return array($headings, $cpfs, $subscription_types);
}

/**
 * Get field mapping data for CSV import/export.
 *
 * @return array A map of heading information (human name to field name/encoding details)
 */
function member_get_csv_headings()
{
    $headings = array(
        'ID' => 'id',
        'Username' => 'm_username',
        'E-mail address' => 'm_email_address',
        'Password' => 'm_pass_hash_salted/m_pass_salt/m_password_compat_scheme'
    );
    if (addon_installed('cns_member_avatars')) {
        $headings += array(
            'Avatar' => '#m_avatar_url',
        );
    }
    if (addon_installed('cns_member_photos')) {
        $headings += array(
            'Photo' => '#m_photo_url',
        );
    }
    $headings += array(
        'Signature' => '*m_signature',
        'Validated' => '!m_validated',
        'Join time' => '&m_join_time',
        'Last visit' => '&m_last_visit_time',
        'Number of posts' => 'm_cache_num_posts',
        'Usergroup' => '@m_primary_group',
        'Banned' => '!m_is_perm_banned',
        'Date of birth' => 'm_dob_year/m_dob_month/m_dob_day',
        'Reveal age' => '!m_reveal_age',
        'Language' => 'm_language',
        'Accept member e-mails' => '!m_allow_emails',
        'Opt-in' => '!m_allow_emails_from_staff',
        'Auto mark read' => 'm_auto_mark_read',
    );
    return $headings;
}

/**
 * Get a list of timezones.
 *
 * @param  ?string $timezone Current timezone to select (null: server default)
 * @return Tempcode List of timezones
 */
function create_selection_list_timezone_list($timezone = null)
{
    if (is_null($timezone)) {
        $timezone = get_site_timezone();
    }

    $timezone_list = '';//new Tempcode();
    $time_now = time();
    foreach (get_timezone_list() as $_timezone => $timezone_nice) {
        $timezone_list .= '<option ' . (($timezone == $_timezone) ? 'selected="selected" ' : '') . 'value="' . escape_html($_timezone) . '">' . escape_html($timezone_nice) . '</option>'; // XHTMLXHTML
        //$timezone_list->attach(do_template('CNS_AUTO_TIME_ZONE_ENTRY', array('_GUID' => '2aed8a9fcccb52e5d52b5a307a906b3a', 'HOUR' => date('H', tz_time($time_now, $_timezone)), 'DW' => date('w', tz_time(time(), $_timezone)), 'NAME' => $_timezone, 'SELECTED' => ($timezone == $_timezone), 'CLASS' => '', 'TEXT' => $timezone_nice)));
    }
    return make_string_tempcode($timezone_list);
}

/**
 * Validate an IP address, indirectly by passing through a confirmation code.
 */
function approve_ip_script()
{
    require_code('site');
    attach_to_screen_header('<meta name="robots" content="noindex" />'); // XHTMLXHTML

    $keep = keep_symbol(array('1'));

    $code = either_param_string('code', '');
    if ($code == '') {
        $title = get_screen_title('CONFIRM');
        require_code('form_templates');
        $fields = new Tempcode();
        $fields->attach(form_input_codename(do_lang_tempcode('CODE'), '', 'code', '', true));
        $submit_name = do_lang_tempcode('PROCEED');
        $url = find_script('approve_ip') . $keep;
        $middle = do_template('FORM_SCREEN', array('_GUID' => 'd92ce4ec82dc709f920a4ce6760778de', 'TITLE' => $title, 'SKIP_WEBSTANDARDS' => true, 'HIDDEN' => '', 'URL' => $url, 'FIELDS' => $fields, 'TEXT' => do_lang_tempcode('MISSING_CONFIRM_CODE'), 'SUBMIT_ICON' => 'buttons__proceed', 'SUBMIT_NAME' => $submit_name));
        $echo = globalise($middle, null, '', true, true);
        $echo->evaluate_echo();
        exit();
    }

    // If we're still here, we're ok to go
    require_lang('cns');
    $test = $GLOBALS['FORUM_DB']->query_select_value_if_there('f_member_known_login_ips', 'i_val_code', array('i_val_code' => $code));
    if (is_null($test)) {
        warn_exit(do_lang_tempcode('ALREADY_VALIDATED'));
    }
    $GLOBALS['FORUM_DB']->query_update('f_member_known_login_ips', array('i_val_code' => ''), array('i_val_code' => $code), '', 1);

    $title = get_screen_title('CONFIRM');
    $middle = redirect_screen($title, get_base_url() . $keep, do_lang_tempcode('SUCCESS'));
    $echo = globalise($middle, null, '', true, true);
    $echo->evaluate_echo();
    exit();
}

/**
 * If we are using human names for usernames, a conflict is likely. Store a suffixed variety. Maybe later Composr will strip these suffixes out in some contexts.
 *
 * @param  SHORT_TEXT $username The desired human name for the member profile.
 * @return SHORT_TEXT A unique username.
 */
function get_username_from_human_name($username)
{
    $username = preg_replace('# \(\d+\)$#', '', $username);
    $_username = $username;
    $i = 1;
    do {
        $test = $GLOBALS['FORUM_DB']->query_select_value_if_there('f_members', 'id', array('m_username' => $_username));
        if (!is_null($test)) {
            $i++;
            $_username = $username . ' (' . strval($i) . ')';
        }
    } while (!is_null($test));
    $username = $_username;
    return $username;
}

/**
 * Get a form for finishing off a member profile (such as for LDAP or httpauth, where a partial profile is automatically made, but needs completion).
 *
 * @param  SHORT_TEXT $username The username for the member profile.
 * @param  ID_TEXT $type The type of member profile we are finishing off.
 * @param  string $email_address Auto-detected e-mail address (blank: none)
 * @param  ?integer $dob_day Auto-detected DOB day (null: unknown)
 * @param  ?integer $dob_month Auto-detected DOB month (null: unknown)
 * @param  ?integer $dob_year Auto-detected DOB year (null: unknown)
 * @param  ?ID_TEXT $timezone Auto-detected Timezone (null: unknown)
 * @param  ?ID_TEXT $language Auto-detected Language (null: unknown)
 * @return Tempcode The form.
 */
function cns_member_external_linker_ask($username, $type, $email_address = '', $dob_day = null, $dob_month = null, $dob_year = null, $timezone = null, $language = null)
{
    require_lang('cns');

    // If somehow, we're not fully started up, or in a messy state
    require_code('urls');

    cms_ob_end_clean(); // Emergency output, potentially, so kill off any active buffer

    $title = get_screen_title('FINISH_PROFILE');

    if ($username != '') {
        $username = get_username_from_human_name($username);
    }

    list($fields, $hidden) = cns_get_member_fields(true, null, null, $email_address, 1, $dob_day, $dob_month, $dob_year, $timezone, null, null, 1, 0, null, $language, 1, 1, 1, null, $username, 0, $type);
    $hidden->attach(build_keep_post_fields());
    $hidden->attach(form_input_hidden('finishing_profile', '1'));

    $text = do_lang_tempcode('ENTER_PROFILE_DETAILS_FINISH');

    $submit_name = do_lang_tempcode('PROCEED');
    $url = get_self_url();

    return do_template('FORM_SCREEN', array('_GUID' => 'f3fa74f4842f3660f0831f8d708d256d', 'HIDDEN' => $hidden, 'TITLE' => $title, 'FIELDS' => $fields, 'TEXT' => $text, 'SUBMIT_ICON' => 'menu__site_meta__user_actions__join', 'SUBMIT_NAME' => $submit_name, 'URL' => $url));
}

/**
 * Finishing off of a member profile (such as for LDAP or httpauth, where a partial profile is automatically made, but needs completion).
 *
 * @param  SHORT_TEXT $username The username for the member profile.
 * @param  SHORT_TEXT $password The password for the member profile.
 * @param  ID_TEXT $type The type of member profile we are finishing off.
 * @param  boolean $email_check Whether to check for duplicated email addresses.
 * @param  string $email_address Auto-detected e-mail address (blank: none)
 * @param  ?integer $dob_day Auto-detected DOB day (null: unknown)
 * @param  ?integer $dob_month Auto-detected DOB month (null: unknown)
 * @param  ?integer $dob_year Auto-detected DOB year (null: unknown)
 * @param  ?ID_TEXT $timezone Auto-detected Timezone (null: unknown)
 * @param  ?ID_TEXT $language Auto-detected Language (null: unknown)
 * @param  ?URLPATH $avatar_url The URL to the member's avatar (blank: none) (null: choose one automatically).
 * @param  URLPATH $photo_url The URL to the member's photo (blank: none).
 * @param  URLPATH $photo_thumb_url The URL to the member's photo thumbnail (blank: none).
 * @return MEMBER The member ID for the finished off profile.
 */
function cns_member_external_linker($username, $password, $type, $email_check = true, $email_address = '', $dob_day = null, $dob_month = null, $dob_year = null, $timezone = null, $language = null, $avatar_url = null, $photo_url = '', $photo_thumb_url = '')
{
    // Read in data
    $email_address = trim(post_param_string('email_address', $email_address));
    require_code('temporal2');
    list($dob_year, $dob_month, $dob_day) = post_param_date_components('dob', $dob_year, $dob_month, $dob_day);
    $reveal_age = post_param_integer('reveal_age', 0); // For default privacy, default off
    require_code('temporal');
    if (is_null($timezone)) {
        $timezone = get_site_timezone();
    }
    $timezone = post_param_string('timezone', $timezone);
    if (is_null($language)) {
        $language = get_site_default_lang();
    }
    $language = post_param_string('language', $language);
    $allow_emails = post_param_integer('allow_emails', 0); // For default privacy, default off
    $allow_emails_from_staff = post_param_integer('allow_emails_from_staff', 0); // For default privacy, default off
    require_code('cns_groups');
    $custom_fields = cns_get_all_custom_fields_match(
        cns_get_all_default_groups(true), // groups
        null, // public view
        null, // owner view
        null, // owner set
        null, // required
        null, // show in posts
        null, // show in post previews
        null, // special start
        true // show on join form
    );
    $actual_custom_fields = cns_read_in_custom_fields($custom_fields);
    foreach ($actual_custom_fields as $key => $val) {
        if ($val == STRING_MAGIC_NULL) {
            $actual_custom_fields[$key] = '';
        }
    }
    $groups = cns_get_all_default_groups(true); // $groups will contain the built in default primary group too (it is not $secondary_groups)
    $primary_group = post_param_integer('primary_group', null);
    if (($primary_group !== null) && (!in_array($primary_group, $groups)/*= not built in default, which is automatically ok to join without extra security*/)) {
        // Check security
        $test = $GLOBALS['FORUM_DB']->query_select_value('f_groups', 'g_is_presented_at_install', array('id' => $primary_group));
        if ($test == 1) {
            $groups = cns_get_all_default_groups(false); // Get it so it does not include the built in default primary group
            $groups[] = $primary_group; // And add in the *chosen* primary group
        } else {
            $primary_group = null;
        }
    } else {
        $primary_group = null;
    }
    if ($primary_group === null) { // Security error, or built in default (which will already be in $groups)
        $primary_group = get_first_default_group();
    }

    // Check that the given address isn't already used (if one_per_email_address on)
    if ((get_option('one_per_email_address') != '0') && ($email_address != '') && ($email_check)) {
        $test = $GLOBALS['FORUM_DB']->query_select_value_if_there('f_members', 'm_username', array('m_email_address' => $email_address));
        if (!is_null($test)) {
            global $MEMBER_CACHED;
            $MEMBER_CACHED = db_get_first_id();
            $reset_url = build_url(array('page' => 'lost_password', 'email_address' => $email_address), get_module_zone('lost_password'));
            warn_exit(do_lang_tempcode('EMAIL_ADDRESS_IN_USE', escape_html(get_site_name()), escape_html($reset_url->evaluate())));
        }
    }

    $require_new_member_validation = get_option('require_new_member_validation') == '1';
    $validated = $require_new_member_validation ? 0 : 1;
    if ($require_new_member_validation) {
        require_code('site');
        attach_message(do_lang_tempcode('AWAITING_MEMBER_VALIDATION'), 'notice');
    }

    // Add member
    require_code('cns_members_action');
    $ret = cns_make_member($username, $password, $email_address, $groups, $dob_day, $dob_month, $dob_year, $actual_custom_fields, $timezone, null, $validated, time(), time(), '', $avatar_url, '', 0, 1, $reveal_age, '', $photo_url, $photo_thumb_url, 1, 1, $language, $allow_emails, $allow_emails_from_staff, get_ip_address(), '', false, $type, '');
    return $ret;
}

/**
 * Read in the custom profile field POST data.
 *
 * @param  array $custom_fields The CPF field rows that we'll be reading in the member's values for.
 * @param  ?MEMBER $member_id Member involved (null: new member)
 * @return array The CPF data.
 */
function cns_read_in_custom_fields($custom_fields, $member_id = null)
{
    require_code('fields');
    require_code('cns_members_action');

    $actual_custom_fields = array();
    foreach ($custom_fields as $custom_field) {
        $ob = get_fields_hook($custom_field['cf_type']);

        $old_value = is_null($member_id) ? null : $GLOBALS['FORUM_DB']->query_select_value_if_there('f_member_custom_fields', 'field_' . strval($custom_field['id']), array('mf_member_id' => $member_id));

        // Field not required if not yet filled in but member already registered, if PRIVILEGE ON for that. Prevents annoyance for new required CPFs added later.
        if (!member_field_is_required($member_id, 'required_cpfs', $old_value)) {
            $custom_field['cf_required'] = 0;
        }

        $value = $ob->inputted_to_field_value($member_id !== null, $custom_field, 'uploads/cns_cpf_upload', ($old_value === null) ? null : array('cv_value' => $old_value));
        if ((fractional_edit()) && ($value != STRING_MAGIC_NULL)) {
            $rendered = $ob->render_field_value($custom_field, $value, 0, null, 'f_member_custom_fields', $member_id, 'ce_id', 'cf_id', 'field_' . strval($custom_field['id']), $member_id);
            $_POST['field_' . strval($custom_field['id']) . '__altered_rendered_output'] = is_object($rendered) ? $rendered->evaluate() : $rendered;
        }
        $actual_custom_fields[$custom_field['id']] = $value;
    }
    return $actual_custom_fields;
}

/**
 * Get form fields for adding/editing/finishing a member profile.
 *
 * @param  boolean $mini_mode Whether we are only handling the essential details of a profile.
 * @param  ?MEMBER $member_id The ID of the member we are handling (null: new member).
 * @param  ?array $groups A list of usergroups (null: default/current usergroups).
 * @param  SHORT_TEXT $email_address The e-mail address.
 * @param  BINARY $preview_posts Whether posts are previewed before they are made.
 * @param  ?integer $dob_day Day of date of birth (null: not known).
 * @param  ?integer $dob_month Month of date of birth (null: not known).
 * @param  ?integer $dob_year Year of date of birth (null: not known).
 * @param  ?ID_TEXT $timezone The member timezone (null: site default).
 * @param  ?array $custom_fields A map of custom fields values (field-id=>value) (null: not known).
 * @param  ?ID_TEXT $theme The members default theme (null: not known).
 * @param  BINARY $reveal_age Whether the members 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  ?LANGUAGE_NAME $language The members language (null: auto detect).
 * @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 $validated Whether the profile has been validated.
 * @param  ?GROUP $primary_group The members primary (null: not known).
 * @param  SHORT_TEXT $username The username.
 * @param  BINARY $is_perm_banned Whether the member is permanently banned.
 * @param  ID_TEXT $special_type The special type of profile this is (blank: not a special type).
 * @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  ?TIME $on_probation_until When the member is on probation until (null: just finished probation / or effectively was never on it)
 * @return array A pair: The form fields, Hidden fields (both Tempcode).
 */
function cns_get_member_fields($mini_mode = true, $member_id = null, $groups = null, $email_address = '', $preview_posts = 0, $dob_day = null, $dob_month = null, $dob_year = null, $timezone = null, $custom_fields = null, $theme = null, $reveal_age = 1, $views_signatures = 1, $auto_monitor_contrib_content = null, $language = null, $allow_emails = 1, $allow_emails_from_staff = 1, $validated = 1, $primary_group = null, $username = '', $is_perm_banned = 0, $special_type = '', $highlighted_name = 0, $pt_allow = '*', $pt_rules_text = '', $on_probation_until = null)
{
    $fields = new Tempcode();
    $hidden = new Tempcode();

    list($_fields, $_hidden) = cns_get_member_fields_settings($mini_mode, $member_id, $groups, $email_address, $preview_posts, $dob_day, $dob_month, $dob_year, $timezone, $theme, $reveal_age, $views_signatures, $auto_monitor_contrib_content, $language, $allow_emails, $allow_emails_from_staff, $validated, $primary_group, $username, $is_perm_banned, $special_type, $highlighted_name, $pt_allow, $pt_rules_text, $on_probation_until);
    $fields->attach($_fields);
    $hidden->attach($_hidden);

    if (!$mini_mode) {
        $fields->attach(do_template('FORM_SCREEN_FIELD_SPACER', array(
            '_GUID' => '14205f6bf83c469a1404d24967d7b6f6',
            'TITLE' => do_lang_tempcode('PROFILE'),
            'SECTION_HIDDEN' => (get_page_name() == 'admin_cns_members'),
        )));
    }

    list($_fields, $_hidden) = cns_get_member_fields_profile($mini_mode, $member_id, $groups, $custom_fields);
    $fields->attach($_fields);
    $hidden->attach($_hidden);

    return array($fields, $hidden);
}

/**
 * Get form fields for adding/editing/finishing a member profile.
 *
 * @param  boolean $mini_mode Whether we are only handling the essential details of a profile.
 * @param  ?MEMBER $member_id The ID of the member we are handling (null: new member).
 * @param  ?array $groups A list of usergroups (null: default/current usergroups).
 * @param  SHORT_TEXT $email_address The e-mail address.
 * @param  ?BINARY $preview_posts Whether posts are previewed before they are made (null: calculate statistically).
 * @param  ?integer $dob_day Day of date of birth (null: not known).
 * @param  ?integer $dob_month Month of date of birth (null: not known).
 * @param  ?integer $dob_year Year of date of birth (null: not known).
 * @param  ?ID_TEXT $timezone The member timezone (null: site default).
 * @param  ?ID_TEXT $theme The members default theme (null: not known).
 * @param  BINARY $reveal_age Whether the members 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  ?LANGUAGE_NAME $language The members language (null: auto detect).
 * @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 $validated Whether the profile has been validated.
 * @param  ?GROUP $primary_group The members primary (null: not known).
 * @param  SHORT_TEXT $username The username.
 * @param  BINARY $is_perm_banned Whether the member is permanently banned.
 * @param  ID_TEXT $special_type The special type of profile this is (blank: not a special type).
 * @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  ?TIME $on_probation_until When the member is on probation until (null: just finished probation / or effectively was never on it)
 * @return array A pair: The form fields, Hidden fields (both Tempcode).
 */
function cns_get_member_fields_settings($mini_mode = true, $member_id = null, $groups = null, $email_address = '', $preview_posts = null, $dob_day = null, $dob_month = null, $dob_year = null, $timezone = null, $theme = null, $reveal_age = 1, $views_signatures = 1, $auto_monitor_contrib_content = null, $language = null, $allow_emails = 1, $allow_emails_from_staff = 1, $validated = 1, $primary_group = null, $username = '', $is_perm_banned = 0, $special_type = '', $highlighted_name = 0, $pt_allow = '*', $pt_rules_text = '', $on_probation_until = null)
{
    require_code('form_templates');
    require_code('cns_members_action');

    $preview_posts = take_param_int_modeavg($preview_posts, 'm_preview_posts', 'f_members', 0);

    require_code('cns_field_editability');

    if (is_null($auto_monitor_contrib_content)) {
        $auto_monitor_contrib_content = (get_option('allow_auto_notifications') == '0') ? 0 : 1;
    }

    $hidden = new Tempcode();

    if ($member_id === $GLOBALS['CNS_DRIVER']->get_guest_id()) {
        fatal_exit(do_lang_tempcode('INTERNAL_ERROR'));
    }
    require_code('form_templates');
    require_code('encryption');
    if (($special_type == '') && ($member_id !== null)) {
        $special_type = get_member_special_type($member_id);
    }

    if (is_null($groups)) {
        $groups = is_null($member_id) ? cns_get_all_default_groups(true) : $GLOBALS['CNS_DRIVER']->get_members_groups($member_id);
    }

    $fields = new Tempcode();

    // Username
    if (cns_field_editable('username', $special_type)) {
        if ((is_null($member_id)) || (has_actual_page_access(get_member(), 'admin_cns_members')) || (has_privilege($member_id, 'rename_self'))) {
            $prohibit_username_whitespace = get_option('prohibit_username_whitespace');
            if ($prohibit_username_whitespace == '1') {
                $pattern = '[^\s]*';
                $pattern_error = do_lang('USERNAME_PASSWORD_WHITESPACE');
            } else {
                $pattern = null;
                $pattern_error = null;
            }
            $fields->attach(form_input_line(do_lang_tempcode('USERNAME'), do_lang_tempcode('DESCRIPTION_USERNAME'), ($member_id === null) ? 'username' : 'edit_username', $username, true, null, null, 'text', null, $pattern, $pattern_error));
        }
    }

    // Password
    if (cns_field_editable('password', $special_type)) {
        if ((is_null($member_id)) || ($member_id == get_member()) || (has_privilege(get_member(), 'assume_any_member'))) {
            $temporary_password = (!is_null($member_id)) && ($member_id == get_member() && ($GLOBALS['FORUM_DRIVER']->get_member_row_field($member_id, 'm_password_compat_scheme') == 'temporary'));
            if ($temporary_password) {
                $password_field_description = do_lang_tempcode('DESCRIPTION_PASSWORD_TEMPORARY');
            } else {
                $password_field_description = do_lang_tempcode('DESCRIPTION_PASSWORD' . (!is_null($member_id) ? '_EDIT' : ''));
            }
            $fields->attach(form_input_password(do_lang_tempcode(is_null($member_id) ? 'PASSWORD' : 'NEW_PASSWORD'), $password_field_description, is_null($member_id) ? 'password' : 'edit_password', $mini_mode || $temporary_password));
            $fields->attach(form_input_password(do_lang_tempcode('CONFIRM_PASSWORD'), '', 'password_confirm', $mini_mode || $temporary_password));
        }
    }

    // Work out what options we need to present
    $doing_international = (get_option('allow_international') == '1');
    $_langs = find_all_langs();
    $doing_langs = multi_lang();
    $doing_email_option = (get_option('allow_email_disable') == '1') && (addon_installed('cns_contact_member'));
    $doing_email_from_staff_option = (get_option('allow_email_from_staff_disable') == '1');
    $unspecced_theme_zone_exists = $GLOBALS['SITE_DB']->query_value_if_there('SELECT COUNT(*) FROM ' . get_table_prefix() . 'zones WHERE ' . db_string_equal_to('zone_theme', '') . ' OR ' . db_string_equal_to('zone_theme', '-1'));
    $doing_theme_option = ($unspecced_theme_zone_exists != 0) && (!$mini_mode);
    $doing_local_forum_options = (addon_installed('cns_forum')) && (!$mini_mode);

    // E-mail address
    if (cns_field_editable('email', $special_type)) {
        if ($email_address == '') {
            $email_address = trim(get_param_string('email_address', ''));
        }
        $email_description = new Tempcode();
        if ((get_option('valid_email_domains') != '') && ($mini_mode)) { // domain restriction only applies on public join form ($mini_mode)
            $email_description = do_lang_tempcode('MUST_BE_EMAIL_DOMAIN', '<kbd>*.' . preg_replace('#\s*,\s*#', '</kbd>, <kbd>*.', escape_html(get_option('valid_email_domains'))) . '</kbd>', escape_html(get_option('valid_email_domains')));
        } else {
            if (get_option('email_confirm_join') == '1') {
                $email_description = do_lang_tempcode('MUST_BE_REAL_ADDRESS');
            }
        }

        $email_address_required = member_field_is_required($member_id, 'email_address');

        $fields->attach(form_input_email(do_lang_tempcode('EMAIL_ADDRESS'), $email_description, 'email_address', $email_address, $email_address_required));
        if ((is_null($member_id)) && ($email_address == '') && (get_option('email_confirm_join') == '1')) {
            $fields->attach(form_input_email(do_lang_tempcode('CONFIRM_EMAIL_ADDRESS'), '', 'email_address_confirm', '', $email_address_required));
        }
    }

    // E-mail privacy
    if ($doing_email_option) {
        $field_title = do_lang_tempcode('ALLOW_EMAILS');
        if (cns_field_editable('email', $special_type)) {
            $field_title = do_lang_tempcode('RELATED_FIELD', $field_title);
        }
        $fields->attach(form_input_tick($field_title, do_lang_tempcode('DESCRIPTION_ALLOW_EMAILS'), 'allow_emails', $allow_emails == 1));
    }
    if ($doing_email_from_staff_option) {
        $field_title = do_lang_tempcode('ALLOW_EMAILS_FROM_STAFF');
        if (cns_field_editable('email', $special_type)) {
            $field_title = do_lang_tempcode('RELATED_FIELD', $field_title);
        }
        $fields->attach(form_input_tick($field_title, do_lang_tempcode('DESCRIPTION_ALLOW_EMAILS_FROM_STAFF'), 'allow_emails_from_staff', $allow_emails_from_staff == 1));
    }

    // DOB
    if (cns_field_editable('dob', $special_type)) {
        $default_time = is_null($dob_month) ? null : usertime_to_utctime(mktime(0, 0, 0, $dob_month, $dob_day, $dob_year));
        if (get_option('dobs') == '1') {
            $dob_required = member_field_is_required($member_id, 'dob');
            $fields->attach(form_input_date(do_lang_tempcode($dob_required ? 'DATE_OF_BIRTH' : 'ENTER_YOUR_BIRTHDAY'), '', 'dob', $dob_required, false, false, $default_time, -130));
            if (addon_installed('cns_forum')) {
                $fields->attach(form_input_tick(do_lang_tempcode('RELATED_FIELD', do_lang_tempcode('REVEAL_AGE')), do_lang_tempcode('DESCRIPTION_REVEAL_AGE'), 'reveal_age', $reveal_age == 1));
            }
        }
    }

    /*
    if (!$mini_mode) {
        if (($doing_international) || ($doing_langs) || ($doing_email_option) || ($doing_wide_option) || ($doing_theme_option) || ($doing_local_forum_options)) {
            $fields->attach(do_template('FORM_SCREEN_FIELD_SPACER', array('_GUID' => '3cd79bbea084ec1fe148edddad7d52b4', 'FORCE_OPEN' => is_null($member_id) ? true : null, 'TITLE' => do_lang_tempcode('SETTINGS'))));
        }
    }
    */

    require_lang('config');

    // Timezones, if enabled
    if ($doing_international) {
        $timezone_list = create_selection_list_timezone_list($timezone);
        $fields->attach(form_input_list(do_lang_tempcode('TIMEZONE'), do_lang_tempcode('DESCRIPTION_TIMEZONE_MEMBER'), 'timezone', $timezone_list));
    }

    // Language choice, if we have multiple languages on site
    if ($doing_langs) {
        $lang_list = new Tempcode();
        $no_lang_set = (is_null($language)) || ($language == '');
        $allow_no_lang_set = (get_value('allow_no_lang_selection') === '1');
        if ($allow_no_lang_set) {
            $lang_list->attach(form_input_list_entry('', $no_lang_set, do_lang_tempcode('UNSET')));
        } else {
            if ($no_lang_set) {
                $language = user_lang();
            }
        }
        $lang_list->attach(create_selection_list_langs($language));
        $fields->attach(form_input_list(do_lang_tempcode('LANGUAGE'), '', 'language', $lang_list, null, false, !$allow_no_lang_set));
    }

    if (!$mini_mode) {
        // Theme, if we have any zones giving a choice
        require_code('themes2');
        $entries = create_selection_list_themes($theme, false, false, 'RELY_SITE_DEFAULT');
        require_lang('themes');
        if ($doing_theme_option) {
            $fields->attach(form_input_list(do_lang_tempcode('THEME'), do_lang_tempcode('DESCRIPTION_THEME'), 'theme', $entries));
        }

        // Various forum options
        if (addon_installed('cns_forum')) {
            if (get_option('forced_preview_option') == '1') {
                $fields->attach(form_input_tick(do_lang_tempcode('PREVIEW_POSTS'), do_lang_tempcode('DESCRIPTION_PREVIEW_POSTS'), 'preview_posts', $preview_posts == 1));
            }
            if (addon_installed('cns_signatures')) {
                if (get_option('enable_views_sigs_option', true) === '1') {
                    $fields->attach(form_input_tick(do_lang_tempcode('VIEWS_SIGNATURES'), do_lang_tempcode('DESCRIPTION_VIEWS_SIGNATURES'), 'views_signatures', $views_signatures == 1));
                } else {
                    $hidden->attach(form_input_hidden('views_signatures', '1'));
                }
            }
            //$fields->attach(form_input_tick(do_lang_tempcode('AUTO_NOTIFICATION_CONTRIB_CONTENT'), do_lang_tempcode('DESCRIPTION_AUTO_NOTIFICATION_CONTRIB_CONTENT'), 'auto_monitor_contrib_content', $auto_monitor_contrib_content == 1));  Now on notifications tab, even though it is technically an account setting
            $usergroup_list = new Tempcode();
            $lgroups = $GLOBALS['CNS_DRIVER']->get_usergroup_list(true, true, false, null, null, true);
            foreach ($lgroups as $key => $val) {
                if ($key != db_get_first_id()) {
                    $usergroup_list->attach(form_input_list_entry(strval($key), ($pt_allow == '*') || count(array_intersect(array(strval($key)), explode(',', $pt_allow))) != 0, $val));
                }
            }
            if (get_option('enable_pt_restrict') == '1') {
                $fields->attach(do_template('FORM_SCREEN_FIELD_SPACER', array('_GUID' => '7e5deb351a7a5214fbff10049839e258', 'TITLE' => do_lang_tempcode('PRIVATE_TOPICS'), 'SECTION_HIDDEN' => ($pt_allow == '*') && ($pt_rules_text == ''))));
                $fields->attach(form_input_multi_list(do_lang_tempcode('PT_ALLOW'), addon_installed('chat') ? do_lang_tempcode('PT_ALLOW_DESCRIPTION_CHAT') : do_lang_tempcode('PT_ALLOW_DESCRIPTION'), 'pt_allow', $usergroup_list));
                $fields->attach(form_input_text_comcode(do_lang_tempcode('PT_RULES_TEXT'), do_lang_tempcode('PT_RULES_TEXT_DESCRIPTION'), 'pt_rules_text', $pt_rules_text, false));
            }

            if (get_option('is_on_automatic_mark_topic_read') == '0') {
                require_code('users');
                $auto_mark_read = $GLOBALS['FORUM_DRIVER']->get_member_row_field($member_id, 'm_auto_mark_read');

                $fields->attach(form_input_tick(do_lang_tempcode('ENABLE_AUTO_MARK_READ'), do_lang_tempcode('DESCRIPTION_ENABLE_AUTO_MARK_READ'), 'auto_mark_read', $auto_mark_read == 1));
            } else {
                $hidden->attach(form_input_hidden('auto_mark_read', '1'));
            }
        }

        // Prepare list of usergroups, if maybe we are gonna let (a) usergroup-change field(s)
        $group_count = $GLOBALS['FORUM_DB']->query_select_value('f_groups', 'COUNT(*)');
        $rows = $GLOBALS['FORUM_DB']->query_select('f_groups', array('id', 'g_name', 'g_hidden', 'g_open_membership'), ($group_count > 200) ? array('g_is_private_club' => 0) : null, 'ORDER BY g_order,' . $GLOBALS['FORUM_DB']->translate_field_ref('g_name'));
        $_groups = new Tempcode();
        $default_primary_group = get_first_default_group();
        $current_primary_group = null;
        foreach ($rows as $group) {
            if ($group['id'] != db_get_first_id()) {
                $selected = ($group['id'] == $primary_group) || (is_null($primary_group) && ($group['id'] == $default_primary_group));
                if ($selected) {
                    $current_primary_group = $group['id'];
                }
                $_groups->attach(form_input_list_entry(strval($group['id']), $selected, get_translated_text($group['g_name'], $GLOBALS['FORUM_DB'])));
            }
        }

        // Some admin options...
        if (has_privilege(get_member(), 'member_maintenance')) {
            $fields->attach(do_template('FORM_SCREEN_FIELD_SPACER', array('_GUID' => '04422238c372edd0b11c11a05feb6267', 'TITLE' => do_lang_tempcode('MEMBER_ACCESS'))));

            // Probation
            if (has_privilege(get_member(), 'probate_members')) {
                if ((!is_null($member_id)) && ($member_id != get_member())) { // Can't put someone new on probation, and can't put yourself on probation
                    $fields->attach(form_input_date(do_lang_tempcode('ON_PROBATION_UNTIL'), do_lang_tempcode('DESCRIPTION_ON_PROBATION_UNTIL'), 'on_probation_until', false, is_null($on_probation_until) || $on_probation_until <= time(), true, $on_probation_until, 2));
                }
            }

            // Primary usergroup
            if (cns_field_editable('primary_group', $special_type)) {
                if (has_privilege(get_member(), 'assume_any_member')) {
                    if ((is_null($member_id)) || (!$GLOBALS['FORUM_DRIVER']->is_super_admin($member_id)) || (count($GLOBALS['FORUM_DRIVER']->member_group_query($GLOBALS['FORUM_DRIVER']->get_super_admin_groups(), 2)) > 1)) {
                        $fields->attach(form_input_list(do_lang_tempcode('PRIMARY_GROUP'), do_lang_tempcode('DESCRIPTION_PRIMARY_GROUP'), 'primary_group', $_groups));
                    }
                }
            }
        }

        // Secondary usergroups
        if (cns_field_editable('secondary_groups', $special_type)) {
            $_groups2 = new Tempcode();
            $members_groups = is_null($member_id) ? array() : cns_get_members_groups($member_id, false, false, false);
            foreach ($rows as $group) {
                if (($group['g_hidden'] == 1) && (!array_key_exists($group['id'], $members_groups)) && (!has_privilege(get_member(), 'see_hidden_groups'))) {
                    continue;
                }

                if (($group['id'] != db_get_first_id()) && ($group['id'] != $current_primary_group) && ((array_key_exists($group['id'], $members_groups)) || (has_privilege(get_member(), 'assume_any_member')) || ($group['g_open_membership'] == 1))) {
                    $selected = array_key_exists($group['id'], $members_groups);
                    $_groups2->attach(form_input_list_entry(strval($group['id']), $selected, get_translated_text($group['g_name'], $GLOBALS['FORUM_DB'])));
                }
            }
            $sec_url = build_url(array('page' => 'groups', 'type' => 'browse'), get_module_zone('groups'));
            if (!$_groups2->is_empty()) {
                $fields->attach(form_input_multi_list(do_lang_tempcode('SECONDARY_GROUP_MEMBERSHIP'), do_lang_tempcode('DESCRIPTION_SECONDARY_GROUP', escape_html($sec_url->evaluate())), 'secondary_groups', $_groups2));
            }
        }

        // Special admin options
        if (has_privilege(get_member(), 'member_maintenance')) {
            if ($validated == 0) {
                $validated = get_param_integer('validated', 0);
                if (($validated == 1) && (addon_installed('unvalidated'))) {
                    attach_message(do_lang_tempcode('WILL_BE_VALIDATED_WHEN_SAVING'));
                }
            }
            if (addon_installed('unvalidated')) {
                $fields->attach(form_input_tick(do_lang_tempcode('VALIDATED'), do_lang_tempcode('DESCRIPTION_MEMBER_VALIDATED'), 'validated', $validated == 1));
            }
            if (get_option('enable_highlight_name') == '1') {
                $fields->attach(form_input_tick(do_lang_tempcode('HIGHLIGHTED_NAME'), do_lang_tempcode(addon_installed('pointstore') ? 'DESCRIPTION_HIGHLIGHTED_NAME_P' : 'DESCRIPTION_HIGHLIGHTED_NAME'), 'highlighted_name', $highlighted_name == 1));
            }
            if ((!is_null($member_id)) && ($member_id != get_member())) {// Can't ban someone new, and can't ban yourself
                $fields->attach(form_input_tick(do_lang_tempcode('BANNED'), do_lang_tempcode('DESCRIPTION_MEMBER_BANNED'), 'is_perm_banned', $is_perm_banned == 1));
            }
        }

        if (addon_installed('content_reviews')) {
            require_code('content_reviews2');
            $fields->attach(content_review_get_fields('member', is_null($member_id) ? null : strval($member_id)));
        }
    }

    return array($fields, $hidden);
}

/**
 * Get form fields for adding/editing/finishing a member profile.
 *
 * @param  boolean $mini_mode Whether we are only handling the essential details of a profile.
 * @param  ?MEMBER $member_id The ID of the member we are handling (null: new member).
 * @param  ?array $groups A list of usergroups (null: default/current usergroups).
 * @param  ?array $custom_fields A map of custom fields values (field-id=>value) (null: not known).
 * @return array A pair: The form fields, Hidden fields (both Tempcode).
 */
function cns_get_member_fields_profile($mini_mode = true, $member_id = null, $groups = null, $custom_fields = null)
{
    require_code('cns_members_action');

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

    if (is_null($groups)) {
        $groups = is_null($member_id) ? cns_get_all_default_groups(true) : $GLOBALS['CNS_DRIVER']->get_members_groups($member_id);
    }

    $_custom_fields = cns_get_all_custom_fields_match(
        $groups, // groups
        ($mini_mode || (is_null($member_id)) || ($member_id == get_member()) || (has_privilege(get_member(), 'view_any_profile_field'))) ? null : 1, // public view
        null, // owner view
        ($mini_mode || (is_null($member_id)) || ($member_id != get_member()) || (has_privilege(get_member(), 'view_any_profile_field'))) ? null : 1, // owner set
        null, // required
        null, // show in posts
        null, // show in post previews
        null, // special start
        $mini_mode ? true : null // show on join form
    );
    $GLOBALS['NO_DEV_MODE_FULLSTOP_CHECK'] = true;
    $field_groups = array();
    require_code('fields');
    require_code('encryption');
    foreach ($_custom_fields as $custom_field) {
        $ob = get_fields_hook($custom_field['cf_type']);
        list(, , $storage_type) = $ob->get_field_value_row_bits($custom_field);

        $existing_field = (!is_null($custom_fields)) && (array_key_exists($custom_field['trans_name'], $custom_fields));
        if ($existing_field) {
            $value = $custom_fields[$custom_field['trans_name']]['RAW'];

            if (($custom_field['cf_encrypted'] == 1) && (is_encryption_enabled())) {
                $value = remove_magic_encryption_marker($value);
            }

            if (!member_field_is_required($member_id, 'required_cpfs', $value) && $custom_field['cf_type'] != 'tick'/*HACKHACK*/) {
                $custom_field['cf_required'] = 0;
            }
        } else {
            $value = $custom_field['cf_default'];

            if (!member_field_is_required($member_id, 'required_cpfs', '')) {
                $custom_field['cf_required'] = 0;
            }
        }

        $result = mixed();

        $_description = escape_html(get_translated_text($custom_field['cf_description'], $GLOBALS['FORUM_DB']));
        $field_cat = '';
        $matches = array();
        if (strpos($custom_field['trans_name'], ': ') !== false) {
            $field_cat = substr($custom_field['trans_name'], 0, strpos($custom_field['trans_name'], ': '));
            if ($field_cat . ': ' == $custom_field['trans_name']) {
                $custom_field['trans_name'] = $field_cat; // Just been pulled out as heading, nothing after ": "
            } else {
                $custom_field['trans_name'] = substr($custom_field['trans_name'], strpos($custom_field['trans_name'], ': ') + 2);
            }
        } elseif (preg_match('#(^\([A-Z][^\)]*\) )|( \([A-Z][^\)]*\)$)#', $custom_field['trans_name'], $matches) != 0) {
            $field_cat = trim($matches[0], '() ');
            $custom_field['trans_name'] = str_replace($matches[0], '', $custom_field['trans_name']);
        }

        $result = $ob->get_field_inputter($custom_field['trans_name'], $_description, $custom_field, $value, !$existing_field);

        if (!array_key_exists($field_cat, $field_groups)) {
            $field_groups[$field_cat] = new Tempcode();
        }

        if (is_array($result)) {
            $field_groups[$field_cat]->attach($result[0]);
            $hidden->attach($result[1]);
        } else {
            $field_groups[$field_cat]->attach($result);
        }
    }
    if (array_key_exists('', $field_groups)) { // Blank prefix must go first
        $field_groups_blank = $field_groups[''];
        unset($field_groups['']);
        $field_groups = array_merge(array($field_groups_blank), $field_groups);
    }
    foreach ($field_groups as $field_group_title => $extra_fields) {
        if (is_integer($field_group_title)) {
            $field_group_title = ($field_group_title == 0) ? '' : strval($field_group_title);
        }

        if ($field_group_title != '') {
            $fields->attach(do_template('FORM_SCREEN_FIELD_SPACER', array(
                '_GUID' => 'af91e3c040a0a18a4d9cc1143c0d2007',
                'TITLE' => $field_group_title,
                'SECTION_HIDDEN' => (get_page_name() == 'admin_cns_members'),
            )));
        }
        $fields->attach($extra_fields);
    }
    $GLOBALS['NO_DEV_MODE_FULLSTOP_CHECK'] = false;

    return array($fields, $hidden);
}

/**
 * Edit a member.
 *
 * @param  AUTO_LINK $member_id The ID of the member.
 * @param  ?SHORT_TEXT $email_address The e-mail address. (null: don't change)
 * @param  ?BINARY $preview_posts Whether posts are previewed before they are made. (null: don't change)
 * @param  ?integer $dob_day Day of date of birth. (null: don't change) (-1: deset)
 * @param  ?integer $dob_month Month of date of birth. (null: don't change) (-1: deset)
 * @param  ?integer $dob_year Year of date of birth. (null: don't change) (-1: deset)
 * @param  ?ID_TEXT $timezone The member timezone. (null: don't change)
 * @param  ?GROUP $primary_group The members primary (null: don't change).
 * @param  array $custom_fields A map of custom fields values (field-id=>value).
 * @param  ?ID_TEXT $theme The members default theme. (null: don't change)
 * @param  ?BINARY $reveal_age Whether the members age may be shown. (null: don't change)
 * @param  ?BINARY $views_signatures Whether the member sees signatures in posts. (null: don't change)
 * @param  ?BINARY $auto_monitor_contrib_content Whether the member automatically is enabled for notifications for content they contribute to. (null: don't change)
 * @param  ?LANGUAGE_NAME $language The members language. (null: don't change)
 * @param  ?BINARY $allow_emails Whether the member allows e-mails via the site. (null: don't change)
 * @param  ?BINARY $allow_emails_from_staff Whether the member allows e-mails from staff via the site. (null: don't change)
 * @param  ?BINARY $validated Whether the profile has been validated (null: do not change this). (null: don't change)
 * @param  ?string $username The username. (null: don't change)
 * @param  ?string $password The password. (null: don't change)
 * @param  ?BINARY $highlighted_name Whether the member username will be highlighted. (null: don't change)
 * @param  ?SHORT_TEXT $pt_allow Usergroups that may PT the member. (null: don't change)
 * @param  ?LONG_TEXT $pt_rules_text Rules that other members must agree to before they may start a PT with the member. (null: don't change)
 * @param  ?TIME $on_probation_until When the member is on probation until (null: don't change)
 * @param  ?BINARY $auto_mark_read Mark topics as read automatically (null: don't change)
 * @param  ?TIME $join_time When the member joined (null: don't change)
 * @param  ?URLPATH $avatar_url Avatar (null: don't change)
 * @param  ?LONG_TEXT $signature Signature (null: don't change)
 * @param  ?BINARY $is_perm_banned Banned status (null: don't change)
 * @param  ?URLPATH $photo_url Photo URL (null: don't change)
 * @param  ?URLPATH $photo_thumb_url URL of thumbnail of photo (null: don't change)
 * @param  ?SHORT_TEXT $salt Password salt (null: don't change)
 * @param  ?ID_TEXT $password_compatibility_scheme Password compatibility scheme (null: don't change)
 * @param  boolean $skip_checks Whether to skip security checks and most of the change-triggered emails
 */
function cns_edit_member($member_id, $email_address, $preview_posts, $dob_day, $dob_month, $dob_year, $timezone, $primary_group, $custom_fields, $theme, $reveal_age, $views_signatures, $auto_monitor_contrib_content, $language, $allow_emails, $allow_emails_from_staff, $validated = null, $username = null, $password = null, $highlighted_name = null, $pt_allow = '*', $pt_rules_text = '', $on_probation_until = null, $auto_mark_read = null, $join_time = null, $avatar_url = null, $signature = null, $is_perm_banned = null, $photo_url = null, $photo_thumb_url = null, $salt = null, $password_compatibility_scheme = null, $skip_checks = false)
{
    require_code('type_sanitisation');
    require_code('cns_members_action');

    $update = array();

    if (!$skip_checks) {
        $old_email_address = $GLOBALS['CNS_DRIVER']->get_member_row_field($member_id, 'm_email_address');

        $email_address_required = member_field_is_required($member_id, 'email_address');

        if ((!is_null($email_address)) && ($email_address != '') && ($email_address != STRING_MAGIC_NULL) && (!is_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 != '') && ($email_address != $old_email_address) && ($email_address != STRING_MAGIC_NULL)) {
            $test = $GLOBALS['FORUM_DB']->query_select_value_if_there('f_members', 'id', array('m_email_address' => $email_address));
            if ((!is_null($test)) && ($test != $member_id)) {
                warn_exit(do_lang_tempcode('_EMAIL_ADDRESS_IN_USE'));
            }
        }
    }

    if (!is_null($username)) {
        if (!$skip_checks) {
            cns_check_name_valid($username, $member_id, $password);

            require_code('urls2');
            suggest_new_idmoniker_for('members', 'view', strval($member_id), '', $username);
        }
    }

    if (!is_null($password)) { // Password change
        if ((is_null($password_compatibility_scheme)) && (get_value('no_password_hashing') === '1')) {
            $password_compatibility_scheme = 'plain';
            $update['m_password_change_code'] = '';
            $salt = '';
        }

        if ((!is_null($salt)) || (!is_null($password_compatibility_scheme))) {
            if (!is_null($salt)) {
                $update['m_pass_salt'] = $salt;
            }
            if (!is_null($password_compatibility_scheme)) {
                $update['m_password_compat_scheme'] = $password_compatibility_scheme;
            }
            $update['m_pass_hash_salted'] = $password;
        } else {
            require_code('crypt');
            $update['m_password_change_code'] = '';
            $salt = $GLOBALS['CNS_DRIVER']->get_member_row_field($member_id, 'm_pass_salt');
            $update['m_pass_hash_salted'] = ratchet_hash($password, $salt);
            $update['m_password_compat_scheme'] = '';
        }

        $password_change_days = get_option('password_change_days');
        if (intval($password_change_days) > 0) {
            if ($password_compatibility_scheme == '') {
                require_code('password_rules');
                bump_password_change_date($member_id, $password, $update['m_pass_hash_salted'], $salt, $skip_checks);

                $update['m_last_visit_time'] = time(); // Needed when an admin changing another password (but okay always): So that the password isn't assumed auto-expired, forcing them to reset it again
            }
        }
    }

    // Supplement custom field values given with defaults, and check constraints
    $all_fields = cns_get_all_custom_fields_match($GLOBALS['CNS_DRIVER']->get_members_groups($member_id));
    foreach ($all_fields as $field) {
        $field_id = $field['id'];

        if (array_key_exists($field_id, $custom_fields)) {
            if (!$skip_checks) {
                if (($field['cf_owner_set'] == 0) && ($member_id == get_member()) && (!has_privilege(get_member(), 'view_any_profile_field'))) {
                    access_denied('I_ERROR');
                }
            }
        }
    }

    // Set custom profile field values
    $all_fields_types = collapse_2d_complexity('id', 'cf_type', $all_fields);
    $changes = array();
    foreach ($custom_fields as $field_id => $value) {
        if (!array_key_exists($field_id, $all_fields_types)) {
            continue; // Trying to set a field we're not allowed to (doesn't apply to our group)
        }

        $change = cns_set_custom_field($member_id, $field_id, $value, $all_fields_types[$field_id], true);
        if (!is_null($change)) {
            $changes = array_merge($changes, $change);
        }
    }
    if (count($changes) != 0) {
        $GLOBALS['FORUM_DB']->query_update('f_member_custom_fields', $changes, array('mf_member_id' => $member_id), '', 1);
    }

    $old_primary_group = $GLOBALS['CNS_DRIVER']->get_member_row_field($member_id, 'm_primary_group');

    $_pt_rules_text = $GLOBALS['CNS_DRIVER']->get_member_row_field($member_id, 'm_pt_rules_text');
    $_signature = $GLOBALS['CNS_DRIVER']->get_member_row_field($member_id, 'm_signature');

    if (!is_null($theme)) {
        $update['m_theme'] = $theme;
    }
    if (!is_null($preview_posts)) {
        $update['m_preview_posts'] = $preview_posts;
    }
    if (!is_null($dob_day)) {
        $update['m_dob_day'] = ($dob_day == -1) ? null : $dob_day;
    }
    if (!is_null($dob_month)) {
        $update['m_dob_month'] = ($dob_month == -1) ? null : $dob_month;
    }
    if (!is_null($dob_year)) {
        $update['m_dob_year'] = ($dob_year == -1) ? null : $dob_year;
    }
    if (!is_null($timezone)) {
        $update['m_timezone_offset'] = $timezone;
    }
    if (!is_null($reveal_age)) {
        $update['m_reveal_age'] = $reveal_age;
    }
    if (!is_null($email_address)) {
        $update['m_email_address'] = $email_address;
    }
    if (!is_null($views_signatures)) {
        $update['m_views_signatures'] = $views_signatures;
    }
    if (!is_null($auto_monitor_contrib_content)) {
        $update['m_auto_monitor_contrib_content'] = $auto_monitor_contrib_content;
    }
    if (!is_null($language)) {
        $update['m_language'] = $language;
    }
    $doing_email_option = (get_option('allow_email_disable') == '1') && (addon_installed('cns_contact_member'));
    if ((!is_null($allow_emails)) && ($doing_email_option)) {
        $update['m_allow_emails'] = $allow_emails;
    }
    $doing_email_from_staff_option = (get_option('allow_email_from_staff_disable') == '1');
    if ((!is_null($allow_emails_from_staff)) && ($doing_email_from_staff_option)) {
        $update['m_allow_emails_from_staff'] = $allow_emails_from_staff;
    }
    if (!is_null($pt_allow)) {
        $update['m_pt_allow'] = $pt_allow;
    }
    if (!is_null($pt_rules_text)) {
        $update += lang_remap_comcode('m_pt_rules_text', $_pt_rules_text, $pt_rules_text, $GLOBALS['FORUM_DB']);
    }
    if (($skip_checks) || (has_privilege(get_member(), 'probate_members'))) {
        $update['m_on_probation_until'] = $on_probation_until;
    }
    if (!is_null($auto_mark_read)) {
        $update['m_auto_mark_read'] = $auto_mark_read;
    }
    if (!is_null($join_time)) {
        $update['m_join_time'] = $join_time;
    }
    if (!is_null($avatar_url)) {
        $update['m_avatar_url'] = $avatar_url;
    }
    if (!is_null($signature)) {
        $update += lang_remap_comcode('m_signature', $_signature, $signature, $GLOBALS['FORUM_DB']);
    }
    if (!is_null($is_perm_banned)) {
        $update['m_is_perm_banned'] = $is_perm_banned;
    }
    if (!is_null($photo_url)) {
        $update['m_photo_url'] = $photo_url;
    }
    if (!is_null($photo_thumb_url)) {
        $update['m_photo_thumb_url'] = $photo_thumb_url;
    }

    $old_username = $GLOBALS['CNS_DRIVER']->get_member_row_field($member_id, 'm_username');
    if ((!is_null($username)) && (!is_null($old_username)) && ($username != $old_username) && (($skip_checks) || (has_actual_page_access(get_member(), 'admin_cns_members')) || (has_privilege($member_id, 'rename_self')))) { // Username change
        $update['m_username'] = $username;

        // Reassign personal galleries
        if (addon_installed('galleries')) {
            require_lang('galleries');
            $personal_galleries = $GLOBALS['SITE_DB']->query('SELECT name,fullname,parent_id FROM ' . get_table_prefix() . 'galleries WHERE name LIKE \'member\_' . strval($member_id) . '\_%\'');
            foreach ($personal_galleries as $gallery) {
                $parent_title = get_translated_text($GLOBALS['SITE_DB']->query_select_value('galleries', 'fullname', array('name' => $gallery['parent_id'])));
                if (get_translated_text($gallery['fullname']) == do_lang('PERSONAL_GALLERY_OF', $old_username, $parent_title)) {
                    $new_fullname = do_lang('PERSONAL_GALLERY_OF', $username, $parent_title);
                    $GLOBALS['SITE_DB']->query_update('galleries', lang_remap_comcode('fullname', $gallery['fullname'], $new_fullname), array('name' => $gallery['name']), '', 1);
                }
            }
        }

        require_code('notifications');

        $subject = do_lang('USERNAME_CHANGED_MAIL_SUBJECT', $username, $old_username, null, get_lang($member_id));
        $mail = do_notification_lang('USERNAME_CHANGED_MAIL', comcode_escape(get_site_name()), comcode_escape($username), comcode_escape($old_username), get_lang($member_id));
        dispatch_notification('cns_username_changed', null, $subject, $mail, array($member_id));

        $subject = do_lang('STAFF_USERNAME_CHANGED_MAIL_SUBJECT', $username, $old_username, null, get_site_default_lang());
        $mail = do_notification_lang('STAFF_USERNAME_CHANGED_MAIL', comcode_escape(get_site_name()), comcode_escape($username), comcode_escape($old_username), get_site_default_lang());
        dispatch_notification('cns_username_changed_staff', null, $subject, $mail, null, get_member(), 3, false, false, null, null, '', '', '', '', null, true);

        if (addon_installed('news')) {
            $GLOBALS['SITE_DB']->query_update('news', array('author' => $username), array('author' => $old_username));
        }

        update_member_username_caching($member_id, $username);
    }
    if (!is_null($password)) { // Password change
        // Security, clear out sessions from other people on this user - just in case the reset is due to suspicious activity
        $GLOBALS['SITE_DB']->query('DELETE FROM ' . get_table_prefix() . 'sessions WHERE member_id=' . strval($member_id) . ' AND ' . db_string_not_equal_to('the_session', get_session_id()));

        if (!$skip_checks) {
            if (($member_id == get_member()) || (get_value('disable_password_change_notifications_for_staff') !== '1')) {
                if (get_page_name() != 'admin_cns_members') {
                    require_code('notifications');

                    $part_b = '';
                    if (!has_actual_page_access(get_member(), 'admin_cns_members')) {
                        $part_b = do_lang('PASSWORD_CHANGED_MAIL_BODY_2', get_ip_address());
                    }
                    $mail = do_notification_lang('PASSWORD_CHANGED_MAIL_BODY', get_site_name(), $part_b, null, get_lang($member_id));

                    dispatch_notification('cns_password_changed', null, do_lang('PASSWORD_CHANGED_MAIL_SUBJECT', null, null, null, get_lang($member_id)), $mail, array($member_id), null, 2);
                }
            }
        }
    }
    if (!is_null($validated)) {
        $update['m_validated_email_confirm_code'] = '';
        if (addon_installed('unvalidated')) {
            $update['m_validated'] = $validated;

            if (($validated == 1) && ($GLOBALS['CNS_DRIVER']->get_member_row_field($member_id, 'm_validated') == 0)) {
                $update['m_join_time'] = time(); // So welcome mails go out correctly
            }
        }
    }
    if (!is_null($highlighted_name)) {
        $update['m_highlighted_name'] = $highlighted_name;
    }
    if (!is_null($primary_group)) {
        $update['m_primary_group'] = $primary_group;

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

            log_it('MEMBER_PRIMARY_GROUP_CHANGED', strval($member_id), strval($primary_group));
        }
    }

    $join_time = $GLOBALS['FORUM_DRIVER']->get_member_row_field($member_id, 'm_join_time');

    $GLOBALS['FORUM_DB']->query_update('f_members', $update, array('id' => $member_id), '', 1);

    if (get_member() != $member_id) {
        log_it('EDIT_MEMBER_PROFILE', strval($member_id), $username);
    }

    $old_validated = $GLOBALS['CNS_DRIVER']->get_member_row_field($member_id, 'm_validated');
    if (($old_validated == 0) && ($validated == 1)) {
        require_code('mail');
        $_login_url = build_url(array('page' => 'login'), get_module_zone('login'), null, false, false, true);
        $login_url = $_login_url->evaluate();
        $_username = $GLOBALS['CNS_DRIVER']->get_member_row_field($member_id, 'm_username');
        // NB: Same mail also sent in settings.php (quick-validate feature)
        $vm_subject = do_lang('VALIDATED_MEMBER_SUBJECT', get_site_name(), null, get_lang($member_id));
        $vm_body = do_lang('MEMBER_VALIDATED', get_site_name(), $_username, $login_url, get_lang($member_id));
        mail_wrap($vm_subject, $vm_body, array($email_address), $_username, '', '', 3, null, false, null, false, false, false, 'MAIL', false, null, null, $join_time);
    }

    $old_email_address = $GLOBALS['FORUM_DRIVER']->get_member_row_field($member_id, 'm_email_address');
    if ($old_email_address != $email_address) {
        $GLOBALS['FORUM_DB']->query_update('f_invites', array('i_email_address' => $old_email_address), array('i_email_address' => $email_address));
    }

    delete_value('cns_newest_member_id');
    delete_value('cns_newest_member_username');

    // Decache from run-time cache
    unset($GLOBALS['FORUM_DRIVER']->MEMBER_ROWS_CACHED[$member_id]);
    unset($GLOBALS['MEMBER_CACHE_FIELD_MAPPINGS'][$member_id]);
    unset($GLOBALS['TIMEZONE_MEMBER_CACHE'][$member_id]);
    unset($GLOBALS['USER_NAME_CACHE'][$member_id]);

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

    decache('main_members');
    if (($GLOBALS['FORUM_DRIVER']->is_super_admin($member_id)) && ($old_email_address == '')) {
        decache('main_staff_checklist'); // As it tracks whether admin have e-mail address set
    }

    require_code('sitemap_xml');
    notify_sitemap_node_edit('_SEARCH:members:view:' . strval($member_id), true);
}

/**
 * Delete a member.
 *
 * @param  AUTO_LINK $member_id The ID of the member.
 */
function cns_delete_member($member_id)
{
    $username = $GLOBALS['CNS_DRIVER']->get_member_row_field($member_id, 'm_username');
    $signature = $GLOBALS['CNS_DRIVER']->get_member_row_field($member_id, 'm_signature');
    require_code('attachments2');
    require_code('attachments3');
    delete_lang_comcode_attachments($signature, 'signature', strval($member_id), $GLOBALS['FORUM_DB']);
    $GLOBALS['FORUM_DB']->query_delete('f_members', array('id' => $member_id), '', 1);
    $GLOBALS['FORUM_DB']->query_delete('f_group_members', array('gm_member_id' => $member_id));
    $GLOBALS['FORUM_DB']->query_update('f_groups', array('g_group_leader' => get_member()), array('g_group_leader' => $member_id));
    $GLOBALS['SITE_DB']->query_delete('sessions', array('member_id' => $member_id));

    require_code('fields');

    // Delete custom profile fields
    $cpfs = $GLOBALS['FORUM_DB']->query_select('f_custom_fields');
    $fields_row = $GLOBALS['FORUM_DB']->query_select('f_member_custom_fields', array('*'), array('mf_member_id' => $member_id), '', 1);
    if (array_key_exists(0, $fields_row)) {
        foreach ($cpfs as $field) {
            $l = $fields_row[0]['field_' . strval($field['id'])];

            $object = get_fields_hook($field['cf_type']);

            list(, , $storage_type) = $object->get_field_value_row_bits($field);

            if (method_exists($object, 'cleanup')) {
                $object->cleanup(array('cv_value' => $l));
            }

            if ((strpos($storage_type, '_trans') !== false) && (!is_null($l))) {
                if (true) { // Always do this just in case it is for attachments
                    require_code('attachments2');
                    require_code('attachments3');
                    delete_lang_comcode_attachments($l, 'null', strval($member_id), $GLOBALS['FORUM_DB']);
                } else {
                    delete_lang($l, $GLOBALS['FORUM_DB']);
                }
            }
        }
    }
    $GLOBALS['FORUM_DB']->query_delete('f_member_custom_fields', array('mf_member_id' => $member_id), '', 1);

    // Cleanup images
    $old = $GLOBALS['CNS_DRIVER']->get_member_row_field($member_id, 'm_avatar_url');
    if ((url_is_local($old)) && ((substr($old, 0, 20) == 'uploads/cns_avatars/') || (substr($old, 0, 16) == 'uploads/avatars/'))) {
        @unlink(get_custom_file_base() . '/' . rawurldecode($old));
        sync_file(rawurldecode($old));
    }
    $old = $GLOBALS['CNS_DRIVER']->get_member_row_field($member_id, 'm_photo_url');
    if ((url_is_local($old)) && ((substr($old, 0, 19) == 'uploads/cns_photos/') || (substr($old, 0, 15) == 'uploads/photos/'))) {
        @unlink(get_custom_file_base() . '/' . rawurldecode($old));
        sync_file(rawurldecode($old));
    }

    if (addon_installed('catalogues')) {
        update_catalogue_content_ref('member', strval($member_id), '');
    }

    delete_value('cns_newest_member_id');
    delete_value('cns_newest_member_username');

    log_it('DELETE_MEMBER', strval($member_id), $username);

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

    decache('main_members');

    require_code('sitemap_xml');
    notify_sitemap_node_delete('_SEARCH:members:view:' . strval($member_id));
}

/**
 * Ban a member.
 *
 * @param  AUTO_LINK $member_id The ID of the member.
 */
function cns_ban_member($member_id)
{
    if ($GLOBALS['CNS_DRIVER']->get_member_row_field($member_id, 'm_is_perm_banned') == 1) {
        return;
    }

    require_code('mail');

    $username = $GLOBALS['CNS_DRIVER']->get_member_row_field($member_id, 'm_username');
    $email_address = $GLOBALS['CNS_DRIVER']->get_member_row_field($member_id, 'm_email_address');
    $join_time = $GLOBALS['FORUM_DRIVER']->get_member_row_field($member_id, 'm_join_time');

    $GLOBALS['FORUM_DB']->query_update('f_members', array('m_is_perm_banned' => 1), array('id' => $member_id), '', 1);

    log_it('BAN_MEMBER', strval($member_id), $username);

    require_lang('cns');
    $mail = do_lang('BAN_MEMBER_MAIL', $username, get_site_name(), array(), get_lang($member_id));
    mail_wrap(do_lang('BAN_MEMBER_MAIL_SUBJECT', null, null, null, get_lang($member_id)), $mail, array($email_address), $username, '', '', 2, null, false, null, false, false, false, 'MAIL', false, null, null, $join_time);

    decache('main_members');

    unset($GLOBALS['FORUM_DRIVER']->MEMBER_ROWS_CACHED[$member_id]);
}

/**
 * Unban a member.
 *
 * @param  AUTO_LINK $member_id The ID of the member.
 */
function cns_unban_member($member_id)
{
    if ($GLOBALS['CNS_DRIVER']->get_member_row_field($member_id, 'm_is_perm_banned') == 0) {
        return;
    }

    $username = $GLOBALS['CNS_DRIVER']->get_member_row_field($member_id, 'm_username');
    $email_address = $GLOBALS['CNS_DRIVER']->get_member_row_field($member_id, 'm_email_address');
    $join_time = $GLOBALS['FORUM_DRIVER']->get_member_row_field($member_id, 'm_join_time');

    $GLOBALS['FORUM_DB']->query_update('f_members', array('m_is_perm_banned' => 0), array('id' => $member_id), '', 1);

    log_it('UNBAN_MEMBER', strval($member_id), $username);

    require_lang('cns');
    $mail = do_lang('UNBAN_MEMBER_MAIL', $username, get_site_name(), array(), get_lang($member_id));
    mail_wrap(do_lang('UNBAN_MEMBER_MAIL_SUBJECT', null, null, null, get_lang($member_id)), $mail, array($email_address), $username, '', '', 2, null, false, null, false, false, false, 'MAIL', false, null, null, $join_time);

    decache('main_members');

    unset($GLOBALS['FORUM_DRIVER']->MEMBER_ROWS_CACHED[$member_id]);
}

/**
 * Edit a custom profile field.
 *
 * @param  AUTO_LINK $id The ID of the custom profile field.
 * @param  SHORT_TEXT $name Name of the field.
 * @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 should be encrypted.
 * @param  BINARY $required Whether the field is to be shown on the join form
 * @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.
 * @param  LONG_TEXT $only_group The usergroups that this field is confined to (comma-separated list).
 * @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 $show_on_join_form Whether it is required that every member have this field filled in.
 * @param  SHORT_TEXT $options Field options
 */
function cns_edit_custom_field($id, $name, $description, $default, $public_view, $owner_view, $owner_set, $encrypted, $required, $show_in_posts, $show_in_post_previews, $order, $only_group, $type, $show_on_join_form, $options)
{
    $dbs_back = $GLOBALS['NO_DB_SCOPE_CHECK'];
    $GLOBALS['NO_DB_SCOPE_CHECK'] = true;

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

    $info = $GLOBALS['FORUM_DB']->query_select('f_custom_fields', array('cf_name', 'cf_description'), array('id' => $id), '', 1);
    $_name = $info[0]['cf_name'];
    $_description = $info[0]['cf_description'];

    $map = array(
        'cf_default' => $default,
        'cf_public_view' => $public_view,
        'cf_owner_view' => $owner_view,
        'cf_owner_set' => $owner_set,
        '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_type' => $type,
        'cf_show_on_join_form' => $show_on_join_form,
        'cf_options' => $options,
    );
    $map += lang_remap('cf_name', $_name, $name, $GLOBALS['FORUM_DB']);
    $map += lang_remap('cf_description', $_description, $description, $GLOBALS['FORUM_DB']);

    $GLOBALS['FORUM_DB']->query_update('f_custom_fields', $map, array('id' => $id), '', 1);

    require_code('cns_members_action');

    list($_type, $index) = get_cpf_storage_for($type);

    require_code('database_action');

    if (substr(get_db_type(), 0, 5) == 'mysql') {
        $GLOBALS['SITE_DB']->query('SET sql_mode=\'\'', null, null, true); // Turn off strict mode
    }
    $GLOBALS['FORUM_DB']->alter_table_field('f_member_custom_fields', 'field_' . strval($id), $_type); // LEGACY: Field type should not have changed, but bugs can happen, especially between CMS versions, so we allow a CPF edit as a "fixup" op

    build_cpf_indices($id, $index, $type, $_type);

    log_it('EDIT_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));
    }

    $GLOBALS['NO_DB_SCOPE_CHECK'] = $dbs_back;

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

    decache('main_members');
}

/**
 * Delete a custom profile field.
 *
 * @param  AUTO_LINK $id The ID of the custom profile field.
 */
function cns_delete_custom_field($id)
{
    $dbs_back = $GLOBALS['NO_DB_SCOPE_CHECK'];
    $GLOBALS['NO_DB_SCOPE_CHECK'] = true;

    $info = $GLOBALS['FORUM_DB']->query_select('f_custom_fields', array('cf_name', 'cf_description'), array('id' => $id), '', 1);
    if (!array_key_exists(0, $info)) {
        warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'cpf'));
    }
    $_name = $info[0]['cf_name'];
    $_description = $info[0]['cf_description'];

    require_code('database_action');
    delete_lang($_name, $GLOBALS['FORUM_DB']);
    delete_lang($_description, $GLOBALS['FORUM_DB']);
    $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));
    $GLOBALS['FORUM_DB']->delete_table_field('f_member_custom_fields', 'field_' . strval($id));
    $GLOBALS['FORUM_DB']->query_delete('f_custom_fields', array('id' => $id), '', 1);

    $GLOBALS['NO_DB_SCOPE_CHECK'] = $dbs_back;

    global $TABLE_LANG_FIELDS_CACHE;
    unset($TABLE_LANG_FIELDS_CACHE['f_member_custom_fields']);

    log_it('DELETE_CUSTOM_PROFILE_FIELD', strval($id), get_translated_text($_name, $GLOBALS['FORUM_DB']));

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

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

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

/**
 * Set a custom profile field for a member.
 *
 * @param  MEMBER $member_id The member.
 * @param  AUTO_LINK $field_id The field being set.
 * @param  mixed $value The value of the field. For a trans-type field, this can be either a lang-ID to be copied (from forum DB), or an actual string.
 * @param  ?ID_TEXT $type The field type (null: look it up).
 * @param  boolean $defer Whether to defer the change, by returning a result change rather than doing it right away.
 * @return ?array Mapping change (null: none / can't defer).
 */
function cns_set_custom_field($member_id, $field_id, $value, $type = null, $defer = false)
{
    if ($value === STRING_MAGIC_NULL) {
        return null;
    }

    if (is_null($type)) {
        $type = $GLOBALS['FORUM_DB']->query_select_value('f_custom_fields', 'cf_type', array('id' => $field_id));
    }

    cns_get_custom_field_mappings($member_id); // This will do an auto-repair if CPF storage row is missing

    $db_fieldname = 'field_' . strval($field_id);

    global $ANY_FIELD_ENCRYPTED;
    if ($ANY_FIELD_ENCRYPTED === null) {
        $ANY_FIELD_ENCRYPTED = !is_null($GLOBALS['FORUM_DB']->query_select_value_if_there('f_custom_fields', 'cf_encrypted', array('cf_encrypted' => 1)));
    }

    if ($ANY_FIELD_ENCRYPTED) {
        $encrypted = $GLOBALS['FORUM_DB']->query_select_value('f_custom_fields', 'cf_encrypted', array('id' => $field_id));
        if ($encrypted) {
            require_code('encryption');
            $current = $GLOBALS['FORUM_DB']->query_select_value_if_there('f_member_custom_fields', $db_fieldname, array('mf_member_id' => $member_id));
            if ($current === null) {
                return null;
            }
            if ((remove_magic_encryption_marker($value) == remove_magic_encryption_marker($current)) && (is_data_encrypted($current))) {
                return null;
            }
            $value = encrypt_data($value);
        }
    } else {
        $encrypted = false;
    }

    require_code('fields');
    $ob = get_fields_hook($type);
    list(, , $storage_type) = $ob->get_field_value_row_bits(array('id' => $field_id, 'cf_default' => '', 'cf_type' => $type));

    static $done_one_posting_field = false;

    $ret = null;

    if (strpos($storage_type, '_trans') !== false) {
        if (is_integer($value)) {
            $value = get_translated_text($value, $GLOBALS['FORUM_DB']);
        }

        $map = array();

        $current = $GLOBALS['FORUM_DB']->query_select_value_if_there('f_member_custom_fields', $db_fieldname, array('mf_member_id' => $member_id));
        if (is_null($current)) {
            if (($type == 'posting_field') && (!$done_one_posting_field)) {
                $done_one_posting_field = true;
                require_code('attachments2');
                $map += insert_lang_comcode_attachments($db_fieldname, 3, $value, 'null', strval($member_id), $GLOBALS['FORUM_DB']);
            } else {
                $map += insert_lang_comcode($db_fieldname, $value, 3, $GLOBALS['FORUM_DB']);
            }

            $GLOBALS['FORUM_DB']->query_update('f_member_custom_fields', $map, array('mf_member_id' => $member_id), '', 1);
        } else {
            if (($type == 'posting_field') && (!$done_one_posting_field)) {
                $done_one_posting_field = true;
                require_code('attachments2');
                require_code('attachments3');
                $map += update_lang_comcode_attachments($db_fieldname, $current, $value, 'null', strval($member_id), $GLOBALS['FORUM_DB'], $member_id);
            } else {
                $map += lang_remap_comcode($db_fieldname, $current, $value, $GLOBALS['FORUM_DB']);
            }

            $GLOBALS['FORUM_DB']->query_update('f_member_custom_fields', $map, array('mf_member_id' => $member_id), '', 1);
        }
    } else {
        $change = array();

        if (is_string($value)) {
            switch ($storage_type) {
                case 'short_trans':
                case 'long_trans':
                    $change += insert_lang($db_fieldname, $value, 3, $GLOBALS['FORUM_DB']);
                    break;
                case 'integer':
                    $change[$db_fieldname] = ($value == '') ? null : intval($value);
                    break;
                case 'float':
                    $change[$db_fieldname] = ($value == '') ? null : floatval($value);
                    break;
                default:
                    $change[$db_fieldname] = $value;
                    break;
            }
        } elseif ($value === null) {
            switch ($storage_type) {
                case 'integer':
                case 'float':
                    $change[$db_fieldname] = $value;
                    break;
            }
        } else {
            $change[$db_fieldname] = $value;
        }

        if (!$defer) {
            $GLOBALS['FORUM_DB']->query_update('f_member_custom_fields', $change, array('mf_member_id' => $member_id), '', 1);
        }

        $ret = $change;
    }

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

    global $MEMBER_CACHE_FIELD_MAPPINGS;
    if ((isset($MEMBER_CACHE_FIELD_MAPPINGS)) && (isset($MEMBER_CACHE_FIELD_MAPPINGS[$member_id]))) {
        unset($MEMBER_CACHE_FIELD_MAPPINGS[$member_id]);
    }

    return $ret;
}

/**
 * Check a username is valid for adding, and possibly also the password.
 *
 * @param  ?SHORT_TEXT $username The username (may get altered) (null: nothing to check).
 * @param  ?MEMBER $member_id The member (null: member not actually added yet; this ID is only given for the duplication check, to make sure it doesn't think we are duplicating with ourself).
 * @param  ?SHORT_TEXT $password The password (null: nothing to check).
 * @param  boolean $return_errors Whether to return errors instead of dieing on them.
 * @return ?Tempcode Error (null: none).
 */
function cns_check_name_valid(&$username, $member_id = null, $password = null, $return_errors = false)
{
    // Check it doesn't already exist
    if (!is_null($username)) {
        $test = is_null($member_id) ? null : $GLOBALS['FORUM_DB']->query_select_value_if_there('f_members', 'id', array('m_username' => $username, 'id' => $member_id)); // Precedence on an ID match in case there are duplicate usernames and user is trying to fix that
        if (is_null($test)) {
            $test = $GLOBALS['FORUM_DB']->query_select_value_if_there('f_members', 'id', array('m_username' => $username));
        }
        if ((!is_null($test)) && ($test !== $member_id)) {
            $error = do_lang_tempcode('USERNAME_ALREADY_EXISTS');
            if ($return_errors) {
                return $error;
            }
            warn_exit($error);
        }
        $username_changed = is_null($test);
    } else {
        $username_changed = false;
    }

    if (!is_null($username)) {
        // Check for disallowed symbols in username
        $disallowed_characters = array(/*'<','>','&','"',"'",'$',','*/); // Actually we can tolerate this stuff
        foreach ($disallowed_characters as $disallowed_character) {
            if ((strpos($username, $disallowed_character) !== false) && ($username_changed)) {
                $error = do_lang_tempcode('USERNAME_BAD_SYMBOLS', escape_html($disallowed_character));
                if ($return_errors) {
                    return $error;
                }
                warn_exit($error);
            }
        }
        if ((strpos($username, '@') !== false) && (strpos($username, '.') !== false) && ($username_changed)) {
            $error = do_lang_tempcode('USERNAME_BAD_SYMBOLS', escape_html('@ / .'));
            if ($return_errors) {
                return $error;
            }
            warn_exit($error);
        }
    }

    // Check lengths
    if (get_page_name() != 'admin_cns_members') {
        if (!is_null($username)) {
            $_maximum_username_length = get_option('maximum_username_length');
            $maximum_username_length = intval($_maximum_username_length);
            if ((cms_mb_strlen($username) > $maximum_username_length) && ($username_changed)) {
                $error = do_lang_tempcode('USERNAME_TOO_LONG', escape_html(integer_format($maximum_username_length)));
                if ($return_errors) {
                    return $error;
                }
                warn_exit($error);
            }
            $_minimum_username_length = get_option('minimum_username_length');
            $minimum_username_length = intval($_minimum_username_length);
            if ((cms_mb_strlen($username) < $minimum_username_length) && ($username_changed)) {
                $error = do_lang_tempcode('USERNAME_TOO_SHORT', escape_html(integer_format($minimum_username_length)));
                if ($return_errors) {
                    return $error;
                }
                warn_exit($error);
            }
        }
        if (!is_null($password)) {
            require_code('password_rules');
            $test = check_password_complexity($username, $password, $return_errors);
            if (!is_null($test)) {
                return $test;
            }
        }
    }

    // Check for whitespace
    if (!is_null($username)) {
        $prohibit_username_whitespace = get_option('prohibit_username_whitespace');
        if (($prohibit_username_whitespace === '1') && (cms_preg_match_safe('#\s#', $username) != 0) && ($username_changed)) {
            $error = do_lang_tempcode('USERNAME_PASSWORD_WHITESPACE');
            if ($return_errors) {
                return $error;
            }
            warn_exit($error);
        }
    }
    if (!is_null($password)) {
        $prohibit_password_whitespace = get_option('prohibit_password_whitespace');
        if (($prohibit_password_whitespace === '1') && (cms_preg_match_safe('#\s#', $password) != 0) && ($username_changed)) {
            $error = do_lang_tempcode('USERNAME_PASSWORD_WHITESPACE');
            if ($return_errors) {
                return $error;
            }
            warn_exit($error);
        }
    }

    // Check against restricted usernames
    if ((get_page_name() != 'admin_cns_members') && ($username_changed)) {
        $restricted_usernames = explode(',', get_option('restricted_usernames'));
        $restricted_usernames[] = do_lang('GUEST');
        $restricted_usernames[] = do_lang('UNKNOWN');
        $restricted_usernames[] = do_lang('SYSTEM');
        foreach ($restricted_usernames as $_restricted_username) {
            $restricted_username = trim($_restricted_username);
            if ($restricted_username == '') {
                continue;
            }
            if (strpos($username, $restricted_username) !== false) {
                $error = do_lang_tempcode('USERNAME_BAD_SUBSTRING', escape_html($restricted_username));
                if ($return_errors) {
                    return $error;
                }
                warn_exit($error);
            }
        }
    }

    // Check it is not numeric
    if (is_numeric($username)) {
        $error = do_lang_tempcode('USERNAME_NUMERIC');
        if ($return_errors) {
            return $error;
        }
        warn_exit($error);
    }

    return null;
}

/**
 * Edit a member's personal title, and check validity.
 *
 * @param  SHORT_TEXT $new_title The new title.
 * @param  ?MEMBER $member_id The member (null: the current member).
 */
function cns_member_choose_title($new_title, $member_id = null)
{
    if (is_null($member_id)) {
        $member_id = get_member();
    }

    $old_title = $GLOBALS['CNS_DRIVER']->get_member_row_field($member_id, 'm_title');
    if ($old_title == $new_title) {
        return;
    }

    if (cms_mb_strlen($new_title) > intval(get_option('max_member_title_length'))) {
        warn_exit(do_lang_tempcode('MEMBER_TITLE_TOO_BIG'));
    }

    $GLOBALS['FORUM_DB']->query_update('f_members', array('m_title' => $new_title), array('id' => $member_id), '', 1);

    // Decache from run-time cache
    unset($GLOBALS['FORUM_DRIVER']->MEMBER_ROWS_CACHED[$member_id]);
    unset($GLOBALS['MEMBER_CACHE_FIELD_MAPPINGS'][$member_id]);

    decache('main_members');
}

/**
 * Edit a member's signature, and check validity.
 *
 * @param  LONG_TEXT $new_signature The new signature.
 * @param  ?MEMBER $member_id The member (null: the current member).
 */
function cns_member_choose_signature($new_signature, $member_id = null)
{
    if (is_null($member_id)) {
        $member_id = get_member();
    }

    $max_sig_length = cns_get_member_best_group_property($member_id, 'max_sig_length_comcode');
    if (cms_mb_strlen($new_signature) > $max_sig_length) {
        warn_exit(make_string_tempcode(escape_html(do_lang('SIGNATURE_TOO_BIG'))));
    }

    $_signature = $GLOBALS['CNS_DRIVER']->get_member_row_field($member_id, 'm_signature');

    if (get_translated_text($_signature, $GLOBALS['FORUM_DB']) == $new_signature) {
        return;
    }

    require_code('attachments2');
    require_code('attachments3');
    $map = array();
    $map += update_lang_comcode_attachments('m_signature', $_signature, $new_signature, 'cns_signature', strval($member_id), $GLOBALS['FORUM_DB'], $member_id);
    $GLOBALS['FORUM_DB']->query_update('f_members', $map, array('id' => $member_id), '', 1);

    require_code('notifications');
    $subject = do_lang('CHOOSE_SIGNATURE_SUBJECT', $GLOBALS['FORUM_DRIVER']->get_username($member_id, true), $GLOBALS['FORUM_DRIVER']->get_username($member_id), null, get_lang($member_id));
    $body = do_notification_lang('CHOOSE_SIGNATURE_BODY', $new_signature, $GLOBALS['FORUM_DRIVER']->get_username($member_id), $GLOBALS['FORUM_DRIVER']->get_username($member_id, true), get_lang($member_id));
    dispatch_notification('cns_choose_signature', null, $subject, $body, null, get_member(), 3, false, false, null, null, '', '', '', '', null, true);

    // Decache from run-time cache
    unset($GLOBALS['FORUM_DRIVER']->MEMBER_ROWS_CACHED[$member_id]);
    unset($GLOBALS['MEMBER_CACHE_FIELD_MAPPINGS'][$member_id]);
}

/**
 * Edit a member's avatar, and check validity.
 *
 * @param  URLPATH $avatar_url The new avatar URL.
 * @param  ?MEMBER $member_id The member (null: the current member).
 */
function cns_member_choose_avatar($avatar_url, $member_id = null)
{
    if (is_null($member_id)) {
        $member_id = get_member();
    }

    $old = $GLOBALS['FORUM_DB']->query_select_value('f_members', 'm_avatar_url', array('id' => $member_id));
    if ($old == $avatar_url) {
        return;
    }

    // Check it has valid dimensions
    if ($avatar_url != '') {
        require_code('images');
        if (!is_image($avatar_url, true)) {
            $ext = get_file_extension($avatar_url);
            warn_exit(do_lang_tempcode('UNKNOWN_FORMAT', escape_html($ext)));
        }
        $stub = url_is_local($avatar_url) ? (get_complex_base_url($avatar_url) . '/') : '';
        if (function_exists('imagetypes')) {
            $file_path_stub = convert_url_to_path($stub . $avatar_url);
            if (!is_null($file_path_stub)) {
                $from_file = @file_get_contents($file_path_stub);
            } else {
                $from_file = http_download_file($stub . $avatar_url, 1024 * 1024 * 4/*reasonable limit*/, false);
            }
            if (is_null($from_file)) {
                warn_exit(do_lang_tempcode('MISSING_RESOURCE', do_lang_tempcode('URL')));
            }

            $test = cms_getimagesizefromstring($from_file, get_file_extension($avatar_url));
            if (!$test) {
                warn_exit(do_lang_tempcode('CORRUPT_FILE', escape_html($avatar_url)));
            }
            list($sx, $sy) = $test;

            require_code('cns_groups');
            $width = cns_get_member_best_group_property($member_id, 'max_avatar_width');
            $height = cns_get_member_best_group_property($member_id, 'max_avatar_height');
            if (($sx > $width) || ($sy > $height)) {
                require_code('images');
                $file_path = get_custom_file_base() . '/' . rawurldecode($avatar_url);
                if ((!is_saveable_image($file_path)) || (!url_is_local($avatar_url))) {
                    if ((url_is_local($avatar_url)) && (substr($avatar_url, 0, 20) == 'uploads/cns_avatars/')) {
                        unlink($file_path);
                        sync_file(rawurldecode($avatar_url));
                    }
                    warn_exit(do_lang_tempcode('IMAGE_BAD_DIMENSIONS', strval($width) . 'x' . strval($height), strval($sx) . 'x' . strval($sy)));
                }
                convert_image($file_path, $file_path, $width, $height, -1, false, get_file_extension($file_path), true, true);
            }
        }

        if ((substr($avatar_url, 0, 7) != 'themes/') && (addon_installed('cns_avatars'))) {
            require_code('notifications');
            $subject = do_lang('CHOOSE_AVATAR_SUBJECT', $GLOBALS['FORUM_DRIVER']->get_username($member_id, true), $GLOBALS['FORUM_DRIVER']->get_username($member_id), null, get_lang($member_id));
            $body = do_notification_lang('CHOOSE_AVATAR_BODY', $stub . $avatar_url, $GLOBALS['FORUM_DRIVER']->get_username($member_id), $GLOBALS['FORUM_DRIVER']->get_username($member_id, true), get_lang($member_id));
            dispatch_notification('cns_choose_avatar', null, $subject, $body, null, get_member(), 3, false, false, null, null, '', '', '', '', null, true);
        }
    }

    // Cleanup old avatar
    if ((url_is_local($old)) && ((substr($old, 0, 20) == 'uploads/cns_avatars/') || (substr($old, 0, 16) == 'uploads/avatars/')) && ($old != $avatar_url)) {
        @unlink(get_custom_file_base() . '/' . rawurldecode($old));
        sync_file(rawurldecode($old));
    }

    $GLOBALS['FORUM_DB']->query_update('f_members', array('m_avatar_url' => $avatar_url), array('id' => $member_id), '', 1);

    // Decache from run-time cache
    unset($GLOBALS['FORUM_DRIVER']->MEMBER_ROWS_CACHED[$member_id]);
    unset($GLOBALS['MEMBER_CACHE_FIELD_MAPPINGS'][$member_id]);

    decache('main_friends_list');

    decache('main_members');
}

/**
 * Edit a member's photo, and check validity.
 *
 * @param  ID_TEXT $param_name The identifier for the name of the posted URL field.
 * @param  ID_TEXT $upload_name The identifier for the name of the posted upload.
 * @param  ?MEMBER $member_id The member (null: the current member).
 */
function cns_member_choose_photo($param_name, $upload_name, $member_id = null)
{
    if (is_null($member_id)) {
        $member_id = get_member();
    }

    require_code('uploads');

    if (((!array_key_exists($upload_name, $_FILES)) || (!is_plupload()) && (!is_uploaded_file($_FILES[$upload_name]['tmp_name'])))) {
        $old = $GLOBALS['FORUM_DB']->query_select_value('f_members', 'm_photo_url', array('id' => $member_id));
        $x = post_param_string($param_name, '');
        if (($x != '') && (url_is_local($x))) {
            if (!$GLOBALS['FORUM_DRIVER']->is_super_admin(get_member())) {
                if ($old != $x) {
                    access_denied('ASSOCIATE_EXISTING_FILE');
                }
            }
        }
        if ($old == $x) {
            return; // Not changed, bomb out as we don't want to generate a thumbnail
        }
    }

    // Find photo URL
    set_images_cleanup_pipeline_settings(IMG_RECOMPRESS_LOSSLESS, null, null, true); // Code to strip GPS
    $urls = get_url($param_name, $upload_name, file_exists(get_custom_file_base() . '/uploads/photos') ? 'uploads/photos' : 'uploads/cns_photos', 0, CMS_UPLOAD_IMAGE, true, 'thumb_' . $param_name, $upload_name . '2', false, true);
    reset_images_cleanup_pipeline_settings();
    if (!(strlen($urls[0]) > 1)) {
        $urls[1] = '';
    }
    if (((get_base_url() != get_forum_base_url()) || ((!empty($GLOBALS['SITE_INFO']['on_msn'])) && ($GLOBALS['SITE_INFO']['on_msn'] == '1'))) && ($urls[0] != '') && (url_is_local($urls[0]))) {
        $urls[0] = get_base_url() . '/' . $urls[0];
    }
    if (((get_base_url() != get_forum_base_url()) || ((!empty($GLOBALS['SITE_INFO']['on_msn'])) && ($GLOBALS['SITE_INFO']['on_msn'] == '1'))) && ($urls[1] != '') && (url_is_local($urls[1]))) {
        $urls[1] = get_base_url() . '/' . $urls[1];
    }

    // At this point in the code, we know a photo was uploaded or changed to blank.
    //  If we don't have GD, we need them to have uploaded a thumbnail too.
    if (!function_exists('imagetypes')) {
        if (((!array_key_exists($upload_name . '2', $_FILES)) || (!is_plupload()) && (!is_uploaded_file($_FILES[$upload_name . '2']['tmp_name'])))) {
            $field = post_param_string('thumb_' . $param_name, '');
            if (($field == '') && ($urls[0] != '')) {
                warn_exit(do_lang_tempcode('IMPROPERLY_FILLED_IN_UPLOAD'));
            }
            if (($field != '') && (url_is_local($field)) && (!$GLOBALS['FORUM_DRIVER']->is_super_admin(get_member()))) {
                $old = $GLOBALS['FORUM_DB']->query_select_value('f_members', 'm_photo_thumb_url', array('id' => $member_id));
                if ($old != $field) {
                    access_denied('ASSOCIATE_EXISTING_FILE');
                }
            }
        }
    }

    cns_member_choose_photo_concrete($urls[0], $urls[1], $member_id);

    decache('main_members');
}

/**
 * Edit a member's photo.
 *
 * @param  URLPATH $url URL to photo.
 * @param  URLPATH $thumb_url URL to thumbnail photo.
 * @param  ?MEMBER $member_id The member (null: the current member).
 */
function cns_member_choose_photo_concrete($url, $thumb_url, $member_id = null)
{
    if (is_null($member_id)) {
        $member_id = get_member();
    }

    // Cleanup old photo
    $old = $GLOBALS['FORUM_DB']->query_select_value('f_members', 'm_photo_url', array('id' => $member_id));
    if ($old == $url) {
        return;
    }
    if ((url_is_local($old)) && ((substr($old, 0, 19) == 'uploads/cns_photos/') || (substr($old, 0, 15) == 'uploads/photos/'))) {
        sync_file(rawurldecode($old));
        @unlink(get_custom_file_base() . '/' . rawurldecode($old));
    }

    $GLOBALS['FORUM_DB']->query_update('f_members', array('m_photo_url' => $url, 'm_photo_thumb_url' => $thumb_url), array('id' => $member_id), '', 1);

    require_code('notifications');
    $subject = do_lang('CHOOSE_PHOTO_SUBJECT', $GLOBALS['FORUM_DRIVER']->get_username($member_id, true), $GLOBALS['FORUM_DRIVER']->get_username($member_id), null, get_lang($member_id));
    $body = do_notification_lang('CHOOSE_PHOTO_BODY', $url, $thumb_url, array($GLOBALS['FORUM_DRIVER']->get_username($member_id), $GLOBALS['FORUM_DRIVER']->get_username($member_id, true)), get_lang($member_id));
    dispatch_notification('cns_choose_photo', null, $subject, $body, null, get_member(), 3, false, false, null, null, '', '', '', '', null, true);

    // If Avatars addon not installed, use photo for it
    if (!addon_installed('cns_avatars')) {
        $avatar_url = $url;
        if (function_exists('imagetypes')) {
            $stub = url_is_local($avatar_url) ? (get_complex_base_url($avatar_url) . '/') : '';
            $file_path = convert_url_to_path($stub . $avatar_url);
            if (!is_null($file_path)) {
                $new_file_path = str_replace('/cns_photos/', '/cns_avatars/', $file_path);
                if (!file_exists($new_file_path)) {
                    copy($file_path, $new_file_path);
                    fix_permissions($new_file_path);
                    sync_file($new_file_path);
                }
                $avatar_url = str_replace('/cns_photos/', '/cns_avatars/', $avatar_url);
            }
        }

        cns_member_choose_avatar($avatar_url, $member_id);
    }

    // Decache from run-time cache
    unset($GLOBALS['FORUM_DRIVER']->MEMBER_ROWS_CACHED[$member_id]);
    unset($GLOBALS['MEMBER_CACHE_FIELD_MAPPINGS'][$member_id]);

    decache('main_friends_list');

    decache('main_members');
}

/**
 * Update caching against a member's username. This doesn't change the username in the actual member record -- it is assumed that this will be done elsewhere.
 *
 * @param  MEMBER $member_id The member ID.
 * @param  ID_TEXT $username The new username that is being set for them.
 */
function update_member_username_caching($member_id, $username)
{
    // Fix caching for usernames
    $to_fix = array(
        'f_forums/f_cache_last_username/f_cache_last_member_id',
        'f_posts/p_poster_name_if_guest/p_poster',
        'f_topics/t_cache_first_username/t_cache_first_member_id',
        'f_topics/t_cache_last_username/t_cache_last_member_id',
        'sessions/cache_username/member_id',
    );
    foreach ($to_fix as $fix) {
        list($table, $field, $updating_field) = explode('/', $fix, 3);
        $con = $GLOBALS[(substr($table, 0, 2) == 'f_') ? 'FORUM_DB' : 'SITE_DB'];
        $con->query_update($table, array($field => $username), array($updating_field => $member_id));
    }
}

/**
 * Delete a custom profile field from one of the predefined templates (this is often used by importers).
 *
 * @param  ID_TEXT $field The identifier of the boiler custom profile field.
 */
function cns_delete_boiler_custom_field($field)
{
    require_lang('cns_special_cpf');

    $test = $GLOBALS['FORUM_DB']->query_select_value_if_there('f_custom_fields', 'id', array($GLOBALS['SITE_DB']->translate_field_ref('cf_name') => do_lang('DEFAULT_CPF_' . $field . '_NAME')));
    if (!is_null($test)) {
        require_code('cns_members_action');
        cns_delete_custom_field($test);
    }
}

/**
 * Rebuild custom profile field indices.
 *
 * @param  boolean $leave_existing Whether to leave existing indexes alone (may be useful as deleting then recreating indexes can be very slow).
 */
function rebuild_all_cpf_indices($leave_existing = false)
{
    $GLOBALS['NO_QUERY_LIMIT'] = true; // TODO: Change in v11

    $fields = $GLOBALS['FORUM_DB']->query_select('f_custom_fields', array('id', 'cf_type'), array(), 'ORDER BY cf_required+cf_show_on_join_form DESC,cf_public_view+cf_owner_set DESC,cf_order DESC'); // TODO: Respect searchable setting in v11

    if (!$leave_existing) {
        // Delete existing indexes
        foreach ($fields as $field) {
            $id = $field['id'];

            $GLOBALS['FORUM_DB']->delete_index_if_exists('f_member_custom_fields', 'field_' . strval($id)); // LEGACY
            $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));
        }

        // Delete any stragglers (already deleted fields or inconsistent naming)
        $GLOBALS['FORUM_DB']->query_delete('db_meta_indices', array('i_table' => 'f_member_custom_fields'));
        if (strpos(get_db_type(), 'mysql') !== false) {
            $indexes = $GLOBALS['FORUM_DB']->query('SHOW INDEXES FROM ' . $GLOBALS['FORUM_DB']->get_table_prefix() . 'f_member_custom_fields WHERE Column_name<>\'mf_member_id\'');
            foreach ($indexes as $index) {
                $GLOBALS['FORUM_DB']->query('DROP INDEX ' . $index['Key_name'] . ' ON ' . $GLOBALS['FORUM_DB']->get_table_prefix() . 'f_member_custom_fields');
            }
        }
    }

    // Rebuild indexes
    require_code('cns_members_action');
    foreach ($fields as $field) {
        $id = $field['id'];
        $type = $field['cf_type'];
        list($_type, $index) = get_cpf_storage_for($type);

        $okay = build_cpf_indices($id, $index, $type, $_type);
        if (!$okay) { // Limit was hit
            break;
        }
    }
}
