PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 1.0.96
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v1.0.96
2.10.0 2.10.01 2.9.1 2.9.0 2.8.1 2.8.0 2.7.7 2.7.5 2.7.0 2.6.01 2.6.0 2.5.0 2.4.01 trunk 1.0.90 1.0.91 1.0.92 1.0.93 1.0.94 1.0.95 1.0.96 1.0.97 1.0.98 1.0.99 1.1.0 All 77 releases
fluent-community / app / Services / Helper.php

Helper.php in FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses 1.0.96, at app/Services/Helper.php

1,690 lines 60.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCommunity\App\Services;
4
5 use FluentCommunity\App\App;
6 use FluentCommunity\App\Functions\Utility;
7 use FluentCommunity\App\Hooks\Handlers\ActivationHandler;
8 use FluentCommunity\App\Models\BaseSpace;
9 use FluentCommunity\App\Models\Space;
10 use FluentCommunity\App\Models\Media;
11 use FluentCommunity\App\Models\Meta;
12 use FluentCommunity\App\Models\SpaceUserPivot;
13 use FluentCommunity\App\Models\User;
14 use FluentCommunity\App\Models\XProfile;
15 use FluentCommunity\Framework\Support\Arr;
16 use FluentCommunity\App\Models\SpaceGroup;
17
18 /**
19 * Helper class for various utility functions.
20 */
21 class Helper
22 {
23
24 public static function isRtl()
25 {
26 return apply_filters('fluent_community/is_rtl', is_rtl());
27 }
28
29 /**
30 * Get the portal slug.
31 *
32 * @return string The portal slug.
33 */
34 public static function getPortalSlug($forRoute = false)
35 {
36 $settings = self::generalSettings();
37 $slug = apply_filters('fluent_community/portal_slug', $settings['slug']);
38
39 if (!$forRoute) {
40 return $slug;
41 }
42
43 $siteUrl = get_site_url();
44
45 $poralUrl = self::baseUrl('/');
46
47 // get the url without path
48 $siteUrl = str_replace(parse_url($siteUrl, PHP_URL_PATH), '', $siteUrl);
49
50 $slug = str_replace($siteUrl, '', $poralUrl);
51
52 // remove the first and last slashes
53 return trim($slug, '/');
54 }
55
56 /**
57 * Get the portal route type.
58 *
59 * @return string The portal route type.
60 */
61 public static function getPortalRouteType()
62 {
63 return apply_filters('fluent_community/portal_route_type', 'WebHistory');
64 }
65
66 /**
67 * Check if the portal is headless.
68 *
69 * @return bool True if headless, false otherwise.
70 */
71 public static function isHeadless()
72 {
73 return apply_filters('fluent_community/portal_page_headless', false);
74 }
75
76 /**
77 * Check if the portal has a color scheme.
78 *
79 * @return bool True if has color scheme, false otherwise.
80 */
81 public static function hasColorScheme()
82 {
83 $status = Utility::isCustomizationEnabled('dark_mode');
84 return apply_filters('fluent_community/has_color_scheme', $status);
85 }
86
87 /**
88 * Check if the user is a site admin.
89 *
90 * @param int|null $userId The user ID to check. If null, checks the current user.
91 * @return bool True if the user is a site admin, false otherwise.
92 */
93 public static function isSiteAdmin($userId = null)
94 {
95 $capability = apply_filters('fluent_community/super_admin_capability', 'manage_options');
96
97 if (!$capability) {
98 return false;
99 }
100
101 if ($userId === null) {
102 $userId = get_current_user_id();
103 }
104
105 if (!$userId) {
106 return false;
107 }
108
109 return user_can($userId, $capability);
110 }
111
112 public static function isModerator($user = null)
113 {
114 if (!$user) {
115 $user = self::getCurrentUser();
116 }
117 return $user && $user->isCommunityModerator();
118 }
119
120 /**
121 * Get the URL for an asset file.
122 *
123 * @param string $file The file name.
124 * @return string The full URL to the asset.
125 */
126 public static function assetUrl($file = '')
127 {
128 return FLUENT_COMMUNITY_PLUGIN_URL . 'assets/' . $file;
129 }
130
131 /**
132 * Get the base URL for the portal.
133 *
134 * @param string $path The path to append to the base URL.
135 * @return string The full base URL.
136 */
137 public static function baseUrl($path = '')
138 {
139 $baseUrl = apply_filters('fluent_community/base_url', home_url(self::getPortalSlug()));
140 $baseUrl = rtrim($baseUrl, '/');
141
142 if (self::getPortalRouteType() != 'hash') {
143 return $baseUrl . '/' . ltrim($path, '/');
144 }
145
146 if (!$path) {
147 return $baseUrl . '/';
148 }
149
150 return $baseUrl . '/#/' . ltrim($path, '/');
151 }
152
153 public static function getAuthUrl()
154 {
155 $settings = self::generalSettings();
156
157 return Arr::get($settings, 'cutsom_auth_url', '');
158 }
159
160 /**
161 * Get the space IDs for a user.
162 *
163 * @param int|null $userId The user ID. If null, uses the current user.
164 * @return array An array of space IDs.
165 */
166 public static function getUserSpaceIds($userId = null)
167 {
168 if (!$userId) {
169 $userId = get_current_user_id();
170 }
171
172 return SpaceUserPivot::where('user_id', $userId)
173 ->where('status', 'active')
174 ->pluck('space_id')
175 ->toArray();
176 }
177
178 public static function getUserSpaces($userId = null)
179 {
180 if (!$userId) {
181 $userId = get_current_user_id();
182 }
183
184 return Space::whereHas('members', function ($query) use ($userId) {
185 $query->where('user_id', $userId);
186 })->get();
187 }
188
189 /**
190 * Check if a user is in a specific space.
191 *
192 * @param int $userId The user ID.
193 * @param int $spaceId The space ID.
194 * @return bool True if the user is in the space, false otherwise.
195 */
196 public static function isUserInSpace($userId, $spaceId)
197 {
198 return SpaceUserPivot::where('user_id', $userId)
199 ->where('space_id', $spaceId)
200 ->where('status', 'active')
201 ->exists();
202 }
203
204 /**
205 * Generate HTML attributes from an array.
206 *
207 * @param array $atts An array of attribute key-value pairs.
208 * @return string The generated HTML attributes string.
209 */
210 public static function attrs($atts = [])
211 {
212 $text = '';
213
214 foreach ($atts as $key => $value) {
215 $text .= "$key=\"$value\" ";
216 }
217
218 return $text;
219 }
220
221 /**
222 * Get media from a URL.
223 *
224 * @param string|array $url The URL or an array containing URL information.
225 * @return Media|null The Media object if found, null otherwise.
226 */
227 public static function getMediaFromUrl($url)
228 {
229 if (is_array($url) && isset($url['provider'])) {
230 $provider = Arr::get($url, 'provider');
231
232 if ($provider == 'giphy') {
233 return null;
234 }
235
236 $url = Arr::get($url, 'url');
237 }
238
239 if (!$url) {
240 return null;
241 }
242
243 $parsedUrl = wp_parse_url($url, PHP_URL_QUERY);
244
245 if (!$parsedUrl) {
246 return null;
247 }
248
249 // Parse the query string to get the media_key value
250 parse_str($parsedUrl, $queryParams);
251
252 $key = Arr::get($queryParams, 'media_key');
253
254 if (!$key) {
255 return null;
256 }
257
258 return Media::where('media_key', $key)->first();
259 }
260
261 /**
262 * Get media items from multiple URLs.
263 *
264 * @param array $urls An array of URLs.
265 * @return array An array of Media objects.
266 */
267 public static function getMediaItemsFromUrl($urls)
268 {
269 $mediaItems = [];
270
271 foreach ($urls as $url) {
272 $media = self::getMediaFromUrl($url);
273
274 if ($media) {
275 $mediaItems[] = $media;
276 }
277 }
278
279 return $mediaItems;
280 }
281
282 /**
283 * Get general settings for the community.
284 *
285 * @param bool $cached Whether to use cached settings.
286 * @return array The general settings.
287 */
288 public static function generalSettings($cached = true)
289 {
290 static $settings = null;
291
292 if ($cached && $settings) {
293 return $settings;
294 }
295
296 $settings = get_option('fluent_community_settings', []);
297
298 $defaults = [
299 'site_title' => get_bloginfo('name'),
300 'slug' => 'portal',
301 'logo' => '',
302 'white_logo' => '',
303 'featured_image' => '',
304 'access' => [
305 'acess_level' => 'logged_in', // logged_in, public, role_based
306 'access_roles' => []
307 ],
308 'auth_form_type' => 'default',
309 'disable_global_posts' => 'yes',
310 'auth_content' => 'Please login first to access this page',
311 'auth_redirect' => '',
312 'restricted_role_content' => 'Sorry, you can not access to this page. Only authorized users can access this page.',
313 'auth_url' => '',
314 'cutsom_auth_url' => self::baseUrl('?fcom_action=auth'),
315 ];
316
317 $settings = wp_parse_args($settings, $defaults);
318 if($settings['auth_form_type'] != 'custom' || empty($settings['auth_form_type'])) {
319 $settings['cutsom_auth_url'] = self::baseUrl('?fcom_action=auth');
320 }
321
322 if (defined('FLUENT_COMMUNITY_PORTAL_SLUG')) {
323 $settings['slug'] = \FLUENT_COMMUNITY_PORTAL_SLUG;
324 $settings['is_slug_defined'] = true;
325 } else {
326 unset($settings['is_slug_defined']);
327 }
328
329 return $settings;
330 }
331
332 public static function hasGlobalPost()
333 {
334 $settings = self::generalSettings();
335 $status = Arr::get($settings, 'disable_global_posts', '') != 'yes';
336
337 return apply_filters('fluent_community/has_global_post', $status);
338 }
339
340 /**
341 * Check if a user can access the portal.
342 *
343 * @param int|null $userId The user ID. If null, uses the current user.
344 * @return bool True if the user can access the portal, false otherwise.
345 */
346 public static function canAccessPortal($userId = null)
347 {
348 $settings = self::generalSettings();
349 $accessLevel = Arr::get($settings, 'access.acess_level');
350
351 if ($accessLevel == 'public') {
352 return apply_filters('fluent_community/can_access_portal', true);
353 }
354
355 if (!$userId) {
356 $userId = get_current_user_id();
357 }
358
359 if (!$userId) {
360 return apply_filters('fluent_community/can_access_portal', false);
361 }
362
363 if ($accessLevel == 'logged_in') {
364 return apply_filters('fluent_community/can_access_portal', true);
365 }
366
367 if (user_can($userId, 'edit_pages')) {
368 return apply_filters('fluent_community/can_access_portal', true);
369 }
370
371 $roles = Arr::get($settings, 'access.access_roles', []);
372
373 $user = get_user_by('ID', $userId);
374
375 if (!$user) {
376 return apply_filters('fluent_community/can_access_portal', false);
377 }
378
379 $result = !!array_intersect(array_values($user->roles), $roles);
380
381 if (!$result) {
382 return apply_filters('fluent_community/can_access_portal', false);
383 }
384
385 $xProfile = Helper::getCurrentProfile();
386
387 $result = $xProfile && $xProfile->status == 'active';
388
389 return apply_filters('fluent_community/can_access_portal', $result);
390 }
391
392 /**
393 * Get the portal route paths.
394 *
395 * @return array An array of portal route paths.
396 */
397 public static function portalRoutePaths()
398 {
399 return apply_filters('fluent_community/app_route_paths', [
400 'portal_home',
401 'space',
402 'discover',
403 'members',
404 'courses',
405 'u',
406 'leaderboards',
407 'chat',
408 'notifications',
409 'bookmarks',
410 'post',
411 'admin'
412 ]);
413 }
414
415 /**
416 * Get the current user's profile.
417 *
418 * @param bool $cached Whether to use cached profile.
419 * @return XProfile|null The user's profile or null if not found.
420 */
421 public static function getCurrentProfile($cached = true)
422 {
423 $userId = get_current_user_id();
424 if (!$userId) {
425 return null;
426 }
427
428 static $profile;
429
430 if ($profile && $cached) {
431 return $profile;
432 }
433
434 if (!$userId) {
435 return null;
436 }
437
438 $profile = XProfile::where('user_id', $userId)->first();
439
440 return $profile;
441 }
442
443 /**
444 * Get the current user Model.
445 *
446 * @param bool $cached Whether to use cached user.
447 * @return User|false The User model or false if not found.
448 */
449 public static function getCurrentUser($cached = true)
450 {
451 $userId = get_current_user_id();
452 if (!$userId) {
453 return false;
454 }
455
456 static $user;
457 if ($user && $cached) {
458 return $user;
459 }
460
461 $user = User::find($userId);
462
463 return $user;
464 }
465
466 /**
467 * Get the route paths for the community.
468 *
469 * @return array An array of route paths.
470 */
471 private static function getRoutePaths()
472 {
473 return [
474 'dashboard' => '/dashboard',
475 'all_feeds' => '/',
476 'single_feed' => '/post/:feed_slug',
477 'space_feeds' => '/space/:space/home',
478 'space_feed' => '/space/:space/post/:feed_slug',
479 'space_members' => '/space/:space/members',
480 'spaces' => '/discover/spaces',
481 'settings' => '/admin/settings',
482 'admin_moderators' => '/admin/settings/moderators',
483 'all_members' => '/members',
484 'user_profile' => '/u/:username/',
485 'user_communities' => '/u/:username/spaces',
486 'update_profile' => '/u/:username/update',
487 'discussions' => '/discussions',
488 'create_topic' => '/discussions/create-topic',
489 'topic' => '/discussions/topic/:slug',
490 'notifications' => '/notifications',
491 'bookmarks' => '/bookmarks',
492 'courses' => '/courses',
493 'view_course' => '/courses/view/:course_id/lessons',
494 'view_lesson' => '/courses/view/:course_id/lessons/:lesson_slug/view',
495 'manage_courses' => '/admin/manage-courses',
496 'edit_lessons' => '/admin/manage-courses/edit/:course_id/lessons',
497 'course_students' => '/admin/manage-courses/edit/:course_id/students',
498 'course_overview' => '/admin/manage-courses/edit/:course_id/overview',
499 'manage_leaderboard' => '/admin/manage-leaderboard',
500 ];
501 }
502
503 /**
504 * Get the URL for a JavaScript route.
505 *
506 * @param array $route The route information.
507 * @return string The URL for the route.
508 */
509 public static function getUrlByJsRoute($route = [])
510 {
511 $routePaths = self::getRoutePaths();
512
513 $routeName = Arr::get($route, 'name', '');
514
515 if (!$routeName || !isset($routePaths[$routeName])) {
516 return self::baseUrl();
517 }
518
519 $path = $routePaths[$routeName];
520
521 $params = (array)Arr::get($route, 'params', []);
522
523 if (!$params) {
524 return self::baseUrl($path);
525 }
526
527 $replaces = [];
528
529 foreach ($params as $paramKey => $paramValue) {
530 $replaces[':' . $paramKey] = $paramValue;
531 }
532
533 $path = str_replace(array_keys($replaces), array_values($replaces), $path);
534
535 return self::baseUrl($path);
536
537 }
538
539 /**
540 * Get the route name from a request path.
541 *
542 * @param string $path The request path.
543 * @return string|false The route name or false if not found.
544 */
545 public static function getRouteNameByRequestPath($path)
546 {
547 $path = '//' . $path;
548
549 if (strpos($path, '/u/')) {
550 return 'user_profile';
551 }
552
553 if (strpos($path, '/post/')) {
554 return 'feed_view';
555 }
556
557 if (strpos($path, '/lessons/')) {
558 return 'lesson_view';
559 }
560
561 if (strpos($path, '/course/')) {
562 return 'course_view';
563 }
564
565 if (strpos($path, '/space/') && !strpos($path, '/discover/spaces')) {
566 return 'community_view';
567 }
568
569 if (strpos($path, '/admin')) {
570 return 'admin';
571 }
572
573 return false;
574 }
575
576 /**
577 * Get a human-readable excerpt from content.
578 *
579 * @param string $content The content to extract from.
580 * @param int $length The maximum length of the excerpt.
581 * @return string The human-readable excerpt.
582 */
583 public static function getHumanExcerpt($content, $length = 100)
584 {
585 if ($content) {
586 $patterns = [
587 '/^#{1,6}\s+/m' => '',
588 // Bold and Italic: remove '*' and '_' symbols
589 '/(\*\*|__)(.*?)\1/' => '$2',
590 '/(\*|_)(.*?)\1/' => '$2',
591 // Code blocks: remove triple backticks
592 '/^```\s*\w*\s*\n([\s\S]*?)\n```\s*$/m' => '$1',
593 // Inline code: remove single backticks
594 '/`([^`]+)`/' => '$1',
595 // Blockquotes: remove '>' symbol
596 '/^\s*>\s?/m' => '',
597 // Horizontal rules: replace with empty line
598 '/^\s*([-*_])\1{2,}\s*$/m' => "\n",
599 // Links: keep only the link text
600 '/\[([^\]]+)\]\([^\)]+\)/' => '$1',
601 // Images: keep only the alt text
602 '/!\[([^\]]+)\]\([^\)]+\)/' => '$1',
603 // Strikethrough: remove '~~' symbols
604 '/~~(.*?)~~/' => '$1',
605 // Task lists: remove checkbox syntax
606 '/^\s*[-*+]\s+\[[ xX]\]\s+/m' => '',
607 ];
608
609 $content = preg_replace(array_keys($patterns), array_values($patterns), $content);
610
611 // remove all tags
612 $content = wp_strip_all_tags($content);
613 // remove new lines and tabs
614 $content = str_replace(["\r", "\n", "\t"], ' ', $content);
615 // remove multiple spaces
616 $content = preg_replace('/\s+/', ' ', $content);
617
618 // trim
619 $content = trim($content);
620 }
621
622 if (!$content) {
623 return '';
624 }
625
626 if (mb_strlen($content) <= $length) {
627 return $content;
628 }
629
630 // return the first $length chars of the content with ... at the end
631 return mb_substr($content, 0, $length) . '...';
632 }
633
634 /**
635 * Check if the portal is publicly accessible.
636 *
637 * @return bool True if publicly accessible, false otherwise.
638 */
639 public static function isPublicAccessible()
640 {
641 $settings = self::generalSettings();
642 return Arr::get($settings, 'access.acess_level') == 'public';
643 }
644
645 /**
646 * Get media by provider.
647 *
648 * @param array $images The array of images.
649 * @param string $provider The provider to filter by.
650 * @return array The filtered array of images.
651 */
652 public static function getMediaByProvider($images, $provider = 'uploader')
653 {
654 if (is_array($images)) {
655 return array_filter($images, function ($image) use ($provider) {
656 if (is_array($image)) {
657 if (isset($image['provider'])) {
658 return Arr::get($image, 'provider') == $provider;
659 }
660
661 return $provider === 'uploader'; // for existing images when no provider was set.
662 }
663 });
664 }
665
666 return [];
667 }
668
669 /**
670 * Get the community menu groups.
671 *
672 * @param User|null $user The user to get menu groups for.
673 * @return array The community menu groups.
674 */
675 public static function getCommunityMenuGroups($user = null)
676 {
677 if (!$user) {
678 $user = self::getCurrentUser();
679 }
680
681 $communityGroups = self::getAllCommunityGroups($user);
682
683 if ($communityGroups->isEmpty()) {
684 return [];
685 }
686
687 $isMod = $user && $user->isCommunityModerator();
688
689 $formattedGroups = [];
690
691 foreach ($communityGroups as $communityGroup) {
692 $spaces = $communityGroup->spaces;
693 $validSpaces = [];
694 $isShowAll = Arr::get($communityGroup->settings, 'always_show_spaces') === 'yes';
695
696 foreach ($spaces as $space) {
697 if ($isMod) {
698 $validSpaces[] = self::transformSpaceToLink($space);
699 continue;
700 }
701
702 if ($space->privacy === 'secret') {
703 if (!$user || !$space->getMembership($user->ID)) {
704 continue;
705 }
706 $validSpaces[] = self::transformSpaceToLink($space);
707 continue;
708 }
709
710 if ($isShowAll || $space->privacy = 'public') {
711 $validSpace = self::transformSpaceToLink($space);
712
713 if ($space->privacy == 'private') {
714 if (!$user || !$space->getMembership($user->ID)) {
715 $validSpace['show_lock'] = true;
716 }
717 }
718
719 $validSpaces[] = $validSpace;
720 continue;
721 }
722
723 if (!$user || $space->getMembership($user->ID)) {
724 continue;
725 }
726
727 $validSpaces[] = self::transformSpaceToLink($space);
728 }
729
730 if (!$validSpaces && !$isMod) {
731 continue;
732 }
733
734 $formattedGroups[] = [
735 'id' => $communityGroup->id,
736 'title' => $communityGroup->title,
737 'slug' => $communityGroup->slug,
738 'logo' => $communityGroup->logo,
739 'children' => $validSpaces
740 ];
741 }
742
743 return apply_filters('fluent_community/menu_groups_for_user', $formattedGroups, $user);
744 }
745
746 /**
747 * Transform a space to a link array.
748 *
749 * @param Space $space The space to transform.
750 * @return array The transformed space link array.
751 */
752 private static function transformSpaceToLink($space)
753 {
754
755 $logo = $space->logo;
756
757 $title = $space->title;
758
759 if ($space->status == 'draft') {
760 $title = $title . ' ' . __('(Draft)', 'fluent-community');
761 }
762
763 return [
764 'title' => $title,
765 'icon_image' => $logo,
766 'shape_svg' => !$logo ? Arr::get($space->settings, 'shape_svg', '') : '',
767 'emoji' => !$logo ? Arr::get($space->settings, 'emoji', '') : '',
768 'permalink' => $space->getPermalink(),
769 'link_classes' => 'space_menu_item route_url fcom_space_id_' . $space->id . ' fcom_space_' . $space->slug
770 ];
771 }
772
773 public static function isAlreadyOnboarded()
774 {
775 $communitySettings = get_option('fluent_community_settings', []);
776
777 return !empty($communitySettings);
778 }
779
780 /**
781 * Get all community groups.
782 *
783 * @param User $user The user to get groups for.
784 * @param bool $willCreate Whether to create a default group if none exist.
785 * @return \FluentCommunity\Framework\Support\Collection Collection of Groups
786 */
787 public static function getAllCommunityGroups($user, $willCreate = true)
788 {
789 $isModerator = $user && $user->isCommunityModerator();
790
791 $communityGroups = SpaceGroup::query()->orderBy('serial', 'ASC')
792 ->with([
793 'spaces' => function ($query) use ($isModerator) {
794 if ($isModerator) {
795 $query->orderBy('serial', 'ASC');
796 } else {
797 $query->where('status', 'published')
798 ->orderBy('serial', 'ASC');
799 }
800 }
801 ])
802 ->get();
803
804 if ($communityGroups->isEmpty() && $willCreate) {
805 $createdSpace = (new ActivationHandler(App::make()))->maybeCreateDefaultSpaceGroup();
806
807 if ($createdSpace) {
808 Space::where('type', 'community')->update([
809 'parent_id' => $createdSpace->id
810 ]);
811 }
812
813 return self::getAllCommunityGroups($user, false);
814 }
815
816 return $communityGroups;
817 }
818
819 /**
820 * Check if a feature is enabled.
821 *
822 * @param string $feature The feature to check.
823 * @return bool True if the feature is enabled, false otherwise.
824 */
825 public static function isFeatureEnabled($feature)
826 {
827 $features = Utility::getFeaturesConfig();
828
829 return isset($features[$feature]) && $features[$feature] === 'yes';
830 }
831
832 /**
833 * Get the menu items group.
834 *
835 * @param string $context The context for getting menu items.
836 * @return array The menu items group.
837 */
838 public static function getMenuItemsGroup($context = 'view')
839 {
840 static $menuGroups;
841
842 if ($menuGroups && $context === 'view') {
843 return $menuGroups;
844 }
845
846 $menuGroups = Utility::getOption('fluent_community_menu_groups', []);
847
848 $membersPageStatus = Utility::canViewMembersPage() ? 'yes' : 'no';
849
850 $defaultMainMenuItems = [
851 'all_feeds' => [
852 'slug' => 'all_feeds',
853 'title' => __('Feed', 'fluent-community'),
854 'is_system' => 'yes',
855 'is_locked' => 'yes',
856 'enabled' => 'yes',
857 'permalink' => self::baseUrl('/'),
858 'link_classes' => 'fcom_dashboard route_url',
859 'shape_svg' => '<svg width="20" height="18" viewBox="0 0 20 18" fill="none"><path fill-rule="evenodd" clip-rule="evenodd" d="M10 13.166H10.0075H10Z" fill="currentColor"/><path d="M10 13.166H10.0075" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M16.6666 6.08301V10.2497C16.6666 13.3924 16.6666 14.9637 15.6903 15.94C14.714 16.9163 13.1426 16.9163 9.99992 16.9163C6.85722 16.9163 5.28587 16.9163 4.30956 15.94C3.33325 14.9637 3.33325 13.3924 3.33325 10.2497V6.08301" stroke="currentColor" stroke-width="1.5"/><path d="M18.3333 7.74967L14.714 4.27925C12.4918 2.14842 11.3807 1.08301 9.99996 1.08301C8.61925 1.08301 7.50814 2.14842 5.28592 4.27924L1.66663 7.74967" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>'
860 ],
861 'spaces' => [
862 'slug' => 'spaces',
863 'title' => __('Spaces', 'fluent-community'),
864 'is_system' => 'yes',
865 'is_locked' => 'yes',
866 'enabled' => 'yes',
867 'permalink' => self::baseUrl('discover/spaces'),
868 'link_classes' => 'fcom_spaces route_url',
869 'shape_svg' => '<svg version="1.1" viewBox="0 0 128 128" xml:space="preserve"><g><path d="M64,42c-13.2,0-24,10.8-24,24s10.8,24,24,24s24-10.8,24-24S77.2,42,64,42z M64,82c-8.8,0-16-7.2-16-16s7.2-16,16-16 s16,7.2,16,16S72.8,82,64,82z"/><path d="M64,100.8c-14.9,0-29.2,6.2-39.4,17.1l-2.7,2.9l5.8,5.5l2.7-2.9c8.8-9.4,20.7-14.6,33.6-14.6s24.8,5.2,33.6,14.6l2.7,2.9 l5.8-5.5l-2.7-2.9C93.2,107.1,78.9,100.8,64,100.8z"/><path d="M97,47.9v8c9.4,0,18.1,3.8,24.6,10.7l5.8-5.5C119.6,52.7,108.5,47.9,97,47.9z"/><path d="M116.1,20c0-10.5-8.6-19.1-19.1-19.1S77.9,9.5,77.9,20S86.5,39.1,97,39.1S116.1,30.5,116.1,20z M85.9,20 c0-6.1,5-11.1,11.1-11.1s11.1,5,11.1,11.1s-5,11.1-11.1,11.1S85.9,26.1,85.9,20z"/><path d="M31,47.9c-11.5,0-22.6,4.8-30.4,13.2l5.8,5.5c6.4-6.9,15.2-10.7,24.6-10.7V47.9z"/><path d="M50.1,20C50.1,9.5,41.5,0.9,31,0.9S11.9,9.5,11.9,20S20.5,39.1,31,39.1S50.1,30.5,50.1,20z M31,31.1 c-6.1,0-11.1-5-11.1-11.1S24.9,8.9,31,8.9s11.1,5,11.1,11.1S37.1,31.1,31,31.1z"/></g></svg>'
870 ],
871 'all_courses' => [
872 'slug' => 'all_courses',
873 'title' => 'Courses',
874 'link_classes' => 'fcom_courses route_url',
875 'is_system' => 'yes',
876 'is_locked' => 'yes',
877 'enabled' => 'yes',
878 'is_unavailable' => self::isFeatureEnabled('course_module') ? 'no' : 'yes',
879 'permalink' => self::baseUrl('courses'),
880 'shape_svg' => '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10.734 5.84746L14.7114 6.9072M9.88139 9.01146L11.8701 9.54132M9.98031 14.9723L10.7758 15.1843C13.0258 15.7838 14.1508 16.0835 15.037 15.5747C15.9233 15.0659 16.2247 13.9473 16.8276 11.71L17.6802 8.54599C18.2831 6.3087 18.5845 5.19006 18.0728 4.30879C17.5611 3.42752 16.4362 3.12778 14.1862 2.52831L13.3907 2.31636C11.1407 1.71688 10.0157 1.41714 9.12948 1.92594C8.24322 2.43474 7.94178 3.55338 7.3389 5.79067L6.4863 8.95466C5.88342 11.1919 5.58198 12.3106 6.09367 13.1919C6.60536 14.0731 7.73034 14.3729 9.98031 14.9723Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M9.99996 17.4559L9.20634 17.672C6.96165 18.2832 5.83931 18.5889 4.95512 18.0701C4.07093 17.5513 3.7702 16.4107 3.16874 14.1295L2.31814 10.9035C1.71668 8.62232 1.41595 7.48174 1.92643 6.58318C2.36802 5.80591 3.33329 5.83421 4.58329 5.83411" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>'
881 ],
882 'all_members' => [
883 'slug' => 'all_members',
884 'title' => __('Members', 'fluent-community'),
885 'is_system' => 'yes',
886 'is_locked' => 'yes',
887 'is_unavailable' => $membersPageStatus == 'yes' ? 'no' : 'yes',
888 'enabled' => $membersPageStatus,
889 'permalink' => self::baseUrl('members'),
890 'link_classes' => 'fcom_all_members route_url',
891 'shape_svg' => '<svg width="20" height="16" viewBox="0 0 20 16" fill="none"><path d="M17.3116 13C17.936 13 18.4327 12.6071 18.8786 12.0576C19.7915 10.9329 18.2927 10.034 17.721 9.59383C17.1399 9.14635 16.4911 8.89285 15.8332 8.83333M14.9999 7.16667C16.1505 7.16667 17.0832 6.23393 17.0832 5.08333C17.0832 3.93274 16.1505 3 14.9999 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M2.68822 13C2.0638 13 1.56714 12.6071 1.12121 12.0576C0.208326 10.9329 1.70714 10.034 2.27879 9.59383C2.8599 9.14635 3.50874 8.89285 4.16659 8.83333M4.58325 7.16667C3.43266 7.16667 2.49992 6.23393 2.49992 5.08333C2.49992 3.93274 3.43266 3 4.58325 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M6.73642 10.592C5.88494 11.1185 3.65241 12.1936 5.01217 13.5389C5.6764 14.196 6.41619 14.666 7.34627 14.666H12.6536C13.5837 14.666 14.3234 14.196 14.9877 13.5389C16.3474 12.1936 14.1149 11.1185 13.2634 10.592C11.2667 9.35735 8.73313 9.35735 6.73642 10.592Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M12.9166 4.24967C12.9166 5.86051 11.6107 7.16634 9.99992 7.16634C8.38909 7.16634 7.08325 5.86051 7.08325 4.24967C7.08325 2.63884 8.38909 1.33301 9.99992 1.33301C11.6107 1.33301 12.9166 2.63884 12.9166 4.24967Z" stroke="currentColor" stroke-width="1.5"/></svg>',
892 ],
893 'leaderboard' => [
894 'slug' => 'leaderboard',
895 'is_system' => 'yes',
896 'is_locked' => 'yes',
897 'enabled' => 'yes',
898 'is_unavailable' => self::isFeatureEnabled('leader_board_module') ? 'no' : 'yes',
899 'title' => __('Leaderboard', 'fluent-community'),
900 'link_classes' => 'fcom_leaderboards route_url',
901 'permalink' => self::baseUrl('leaderboards'),
902 'shape_svg' => '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="currentColor"><path d="M160-200h160v-320H160v320Zm240 0h160v-560H400v560Zm240 0h160v-240H640v240ZM80-120v-480h240v-240h320v320h240v400H80Z"/></svg>'
903 ]
904 ];
905
906 $mainItems = Arr::get($menuGroups, 'mainMenuItems', []);
907
908 if ($mainItems && is_array($mainItems)) {
909 if (isset($mainItems['all_communities'])) {
910 $mainItems['spaces'] = $defaultMainMenuItems['spaces'];
911 unset($mainItems['all_communities']);
912 }
913
914 foreach ($mainItems as $index => &$item) {
915 if (empty($item['slug'])) {
916 unset($mainItems[$index]);
917 continue;
918 }
919 $defaultItem = Arr::get($defaultMainMenuItems, $item['slug'], []);
920 if ($defaultItem) {
921 $preservedKeys = ['is_system', 'is_locked', 'is_unavailable', 'slug', 'shape_svg'];
922 foreach ($preservedKeys as $key) {
923 if (isset($defaultItem[$key])) {
924 $item[$key] = Arr::get($defaultItem, $key);
925 }
926 }
927 if (Arr::get($defaultItem, 'is_system') === 'yes') {
928 $item['permalink'] = $defaultItem['permalink'];
929 $item['emoji'] = '';
930 $item['icon_image'] = '';
931 $item['link_classes'] = $defaultItem['link_classes'];
932 }
933 }
934 }
935 } else {
936 $mainItems = $defaultMainMenuItems;
937 }
938
939 $defaultProfileDropDownItems = [
940 'my_spaces' => [
941 'slug' => 'my_spaces',
942 'title' => __('My Spaces', 'fluent-community'),
943 'is_system' => 'yes',
944 'is_locked' => 'yes',
945 'enabled' => 'yes',
946 'permalink' => '#{{user_url}}/spaces',
947 'shape_svg' => '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M0.5 16.5C0.5 14.9087 1.13214 13.3826 2.25736 12.2574C3.38258 11.1321 4.9087 10.5 6.5 10.5C8.0913 10.5 9.61742 11.1321 10.7426 12.2574C11.8679 13.3826 12.5 14.9087 12.5 16.5H11C11 15.3065 10.5259 14.1619 9.68198 13.318C8.83807 12.4741 7.69347 12 6.5 12C5.30653 12 4.16193 12.4741 3.31802 13.318C2.47411 14.1619 2 15.3065 2 16.5H0.5ZM6.5 9.75C4.01375 9.75 2 7.73625 2 5.25C2 2.76375 4.01375 0.75 6.5 0.75C8.98625 0.75 11 2.76375 11 5.25C11 7.73625 8.98625 9.75 6.5 9.75ZM6.5 8.25C8.1575 8.25 9.5 6.9075 9.5 5.25C9.5 3.5925 8.1575 2.25 6.5 2.25C4.8425 2.25 3.5 3.5925 3.5 5.25C3.5 6.9075 4.8425 8.25 6.5 8.25ZM12.713 11.0273C13.767 11.5019 14.6615 12.2709 15.2889 13.2418C15.9164 14.2126 16.2501 15.344 16.25 16.5H14.75C14.7502 15.633 14.4999 14.7844 14.0293 14.0562C13.5587 13.328 12.8878 12.7512 12.0972 12.3953L12.7123 11.0273H12.713ZM12.197 2.55975C12.9526 2.87122 13.5987 3.40015 14.0533 4.07942C14.5078 4.75869 14.7503 5.55768 14.75 6.375C14.7503 7.40425 14.3658 8.39642 13.6719 9.15662C12.978 9.91682 12.025 10.3901 11 10.4835V8.97375C11.5557 8.89416 12.0713 8.63851 12.471 8.24434C12.8707 7.85017 13.1335 7.33824 13.2209 6.7837C13.3082 6.22916 13.2155 5.66122 12.9563 5.16327C12.6971 4.66531 12.2851 4.26356 11.7808 4.017L12.197 2.55975Z" fill="currentColor"/></svg>'
948 ],
949 'bookmarks' => [
950 'slug' => 'bookmarks',
951 'title' => __('Bookmarks', 'fluent-community'),
952 'is_system' => 'yes',
953 'is_locked' => 'yes',
954 'enabled' => 'yes',
955 'permalink' => self::baseUrl('bookmarks'),
956 'shape_svg' => '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M0.75 0.5H11.25C11.4489 0.5 11.6397 0.579018 11.7803 0.71967C11.921 0.860322 12 1.05109 12 1.25V15.6073C12.0001 15.6743 11.9822 15.7402 11.9482 15.7979C11.9142 15.8557 11.8653 15.9033 11.8066 15.9358C11.7479 15.9683 11.6816 15.9844 11.6146 15.9826C11.5476 15.9807 11.4823 15.9609 11.4255 15.9252L6 12.5225L0.5745 15.9245C0.517776 15.9601 0.452541 15.9799 0.385576 15.9818C0.318612 15.9837 0.252365 15.9676 0.193721 15.9352C0.135078 15.9029 0.0861801 15.8554 0.0521121 15.7977C0.0180441 15.74 4.98531e-05 15.6742 0 15.6073V1.25C0 1.05109 0.0790178 0.860322 0.21967 0.71967C0.360322 0.579018 0.551088 0.5 0.75 0.5ZM10.5 2H1.5V13.574L6 10.7533L10.5 13.574V2Z" fill="currentColor"/></svg>'
957 ],
958 'logout' => [
959 'slug' => 'logout',
960 'title' => __('Logout', 'fluent-community'),
961 'is_system' => 'yes',
962 'is_locked' => 'yes',
963 'enabled' => 'yes',
964 'permalink' => '#{{logout_url}}',
965 'shape_svg' => '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M0.75 15.5C0.551088 15.5 0.360322 15.421 0.21967 15.2803C0.0790178 15.1397 0 14.9489 0 14.75V1.25C0 1.05109 0.0790178 0.860322 0.21967 0.71967C0.360322 0.579018 0.551088 0.5 0.75 0.5H11.25C11.4489 0.5 11.6397 0.579018 11.7803 0.71967C11.921 0.860322 12 1.05109 12 1.25V3.5H10.5V2H1.5V14H10.5V12.5H12V14.75C12 14.9489 11.921 15.1397 11.7803 15.2803C11.6397 15.421 11.4489 15.5 11.25 15.5H0.75ZM10.5 11V8.75H5.25V7.25H10.5V5L14.25 8L10.5 11Z" fill="currentColor"/></svg>'
966 ]
967 ];
968
969 $profileDropDownItems = Arr::get($menuGroups, 'profileDropdownItems', []);
970
971 if ($profileDropDownItems && is_array($profileDropDownItems)) {
972
973 unset($profileDropDownItems['profile']);
974
975 foreach ($profileDropDownItems as $index => &$item) {
976 if (empty($item['slug'])) {
977 unset($profileDropDownItems[$index]);
978 continue;
979 }
980 $defaultItem = Arr::get($defaultProfileDropDownItems, $item['slug'], []);
981 if ($defaultItem) {
982 $preservedKeys = ['is_system', 'is_locked', 'is_unavailable', 'slug', 'svg_icon'];
983 foreach ($preservedKeys as $key) {
984 if (isset($defaultItem[$key])) {
985 $item[$key] = Arr::get($defaultItem, $key);
986 }
987 }
988 if (Arr::get($defaultItem, 'is_system') === 'yes') {
989 $item['permalink'] = $defaultItem['permalink'];
990 if (empty($item['shape_svg'])) {
991 $item['shape_svg'] = $defaultItem['shape_svg'];
992 }
993 }
994 }
995 }
996 } else {
997 $profileDropDownItems = $defaultProfileDropDownItems;
998 }
999
1000 $beforeCommunityMenuItems = Arr::get($menuGroups, 'beforeCommunityMenuItems', []);
1001 $afterCommunityMenuGroups = Arr::get($menuGroups, 'afterCommunityLinkGroups', []);
1002
1003 if (!is_array($beforeCommunityMenuItems)) {
1004 $beforeCommunityMenuItems = [];
1005 }
1006
1007 if (!is_array($afterCommunityMenuGroups)) {
1008 $afterCommunityMenuGroups = [];
1009 }
1010
1011 if ($context == 'view') {
1012 $mainItems = array_filter($mainItems, function ($item) {
1013 return Arr::get($item, 'enabled') === 'yes' && Arr::get($item, 'is_unavailable') !== 'yes';
1014 });
1015
1016 $profileDropDownItems = array_filter($profileDropDownItems, function ($item) {
1017 return Arr::get($item, 'enabled') === 'yes' && Arr::get($item, 'is_unavailable') !== 'yes';
1018 });
1019
1020 $beforeCommunityMenuItems = array_filter($beforeCommunityMenuItems, function ($item) {
1021 return Arr::get($item, 'enabled') === 'yes' && Arr::get($item, 'is_unavailable') !== 'yes';
1022 });
1023
1024 $validGroups = [];
1025 foreach ($afterCommunityMenuGroups as $group) {
1026 if (empty($group['items']) || !is_array($group['items'])) {
1027 continue;
1028 }
1029
1030 $group['items'] = array_filter($group['items'], function ($item) {
1031 return Arr::get($item, 'enabled') === 'yes' && Arr::get($item, 'is_unavailable') !== 'yes';
1032 });
1033
1034 if ($group['items']) {
1035 $validGroups[] = $group;
1036 }
1037 }
1038
1039 $afterCommunityMenuGroups = $validGroups;
1040 }
1041
1042 $menuGroups['mainMenuItems'] = $mainItems;
1043 $menuGroups['profileDropdownItems'] = $profileDropDownItems;
1044 $menuGroups['beforeCommunityMenuItems'] = $beforeCommunityMenuItems;
1045 $menuGroups['afterCommunityLinkGroups'] = $afterCommunityMenuGroups;
1046
1047 if ($context == 'view') {
1048 $menuGroups = apply_filters('fluent_community/menu_groups', $menuGroups);
1049 }
1050
1051 return $menuGroups;
1052 }
1053
1054 /**
1055 * Get the meta data for a space.
1056 *
1057 * @param int $spaceId The ID of the space.
1058 * @param string $key The meta key.
1059 * @param mixed $default The default value if the meta key is not found.
1060 * @return mixed The meta value or the default value if not found.
1061 */
1062 public static function getSpaceMeta($spaceId, $key, $default = null)
1063 {
1064 $meta = Meta::where('object_type', 'space')
1065 ->where('meta_key', $key)
1066 ->where('object_id', $spaceId)
1067 ->first();
1068
1069 if (!$meta) {
1070 return $default;
1071 }
1072
1073 return $meta->value;
1074 }
1075
1076 /**
1077 * Update the meta data for a space.
1078 *
1079 * @param int $spaceId The ID of the space.
1080 * @param string $key The meta key.
1081 * @param mixed $value The meta value.
1082 * @return Meta The updated meta object.
1083 */
1084 public static function updateSpaceMeta($spaceId, $key, $value)
1085 {
1086 $meta = Meta::where('object_type', 'space')
1087 ->where('meta_key', $key)
1088 ->where('object_id', $spaceId)
1089 ->first();
1090
1091 if ($meta) {
1092 $meta->value = $value;
1093 $meta->save();
1094 } else {
1095 $meta = Meta::create([
1096 'object_type' => 'space',
1097 'object_id' => $spaceId,
1098 'meta_key' => $key,
1099 'value' => $value
1100 ]);
1101 }
1102
1103 return $meta;
1104 }
1105
1106
1107 /**
1108 * Encrypt or decrypt a value.
1109 *
1110 * @param string $value The value to encrypt or decrypt.
1111 * @param string $type The type of operation ('e' for encrypt, 'd' for decrypt).
1112 * @return string|false The encrypted or decrypted value or false if an error occurs.
1113 */
1114 public static function encryptDecrypt($value, $type = 'e')
1115 {
1116 if (!$value) {
1117 return $value;
1118 }
1119
1120 if (!extension_loaded('openssl')) {
1121 return $value;
1122 }
1123
1124 if (defined('FLUENT_COM_ENCRYPT_SALT')) {
1125 $salt = FLUENT_COM_ENCRYPT_SALT;
1126 } else {
1127 $salt = (defined('LOGGED_IN_SALT') && '' !== LOGGED_IN_SALT) ? LOGGED_IN_SALT : 'this-is-a-fallback-salt-but-not-secure';
1128 }
1129
1130 if (defined('FLUENT_COM__ENCRYPT_KEY')) {
1131 $key = FLUENT_COM__ENCRYPT_KEY;
1132 } else {
1133 $key = (defined('LOGGED_IN_KEY') && '' !== LOGGED_IN_KEY) ? LOGGED_IN_KEY : 'this-is-a-fallback-key-but-not-secure';
1134 }
1135
1136 if ($type == 'e') {
1137 $method = 'aes-256-ctr';
1138 $ivlen = openssl_cipher_iv_length($method);
1139 $iv = openssl_random_pseudo_bytes($ivlen);
1140
1141 $raw_value = openssl_encrypt($value . $salt, $method, $key, 0, $iv);
1142 if (!$raw_value) {
1143 return false;
1144 }
1145
1146 return base64_encode($iv . $raw_value); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
1147 }
1148
1149 $raw_value = base64_decode($value, true); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
1150
1151 $method = 'aes-256-ctr';
1152 $ivlen = openssl_cipher_iv_length($method);
1153 $iv = substr($raw_value, 0, $ivlen);
1154
1155 $raw_value = substr($raw_value, $ivlen);
1156
1157 $newValue = openssl_decrypt($raw_value, $method, $key, 0, $iv);
1158 if (!$newValue || substr($newValue, -strlen($salt)) !== $salt) {
1159 return false;
1160 }
1161
1162 return substr($newValue, 0, -strlen($salt));
1163 }
1164
1165
1166 /**
1167 * Get the welcome banner configuration.
1168 *
1169 * @return array The welcome banner configuration.
1170 */
1171 public static function getWelcomeBannerSettings()
1172 {
1173 return Utility::getFromCache('welcome_banner_settings', function () {
1174 $defaults = [
1175 'login' => [
1176 'enabled' => 'no',
1177 'description' => '',
1178 'mediaType' => 'image',
1179 'allowClose' => 'no',
1180 'bannerImage' => '',
1181 'bannerVideo' => [
1182 'type' => 'oembed',
1183 'url' => '',
1184 'content_type' => '',
1185 'provider' => '',
1186 'title' => '',
1187 'author_name' => '',
1188 'html' => ''
1189 ],
1190 'ctaButtons' => []
1191 ],
1192 'logout' => [
1193 'enabled' => 'no',
1194 'description' => '',
1195 'mediaType' => 'image',
1196 'useCustomUrl' => 'no',
1197 'bannerImage' => '',
1198 'bannerVideo' => [
1199 'type' => 'oembed',
1200 'url' => '',
1201 'content_type' => '',
1202 'provider' => '',
1203 'title' => '',
1204 'author_name' => '',
1205 'html' => ''
1206 ],
1207 'ctaButtons' => []
1208 ]
1209 ];
1210
1211 $settings = Utility::getOption('welcome_banner_settings', []);
1212
1213 $settings = wp_parse_args($settings, $defaults);
1214
1215 if (empty(Arr::get($settings, 'login.bannerVideo'))) {
1216 $settings['login']['bannerVideo'] = $defaults['login']['bannerVideo'];
1217 }
1218
1219 if (empty(Arr::get($settings, 'logout.bannerVideo'))) {
1220 $settings['logout']['bannerVideo'] = $defaults['logout']['bannerVideo'];
1221 }
1222
1223 return $settings;
1224 }, WEEK_IN_SECONDS);
1225 }
1226
1227 public static function getWelcomeBanner($view = 'login')
1228 {
1229 $settings = self::getWelcomeBannerSettings();
1230 $welcomeBanner = Arr::get($settings, $view, []);
1231 if (Arr::get($welcomeBanner, 'enabled') != 'yes') {
1232 return null;
1233 }
1234
1235 unset($welcomeBanner['description']);
1236
1237 if ($view == 'login') {
1238 return apply_filters('fluent_community/welcome_banner_for_logged_in', $welcomeBanner);
1239 }
1240
1241 return apply_filters('fluent_community/welcome_banner_for_guests', $welcomeBanner);
1242 }
1243
1244 public static function getEnabledFeedLinks()
1245 {
1246 $links = array_filter(self::getFeedLinks(), function ($item) {
1247 return Arr::get($item, 'enabled') == 'yes' && Arr::get($item, 'is_unavailable') != 'yes';
1248 });
1249
1250 return array_values($links);
1251 }
1252
1253 public static function getFeedLinks()
1254 {
1255 return Utility::getFromCache('feed_links', function () {
1256 return Utility::getOption('feed_links', []);
1257 }, WEEK_IN_SECONDS);
1258 }
1259
1260 public static function updateFeedLinks($links)
1261 {
1262 Utility::updateOption('feed_links', $links);
1263 Utility::setCache('feed_links', $links, WEEK_IN_SECONDS);
1264 }
1265
1266 /**
1267 * Get the full name of a WordPress user.
1268 *
1269 * @param int|null $id The ID of the user.
1270 * @return string The full name of the user.
1271 */
1272 public static function getWpUserFullName($id = null)
1273 {
1274 $id = $id ?: get_current_user_id();
1275 $user = get_user_by('ID', $id);
1276
1277 $fullName = $user->display_name;
1278 if ($user->first_name && $user->last_name) {
1279 $fullName = $user->first_name . ' ' . $user->last_name;
1280 }
1281
1282 return $fullName;
1283 }
1284
1285 /**
1286 * Get the onboarding settings.
1287 *
1288 * @return array The onboarding settings.
1289 */
1290 public static function getOnboardingSettings()
1291 {
1292 $default = [
1293 'is_onboarding_enabled' => 'no',
1294 'registration_page_url' => '',
1295 ];
1296
1297 $settings = Utility::getOption('onboarding_settings', $default);
1298
1299 return wp_parse_args($settings, $default);
1300 }
1301
1302 /**
1303 * Get all WordPress published pages.
1304 *
1305 * @return array An array of page data.
1306 */
1307 public static function getAllWpPublishedPage()
1308 {
1309 $posts = get_posts(array(
1310 'post_status' => 'publish',
1311 'numberposts' => -1,
1312 'post_type' => 'any',
1313 ));
1314
1315 return array_map(function ($post) {
1316 return array(
1317 'id' => $post->ID,
1318 'permalink' => get_permalink($post),
1319 'title' => get_the_title($post),
1320 );
1321 }, $posts);
1322 }
1323
1324 /**
1325 * Add a user to a space.
1326 *
1327 * @param Space | int $space space to add the user to.
1328 * @param int $userId The ID of the user to add.
1329 * @param string $role The role of the user in the space.
1330 * @param string $by The source of the action.
1331 * @return bool True if the user was added, false otherwise.
1332 */
1333 public static function addToSpace($space, $userId, $role = 'member', $by = 'self')
1334 {
1335 if (is_numeric($space)) {
1336 $space = BaseSpace::withoutGlobalScopes()->find($space);
1337 }
1338
1339 if (!$space) {
1340 return false;
1341 }
1342
1343 $user = User::find($userId);
1344
1345 if (!$user) {
1346 return false;
1347 }
1348
1349 $user->syncXProfile();
1350
1351 $exist = SpaceUserPivot::where('user_id', $userId)
1352 ->where('space_id', $space->id)
1353 ->first();
1354
1355 if ($exist) {
1356 if ($exist->status != 'active') {
1357 $exist->status = 'active';
1358 $exist->role = $role;
1359 $exist->save();
1360
1361 if ($space->type == 'course') {
1362 do_action('fluent_community/course/enrolled', $space, $userId, $by);
1363 } else {
1364 do_action('fluent_community/space/joined', $space, $userId, $by);
1365 }
1366
1367 return true;
1368 }
1369
1370 return false;
1371 }
1372
1373 SpaceUserPivot::create([
1374 'space_id' => $space->id,
1375 'role' => $role,
1376 'user_id' => $userId
1377 ]);
1378
1379 if ($space->type == 'course') {
1380 do_action('fluent_community/course/enrolled', $space, $userId, $by);
1381 } else {
1382 do_action('fluent_community/space/joined', $space, $userId, $by);
1383 }
1384
1385 return true;
1386 }
1387
1388 /**
1389 * Remove a user from a space if exist.
1390 *
1391 * @param int $userId The ID of the user.
1392 * @param int $spaceId The ID of the space.
1393 * @param string $by The source of the action. self | by_admin
1394 * @return bool True if the user is in the space, false otherwise.
1395 */
1396 public static function removeFromSpace($space, $userId, $by = 'self')
1397 {
1398 $user = User::find($userId);
1399 if (!$user) {
1400 return false;
1401 }
1402
1403 if (is_numeric($space)) {
1404 $space = BaseSpace::query()->withoutGlobalScopes()->find($space);
1405 }
1406
1407 if (!$space) {
1408 return false;
1409 }
1410
1411 if (!self::isUserInSpace($userId, $space->id)) {
1412 return false;
1413 }
1414
1415 SpaceUserPivot::bySpace($space->id)
1416 ->byUser($userId)
1417 ->delete();
1418
1419 $user->cacheAccessSpaces();
1420
1421 if ($space->type == 'course') {
1422 do_action('fluent_community/course/student_left', $space, $userId, $by);
1423 } else {
1424 do_action('fluent_community/space/user_left', $space, $userId, $by);
1425 }
1426
1427 return true;
1428 }
1429
1430 /**
1431 * Render a link with icon.
1432 *
1433 * @param array $link The link data.
1434 * @param string $linkClass Additional classes for the link.
1435 * @param string $fallback The fallback content if no icon is found.
1436 * @param bool $renderIcon Whether to render the icon or not.
1437 */
1438 public static function renderLink($link, $linkClass = '', $fallback = '<span class="fcom_no_avatar"></span>', $renderIcon = true)
1439 {
1440 $linkAtts = array_filter([
1441 'class' => trim($linkClass . ' ' . Arr::get($link, 'link_classes')) . ' fcom_compt_link',
1442 'target' => Arr::get($link, 'new_tab') === 'yes' ? '_blank' : '',
1443 'rel' => Arr::get($link, 'new_tab') === 'yes' ? 'noopener noreferrer' : '',
1444 ]);
1445 ?>
1446 <a aria-label="Go to <?php echo esc_attr(Arr::get($link, 'title')); ?> page"
1447 href="<?php echo esc_url($link['permalink']); ?>" <?php foreach ($linkAtts as $key => $value) {
1448 echo esc_attr($key) . '="' . esc_attr($value) . '"';
1449 } ?>>
1450 <?php $renderIcon && self::printLinkIcon($link, $fallback); ?>
1451 <span class="community_name"><?php echo wp_kses_post(Arr::get($link, 'title')); ?></span>
1452 <?php if (Arr::get($link, 'show_lock')): ?>
1453 <span class="fcom_space_lock">
1454 <i class="el-icon">
1455 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
1456 <path fill="currentColor"
1457 d="M224 448a32 32 0 0 0-32 32v384a32 32 0 0 0 32 32h576a32 32 0 0 0 32-32V480a32 32 0 0 0-32-32zm0-64h576a96 96 0 0 1 96 96v384a96 96 0 0 1-96 96H224a96 96 0 0 1-96-96V480a96 96 0 0 1 96-96"></path>
1458 <path fill="currentColor"
1459 d="M512 544a32 32 0 0 1 32 32v192a32 32 0 1 1-64 0V576a32 32 0 0 1 32-32m192-160v-64a192 192 0 1 0-384 0v64zM512 64a256 256 0 0 1 256 256v128H256V320A256 256 0 0 1 512 64"></path>
1460 </svg>
1461 </i>
1462 </span>
1463 <?php endif; ?>
1464
1465 </a>
1466 <?php
1467 }
1468
1469 /**
1470 * Print a link icon.
1471 *
1472 * @param array $link The link data.
1473 * @param string $fallback The fallback content if no icon is found.
1474 */
1475 public static function printLinkIcon($link, $fallback = '<span class="fcom_no_avatar"></span>')
1476 {
1477 ?>
1478 <?php if ($img = Arr::get($link, 'icon_image')): ?>
1479 <div class="community_avatar">
1480 <img alt="" src="<?php echo esc_url($img); ?>"/>
1481 </div>
1482 <?php elseif ($emoji = Arr::get($link, 'emoji')): ?>
1483 <div class="community_avatar">
1484 <span class="fcom_emoji"><?php echo esc_html($emoji); ?></span>
1485 </div>
1486 <?php elseif ($svg = Arr::get($link, 'shape_svg')): ?>
1487 <div class="community_avatar">
1488 <span class="fcom_shape"><i
1489 class="el-icon"><?php echo \FluentCommunity\App\Services\CustomSanitizer::sanitizeSvg($svg); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></i></span>
1490 </div>
1491 <?php else:
1492 echo '<div class="community_avatar">' . wp_kses_post($fallback) . '</div>';
1493 endif;
1494 }
1495
1496 /**
1497 * Get the IP address of the user.
1498 *
1499 * @param bool $anonymize Whether to anonymize the IP address.
1500 * @return string The IP address.
1501 */
1502 public static function getIp($anonymize = false)
1503 {
1504 static $ipAddress;
1505
1506 if ($ipAddress) {
1507 return $ipAddress;
1508 }
1509
1510 if (empty($_SERVER['REMOTE_ADDR'])) {
1511 // It's a local cli request
1512 return '127.0.0.1';
1513 }
1514
1515 $ipAddress = '';
1516 if (isset($_SERVER["HTTP_CF_CONNECTING_IP"])) {
1517 $ipAddress = sanitize_text_field(wp_unslash($_SERVER["REMOTE_ADDR"]));
1518 //If it's a valid Cloudflare request
1519 if (self::isCfIp($ipAddress)) {
1520 //Use the CF-Connecting-IP header.
1521 $ipAddress = sanitize_text_field(wp_unslash($_SERVER['HTTP_CF_CONNECTING_IP']));
1522 }
1523 } else if ($_SERVER['REMOTE_ADDR'] == '127.0.0.1') {
1524 // most probably it's local reverse proxy
1525 if (isset($_SERVER["HTTP_CLIENT_IP"])) {
1526 $ipAddress = sanitize_text_field(wp_unslash($_SERVER["HTTP_CLIENT_IP"]));
1527 } else if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
1528 $ipAddress = (string)rest_is_ip_address(trim(current(preg_split('/,/', sanitize_text_field(wp_unslash($_SERVER['HTTP_X_FORWARDED_FOR']))))));
1529 }
1530 }
1531
1532 if (!$ipAddress) {
1533 $ipAddress = sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR']));
1534 }
1535
1536 $ipAddress = preg_replace('/^(\d+\.\d+\.\d+\.\d+):\d+$/', '\1', $ipAddress);
1537
1538 $ipAddress = apply_filters('fluent_auth/user_ip', $ipAddress);
1539
1540 if ($anonymize) {
1541 return wp_privacy_anonymize_ip($ipAddress);
1542 }
1543
1544 return $ipAddress;
1545 }
1546
1547 /**
1548 * Check if the IP address is from Cloudflare.
1549 *
1550 * @param string $ip The IP address to check.
1551 * @return bool True if the IP is from Cloudflare, false otherwise.
1552 */
1553 public static function isCfIp($ip = '')
1554 {
1555 if (!$ip && isset($_SERVER["REMOTE_ADDR"])) {
1556 $ip = sanitize_text_field(wp_unslash($_SERVER["REMOTE_ADDR"]));
1557 }
1558
1559 if (!$ip) {
1560 return false;
1561 }
1562
1563 $cloudflareIPRanges = array(
1564 '173.245.48.0/20',
1565 '103.21.244.0/22',
1566 '103.22.200.0/22',
1567 '103.31.4.0/22',
1568 '141.101.64.0/18',
1569 '108.162.192.0/18',
1570 '190.93.240.0/20',
1571 '188.114.96.0/20',
1572 '197.234.240.0/22',
1573 '198.41.128.0/17',
1574 '162.158.0.0/15',
1575 '104.16.0.0/13',
1576 '104.24.0.0/14',
1577 '172.64.0.0/13',
1578 '131.0.72.0/22',
1579 );
1580
1581 //Make sure that the request came via Cloudflare.
1582 foreach ($cloudflareIPRanges as $range) {
1583 //Use the ip_in_range function from Joomla.
1584 if (self::ipInRange($ip, $range)) {
1585 //IP is valid. Belongs to Cloudflare.
1586 return true;
1587 }
1588 }
1589
1590 return false;
1591 }
1592
1593 /**
1594 * Check if the IP address is in the given range.
1595 *
1596 * @param string $ip The IP address to check.
1597 * @param string $range The range to check against.
1598 * @return bool True if the IP is in the range, false otherwise.
1599 */
1600 private static function ipInRange($ip, $range)
1601 {
1602 if (strpos($range, '/') !== false) {
1603 // $range is in IP/NETMASK format
1604 list($range, $netmask) = explode('/', $range, 2);
1605 if (strpos($netmask, '.') !== false) {
1606 // $netmask is a 255.255.0.0 format
1607 $netmask = str_replace('*', '0', $netmask);
1608 $netmask_dec = ip2long($netmask);
1609 return ((ip2long($ip) & $netmask_dec) == (ip2long($range) & $netmask_dec));
1610 } else {
1611 // $netmask is a CIDR size block
1612 // fix the range argument
1613 $x = explode('.', $range);
1614 while (count($x) < 4) $x[] = '0';
1615 list($a, $b, $c, $d) = $x;
1616 $range = sprintf("%u.%u.%u.%u", empty($a) ? '0' : $a, empty($b) ? '0' : $b, empty($c) ? '0' : $c, empty($d) ? '0' : $d);
1617 $range_dec = ip2long($range);
1618 $ip_dec = ip2long($ip);
1619
1620 # Strategy 1 - Create the netmask with 'netmask' 1s and then fill it to 32 with 0s
1621 #$netmask_dec = bindec(str_pad('', $netmask, '1') . str_pad('', 32-$netmask, '0'));
1622
1623 # Strategy 2 - Use math to create it
1624 $wildcard_dec = pow(2, (32 - $netmask)) - 1;
1625 $netmask_dec = ~$wildcard_dec;
1626
1627 return (($ip_dec & $netmask_dec) == ($range_dec & $netmask_dec));
1628 }
1629 } else {
1630 // range might be 255.255.*.* or 1.2.3.0-1.2.3.255
1631 if (strpos($range, '*') !== false) { // a.b.*.* format
1632 // Just convert to A-B format by setting * to 0 for A and 255 for B
1633 $lower = str_replace('*', '0', $range);
1634 $upper = str_replace('*', '255', $range);
1635 $range = "$lower-$upper";
1636 }
1637
1638 if (strpos($range, '-') !== false) { // A-B format
1639 list($lower, $upper) = explode('-', $range, 2);
1640 $lower_dec = (float)sprintf("%u", ip2long($lower));
1641 $upper_dec = (float)sprintf("%u", ip2long($upper));
1642 $ip_dec = (float)sprintf("%u", ip2long($ip));
1643 return (($ip_dec >= $lower_dec) && ($ip_dec <= $upper_dec));
1644 }
1645 return false;
1646 }
1647 }
1648
1649 public static function getPortalRequestPath($requestUri)
1650 {
1651 $portalSlug = self::getPortalSlug();
1652
1653 if ($portalSlug == $requestUri) {
1654 return 'portal_home';
1655 }
1656
1657 if (!$requestUri) {
1658 return false;
1659 }
1660
1661 if ($portalSlug) {
1662 // remove the portal slug from the request uri. Don't use str_replace as it will replace all occurrences
1663 $requestUri = substr($requestUri, strlen($portalSlug));
1664 }
1665
1666 $parts = explode('/', $requestUri);
1667 $start = $parts[0];
1668
1669 $routeStats = self::portalRoutePaths();
1670
1671 if (in_array($start, $routeStats)) {
1672 return $requestUri;
1673 }
1674 return false;
1675 }
1676
1677 public static function getTopicsConfig()
1678 {
1679 return Utility::getFromCache('topics_config', function () {
1680 $config = Utility::getOption('topics_config', []);
1681 $default = [
1682 'max_topics_per_post' => 1,
1683 'max_topics_per_space' => 20,
1684 'show_on_post_card' => 'yes'
1685 ];
1686 return wp_parse_args($config, $default);
1687 }, WEEK_IN_SECONDS);
1688 }
1689 }
1690