query_select('f_custom_fields', ['id', 'cf_type', 'cf_name', 'cf_order'], [], 'ORDER BY cf_order,' . $GLOBALS['FORUM_DB']->translate_field_ref('cf_name')));
// Headings
$headings = member_get_spreadsheet_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 = [];
if (addon_installed('ecommerce')) {
require_lang('ecommerce');
$usergroup_subscription_rows = $GLOBALS['FORUM_DB']->query_select('f_usergroup_subs', ['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 = [
'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 [$headings, $cpfs, $subscription_types];
}
/**
* Get field mapping data for spreadsheet import/export.
*
* @return array A map of heading information (human name to field name/encoding details)
*/
function member_get_spreadsheet_headings() : array
{
$headings = [
'ID' => 'id',
'Username' => 'm_username',
'E-mail address' => 'm_email_address',
];
if (has_privilege(get_member(), 'assume_any_member')) {
$headings += [
'Password' => 'm_pass_hash_salted/m_pass_salt/m_password_compat_scheme',
];
}
if (addon_installed('cns_member_avatars')) {
$headings += [
'Avatar' => '#m_avatar_url',
];
}
if (addon_installed('cns_member_photos')) {
$headings += [
'Photo' => '#m_photo_url',
];
}
$headings += [
'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',
];
return $headings;
}
/**
* Given a username, append (or change) a discriminator if the username already exists.
* A discriminator is a # followed by 4 random letters or numbers suffixed at the end of a username (in rare cases, it might also contain a - or _).
*
* @param SHORT_TEXT $username The desired name for the member profile
* @param boolean $username_only Whether we just want the username without a discriminator, and $username might contain a discriminator
* @return SHORT_TEXT A username with a discriminator if necessary
*/
function process_username_discriminator(string $username, bool $username_only = false) : string
{
$username = preg_replace('#\#\w{4}$#', '', $username);
if ($username_only) {
return $username;
}
$_username = $username;
// Get existing usernames matching itself or any discriminator
$rows = $GLOBALS['FORUM_DB']->query_parameterised('SELECT DISTINCT m_username FROM {prefix}f_members WHERE m_username LIKE \'' . db_encode_like(db_escape_string('{username}#%')) . '\' OR ' . db_string_equal_to('{username}', $username), ['username' => $username]);
$usernames = collapse_1d_complexity('m_username', $rows);
$time_taken = microtime(true);
do {
$test = in_array($_username, $usernames);
if ($test) {
require_code('crypt');
if (count($usernames) < 100000) { // Should rarely ever happen but base32 affords us a little over 1 million discriminators; we should opt for base64 if we have 100k+ matches.
$_username = $username . '#' . get_secure_random_string(4, CRYPT_BASE32);
} else {
$_username = $username . '#' . get_secure_random_string(4, CRYPT_BASE64);
}
if ((microtime(true) - $time_taken) >= 3.0) { // Stop trying and error if we still do not have a good username after 3 seconds
warn_exit(do_lang_tempcode('INTERNAL_ERROR', escape_html('8de41393322c53b49b8527f890d314f1')));
}
}
} while ($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 ID_TEXT $type The type of member profile we are finishing off
* @param SHORT_TEXT $username The username for the member profile
* @param EMAIL $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(string $type, string $username, string $email_address = '', ?int $dob_day = null, ?int $dob_month = null, ?int $dob_year = null, ?string $timezone = null, ?string $language = null) : object
{
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 = process_username_discriminator($username);
}
list($fields, $hidden) = cns_get_member_fields(true, $type, null, $username, $email_address, null, null, $dob_day, $dob_month, $dob_year, null, $timezone, null, $language);
$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', [
'_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 ID_TEXT $type The type of member profile we are finishing off
* @param SHORT_TEXT $username The username for the member profile
* @param SHORT_TEXT $password The password for the member profile
* @param boolean $email_check Whether to check for duplicated e-mail addresses
* @param EMAIL $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)
* @return MEMBER The member ID for the finished off profile
*/
function cns_member_external_linker(string $type, string $username, string $password, bool $email_check = true, string $email_address = '', ?int $dob_day = null, ?int $dob_month = null, ?int $dob_year = null, ?string $timezone = null, ?string $language = null, ?string $avatar_url = null, string $photo_url = '') : int
{
// Read in data...
require_code('temporal');
require_code('temporal2');
require_code('cns_groups');
$email_address = post_param_string('email', $email_address, INPUT_FILTER_POST_IDENTIFIER);
$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', ['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();
}
list($dob_year, $dob_month, $dob_day) = post_param_date_components('birthday', $dob_year, $dob_month, $dob_day);
$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] = '';
}
}
$timezone = post_param_string('timezone', $timezone);
$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
$reveal_age = post_param_integer('reveal_age', 0); // For default privacy, default off
// 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', ['m_email_address' => $email_address]);
if ($test !== null) {
global $MEMBER_CACHED;
$MEMBER_CACHED = db_get_first_id();
$reset_url = build_url(['page' => 'lost_password', 'email' => $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, // username
$password, // password
$email_address, // email_address
null, // primary_group
$groups, // secondary_groups
$dob_day, // dob_day
$dob_month, // dob_month
$dob_year, // dob_year
$actual_custom_fields, // custom_fields
$timezone, // timezone
'', // TODO: region
$language, // language
'', // theme
'', // title
$photo_url, // photo_url
$avatar_url, // avatar_url
'', // signature
1, // preview_posts
$reveal_age, // reveal_age
1, // views_signatures
null, // auto_monitor_contrib_content
null, // smart_topic_notification
null, // mailing_list_style
1, // auto_mark_read
null, // sound_enabled
$allow_emails, // allow_emails
$allow_emails_from_staff, // allow_emails_from_staff
0, // highlighted_name
'*', // pt_allow
'', // pt_rules_text
$validated, // validated
'', // validated_email_confirm_code
null, // probation_expiration_time
'0', // is_perm_banned
false, // check_correctness
null, // ip_address
$type // password_compatibility_scheme
);
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(array $custom_fields, ?int $member_id = null) : array
{
require_code('fields');
require_code('cns_members_action');
$actual_custom_fields = [];
foreach ($custom_fields as $custom_field) {
$ob = get_fields_hook($custom_field['cf_type']);
$old_value = ($member_id === null) ? null : $GLOBALS['FORUM_DB']->query_select_value_if_there('f_member_custom_fields', 'field_' . strval($custom_field['id']), ['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 : ['cv_value' => $old_value]);
// Required field validation (a standard for all field hooks except tick)
if (($custom_field['cf_type'] != 'tick') && ($custom_field['cf_required'] == 1) && (($value == '') || ($value === null) || (($value == STRING_MAGIC_NULL) && !fractional_edit()))) {
warn_exit(do_lang_tempcode('_REQUIRED_NOT_FILLED_IN', get_translated_tempcode('f_custom_fields', $custom_field, 'cf_name')));
}
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 account.
*
* @param boolean $mini_mode Whether we are only handling the essential details of a profile
* @param ID_TEXT $special_type The special type of profile this is (blank: not a special type)
* @param ?MEMBER $member_id The ID of the member we are handling (null: new member)
* @param SHORT_TEXT $username The username
* @param SHORT_TEXT $email_address The e-mail address
* @param ?GROUP $primary_group The member's primary usergroup (null: not known)
* @param ?array $groups A list of usergroups (null: default/current usergroups)
* @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 ?array $custom_fields A map of custom fields values (field-id=>value) (null: not known)
* @param ?ID_TEXT $timezone The member timezone (null: site default)
* @param ?ID_TEXT $region The member region (null: not known)
* @param ?LANGUAGE_NAME $language The member's language (null: auto detect)
* @param ?ID_TEXT $theme The member's default theme (null: not known)
* @param BINARY $preview_posts Whether posts are previewed before they are made
* @param BINARY $reveal_age Whether the member's age may be shown
* @param BINARY $views_signatures Whether the member sees signatures in posts
* @param ?BINARY $auto_monitor_contrib_content Whether the member automatically is enabled for notifications for content they contribute to (null: get default from config)
* @param ?BINARY $smart_topic_notification Whether to do smart topic notification [i.e. avoid sending so many notifications] (null: global configured default)
* @param ?BINARY $mailing_list_style Whether to send mailing-list style notifications (null: global configured default)
* @param BINARY $auto_mark_read Mark topics as read automatically
* @param ?BINARY $sound_enabled Whether sound is enabled (null: global configured default)
* @param BINARY $allow_emails Whether the member allows e-mails via the site
* @param BINARY $allow_emails_from_staff Whether the member allows e-mails from staff via the site
* @param BINARY $highlighted_name Whether the member username will be highlighted
* @param SHORT_TEXT $pt_allow Usergroups that may PT the member
* @param LONG_TEXT $pt_rules_text Rules that other members must agree to before they may start a PT with the member
* @param BINARY $validated Whether the account has been validated
* @param ?TIME $probation_expiration_time When the member is on probation until (null: just finished probation / or effectively was never on it)
* @param ID_TEXT $is_perm_banned Whether the member is permanently banned
* @param integer $parental_consent The parental consent status of the member
* @set 0 1 2
* @param array $adjusted_config_options A map of adjusted config options
* @return array A tuple: The form fields, Hidden fields (both Tempcode), Whether separate sections were used
*/
function cns_get_member_fields(bool $mini_mode = true, string $special_type = '', ?int $member_id = null, string $username = '', string $email_address = '', ?int $primary_group = null, ?array $groups = null, ?int $dob_day = null, ?int $dob_month = null, ?int $dob_year = null, ?array $custom_fields = null, ?string $timezone = null, ?string $region = null, ?string $language = null, ?string $theme = null, int $preview_posts = 0, int $reveal_age = 1, int $views_signatures = 1, ?int $auto_monitor_contrib_content = null, ?int $smart_topic_notification = null, ?int $mailing_list_style = null, int $auto_mark_read = 1, ?int $sound_enabled = null, int $allow_emails = 1, int $allow_emails_from_staff = 1, int $highlighted_name = 0, string $pt_allow = '*', string $pt_rules_text = '', int $validated = 1, ?int $probation_expiration_time = null, string $is_perm_banned = '0', int $parental_consent = 0, array $adjusted_config_options = []) : array
{
$fields = new Tempcode();
$hidden = new Tempcode();
list($_fields, $_hidden, $added_section_1) = cns_get_member_fields_settings($mini_mode, $special_type, $member_id, $username, $email_address, $primary_group, $groups, $dob_day, $dob_month, $dob_year, $timezone, $region, $language, $theme, $preview_posts, $reveal_age, $views_signatures, $auto_monitor_contrib_content, $smart_topic_notification, $mailing_list_style, $auto_mark_read, $sound_enabled, $allow_emails, $allow_emails_from_staff, $highlighted_name, $pt_allow, $pt_rules_text, $validated, $probation_expiration_time, $is_perm_banned, $parental_consent, $adjusted_config_options);
$fields->attach($_fields);
$hidden->attach($_hidden);
if (!$mini_mode) {
$fields->attach(do_template('FORM_SCREEN_FIELD_SPACER', [
'_GUID' => '14205f6bf83c469a1404d24967d7b6f6',
'TITLE' => do_lang_tempcode('PROFILE'),
'SECTION_HIDDEN' => (get_page_name() == 'admin_cns_members'),
]));
$added_section_1 = true;
}
list($_fields, $_hidden, $added_section_2) = cns_get_member_fields_profile($mini_mode, $member_id, $groups, $custom_fields, $adjusted_config_options);
$fields->attach($_fields);
$hidden->attach($_hidden);
return [$fields, $hidden, $added_section_1 || $added_section_2];
}
/**
* Get form fields for adding/editing/finishing a member account: settings only.
*
* @param boolean $mini_mode Whether we are only handling the essential details of a profile
* @param ID_TEXT $special_type The special type of profile this is (blank: not a special type)
* @param ?MEMBER $member_id The ID of the member we are handling (null: new member)
* @param SHORT_TEXT $username The username
* @param SHORT_TEXT $email_address The e-mail address
* @param ?GROUP $primary_group The member's primary usergroup (null: not known)
* @param ?array $groups A list of usergroups (null: default usergroups)
* @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 $region The member region (null: not known)
* @param ?LANGUAGE_NAME $language The member's language (null: auto detect)
* @param ?ID_TEXT $theme The member's default theme (null: not known)
* @param ?BINARY $preview_posts Whether posts are previewed before they are made (null: calculate statistically)
* @param BINARY $reveal_age Whether the member's age may be shown
* @param BINARY $views_signatures Whether the member sees signatures in posts
* @param ?BINARY $auto_monitor_contrib_content Whether the member automatically is enabled for notifications for content they contribute to (null: get default from config)
* @param ?BINARY $smart_topic_notification Whether to do smart topic notification [i.e. avoid sending so many notifications] (null: global configured default)
* @param ?BINARY $mailing_list_style Whether to send mailing-list style notifications (null: global configured default)
* @param BINARY $auto_mark_read Mark topics as read automatically
* @param ?BINARY $sound_enabled Whether sound is enabled (null: global configured default)
* @param BINARY $allow_emails Whether the member allows e-mails via the site
* @param BINARY $allow_emails_from_staff Whether the member allows e-mails from staff via the site
* @param BINARY $highlighted_name Whether the member username will be highlighted
* @param SHORT_TEXT $pt_allow Usergroups that may PT the member
* @param LONG_TEXT $pt_rules_text Rules that other members must agree to before they may start a PT with the member
* @param BINARY $validated Whether the account has been validated
* @param ?TIME $probation_expiration_time When the member is on probation until (null: just finished probation / or effectively was never on it)
* @param ID_TEXT $is_perm_banned Whether the member is permanently banned
* @param integer $parental_consent The parental consent status of the member
* @set 0 1 2
* @param array $adjusted_config_options A map of adjusted config options
* @return array A pair: The form fields, Hidden fields (both Tempcode), Whether separate sections were used
*/
function cns_get_member_fields_settings(bool $mini_mode = true, string $special_type = '', ?int $member_id = null, string $username = '', string $email_address = '', ?int $primary_group = null, ?array $groups = null, ?int $dob_day = null, ?int $dob_month = null, ?int $dob_year = null, ?string $timezone = null, ?string $region = null, ?string $language = null, ?string $theme = null, ?int $preview_posts = null, int $reveal_age = 1, int $views_signatures = 1, ?int $auto_monitor_contrib_content = null, ?int $smart_topic_notification = null, ?int $mailing_list_style = null, int $auto_mark_read = 1, ?int $sound_enabled = null, int $allow_emails = 1, int $allow_emails_from_staff = 1, int $highlighted_name = 0, string $pt_allow = '*', string $pt_rules_text = '', int $validated = 1, ?int $probation_expiration_time = null, string $is_perm_banned = '0', int $parental_consent = 0, array $adjusted_config_options = []) : array
{
require_code('form_templates');
require_code('cns_members_action');
require_code('cns_field_editability');
require_code('form_templates');
require_code('encryption');
require_code('temporal');
$added_section = false;
if (($special_type == '') && ($member_id !== null)) {
$special_type = get_member_special_type($member_id);
}
$default_primary_group = get_first_default_group();
if ($groups === null) {
$groups = cns_get_all_default_groups(true);
}
$preview_posts = take_param_int_modeavg($preview_posts, 'm_preview_posts', 'f_members', 0);
// Not needed as it is managed on the notifications tab, and may cause errors if cns_forum is not installed
/*
if ($auto_monitor_contrib_content === null) {
$auto_monitor_contrib_content = (get_option_with_overrides('allow_auto_notifications', $adjusted_config_options) == '0') ? 0 : 1;
}
if ($smart_topic_notification === null) {
$smart_topic_notification = (get_option_with_overrides('smart_topic_notification_default', $adjusted_config_options) == '1') ? 1 : 0;
}
if ($mailing_list_style === null) {
$mailing_list_style = (get_option_with_overrides('mailing_list_style_default', $adjusted_config_options) == '1') ? 1 : 0;
}
*/
if ($sound_enabled === null) {
$sound_enabled = ((addon_installed('cns_forum')) && (get_option_with_overrides('sound_enabled_default', $adjusted_config_options) == '1')) ? 1 : 0;
}
$hidden = new Tempcode();
if ($member_id === $GLOBALS['CNS_DRIVER']->get_guest_id()) {
fatal_exit(do_lang_tempcode('INTERNAL_ERROR', escape_html('d9343d1793595470a889a2db4b301813')));
}
if ($timezone === null) {
$timezone = get_site_timezone();
}
$fields = new Tempcode();
// Username
if (cns_field_editable('username', $special_type)) {
if (($member_id === null) || (has_actual_page_access(get_member(), 'admin_cns_members')) || (has_privilege($member_id, 'rename_self'))) {
$prohibit_username_whitespace = get_option_with_overrides('prohibit_username_whitespace', $adjusted_config_options);
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));
}
}
// Work out what options we need to present
$doing_timezones = member_field_is_required($member_id, 'timezone_offset', null, null, $adjusted_config_options);
$doing_region = member_field_is_required($member_id, 'region', null, null, $adjusted_config_options);
if (((multi_lang()) || ((isset($adjusted_config_options['enable_language_selection'])) && ($adjusted_config_options['enable_language_selection'])))) {
$doing_langs = (get_option_with_overrides('enable_language_selection', $adjusted_config_options) == (($member_id === null) ? '2' : '1'));
} else {
$doing_langs = false;
}
$doing_email_option = (get_option_with_overrides('member_email_receipt_configurability', $adjusted_config_options) == (($member_id === null) ? '2' : '1')) && (addon_installed('cns_contact_member'));
$doing_email_from_staff_option = (get_option_with_overrides('staff_email_receipt_configurability', $adjusted_config_options) == (($member_id === null) ? '2' : '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 = get_param_string('email', '', INPUT_FILTER_GET_IDENTIFIER);
}
$email_description = new Tempcode();
$valid_email_domains = get_option_with_overrides('valid_email_domains', $adjusted_config_options);
if (($valid_email_domains != '') && ($mini_mode)) { // domain restriction only applies on public join form ($mini_mode)
$email_description = do_lang_tempcode('MUST_BE_EMAIL_DOMAIN', '*.' . preg_replace('#\s*,\s*#', ', *.', escape_html($valid_email_domains)) . '', escape_html($valid_email_domains));
} else {
if (get_option_with_overrides('email_confirm_join', $adjusted_config_options) == '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', $email_address, $email_address_required));
if (($member_id === null) && ($email_address == '') && (get_option_with_overrides('email_confirm_join', $adjusted_config_options) == '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)) {
$can_edit_birthday = cns_can_edit_birthday($member_id);
$default_time = ($dob_month === null) ? null : usertime_to_utctime(cms_mktime(0, 0, 0, $dob_month, $dob_day, $dob_year));
if (get_option_with_overrides('dobs', $adjusted_config_options) >= (($member_id === null) ? '2' : '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'), $can_edit_birthday ? '' : do_lang_tempcode('DATE_OF_BIRTH_NO_SELF_EDIT'), 'birthday', $dob_required, false, false, $default_time, -130, null, null, true, null, true, null, (($member_id !== null) && (!$can_edit_birthday))));
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_timezones) || ($doing_langs) || ($doing_email_option) || ($doing_wide_option) || ($doing_theme_option) || ($doing_local_forum_options)) {
$fields->attach(do_template('FORM_SCREEN_FIELD_SPACER', ['_GUID' => '3cd79bbea084ec1fe148edddad7d52b4', 'FORCE_OPEN' => ($member_id === null] ? true : null, 'TITLE' => do_lang_tempcode('SETTINGS'))));
$added_section = true;
}
}
*/
// Password (intentionally put down here as the username, e-mail address, and DOB may influence the password strength)
if (cns_field_editable('password', $special_type)) {
if (($member_id === null) || ($member_id == get_member()) || (has_privilege(get_member(), 'assume_any_member'))) {
$compat_scheme = ($member_id === null) ? '' : $GLOBALS['FORUM_DRIVER']->get_member_row_field($member_id, 'm_password_compat_scheme');
$temporary_password = ($member_id !== null) && ($member_id == get_member() && (($compat_scheme == 'temporary'/*LEGACY*/) || ($compat_scheme == 'expired'/*LEGACY*/) || ($compat_scheme == 'bcrypt_temporary') || ($compat_scheme == 'bcrypt_expired')));
if ($temporary_password) {
$password_field_description = do_lang_tempcode('DESCRIPTION_PASSWORD_TEMPORARY');
} else {
$password_field_description = do_lang_tempcode('DESCRIPTION_PASSWORD' . (($member_id !== null) ? '_EDIT' : ''));
}
$fields->attach(form_input_password(do_lang_tempcode(($member_id === null) ? 'PASSWORD' : 'NEW_PASSWORD'), $password_field_description, ($member_id === null) ? 'password' : 'edit_password', $mini_mode || $temporary_password, null, '', null, true));
$fields->attach(form_input_password(do_lang_tempcode('CONFIRM_PASSWORD'), '', 'password_confirm', $mini_mode || $temporary_password));
}
}
require_lang('config');
// Timezones, if enabled
if (cns_field_editable('timezone_offset', $special_type)) {
if ($doing_timezones) {
$can_edit_timezone = cns_can_edit_timezone($member_id);
if ($can_edit_timezone) {
$tz_description = do_lang_tempcode('DESCRIPTION_TIMEZONE_MEMBER');
} else {
$tz_description = do_lang_tempcode('DESCRIPTION_TIMEZONE_MEMBER_LOCKED');
}
$fields->attach(form_input_timezone(do_lang_tempcode('TIMEZONE'), $tz_description, 'timezone', $timezone, true, 10, null, (($member_id !== null) && (!$can_edit_timezone))));
}
}
// Region
if (cns_field_editable('region', $special_type)) {
if ($doing_region) {
require_code('locations');
require_lang('locations');
$can_edit_region = cns_can_edit_region($member_id);
if ($can_edit_region) {
$region_description = do_lang_tempcode('DESCRIPTION_REGION_MEMBER');
} else {
$region_description = do_lang_tempcode('DESCRIPTION_REGION_MEMBER_LOCKED');
}
$fields->attach(form_input_region(do_lang_tempcode('REGION'), $region_description, 'region', $region, true, (($member_id !== null) && (!$can_edit_region))));
}
}
// Language choice, if we have multiple languages on site
if ($doing_langs) {
$lang_list = new Tempcode();
$require_lang_set = (get_value('disable_required_lang_selection') !== '1');
if (!$require_lang_set) {
$lang_list->attach(form_input_list_entry('', empty($language), do_lang_tempcode('UNSET')));
} else {
if (empty($language)) {
$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, $require_lang_set));
}
if (!$mini_mode) {
// Theme, if we have any zones giving a choice
require_code('themes2');
$entries = create_selection_list_themes($theme, true, 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_with_overrides('forced_preview_option', $adjusted_config_options) == '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_with_overrides('enable_views_sigs_option', $adjusted_config_options) === '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'));
}
}
/*
Actually managed on the notifications tab, even though technically account settings
$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));
$fields->attach(form_input_tick(do_lang_tempcode('SMART_TOPIC_NOTIFICATION'), do_lang_tempcode('DESCRIPTION_SMART_TOPIC_NOTIFICATION'), 'smart_topic_notification', $smart_topic_notification == 1));
if (addon_installed('cns_forum')) {
require_code('cns_forums2');
require_lang('cns_mailinglists');
$test = cns_has_mailing_list_style();
if ($test[0] > 0) {
$mlsn_description_caveat = $test[1] ? new Tempcode() : do_lang_tempcode('DESCRIPTION_MAILING_LIST_STYLE_CAVEAT');
$mlsn_description = do_lang_tempcode('DESCRIPTION_MAILING_LIST_STYLE', $mlsn_description_caveat);
$fields->attach(form_input_tick(do_lang_tempcode('MAILING_LIST_STYLE'), $mlsn_description, 'mailing_list_style', $mailing_list_style == 1));
}
}
*/
if (get_option_with_overrides('is_on_automatic_mark_topic_read', $adjusted_config_options) == '0') {
$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'));
}
$fields->attach(form_input_tick(do_lang_tempcode('SOUND_ENABLED'), do_lang_tempcode('DESCRIPTION_SOUND_ENABLED'), 'sound_enabled', $sound_enabled == 1));
$usergroup_list = new Tempcode();
$lgroups = $GLOBALS['CNS_DRIVER']->get_usergroup_list(true, true, false, [], null, true);
foreach ($lgroups as $key => $val) {
if ($key != db_get_first_id()) {
$usergroup_list->attach(form_input_list_entry(strval($key), ($pt_allow == '*') || (!empty(array_intersect([strval($key)], explode(',', $pt_allow)))), $val));
}
}
if (get_option_with_overrides('enable_pt_restrict', $adjusted_config_options) == '1') {
$fields->attach(do_template('FORM_SCREEN_FIELD_SPACER', ['_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));
$added_section = true;
}
}
// Prepare list of usergroups, if maybe we are gonna let (a) usergroup-change field(s)
$group_count = $GLOBALS['FORUM_DB']->get_table_count_approx('f_groups');
$rows = $GLOBALS['FORUM_DB']->query_select('f_groups', ['id', 'g_name', 'g_hidden', 'g_open_membership', 'g_order'], ($group_count > 200) ? ['g_is_private_club' => 0] : [], 'ORDER BY g_order,' . $GLOBALS['FORUM_DB']->translate_field_ref('g_name'));
$_groups = new Tempcode();
$current_primary_group = null;
foreach ($rows as $group) {
if ($group['id'] != db_get_first_id()) {
$selected = ($group['id'] == $primary_group) || (($primary_group === null) && ($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', ['_GUID' => '04422238c372edd0b11c11a05feb6267', 'TITLE' => do_lang_tempcode('MEMBER_ACCESS')]));
$added_section = true;
// Probation
if (has_privilege(get_member(), 'probate_members')) {
if (($member_id !== null) && ($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('PROBATION_EXPIRATION_TIME'), do_lang_tempcode('DESCRIPTION_PROBATION_EXPIRATION_TIME'), 'probation_expiration_time', false, ($probation_expiration_time === null) || $probation_expiration_time <= time(), true, $probation_expiration_time, 2));
}
}
// Primary usergroup
if (cns_field_editable('primary_group', $special_type)) {
if (has_privilege(get_member(), 'assume_any_member')) {
if (($member_id === null) || (!$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 = ($member_id === null) ? [] : cns_get_members_groups($member_id, false, false, false); // We can't use $groups because it isn't tuned for a form
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()) && ((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) && ($group['id'] != $current_primary_group);
$_groups2->attach(form_input_list_entry(strval($group['id']), $selected, get_translated_text($group['g_name'], $GLOBALS['FORUM_DB'])));
}
}
$sec_url = build_url(['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')) {
// Name highlighting
if (get_option_with_overrides('enable_highlight_name', $adjusted_config_options) == '1') {
$fields->attach(form_input_tick(do_lang_tempcode('HIGHLIGHTED_NAME'), do_lang_tempcode(addon_installed('ecommerce') ? 'DESCRIPTION_HIGHLIGHTED_NAME_P' : 'DESCRIPTION_HIGHLIGHTED_NAME'), 'highlighted_name', $highlighted_name == 1));
}
// Validation
$_validated = get_param_integer('validated', 0);
if ($validated == 0) {
if (($_validated == 1) && (addon_installed('validation'))) {
$validated = 1;
attach_message(do_lang_tempcode('WILL_BE_VALIDATED_WHEN_SAVING'), 'notice');
}
} elseif (($validated == 1) && ($_validated == 1) && ($member_id !== null)) {
$action_log = build_url(['page' => 'admin_actionlog', 'type' => 'list', 'to_type' => 'VALIDATE_MEMBER', 'param_a' => strval($member_id)]);
attach_message(do_lang_tempcode('ALREADY_VALIDATED', escape_html($action_log->evaluate())), 'warn');
}
if (addon_installed('validation')) {
$fields->attach(form_input_tick(do_lang_tempcode('VALIDATED'), do_lang_tempcode('DESCRIPTION_MEMBER_VALIDATED'), 'validated', $validated == 1));
}
// Parental consent
if (get_member() !== $member_id) { // Must not allow staff to edit this setting on their own profile
$consent = new Tempcode();
$consent->attach(form_input_list_entry('0', ($parental_consent == 0), do_lang('PARENTAL_CONSENT_STATUS_0')));
$consent->attach(form_input_list_entry('1', ($parental_consent == 1), do_lang('PARENTAL_CONSENT_STATUS_1')));
$consent->attach(form_input_list_entry('2', ($parental_consent == 2), do_lang('PARENTAL_CONSENT_STATUS_2')));
$fields->attach(form_input_list(do_lang_tempcode('PARENTAL_CONSENT_STATUS'), do_lang_tempcode('DESCRIPTION_PARENTAL_CONSENT_STATUS'), 'parental_consent', $consent));
}
// Banning
if (($member_id !== null) && ($member_id != get_member())) {// Can't ban someone new, and can't ban yourself
require_code('input_filter');
list(, $reasoned_bans) = Source_advanced_banning_loader::load_advanced_banning();
if (empty($reasoned_bans)) {
$fields->attach(form_input_tick(do_lang_tempcode('BANNED'), do_lang_tempcode('DESCRIPTION_MEMBER_BANNED'), 'is_perm_banned', $is_perm_banned != '0'));
} else {
$reasoned_bans_list = new Tempcode();
$reasoned_bans_list->attach(form_input_list_entry('0', '0' == $is_perm_banned, do_lang_tempcode('NO')));
$reasoned_bans_list->attach(form_input_list_entry('1', '1' == $is_perm_banned, do_lang_tempcode('YES')));
foreach (array_keys($reasoned_bans) as $reasoned_ban) {
$reasoned_bans_list->attach(form_input_list_entry($reasoned_ban, $reasoned_ban == $is_perm_banned));
}
$fields->attach(form_input_list(do_lang_tempcode('BANNED'), do_lang_tempcode('DESCRIPTION_MEMBER_BANNED'), 'is_perm_banned', $reasoned_bans_list, null, false, false));
}
$fields->attach(do_template('FORM_SCREEN_FIELD_SPACER', ['_GUID' => '03452238c372edd0b11c11a05feb6267', 'TITLE' => do_lang_tempcode('ACTIONS')]));
}
}
if (addon_installed('content_reviews')) {
require_code('content_reviews2');
$content_review_fields = content_review_get_fields(has_privilege(get_member(), 'member_maintenance'), 'member', ($member_id === null) ? null : strval($member_id));
if (!$content_review_fields->is_empty()) {
$fields->attach($content_review_fields);
$added_section = true;
}
}
}
return [$fields, $hidden, $added_section];
}
/**
* Get form fields for adding/editing/finishing a member account: profile fields only.
*
* @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)
* @param array $adjusted_config_options A map of adjusted config options
* @return array A tuple: The form fields, Hidden fields (both Tempcode), Whether separate sections were used
*/
function cns_get_member_fields_profile(bool $mini_mode = true, ?int $member_id = null, ?array $groups = null, ?array $custom_fields = null, array $adjusted_config_options = []) : array
{
require_code('cns_members_action');
$fields = new Tempcode();
$hidden = new Tempcode();
$added_section = false;
if ($groups === null) {
$groups = ($member_id === null) ? 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 || ($member_id === null) || ($member_id == get_member()) || (has_privilege(get_member(), 'view_any_profile_field'))) ? null : 1, // public view
null, // owner view
($mini_mode || ($member_id === null) || ($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,
$adjusted_config_options
);
$fields_to_skip = _cpfs_internal_use_only();
$GLOBALS['NO_DEV_MODE_FULLSTOP_CHECK'] = true;
$field_groups = [];
require_code('fields');
require_code('encryption');
foreach ($_custom_fields as $custom_field) {
// Skip internal use only fields
if (in_array($custom_field['id'], $fields_to_skip)) {
continue;
}
$ob = get_fields_hook($custom_field['cf_type']);
list(, , $storage_type) = $ob->get_field_value_row_bits($custom_field);
$existing_field = ($custom_fields !== null) && (array_key_exists($custom_field['trans_name'], $custom_fields));
if ($existing_field) {
$value = $custom_fields[$custom_field['trans_name']]['RAW'];
if (is_data_encrypted($value)) {
$value = remove_magic_encryption_marker($value);
}
if (!member_field_is_required($member_id, 'required_cpfs', $value) && $custom_field['cf_type'] != 'tick'/*FUDGE*/) {
$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 = null;
$_description = escape_html(get_translated_text($custom_field['cf_description'], $GLOBALS['FORUM_DB']));
$field_cat = '';
$matches = [];
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([$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', [
'_GUID' => 'af91e3c040a0a18a4d9cc1143c0d2007',
'TITLE' => $field_group_title,
'SECTION_HIDDEN' => (get_page_name() == 'admin_cns_members'),
]));
$added_section = true;
}
$fields->attach($extra_fields);
}
$GLOBALS['NO_DEV_MODE_FULLSTOP_CHECK'] = false;
return [$fields, $hidden, $added_section];
}
/**
* Return an array of custom fields that should never be editable on the UI.
*
* @return array Array of custom field IDs that should never be editable on the UI
* @ignore
*/
function _cpfs_internal_use_only() : array
{
require_code('cns_members');
$ret = [];
$fields_to_find = ['cms_points_balance', 'cms_points_rank']; // FUDGE
foreach ($fields_to_find as $field) {
$cpf_id = find_cms_cpf_field_id($field);
if ($cpf_id !== null) {
$ret[] = $cpf_id;
}
}
return $ret;
}
/**
* Edit a member.
*
* @param AUTO_LINK $member_id The ID of the member
* @param ?string $username The username (null: don't change)
* @param ?string $password The password, plain-text if compat scheme is software-based, else hashed (null: don't change)
* @param ?SHORT_TEXT $email_address The e-mail address (null: don't change)
* @param ?GROUP $primary_group The member's primary usergroup (null: don't change) (you must handle updating group approvals manually)
* @param ?integer $dob_day Day of date of birth (null: don't change) (-1: unset)
* @param ?integer $dob_month Month of date of birth (null: don't change) (-1: unset)
* @param ?integer $dob_year Year of date of birth (null: don't change) (-1: unset)
* @param ?array $custom_fields A map of custom fields values, things specified are not (field-id=>value) (null: don't change)
* @param ?ID_TEXT $timezone The member timezone (null: don't change)
* @param ?ID_TEXT $region The member region (null: don't change)
* @param ?LANGUAGE_NAME $language The member's language (null: don't change)
* @param ?ID_TEXT $theme The member's default theme (null: don't change)
* @param ?SHORT_TEXT $title The member's title (blank: get from primary) (null: don't change)
* @param ?URLPATH $photo_url Photo URL (null: don't change)
* @param ?URLPATH $avatar_url Avatar (null: don't change)
* @param ?LONG_TEXT $signature Signature (null: don't change)
* @param ?BINARY $preview_posts Whether posts are previewed before they are made (null: don't change)
* @param ?BINARY $reveal_age Whether the member's 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 ?BINARY $smart_topic_notification Whether to do smart topic notification [i.e. avoid sending so many notifications] (null: don't change)
* @param ?BINARY $mailing_list_style Whether to send mailing-list style notifications (null: don't change)
* @param ?BINARY $auto_mark_read Mark topics as read automatically (null: don't change)
* @param ?BINARY $sound_enabled Whether sound is enabled (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 $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 ?BINARY $validated Whether the account has been validated (null: do not change this) (null: don't change)
* @param ?TIME $probation_expiration_time When the member is on probation until (null: don't change)
* @param ?ID_TEXT $is_perm_banned Banned status (null: don't change)
* @param boolean $check_correctness Whether to check details for correctness and do most of the change-triggered e-mails
* @param ?ID_TEXT $password_compat_scheme Password compatibility scheme (null: don't change)
* @param ?SHORT_TEXT $salt Password salt (null: don't change) (blank: generate a new one depending on our password compat scheme)
* @param ?TIME $join_time When the member joined (null: don't change)
* @param ?boolean $sensitive_change_alert Whether to send an alert to the member that their login information has changed, if applicable (null: use the value of $check_correctness)
*/
function cns_edit_member(int $member_id, ?string $username = null, ?string $password = null, ?string $email_address = null, ?int $primary_group = null, ?int $dob_day = null, ?int $dob_month = null, ?int $dob_year = null, ?array $custom_fields = null, ?string $timezone = null, ?string $region = null, ?string $language = null, ?string $theme = null, ?string $title = null, ?string $photo_url = null, ?string $avatar_url = null, ?string $signature = null, ?int $preview_posts = null, ?int $reveal_age = null, ?int $views_signatures = null, ?int $auto_monitor_contrib_content = null, ?int $smart_topic_notification = null, ?int $mailing_list_style = null, ?int $auto_mark_read = null, ?int $sound_enabled = null, ?int $allow_emails = null, ?int $allow_emails_from_staff = null, ?int $highlighted_name = null, ?string $pt_allow = '*', ?string $pt_rules_text = '', ?int $validated = null, ?int $probation_expiration_time = null, ?string $is_perm_banned = null, bool $check_correctness = true, ?string $password_compat_scheme = null, ?string $salt = null, ?int $join_time = null, ?bool $sensitive_change_alert = null)
{
// Cannot edit a member pending deletion
if ($GLOBALS['CNS_DRIVER']->get_member_row_field($member_id, 'm_password_compat_scheme') == 'pending_deletion') {
warn_exit(do_lang_tempcode('MEMBER_PENDING_DELETION'));
}
if ($sensitive_change_alert === null) {
$sensitive_change_alert = $check_correctness;
}
require_code('type_sanitisation');
require_code('cns_members_action');
$update = [];
$old_email_address = $GLOBALS['CNS_DRIVER']->get_member_row_field($member_id, 'm_email_address');
$old_username = $GLOBALS['CNS_DRIVER']->get_member_row_field($member_id, 'm_username');
$old_password_hashed = $GLOBALS['CNS_DRIVER']->get_member_row_field($member_id, 'm_pass_hash_salted');
$old_password_compat_scheme = $GLOBALS['CNS_DRIVER']->get_member_row_field($member_id, 'm_password_compat_scheme');
// Check for invalid or used e-mails
if ($check_correctness) {
if ((!cms_empty_safe($email_address)) && ($email_address != STRING_MAGIC_NULL) && (!is_valid_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', ['m_email_address' => $email_address]);
if (($test !== null) && ($test != $member_id)) {
warn_exit(do_lang_tempcode('_EMAIL_ADDRESS_IN_USE'));
}
}
}
// Check for valid username
if ($username !== null && $check_correctness) {
require_code('temporal');
cns_check_name_valid($username, $member_id, $password, $email_address, ($dob_year === null) ? null : cms_mktime(12, 0, 0, $dob_month, $dob_day, $dob_year));
require_code('urls2');
suggest_new_idmoniker_for('members', 'view', strval($member_id), '', $username);
}
// Password change
if ($password !== null) {
// Invalidate any existing login key
$update['m_login_key_hash'] = '';
// Determine password scheme
if ($password_compat_scheme === null) {
if (get_value('disable_password_hashing') === '1') {
$password_compat_scheme = 'plain';
} else {
$password_compat_scheme = $GLOBALS['CNS_DRIVER']->get_member_row_field($member_id, 'm_password_compat_scheme');
}
} else {
$update['m_password_compat_scheme'] = $password_compat_scheme;
require_code('users_active_actions');
handle_active_logout__login_providers($member_id);
}
// Process some update code depending on our scheme
$update['m_password_change_code'] = '';
$update['m_password_change_code_time'] = null;
switch ($password_compat_scheme) {
case 'plain': // Do not allow unless hashing disabled; force to bcrypt for security
case 'md5': // Do not allow unless hashing disabled; force to bcrypt for security
if (get_value('disable_password_hashing') === '1') {
break;
}
// no break
case '': // Old v10 bcrypt
$update['m_password_compat_scheme'] = 'bcrypt';
handle_active_logout__login_providers($member_id);
// no break
case 'bcrypt':
case 'bcrypt_temporary':
case 'bcrypt_expired':
require_code('crypt');
if (($salt !== null) && ($salt != '')) {
$update['m_pass_salt'] = $salt;
} else {
if ($salt === null) {
$salt = $GLOBALS['CNS_DRIVER']->get_member_row_field($member_id, 'm_pass_salt');
}
if ($salt == '') { // Generate a new salt
$salt = get_secure_random_string(32, CRYPT_BASE64);
$update['m_pass_salt'] = $salt;
}
}
$password_hashed = ratchet_hash($password, $salt);
break;
default: // Some special scheme; we assume $password is already hashed in this case
$password_hashed = $password;
if ($salt !== null) {
$update['m_pass_salt'] = $salt;
}
}
$update['m_pass_hash_salted'] = $password_hashed;
// Set when the password must be changed, if applicable
$password_change_days = get_option('password_change_days');
if (intval($password_change_days) > 0) {
if ($password_compat_scheme == 'bcrypt') {
require_code('password_rules');
bump_password_change_date($member_id, $password, $check_correctness);
$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
}
}
}
$changes = [];
if ($custom_fields !== null) {
$groups = $GLOBALS['CNS_DRIVER']->get_members_groups($member_id);
$all_fields = cns_get_all_custom_fields_match(
$groups, // groups
(($member_id == get_member()) || (has_privilege(get_member(), 'view_any_profile_field'))) ? null : 1, // public view
null, // owner view
(($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
null // show on join form
);
$fields_to_skip = _cpfs_internal_use_only();
$phone_number_field = find_cms_cpf_field_id('cms_mobile_phone_number');
if ($phone_number_field !== null) {
$phone_number = null;
$old_phone_number = get_cms_cpf('mobile_phone_number', $member_id);
}
// Set Custom Profile Field values
$all_fields_types = collapse_2d_complexity('id', 'cf_type', $all_fields);
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)
}
if (in_array($field_id, $fields_to_skip)) {
continue; // Trying to set a field we're not allowed to (internal field)
}
if ($field_id === $phone_number_field) {
// Log phone number changes
if ($value != $old_phone_number) {
log_it('EDIT_MEMBER_PHONE_NUMBER', strval($member_id), $old_phone_number);
}
$phone_number = $value;
}
$change = cns_set_custom_field($member_id, $field_id, $value, $all_fields_types[$field_id], true);
if ($change !== null) {
$changes = array_merge($changes, $change);
}
}
if (!empty($changes)) {
$GLOBALS['FORUM_DB']->query_update('f_member_custom_fields', $changes, ['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');
// Begin populating what needs updated
if ($email_address !== null) {
$update['m_email_address'] = $email_address;
}
$can_edit_birthday = cns_can_edit_birthday($member_id);
if ($can_edit_birthday) {
if ($dob_day !== null) {
$update['m_dob_day'] = ($dob_day == -1) ? null : $dob_day;
}
if ($dob_month !== null) {
$update['m_dob_month'] = ($dob_month == -1) ? null : $dob_month;
}
if ($dob_year !== null) {
$update['m_dob_year'] = ($dob_year == -1) ? null : $dob_year;
}
}
$can_edit_timezone = cns_can_edit_timezone($member_id);
if ($can_edit_timezone) {
if ($timezone !== null) {
$update['m_timezone_offset'] = $timezone;
}
}
$can_edit_region = cns_can_edit_region($member_id);
if ($can_edit_region) {
if ($region !== null) {
$update['m_region'] = $region;
}
}
if ($language !== null) {
$update['m_language'] = $language;
}
if ($theme !== null) {
$update['m_theme'] = $theme;
}
if ($photo_url !== null) {
$update['m_photo_url'] = $photo_url;
}
if ($title !== null) {
$update['m_title'] = $title;
}
if ($avatar_url !== null) {
$update['m_avatar_url'] = $avatar_url;
}
if ($signature !== null) {
$update += lang_remap_comcode('m_signature', $_signature, $signature, $GLOBALS['FORUM_DB']);
}
if ($preview_posts !== null) {
$update['m_preview_posts'] = $preview_posts;
}
if ($reveal_age !== null) {
$update['m_reveal_age'] = $reveal_age;
}
if ($views_signatures !== null) {
$update['m_views_signatures'] = $views_signatures;
}
if ($auto_monitor_contrib_content !== null) {
$update['m_auto_monitor_contrib_content'] = $auto_monitor_contrib_content;
}
if ($smart_topic_notification !== null) {
$update['m_smart_topic_notification'] = $smart_topic_notification;
}
if ($mailing_list_style !== null) {
$update['m_mailing_list_style'] = $mailing_list_style;
}
if ($auto_mark_read !== null) {
$update['m_auto_mark_read'] = $auto_mark_read;
}
if ($sound_enabled !== null) {
$update['m_sound_enabled'] = $sound_enabled;
}
$doing_email_option = (get_option('member_email_receipt_configurability') != '0') && (addon_installed('cns_contact_member'));
if (($allow_emails !== null) && ($doing_email_option)) {
$update['m_allow_emails'] = $allow_emails;
}
$doing_email_from_staff_option = (get_option('staff_email_receipt_configurability') != '0');
if (($allow_emails_from_staff !== null) && ($doing_email_from_staff_option)) {
$update['m_allow_emails_from_staff'] = $allow_emails_from_staff;
}
if ($pt_allow !== null) {
$update['m_pt_allow'] = $pt_allow;
}
if ($pt_rules_text !== null) {
$update += lang_remap_comcode('m_pt_rules_text', $_pt_rules_text, $pt_rules_text, $GLOBALS['FORUM_DB']);
}
if ((!$check_correctness) || (has_privilege(get_member(), 'probate_members'))) {
$update['m_probation_expiration_time'] = $probation_expiration_time;
}
if ($is_perm_banned !== null) {
$update['m_is_perm_banned'] = $is_perm_banned;
}
if ($join_time !== null) {
$update['m_join_time'] = $join_time;
}
if (($username !== null) && ($old_username !== null) && ($username != $old_username) && ((!$check_correctness) || (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', ['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), ['name' => $gallery['name']], '', 1);
}
}
}
// Update author profile
if (addon_installed('news')) {
$GLOBALS['SITE_DB']->query_update('news', ['author' => $username], ['author' => $old_username]);
}
update_member_username_caching($member_id, $username);
log_it('EDIT_MEMBER_USERNAME', strval($member_id), $old_username);
}
if ($password !== null) { // Password change
// Security, clear out sessions from other people on this user - just in case the reset is due to suspicious activity
require_code('users_active_actions');
delete_session_by_member_id($member_id, get_session_id());
// Log the change
log_it('EDIT_MEMBER_PASSWORD', strval($member_id));
}
if ($validated !== null) {
$update['m_validated_email_confirm_code'] = '';
if (addon_installed('validation')) {
$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 ($highlighted_name !== null) {
$update['m_highlighted_name'] = $highlighted_name;
}
if ($primary_group !== null) {
$update['m_primary_group'] = $primary_group;
if ($primary_group != $old_primary_group) {
$GLOBALS['FORUM_DB']->query_insert('f_group_join_log', [
'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, ['id' => $member_id], '', 1);
if (get_member() != $member_id) {
log_it('EDIT_MEMBER_PROFILE', strval($member_id), $username);
}
// Send out an account validated e-mail if the member is being marked valid, and also log it
$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(['page' => 'login'], get_module_zone('login'), [], false, false, true);
$login_url = $_login_url->evaluate();
// 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(), $old_username, $login_url, get_lang($member_id));
// Necessary to use dispatch_mail in case the member was locked out of their account
dispatch_mail($vm_subject, $vm_body, '', [$email_address], $old_username, '', '', ['require_recipient_valid_since' => $join_time]);
$current_username = $GLOBALS['FORUM_DRIVER']->get_username(get_member());
log_it('VALIDATE_MEMBER', strval($member_id), $current_username);
}
// Update invites, and log, when e-mail is changed
if ($old_email_address != $email_address) {
$GLOBALS['FORUM_DB']->query_update('f_invites', ['i_email_address' => $old_email_address], ['i_email_address' => $email_address]);
log_it('EDIT_MEMBER_EMAIL', strval($member_id), $old_email_address);
}
// E-mail and notify to inform of sensitive changes (username, password, e-mail, or phone number)
$username_changed = ($username !== null) && ($username !== $old_username);
$email_address_changed = ($email_address !== null) && ($email_address !== $old_email_address);
$password_changed = (array_key_exists('m_pass_hash_salted', $update)) && ($update['m_pass_hash_salted'] != $old_password_hashed);
$phone_number_changed = ($custom_fields !== null) && ($phone_number_field !== null) && ($phone_number !== null) && ($old_phone_number !== $phone_number);
if (($username_changed || $email_address_changed || $password_changed || $phone_number_changed)) {
$current_username = $GLOBALS['FORUM_DRIVER']->get_username(get_member());
$_sensitive_changes = [];
if ($username_changed) {
$_sensitive_changes[] = do_lang('SECURITY_ASPECT_CHANGED__USERNAME', comcode_escape($old_username), comcode_escape($username));
}
if ($email_address_changed) {
require_code('crypt');
// New e-mail should be masked in case the member's original e-mail account is compromised so hackers do not target / spam their new address
$masked_email_address = mask_email_address($email_address);
$_sensitive_changes[] = do_lang('SECURITY_ASPECT_CHANGED__EMAIL_ADDRESS', comcode_escape($old_email_address), comcode_escape($masked_email_address));
}
if ($password_changed) {
$_sensitive_changes[] = do_lang('SECURITY_ASPECT_CHANGED__PASSWORD');
}
if ($phone_number_changed) {
require_code('crypt');
// Mask old and new phone number so hackers of compromised accounts cannot spam members' phones
$masked_old_phone_number = mask_phone_number($old_phone_number);
$masked_phone_number = mask_phone_number($phone_number);
$_sensitive_changes[] = do_lang('SECURITY_ASPECT_CHANGED__PHONE_NUMBER', comcode_escape($masked_old_phone_number), comcode_escape($masked_phone_number));
}
$sensitive_changes = implode("\n", $_sensitive_changes);
$part_b = '';
if (!has_actual_page_access(get_member(), 'admin_cns_members')) { // If change not by an admin
$part_b = do_lang('SECURITY_ASPECT_CHANGED_BODY_2', get_ip_address());
}
// Notify member e-mail addresses if specified
if ((($old_email_address != '') || ($email_address !== null)) && ($sensitive_change_alert)) {
require_code('mail');
if ($old_email_address != '') { // E-mail of security aspects that were changed to the e-mail on file (old)
$cm_subject = do_lang('SECURITY_ASPECT_CHANGED_SUBJECT', comcode_escape($old_username), comcode_escape($current_username), [get_site_name()]);
$cm_body = do_lang('SECURITY_ASPECT_CHANGED_BODY', comcode_escape($old_username), comcode_escape($current_username), [get_site_name(), $sensitive_changes, $part_b]);
dispatch_mail($cm_subject, $cm_body, do_lang('mail:NO_MAIL_WEB_VERSION__SENSITIVE'), [$old_email_address], $old_username, '', '', ['require_recipient_valid_since' => $join_time]);
}
if (($email_address !== null) && ($email_address !== $old_email_address)) { // When a new e-mail is specified, also e-mail the new address a vague message about their e-mail being associated with an account
$cm_subject = do_lang('EMAIL_ASSOCIATED_SUBJECT', comcode_escape($old_username), comcode_escape($current_username), [get_site_name()]);
$cm_body = do_lang('EMAIL_ASSOCIATED_BODY', comcode_escape($old_username), comcode_escape($current_username), [get_site_name(), comcode_escape($email_address)]);
dispatch_mail($cm_subject, $cm_body, do_lang('mail:NO_MAIL_WEB_VERSION__SENSITIVE'), [$email_address], $old_username, '', '', ['require_recipient_valid_since' => $join_time]);
}
}
// Notify staff
require_code('notifications');
$subject = do_lang('STAFF_SECURITY_ASPECT_CHANGED_SUBJECT', comcode_escape($old_username), comcode_escape($current_username), [get_site_name()], get_site_default_lang());
$mail = do_notification_lang('STAFF_SECURITY_ASPECT_CHANGED_BODY', comcode_escape($old_username), comcode_escape($current_username), [comcode_escape(get_site_name()), comcode_escape($sensitive_changes), comcode_escape($part_b)], get_site_default_lang());
Source_notification_dispatcher::dispatch_notification('cns_profile_high_impact_edit', null, $subject, $mail, null, get_member(), ['use_real_from' => true]);
}
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]);
unset($GLOBALS['USERS_GROUPS_CACHE'][$member_id]);
unset($GLOBALS['GROUP_MEMBERS_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));
}
delete_cache_entry('main_members');
if (($GLOBALS['FORUM_DRIVER']->is_super_admin($member_id)) && ($old_email_address == '')) {
delete_cache_entry('main_staff_checklist'); // As it tracks whether admin have e-mail address set
}
require_code('sitemap_xml');
if ($validated == 1) {
notify_sitemap_node_edit('_SEARCH:members:view:' . strval($member_id));
} else {
notify_sitemap_node_delete('_SEARCH:members:view:' . strval($member_id));
}
}
/**
* Delete a member.
*
* @param MEMBER $member_id The ID of the member
* @param ?MEMBER $member_id_deleting The ID of the member doing the deleting (null: current member)
*/
function cns_delete_member(int $member_id, ?int $member_id_deleting = null)
{
$info = $GLOBALS['FORUM_DB']->query_select('f_members', ['id'], ['id' => $member_id], '', 1);
if (!array_key_exists(0, $info)) {
warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'member'));
}
if ($member_id_deleting === null) {
$member_id_deleting = get_member();
}
$username = $GLOBALS['CNS_DRIVER']->get_member_row_field($member_id, 'm_username');
$by_username = $GLOBALS['CNS_DRIVER']->get_member_row_field($member_id_deleting, 'm_username');
$signature = $GLOBALS['CNS_DRIVER']->get_member_row_field($member_id, 'm_signature');
$email_address = $GLOBALS['CNS_DRIVER']->get_member_row_field($member_id, 'm_email_address');
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', ['id' => $member_id], '', 1);
$GLOBALS['FORUM_DB']->query_delete('f_group_members', ['gm_member_id' => $member_id]);
$GLOBALS['FORUM_DB']->query_update('f_groups', ['g_group_lead_member' => get_member()], ['g_group_lead_member' => $member_id]);
require_code('users_active_actions');
delete_session_by_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', ['*'], ['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(['cv_value' => $l]);
}
if ((strpos($storage_type, '_trans') !== false) && ($l !== null)) {
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', ['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');
// Must use cns_mod_log_it instead of log_it because we need to define the member who did the deleting
require_code('cns_general_action2');
cns_mod_log_it('DELETE_MEMBER', strval($member_id), $username, '', $member_id_deleting);
// E-mail the member to inform them their account was deleted
if ($email_address != '') {
$part_b = '';
if (!has_actual_page_access(get_member(), 'admin_cns_members')) { // If change not by an admin
$part_b = do_lang('SECURITY_ASPECT_CHANGED_BODY_2', get_ip_address());
}
require_code('mail');
$dm_subject = do_lang('ACCOUNT_DELETED_SUBJECT', comcode_escape($username), comcode_escape($by_username), [get_site_name()]);
$dm_body = do_lang('ACCOUNT_DELETED_BODY', comcode_escape($username), comcode_escape($by_username), [get_site_name(), comcode_escape($email_address), $part_b]);
dispatch_mail($dm_subject, $dm_body, '', [$email_address], $username, '', '');
}
// Also notify staff
require_code('notifications');
require_lang('cns');
$message = do_notification_lang('MEMBER_DELETED_MAIL', comcode_escape($username), comcode_escape($by_username));
Source_notification_dispatcher::dispatch_notification('cns_member_deleted', null, do_lang('MEMBER_DELETED_SUBJECT', comcode_escape($username)), $message, null, $member_id_deleting);
if ((addon_installed('commandr')) && (!running_script('install')) && (!get_mass_import_mode())) {
require_code('resource_fs');
expunge_resource_fs_moniker('member', strval($member_id));
}
delete_cache_entry('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
* @param ID_TEXT $reasoned_ban The reasoned ban value ('1' is just a regular ban, the norm)
* @param boolean $automatic Whether it is an automatic ban
*/
function cns_ban_member(int $member_id, string $reasoned_ban = '1', bool $automatic = false)
{
if ($reasoned_ban == '0') {
fatal_exit(do_lang_tempcode('INTERNAL_ERROR', escape_html('515848cfdbbd51f785f7ef2b9a6716b4')));
}
$previous_value = $GLOBALS['CNS_DRIVER']->get_member_row_field($member_id, 'm_is_perm_banned');
$GLOBALS['FORUM_DB']->query_update('f_members', ['m_is_perm_banned' => $reasoned_ban], ['id' => $member_id], '', 1);
if ($previous_value != '0') {
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');
log_it($automatic ? 'BAN_MEMBER_AUTOMATIC' : 'BAN_MEMBER', strval($member_id), $username);
require_lang('cns');
$mail = do_lang('BAN_MEMBER_MAIL', $username, get_site_name(), [], get_lang($member_id));
dispatch_mail(do_lang('BAN_MEMBER_MAIL_SUBJECT', null, null, null, get_lang($member_id)), $mail, '', [$email_address], $username, '', '', ['priority' => 2, 'require_recipient_valid_since' => $join_time]);
delete_cache_entry('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(int $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', ['m_is_perm_banned' => '0'], ['id' => $member_id], '', 1);
log_it('UNBAN_MEMBER', strval($member_id), $username);
require_code('mail');
require_lang('cns');
$mail = do_lang('UNBAN_MEMBER_MAIL', $username, get_site_name(), [], get_lang($member_id));
dispatch_mail(do_lang('UNBAN_MEMBER_MAIL_SUBJECT', null, null, null, get_lang($member_id)), $mail, '', [$email_address], $username, '', '', ['priority' => 2, 'require_recipient_valid_since' => $join_time]);
delete_cache_entry('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 to show this field for filling in when a member joins the site
* @param SHORT_TEXT $options Field options
* @param BINARY $include_in_main_search Whether to include in main keyword search
* @param BINARY $allow_template_search Whether to allow template search
* @param ID_TEXT $icon An icon to show the CPF with on the member profiles
* @param ID_TEXT $section A section to show with on the member-links part of member profiles
* @param LONG_TEXT $tempcode This is Tempcode that is used for displaying the field. See the DESCRIPTION_CPF_CODE language string.
* @param ID_TEXT $autofill_type Autofill field name from https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill-field
* @param ID_TEXT $autofill_hint Autofill hint: '' or 'shipping' or 'billing'
*/
function cns_edit_custom_field(int $id, string $name, string $description, string $default, int $public_view, int $owner_view, int $owner_set, int $encrypted, int $required, int $show_in_posts, int $show_in_post_previews, int $order, string $only_group, string $type, int $show_on_join_form, string $options, int $include_in_main_search, int $allow_template_search, string $icon, string $section, string $tempcode, string $autofill_type, string $autofill_hint)
{
if ($only_group == '-1') {
$only_group = '';
}
$info = $GLOBALS['FORUM_DB']->query_select('f_custom_fields', ['cf_name', 'cf_description'], ['id' => $id], '', 1);
$_name = $info[0]['cf_name'];
$_description = $info[0]['cf_description'];
$map = [
'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,
'cf_include_in_main_search' => $include_in_main_search,
'cf_allow_template_search' => $allow_template_search,
'cf_icon' => $icon,
'cf_section' => $section,
'cf_tempcode' => $tempcode,
'cf_autofill_type' => $autofill_type,
'cf_autofill_hint' => $autofill_hint,
];
$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, ['id' => $id], '', 1);
require_code('cns_members_action');
list($_type) = get_cpf_storage_for($type);
if (is_object($GLOBALS['FORUM_DB']->connection_read)) {
$smq = $GLOBALS['FORUM_DB']->strict_mode_query(false);
if ($smq !== null) {
$GLOBALS['FORUM_DB']->query($smq, null, 0, true); // Suppress errors in case access denied
}
}
$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, $include_in_main_search == 1 || $allow_template_search == 1, $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));
}
if (function_exists('persistent_cache_delete')) {
persistent_cache_delete('CUSTOM_FIELD_CACHE');
persistent_cache_delete('LIST_CPFS');
}
delete_cache_entry('main_members');
}
/**
* Delete a Custom Profile Field.
*
* @param AUTO_LINK $id The ID of the Custom Profile Field
*/
function cns_delete_custom_field(int $id)
{
$info = $GLOBALS['FORUM_DB']->query_select('f_custom_fields', ['cf_name', 'cf_description'], ['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'];
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', ['id' => $id], '', 1);
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('delete_cache_entry')) {
delete_cache_entry('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(int $member_id, int $field_id, $value, ?string $type = null, bool $defer = false) : ?array
{
if ($value === STRING_MAGIC_NULL) {
return null;
}
if ($type === null) {
$type = $GLOBALS['FORUM_DB']->query_select_value('f_custom_fields', 'cf_type', ['id' => $field_id]);
}
require_code('cns_members');
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 = ($GLOBALS['FORUM_DB']->query_select_value_if_there('f_custom_fields', 'cf_encrypted', ['cf_encrypted' => 1]) !== null);
}
if ($ANY_FIELD_ENCRYPTED) {
$encrypted = $GLOBALS['FORUM_DB']->query_select_value('f_custom_fields', 'cf_encrypted', ['id' => $field_id]);
if (($encrypted) && (is_string($value))) {
require_code('encryption');
if (is_encryption_enabled()) {
$current = $GLOBALS['FORUM_DB']->query_select_value_if_there('f_member_custom_fields', $db_fieldname, ['mf_member_id' => $member_id]);
if ($current === null) {
return null;
}
if ((is_data_encrypted($current)) && (remove_magic_encryption_marker($value) == remove_magic_encryption_marker($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(['id' => $field_id, 'cf_default' => '', 'cf_type' => $type]);
static $done_one_posting_field = false;
if (strpos($storage_type, '_trans') !== false) {
if (is_integer($value)) {
$value = get_translated_text($value, $GLOBALS['FORUM_DB']);
}
$map = [];
$current = $GLOBALS['FORUM_DB']->query_select_value_if_there('f_member_custom_fields', $db_fieldname, ['mf_member_id' => $member_id]);
if ($current === null) {
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, ['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, ['mf_member_id' => $member_id], '', 1);
}
$ret = null;
} else {
$change = [];
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, ['mf_member_id' => $member_id], '', 1);
}
$ret = $change;
}
if (function_exists('delete_cache_entry')) {
delete_cache_entry('main_members');
}
global $MEMBER_CACHE_FIELD_MAPPINGS;
if (isset($MEMBER_CACHE_FIELD_MAPPINGS, $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 ?EMAIL $email_address The e-mail address that will go with the password (null: unknown)
* @param ?TIME $dob The date of birth that will go with the password (null: unknown)
* @param boolean $return_errors Whether to return errors instead of dying on them
* @return ?Tempcode Error (null: none)
*/
function cns_check_name_valid(?string &$username, ?int $member_id = null, ?string $password = null, ?string $email_address = null, ?int $dob = null, bool $return_errors = false) : ?object
{
// Check it doesn't already exist
if ($username !== null) {
$test = ($member_id === null) ? null : $GLOBALS['FORUM_DB']->query_select_value_if_there('f_members', 'id', ['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 ($test === null) {
$test = $GLOBALS['FORUM_DB']->query_select_value_if_there('f_members', 'id', ['m_username' => $username]);
}
$error = do_lang_tempcode('USERNAME_ALREADY_EXISTS');
if (($test !== null) && ($test !== $member_id)) {
if ($return_errors) {
return $error;
}
warn_exit($error);
}
$username_known_available = ($test === null);
} else {
$username_known_available = false;
}
if ($username !== null) {
// Check for disallowed symbols in username
$disallowed_characters = [/*'<','>','&','"',"'",'$',','*/]; // Actually we can tolerate this stuff
foreach ($disallowed_characters as $disallowed_character) {
if ((strpos($username, $disallowed_character) !== false) && ($username_known_available)) {
$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_known_available)) {
$error = do_lang_tempcode('USERNAME_BAD_SYMBOLS', escape_html('@ / .'));
if ($return_errors) {
return $error;
}
warn_exit($error);
}
}
// Check lengths
if ($username !== null) {
if (get_page_name() != 'admin_cns_members') {
$_maximum_username_length = get_option('maximum_username_length');
$maximum_username_length = min(100, intval($_maximum_username_length));
} else {
$maximum_username_length = 100;
}
if ((cms_mb_strlen($username) > $maximum_username_length) && ($username_known_available)) {
$error = do_lang_tempcode('USERNAME_TOO_LONG', escape_html(integer_format($maximum_username_length)));
if ($return_errors) {
return $error;
}
warn_exit($error);
}
if (get_page_name() != 'admin_cns_members') {
$_minimum_username_length = get_option('minimum_username_length');
$minimum_username_length = max(1, intval($_minimum_username_length));
} else {
$minimum_username_length = 1;
}
if ((cms_mb_strlen($username) < $minimum_username_length) && ($username_known_available)) {
$error = do_lang_tempcode('USERNAME_TOO_SHORT', escape_html(integer_format($minimum_username_length)));
if ($return_errors) {
return $error;
}
warn_exit($error);
}
}
if (get_page_name() != 'admin_cns_members') {
if ($password !== null) {
require_code('password_rules');
$test = check_password_complexity($password, ($username === null) ? '' : $username, ($email_address === null) ? '' : $email_address, $dob, $return_errors);
if ($test !== null) {
return $test;
}
}
}
// Check for whitespace
if ($username !== null) {
$prohibit_username_whitespace = get_option('prohibit_username_whitespace');
if (($prohibit_username_whitespace === '1') && (cms_preg_match_safe('#\s#', $username) != 0) && ($username_known_available)) {
$error = do_lang_tempcode('USERNAME_PASSWORD_WHITESPACE');
if ($return_errors) {
return $error;
}
warn_exit($error);
}
}
if ($password !== null) {
$prohibit_password_whitespace = get_option('prohibit_password_whitespace');
if (($prohibit_password_whitespace === '1') && (cms_preg_match_safe('#\s#', $password) != 0) && ($username_known_available)) {
$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_known_available)) {
$restricted_usernames = explode(',', get_option('restricted_usernames'));
$restricted_usernames[] = do_lang('GUEST');
$restricted_usernames[] = do_lang('UNKNOWN');
$restricted_usernames[] = do_lang('DELETED');
$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(string $new_title, ?int $member_id = null)
{
if (!addon_installed('cns_member_titles')) {
return;
}
if ($member_id === null) {
$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', ['m_title' => $new_title], ['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]);
delete_cache_entry('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(string $new_signature, ?int $member_id = null)
{
if ($member_id === null) {
$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) {
require_lang('cns');
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 = [];
$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, ['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));
Source_notification_dispatcher::dispatch_notification('cns_profile_high_impact_edit', null, $subject, $body, null, get_member(), ['use_real_from' => 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(string $avatar_url, ?int $member_id = null)
{
if ($member_id === null) {
$member_id = get_member();
}
$old = $GLOBALS['FORUM_DB']->query_select_value('f_members', 'm_avatar_url', ['id' => $member_id]);
if ($old == $avatar_url) {
return;
}
// Check it has valid dimensions
if ($avatar_url != '') {
require_code('images');
if (!is_image($avatar_url, IMAGE_CRITERIA_WEBSAFE, has_privilege($member_id, 'comcode_dangerous'), 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) . '/') : '';
$file_path_stub = convert_url_to_path($stub . $avatar_url);
if ($file_path_stub !== null) {
$from_file = @strval(cms_file_get_contents_safe($file_path_stub, FILE_READ_LOCK));
} else {
$from_file = http_get_contents($stub . $avatar_url, ['byte_limit' => 1024 * 1024 * 4/*reasonable limit*/, 'triger_error' => false]);
}
$test = cms_getimagesizefromstring($from_file, get_file_extension($avatar_url));
if (($test !== null) && ($test[0] !== null) && ($test[1] !== null)) { // If we can get a size (if we can't it could mean many things - e.g. vector, missing, corrupt)
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)) {
// Size down, if possible
require_code('images');
if ((!is_image($avatar_url, IMAGE_CRITERIA_GD_WRITE)) || (!url_is_local($avatar_url)) || (substr($avatar_url, 0, 20) != 'uploads/cns_avatars/')) {
if ((url_is_local($avatar_url)) && (substr($avatar_url, 0, 20) == 'uploads/cns_avatars/')) {
unlink(get_custom_file_base() . '/' . rawurldecode($avatar_url));
sync_file(get_custom_file_base() . '/' . rawurldecode($avatar_url));
}
warn_exit(do_lang_tempcode('IMAGE_BAD_DIMENSIONS', escape_html(strval($width) . 'x' . strval($height)), escape_html(strval($sx) . 'x' . strval($sy))));
}
$file_path = get_custom_file_base() . '/' . rawurldecode($avatar_url);
$avatar_url = convert_image($file_path, $file_path, $width, $height, null, 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));
Source_notification_dispatcher::dispatch_notification('cns_profile_high_impact_edit', null, $subject, $body, null, get_member(), ['use_real_from' => 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', ['m_avatar_url' => $avatar_url], ['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]);
delete_cache_entry('main_friends_list');
delete_cache_entry('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(string $param_name, string $upload_name, ?int $member_id = null)
{
if ($member_id === null) {
$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', ['id' => $member_id]);
$x = post_param_string($param_name, '', INPUT_FILTER_URL_GENERAL);
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 need to do anything more
}
}
// 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', OBFUSCATE_NEVER, CMS_UPLOAD_IMAGE, true, '', '', false, true);
reset_images_cleanup_pipeline_settings();
if (((get_base_url() != get_forum_base_url()) || (!empty($GLOBALS['SITE_INFO']['on_msn']))) && ($urls[0] != '') && (url_is_local($urls[0]))) {
$urls[0] = get_base_url() . '/' . $urls[0];
}
cns_member_choose_photo_concrete($urls[0], $member_id);
delete_cache_entry('main_members');
}
/**
* Edit a member's photo.
*
* @param URLPATH $url URL to photo
* @param ?MEMBER $member_id The member (null: the current member)
*/
function cns_member_choose_photo_concrete(string $url, ?int $member_id = null)
{
if ($member_id === null) {
$member_id = get_member();
}
// Cleanup old photo
$old = $GLOBALS['FORUM_DB']->query_select_value('f_members', 'm_photo_url', ['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', ['m_photo_url' => $url], ['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, $GLOBALS['FORUM_DRIVER']->get_username($member_id), [$GLOBALS['FORUM_DRIVER']->get_username($member_id, true)], get_lang($member_id));
Source_notification_dispatcher::dispatch_notification('cns_profile_high_impact_edit', null, $subject, $body, null, get_member(), ['use_real_from' => true]);
// If Avatars addon not installed, use photo for it
if (!addon_installed('cns_avatars')) {
$avatar_url = $url;
$stub = url_is_local($avatar_url) ? (get_complex_base_url($avatar_url) . '/') : '';
$file_path = convert_url_to_path($stub . $avatar_url);
if ($file_path !== null) {
$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]);
delete_cache_entry('main_friends_list');
delete_cache_entry('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(int $member_id, string $username)
{
// Fix caching for usernames
$to_fix = [
'f_forums/f_cache_last_username/f_cache_last_member_id',
'f_posts/p_poster_name_if_guest/p_posting_member',
'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);
$db = get_db_for($table);
$db->query_update($table, [$field => $username], [$updating_field => $member_id]);
}
}
/**
* Get details of predefined templated fields.
*
* @return array List of predefined templated fields, each being a map
*/
function cns_predefined_custom_field_details() : array
{
return [
'sn_twitter' => [
'type' => 'codename',
'icon' => 'icons/links/twitter',
'section' => '',
'tempcode' => '{NAME*}',
],
/* Shut down as of May 2025
'im_skype' => [
'type' => 'codename',
'icon' => 'icons/links/skype',
'section' => 'contact',
'tempcode' => '{NAME*}',
],
*/
'im_jabber' => [
'type' => 'codename',
'icon' => 'icons/links/jabber',
'section' => 'contact',
'tempcode' => '{NAME*}',
],
'im_discord' => [
'type' => 'codename',
'icon' => 'icons/links/discord',
'section' => 'contact',
'tempcode' => '{NAME*}: {RAW*}',
],
'github' => [
'type' => 'codename',
'icon' => 'icons/links/github',
'section' => '',
'tempcode' => '{NAME*}',
],
'gitlab' => [
'type' => 'codename',
'icon' => 'icons/links/gitlab',
'section' => '',
'tempcode' => '{NAME*}',
],
'sn_instagram' => [
'type' => 'codename',
'icon' => 'icons/links/instagram',
'section' => '',
'tempcode' => '{NAME*}',
],
'sn_tiktok' => [
'type' => 'codename',
'icon' => 'icons/links/tiktok',
'section' => '',
'tempcode' => '{NAME*}: {RAW*}',
],
'sn_minds' => [
'type' => 'codename',
'icon' => 'icons/links/minds',
'section' => '',
'tempcode' => '{NAME*}',
],
'sn_pinterest' => [
'type' => 'codename',
'icon' => 'icons/links/pinterest',
'section' => '',
'tempcode' => '{NAME*}',
],
'sn_snapchat' => [
'type' => 'codename',
'icon' => 'icons/links/snapchat',
'section' => '',
'tempcode' => '{NAME*}',
],
'soundcloud' => [
'type' => 'codename',
'icon' => 'icons/links/soundcloud',
'section' => '',
'tempcode' => '{NAME*}',
],
'im_telegram' => [
'type' => 'codename',
'icon' => 'icons/links/telegram',
'section' => 'contact',
'tempcode' => '{NAME*}: {RAW*}',
],
'sn_tumblr' => [
'type' => 'codename',
'icon' => 'icons/links/tumblr',
'section' => '',
'tempcode' => '{NAME*}',
],
'sn_twitch' => [
'type' => 'codename',
'icon' => 'icons/links/twitch',
'section' => '',
'tempcode' => '{NAME*}',
],
'im_whatsapp' => [
'type' => 'codename',
'icon' => 'icons/links/whatsapp',
'section' => 'contact',
'tempcode' => '{NAME*}',
],
'sn_sina_weibo' => [
'type' => 'codename',
'icon' => 'icons/links/sina_weibo',
'section' => '',
'tempcode' => '{NAME*}',
],
'im_wechat' => [
'type' => 'codename',
'icon' => 'icons/links/wechat',
'section' => 'contact',
'tempcode' => '{NAME*}',
],
'playstation_network' => [
'type' => 'codename',
'icon' => 'icons/links/playstation_network',
'section' => '',
'tempcode' => '{NAME*}',
],
'xbox_live' => [
'type' => 'codename',
'icon' => 'icons/links/xbox_live',
'section' => '',
'tempcode' => '{NAME*}',
],
'steam' => [
'type' => 'codename',
'icon' => 'icons/links/steam',
'section' => '',
'tempcode' => '{NAME*}: {RAW*}',
],
'sn_steemit' => [
'type' => 'codename',
'icon' => 'icons/links/steemit',
'section' => '',
'tempcode' => '{NAME*}',
],
'utopian' => [
'type' => 'codename',
'icon' => 'icons/links/utopian',
'section' => '',
'tempcode' => '{NAME*}',
],
'dtube' => [
'type' => 'codename',
'icon' => 'icons/links/dtube',
'section' => '',
'tempcode' => '{NAME*}',
],
'im_line' => [
'type' => 'codename',
'icon' => 'icons/links/line',
'section' => 'contact',
'tempcode' => '{NAME*}: {RAW*}',
],
'im_viber' => [
'type' => 'codename',
'icon' => 'icons/links/viber',
'section' => 'contact',
'tempcode' => '{NAME*}',
],
'sn_facebook' => [
'type' => 'url',
'icon' => 'icons/links/facebook',
'section' => '',
'tempcode' => '{NAME*}',
],
'amazon' => [
'type' => 'url',
'icon' => 'icons/links/amazon',
'section' => '',
'tempcode' => '{NAME*}',
],
'bandcamp' => [
'type' => 'url',
'icon' => 'icons/links/bandcamp',
'section' => '',
'tempcode' => '{NAME*}',
],
'dailymotion' => [
'type' => 'url',
'icon' => 'icons/links/dailymotion',
'section' => '',
'tempcode' => '{NAME*}',
],
'dropbox' => [
'type' => 'url',
'icon' => 'icons/links/dropbox',
'section' => '',
'tempcode' => '{NAME*}',
],
'flattr' => [
'type' => 'url',
'icon' => 'icons/links/flattr',
'section' => '',
'tempcode' => '{NAME*}',
],
'hacker_news' => [
'type' => 'url',
'icon' => 'icons/links/hacker_news',
'section' => '',
'tempcode' => '{NAME*}',
],
'sn_linkedin' => [
'type' => 'url',
'icon' => 'icons/links/linkedin',
'section' => '',
'tempcode' => '{NAME*}',
],
'patreon' => [
'type' => 'url',
'icon' => 'icons/links/patreon',
'section' => '',
'tempcode' => '{NAME*}',
],
'quora' => [
'type' => 'url',
'icon' => 'icons/links/quora',
'section' => '',
'tempcode' => '{NAME*}',
],
'sn_reddit' => [
'type' => 'url',
'icon' => 'icons/links/reddit',
'section' => '',
'tempcode' => '{NAME*}',
],
'slashdot' => [
'type' => 'url',
'icon' => 'icons/links/slashdot',
'section' => '',
'tempcode' => '{NAME*}',
],
'spotify' => [
'type' => 'url',
'icon' => 'icons/links/spotify',
'section' => '',
'tempcode' => '{NAME*}',
],
'stackexchange' => [
'type' => 'url',
'icon' => 'icons/links/stackexchange',
'section' => '',
'tempcode' => '{NAME*}',
],
'stack_overflow' => [
'type' => 'url',
'icon' => 'icons/links/stack_overflow',
'section' => '',
'tempcode' => '{NAME*}',
],
'vimeo' => [
'type' => 'url',
'icon' => 'icons/links/vimeo',
'section' => '',
'tempcode' => '{NAME*}',
],
'youtube' => [
'type' => 'url',
'icon' => 'icons/links/youtube',
'section' => '',
'tempcode' => '{NAME*}',
],
'sn_vkontakte' => [
'type' => 'url',
'icon' => 'icons/links/vkontakte',
'section' => '',
'tempcode' => '{NAME*}',
],
'sn_baidu_tieba' => [
'type' => 'url',
'icon' => 'icons/links/baidu_tieba',
'section' => '',
'tempcode' => '{NAME*}',
],
'sn_qzone' => [
'type' => 'url',
'icon' => 'icons/links/qzone',
'section' => '',
'tempcode' => '{NAME*}',
],
'bitcoin' => [
'type' => 'url',
'icon' => 'icons/links/bitcoin',
'section' => '',
'tempcode' => '{NAME*}',
],
'website' => [
'type' => 'url',
'icon' => 'icons/menu/home',
'section' => '',
'tempcode' => '{NAME*}',
],
'gender' => [
'type' => 'short_text',
'icon' => '',
'section' => '',
'tempcode' => '',
],
'location' => [
'type' => 'short_text',
'icon' => '',
'section' => '',
'tempcode' => '',
],
'occupation' => [
'type' => 'short_text',
'icon' => '',
'section' => '',
'tempcode' => '',
],
'paypal' => [
'type' => 'email',
'icon' => 'icons/links/paypal',
'section' => '',
'tempcode' => '{NAME*}',
],
'sn_mastodon' => [
'type' => 'email',
'icon' => 'icons/links/mastodon',
'section' => '',
'tempcode' => '{NAME*}',
],
'sn_diaspora' => [
'type' => 'email',
'icon' => 'icons/links/diaspora',
'section' => '',
'tempcode' => '{NAME*}',
],
'about' => [
'type' => 'long_trans',
'icon' => '',
'section' => '',
'tempcode' => '',
],
'staff_notes' => [
'type' => 'long_trans',
'icon' => '',
'section' => '',
'tempcode' => '',
],
'interests' => [
'type' => 'long_trans',
'icon' => '',
'section' => '',
'tempcode' => '',
],
];
}
/**
* Make a Custom Profile Field from one of the predefined templates (this is often used by importers).
* Also see the cpf_install source file.
*
* @param ID_TEXT $type The identifier of the predefined Custom Profile Field
* @return AUTO_LINK The ID of the new Custom Profile Field
*/
function cns_make_predefined_content_field(string $type) : int
{
require_lang('cns');
require_lang('cns_special_cpf');
require_lang('fields');
$details = cns_predefined_custom_field_details();
$_type = $details[$type]['type'];
$icon = $details[$type]['icon'];
$section = $details[$type]['section'];
$tempcode = $details[$type]['tempcode'];
$public_view = 1;
$owner_view = 1;
$owner_set = 1;
$required = 0;
$show_in_posts = 0;
$show_in_post_previews = 0;
$include_in_main_search = 0;
$allow_template_search = 0;
if ($type == 'staff_notes') {
$public_view = 0;
$owner_view = 0;
$owner_set = 0;
}
if ($type == 'interests' || $type == 'location') {
$show_in_posts = 1;
$show_in_post_previews = 1;
}
$title = do_lang('DEFAULT_CPF_' . $type . '_NAME');
$description = do_lang('DEFAULT_CPF_' . $type . '_DESCRIPTION');
return cns_make_custom_field($title, 0, $description, '', $public_view, $owner_view, $owner_set, 0, $_type, $required, $show_in_posts, $show_in_post_previews, null, '', 0, '', $include_in_main_search, $allow_template_search, $icon, $section, $tempcode, true);
}
/**
* 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(bool $leave_existing = false)
{
push_query_limiting(false);
$fields = $GLOBALS['FORUM_DB']->query_select('f_custom_fields', ['*'], [], 'ORDER BY cf_include_in_main_search DESC,cf_required+cf_show_on_join_form DESC,cf_public_view+cf_owner_set DESC,cf_order DESC');
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', ['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 ' . db_string_not_equal_to('Column_name', 'mf_member_id'));
foreach ($indexes as $index) {
$query = $GLOBALS['FORUM_DB']->driver->drop_index__sql($GLOBALS['FORUM_DB']->get_table_prefix() . 'f_member_custom_fields', $index['Key_name']);
if ($query !== null) {
$GLOBALS['FORUM_DB']->query($query);
}
}
}
}
// Rebuild indexes
require_code('cns_members_action');
foreach ($fields as $field) {
$id = $field['id'];
$type = $field['cf_type'];
list($_type) = get_cpf_storage_for($type);
$okay = build_cpf_indices($id, $field['cf_include_in_main_search'] == 1 || $field['cf_allow_template_search'] == 1, $type, $_type, true);
if (!$okay) { // Limit was hit
break;
}
}
pop_query_limiting();
}
/**
* Check if the date of birth field can be edited.
*
* @param ?MEMBER $member_id The member being edited (null: new member)
* @return boolean Whether the birthday can be edited after joining (if $member_id is null) or if it can be edited right now (if $member_id is not null)
*/
function cns_can_edit_birthday(?int $member_id) : bool
{
// Other members (e.g. staff) can edit the field depending on permissions / privileges
if (($member_id !== null) && (get_member() != $member_id)) {
return true;
}
// First, determine if the birthday is set now by looking at year
$birthday_set = false;
if ($member_id !== null) {
$_dob_year = $GLOBALS['CNS_DRIVER']->get_member_row_field($member_id, 'm_dob_year');
$birthday_set = ($_dob_year !== null);
}
// Parental controls
require_code('cns_parental_controls');
$pc = object_factory('Source_parental_controls', false, [false], true);
if ($pc->get_attribute('lock_dob') !== null) {
if ($member_id === null) {
return false;
}
return (!$birthday_set);
}
// Birthday points
if (addon_installed('points')) {
$_birthday_points = get_option('points_birthday', true);
$birthday_points = ($_birthday_points !== null) && ($_birthday_points != '') && (intval($_birthday_points) > 0);
if ($member_id !== null) {
$can_edit_birthday = ((!$birthday_points) || (!is_guest($member_id) && (!$birthday_set)));
} else {
$can_edit_birthday = (!$birthday_points);
}
if (!$can_edit_birthday) {
return false;
}
}
return true;
}
/**
* Check if the time zone field can be edited.
*
* @param ?MEMBER $member_id The member being edited (null: new member)
* @return boolean Whether the field can be edited after joining (if $member_id is null) or if it can be edited right now (if $member_id is not null)
*/
function cns_can_edit_timezone(?int $member_id) : bool
{
// Other members (e.g. staff) can edit the field depending on permissions / privileges
if (($member_id !== null) && (get_member() != $member_id)) {
return true;
}
// Parental controls
require_code('cns_parental_controls');
$pc = object_factory('Source_parental_controls', false, [false], true);
if ($pc->get_attribute('lock_timezone') !== null) {
return false;
}
return true;
}
/**
* Check if the region field can be edited.
*
* @param ?MEMBER $member_id The member being edited (null: new member)
* @return boolean Whether the field can be edited after joining (if $member_id is null) or if it can be edited right now (if $member_id is not null)
*/
function cns_can_edit_region(?int $member_id) : bool
{
// Other members (e.g. staff) can edit the field depending on permissions / privileges
if (($member_id !== null) && (get_member() != $member_id)) {
return true;
}
// Parental controls
require_code('cns_parental_controls');
$pc = object_factory('Source_parental_controls', false, [false], true);
if ($pc->get_attribute('lock_region') !== null) {
return false;
}
return true;
}