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