PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 1.0.95
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v1.0.95
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.95, at app/Services/Helper.php

1,682 lines 60.5 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 if (!$userId) {
110 return current_user_can($capability);
111 }
112
113 return user_can($userId, $capability);
114 }
115
116 public static function isModerator($user = null)
117 {
118 if(!$user) {
119 $user = self::getCurrentUser();
120 }
121 return $user && $user->isCommunityModerator();
122 }
123
124 /**
125 * Get the URL for an asset file.
126 *
127 * @param string $file The file name.
128 * @return string The full URL to the asset.
129 */
130 public static function assetUrl($file = '')
131 {
132 return FLUENT_COMMUNITY_PLUGIN_URL . 'assets/' . $file;
133 }
134
135 /**
136 * Get the base URL for the portal.
137 *
138 * @param string $path The path to append to the base URL.
139 * @return string The full base URL.
140 */
141 public static function baseUrl($path = '')
142 {
143 $baseUrl = apply_filters('fluent_community/base_url', home_url(self::getPortalSlug()));
144 $baseUrl = rtrim($baseUrl, '/');
145
146 if (self::getPortalRouteType() != 'hash') {
147 return $baseUrl . '/' . ltrim($path, '/');
148 }
149
150 if (!$path) {
151 return $baseUrl . '/';
152 }
153
154 return $baseUrl . '/#/' . ltrim($path, '/');
155 }
156
157 /**
158 * Get the space IDs for a user.
159 *
160 * @param int|null $userId The user ID. If null, uses the current user.
161 * @return array An array of space IDs.
162 */
163 public static function getUserSpaceIds($userId = null)
164 {
165 if (!$userId) {
166 $userId = get_current_user_id();
167 }
168
169 return SpaceUserPivot::where('user_id', $userId)
170 ->where('status', 'active')
171 ->pluck('space_id')
172 ->toArray();
173 }
174
175 public static function getUserSpaces($userId = null)
176 {
177 if (!$userId) {
178 $userId = get_current_user_id();
179 }
180
181 return Space::whereHas('members', function ($query) use ($userId) {
182 $query->where('user_id', $userId);
183 })->get();
184 }
185
186 /**
187 * Check if a user is in a specific space.
188 *
189 * @param int $userId The user ID.
190 * @param int $spaceId The space ID.
191 * @return bool True if the user is in the space, false otherwise.
192 */
193 public static function isUserInSpace($userId, $spaceId)
194 {
195 return SpaceUserPivot::where('user_id', $userId)
196 ->where('space_id', $spaceId)
197 ->where('status', 'active')
198 ->exists();
199 }
200
201 /**
202 * Generate HTML attributes from an array.
203 *
204 * @param array $atts An array of attribute key-value pairs.
205 * @return string The generated HTML attributes string.
206 */
207 public static function attrs($atts = [])
208 {
209 $text = '';
210
211 foreach ($atts as $key => $value) {
212 $text .= "$key=\"$value\" ";
213 }
214
215 return $text;
216 }
217
218 /**
219 * Get media from a URL.
220 *
221 * @param string|array $url The URL or an array containing URL information.
222 * @return Media|null The Media object if found, null otherwise.
223 */
224 public static function getMediaFromUrl($url)
225 {
226 if (is_array($url) && isset($url['provider'])) {
227 $provider = Arr::get($url, 'provider');
228
229 if ($provider == 'giphy') {
230 return null;
231 }
232
233 $url = Arr::get($url, 'url');
234 }
235
236 if (!$url) {
237 return null;
238 }
239
240 $parsedUrl = wp_parse_url($url, PHP_URL_QUERY);
241
242 if (!$parsedUrl) {
243 return null;
244 }
245
246 // Parse the query string to get the media_key value
247 parse_str($parsedUrl, $queryParams);
248
249 $key = Arr::get($queryParams, 'media_key');
250
251 if (!$key) {
252 return null;
253 }
254
255 return Media::where('media_key', $key)->first();
256 }
257
258 /**
259 * Get media items from multiple URLs.
260 *
261 * @param array $urls An array of URLs.
262 * @return array An array of Media objects.
263 */
264 public static function getMediaItemsFromUrl($urls)
265 {
266 $mediaItems = [];
267
268 foreach ($urls as $url) {
269 $media = self::getMediaFromUrl($url);
270
271 if ($media) {
272 $mediaItems[] = $media;
273 }
274 }
275
276 return $mediaItems;
277 }
278
279 /**
280 * Get general settings for the community.
281 *
282 * @param bool $cached Whether to use cached settings.
283 * @return array The general settings.
284 */
285 public static function generalSettings($cached = true)
286 {
287 static $settings = null;
288
289 if ($cached && $settings) {
290 return $settings;
291 }
292
293 $settings = get_option('fluent_community_settings', []);
294
295 $defaults = [
296 'site_title' => get_bloginfo('name'),
297 'slug' => 'portal',
298 'logo' => '',
299 'white_logo' => '',
300 'featured_image' => '',
301 'access' => [
302 'acess_level' => 'logged_in', // logged_in, public, role_based
303 'access_roles' => []
304 ],
305 'disable_global_posts' => 'yes',
306 'auth_content' => 'Please login first to access this page',
307 'auth_redirect' => '',
308 'restricted_role_content' => 'Sorry, you can not access to this page. Only authorized users can access this page.',
309 'auth_url' => ''
310 ];
311
312 $settings = wp_parse_args($settings, $defaults);
313
314 if (defined('FLUENT_COMMUNITY_PORTAL_SLUG')) {
315 $settings['slug'] = \FLUENT_COMMUNITY_PORTAL_SLUG;
316 $settings['is_slug_defined'] = true;
317 } else {
318 unset($settings['is_slug_defined']);
319 }
320
321 return $settings;
322 }
323
324 public static function hasGlobalPost()
325 {
326 $settings = self::generalSettings();
327 $status = Arr::get($settings, 'disable_global_posts', '') != 'yes';
328
329 return apply_filters('fluent_community/has_global_post', $status);
330 }
331
332 /**
333 * Check if a user can access the portal.
334 *
335 * @param int|null $userId The user ID. If null, uses the current user.
336 * @return bool True if the user can access the portal, false otherwise.
337 */
338 public static function canAccessPortal($userId = null)
339 {
340 $settings = self::generalSettings();
341 $accessLevel = Arr::get($settings, 'access.acess_level');
342
343 if ($accessLevel == 'public') {
344 return apply_filters('fluent_community/can_access_portal', true);
345 }
346
347 if (!$userId) {
348 $userId = get_current_user_id();
349 }
350
351 if (!$userId) {
352 return apply_filters('fluent_community/can_access_portal', false);
353 }
354
355 if ($accessLevel == 'logged_in') {
356 return apply_filters('fluent_community/can_access_portal', true);
357 }
358
359 if (user_can($userId, 'edit_pages')) {
360 return apply_filters('fluent_community/can_access_portal', true);
361 }
362
363 $roles = Arr::get($settings, 'access.access_roles', []);
364
365 $user = get_user_by('ID', $userId);
366
367 if (!$user) {
368 return apply_filters('fluent_community/can_access_portal', false);
369 }
370
371 $result = !!array_intersect(array_values($user->roles), $roles);
372
373 if (!$result) {
374 return apply_filters('fluent_community/can_access_portal', false);
375 }
376
377 $xProfile = Helper::getCurrentProfile();
378
379 $result = $xProfile && $xProfile->status == 'active';
380
381 return apply_filters('fluent_community/can_access_portal', $result);
382 }
383
384 /**
385 * Get the portal route paths.
386 *
387 * @return array An array of portal route paths.
388 */
389 public static function portalRoutePaths()
390 {
391 return apply_filters('fluent_community/app_route_paths', [
392 'portal_home',
393 'space',
394 'discover',
395 'members',
396 'courses',
397 'u',
398 'leaderboards',
399 'chat',
400 'notifications',
401 'bookmarks',
402 'post',
403 'admin'
404 ]);
405 }
406
407 /**
408 * Get the current user's profile.
409 *
410 * @param bool $cached Whether to use cached profile.
411 * @return XProfile|null The user's profile or null if not found.
412 */
413 public static function getCurrentProfile($cached = true)
414 {
415 $userId = get_current_user_id();
416 if (!$userId) {
417 return null;
418 }
419
420 static $profile;
421
422 if ($profile && $cached) {
423 return $profile;
424 }
425
426 if (!$userId) {
427 return null;
428 }
429
430 $profile = XProfile::where('user_id', $userId)->first();
431
432 return $profile;
433 }
434
435 /**
436 * Get the current user Model.
437 *
438 * @param bool $cached Whether to use cached user.
439 * @return User|false The User model or false if not found.
440 */
441 public static function getCurrentUser($cached = true)
442 {
443 $userId = get_current_user_id();
444 if (!$userId) {
445 return false;
446 }
447
448 static $user;
449 if ($user && $cached) {
450 return $user;
451 }
452
453 $user = User::find($userId);
454
455 return $user;
456 }
457
458 /**
459 * Get the route paths for the community.
460 *
461 * @return array An array of route paths.
462 */
463 private static function getRoutePaths()
464 {
465 return [
466 'dashboard' => '/dashboard',
467 'all_feeds' => '/',
468 'single_feed' => '/post/:feed_slug',
469 'space_feeds' => '/space/:space/home',
470 'space_feed' => '/space/:space/post/:feed_slug',
471 'space_members' => '/space/:space/members',
472 'spaces' => '/discover/spaces',
473 'settings' => '/admin/settings',
474 'admin_moderators' => '/admin/settings/moderators',
475 'all_members' => '/members',
476 'user_profile' => '/u/:username/',
477 'user_communities' => '/u/:username/spaces',
478 'update_profile' => '/u/:username/update',
479 'discussions' => '/discussions',
480 'create_topic' => '/discussions/create-topic',
481 'topic' => '/discussions/topic/:slug',
482 'notifications' => '/notifications',
483 'bookmarks' => '/bookmarks',
484 'courses' => '/courses',
485 'view_course' => '/courses/view/:course_id/lessons',
486 'view_lesson' => '/courses/view/:course_id/lessons/:lesson_slug/view',
487 'manage_courses' => '/admin/manage-courses',
488 'edit_lessons' => '/admin/manage-courses/edit/:course_id/lessons',
489 'course_students' => '/admin/manage-courses/edit/:course_id/students',
490 'course_overview' => '/admin/manage-courses/edit/:course_id/overview',
491 'manage_leaderboard' => '/admin/manage-leaderboard',
492 ];
493 }
494
495 /**
496 * Get the URL for a JavaScript route.
497 *
498 * @param array $route The route information.
499 * @return string The URL for the route.
500 */
501 public static function getUrlByJsRoute($route = [])
502 {
503 $routePaths = self::getRoutePaths();
504
505 $routeName = Arr::get($route, 'name', '');
506
507 if (!$routeName || !isset($routePaths[$routeName])) {
508 return self::baseUrl();
509 }
510
511 $path = $routePaths[$routeName];
512
513 $params = (array)Arr::get($route, 'params', []);
514
515 if (!$params) {
516 return self::baseUrl($path);
517 }
518
519 $replaces = [];
520
521 foreach ($params as $paramKey => $paramValue) {
522 $replaces[':' . $paramKey] = $paramValue;
523 }
524
525 $path = str_replace(array_keys($replaces), array_values($replaces), $path);
526
527 return self::baseUrl($path);
528
529 }
530
531 /**
532 * Get the route name from a request path.
533 *
534 * @param string $path The request path.
535 * @return string|false The route name or false if not found.
536 */
537 public static function getRouteNameByRequestPath($path)
538 {
539 $path = '//' . $path;
540
541 if (strpos($path, '/u/')) {
542 return 'user_profile';
543 }
544
545 if (strpos($path, '/post/')) {
546 return 'feed_view';
547 }
548
549 if (strpos($path, '/lessons/')) {
550 return 'lesson_view';
551 }
552
553 if (strpos($path, '/course/')) {
554 return 'course_view';
555 }
556
557 if (strpos($path, '/space/') && !strpos($path, '/discover/spaces')) {
558 return 'community_view';
559 }
560
561 if (strpos($path, '/admin')) {
562 return 'admin';
563 }
564
565 return false;
566 }
567
568 /**
569 * Get a human-readable excerpt from content.
570 *
571 * @param string $content The content to extract from.
572 * @param int $length The maximum length of the excerpt.
573 * @return string The human-readable excerpt.
574 */
575 public static function getHumanExcerpt($content, $length = 100)
576 {
577 if ($content) {
578 $patterns = [
579 '/^#{1,6}\s+/m' => '',
580 // Bold and Italic: remove '*' and '_' symbols
581 '/(\*\*|__)(.*?)\1/' => '$2',
582 '/(\*|_)(.*?)\1/' => '$2',
583 // Code blocks: remove triple backticks
584 '/^```\s*\w*\s*\n([\s\S]*?)\n```\s*$/m' => '$1',
585 // Inline code: remove single backticks
586 '/`([^`]+)`/' => '$1',
587 // Blockquotes: remove '>' symbol
588 '/^\s*>\s?/m' => '',
589 // Horizontal rules: replace with empty line
590 '/^\s*([-*_])\1{2,}\s*$/m' => "\n",
591 // Links: keep only the link text
592 '/\[([^\]]+)\]\([^\)]+\)/' => '$1',
593 // Images: keep only the alt text
594 '/!\[([^\]]+)\]\([^\)]+\)/' => '$1',
595 // Strikethrough: remove '~~' symbols
596 '/~~(.*?)~~/' => '$1',
597 // Task lists: remove checkbox syntax
598 '/^\s*[-*+]\s+\[[ xX]\]\s+/m' => '',
599 ];
600
601 $content = preg_replace(array_keys($patterns), array_values($patterns), $content);
602
603 // remove all tags
604 $content = wp_strip_all_tags($content);
605 // remove new lines and tabs
606 $content = str_replace(["\r", "\n", "\t"], ' ', $content);
607 // remove multiple spaces
608 $content = preg_replace('/\s+/', ' ', $content);
609
610 // trim
611 $content = trim($content);
612 }
613
614 if (!$content) {
615 return '';
616 }
617
618 if (mb_strlen($content) <= $length) {
619 return $content;
620 }
621
622 // return the first $length chars of the content with ... at the end
623 return mb_substr($content, 0, $length) . '...';
624 }
625
626 /**
627 * Check if the portal is publicly accessible.
628 *
629 * @return bool True if publicly accessible, false otherwise.
630 */
631 public static function isPublicAccessible()
632 {
633 $settings = self::generalSettings();
634 return Arr::get($settings, 'access.acess_level') == 'public';
635 }
636
637 /**
638 * Get media by provider.
639 *
640 * @param array $images The array of images.
641 * @param string $provider The provider to filter by.
642 * @return array The filtered array of images.
643 */
644 public static function getMediaByProvider($images, $provider = 'uploader')
645 {
646 if (is_array($images)) {
647 return array_filter($images, function ($image) use ($provider) {
648 if (is_array($image)) {
649 if (isset($image['provider'])) {
650 return Arr::get($image, 'provider') == $provider;
651 }
652
653 return $provider === 'uploader'; // for existing images when no provider was set.
654 }
655 });
656 }
657
658 return [];
659 }
660
661 /**
662 * Get the community menu groups.
663 *
664 * @param User|null $user The user to get menu groups for.
665 * @return array The community menu groups.
666 */
667 public static function getCommunityMenuGroups($user = null)
668 {
669 if (!$user) {
670 $user = self::getCurrentUser();
671 }
672
673 $communityGroups = self::getAllCommunityGroups($user);
674
675 if ($communityGroups->isEmpty()) {
676 return [];
677 }
678
679 $isMod = $user && $user->isCommunityModerator();
680
681 $formattedGroups = [];
682
683 foreach ($communityGroups as $communityGroup) {
684 $spaces = $communityGroup->spaces;
685 $validSpaces = [];
686 $isShowAll = Arr::get($communityGroup->settings, 'always_show_spaces') === 'yes';
687
688 foreach ($spaces as $space) {
689 if ($isMod) {
690 $validSpaces[] = self::transformSpaceToLink($space);
691 continue;
692 }
693
694 if ($space->privacy === 'secret') {
695 if (!$user || !$space->getMembership($user->ID)) {
696 continue;
697 }
698 $validSpaces[] = self::transformSpaceToLink($space);
699 continue;
700 }
701
702 if ($isShowAll || $space->privacy = 'public') {
703 $validSpace = self::transformSpaceToLink($space);
704
705 if ($space->privacy == 'private') {
706 if (!$user || !$space->getMembership($user->ID)) {
707 $validSpace['show_lock'] = true;
708 }
709 }
710
711 $validSpaces[] = $validSpace;
712 continue;
713 }
714
715 if (!$user || $space->getMembership($user->ID)) {
716 continue;
717 }
718
719 $validSpaces[] = self::transformSpaceToLink($space);
720 }
721
722 if (!$validSpaces && !$isMod) {
723 continue;
724 }
725
726 $formattedGroups[] = [
727 'id' => $communityGroup->id,
728 'title' => $communityGroup->title,
729 'slug' => $communityGroup->slug,
730 'logo' => $communityGroup->logo,
731 'children' => $validSpaces
732 ];
733 }
734
735 return apply_filters('fluent_community/menu_groups_for_user', $formattedGroups, $user);
736 }
737
738 /**
739 * Transform a space to a link array.
740 *
741 * @param Space $space The space to transform.
742 * @return array The transformed space link array.
743 */
744 private static function transformSpaceToLink($space)
745 {
746
747 $logo = $space->logo;
748
749 $title = $space->title;
750
751 if ($space->status == 'draft') {
752 $title = $title . ' ' . __('(Draft)', 'fluent-community');
753 }
754
755 return [
756 'title' => $title,
757 'icon_image' => $logo,
758 'shape_svg' => !$logo ? Arr::get($space->settings, 'shape_svg', '') : '',
759 'emoji' => !$logo ? Arr::get($space->settings, 'emoji', '') : '',
760 'permalink' => $space->getPermalink(),
761 'link_classes' => 'space_menu_item route_url fcom_space_id_' . $space->id . ' fcom_space_' . $space->slug
762 ];
763 }
764
765 public static function isAlreadyOnboarded()
766 {
767 $communitySettings = get_option('fluent_community_settings', []);
768
769 return !empty($communitySettings);
770 }
771
772 /**
773 * Get all community groups.
774 *
775 * @param User $user The user to get groups for.
776 * @param bool $willCreate Whether to create a default group if none exist.
777 * @return \FluentCommunity\Framework\Support\Collection Collection of Groups
778 */
779 public static function getAllCommunityGroups($user, $willCreate = true)
780 {
781 $isModerator = $user && $user->isCommunityModerator();
782
783 $communityGroups = SpaceGroup::query()->orderBy('serial', 'ASC')
784 ->with([
785 'spaces' => function ($query) use ($isModerator) {
786 if ($isModerator) {
787 $query->orderBy('serial', 'ASC');
788 } else {
789 $query->where('status', 'published')
790 ->orderBy('serial', 'ASC');
791 }
792 }
793 ])
794 ->get();
795
796 if ($communityGroups->isEmpty() && $willCreate) {
797 $createdSpace = (new ActivationHandler(App::make()))->maybeCreateDefaultSpaceGroup();
798
799 if ($createdSpace) {
800 Space::where('type', 'community')->update([
801 'parent_id' => $createdSpace->id
802 ]);
803 }
804
805 return self::getAllCommunityGroups($user, false);
806 }
807
808 return $communityGroups;
809 }
810
811 /**
812 * Check if a feature is enabled.
813 *
814 * @param string $feature The feature to check.
815 * @return bool True if the feature is enabled, false otherwise.
816 */
817 public static function isFeatureEnabled($feature)
818 {
819 $features = Utility::getFeaturesConfig();
820
821 return isset($features[$feature]) && $features[$feature] === 'yes';
822 }
823
824 /**
825 * Get the menu items group.
826 *
827 * @param string $context The context for getting menu items.
828 * @return array The menu items group.
829 */
830 public static function getMenuItemsGroup($context = 'view')
831 {
832 static $menuGroups;
833
834 if ($menuGroups && $context === 'view') {
835 return $menuGroups;
836 }
837
838 $menuGroups = Utility::getOption('fluent_community_menu_groups', []);
839
840 $membersPageStatus = Utility::canViewMembersPage() ? 'yes' : 'no';
841
842 $defaultMainMenuItems = [
843 'all_feeds' => [
844 'slug' => 'all_feeds',
845 'title' => __('Feed', 'fluent-community'),
846 'is_system' => 'yes',
847 'is_locked' => 'yes',
848 'enabled' => 'yes',
849 'permalink' => self::baseUrl('/'),
850 'link_classes' => 'fcom_dashboard route_url',
851 '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>'
852 ],
853 'spaces' => [
854 'slug' => 'spaces',
855 'title' => __('Spaces', 'fluent-community'),
856 'is_system' => 'yes',
857 'is_locked' => 'yes',
858 'enabled' => 'yes',
859 'permalink' => self::baseUrl('discover/spaces'),
860 'link_classes' => 'fcom_spaces route_url',
861 '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>'
862 ],
863 'all_courses' => [
864 'slug' => 'all_courses',
865 'title' => 'Courses',
866 'link_classes' => 'fcom_courses route_url',
867 'is_system' => 'yes',
868 'is_locked' => 'yes',
869 'enabled' => 'yes',
870 'is_unavailable' => self::isFeatureEnabled('course_module') ? 'no' : 'yes',
871 'permalink' => self::baseUrl('courses'),
872 '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>'
873 ],
874 'all_members' => [
875 'slug' => 'all_members',
876 'title' => __('Members', 'fluent-community'),
877 'is_system' => 'yes',
878 'is_locked' => 'yes',
879 'is_unavailable' => $membersPageStatus == 'yes' ? 'no' : 'yes',
880 'enabled' => $membersPageStatus,
881 'permalink' => self::baseUrl('members'),
882 'link_classes' => 'fcom_all_members route_url',
883 '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>',
884 ],
885 'leaderboard' => [
886 'slug' => 'leaderboard',
887 'is_system' => 'yes',
888 'is_locked' => 'yes',
889 'enabled' => 'yes',
890 'is_unavailable' => self::isFeatureEnabled('leader_board_module') ? 'no' : 'yes',
891 'title' => __('Leaderboard', 'fluent-community'),
892 'link_classes' => 'fcom_leaderboards route_url',
893 'permalink' => self::baseUrl('leaderboards'),
894 '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>'
895 ]
896 ];
897
898 $mainItems = Arr::get($menuGroups, 'mainMenuItems', []);
899
900 if ($mainItems && is_array($mainItems)) {
901 if (isset($mainItems['all_communities'])) {
902 $mainItems['spaces'] = $defaultMainMenuItems['spaces'];
903 unset($mainItems['all_communities']);
904 }
905
906 foreach ($mainItems as $index => &$item) {
907 if (empty($item['slug'])) {
908 unset($mainItems[$index]);
909 continue;
910 }
911 $defaultItem = Arr::get($defaultMainMenuItems, $item['slug'], []);
912 if ($defaultItem) {
913 $preservedKeys = ['is_system', 'is_locked', 'is_unavailable', 'slug', 'shape_svg'];
914 foreach ($preservedKeys as $key) {
915 if (isset($defaultItem[$key])) {
916 $item[$key] = Arr::get($defaultItem, $key);
917 }
918 }
919 if (Arr::get($defaultItem, 'is_system') === 'yes') {
920 $item['permalink'] = $defaultItem['permalink'];
921 $item['emoji'] = '';
922 $item['icon_image'] = '';
923 $item['link_classes'] = $defaultItem['link_classes'];
924 }
925 }
926 }
927 } else {
928 $mainItems = $defaultMainMenuItems;
929 }
930
931 $defaultProfileDropDownItems = [
932 'my_spaces' => [
933 'slug' => 'my_spaces',
934 'title' => __('My Spaces', 'fluent-community'),
935 'is_system' => 'yes',
936 'is_locked' => 'yes',
937 'enabled' => 'yes',
938 'permalink' => '#{{user_url}}/spaces',
939 '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>'
940 ],
941 'bookmarks' => [
942 'slug' => 'bookmarks',
943 'title' => __('Bookmarks', 'fluent-community'),
944 'is_system' => 'yes',
945 'is_locked' => 'yes',
946 'enabled' => 'yes',
947 'permalink' => self::baseUrl('bookmarks'),
948 '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>'
949 ],
950 'logout' => [
951 'slug' => 'logout',
952 'title' => __('Logout', 'fluent-community'),
953 'is_system' => 'yes',
954 'is_locked' => 'yes',
955 'enabled' => 'yes',
956 'permalink' => '#{{logout_url}}',
957 '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>'
958 ]
959 ];
960
961 $profileDropDownItems = Arr::get($menuGroups, 'profileDropdownItems', []);
962
963 if ($profileDropDownItems && is_array($profileDropDownItems)) {
964
965 unset($profileDropDownItems['profile']);
966
967 foreach ($profileDropDownItems as $index => &$item) {
968 if (empty($item['slug'])) {
969 unset($profileDropDownItems[$index]);
970 continue;
971 }
972 $defaultItem = Arr::get($defaultProfileDropDownItems, $item['slug'], []);
973 if ($defaultItem) {
974 $preservedKeys = ['is_system', 'is_locked', 'is_unavailable', 'slug', 'svg_icon'];
975 foreach ($preservedKeys as $key) {
976 if (isset($defaultItem[$key])) {
977 $item[$key] = Arr::get($defaultItem, $key);
978 }
979 }
980 if (Arr::get($defaultItem, 'is_system') === 'yes') {
981 $item['permalink'] = $defaultItem['permalink'];
982 if (empty($item['shape_svg'])) {
983 $item['shape_svg'] = $defaultItem['shape_svg'];
984 }
985 }
986 }
987 }
988 } else {
989 $profileDropDownItems = $defaultProfileDropDownItems;
990 }
991
992 $beforeCommunityMenuItems = Arr::get($menuGroups, 'beforeCommunityMenuItems', []);
993 $afterCommunityMenuGroups = Arr::get($menuGroups, 'afterCommunityLinkGroups', []);
994
995 if (!is_array($beforeCommunityMenuItems)) {
996 $beforeCommunityMenuItems = [];
997 }
998
999 if (!is_array($afterCommunityMenuGroups)) {
1000 $afterCommunityMenuGroups = [];
1001 }
1002
1003 if ($context == 'view') {
1004 $mainItems = array_filter($mainItems, function ($item) {
1005 return Arr::get($item, 'enabled') === 'yes' && Arr::get($item, 'is_unavailable') !== 'yes';
1006 });
1007
1008 $profileDropDownItems = array_filter($profileDropDownItems, function ($item) {
1009 return Arr::get($item, 'enabled') === 'yes' && Arr::get($item, 'is_unavailable') !== 'yes';
1010 });
1011
1012 $beforeCommunityMenuItems = array_filter($beforeCommunityMenuItems, function ($item) {
1013 return Arr::get($item, 'enabled') === 'yes' && Arr::get($item, 'is_unavailable') !== 'yes';
1014 });
1015
1016 $validGroups = [];
1017 foreach ($afterCommunityMenuGroups as $group) {
1018 if (empty($group['items']) || !is_array($group['items'])) {
1019 continue;
1020 }
1021
1022 $group['items'] = array_filter($group['items'], function ($item) {
1023 return Arr::get($item, 'enabled') === 'yes' && Arr::get($item, 'is_unavailable') !== 'yes';
1024 });
1025
1026 if ($group['items']) {
1027 $validGroups[] = $group;
1028 }
1029 }
1030
1031 $afterCommunityMenuGroups = $validGroups;
1032 }
1033
1034 $menuGroups['mainMenuItems'] = $mainItems;
1035 $menuGroups['profileDropdownItems'] = $profileDropDownItems;
1036 $menuGroups['beforeCommunityMenuItems'] = $beforeCommunityMenuItems;
1037 $menuGroups['afterCommunityLinkGroups'] = $afterCommunityMenuGroups;
1038
1039 if ($context == 'view') {
1040 $menuGroups = apply_filters('fluent_community/menu_groups', $menuGroups);
1041 }
1042
1043 return $menuGroups;
1044 }
1045
1046 /**
1047 * Get the meta data for a space.
1048 *
1049 * @param int $spaceId The ID of the space.
1050 * @param string $key The meta key.
1051 * @param mixed $default The default value if the meta key is not found.
1052 * @return mixed The meta value or the default value if not found.
1053 */
1054 public static function getSpaceMeta($spaceId, $key, $default = null)
1055 {
1056 $meta = Meta::where('object_type', 'space')
1057 ->where('meta_key', $key)
1058 ->where('object_id', $spaceId)
1059 ->first();
1060
1061 if (!$meta) {
1062 return $default;
1063 }
1064
1065 return $meta->value;
1066 }
1067
1068 /**
1069 * Update the meta data for a space.
1070 *
1071 * @param int $spaceId The ID of the space.
1072 * @param string $key The meta key.
1073 * @param mixed $value The meta value.
1074 * @return Meta The updated meta object.
1075 */
1076 public static function updateSpaceMeta($spaceId, $key, $value)
1077 {
1078 $meta = Meta::where('object_type', 'space')
1079 ->where('meta_key', $key)
1080 ->where('object_id', $spaceId)
1081 ->first();
1082
1083 if ($meta) {
1084 $meta->value = $value;
1085 $meta->save();
1086 } else {
1087 $meta = Meta::create([
1088 'object_type' => 'space',
1089 'object_id' => $spaceId,
1090 'meta_key' => $key,
1091 'value' => $value
1092 ]);
1093 }
1094
1095 return $meta;
1096 }
1097
1098
1099 /**
1100 * Encrypt or decrypt a value.
1101 *
1102 * @param string $value The value to encrypt or decrypt.
1103 * @param string $type The type of operation ('e' for encrypt, 'd' for decrypt).
1104 * @return string|false The encrypted or decrypted value or false if an error occurs.
1105 */
1106 public static function encryptDecrypt($value, $type = 'e')
1107 {
1108 if (!$value) {
1109 return $value;
1110 }
1111
1112 if (!extension_loaded('openssl')) {
1113 return $value;
1114 }
1115
1116 if (defined('FLUENT_COM_ENCRYPT_SALT')) {
1117 $salt = FLUENT_COM_ENCRYPT_SALT;
1118 } else {
1119 $salt = (defined('LOGGED_IN_SALT') && '' !== LOGGED_IN_SALT) ? LOGGED_IN_SALT : 'this-is-a-fallback-salt-but-not-secure';
1120 }
1121
1122 if (defined('FLUENT_COM__ENCRYPT_KEY')) {
1123 $key = FLUENT_COM__ENCRYPT_KEY;
1124 } else {
1125 $key = (defined('LOGGED_IN_KEY') && '' !== LOGGED_IN_KEY) ? LOGGED_IN_KEY : 'this-is-a-fallback-key-but-not-secure';
1126 }
1127
1128 if ($type == 'e') {
1129 $method = 'aes-256-ctr';
1130 $ivlen = openssl_cipher_iv_length($method);
1131 $iv = openssl_random_pseudo_bytes($ivlen);
1132
1133 $raw_value = openssl_encrypt($value . $salt, $method, $key, 0, $iv);
1134 if (!$raw_value) {
1135 return false;
1136 }
1137
1138 return base64_encode($iv . $raw_value); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
1139 }
1140
1141 $raw_value = base64_decode($value, true); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
1142
1143 $method = 'aes-256-ctr';
1144 $ivlen = openssl_cipher_iv_length($method);
1145 $iv = substr($raw_value, 0, $ivlen);
1146
1147 $raw_value = substr($raw_value, $ivlen);
1148
1149 $newValue = openssl_decrypt($raw_value, $method, $key, 0, $iv);
1150 if (!$newValue || substr($newValue, -strlen($salt)) !== $salt) {
1151 return false;
1152 }
1153
1154 return substr($newValue, 0, -strlen($salt));
1155 }
1156
1157
1158 /**
1159 * Get the welcome banner configuration.
1160 *
1161 * @return array The welcome banner configuration.
1162 */
1163 public static function getWelcomeBannerSettings()
1164 {
1165 return Utility::getFromCache('welcome_banner_settings', function () {
1166 $defaults = [
1167 'login' => [
1168 'enabled' => 'no',
1169 'description' => '',
1170 'mediaType' => 'image',
1171 'allowClose' => 'no',
1172 'bannerImage' => '',
1173 'bannerVideo' => [
1174 'type' => 'oembed',
1175 'url' => '',
1176 'content_type' => '',
1177 'provider' => '',
1178 'title' => '',
1179 'author_name' => '',
1180 'html' => ''
1181 ],
1182 'ctaButtons' => []
1183 ],
1184 'logout' => [
1185 'enabled' => 'no',
1186 'description' => '',
1187 'mediaType' => 'image',
1188 'useCustomUrl' => 'no',
1189 'bannerImage' => '',
1190 'bannerVideo' => [
1191 'type' => 'oembed',
1192 'url' => '',
1193 'content_type' => '',
1194 'provider' => '',
1195 'title' => '',
1196 'author_name' => '',
1197 'html' => ''
1198 ],
1199 'ctaButtons' => []
1200 ]
1201 ];
1202
1203 $settings = Utility::getOption('welcome_banner_settings', []);
1204
1205 $settings = wp_parse_args($settings, $defaults);
1206
1207 if (empty(Arr::get($settings, 'login.bannerVideo'))) {
1208 $settings['login']['bannerVideo'] = $defaults['login']['bannerVideo'];
1209 }
1210
1211 if (empty(Arr::get($settings, 'logout.bannerVideo'))) {
1212 $settings['logout']['bannerVideo'] = $defaults['logout']['bannerVideo'];
1213 }
1214
1215 return $settings;
1216 }, WEEK_IN_SECONDS);
1217 }
1218
1219 public static function getWelcomeBanner($view = 'login')
1220 {
1221 $settings = self::getWelcomeBannerSettings();
1222 $welcomeBanner = Arr::get($settings, $view, []);
1223 if (Arr::get($welcomeBanner, 'enabled') != 'yes') {
1224 return null;
1225 }
1226
1227 unset($welcomeBanner['description']);
1228
1229 if ($view == 'login') {
1230 return apply_filters('fluent_community/welcome_banner_for_logged_in', $welcomeBanner);
1231 }
1232
1233 return apply_filters('fluent_community/welcome_banner_for_guests', $welcomeBanner);
1234 }
1235
1236 public static function getEnabledFeedLinks()
1237 {
1238 $links = array_filter(self::getFeedLinks(), function ($item) {
1239 return Arr::get($item, 'enabled') == 'yes' && Arr::get($item, 'is_unavailable') != 'yes';
1240 });
1241
1242 return array_values($links);
1243 }
1244
1245 public static function getFeedLinks()
1246 {
1247 return Utility::getFromCache('feed_links', function () {
1248 return Utility::getOption('feed_links', []);
1249 }, WEEK_IN_SECONDS);
1250 }
1251
1252 public static function updateFeedLinks($links)
1253 {
1254 Utility::updateOption('feed_links', $links);
1255 Utility::setCache('feed_links', $links, WEEK_IN_SECONDS);
1256 }
1257
1258 /**
1259 * Get the full name of a WordPress user.
1260 *
1261 * @param int|null $id The ID of the user.
1262 * @return string The full name of the user.
1263 */
1264 public static function getWpUserFullName($id = null)
1265 {
1266 $id = $id ?: get_current_user_id();
1267 $user = get_user_by('ID', $id);
1268
1269 $fullName = $user->display_name;
1270 if ($user->first_name && $user->last_name) {
1271 $fullName = $user->first_name . ' ' . $user->last_name;
1272 }
1273
1274 return $fullName;
1275 }
1276
1277 /**
1278 * Get the onboarding settings.
1279 *
1280 * @return array The onboarding settings.
1281 */
1282 public static function getOnboardingSettings()
1283 {
1284 $default = [
1285 'is_onboarding_enabled' => 'no',
1286 'registration_page_url' => '',
1287 ];
1288
1289 $settings = Utility::getOption('onboarding_settings', $default);
1290
1291 return wp_parse_args($settings, $default);
1292 }
1293
1294 /**
1295 * Get all WordPress published pages.
1296 *
1297 * @return array An array of page data.
1298 */
1299 public static function getAllWpPublishedPage()
1300 {
1301 $posts = get_posts(array(
1302 'post_status' => 'publish',
1303 'numberposts' => -1,
1304 'post_type' => 'any',
1305 ));
1306
1307 return array_map(function ($post) {
1308 return array(
1309 'id' => $post->ID,
1310 'permalink' => get_permalink($post),
1311 'title' => get_the_title($post),
1312 );
1313 }, $posts);
1314 }
1315
1316 /**
1317 * Add a user to a space.
1318 *
1319 * @param Space | int $space space to add the user to.
1320 * @param int $userId The ID of the user to add.
1321 * @param string $role The role of the user in the space.
1322 * @param string $by The source of the action.
1323 * @return bool True if the user was added, false otherwise.
1324 */
1325 public static function addToSpace($space, $userId, $role = 'member', $by = 'self')
1326 {
1327 if (is_numeric($space)) {
1328 $space = BaseSpace::withoutGlobalScopes()->find($space);
1329 }
1330
1331 if (!$space) {
1332 return false;
1333 }
1334
1335 $user = User::find($userId);
1336
1337 if (!$user) {
1338 return false;
1339 }
1340
1341 $user->syncXProfile();
1342
1343 $exist = SpaceUserPivot::where('user_id', $userId)
1344 ->where('space_id', $space->id)
1345 ->first();
1346
1347 if ($exist) {
1348 if ($exist->status != 'active') {
1349 $exist->status = 'active';
1350 $exist->role = $role;
1351 $exist->save();
1352
1353 if ($space->type == 'course') {
1354 do_action('fluent_community/course/enrolled', $space, $userId, $by);
1355 } else {
1356 do_action('fluent_community/space/joined', $space, $userId, $by);
1357 }
1358
1359 return true;
1360 }
1361
1362 return false;
1363 }
1364
1365 SpaceUserPivot::create([
1366 'space_id' => $space->id,
1367 'role' => $role,
1368 'user_id' => $userId
1369 ]);
1370
1371 if ($space->type == 'course') {
1372 do_action('fluent_community/course/enrolled', $space, $userId, $by);
1373 } else {
1374 do_action('fluent_community/space/joined', $space, $userId, $by);
1375 }
1376
1377 return true;
1378 }
1379
1380 /**
1381 * Remove a user from a space if exist.
1382 *
1383 * @param int $userId The ID of the user.
1384 * @param int $spaceId The ID of the space.
1385 * @param string $by The source of the action. self | by_admin
1386 * @return bool True if the user is in the space, false otherwise.
1387 */
1388 public static function removeFromSpace($space, $userId, $by = 'self')
1389 {
1390 $user = User::find($userId);
1391 if (!$user) {
1392 return false;
1393 }
1394
1395 if (is_numeric($space)) {
1396 $space = BaseSpace::query()->withoutGlobalScopes()->find($space);
1397 }
1398
1399 if (!$space) {
1400 return false;
1401 }
1402
1403 if (!self::isUserInSpace($userId, $space->id)) {
1404 return false;
1405 }
1406
1407 SpaceUserPivot::bySpace($space->id)
1408 ->byUser($userId)
1409 ->delete();
1410
1411 $user->cacheAccessSpaces();
1412
1413 if ($space->type == 'course') {
1414 do_action('fluent_community/course/student_left', $space, $userId, $by);
1415 } else {
1416 do_action('fluent_community/space/user_left', $space, $userId, $by);
1417 }
1418
1419 return true;
1420 }
1421
1422 /**
1423 * Render a link with icon.
1424 *
1425 * @param array $link The link data.
1426 * @param string $linkClass Additional classes for the link.
1427 * @param string $fallback The fallback content if no icon is found.
1428 * @param bool $renderIcon Whether to render the icon or not.
1429 */
1430 public static function renderLink($link, $linkClass = '', $fallback = '<span class="fcom_no_avatar"></span>', $renderIcon = true)
1431 {
1432 $linkAtts = array_filter([
1433 'class' => trim($linkClass . ' ' . Arr::get($link, 'link_classes')) . ' fcom_compt_link',
1434 'target' => Arr::get($link, 'new_tab') === 'yes' ? '_blank' : '',
1435 'rel' => Arr::get($link, 'new_tab') === 'yes' ? 'noopener noreferrer' : '',
1436 ]);
1437 ?>
1438 <a aria-label="Go to <?php echo esc_attr(Arr::get($link, 'title')); ?> page"
1439 href="<?php echo esc_url($link['permalink']); ?>" <?php foreach ($linkAtts as $key => $value) {
1440 echo esc_attr($key) . '="' . esc_attr($value) . '"';
1441 } ?>>
1442 <?php $renderIcon && self::printLinkIcon($link, $fallback); ?>
1443 <span class="community_name"><?php echo wp_kses_post(Arr::get($link, 'title')); ?></span>
1444 <?php if (Arr::get($link, 'show_lock')): ?>
1445 <span class="fcom_space_lock">
1446 <i class="el-icon">
1447 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
1448 <path fill="currentColor"
1449 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>
1450 <path fill="currentColor"
1451 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>
1452 </svg>
1453 </i>
1454 </span>
1455 <?php endif; ?>
1456
1457 </a>
1458 <?php
1459 }
1460
1461 /**
1462 * Print a link icon.
1463 *
1464 * @param array $link The link data.
1465 * @param string $fallback The fallback content if no icon is found.
1466 */
1467 public static function printLinkIcon($link, $fallback = '<span class="fcom_no_avatar"></span>')
1468 {
1469 ?>
1470 <?php if ($img = Arr::get($link, 'icon_image')): ?>
1471 <div class="community_avatar">
1472 <img alt="" src="<?php echo esc_url($img); ?>"/>
1473 </div>
1474 <?php elseif ($emoji = Arr::get($link, 'emoji')): ?>
1475 <div class="community_avatar">
1476 <span class="fcom_emoji"><?php echo esc_html($emoji); ?></span>
1477 </div>
1478 <?php elseif ($svg = Arr::get($link, 'shape_svg')): ?>
1479 <div class="community_avatar">
1480 <span class="fcom_shape"><i
1481 class="el-icon"><?php echo \FluentCommunity\App\Services\CustomSanitizer::sanitizeSvg($svg); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></i></span>
1482 </div>
1483 <?php else:
1484 echo '<div class="community_avatar">' . wp_kses_post($fallback) . '</div>';
1485 endif;
1486 }
1487
1488 /**
1489 * Get the IP address of the user.
1490 *
1491 * @param bool $anonymize Whether to anonymize the IP address.
1492 * @return string The IP address.
1493 */
1494 public static function getIp($anonymize = false)
1495 {
1496 static $ipAddress;
1497
1498 if ($ipAddress) {
1499 return $ipAddress;
1500 }
1501
1502 if (empty($_SERVER['REMOTE_ADDR'])) {
1503 // It's a local cli request
1504 return '127.0.0.1';
1505 }
1506
1507 $ipAddress = '';
1508 if (isset($_SERVER["HTTP_CF_CONNECTING_IP"])) {
1509 $ipAddress = sanitize_text_field(wp_unslash($_SERVER["REMOTE_ADDR"]));
1510 //If it's a valid Cloudflare request
1511 if (self::isCfIp($ipAddress)) {
1512 //Use the CF-Connecting-IP header.
1513 $ipAddress = sanitize_text_field(wp_unslash($_SERVER['HTTP_CF_CONNECTING_IP']));
1514 }
1515 } else if ($_SERVER['REMOTE_ADDR'] == '127.0.0.1') {
1516 // most probably it's local reverse proxy
1517 if (isset($_SERVER["HTTP_CLIENT_IP"])) {
1518 $ipAddress = sanitize_text_field(wp_unslash($_SERVER["HTTP_CLIENT_IP"]));
1519 } else if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
1520 $ipAddress = (string)rest_is_ip_address(trim(current(preg_split('/,/', sanitize_text_field(wp_unslash($_SERVER['HTTP_X_FORWARDED_FOR']))))));
1521 }
1522 }
1523
1524 if (!$ipAddress) {
1525 $ipAddress = sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR']));
1526 }
1527
1528 $ipAddress = preg_replace('/^(\d+\.\d+\.\d+\.\d+):\d+$/', '\1', $ipAddress);
1529
1530 $ipAddress = apply_filters('fluent_auth/user_ip', $ipAddress);
1531
1532 if ($anonymize) {
1533 return wp_privacy_anonymize_ip($ipAddress);
1534 }
1535
1536 return $ipAddress;
1537 }
1538
1539 /**
1540 * Check if the IP address is from Cloudflare.
1541 *
1542 * @param string $ip The IP address to check.
1543 * @return bool True if the IP is from Cloudflare, false otherwise.
1544 */
1545 public static function isCfIp($ip = '')
1546 {
1547 if (!$ip && isset($_SERVER["REMOTE_ADDR"])) {
1548 $ip = sanitize_text_field(wp_unslash($_SERVER["REMOTE_ADDR"]));
1549 }
1550
1551 if (!$ip) {
1552 return false;
1553 }
1554
1555 $cloudflareIPRanges = array(
1556 '173.245.48.0/20',
1557 '103.21.244.0/22',
1558 '103.22.200.0/22',
1559 '103.31.4.0/22',
1560 '141.101.64.0/18',
1561 '108.162.192.0/18',
1562 '190.93.240.0/20',
1563 '188.114.96.0/20',
1564 '197.234.240.0/22',
1565 '198.41.128.0/17',
1566 '162.158.0.0/15',
1567 '104.16.0.0/13',
1568 '104.24.0.0/14',
1569 '172.64.0.0/13',
1570 '131.0.72.0/22',
1571 );
1572
1573 //Make sure that the request came via Cloudflare.
1574 foreach ($cloudflareIPRanges as $range) {
1575 //Use the ip_in_range function from Joomla.
1576 if (self::ipInRange($ip, $range)) {
1577 //IP is valid. Belongs to Cloudflare.
1578 return true;
1579 }
1580 }
1581
1582 return false;
1583 }
1584
1585 /**
1586 * Check if the IP address is in the given range.
1587 *
1588 * @param string $ip The IP address to check.
1589 * @param string $range The range to check against.
1590 * @return bool True if the IP is in the range, false otherwise.
1591 */
1592 private static function ipInRange($ip, $range)
1593 {
1594 if (strpos($range, '/') !== false) {
1595 // $range is in IP/NETMASK format
1596 list($range, $netmask) = explode('/', $range, 2);
1597 if (strpos($netmask, '.') !== false) {
1598 // $netmask is a 255.255.0.0 format
1599 $netmask = str_replace('*', '0', $netmask);
1600 $netmask_dec = ip2long($netmask);
1601 return ((ip2long($ip) & $netmask_dec) == (ip2long($range) & $netmask_dec));
1602 } else {
1603 // $netmask is a CIDR size block
1604 // fix the range argument
1605 $x = explode('.', $range);
1606 while (count($x) < 4) $x[] = '0';
1607 list($a, $b, $c, $d) = $x;
1608 $range = sprintf("%u.%u.%u.%u", empty($a) ? '0' : $a, empty($b) ? '0' : $b, empty($c) ? '0' : $c, empty($d) ? '0' : $d);
1609 $range_dec = ip2long($range);
1610 $ip_dec = ip2long($ip);
1611
1612 # Strategy 1 - Create the netmask with 'netmask' 1s and then fill it to 32 with 0s
1613 #$netmask_dec = bindec(str_pad('', $netmask, '1') . str_pad('', 32-$netmask, '0'));
1614
1615 # Strategy 2 - Use math to create it
1616 $wildcard_dec = pow(2, (32 - $netmask)) - 1;
1617 $netmask_dec = ~$wildcard_dec;
1618
1619 return (($ip_dec & $netmask_dec) == ($range_dec & $netmask_dec));
1620 }
1621 } else {
1622 // range might be 255.255.*.* or 1.2.3.0-1.2.3.255
1623 if (strpos($range, '*') !== false) { // a.b.*.* format
1624 // Just convert to A-B format by setting * to 0 for A and 255 for B
1625 $lower = str_replace('*', '0', $range);
1626 $upper = str_replace('*', '255', $range);
1627 $range = "$lower-$upper";
1628 }
1629
1630 if (strpos($range, '-') !== false) { // A-B format
1631 list($lower, $upper) = explode('-', $range, 2);
1632 $lower_dec = (float)sprintf("%u", ip2long($lower));
1633 $upper_dec = (float)sprintf("%u", ip2long($upper));
1634 $ip_dec = (float)sprintf("%u", ip2long($ip));
1635 return (($ip_dec >= $lower_dec) && ($ip_dec <= $upper_dec));
1636 }
1637 return false;
1638 }
1639 }
1640
1641 public static function getPortalRequestPath($requestUri)
1642 {
1643 $portalSlug = self::getPortalSlug();
1644
1645 if ($portalSlug == $requestUri) {
1646 return 'portal_home';
1647 }
1648
1649 if (!$requestUri) {
1650 return false;
1651 }
1652
1653 if ($portalSlug) {
1654 // remove the portal slug from the request uri. Don't use str_replace as it will replace all occurrences
1655 $requestUri = substr($requestUri, strlen($portalSlug));
1656 }
1657
1658 $parts = explode('/', $requestUri);
1659 $start = $parts[0];
1660
1661 $routeStats = self::portalRoutePaths();
1662
1663 if (in_array($start, $routeStats)) {
1664 return $requestUri;
1665 }
1666 return false;
1667 }
1668
1669 public static function getTopicsConfig()
1670 {
1671 return Utility::getFromCache('topics_config', function () {
1672 $config = Utility::getOption('topics_config', []);
1673 $default = [
1674 'max_topics_per_post' => 1,
1675 'max_topics_per_space' => 20,
1676 'show_on_post_card' => 'yes'
1677 ];
1678 return wp_parse_args($config, $default);
1679 }, WEEK_IN_SECONDS);
1680 }
1681 }
1682