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; }