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

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