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

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