beginTransaction(); try { $result = $callback(); $db->commit(); return $result; } catch (\Exception $e) { $db->rollBack(); throw $e; } } /** * Check if POST content length exceeds PHP limits * * @return array|false Error array if limit exceeded, false otherwise */ public static function checkUploadSizeError() { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- Server variable check $contentLength = isset($_SERVER['CONTENT_LENGTH']) ? (int) $_SERVER['CONTENT_LENGTH'] : 0; $postMaxSize = wp_convert_hr_to_bytes(ini_get('post_max_size')); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- read-only existence check on superglobals, no state mutation if ($contentLength > 0 && ($contentLength > $postMaxSize || (empty($_FILES) && empty($_POST)))) { return [ 'message' => sprintf( /* translators: %s: max size */ __('Upload failed: File exceeds server limit (%s). Please upload a smaller file.', 'fluent-community'), size_format($postMaxSize) ) ]; } return false; } /** * Get the portal slug. * * @return string The portal slug. */ /** * Get the portal slug. * * @return string The portal slug. */ public static function getPortalSlug($forRoute = false) { $settings = get_option('fluent_community_settings', []); if (isset($settings['slug'])) { $slug = $settings['slug']; } else { $slug = 'portal'; } if (defined('FLUENT_COMMUNITY_PORTAL_SLUG')) { $slug = \FLUENT_COMMUNITY_PORTAL_SLUG; } $slug = apply_filters('fluent_community/portal_slug', $slug); if (!$forRoute) { return $slug; } $siteUrl = get_home_url(); $poralUrl = self::baseUrl('/'); $urlPath = wp_parse_url($siteUrl, PHP_URL_PATH); if ($urlPath) { // get the url without path $siteUrl = str_replace($urlPath, '', $siteUrl); } $slug = str_replace($siteUrl, '', $poralUrl); // remove the first and last slashes return trim($slug, '/'); } /** * Get the portal route type. * * @return string The portal route type. */ public static function getPortalRouteType() { return apply_filters('fluent_community/portal_route_type', 'WebHistory'); } /** * Check if the portal is headless. * * @return bool True if headless, false otherwise. */ public static function isHeadless() { return apply_filters('fluent_community/portal_page_headless', false); } /** * Check if the portal has a color scheme. * * @return bool True if has color scheme, false otherwise. */ public static function hasColorScheme() { $status = Utility::isCustomizationEnabled('dark_mode'); return apply_filters('fluent_community/has_color_scheme', $status); } /** * Admin default theme mode: 'light', 'dark', or 'system'. */ public static function getDefaultThemeMode() { $settings = Utility::getCustomizationSettings(); $mode = isset($settings['default_theme_mode']) ? $settings['default_theme_mode'] : 'light'; if (!in_array($mode, ['light', 'dark', 'system'], true)) { $mode = 'light'; } return apply_filters('fluent_community/default_theme_mode', $mode); } /** * Pre-paint script that sets the theme before first render (no flash). * Precedence mirrors runtime: host-theme cookie → user pick → admin default. * Not persisted. Gate on hasColorScheme(). */ public static function renderColorSchemePrePaintScript() { $defaultMode = self::getDefaultThemeMode(); $portalVars = apply_filters('fluent_community/general_portal_vars', ['color_switch_cookie_name' => '']); $cookieName = isset($portalVars['color_switch_cookie_name']) ? $portalVars['color_switch_cookie_name'] : ''; ?> getPermissions(), 'community_admin'); } public static function isModerator($user = null) { if (!$user) { $user = self::getCurrentUser(); } return $user && $user->hasCommunityModeratorAccess(); } /** * Get the URL for an asset file. * * @param string $file The file name. * @return string The full URL to the asset. */ public static function assetUrl($file = '') { return FLUENT_COMMUNITY_PLUGIN_URL . 'assets/' . $file; } /** * Get the base URL for the portal. * * @param string $path The path to append to the base URL. * @return string The full base URL. */ public static function baseUrl($path = '') { $baseUrl = apply_filters('fluent_community/base_url', home_url(self::getPortalSlug())); $baseUrl = rtrim($baseUrl, '/'); if (self::getPortalRouteType() != 'hash') { return $baseUrl . '/' . ltrim($path, '/'); } if (!$path) { return $baseUrl . '/'; } return $baseUrl . '/#/' . ltrim($path, '/'); } public static function getAuthUrl() { $settings = self::generalSettings(); return Arr::get($settings, 'cutsom_auth_url', ''); } /** * Get the space IDs for a user. * * @param int|null $userId The user ID. If null, uses the current user. * @return array An array of space IDs. */ public static function getUserSpaceIds($userId = null) { if (!$userId) { $userId = get_current_user_id(); } return SpaceUserPivot::where('user_id', $userId) ->where('status', 'active') ->pluck('space_id') ->toArray(); } public static function getUserSpaces($userId = null) { if (!$userId) { $userId = get_current_user_id(); } return Space::whereHas('members', function ($query) use ($userId) { $query->where('user_id', $userId); })->get(); } /** * Check if a user is in a specific space. * * @param int $userId The user ID. * @param int $spaceId The space ID. * @return bool True if the user is in the space, false otherwise. */ public static function isUserInSpace($userId, $spaceId) { if (!$userId || !$spaceId) { return false; } return SpaceUserPivot::where('user_id', $userId) ->where('space_id', $spaceId) ->where('status', 'active') ->exists(); } /** * Generate HTML attributes from an array. * * @param array $atts An array of attribute key-value pairs. * @return string The generated HTML attributes string. */ public static function attrs($atts = []) { $text = ''; foreach ($atts as $key => $value) { $text .= "$key=\"$value\" "; } return $text; } /** * Get media from a URL. * * @param string|array $url The URL or an array containing URL information. * @return Media|null The Media object if found, null otherwise. */ public static function getMediaFromUrl($url) { if (is_array($url) && isset($url['provider'])) { $provider = Arr::get($url, 'provider'); if ($provider == 'giphy') { return null; } $url = Arr::get($url, 'url'); } if (!$url) { return null; } $parsedUrl = wp_parse_url($url, PHP_URL_QUERY); if (!$parsedUrl) { return null; } // Parse the query string to get the media_key value parse_str($parsedUrl, $queryParams); $key = Arr::get($queryParams, 'media_key'); if (!$key) { return null; } return Media::where('media_key', $key)->first(); } public static function removeMediaByUrl($url = '', $subObjectId = null) { if (!$url || !$subObjectId) { return; } do_action('fluent_community/remove_medias_by_url', [$url], [ 'sub_object_id' => $subObjectId, ]); } /** * Get media items from multiple URLs. * * @param array $urls An array of URLs. * @return array An array of Media objects. */ public static function getMediaItemsFromUrl($urls) { $mediaItems = []; foreach ($urls as $url) { $media = self::getMediaFromUrl($url); if ($media) { $mediaItems[] = $media; } } return $mediaItems; } /** * Get general settings for the community. * * @param bool $cached Whether to use cached settings. * @return array The general settings. */ public static function generalSettings($cached = true) { static $settings = null; if ($cached && $settings) { return $settings; } $settings = get_option('fluent_community_settings', []); $defaults = [ 'site_title' => get_bloginfo('name'), 'slug' => 'portal', 'logo' => '', 'white_logo' => '', 'logo_permalink_type' => 'default', 'logo_permalink' => '', 'featured_image' => '', 'access' => [ 'acess_level' => 'public', // logged_in, public, role_based 'access_roles' => [] ], 'auth_form_type' => 'default', 'explicit_registration' => 'no', 'disable_global_posts' => 'yes', 'auth_content' => 'Please login first to access this page', 'auth_redirect' => '', 'restricted_role_content' => 'Sorry, you cannot access this page. Only authorized users can access this page.', 'auth_url' => '', 'cutsom_auth_url' => self::baseUrl('?fcom_action=auth'), 'use_custom_signup_page' => 'no', 'custom_signup_url' => '' ]; $settings = wp_parse_args($settings, $defaults); if ($settings['auth_form_type'] != 'custom' || empty($settings['auth_form_type'])) { $settings['cutsom_auth_url'] = self::baseUrl('?fcom_action=auth'); } if (defined('FLUENT_COMMUNITY_PORTAL_SLUG')) { $settings['slug'] = \FLUENT_COMMUNITY_PORTAL_SLUG; $settings['is_slug_defined'] = true; } else { unset($settings['is_slug_defined']); } return $settings; } public static function hasGlobalPost() { $settings = self::generalSettings(); $status = Arr::get($settings, 'disable_global_posts', '') != 'yes'; return apply_filters('fluent_community/has_global_post', $status); } /** * Check if a user can access the portal. * * @param int|null $userId The user ID. If null, uses the current user. * @return bool True if the user can access the portal, false otherwise. */ public static function canAccessPortal($userId = null, $requireActiveProfile = true) { $settings = self::generalSettings(); $accessLevel = Arr::get($settings, 'access.acess_level'); if ($accessLevel == 'public') { return apply_filters('fluent_community/can_access_portal', true); } if (!$userId) { $userId = get_current_user_id(); } if (!$userId) { return apply_filters('fluent_community/can_access_portal', false); } if ($accessLevel == 'logged_in') { return apply_filters('fluent_community/can_access_portal', true); } if (user_can($userId, 'edit_pages')) { return apply_filters('fluent_community/can_access_portal', true); } $roles = Arr::get($settings, 'access.access_roles', []); $user = get_user_by('ID', $userId); if (!$user) { return apply_filters('fluent_community/can_access_portal', false); } $result = !!array_intersect(array_values($user->roles), $roles); if (!$result) { return apply_filters('fluent_community/can_access_portal', false); } if (!$requireActiveProfile) { return apply_filters('fluent_community/can_access_portal', true); } $xProfile = Helper::getCurrentProfile(); $result = $xProfile && $xProfile->status == 'active'; return apply_filters('fluent_community/can_access_portal', $result); } /** * Get the portal route paths. * * @return array An array of portal route paths. */ public static function portalRoutePaths() { return apply_filters('fluent_community/app_route_paths', [ 'portal_home', 'members', 'bookmarks', 'chat', 'dashboard', 'leaderboards', 'notifications', 'space', 'discover', 'courses', 'u', 'post', 'admin', 'course', 'site-maps' ]); } /** * Get the current user's profile. * * @param bool $cached Whether to use cached profile. * @return XProfile|null The user's profile or null if not found. */ public static function getCurrentProfile($cached = true) { static $profile; if ($profile && $cached) { return $profile; } $userId = get_current_user_id(); if (!$userId) { $profile = null; return $profile; } $profile = XProfile::where('user_id', $userId)->first(); return $profile; } /** * Get the current user Model. * * @param bool $cached Whether to use cached user. * @return User|false The User model or false if not found. */ public static function getCurrentUser($cached = true) { $userId = get_current_user_id(); if (!$userId) { return false; } static $users = []; if ($cached && isset($users[$userId])) { return $users[$userId]; } return $users[$userId] = User::find($userId); } /** * Get the route paths for the community. * * @return array An array of route paths. */ private static function getRoutePaths() { return [ 'dashboard' => '/dashboard', 'all_feeds' => '/', 'single_feed' => '/post/:feed_slug', 'space_feeds' => '/space/:space/home', 'space_feed' => '/space/:space/post/:feed_slug', 'space_members' => '/space/:space/members', 'spaces' => '/discover/spaces', 'settings' => '/admin/settings', 'admin_moderators' => '/admin/settings/moderators', 'all_members' => '/members', 'user_profile' => '/u/:username/', 'user_communities' => '/u/:username/spaces', 'update_profile' => '/u/:username/update', 'discussions' => '/discussions', 'create_topic' => '/discussions/create-topic', 'topic' => '/discussions/topic/:slug', 'notifications' => '/notifications', 'bookmarks' => '/bookmarks', 'courses' => '/courses', 'view_course' => '/courses/view/:course_id/lessons', 'view_lesson' => '/courses/view/:course_id/lessons/:lesson_slug/view', 'manage_courses' => '/admin/manage-courses', 'edit_lessons' => '/admin/manage-courses/edit/:course_id/lessons', 'course_students' => '/admin/manage-courses/edit/:course_id/students', 'course_overview' => '/admin/manage-courses/edit/:course_id/overview', 'manage_leaderboard' => '/admin/manage-leaderboard', ]; } /** * Get the URL for a JavaScript route. * * @param array $route The route information. * @return string The URL for the route. */ public static function getUrlByJsRoute($route = []) { $routePaths = self::getRoutePaths(); $routeName = Arr::get($route, 'name', ''); if (!$routeName || !isset($routePaths[$routeName])) { return self::baseUrl(); } $path = $routePaths[$routeName]; $params = (array)Arr::get($route, 'params', []); if (!$params) { return self::baseUrl($path); } $replaces = []; foreach ($params as $paramKey => $paramValue) { $replaces[':' . $paramKey] = $paramValue; } $path = str_replace(array_keys($replaces), array_values($replaces), $path); return self::baseUrl($path); } /** * Get the route name from a request path. * * @param string $path The request path. * @return string|false The route name or false if not found. */ public static function getRouteNameByRequestPath($path) { $path = '//' . $path; if (strpos($path, '/u/')) { return 'user_profile'; } if (strpos($path, '/post/')) { return 'feed_view'; } if (strpos($path, '/lessons/')) { return 'lesson_view'; } if (strpos($path, '/course/')) { return 'course_view'; } if (strpos($path, '/space/') && !strpos($path, '/discover/spaces')) { return 'community_view'; } if (strpos($path, '/admin')) { return 'admin'; } return false; } /** * Get a human-readable excerpt from content. * * @param string $content The content to extract from. * @param int $length The maximum length of the excerpt. * @return string The human-readable excerpt. */ public static function getHumanExcerpt($content, $length = 100) { if ($content) { $patterns = [ '/^#{1,6}\s+/m' => '', // Bold and Italic: remove '*' and '_' symbols '/(\*\*|__)(.*?)\1/' => '$2', '/(\*|_)(.*?)\1/' => '$2', // Code blocks: remove triple backticks '/^```\s*\w*\s*\n([\s\S]*?)\n```\s*$/m' => '$1', // Inline code: remove single backticks '/`([^`]+)`/' => '$1', // Blockquotes: remove '>' symbol '/^\s*>\s?/m' => '', // Horizontal rules: replace with empty line '/^\s*([-*_])\1{2,}\s*$/m' => "\n", // Links: keep only the link text '/\[([^\]]+)\]\([^\)]+\)/' => '$1', // Images: keep only the alt text '/!\[([^\]]+)\]\([^\)]+\)/' => '$1', // Strikethrough: remove '~~' symbols '/~~(.*?)~~/' => '$1', // Task lists: remove checkbox syntax '/^\s*[-*+]\s+\[[ xX]\]\s+/m' => '', ]; $content = preg_replace(array_keys($patterns), array_values($patterns), $content); // remove all tags $content = wp_strip_all_tags($content); // remove new lines and tabs $content = str_replace(["\r", "\n", "\t"], ' ', $content); // remove multiple spaces $content = preg_replace('/\s+/', ' ', $content); // trim $content = trim($content); } if (!$content) { return ''; } if (mb_strlen($content) <= $length) { return $content; } // return the first $length chars of the content with ... at the end return mb_substr($content, 0, $length) . '...'; } /** * Check if the portal is publicly accessible. * * @return bool True if publicly accessible, false otherwise. */ public static function isPublicAccessible() { $settings = self::generalSettings(); return Arr::get($settings, 'access.acess_level') == 'public'; } /** * Get media by provider. * * @param array $images The array of images. * @param string $provider The provider to filter by. * @return array The filtered array of images. */ public static function getMediaByProvider($images, $provider = 'uploader') { if (is_array($images)) { return array_filter($images, function ($image) use ($provider) { if (is_array($image)) { if (isset($image['provider'])) { return Arr::get($image, 'provider') == $provider; } return $provider === 'uploader'; // for existing images when no provider was set. } }); } return []; } /** * Get the community menu groups. * * @param User|null $user The user to get menu groups for. * @return array The community menu groups. */ public static function getCommunityMenuGroups($user = null, $view = true) { if (!$user) { $user = self::getCurrentUser(); } $communityGroups = self::getAllCommunityGroups($user); if ($communityGroups->isEmpty()) { return []; } $userSpaceIds = $user ? self::getUserSpaceIds($user->ID) : []; $isComModerator = $user && $user->hasCommunityModeratorAccess(); $isCourseCreator = $user && $user->hasCourseCreatorAccess(); $isSpaceModerator = $user && $user->isSpaceModerator(); $formattedGroups = []; foreach ($communityGroups as $communityGroup) { $validSpaces = []; $spaces = $communityGroup->spaces; $isShowAll = Arr::get($communityGroup->settings, 'always_show_spaces') === 'yes'; if (!$isShowAll && !$isSpaceModerator) { $spaceIds = $spaces->pluck('id')->toArray(); $isNotMemberOfAnySpace = empty(array_intersect($spaceIds, $userSpaceIds)); if ($isNotMemberOfAnySpace) { continue; } } if ($user) { BaseSpace::preloadMemberships($spaces, $user->ID); } foreach ($spaces as $space) { $validSpace = $view ? self::transformSpaceToLink($space, $user) : $space; if (!$validSpace) { continue; } if ($user && $space->isContentSpace()) { $validSpace['unread_badge'] = self::getUnreadFeedsCounts($space->id); } if ($isComModerator && $space->type != 'course') { $validSpaces[] = $validSpace; continue; } if ($isCourseCreator && $space->type == 'course') { $validSpaces[] = $validSpace; continue; } if ($space->privacy == 'public') { $validSpaces[] = $validSpace; continue; } $hasMembership = $user && $space->getMembership($user->ID); if ($space->privacy == 'private') { if (!$user || !$hasMembership) { $validSpace['show_lock'] = true; } } if ($space->privacy == 'secret') { if (!$user || !$hasMembership) { continue; } } $validSpaces[] = $validSpace; } if (!$validSpaces && !$isSpaceModerator) { continue; } $formattedGroups[] = [ 'id' => $communityGroup->id, 'title' => $communityGroup->title, 'slug' => $communityGroup->slug, 'logo' => $communityGroup->logo, 'children' => $validSpaces ]; } return apply_filters('fluent_community/menu_groups_for_user', $formattedGroups, $user); } public static function getUnreadFeedsCounts($spaceId, $force = false) { static $coutsCache = null; if ($coutsCache !== null && !$force) { return Arr::get((array)$coutsCache, $spaceId); } $xprofile = self::getCurrentProfile(); if (!$xprofile || !$xprofile->last_activity) { return 0; } $lastActivityDate = gmdate('Y-m-d H:i:s', strtotime($xprofile->last_activity) - 300); $lastActivityDate = apply_filters('fluent_community/last_activity_date_for_unread_feeds', $lastActivityDate, $xprofile); $unreadCounts = Feed::query() ->select('space_id', Utility::getApp('db')->raw('COUNT(*) as feed_count')) ->where('status', 'published') ->where('created_at', '>', $lastActivityDate) ->groupBy('space_id') ->get(); $coutsCache = []; foreach ($unreadCounts as $unreadCount) { $coutsCache[$unreadCount->space_id] = $unreadCount->feed_count > 10 ? '10+' : $unreadCount->feed_count; } return Arr::get($coutsCache, $spaceId); } /** * Transform a space to a link array. * * @param Space $space The space to transform. * @param User|null $user The user to check permissions for. * @return array|null The transformed space link array. */ private static function transformSpaceToLink($space, $user = null) { $isCustomLink = $space->type == 'sidebar_link'; if ($isCustomLink && !self::canViewSideLinkLink($space, $user)) { return null; } $logo = $space->logo; $title = $space->title; if ($space->status == 'draft') { $title = $title . ' ' . __('(Draft)', 'fluent-community'); } return [ 'title' => $title, 'icon_image' => $logo, 'shape_svg' => !$logo ? Arr::get($space->settings, 'shape_svg', '') : '', 'emoji' => !$logo ? Arr::get($space->settings, 'emoji', '') : '', 'permalink' => $space->getPermalink(), 'is_custom' => $isCustomLink ? 'yes' : 'no', 'new_tab' => ($isCustomLink && Arr::get($space, 'settings.new_tab', 'no') === 'yes') ? 'yes' : 'no', 'link_classes' => 'space_menu_item route_url fcom_space_id_' . $space->id . ' fcom_space_' . $space->slug ]; } public static function canViewSideLinkLink($space, $user = null) { $privacy = $space->privacy; if ($privacy == 'public') { return true; } if ($privacy == 'logged_in') { return !!$user; } if ($privacy == 'logged_out_only') { return !$user; } if (!$user) { return false; } $accessIds = Arr::get($space->settings, 'membership_ids', []); if (!$accessIds) { return true; } $userSpaces = $user->getSpaceIds(); return !!array_intersect($userSpaces, $accessIds); } public static function isAlreadyOnboarded() { $communitySettings = get_option('fluent_community_settings', []); return !empty($communitySettings); } /** * Get all community groups. * * @param User $user The user to get groups for. * @param bool $willCreate Whether to create a default group if none exist. * @return \FluentCommunity\Framework\Support\Collection Collection of Groups */ public static function getAllCommunityGroups($user, $willCreate = true) { $isModerator = $user && $user->isCommunityModerator(); $communityGroups = SpaceGroup::query()->orderBy('serial', 'ASC') ->with([ 'spaces' => function ($query) use ($isModerator) { if ($isModerator) { $query->orderBy('serial', 'ASC'); } else { $query->where('status', 'published') ->orderBy('serial', 'ASC'); } } ]) ->get(); if ($communityGroups->isEmpty() && $willCreate) { $createdSpace = (new ActivationHandler(App::make()))->maybeCreateDefaultSpaceGroup(); if ($createdSpace) { Space::where('type', 'community')->update([ 'parent_id' => $createdSpace->id ]); } return self::getAllCommunityGroups($user, false); } return $communityGroups; } /** * Check if a feature is enabled. * * @param string $feature The feature to check. * @return bool True if the feature is enabled, false otherwise. */ public static function isFeatureEnabled($feature) { $features = Utility::getFeaturesConfig(); return isset($features[$feature]) && $features[$feature] === 'yes'; } /** * Get the menu items group. * * @param string $context The context for getting menu items. * @return array The menu items group. */ public static function getMenuItemsGroup($context = 'view') { static $menuGroups; if ($menuGroups && $context === 'view') { return $menuGroups; } $menuGroups = (array) Utility::getOption('fluent_community_menu_groups', []); $membersPageStatus = Utility::canViewMembersPage() ? 'yes' : 'no'; $leaderboardPageVisibility = (Utility::canViewLeaderboardMembers() || is_user_logged_in()) ? 'yes' : 'no'; $defaultMainMenuItems = [ 'all_feeds' => [ 'slug' => 'all_feeds', 'title' => __('Feed', 'fluent-community'), 'is_system' => 'yes', 'is_locked' => 'yes', 'enabled' => 'yes', 'permalink' => self::baseUrl('/'), 'link_classes' => 'fcom_dashboard route_url', 'shape_svg' => '' ], 'spaces' => [ 'slug' => 'spaces', 'title' => __('Spaces', 'fluent-community'), 'is_system' => 'yes', 'is_locked' => 'yes', 'enabled' => 'yes', 'permalink' => self::baseUrl('discover/spaces'), 'link_classes' => 'fcom_spaces route_url', 'shape_svg' => '' ], 'all_courses' => [ 'slug' => 'all_courses', 'title' => __('Courses', 'fluent-community'), 'link_classes' => 'fcom_courses route_url', 'is_system' => 'yes', 'is_locked' => 'yes', 'enabled' => 'yes', 'is_unavailable' => self::isFeatureEnabled('course_module') ? 'no' : 'yes', 'permalink' => self::baseUrl('courses'), 'shape_svg' => '' ], 'all_members' => [ 'slug' => 'all_members', 'title' => __('Members', 'fluent-community'), 'is_system' => 'yes', 'is_locked' => 'yes', 'is_unavailable' => $membersPageStatus == 'yes' ? 'no' : 'yes', 'enabled' => $membersPageStatus, 'permalink' => self::baseUrl('members'), 'link_classes' => 'fcom_all_members route_url', 'shape_svg' => '', ], 'leaderboard' => [ 'slug' => 'leaderboard', 'is_system' => 'yes', 'is_locked' => 'yes', 'enabled' => $leaderboardPageVisibility, 'is_unavailable' => self::isFeatureEnabled('leader_board_module') && $leaderboardPageVisibility == 'yes' ? 'no' : 'yes', 'title' => __('Leaderboard', 'fluent-community'), 'link_classes' => 'fcom_leaderboards route_url', 'permalink' => self::baseUrl('leaderboards'), 'shape_svg' => '' ] ]; $mainItems = Arr::get($menuGroups, 'mainMenuItems', []); if ($mainItems && is_array($mainItems)) { if (isset($mainItems['all_communities'])) { $mainItems['spaces'] = $defaultMainMenuItems['spaces']; unset($mainItems['all_communities']); } foreach ($mainItems as $index => &$item) { if (empty($item['slug'])) { unset($mainItems[$index]); continue; } $defaultItem = Arr::get($defaultMainMenuItems, $item['slug'], []); if ($defaultItem) { $preservedKeys = ['is_system', 'is_locked', 'is_unavailable', 'slug']; foreach ($preservedKeys as $key) { if (isset($defaultItem[$key])) { $item[$key] = Arr::get($defaultItem, $key); } } if (Arr::get($defaultItem, 'is_system') === 'yes') { $item['permalink'] = $defaultItem['permalink']; $item['link_classes'] = $defaultItem['link_classes']; if (empty($item['shape_svg'])) { $item['shape_svg'] = $defaultItem['shape_svg']; } } } } } else { $mainItems = $defaultMainMenuItems; } $defaultProfileDropDownItems = [ 'my_spaces' => [ 'slug' => 'my_spaces', 'title' => __('My Spaces', 'fluent-community'), 'is_system' => 'yes', 'is_locked' => 'yes', 'enabled' => 'yes', 'permalink' => '#{{user_url}}/spaces', 'shape_svg' => '' ], 'bookmarks' => [ 'slug' => 'bookmarks', 'title' => __('Bookmarks', 'fluent-community'), 'is_system' => 'yes', 'is_locked' => 'yes', 'enabled' => 'yes', 'permalink' => self::baseUrl('bookmarks'), 'shape_svg' => '' ], 'logout' => [ 'slug' => 'logout', 'title' => __('Logout', 'fluent-community'), 'is_system' => 'yes', 'is_locked' => 'yes', 'enabled' => 'yes', 'permalink' => '#{{logout_url}}', 'shape_svg' => '' ] ]; $profileDropDownItems = Arr::get($menuGroups, 'profileDropdownItems', []); if ($profileDropDownItems && is_array($profileDropDownItems)) { unset($profileDropDownItems['profile']); foreach ($profileDropDownItems as $index => &$item) { if (empty($item['slug'])) { unset($profileDropDownItems[$index]); continue; } $defaultItem = Arr::get($defaultProfileDropDownItems, $item['slug'], []); if ($defaultItem) { $preservedKeys = ['is_system', 'is_locked', 'is_unavailable', 'slug']; foreach ($preservedKeys as $key) { if (isset($defaultItem[$key])) { $item[$key] = Arr::get($defaultItem, $key); } } if (Arr::get($defaultItem, 'is_system') === 'yes') { $item['permalink'] = $defaultItem['permalink']; if (empty($item['shape_svg'])) { $item['shape_svg'] = $defaultItem['shape_svg']; } } } } } else { $profileDropDownItems = $defaultProfileDropDownItems; } $beforeCommunityMenuItems = Arr::get($menuGroups, 'beforeCommunityMenuItems', []); $afterCommunityMenuGroups = Arr::get($menuGroups, 'afterCommunityLinkGroups', []); if (!is_array($beforeCommunityMenuItems)) { $beforeCommunityMenuItems = []; } if (!is_array($afterCommunityMenuGroups)) { $afterCommunityMenuGroups = []; } if ($context == 'view') { $currentUser = self::getCurrentUser(); $mainItems = array_filter($mainItems, function ($item) use ($currentUser) { return self::isLinkAccessible($item, $currentUser); }); $profileDropDownItems = array_filter($profileDropDownItems, function ($item) use ($currentUser) { return self::isLinkAccessible($item, $currentUser); }); $beforeCommunityMenuItems = array_filter($beforeCommunityMenuItems, function ($item) use ($currentUser) { return self::isLinkAccessible($item, $currentUser); }); $validGroups = []; foreach ($afterCommunityMenuGroups as $group) { if (empty($group['items']) || !is_array($group['items'])) { continue; } $group['items'] = array_filter($group['items'], function ($item) use ($currentUser) { return self::isLinkAccessible($item, $currentUser); }); if ($group['items']) { $validGroups[] = $group; } } $afterCommunityMenuGroups = $validGroups; } $menuGroups['mainMenuItems'] = $mainItems; $menuGroups['profileDropdownItems'] = $profileDropDownItems; $menuGroups['beforeCommunityMenuItems'] = $beforeCommunityMenuItems; $menuGroups['afterCommunityLinkGroups'] = $afterCommunityMenuGroups; if ($context == 'view') { $menuGroups = apply_filters('fluent_community/menu_groups', $menuGroups); } return $menuGroups; } public static function isLinkAccessible($link, $currentUser = null) { $isEnabled = Arr::get($link, 'enabled', 'yes') === 'yes'; $isUnavailable = Arr::get($link, 'is_unavailable') === 'yes'; if (!$isEnabled || $isUnavailable) { return false; } $privacy = Arr::get($link, 'privacy', ''); if (!$privacy || $privacy === 'public') { return true; } if ($privacy == 'logged_in') { return !!$currentUser; } if ($privacy == 'logged_out_only') { return !$currentUser; } $membershipIds = Arr::get($link, 'membership_ids', []); if (!$membershipIds) { return true; } if (!$currentUser) { return false; } static $userSpacesIds = []; if (!isset($userSpacesIds[$currentUser->ID])) { $userSpacesIds[$currentUser->ID] = $currentUser->getJoinedSpaceIds(); } $ids = $userSpacesIds[$currentUser->ID]; return $ids && !!array_intersect($ids, $membershipIds); } /** * Get the meta data for a space. * * @param int $spaceId The ID of the space. * @param string $key The meta key. * @param mixed $default The default value if the meta key is not found. * @return mixed The meta value or the default value if not found. */ public static function getSpaceMeta($spaceId, $key, $default = null) { $meta = Meta::where('object_type', 'space') ->where('meta_key', $key) ->where('object_id', $spaceId) ->first(); if (!$meta) { return $default; } return $meta->value; } /** * Update the meta data for a space. * * @param int $spaceId The ID of the space. * @param string $key The meta key. * @param mixed $value The meta value. * @return Meta The updated meta object. */ public static function updateSpaceMeta($spaceId, $key, $value) { $meta = Meta::where('object_type', 'space') ->where('meta_key', $key) ->where('object_id', $spaceId) ->first(); if ($meta) { $meta->value = $value; $meta->save(); } else { $meta = Meta::create([ 'object_type' => 'space', 'object_id' => $spaceId, 'meta_key' => $key, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'value' => $value ]); } return $meta; } /** * Encrypt or decrypt a value. * * @param string $value The value to encrypt or decrypt. * @param string $type The type of operation ('e' for encrypt, 'd' for decrypt). * @return string|false The encrypted or decrypted value or false if an error occurs. */ public static function encryptDecrypt($value, $type = 'e') { if (!$value) { return $value; } if (!extension_loaded('openssl')) { return $value; } if (defined('FLUENT_COM_ENCRYPT_SALT')) { $salt = FLUENT_COM_ENCRYPT_SALT; } else { $salt = (defined('LOGGED_IN_SALT') && '' !== LOGGED_IN_SALT) ? LOGGED_IN_SALT : 'this-is-a-fallback-salt-but-not-secure'; } if (defined('FLUENT_COM__ENCRYPT_KEY')) { $key = FLUENT_COM__ENCRYPT_KEY; } else { $key = (defined('LOGGED_IN_KEY') && '' !== LOGGED_IN_KEY) ? LOGGED_IN_KEY : 'this-is-a-fallback-key-but-not-secure'; } if ($type == 'e') { $method = 'aes-256-ctr'; $ivlen = openssl_cipher_iv_length($method); $iv = openssl_random_pseudo_bytes($ivlen); $raw_value = openssl_encrypt($value . $salt, $method, $key, 0, $iv); if (!$raw_value) { return false; } return base64_encode($iv . $raw_value); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode } $raw_value = base64_decode($value, true); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode $method = 'aes-256-ctr'; $ivlen = openssl_cipher_iv_length($method); $iv = substr($raw_value, 0, $ivlen); $raw_value = substr($raw_value, $ivlen); $newValue = openssl_decrypt($raw_value, $method, $key, 0, $iv); if (!$newValue || substr($newValue, -strlen($salt)) !== $salt) { return false; } return substr($newValue, 0, -strlen($salt)); } /** * Get the welcome banner configuration. * * @return array The welcome banner configuration. */ public static function getWelcomeBannerSettings() { $defaults = [ 'login' => [ 'enabled' => 'no', 'description' => '', 'mediaType' => 'image', 'allowClose' => 'no', 'bannerImage' => '', 'bannerVideo' => [ 'type' => 'oembed', 'url' => '', 'content_type' => '', 'provider' => '', 'title' => '', 'author_name' => '', 'html' => '' ], 'ctaButtons' => [] ], 'logout' => [ 'enabled' => 'no', 'description' => '', 'mediaType' => 'image', 'useCustomUrl' => 'no', 'bannerImage' => '', 'bannerVideo' => [ 'type' => 'oembed', 'url' => '', 'content_type' => '', 'provider' => '', 'title' => '', 'author_name' => '', 'html' => '' ], 'ctaButtons' => [] ] ]; $settings = Utility::getOption('welcome_banner_settings', []); $settings = wp_parse_args($settings, $defaults); if (empty(Arr::get($settings, 'login.bannerVideo'))) { $settings['login']['bannerVideo'] = $defaults['login']['bannerVideo']; } if (empty(Arr::get($settings, 'logout.bannerVideo'))) { $settings['logout']['bannerVideo'] = $defaults['logout']['bannerVideo']; } return $settings; } public static function getWelcomeBanner($view = 'login') { $settings = self::getWelcomeBannerSettings(); $welcomeBanner = Arr::get($settings, $view, []); if (Arr::get($welcomeBanner, 'enabled') != 'yes') { return null; } unset($welcomeBanner['description']); if ($view == 'login') { return apply_filters('fluent_community/welcome_banner_for_logged_in', $welcomeBanner); } return apply_filters('fluent_community/welcome_banner_for_guests', $welcomeBanner); } public static function getEnabledFeedLinks() { $links = array_filter(self::getFeedLinks(), function ($item) { return self::isLinkAccessible($item); }); return array_values($links); } public static function getMobileMenuItems($context = 'headless') { $xprofile = Helper::getCurrentProfile(); $mainMenuItems = Arr::get(self::getMenuItemsGroup('view'), 'mainMenuItems', []); $defaultMobileIcons = [ 'all_feeds' => '', 'spaces' => '' ]; $mobileMenuItems = []; foreach ($defaultMobileIcons as $slug => $defaultIcon) { $menuItem = Arr::get($mainMenuItems, $slug); if (!$menuItem) { continue; } $iconSvg = Arr::get($menuItem, 'shape_svg'); $mobileMenuItems[] = [ 'route' => [ 'name' => $slug ], 'title' => Arr::get($menuItem, 'title'), 'icon_svg' => $iconSvg ? CustomSanitizer::sanitizeSvg($iconSvg) : $defaultIcon ]; } if ($xprofile) { $mobileMenuItems[] = [ 'route' => [ 'name' => 'user_profile', 'params' => [ 'username' => $xprofile->username ] ], 'title' => __('Profile', 'fluent-community'), 'icon_svg' => '' ]; } else if (!get_current_user_id()) { $mobileMenuItems[] = [ 'name' => 'login', 'title' => __('Login', 'fluent-community'), 'permalink' => Helper::getAuthUrl(), 'icon_svg' => '' ]; } return apply_filters('fluent_community/mobile_menu', $mobileMenuItems, $xprofile, $context); } public static function getFeedLinks() { return Utility::getOption('feed_links', []); } public static function updateFeedLinks($links) { Utility::updateOption('feed_links', $links); } /** * Get the full name of a WordPress user. * * @param int|null $id The ID of the user. * @return string The full name of the user. */ public static function getWpUserFullName($id = null) { $id = $id ?: get_current_user_id(); $user = get_user_by('ID', $id); $fullName = $user->display_name; if ($user->first_name && $user->last_name) { $fullName = $user->first_name . ' ' . $user->last_name; } return $fullName; } /** * Get the onboarding settings. * * @return array The onboarding settings. */ public static function getOnboardingSettings() { $default = [ 'is_onboarding_enabled' => 'no', 'registration_page_url' => '', ]; $settings = Utility::getOption('onboarding_settings', $default); return wp_parse_args($settings, $default); } /** * Get all WordPress published pages. * * @return array An array of page data. */ public static function getAllWpPublishedPage() { $posts = get_posts(array( 'post_status' => 'publish', 'numberposts' => -1, 'post_type' => 'any', )); return array_map(function ($post) { return array( 'id' => $post->ID, 'permalink' => get_permalink($post), 'title' => get_the_title($post), ); }, $posts); } /** * Add a user to a space. * * @param BaseSpace|int $space space to add the user to. * @param int $userId The ID of the user to add. * @param string $role The role of the user in the space. * @param string $by The source of the action. * @return bool True if the user was added, false otherwise. */ public static function addToSpace($space, $userId, $role = 'member', $by = 'self', $skipSync = false) { if (is_numeric($space)) { $space = BaseSpace::onlyMain()->find($space); } if (!$space || !$space instanceof BaseSpace) { return false; } if (!$skipSync) { $user = User::find($userId); if (!$user) { return false; } $user->syncXProfile(); } if ($role == 'member' && $space->type == 'course') { $role = 'student'; } $exist = SpaceUserPivot::where('user_id', $userId) ->where('space_id', $space->id) ->first(); if ($exist) { if ($exist->status != 'active') { $exist->status = 'active'; if (!in_array($exist->role, ['admin', 'moderator'])) { $exist->role = $role; } $exist->save(); if ($space->type == 'course') { do_action('fluent_community/course/enrolled', $space, $userId, $by); } else { do_action('fluent_community/space/joined', $space, $userId, $by); } return true; } return false; } $created = SpaceUserPivot::create([ 'space_id' => $space->id, 'role' => $role, 'user_id' => $userId ]); if ($space->type == 'course') { if (!$space instanceof Course) { $space = Course::find($space->id); // we are renewing the model to have access to course relations } do_action('fluent_community/course/enrolled', $space, $userId, $by, $created); } else { if (!$space instanceof Space) { $space = Space::find($space->id); // we are renewing the model to have access to space relations } do_action('fluent_community/space/joined', $space, $userId, $by, $created); } return true; } /** * Remove a user from a space if exist. * * @param int $userId The ID of the user. * @param int $spaceId The ID of the space. * @param string $by The source of the action. self | by_admin * @return bool True if the user is in the space, false otherwise. */ public static function removeFromSpace($space, $userId, $by = 'self') { $user = User::find($userId); if (!$user) { return false; } if (is_numeric($space)) { $space = BaseSpace::query()->onlyMain()->find($space); } if (!$space || !$space instanceof BaseSpace) { return false; } if (!self::isUserInSpace($userId, $space->id)) { return false; } SpaceUserPivot::where('space_id', $space->id) ->where('user_id', $userId) ->delete(); $user->cacheAccessSpaces(); if ($space->type == 'course') { if (!$space instanceof Course) { $space = Course::find($space->id); // we are renewing the model to have access to course relations } do_action('fluent_community/course/student_left', $space, $userId, $by); } else { if (!$space instanceof Space) { $space = Space::find($space->id); } // we are renewing the model to have access to space relations do_action('fluent_community/space/user_left', $space, $userId, $by); } return true; } /** * Render a link with icon. * * @param array $link The link data. * @param string $linkClass Additional classes for the link. * @param string $fallback The fallback content if no icon is found. * @param bool $renderIcon Whether to render the icon or not. */ public static function renderLink($link, $linkClass = '', $fallback = '', $renderIcon = true) { if (!$link || empty($link['permalink'])) { return; } $isCustom = Arr::get($link, 'is_custom') == 'yes'; $linkAtts = array_filter([ 'class' => trim($linkClass . ' ' . Arr::get($link, 'link_classes')) . ' fcom_compt_link' . ($isCustom ? ' fcom_custom_link' : ''), 'target' => Arr::get($link, 'new_tab') === 'yes' ? '_blank' : '', 'rel' => Arr::get($link, 'new_tab') === 'yes' ? 'noopener noreferrer' : '', ]); ?> $value) { echo esc_attr($key) . '="' . esc_attr($value) . '"'; } ?>> $item): ?>
  • $item): ?>
  • ') { ?>
    ' . wp_kses_post($fallback) . ''; endif; } /** * Get the IP address of the user. * * @param bool $anonymize Whether to anonymize the IP address. * @return string The IP address. */ public static function getIp($anonymize = false) { static $ipAddress; if ($ipAddress) { return $ipAddress; } if (empty($_SERVER['REMOTE_ADDR'])) { // It's a local cli request return '127.0.0.1'; } $ipAddress = ''; if (isset($_SERVER["HTTP_CF_CONNECTING_IP"])) { $ipAddress = sanitize_text_field(wp_unslash($_SERVER["REMOTE_ADDR"])); //If it's a valid Cloudflare request if (self::isCfIp($ipAddress)) { //Use the CF-Connecting-IP header. $ipAddress = sanitize_text_field(wp_unslash($_SERVER['HTTP_CF_CONNECTING_IP'])); } } else if ($_SERVER['REMOTE_ADDR'] == '127.0.0.1') { // most probably it's local reverse proxy if (isset($_SERVER["HTTP_CLIENT_IP"])) { $ipAddress = sanitize_text_field(wp_unslash($_SERVER["HTTP_CLIENT_IP"])); } else if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { $forwardedIp = trim(current(preg_split('/,/', sanitize_text_field(wp_unslash($_SERVER['HTTP_X_FORWARDED_FOR']))))); if (rest_is_ip_address($forwardedIp)) { $ipAddress = $forwardedIp; } } } if (!$ipAddress) { $ipAddress = sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])); } $ipAddress = preg_replace('/^(\d+\.\d+\.\d+\.\d+):\d+$/', '\1', $ipAddress); $ipAddress = apply_filters('fluent_auth/user_ip', $ipAddress); if ($anonymize) { return wp_privacy_anonymize_ip($ipAddress); } return $ipAddress; } /** * Check if the IP address is from Cloudflare. * * @param string $ip The IP address to check. * @return bool True if the IP is from Cloudflare, false otherwise. */ public static function isCfIp($ip = '') { if (!$ip && isset($_SERVER["REMOTE_ADDR"])) { $ip = sanitize_text_field(wp_unslash($_SERVER["REMOTE_ADDR"])); } if (!$ip) { return false; } $cloudflareIPRanges = array( '173.245.48.0/20', '103.21.244.0/22', '103.22.200.0/22', '103.31.4.0/22', '141.101.64.0/18', '108.162.192.0/18', '190.93.240.0/20', '188.114.96.0/20', '197.234.240.0/22', '198.41.128.0/17', '162.158.0.0/15', '104.16.0.0/13', '104.24.0.0/14', '172.64.0.0/13', '131.0.72.0/22', ); //Make sure that the request came via Cloudflare. foreach ($cloudflareIPRanges as $range) { //Use the ip_in_range function from Joomla. if (self::ipInRange($ip, $range)) { //IP is valid. Belongs to Cloudflare. return true; } } return false; } /** * Check if the IP address is in the given range. * * @param string $ip The IP address to check. * @param string $range The range to check against. * @return bool True if the IP is in the range, false otherwise. */ private static function ipInRange($ip, $range) { if (strpos($range, '/') !== false) { // $range is in IP/NETMASK format list($range, $netmask) = explode('/', $range, 2); if (strpos($netmask, '.') !== false) { // $netmask is a 255.255.0.0 format $netmask = str_replace('*', '0', $netmask); $netmask_dec = ip2long($netmask); return ((ip2long($ip) & $netmask_dec) == (ip2long($range) & $netmask_dec)); } else { // $netmask is a CIDR size block // fix the range argument $x = explode('.', $range); while (count($x) < 4) $x[] = '0'; list($a, $b, $c, $d) = $x; $range = sprintf("%u.%u.%u.%u", empty($a) ? '0' : $a, empty($b) ? '0' : $b, empty($c) ? '0' : $c, empty($d) ? '0' : $d); $range_dec = ip2long($range); $ip_dec = ip2long($ip); # Strategy 1 - Create the netmask with 'netmask' 1s and then fill it to 32 with 0s #$netmask_dec = bindec(str_pad('', $netmask, '1') . str_pad('', 32-$netmask, '0')); # Strategy 2 - Use math to create it $wildcard_dec = pow(2, (32 - $netmask)) - 1; $netmask_dec = ~$wildcard_dec; return (($ip_dec & $netmask_dec) == ($range_dec & $netmask_dec)); } } else { // range might be 255.255.*.* or 1.2.3.0-1.2.3.255 if (strpos($range, '*') !== false) { // a.b.*.* format // Just convert to A-B format by setting * to 0 for A and 255 for B $lower = str_replace('*', '0', $range); $upper = str_replace('*', '255', $range); $range = "$lower-$upper"; } if (strpos($range, '-') !== false) { // A-B format list($lower, $upper) = explode('-', $range, 2); $lower_dec = (float)sprintf("%u", ip2long($lower)); $upper_dec = (float)sprintf("%u", ip2long($upper)); $ip_dec = (float)sprintf("%u", ip2long($ip)); return (($ip_dec >= $lower_dec) && ($ip_dec <= $upper_dec)); } return false; } } public static function getPortalRequestPath($requestUri) { $portalSlug = self::getPortalSlug(); // If portal is mounted at site root with empty requestUri, ignore query-only requests that do not relate to the community portal. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only routing check on $_GET, no state mutation if ($portalSlug === '' && $requestUri === '' && !empty($_GET) && !self::hasSupportedQueryParam()) { return false; } if ($portalSlug == $requestUri) { return 'portal_home'; } if (!$requestUri) { return false; } if ($portalSlug) { // remove the portal slug from the request uri. Don't use str_replace as it will replace all occurrences $requestUri = substr($requestUri, strlen($portalSlug)); } $parts = explode('/', $requestUri); $start = $parts[0]; if (!$portalSlug && $start == 'fcom_route') { return $start; } $routeStats = self::portalRoutePaths(); if (in_array($start, $routeStats)) { return $requestUri; } return false; } /** * Check if the current request has any supported query parameter. * * @return bool */ private static function hasSupportedQueryParam() { $supportedParams = (array) apply_filters('fluent_community/portal_supported_query_params', [ 'customizer_panel', 'create_space' ]); // phpcs:ignore WordPress.Security.NonceVerification.Recommended foreach (array_keys($_GET) as $key) { if (strpos($key, 'fcom_') === 0) { return true; } if (in_array($key, $supportedParams, true)) { return true; } } return false; } public static function getTopicsConfig() { $config = Utility::getOption('topics_config', []); $default = [ 'max_topics_per_post' => 1, 'max_topics_per_space' => 20, 'show_on_post_card' => 'yes' ]; return wp_parse_args($config, $default); } public static function getModerationConfig() { $config = Utility::getOption('moderation_config', []); $default = [ 'is_enabled' => 'no', 'profanity_filter' => "", 'flag_after_threshold' => 0, 'flag_all_new_posts' => 'no', 'first_post_approval' => 'no', 'first_comment_approval' => 'no', 'flag_all_new_posts_spaces' => [], 'auto_flag_user_reject_threshold' => 0, 'auto_flag_user_report_threshold' => 0, ]; return wp_parse_args($config, $default); } public static function getReportReasons() { return apply_filters('fluent_community/report_reasons', [ 'harassment' => __('Harassment', 'fluent-community'), 'spam' => __('Spam', 'fluent-community'), 'offensive' => __('Offensive', 'fluent-community'), 'incorrect_space' => __('Incorrect Space', 'fluent-community'), 'against_community' => __('Against Community Rules', 'fluent-community'), 'other' => __('Other', 'fluent-community'), ]); } public static function htmlToMd($html) { return preg_replace('/(.*?)<\/a>/', '[$2]($1)', $html); } public static function isProfanity($profanity, $text) { $profanity = explode(',', $profanity); if (empty($profanity)) { return false; } $profanity = array_map('trim', $profanity); $profanity = array_map(function ($word) { return mb_strtolower($word, 'UTF-8'); }, $profanity); $text = mb_strtolower($text, 'UTF-8'); // Convert words into a regex pattern (ensuring whole-word matching) $pattern = '/(? 'sunday', 'mon' => 'monday', 'tue' => 'tuesday', 'wed' => 'wednesday', 'thu' => 'thursday', 'fri' => 'friday', 'sat' => 'saturday' ]; return isset($dayMap[$day]) ? $dayMap[$day] : $day . 'day'; } public static function getPostOrderOptions($context = 'feed') { $options = [ 'new_activity' => __('New Activity', 'fluent-community'), 'latest' => __('Latest', 'fluent-community'), 'oldest' => __('Oldest', 'fluent-community'), 'popular' => __('Popular', 'fluent-community'), 'likes' => __('Likes', 'fluent-community'), 'alphabetical' => __('Alphabetical', 'fluent-community'), 'unanswered' => __('Unanswered', 'fluent-community'), ]; return apply_filters('fluent_community/post_order_options', $options, $context); } public static function getCommentOrderOptions($context = 'comment') { $options = [ 'oldest' => __('Earliest', 'fluent-community'), 'latest' => __('Latest', 'fluent-community'), 'popular' => __('Popular', 'fluent-community'), 'most_replied' => __('Most Replied', 'fluent-community'), ]; return apply_filters('fluent_community/comment_order_options', $options, $context); } public static function convertPhpDateToDayJSFormay($phpFormat) { // Mapping PHP date format characters to Day.js format characters $replacements = [ // Day 'd' => 'DD', // Day of the month, 2 digits with leading zeros 'D' => 'ddd', // A textual representation of a day, three letters 'j' => 'D', // Day of the month without leading zeros 'l' => 'dddd', // A full textual representation of the day of the week 'N' => 'E', // ISO-8601 numeric representation of the day of the week 'S' => 'o', // English ordinal suffix for the day of the month, 2 characters 'w' => 'd', // Numeric representation of the day of the week 'z' => 'DDD', // The day of the year (starting from 0) // Week 'W' => 'W', // ISO-8601 week number of year, weeks starting on Monday // Month 'F' => 'MMMM', // A full textual representation of a month 'm' => 'MM', // Numeric representation of a month, with leading zeros 'M' => 'MMM', // A short textual representation of a month, three letters 'n' => 'M', // Numeric representation of a month, without leading zeros 't' => '', // Not supported in Day.js (Number of days in the given month) // Year 'L' => '', // Not supported in Day.js (Whether it's a leap year) 'o' => 'GGGG', // ISO-8601 week-numbering year 'Y' => 'YYYY', // A full numeric representation of a year, 4 digits 'y' => 'YY', // A two digit representation of a year // Time 'a' => 'a', // Lowercase Ante meridiem and Post meridiem 'A' => 'A', // Uppercase Ante meridiem and Post meridiem 'B' => '', // Not supported in Day.js (Swatch Internet time) 'g' => 'h', // 12-hour format of an hour without leading zeros 'G' => 'H', // 24-hour format of an hour without leading zeros 'h' => 'hh', // 12-hour format of an hour with leading zeros 'H' => 'HH', // 24-hour format of an hour with leading zeros 'i' => 'mm', // Minutes with leading zeros 's' => 'ss', // Seconds with leading zeros 'u' => 'SSS', // Milliseconds (Day.js uses SSS for fractional seconds) 'v' => 'SSS', // Milliseconds (Day.js uses SSS for fractional seconds) // Timezone 'e' => '', // Not supported in Day.js (Timezone identifier) 'I' => '', // Not supported in Day.js (Whether or not the date is in daylight saving time) 'O' => 'ZZ', // Difference to Greenwich time (GMT) in hours 'P' => 'Z', // Difference to Greenwich time (GMT) with colon between hours and minutes 'T' => '', // Not supported in Day.js (Timezone abbreviation) 'Z' => '', // Not supported in Day.js (Timezone offset in seconds) // Full Date/Time 'c' => 'YYYY-MM-DDTHH:mm:ssZ', // ISO 8601 date 'r' => 'ddd, DD MMM YYYY HH:mm:ss ZZ', // RFC 2822 formatted date 'U' => 'X', // Seconds since the Unix Epoch (January 1 1970 00:00:00 GMT) ]; // Replace each PHP date format character with Day.js equivalent $dayjsFormat = ""; for ($i = 0; $i < strlen($phpFormat); $i++) { $char = $phpFormat[$i]; // Special handling for G\hi pattern if ($char === 'G' && $i + 2 < strlen($phpFormat) && $phpFormat[$i + 1] === '\\' && $phpFormat[$i + 2] === 'h') { $dayjsFormat .= 'H[h]'; $i += 2; continue; } // Check if the character is escaped if ($char === "\\") { // Day.js escapes literal text with square brackets, not backslashes $i++; if ($i < strlen($phpFormat)) { $dayjsFormat .= "[" . $phpFormat[$i] . "]"; } continue; } // Add the mapped character or the character itself if not found in the mapping $dayjsFormat .= $replacements[$char] ?? $char; } return $dayjsFormat; } public static function getDateFormatter($isDayJs = false) { $format = get_option('date_format'); if ($isDayJs) { return self::convertPhpDateToDayJSFormay($format); } return $format; } public static function getTimeFormatter($isDayJs = false) { $format = get_option('time_format'); if ($isDayJs) { return self::convertPhpDateToDayJSFormay($format); } return $format; } public static function normalizeToAscii($text) { if (function_exists('transliterator_transliterate')) { $result = transliterator_transliterate('Any-Latin; Latin-ASCII', $text); if ($result !== false) { return $result; } } if (function_exists('iconv')) { $result = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $text); if ($result !== false) { return $result; } } return $text; } public static function getPathFromUrl($url) { return rtrim((string) wp_parse_url($url, PHP_URL_PATH), '/'); } // Return the ID of the group that contains the current page public static function getActiveSidebarGroupId($groups) { if (empty($_SERVER['REQUEST_URI'])) { return ''; } $url=esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated $currentPath = self::getPathFromUrl($url); if ($currentPath === '') { return ''; } foreach ($groups as $group) { $items = isset($group['children']) ? $group['children'] : ($group['items'] ?? []); foreach ($items as $item) { $item_path = isset($item['permalink']) ? self::getPathFromUrl($item['permalink']) : ''; if (is_array($item) && !empty($item['permalink']) && $item_path === $currentPath) { return sanitize_key((string) ($group['id'] ?? $group['slug'] ?? '')); } } } return ''; } // Explicit per-user choices from the cookie. Format: "c:1,2|e:3,4" (c = collapsed, e = expanded). public static function getSidebarGroupStates() { $collapsed = []; $expanded = []; $cookie = isset($_COOKIE['fcom_sidebar_group_states']) ? sanitize_text_field(wp_unslash($_COOKIE['fcom_sidebar_group_states'])) : ''; foreach (explode('|', $cookie) as $section) { list($flag, $ids) = array_pad(explode(':', $section, 2), 2, ''); $list = array_filter(array_map('sanitize_key', explode(',', $ids))); if ($flag === 'c') { $collapsed = $list; } elseif ($flag === 'e') { $expanded = $list; } } return [$collapsed, $expanded]; } public static function getCollapsedSidebarGroups($groups = []) { $isDefaultCollapse = Utility::isCustomizationEnabled('collapse_sidebar_groups'); list($collapsedByUser, $expandedByUser) = self::getSidebarGroupStates(); $activeGroupId = self::getActiveSidebarGroupId($groups); /** * Precendence. * 1. No group id or slug => Fallback to expanded * 2. Has Active Link => Always expanded * 3. Explicitly expanded by user => Always expanded * 4. Explicitly collapsed by user => Collapsed * 5. Default Collapse by setting => Collapsed * 6. Otherwise => Expanded */ $collapsed = []; foreach ($groups as $group) { $id = sanitize_key((string) ($group['id'] ?? $group['slug'] ?? '')); // Matching expanded condition (1,2,3) if ($id === '' || $id === $activeGroupId || in_array($id, $expandedByUser, true)) { continue; } // Matching collapsed condition (4,5) if ($isDefaultCollapse || in_array($id, $collapsedByUser, true)) { $collapsed[] = $id; } // else (6) => expanded, do nothing } return $collapsed; } }