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

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