first();
}
public static function getXProfilePublicFields()
{
static $fields;
if ($fields) {
return $fields;
}
$fields = [
'user_id',
'display_name',
'username',
'avatar',
'status',
'total_points',
'is_verified',
'meta'
];
if (Utility::canViewUserProfile()) {
array_push($fields, 'created_at', 'short_description');
}
if (Utility::showLastActivity()) {
$fields[] = 'last_activity';
}
return apply_filters('fluent_community/xprofile_public_fields', $fields);
}
public static function getXprofileHiddenFields()
{
static $hiddenFields;
if ($hiddenFields) {
return $hiddenFields;
}
$hiddenFields = [
'id',
'created_at',
'updated_at'
];
if (Utility::showLastActivity()) {
$hiddenFields[] = 'last_activity';
}
return $hiddenFields;
}
public static function socialLinkProviders($enabledOnly = false)
{
$links = apply_filters('fluent_community/social_link_providers', [
'instagram' => [
'title' => __('Instagram', 'fluent-community'),
'icon_svg' => '',
'placeholder' => 'instagram @username',
'domain' => 'https://instagram.com/',
'enabled' => 'yes'
],
'twitter' => [
'title' => __('Twitter/X', 'fluent-community'),
'icon_svg' => '',
'placeholder' => 'twitter/X @username',
'domain' => 'https://x.com/',
'enabled' => 'yes'
],
'youtube' => [
'title' => __('YouTube', 'fluent-community'),
'icon_svg' => '',
'placeholder' => 'youtube @username',
'domain' => 'https://youtube.com/',
'enabled' => 'yes'
],
'linkedin' => [
'title' => __('LinkedIn', 'fluent-community'),
'icon_svg' => '',
'placeholder' => 'linkedin username',
'domain' => 'https://linkedin.com/in/',
'enabled' => 'yes'
],
'fb' => [
'title' => __('Facebook', 'fluent-community'),
'icon_svg' => '',
'placeholder' => 'fb_username',
'domain' => 'https://facebook.com/',
'enabled' => 'yes'
],
'blue_sky' => [
'title' => __('Bluesky', 'fluent-community'),
'icon_svg' => '',
'placeholder' => 'bluesky_username',
'domain' => 'https://bsky.app/profile/',
'enabled' => 'no'
],
'tiktok' => [
'title' => __('TikTok', 'fluent-community'),
'icon_svg' => '',
'placeholder' => '@tiktok_username',
'domain' => 'https://tiktok.com/',
'enabled' => 'no'
],
'pinterest' => [
'title' => __('Pinterest', 'fluent-community'),
'icon_svg' => '',
'placeholder' => 'pinterest_username',
'domain' => 'https://pinterest.com/',
'enabled' => 'no'
],
'telegram' => [
'title' => __('Telegram', 'fluent-community'),
'icon_svg' => '',
'placeholder' => 'telegram_username',
'domain' => 'https://telegram.me/',
'enabled' => 'no'
],
'snapchat' => [
'title' => __('Snapchat', 'fluent-community'),
'icon_svg' => '',
'placeholder' => 'snapchat_username',
'domain' => 'https://snapchat.com/add/',
'enabled' => 'no'
],
'reddit' => [
'title' => __('Reddit', 'fluent-community'),
'icon_svg' => '',
'placeholder' => 'reddit_username',
'domain' => 'https://www.reddit.com/user/',
'enabled' => 'no'
],
'twitch' => [
'title' => __('Twitch', 'fluent-community'),
'icon_svg' => '',
'placeholder' => 'twitch_username',
'domain' => 'https://www.twitch.tv/',
'enabled' => 'no'
],
'vk' => [
'title' => __('VK', 'fluent-community'),
'icon_svg' => '',
'placeholder' => __('vk username', 'fluent-community'),
'domain' => 'https://vk.com/',
'enabled' => 'yes'
],
'github' => [
'title' => __('Github', 'fluent-community'),
'icon_svg' => '',
'placeholder' => 'github username',
'domain' => 'https://github.com/',
'enabled' => 'yes'
],
'mastodon' => [
'title' => __('Mastodon', 'fluent-community'),
'icon_svg' => '',
'placeholder' => '@mastodon_username',
'domain' => 'https://mastodon.social/',
'enabled' => 'yes'
]
]);
$enabledKeys = self::getEnabledLinkProviderKeys();
foreach ($links as $key => $provider) {
if (in_array($key, $enabledKeys)) {
$links[$key]['enabled'] = 'yes';
} else {
$links[$key]['enabled'] = 'no';
}
if (!empty($provider['icon_svg'])) {
$links[$key]['icon_svg'] = CustomSanitizer::sanitizeSvg($provider['icon_svg']);
}
}
if ($enabledOnly) {
$links = array_filter($links, function ($link) {
return $link['enabled'] === 'yes';
});
}
return $links;
}
public static function getEnabledLinkProviderKeys()
{
$keys = Utility::getOption('enabled_profile_link_keys', NULL);
if ($keys === NULL) {
return ['instagram', 'twitter', 'youtube', 'linkedin', 'fb'];
}
return $keys;
}
public static function getReservedUserNames()
{
return apply_filters('fluent_community/reserved_usernames', [
'admin', 'administrator', 'me', 'moderator', 'mod', 'superuser', 'root', 'system', 'official', 'staff', 'support', 'helpdesk', 'user', 'guest', 'anonymous', 'everyone', 'anybody', 'someone', 'webmaster', 'postmaster', 'hostmaster', 'abuse', 'security', 'ssl', 'firewall', 'no-reply', 'noreply', 'mail', 'email', 'mailer', 'smtp', 'pop', 'imap', 'ftp', 'sftp', 'ssh', 'ceo', 'cfo', 'cto', 'founder', 'cofounder', 'owner', 'president', 'vicepresident', 'director', 'manager', 'supervisor', 'executive', 'info', 'contact', 'sales', 'marketing', 'support', 'billing', 'accounting', 'finance', 'hr', 'humanresources', 'legal', 'compliance', 'it', 'itsupport', 'customerservice', 'customersupport', 'dev', 'developer', 'api', 'sdk', 'app', 'bot', 'chatbot', 'sysadmin', 'devops', 'infosec', 'security', 'test', 'testing', 'beta', 'alpha', 'staging', 'production', 'development', 'home', 'about', 'contact', 'faq', 'help', 'news', 'blog', 'forum', 'community', 'events', 'calendar', 'shop', 'store', 'cart', 'checkout', 'social', 'follow', 'like', 'share', 'tweet', 'post', 'status', 'privacy', 'terms', 'copyright', 'trademark', 'legal', 'policy', 'all', 'none', 'null', 'undefined', 'true', 'false', 'default', 'example', 'sample', 'demo', 'temporary', 'delete', 'remove', 'profanity', 'explicit', 'offensive', 'yourappname', 'yourbrandname', 'yourdomain',
]);
}
public static function isUsernameAvailable($userName, $targetUserId = null)
{
$userName = strtolower($userName);
if (strlen($userName) < 3) {
return false;
}
$reservedUserNames = self::getReservedUserNames();
if (in_array($userName, $reservedUserNames)) {
return false;
}
$user = get_user_by('login', $userName);
if ($user) {
if ($targetUserId && $user->ID != $targetUserId) {
return false;
}
}
$xProfile = XProfile::where('username', $userName)
->when($targetUserId, function ($query) use ($targetUserId) {
return $query->where('user_id', '!=', $targetUserId);
})
->exists();
if ($xProfile) {
return false;
}
return true;
}
public static function generateUserName($user, $useUserName = false)
{
if (!$user instanceof \WP_User) {
$user = get_user_by('ID', $user);
}
return self::createUserNameFromStrings($user->user_login, array_filter([
$user->user_nicename,
$user->first_name,
$user->last_name,
$user->display_name
]), $user->ID);
}
public static function createUserNameFromStrings($maybeEmail, $fallbacks = [], $userId = null)
{
$emailParts = explode('@', $maybeEmail);
$userName = $emailParts[0];
$userName = CustomSanitizer::sanitizeUserName($userName);
if (self::isUsernameAvailable($userName, $userId)) {
return $userName;
}
foreach ($fallbacks as $fallback) {
// only take alphanumeric characters and _ -
$fallback = preg_replace('/[^a-zA-Z0-9_-]/', '', $fallback);
$userName = CustomSanitizer::sanitizeUserName($fallback);
if (self::isUsernameAvailable($userName, $userId)) {
return $userName;
}
}
$userName = strtolower($emailParts[0]);
$finalUserName = $userName;
// loop until we find a unique username
$counter = 2;
while (!self::isUsernameAvailable($userName, $userId)) {
$userName = $finalUserName . $counter;
$counter++;
if ($counter % 100 === 0) {
$finalUserName = $finalUserName . $userId . '-';
}
}
return $userName;
}
public static function createWpUser(array $userData)
{
$email = sanitize_email((string) ($userData['email'] ?? ''));
$fullName = trim((string) ($userData['full_name'] ?? ''));
$password = trim((string) ($userData['password'] ?? ''));
$username = sanitize_user((string) ($userData['username'] ?? ''), true);
if (!is_email($email)) {
return new \WP_Error('invalid_email', __('Invalid email address', 'fluent-community'));
}
if (!$password) {
$password = wp_generate_password(12);
}
$nameParts = explode(' ', $fullName);
$firstName = array_shift($nameParts);
$lastName = implode(' ', $nameParts);
if ($username && self::isUsernameAvailable($username)) {
$userLogin = $username;
} else {
$userLogin = self::createUserNameFromStrings($email, array_filter([$firstName, $lastName]));
}
$role = apply_filters('fluent_community/created_user_role', 'subscriber', $userData);
return wp_insert_user([
'role' => $role,
'user_email' => $email,
'user_login' => $userLogin,
'user_pass' => $password,
'first_name' => sanitize_text_field($firstName),
'last_name' => sanitize_text_field($lastName),
]);
}
public static function getUserAuthHash($userId = null)
{
if (!$userId) {
return '';
}
if (Utility::getPrivacySetting('email_auto_login') === 'no') {
return '';
}
static $cached = [];
if (isset($cached[$userId])) {
return $cached[$userId];
}
$exist = Meta::byType('user')
->byMetaKey('auth_hash')
->byObjectId($userId)
->first();
$validTil = strtotime('+1 day');
if ($exist) {
$hashes = (array)$exist->value;
$validHashes = array_filter($hashes, function ($hash) use ($validTil) {
return $hash['valid_til'] > $validTil;
});
if ($validHashes) {
$lastHash = end($validHashes);
return $lastHash['hash'] . '__' . $exist->id;
}
$newHash = bin2hex(random_bytes(32));
$hashes[] = [
'hash' => $newHash,
'valid_til' => strtotime('+2 days')
];
// remove expired hashes
$hashes = array_filter($hashes, function ($hash) use ($validTil) {
return $hash['valid_til'] > time();
});
$exist->value = array_values($hashes);
$exist->save();
$cached[$userId] = $newHash . '__' . $exist->id;
return $cached[$userId];
}
$newHash = bin2hex(random_bytes(32));
$meta = Meta::create([
'object_type' => 'user',
'object_id' => $userId,
'meta_key' => 'auth_hash', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
'value' => [
[
'hash' => $newHash,
'valid_til' => strtotime('+2 days')
]
]
]);
$cached[$userId] = $newHash . '__' . $meta->id;
return $cached[$userId];
}
public static function signUserUrlWithAuthHash($url, $userId = null)
{
$hash = self::getUserAuthHash($userId);
if (!$hash) {
return $url;
}
return add_query_arg([
'fcom_action' => 'signed_url',
'fcom_url_hash' => $hash
], $url);
}
public static function getSignedNotificationPrefUrl($userId)
{
$notificationPref = Helper::baseUrl('fcom_route?route=user_notification_settings&auth=yes');
return self::signUserUrlWithAuthHash($notificationPref, $userId);
}
/*
* Get WP User by URL Hash
* @param string $hash
* @return \WP_User|null
*/
public static function getUserByUrlHash($hash)
{
$hashParts = explode('__', $hash);
if (count($hashParts) !== 2) {
return null;
}
$metaId = $hashParts[1];
$meta = Meta::where('object_type', 'user')
->where('meta_key', 'auth_hash')
->where('id', $metaId)
->first();
if (!$meta) {
return null;
}
$hashes = (array)$meta->value;
$hash = $hashParts[0];
$validHash = array_filter($hashes, function ($hashData) use ($hash) {
if (!is_array($hashData) || empty($hashData['hash']) || empty($hashData['valid_til'])) {
return false;
}
if ((int)$hashData['valid_til'] <= time()) {
return false;
}
return hash_equals((string)$hashData['hash'], (string)$hash);
});
if (!$validHash) {
return null;
}
// Consume the hash (single-use) to prevent replay attacks
$remainingHashes = array_filter($hashes, function ($hashData) use ($hash) {
return !is_array($hashData) || empty($hashData['hash']) || !hash_equals((string)$hashData['hash'], (string)$hash);
});
$meta->value = array_values($remainingHashes);
if (!$meta->save()) {
return null;
}
return get_user_by('ID', $meta->object_id);
}
public static function canViewUserSpaces($targetUserId, $currentUser = null)
{
$status = Utility::getPrivacySetting('user_space_visibility', 'everybody');
if ($status == 'everybody') {
return true;
}
if ($status == 'logged_in') {
return !!$currentUser;
}
if (is_numeric($currentUser)) {
$currentUser = User::find($currentUser);
}
if (!$currentUser) {
return false;
}
if ($targetUserId == $currentUser->ID) {
return true;
}
return Helper::isModerator($currentUser);
}
/**
* Sends a confirmation request email when a change of user email address is attempted.
*
* @param \WP_User $current_user The current user object.
* @param $new_email string The new email address.
* @return \WP_Error|boolean $error WP_Error object.
*/
public static function sendConfirmationOnProfileEmailChange(\WP_User $current_user, $newEmail)
{
if ($current_user->user_email == $newEmail) {
return false;
}
if (!is_email($newEmail)) {
return new \WP_Error('user_email', __('Error: The email address is not correct.', 'fluent-community'));
}
if (email_exists($newEmail)) {
delete_user_meta($current_user->ID, '_new_email');
return new \WP_Error('user_email', __('Error: The email address is already used.', 'fluent-community'));
}
$hash = md5($newEmail . time() . wp_rand());
$new_user_email = array(
'hash' => $hash,
'newemail' => $newEmail
);
update_user_meta($current_user->ID, '_new_email', $new_user_email);
$sitename = wp_specialchars_decode(get_option('blogname'), ENT_QUOTES);
/* translators: Do not translate USERNAME, ADMIN_URL, EMAIL, SITENAME, SITEURL: those are placeholders. */
$email_text = __(
'Howdy ###USERNAME###,
You recently requested to have the email address on your account changed.
If this is correct, please click on the following link to change it:
###ADMIN_URL###
You can safely ignore and delete this email if you do not want to
take this action.
This email has been sent to ###EMAIL###
Regards,
All at ###SITENAME###
###SITEURL###', 'fluent-community'
);
$content = apply_filters('new_user_email_content', $email_text, $new_user_email); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
$content = str_replace('###USERNAME###', $current_user->user_login, $content);
$content = str_replace('###ADMIN_URL###', esc_url(self_admin_url('profile.php?newuseremail=' . $hash)), $content);
$content = str_replace('###EMAIL###', $newEmail, $content);
$content = str_replace('###SITENAME###', $sitename, $content);
$content = str_replace('###SITEURL###', home_url(), $content);
/* translators: New email address notification email subject. %s: Site title. */
return wp_mail($newEmail, sprintf(__('[%s] Email Change Request', 'fluent-community'), $sitename), $content);
}
}