| @@ -5,8 +5,10 @@ | ||
| 5 | 5 | use FluentCommunity\App\App; |
| 6 | 6 | use FluentCommunity\App\Functions\Utility; |
| 7 | 7 | use FluentCommunity\App\Hooks\Handlers\ActivationHandler; |
| 8 | 8 | use FluentCommunity\App\Models\BaseSpace; |
| 9 | +use FluentCommunity\App\Models\Contact; | |
| 10 | +use FluentCommunity\App\Models\Feed; | |
| 9 | 11 | use FluentCommunity\App\Models\Space; |
| 10 | 12 | use FluentCommunity\App\Models\Media; |
| 11 | 13 | use FluentCommunity\App\Models\Meta; |
| 12 | 14 | use FluentCommunity\App\Models\SpaceUserPivot; |
| @@ -13,8 +15,9 @@ | ||
| 13 | 15 | use FluentCommunity\App\Models\User; |
| 14 | 16 | use FluentCommunity\App\Models\XProfile; |
| 15 | 17 | use FluentCommunity\Framework\Support\Arr; |
| 16 | 18 | use FluentCommunity\App\Models\SpaceGroup; |
| 19 | +use FluentCommunity\Modules\Course\Model\Course; | |
| 17 | 20 | |
| 18 | 21 | /** |
| 19 | 22 | * Helper class for various utility functions. |
| 20 | 23 | */ |
| @@ -26,30 +29,95 @@ | ||
| 26 | 29 | return apply_filters('fluent_community/is_rtl', is_rtl()); |
| 27 | 30 | } |
| 28 | 31 | |
| 29 | 32 | /** |
| 33 | + * Run a callback inside a database transaction. | |
| 34 | + * | |
| 35 | + * @param callable $callback | |
| 36 | + * @return mixed | |
| 37 | + * @throws \Exception | |
| 38 | + */ | |
| 39 | + public static function dbTransaction($callback) | |
| 40 | + { | |
| 41 | + $db = App::make('db'); | |
| 42 | + $db->beginTransaction(); | |
| 43 | + | |
| 44 | + try { | |
| 45 | + $result = $callback(); | |
| 46 | + $db->commit(); | |
| 47 | + return $result; | |
| 48 | + } catch (\Exception $e) { | |
| 49 | + $db->rollBack(); | |
| 50 | + throw $e; | |
| 51 | + } | |
| 52 | + } | |
| 53 | + | |
| 54 | + /** | |
| 55 | + * Check if POST content length exceeds PHP limits | |
| 56 | + * | |
| 57 | + * @return array|false Error array if limit exceeded, false otherwise | |
| 58 | + */ | |
| 59 | + public static function checkUploadSizeError() | |
| 60 | + { | |
| 61 | + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- Server variable check | |
| 62 | + $contentLength = isset($_SERVER['CONTENT_LENGTH']) ? (int) $_SERVER['CONTENT_LENGTH'] : 0; | |
| 63 | + $postMaxSize = wp_convert_hr_to_bytes(ini_get('post_max_size')); | |
| 64 | + | |
| 65 | + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- read-only existence check on superglobals, no state mutation | |
| 66 | + if ($contentLength > 0 && ($contentLength > $postMaxSize || (empty($_FILES) && empty($_POST)))) { | |
| 67 | + return [ | |
| 68 | + 'message' => sprintf( | |
| 69 | + /* translators: %s: max size */ | |
| 70 | + __('Upload failed: File exceeds server limit (%s). Please upload a smaller file.', 'fluent-community'), | |
| 71 | + size_format($postMaxSize) | |
| 72 | + ) | |
| 73 | + ]; | |
| 74 | + } | |
| 75 | + | |
| 76 | + return false; | |
| 77 | + } | |
| 78 | + | |
| 79 | + /** | |
| 30 | 80 | * Get the portal slug. |
| 31 | 81 | * |
| 32 | 82 | * @return string The portal slug. |
| 33 | 83 | */ |
| 84 | + /** | |
| 85 | + * Get the portal slug. | |
| 86 | + * | |
| 87 | + * @return string The portal slug. | |
| 88 | + */ | |
| 34 | 89 | public static function getPortalSlug($forRoute = false) |
| 35 | 90 | { |
| 36 | - $settings = self::generalSettings(); | |
| 37 | - $slug = apply_filters('fluent_community/portal_slug', $settings['slug']); | |
| 91 | + $settings = get_option('fluent_community_settings', []); | |
| 92 | + if (isset($settings['slug'])) { | |
| 93 | + $slug = $settings['slug']; | |
| 94 | + } else { | |
| 95 | + $slug = 'portal'; | |
| 96 | + } | |
| 38 | 97 | |
| 98 | + if (defined('FLUENT_COMMUNITY_PORTAL_SLUG')) { | |
| 99 | + $slug = \FLUENT_COMMUNITY_PORTAL_SLUG; | |
| 100 | + } | |
| 101 | + | |
| 102 | + $slug = apply_filters('fluent_community/portal_slug', $slug); | |
| 103 | + | |
| 39 | 104 | if (!$forRoute) { |
| 40 | 105 | return $slug; |
| 41 | 106 | } |
| 42 | 107 | |
| 43 | - $siteUrl = get_site_url(); | |
| 108 | + $siteUrl = get_home_url(); | |
| 44 | 109 | |
| 45 | 110 | $poralUrl = self::baseUrl('/'); |
| 46 | 111 | |
| 47 | - // get the url without path | |
| 48 | - $siteUrl = str_replace(parse_url($siteUrl, PHP_URL_PATH), '', $siteUrl); | |
| 112 | + $urlPath = wp_parse_url($siteUrl, PHP_URL_PATH); | |
| 49 | 113 | |
| 114 | + if ($urlPath) { | |
| 115 | + // get the url without path | |
| 116 | + $siteUrl = str_replace($urlPath, '', $siteUrl); | |
| 117 | + } | |
| 118 | + | |
| 50 | 119 | $slug = str_replace($siteUrl, '', $poralUrl); |
| 51 | - | |
| 52 | 120 | // remove the first and last slashes |
| 53 | 121 | return trim($slug, '/'); |
| 54 | 122 | } |
| 55 | 123 | |
| @@ -84,15 +152,82 @@ | ||
| 84 | 152 | return apply_filters('fluent_community/has_color_scheme', $status); |
| 85 | 153 | } |
| 86 | 154 | |
| 87 | 155 | /** |
| 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. | |
| 156 | + * Admin default theme mode: 'light', 'dark', or 'system'. | |
| 92 | 157 | */ |
| 93 | - public static function isSiteAdmin($userId = null) | |
| 158 | + public static function getDefaultThemeMode() | |
| 94 | 159 | { |
| 160 | + $settings = Utility::getCustomizationSettings(); | |
| 161 | + $mode = isset($settings['default_theme_mode']) ? $settings['default_theme_mode'] : 'light'; | |
| 162 | + | |
| 163 | + if (!in_array($mode, ['light', 'dark', 'system'], true)) { | |
| 164 | + $mode = 'light'; | |
| 165 | + } | |
| 166 | + | |
| 167 | + return apply_filters('fluent_community/default_theme_mode', $mode); | |
| 168 | + } | |
| 169 | + | |
| 170 | + /** | |
| 171 | + * Pre-paint script that sets the theme before first render (no flash). | |
| 172 | + * Precedence mirrors runtime: host-theme cookie → user pick → admin default. | |
| 173 | + * Not persisted. Gate on hasColorScheme(). | |
| 174 | + */ | |
| 175 | + public static function renderColorSchemePrePaintScript() | |
| 176 | + { | |
| 177 | + $defaultMode = self::getDefaultThemeMode(); | |
| 178 | + $portalVars = apply_filters('fluent_community/general_portal_vars', ['color_switch_cookie_name' => '']); | |
| 179 | + $cookieName = isset($portalVars['color_switch_cookie_name']) ? $portalVars['color_switch_cookie_name'] : ''; | |
| 180 | + ?> | |
| 181 | + <script> | |
| 182 | + (function () { | |
| 183 | + var root = document.documentElement; | |
| 184 | + var cookieName = '<?php echo esc_js($cookieName); ?>'; | |
| 185 | + var mode = null; | |
| 186 | + | |
| 187 | + // host-theme integration cookie (Blocksy/Kadence) wins when present | |
| 188 | + if (cookieName) { | |
| 189 | + var safeName = cookieName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | |
| 190 | + var match = document.cookie.match('(?:^|; )' + safeName + '=([^;]*)'); | |
| 191 | + if (match) { | |
| 192 | + var cookieMode = decodeURIComponent(match[1]); | |
| 193 | + if (cookieMode === 'dark' || cookieMode === 'light') { | |
| 194 | + mode = cookieMode; | |
| 195 | + } | |
| 196 | + } | |
| 197 | + } | |
| 198 | + | |
| 199 | + // explicit user pick | |
| 200 | + if (!mode) { | |
| 201 | + try { | |
| 202 | + var stored = JSON.parse(localStorage.getItem('fcom_global_storage') || '{}').fcom_color_mode; | |
| 203 | + if (stored === 'dark' || stored === 'light') { | |
| 204 | + mode = stored; | |
| 205 | + } | |
| 206 | + } catch (error) {} | |
| 207 | + } | |
| 208 | + | |
| 209 | + // admin default | |
| 210 | + if (!mode) { | |
| 211 | + var defaultMode = '<?php echo esc_js($defaultMode); ?>'; | |
| 212 | + if (defaultMode === 'system') { | |
| 213 | + mode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; | |
| 214 | + } else { | |
| 215 | + mode = defaultMode; | |
| 216 | + } | |
| 217 | + } | |
| 218 | + | |
| 219 | + root.setAttribute('data-color-mode', mode === 'dark' ? 'dark' : 'light'); | |
| 220 | + if (mode === 'dark') { | |
| 221 | + root.classList.add('dark'); | |
| 222 | + } | |
| 223 | + })(); | |
| 224 | + </script> | |
| 225 | + <?php | |
| 226 | + } | |
| 227 | + | |
| 228 | + public static function isSuperAdmin($userId = null) | |
| 229 | + { | |
| 95 | 230 | $capability = apply_filters('fluent_community/super_admin_capability', 'manage_options'); |
| 96 | 231 | |
| 97 | 232 | if (!$capability) { |
| 98 | 233 | return false; |
| @@ -108,14 +243,37 @@ | ||
| 108 | 243 | |
| 109 | 244 | return user_can($userId, $capability); |
| 110 | 245 | } |
| 111 | 246 | |
| 247 | + /** | |
| 248 | + * Check if the user is a site admin. | |
| 249 | + * | |
| 250 | + * @param int|null $userId The user ID to check. If null, checks the current user. | |
| 251 | + * @param \FluentCommunity\App\Models\User|null $user Resolved user model, to save a lookup. | |
| 252 | + * @return bool True if the user is a site admin, false otherwise. | |
| 253 | + */ | |
| 254 | + public static function isSiteAdmin($userId = null, $user = null) | |
| 255 | + { | |
| 256 | + if (self::isSuperAdmin($userId)) { | |
| 257 | + return true; | |
| 258 | + } | |
| 259 | + | |
| 260 | + if (!$user) { | |
| 261 | + $user = ($userId && (int)$userId !== get_current_user_id()) | |
| 262 | + ? User::find($userId) | |
| 263 | + : self::getCurrentUser(); | |
| 264 | + } | |
| 265 | + | |
| 266 | + return $user && Arr::get($user->getPermissions(), 'community_admin'); | |
| 267 | + } | |
| 268 | + | |
| 112 | 269 | public static function isModerator($user = null) |
| 113 | 270 | { |
| 114 | 271 | if (!$user) { |
| 115 | 272 | $user = self::getCurrentUser(); |
| 116 | 273 | } |
| 117 | - return $user && $user->isCommunityModerator(); | |
| 274 | + | |
| 275 | + return $user && $user->hasCommunityModeratorAccess(); | |
| 118 | 276 | } |
| 119 | 277 | |
| 120 | 278 | /** |
| 121 | 279 | * Get the URL for an asset file. |
| @@ -194,8 +352,12 @@ | ||
| 194 | 352 | * @return bool True if the user is in the space, false otherwise. |
| 195 | 353 | */ |
| 196 | 354 | public static function isUserInSpace($userId, $spaceId) |
| 197 | 355 | { |
| 356 | + if (!$userId || !$spaceId) { | |
| 357 | + return false; | |
| 358 | + } | |
| 359 | + | |
| 198 | 360 | return SpaceUserPivot::where('user_id', $userId) |
| 199 | 361 | ->where('space_id', $spaceId) |
| 200 | 362 | ->where('status', 'active') |
| 201 | 363 | ->exists(); |
| @@ -257,8 +419,19 @@ | ||
| 257 | 419 | |
| 258 | 420 | return Media::where('media_key', $key)->first(); |
| 259 | 421 | } |
| 260 | 422 | |
| 423 | + public static function removeMediaByUrl($url = '', $subObjectId = null) | |
| 424 | + { | |
| 425 | + if (!$url || !$subObjectId) { | |
| 426 | + return; | |
| 427 | + } | |
| 428 | + | |
| 429 | + do_action('fluent_community/remove_medias_by_url', [$url], [ | |
| 430 | + 'sub_object_id' => $subObjectId, | |
| 431 | + ]); | |
| 432 | + } | |
| 433 | + | |
| 261 | 434 | /** |
| 262 | 435 | * Get media items from multiple URLs. |
| 263 | 436 | * |
| 264 | 437 | * @param array $urls An array of URLs. |
| @@ -299,24 +472,29 @@ | ||
| 299 | 472 | 'site_title' => get_bloginfo('name'), |
| 300 | 473 | 'slug' => 'portal', |
| 301 | 474 | 'logo' => '', |
| 302 | 475 | 'white_logo' => '', |
| 476 | + 'logo_permalink_type' => 'default', | |
| 477 | + 'logo_permalink' => '', | |
| 303 | 478 | 'featured_image' => '', |
| 304 | 479 | 'access' => [ |
| 305 | - 'acess_level' => 'logged_in', // logged_in, public, role_based | |
| 480 | + 'acess_level' => 'public', // logged_in, public, role_based | |
| 306 | 481 | 'access_roles' => [] |
| 307 | 482 | ], |
| 308 | 483 | 'auth_form_type' => 'default', |
| 484 | + 'explicit_registration' => 'no', | |
| 309 | 485 | 'disable_global_posts' => 'yes', |
| 310 | 486 | 'auth_content' => 'Please login first to access this page', |
| 311 | 487 | 'auth_redirect' => '', |
| 312 | - 'restricted_role_content' => 'Sorry, you can not access to this page. Only authorized users can access this page.', | |
| 488 | + 'restricted_role_content' => 'Sorry, you cannot access this page. Only authorized users can access this page.', | |
| 313 | 489 | 'auth_url' => '', |
| 314 | - 'cutsom_auth_url' => self::baseUrl('?fcom_action=auth'), | |
| 490 | + 'cutsom_auth_url' => self::baseUrl('?fcom_action=auth'), | |
| 491 | + 'use_custom_signup_page' => 'no', | |
| 492 | + 'custom_signup_url' => '' | |
| 315 | 493 | ]; |
| 316 | 494 | |
| 317 | 495 | $settings = wp_parse_args($settings, $defaults); |
| 318 | - if($settings['auth_form_type'] != 'custom' || empty($settings['auth_form_type'])) { | |
| 496 | + if ($settings['auth_form_type'] != 'custom' || empty($settings['auth_form_type'])) { | |
| 319 | 497 | $settings['cutsom_auth_url'] = self::baseUrl('?fcom_action=auth'); |
| 320 | 498 | } |
| 321 | 499 | |
| 322 | 500 | if (defined('FLUENT_COMMUNITY_PORTAL_SLUG')) { |
| @@ -342,9 +520,9 @@ | ||
| 342 | 520 | * |
| 343 | 521 | * @param int|null $userId The user ID. If null, uses the current user. |
| 344 | 522 | * @return bool True if the user can access the portal, false otherwise. |
| 345 | 523 | */ |
| 346 | - public static function canAccessPortal($userId = null) | |
| 524 | + public static function canAccessPortal($userId = null, $requireActiveProfile = true) | |
| 347 | 525 | { |
| 348 | 526 | $settings = self::generalSettings(); |
| 349 | 527 | $accessLevel = Arr::get($settings, 'access.acess_level'); |
| 350 | 528 | |
| @@ -381,8 +559,12 @@ | ||
| 381 | 559 | if (!$result) { |
| 382 | 560 | return apply_filters('fluent_community/can_access_portal', false); |
| 383 | 561 | } |
| 384 | 562 | |
| 563 | + if (!$requireActiveProfile) { | |
| 564 | + return apply_filters('fluent_community/can_access_portal', true); | |
| 565 | + } | |
| 566 | + | |
| 385 | 567 | $xProfile = Helper::getCurrentProfile(); |
| 386 | 568 | |
| 387 | 569 | $result = $xProfile && $xProfile->status == 'active'; |
| 388 | 570 | |
| @@ -397,19 +579,22 @@ | ||
| 397 | 579 | public static function portalRoutePaths() |
| 398 | 580 | { |
| 399 | 581 | return apply_filters('fluent_community/app_route_paths', [ |
| 400 | 582 | 'portal_home', |
| 583 | + 'members', | |
| 584 | + 'bookmarks', | |
| 585 | + 'chat', | |
| 586 | + 'dashboard', | |
| 587 | + 'leaderboards', | |
| 588 | + 'notifications', | |
| 401 | 589 | 'space', |
| 402 | 590 | 'discover', |
| 403 | - 'members', | |
| 404 | 591 | 'courses', |
| 405 | 592 | 'u', |
| 406 | - 'leaderboards', | |
| 407 | - 'chat', | |
| 408 | - 'notifications', | |
| 409 | - 'bookmarks', | |
| 410 | 593 | 'post', |
| 411 | - 'admin' | |
| 594 | + 'admin', | |
| 595 | + 'course', | |
| 596 | + 'site-maps' | |
| 412 | 597 | ]); |
| 413 | 598 | } |
| 414 | 599 | |
| 415 | 600 | /** |
| @@ -419,21 +604,18 @@ | ||
| 419 | 604 | * @return XProfile|null The user's profile or null if not found. |
| 420 | 605 | */ |
| 421 | 606 | public static function getCurrentProfile($cached = true) |
| 422 | 607 | { |
| 423 | - $userId = get_current_user_id(); | |
| 424 | - if (!$userId) { | |
| 425 | - return null; | |
| 426 | - } | |
| 427 | - | |
| 428 | 608 | static $profile; |
| 429 | - | |
| 430 | 609 | if ($profile && $cached) { |
| 431 | 610 | return $profile; |
| 432 | 611 | } |
| 433 | 612 | |
| 613 | + $userId = get_current_user_id(); | |
| 614 | + | |
| 434 | 615 | if (!$userId) { |
| 435 | - return null; | |
| 616 | + $profile = null; | |
| 617 | + return $profile; | |
| 436 | 618 | } |
| 437 | 619 | |
| 438 | 620 | $profile = XProfile::where('user_id', $userId)->first(); |
| 439 | 621 | |
| @@ -452,16 +634,14 @@ | ||
| 452 | 634 | if (!$userId) { |
| 453 | 635 | return false; |
| 454 | 636 | } |
| 455 | 637 | |
| 456 | - static $user; | |
| 457 | - if ($user && $cached) { | |
| 458 | - return $user; | |
| 638 | + static $users = []; | |
| 639 | + if ($cached && isset($users[$userId])) { | |
| 640 | + return $users[$userId]; | |
| 459 | 641 | } |
| 460 | 642 | |
| 461 | - $user = User::find($userId); | |
| 462 | - | |
| 463 | - return $user; | |
| 643 | + return $users[$userId] = User::find($userId); | |
| 464 | 644 | } |
| 465 | 645 | |
| 466 | 646 | /** |
| 467 | 647 | * Get the route paths for the community. |
| @@ -573,8 +753,31 @@ | ||
| 573 | 753 | return false; |
| 574 | 754 | } |
| 575 | 755 | |
| 576 | 756 | /** |
| 757 | + * Sanitize embed markup held in a feed/comment meta array on read. | |
| 758 | + * | |
| 759 | + * meta.media_preview.html is rendered with v-html in _MediaPreview.vue, so it is | |
| 760 | + * sanitized on write. Doing it on read as well neutralizes rows that were stored | |
| 761 | + * before the write-side fix landed, and covers any writer reaching the meta via | |
| 762 | + * the fluent_community/feed/* filters. The emptiness check keeps this free for the | |
| 763 | + * vast majority of rows, which carry no embed markup at all. | |
| 764 | + * | |
| 765 | + * @param array $meta The unserialized meta array. | |
| 766 | + * @return array The meta array with any embed markup passed through the allowlist. | |
| 767 | + */ | |
| 768 | + public static function sanitizeStoredMediaPreview($meta) | |
| 769 | + { | |
| 770 | + if (empty($meta['media_preview']['html'])) { | |
| 771 | + return $meta; | |
| 772 | + } | |
| 773 | + | |
| 774 | + $meta['media_preview']['html'] = RemoteUrlParser::sanitizeOembedHtml($meta['media_preview']['html']); | |
| 775 | + | |
| 776 | + return $meta; | |
| 777 | + } | |
| 778 | + | |
| 779 | + /** | |
| 577 | 780 | * Get a human-readable excerpt from content. |
| 578 | 781 | * |
| 579 | 782 | * @param string $content The content to extract from. |
| 580 | 783 | * @param int $length The maximum length of the excerpt. |
| @@ -595,12 +798,12 @@ | ||
| 595 | 798 | // Blockquotes: remove '>' symbol |
| 596 | 799 | '/^\s*>\s?/m' => '', |
| 597 | 800 | // Horizontal rules: replace with empty line |
| 598 | 801 | '/^\s*([-*_])\1{2,}\s*$/m' => "\n", |
| 802 | + // Images: keep only the alt text (run before links) | |
| 803 | + '/!\[([^\]]*)\]\([^\)]+\)/' => '$1', | |
| 599 | 804 | // Links: keep only the link text |
| 600 | - '/\[([^\]]+)\]\([^\)]+\)/' => '$1', | |
| 601 | - // Images: keep only the alt text | |
| 602 | - '/!\[([^\]]+)\]\([^\)]+\)/' => '$1', | |
| 805 | + '/\[([^\]]*)\]\([^\)]+\)/' => '$1', | |
| 603 | 806 | // Strikethrough: remove '~~' symbols |
| 604 | 807 | '/~~(.*?)~~/' => '$1', |
| 605 | 808 | // Task lists: remove checkbox syntax |
| 606 | 809 | '/^\s*[-*+]\s+\[[ xX]\]\s+/m' => '', |
| @@ -607,8 +810,10 @@ | ||
| 607 | 810 | ]; |
| 608 | 811 | |
| 609 | 812 | $content = preg_replace(array_keys($patterns), array_values($patterns), $content); |
| 610 | 813 | |
| 814 | + $content = html_entity_decode($content, ENT_QUOTES | ENT_HTML5, 'UTF-8'); | |
| 815 | + | |
| 611 | 816 | // remove all tags |
| 612 | 817 | $content = wp_strip_all_tags($content); |
| 613 | 818 | // remove new lines and tabs |
| 614 | 819 | $content = str_replace(["\r", "\n", "\t"], ' ', $content); |
| @@ -671,9 +876,9 @@ | ||
| 671 | 876 | * |
| 672 | 877 | * @param User|null $user The user to get menu groups for. |
| 673 | 878 | * @return array The community menu groups. |
| 674 | 879 | */ |
| 675 | - public static function getCommunityMenuGroups($user = null) | |
| 880 | + public static function getCommunityMenuGroups($user = null, $view = true) | |
| 676 | 881 | { |
| 677 | 882 | if (!$user) { |
| 678 | 883 | $user = self::getCurrentUser(); |
| 679 | 884 | } |
| @@ -683,52 +888,74 @@ | ||
| 683 | 888 | if ($communityGroups->isEmpty()) { |
| 684 | 889 | return []; |
| 685 | 890 | } |
| 686 | 891 | |
| 687 | - $isMod = $user && $user->isCommunityModerator(); | |
| 892 | + $userSpaceIds = $user ? self::getUserSpaceIds($user->ID) : []; | |
| 893 | + $isComModerator = $user && $user->hasCommunityModeratorAccess(); | |
| 894 | + $isCourseCreator = $user && $user->hasCourseCreatorAccess(); | |
| 895 | + $isSpaceModerator = $user && $user->isSpaceModerator(); | |
| 688 | 896 | |
| 689 | 897 | $formattedGroups = []; |
| 690 | - | |
| 691 | 898 | foreach ($communityGroups as $communityGroup) { |
| 899 | + $validSpaces = []; | |
| 692 | 900 | $spaces = $communityGroup->spaces; |
| 693 | - $validSpaces = []; | |
| 694 | 901 | $isShowAll = Arr::get($communityGroup->settings, 'always_show_spaces') === 'yes'; |
| 695 | 902 | |
| 696 | - foreach ($spaces as $space) { | |
| 697 | - if ($isMod) { | |
| 698 | - $validSpaces[] = self::transformSpaceToLink($space); | |
| 903 | + if (!$isShowAll && !$isSpaceModerator) { | |
| 904 | + $spaceIds = $spaces->pluck('id')->toArray(); | |
| 905 | + $isNotMemberOfAnySpace = empty(array_intersect($spaceIds, $userSpaceIds)); | |
| 906 | + if ($isNotMemberOfAnySpace) { | |
| 699 | 907 | continue; |
| 700 | 908 | } |
| 909 | + } | |
| 701 | 910 | |
| 702 | - if ($space->privacy === 'secret') { | |
| 703 | - if (!$user || !$space->getMembership($user->ID)) { | |
| 704 | - continue; | |
| 705 | - } | |
| 706 | - $validSpaces[] = self::transformSpaceToLink($space); | |
| 911 | + if ($user) { | |
| 912 | + BaseSpace::preloadMemberships($spaces, $user->ID); | |
| 913 | + } | |
| 914 | + | |
| 915 | + foreach ($spaces as $space) { | |
| 916 | + $validSpace = $view ? self::transformSpaceToLink($space, $user) : $space; | |
| 917 | + if (!$validSpace) { | |
| 707 | 918 | continue; |
| 708 | 919 | } |
| 709 | 920 | |
| 710 | - if ($isShowAll || $space->privacy = 'public') { | |
| 711 | - $validSpace = self::transformSpaceToLink($space); | |
| 921 | + if ($user && $space->isContentSpace()) { | |
| 922 | + $validSpace['unread_badge'] = self::getUnreadFeedsCounts($space->id); | |
| 923 | + } | |
| 712 | 924 | |
| 713 | - if ($space->privacy == 'private') { | |
| 714 | - if (!$user || !$space->getMembership($user->ID)) { | |
| 715 | - $validSpace['show_lock'] = true; | |
| 716 | - } | |
| 717 | - } | |
| 925 | + if ($isComModerator && $space->type != 'course') { | |
| 926 | + $validSpaces[] = $validSpace; | |
| 927 | + continue; | |
| 928 | + } | |
| 718 | 929 | |
| 930 | + if ($isCourseCreator && $space->type == 'course') { | |
| 719 | 931 | $validSpaces[] = $validSpace; |
| 720 | 932 | continue; |
| 721 | 933 | } |
| 722 | 934 | |
| 723 | - if (!$user || $space->getMembership($user->ID)) { | |
| 935 | + if ($space->privacy == 'public') { | |
| 936 | + $validSpaces[] = $validSpace; | |
| 724 | 937 | continue; |
| 725 | 938 | } |
| 726 | 939 | |
| 727 | - $validSpaces[] = self::transformSpaceToLink($space); | |
| 940 | + $hasMembership = $user && $space->getMembership($user->ID); | |
| 941 | + | |
| 942 | + if ($space->privacy == 'private') { | |
| 943 | + if (!$user || !$hasMembership) { | |
| 944 | + $validSpace['show_lock'] = true; | |
| 945 | + } | |
| 946 | + } | |
| 947 | + | |
| 948 | + if ($space->privacy == 'secret') { | |
| 949 | + if (!$user || !$hasMembership) { | |
| 950 | + continue; | |
| 951 | + } | |
| 952 | + } | |
| 953 | + | |
| 954 | + $validSpaces[] = $validSpace; | |
| 728 | 955 | } |
| 729 | 956 | |
| 730 | - if (!$validSpaces && !$isMod) { | |
| 957 | + if (!$validSpaces && !$isSpaceModerator) { | |
| 731 | 958 | continue; |
| 732 | 959 | } |
| 733 | 960 | |
| 734 | 961 | $formattedGroups[] = [ |
| @@ -742,19 +969,55 @@ | ||
| 742 | 969 | |
| 743 | 970 | return apply_filters('fluent_community/menu_groups_for_user', $formattedGroups, $user); |
| 744 | 971 | } |
| 745 | 972 | |
| 973 | + public static function getUnreadFeedsCounts($spaceId, $force = false) | |
| 974 | + { | |
| 975 | + static $coutsCache = null; | |
| 976 | + if ($coutsCache !== null && !$force) { | |
| 977 | + return Arr::get((array)$coutsCache, $spaceId); | |
| 978 | + } | |
| 979 | + | |
| 980 | + $xprofile = self::getCurrentProfile(); | |
| 981 | + | |
| 982 | + if (!$xprofile || !$xprofile->last_activity) { | |
| 983 | + return 0; | |
| 984 | + } | |
| 985 | + | |
| 986 | + $lastActivityDate = gmdate('Y-m-d H:i:s', strtotime($xprofile->last_activity) - 300); | |
| 987 | + | |
| 988 | + $lastActivityDate = apply_filters('fluent_community/last_activity_date_for_unread_feeds', $lastActivityDate, $xprofile); | |
| 989 | + | |
| 990 | + $unreadCounts = Feed::query() | |
| 991 | + ->select('space_id', Utility::getApp('db')->raw('COUNT(*) as feed_count')) | |
| 992 | + ->where('status', 'published') | |
| 993 | + ->where('created_at', '>', $lastActivityDate) | |
| 994 | + ->groupBy('space_id') | |
| 995 | + ->get(); | |
| 996 | + | |
| 997 | + $coutsCache = []; | |
| 998 | + foreach ($unreadCounts as $unreadCount) { | |
| 999 | + $coutsCache[$unreadCount->space_id] = $unreadCount->feed_count > 10 ? '10+' : $unreadCount->feed_count; | |
| 1000 | + } | |
| 1001 | + | |
| 1002 | + return Arr::get($coutsCache, $spaceId); | |
| 1003 | + } | |
| 1004 | + | |
| 746 | 1005 | /** |
| 747 | 1006 | * Transform a space to a link array. |
| 748 | 1007 | * |
| 749 | 1008 | * @param Space $space The space to transform. |
| 750 | - * @return array The transformed space link array. | |
| 1009 | + * @param User|null $user The user to check permissions for. | |
| 1010 | + * @return array|null The transformed space link array. | |
| 751 | 1011 | */ |
| 752 | - private static function transformSpaceToLink($space) | |
| 1012 | + private static function transformSpaceToLink($space, $user = null) | |
| 753 | 1013 | { |
| 1014 | + $isCustomLink = $space->type == 'sidebar_link'; | |
| 1015 | + if ($isCustomLink && !self::canViewSideLinkLink($space, $user)) { | |
| 1016 | + return null; | |
| 1017 | + } | |
| 754 | 1018 | |
| 755 | 1019 | $logo = $space->logo; |
| 756 | - | |
| 757 | 1020 | $title = $space->title; |
| 758 | 1021 | |
| 759 | 1022 | if ($space->status == 'draft') { |
| 760 | 1023 | $title = $title . ' ' . __('(Draft)', 'fluent-community'); |
| @@ -765,12 +1028,44 @@ | ||
| 765 | 1028 | 'icon_image' => $logo, |
| 766 | 1029 | 'shape_svg' => !$logo ? Arr::get($space->settings, 'shape_svg', '') : '', |
| 767 | 1030 | 'emoji' => !$logo ? Arr::get($space->settings, 'emoji', '') : '', |
| 768 | 1031 | 'permalink' => $space->getPermalink(), |
| 1032 | + 'is_custom' => $isCustomLink ? 'yes' : 'no', | |
| 1033 | + 'new_tab' => ($isCustomLink && Arr::get($space, 'settings.new_tab', 'no') === 'yes') ? 'yes' : 'no', | |
| 769 | 1034 | 'link_classes' => 'space_menu_item route_url fcom_space_id_' . $space->id . ' fcom_space_' . $space->slug |
| 770 | 1035 | ]; |
| 771 | 1036 | } |
| 772 | 1037 | |
| 1038 | + public static function canViewSideLinkLink($space, $user = null) | |
| 1039 | + { | |
| 1040 | + $privacy = $space->privacy; | |
| 1041 | + | |
| 1042 | + if ($privacy == 'public') { | |
| 1043 | + return true; | |
| 1044 | + } | |
| 1045 | + | |
| 1046 | + if ($privacy == 'logged_in') { | |
| 1047 | + return !!$user; | |
| 1048 | + } | |
| 1049 | + | |
| 1050 | + if ($privacy == 'logged_out_only') { | |
| 1051 | + return !$user; | |
| 1052 | + } | |
| 1053 | + | |
| 1054 | + if (!$user) { | |
| 1055 | + return false; | |
| 1056 | + } | |
| 1057 | + | |
| 1058 | + $accessIds = Arr::get($space->settings, 'membership_ids', []); | |
| 1059 | + | |
| 1060 | + if (!$accessIds) { | |
| 1061 | + return true; | |
| 1062 | + } | |
| 1063 | + | |
| 1064 | + $userSpaces = $user->getSpaceIds(); | |
| 1065 | + return !!array_intersect($userSpaces, $accessIds); | |
| 1066 | + } | |
| 1067 | + | |
| 773 | 1068 | public static function isAlreadyOnboarded() |
| 774 | 1069 | { |
| 775 | 1070 | $communitySettings = get_option('fluent_community_settings', []); |
| 776 | 1071 | |
| @@ -842,12 +1137,14 @@ | ||
| 842 | 1137 | if ($menuGroups && $context === 'view') { |
| 843 | 1138 | return $menuGroups; |
| 844 | 1139 | } |
| 845 | 1140 | |
| 846 | - $menuGroups = Utility::getOption('fluent_community_menu_groups', []); | |
| 1141 | + $menuGroups = (array) Utility::getOption('fluent_community_menu_groups', []); | |
| 847 | 1142 | |
| 848 | 1143 | $membersPageStatus = Utility::canViewMembersPage() ? 'yes' : 'no'; |
| 849 | 1144 | |
| 1145 | + $leaderboardPageVisibility = (Utility::canViewLeaderboardMembers() || is_user_logged_in()) ? 'yes' : 'no'; | |
| 1146 | + | |
| 850 | 1147 | $defaultMainMenuItems = [ |
| 851 | 1148 | 'all_feeds' => [ |
| 852 | 1149 | 'slug' => 'all_feeds', |
| 853 | 1150 | 'title' => __('Feed', 'fluent-community'), |
| @@ -869,9 +1166,9 @@ | ||
| 869 | 1166 | 'shape_svg' => '<svg version="1.1" viewBox="0 0 128 128" xml:space="preserve"><g><path d="M64,42c-13.2,0-24,10.8-24,24s10.8,24,24,24s24-10.8,24-24S77.2,42,64,42z M64,82c-8.8,0-16-7.2-16-16s7.2-16,16-16 s16,7.2,16,16S72.8,82,64,82z"/><path d="M64,100.8c-14.9,0-29.2,6.2-39.4,17.1l-2.7,2.9l5.8,5.5l2.7-2.9c8.8-9.4,20.7-14.6,33.6-14.6s24.8,5.2,33.6,14.6l2.7,2.9 l5.8-5.5l-2.7-2.9C93.2,107.1,78.9,100.8,64,100.8z"/><path d="M97,47.9v8c9.4,0,18.1,3.8,24.6,10.7l5.8-5.5C119.6,52.7,108.5,47.9,97,47.9z"/><path d="M116.1,20c0-10.5-8.6-19.1-19.1-19.1S77.9,9.5,77.9,20S86.5,39.1,97,39.1S116.1,30.5,116.1,20z M85.9,20 c0-6.1,5-11.1,11.1-11.1s11.1,5,11.1,11.1s-5,11.1-11.1,11.1S85.9,26.1,85.9,20z"/><path d="M31,47.9c-11.5,0-22.6,4.8-30.4,13.2l5.8,5.5c6.4-6.9,15.2-10.7,24.6-10.7V47.9z"/><path d="M50.1,20C50.1,9.5,41.5,0.9,31,0.9S11.9,9.5,11.9,20S20.5,39.1,31,39.1S50.1,30.5,50.1,20z M31,31.1 c-6.1,0-11.1-5-11.1-11.1S24.9,8.9,31,8.9s11.1,5,11.1,11.1S37.1,31.1,31,31.1z"/></g></svg>' |
| 870 | 1167 | ], |
| 871 | 1168 | 'all_courses' => [ |
| 872 | 1169 | 'slug' => 'all_courses', |
| 873 | - 'title' => 'Courses', | |
| 1170 | + 'title' => __('Courses', 'fluent-community'), | |
| 874 | 1171 | 'link_classes' => 'fcom_courses route_url', |
| 875 | 1172 | 'is_system' => 'yes', |
| 876 | 1173 | 'is_locked' => 'yes', |
| 877 | 1174 | 'enabled' => 'yes', |
| @@ -893,10 +1190,10 @@ | ||
| 893 | 1190 | 'leaderboard' => [ |
| 894 | 1191 | 'slug' => 'leaderboard', |
| 895 | 1192 | 'is_system' => 'yes', |
| 896 | 1193 | 'is_locked' => 'yes', |
| 897 | - 'enabled' => 'yes', | |
| 898 | - 'is_unavailable' => self::isFeatureEnabled('leader_board_module') ? 'no' : 'yes', | |
| 1194 | + 'enabled' => $leaderboardPageVisibility, | |
| 1195 | + 'is_unavailable' => self::isFeatureEnabled('leader_board_module') && $leaderboardPageVisibility == 'yes' ? 'no' : 'yes', | |
| 899 | 1196 | 'title' => __('Leaderboard', 'fluent-community'), |
| 900 | 1197 | 'link_classes' => 'fcom_leaderboards route_url', |
| 901 | 1198 | 'permalink' => self::baseUrl('leaderboards'), |
| 902 | 1199 | '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>' |
| @@ -917,9 +1214,9 @@ | ||
| 917 | 1214 | continue; |
| 918 | 1215 | } |
| 919 | 1216 | $defaultItem = Arr::get($defaultMainMenuItems, $item['slug'], []); |
| 920 | 1217 | if ($defaultItem) { |
| 921 | - $preservedKeys = ['is_system', 'is_locked', 'is_unavailable', 'slug', 'shape_svg']; | |
| 1218 | + $preservedKeys = ['is_system', 'is_locked', 'is_unavailable', 'slug']; | |
| 922 | 1219 | foreach ($preservedKeys as $key) { |
| 923 | 1220 | if (isset($defaultItem[$key])) { |
| 924 | 1221 | $item[$key] = Arr::get($defaultItem, $key); |
| 925 | 1222 | } |
| @@ -925,11 +1222,12 @@ | ||
| 925 | 1222 | } |
| 926 | 1223 | } |
| 927 | 1224 | if (Arr::get($defaultItem, 'is_system') === 'yes') { |
| 928 | 1225 | $item['permalink'] = $defaultItem['permalink']; |
| 929 | - $item['emoji'] = ''; | |
| 930 | - $item['icon_image'] = ''; | |
| 931 | 1226 | $item['link_classes'] = $defaultItem['link_classes']; |
| 1227 | + if (empty($item['shape_svg'])) { | |
| 1228 | + $item['shape_svg'] = $defaultItem['shape_svg']; | |
| 1229 | + } | |
| 932 | 1230 | } |
| 933 | 1231 | } |
| 934 | 1232 | } |
| 935 | 1233 | } else { |
| @@ -968,11 +1266,9 @@ | ||
| 968 | 1266 | |
| 969 | 1267 | $profileDropDownItems = Arr::get($menuGroups, 'profileDropdownItems', []); |
| 970 | 1268 | |
| 971 | 1269 | if ($profileDropDownItems && is_array($profileDropDownItems)) { |
| 972 | - | |
| 973 | 1270 | unset($profileDropDownItems['profile']); |
| 974 | - | |
| 975 | 1271 | foreach ($profileDropDownItems as $index => &$item) { |
| 976 | 1272 | if (empty($item['slug'])) { |
| 977 | 1273 | unset($profileDropDownItems[$index]); |
| 978 | 1274 | continue; |
| @@ -978,9 +1274,9 @@ | ||
| 978 | 1274 | continue; |
| 979 | 1275 | } |
| 980 | 1276 | $defaultItem = Arr::get($defaultProfileDropDownItems, $item['slug'], []); |
| 981 | 1277 | if ($defaultItem) { |
| 982 | - $preservedKeys = ['is_system', 'is_locked', 'is_unavailable', 'slug', 'svg_icon']; | |
| 1278 | + $preservedKeys = ['is_system', 'is_locked', 'is_unavailable', 'slug']; | |
| 983 | 1279 | foreach ($preservedKeys as $key) { |
| 984 | 1280 | if (isset($defaultItem[$key])) { |
| 985 | 1281 | $item[$key] = Arr::get($defaultItem, $key); |
| 986 | 1282 | } |
| @@ -1008,18 +1304,21 @@ | ||
| 1008 | 1304 | $afterCommunityMenuGroups = []; |
| 1009 | 1305 | } |
| 1010 | 1306 | |
| 1011 | 1307 | if ($context == 'view') { |
| 1012 | - $mainItems = array_filter($mainItems, function ($item) { | |
| 1013 | - return Arr::get($item, 'enabled') === 'yes' && Arr::get($item, 'is_unavailable') !== 'yes'; | |
| 1308 | + | |
| 1309 | + $currentUser = self::getCurrentUser(); | |
| 1310 | + | |
| 1311 | + $mainItems = array_filter($mainItems, function ($item) use ($currentUser) { | |
| 1312 | + return self::isLinkAccessible($item, $currentUser); | |
| 1014 | 1313 | }); |
| 1015 | 1314 | |
| 1016 | - $profileDropDownItems = array_filter($profileDropDownItems, function ($item) { | |
| 1017 | - return Arr::get($item, 'enabled') === 'yes' && Arr::get($item, 'is_unavailable') !== 'yes'; | |
| 1315 | + $profileDropDownItems = array_filter($profileDropDownItems, function ($item) use ($currentUser) { | |
| 1316 | + return self::isLinkAccessible($item, $currentUser); | |
| 1018 | 1317 | }); |
| 1019 | 1318 | |
| 1020 | - $beforeCommunityMenuItems = array_filter($beforeCommunityMenuItems, function ($item) { | |
| 1021 | - return Arr::get($item, 'enabled') === 'yes' && Arr::get($item, 'is_unavailable') !== 'yes'; | |
| 1319 | + $beforeCommunityMenuItems = array_filter($beforeCommunityMenuItems, function ($item) use ($currentUser) { | |
| 1320 | + return self::isLinkAccessible($item, $currentUser); | |
| 1022 | 1321 | }); |
| 1023 | 1322 | |
| 1024 | 1323 | $validGroups = []; |
| 1025 | 1324 | foreach ($afterCommunityMenuGroups as $group) { |
| @@ -1026,10 +1325,10 @@ | ||
| 1026 | 1325 | if (empty($group['items']) || !is_array($group['items'])) { |
| 1027 | 1326 | continue; |
| 1028 | 1327 | } |
| 1029 | 1328 | |
| 1030 | - $group['items'] = array_filter($group['items'], function ($item) { | |
| 1031 | - return Arr::get($item, 'enabled') === 'yes' && Arr::get($item, 'is_unavailable') !== 'yes'; | |
| 1329 | + $group['items'] = array_filter($group['items'], function ($item) use ($currentUser) { | |
| 1330 | + return self::isLinkAccessible($item, $currentUser); | |
| 1032 | 1331 | }); |
| 1033 | 1332 | |
| 1034 | 1333 | if ($group['items']) { |
| 1035 | 1334 | $validGroups[] = $group; |
| @@ -1051,8 +1350,72 @@ | ||
| 1051 | 1350 | return $menuGroups; |
| 1052 | 1351 | } |
| 1053 | 1352 | |
| 1054 | 1353 | /** |
| 1354 | + * Drop the links the given user may not see. | |
| 1355 | + * | |
| 1356 | + * Space links carry their own privacy, so every place that hands a space's settings | |
| 1357 | + * to a client has to filter them. Doing that inline is how the feed endpoints came | |
| 1358 | + * to skip it, so both call sites go through here. | |
| 1359 | + * | |
| 1360 | + * @param array $links | |
| 1361 | + * @param \FluentCommunity\App\Models\User|null $currentUser | |
| 1362 | + * @return array | |
| 1363 | + */ | |
| 1364 | + public static function filterAccessibleLinks($links, $currentUser = null) | |
| 1365 | + { | |
| 1366 | + if (!$links || !is_array($links)) { | |
| 1367 | + return []; | |
| 1368 | + } | |
| 1369 | + | |
| 1370 | + return array_values(array_filter($links, function ($link) use ($currentUser) { | |
| 1371 | + return self::isLinkAccessible($link, $currentUser); | |
| 1372 | + })); | |
| 1373 | + } | |
| 1374 | + | |
| 1375 | + public static function isLinkAccessible($link, $currentUser = null) | |
| 1376 | + { | |
| 1377 | + $isEnabled = Arr::get($link, 'enabled', 'yes') === 'yes'; | |
| 1378 | + $isUnavailable = Arr::get($link, 'is_unavailable') === 'yes'; | |
| 1379 | + | |
| 1380 | + if (!$isEnabled || $isUnavailable) { | |
| 1381 | + return false; | |
| 1382 | + } | |
| 1383 | + | |
| 1384 | + $privacy = Arr::get($link, 'privacy', ''); | |
| 1385 | + | |
| 1386 | + if (!$privacy || $privacy === 'public') { | |
| 1387 | + return true; | |
| 1388 | + } | |
| 1389 | + | |
| 1390 | + if ($privacy == 'logged_in') { | |
| 1391 | + return !!$currentUser; | |
| 1392 | + } | |
| 1393 | + | |
| 1394 | + if ($privacy == 'logged_out_only') { | |
| 1395 | + return !$currentUser; | |
| 1396 | + } | |
| 1397 | + | |
| 1398 | + $membershipIds = Arr::get($link, 'membership_ids', []); | |
| 1399 | + if (!$membershipIds) { | |
| 1400 | + return true; | |
| 1401 | + } | |
| 1402 | + | |
| 1403 | + if (!$currentUser) { | |
| 1404 | + return false; | |
| 1405 | + } | |
| 1406 | + | |
| 1407 | + static $userSpacesIds = []; | |
| 1408 | + if (!isset($userSpacesIds[$currentUser->ID])) { | |
| 1409 | + $userSpacesIds[$currentUser->ID] = $currentUser->getJoinedSpaceIds(); | |
| 1410 | + } | |
| 1411 | + | |
| 1412 | + $ids = $userSpacesIds[$currentUser->ID]; | |
| 1413 | + | |
| 1414 | + return $ids && !!array_intersect($ids, $membershipIds); | |
| 1415 | + } | |
| 1416 | + | |
| 1417 | + /** | |
| 1055 | 1418 | * Get the meta data for a space. |
| 1056 | 1419 | * |
| 1057 | 1420 | * @param int $spaceId The ID of the space. |
| 1058 | 1421 | * @param string $key The meta key. |
| @@ -1094,9 +1457,9 @@ | ||
| 1094 | 1457 | } else { |
| 1095 | 1458 | $meta = Meta::create([ |
| 1096 | 1459 | 'object_type' => 'space', |
| 1097 | 1460 | 'object_id' => $spaceId, |
| 1098 | - 'meta_key' => $key, | |
| 1461 | + 'meta_key' => $key, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key | |
| 1099 | 1462 | 'value' => $value |
| 1100 | 1463 | ]); |
| 1101 | 1464 | } |
| 1102 | 1465 | |
| @@ -1169,60 +1532,58 @@ | ||
| 1169 | 1532 | * @return array The welcome banner configuration. |
| 1170 | 1533 | */ |
| 1171 | 1534 | public static function getWelcomeBannerSettings() |
| 1172 | 1535 | { |
| 1173 | - return Utility::getFromCache('welcome_banner_settings', function () { | |
| 1174 | - $defaults = [ | |
| 1175 | - 'login' => [ | |
| 1176 | - 'enabled' => 'no', | |
| 1177 | - 'description' => '', | |
| 1178 | - 'mediaType' => 'image', | |
| 1179 | - 'allowClose' => 'no', | |
| 1180 | - 'bannerImage' => '', | |
| 1181 | - 'bannerVideo' => [ | |
| 1182 | - 'type' => 'oembed', | |
| 1183 | - 'url' => '', | |
| 1184 | - 'content_type' => '', | |
| 1185 | - 'provider' => '', | |
| 1186 | - 'title' => '', | |
| 1187 | - 'author_name' => '', | |
| 1188 | - 'html' => '' | |
| 1189 | - ], | |
| 1190 | - 'ctaButtons' => [] | |
| 1536 | + $defaults = [ | |
| 1537 | + 'login' => [ | |
| 1538 | + 'enabled' => 'no', | |
| 1539 | + 'description' => '', | |
| 1540 | + 'mediaType' => 'image', | |
| 1541 | + 'allowClose' => 'no', | |
| 1542 | + 'bannerImage' => '', | |
| 1543 | + 'bannerVideo' => [ | |
| 1544 | + 'type' => 'oembed', | |
| 1545 | + 'url' => '', | |
| 1546 | + 'content_type' => '', | |
| 1547 | + 'provider' => '', | |
| 1548 | + 'title' => '', | |
| 1549 | + 'author_name' => '', | |
| 1550 | + 'html' => '' | |
| 1191 | 1551 | ], |
| 1192 | - 'logout' => [ | |
| 1193 | - 'enabled' => 'no', | |
| 1194 | - 'description' => '', | |
| 1195 | - 'mediaType' => 'image', | |
| 1196 | - 'useCustomUrl' => 'no', | |
| 1197 | - 'bannerImage' => '', | |
| 1198 | - 'bannerVideo' => [ | |
| 1199 | - 'type' => 'oembed', | |
| 1200 | - 'url' => '', | |
| 1201 | - 'content_type' => '', | |
| 1202 | - 'provider' => '', | |
| 1203 | - 'title' => '', | |
| 1204 | - 'author_name' => '', | |
| 1205 | - 'html' => '' | |
| 1206 | - ], | |
| 1207 | - 'ctaButtons' => [] | |
| 1208 | - ] | |
| 1209 | - ]; | |
| 1552 | + 'ctaButtons' => [] | |
| 1553 | + ], | |
| 1554 | + 'logout' => [ | |
| 1555 | + 'enabled' => 'no', | |
| 1556 | + 'description' => '', | |
| 1557 | + 'mediaType' => 'image', | |
| 1558 | + 'useCustomUrl' => 'no', | |
| 1559 | + 'bannerImage' => '', | |
| 1560 | + 'bannerVideo' => [ | |
| 1561 | + 'type' => 'oembed', | |
| 1562 | + 'url' => '', | |
| 1563 | + 'content_type' => '', | |
| 1564 | + 'provider' => '', | |
| 1565 | + 'title' => '', | |
| 1566 | + 'author_name' => '', | |
| 1567 | + 'html' => '' | |
| 1568 | + ], | |
| 1569 | + 'ctaButtons' => [] | |
| 1570 | + ] | |
| 1571 | + ]; | |
| 1210 | 1572 | |
| 1211 | - $settings = Utility::getOption('welcome_banner_settings', []); | |
| 1573 | + $settings = Utility::getOption('welcome_banner_settings', []); | |
| 1212 | 1574 | |
| 1213 | - $settings = wp_parse_args($settings, $defaults); | |
| 1575 | + $settings = wp_parse_args($settings, $defaults); | |
| 1214 | 1576 | |
| 1215 | - if (empty(Arr::get($settings, 'login.bannerVideo'))) { | |
| 1216 | - $settings['login']['bannerVideo'] = $defaults['login']['bannerVideo']; | |
| 1217 | - } | |
| 1577 | + if (empty(Arr::get($settings, 'login.bannerVideo'))) { | |
| 1578 | + $settings['login']['bannerVideo'] = $defaults['login']['bannerVideo']; | |
| 1579 | + } | |
| 1218 | 1580 | |
| 1219 | - if (empty(Arr::get($settings, 'logout.bannerVideo'))) { | |
| 1220 | - $settings['logout']['bannerVideo'] = $defaults['logout']['bannerVideo']; | |
| 1221 | - } | |
| 1581 | + if (empty(Arr::get($settings, 'logout.bannerVideo'))) { | |
| 1582 | + $settings['logout']['bannerVideo'] = $defaults['logout']['bannerVideo']; | |
| 1583 | + } | |
| 1222 | 1584 | |
| 1223 | - return $settings; | |
| 1224 | - }, WEEK_IN_SECONDS); | |
| 1585 | + return $settings; | |
| 1225 | 1586 | } |
| 1226 | 1587 | |
| 1227 | 1588 | public static function getWelcomeBanner($view = 'login') |
| 1228 | 1589 | { |
| @@ -1243,25 +1604,75 @@ | ||
| 1243 | 1604 | |
| 1244 | 1605 | public static function getEnabledFeedLinks() |
| 1245 | 1606 | { |
| 1246 | 1607 | $links = array_filter(self::getFeedLinks(), function ($item) { |
| 1247 | - return Arr::get($item, 'enabled') == 'yes' && Arr::get($item, 'is_unavailable') != 'yes'; | |
| 1608 | + return self::isLinkAccessible($item); | |
| 1248 | 1609 | }); |
| 1249 | 1610 | |
| 1250 | 1611 | return array_values($links); |
| 1251 | 1612 | } |
| 1252 | 1613 | |
| 1614 | + public static function getMobileMenuItems($context = 'headless') | |
| 1615 | + { | |
| 1616 | + $xprofile = Helper::getCurrentProfile(); | |
| 1617 | + | |
| 1618 | + $mainMenuItems = Arr::get(self::getMenuItemsGroup('view'), 'mainMenuItems', []); | |
| 1619 | + | |
| 1620 | + $defaultMobileIcons = [ | |
| 1621 | + 'all_feeds' => '<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><path d="M10 13.166H10.0075" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><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><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"></path></svg>', | |
| 1622 | + 'spaces' => '<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><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><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><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><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><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"></path></g></svg>' | |
| 1623 | + ]; | |
| 1624 | + | |
| 1625 | + $mobileMenuItems = []; | |
| 1626 | + | |
| 1627 | + foreach ($defaultMobileIcons as $slug => $defaultIcon) { | |
| 1628 | + $menuItem = Arr::get($mainMenuItems, $slug); | |
| 1629 | + if (!$menuItem) { | |
| 1630 | + continue; | |
| 1631 | + } | |
| 1632 | + | |
| 1633 | + $iconSvg = Arr::get($menuItem, 'shape_svg'); | |
| 1634 | + | |
| 1635 | + $mobileMenuItems[] = [ | |
| 1636 | + 'route' => [ | |
| 1637 | + 'name' => $slug | |
| 1638 | + ], | |
| 1639 | + 'title' => Arr::get($menuItem, 'title'), | |
| 1640 | + 'icon_svg' => $iconSvg ? CustomSanitizer::sanitizeSvg($iconSvg) : $defaultIcon | |
| 1641 | + ]; | |
| 1642 | + } | |
| 1643 | + | |
| 1644 | + if ($xprofile) { | |
| 1645 | + $mobileMenuItems[] = [ | |
| 1646 | + 'route' => [ | |
| 1647 | + 'name' => 'user_profile', | |
| 1648 | + 'params' => [ | |
| 1649 | + 'username' => $xprofile->username | |
| 1650 | + ] | |
| 1651 | + ], | |
| 1652 | + 'title' => __('Profile', 'fluent-community'), | |
| 1653 | + 'icon_svg' => '<svg viewBox="0 0 1024 1024"><path fill="currentColor" d="M512 512a192 192 0 1 0 0-384 192 192 0 0 0 0 384m0 64a256 256 0 1 1 0-512 256 256 0 0 1 0 512m320 320v-96a96 96 0 0 0-96-96H288a96 96 0 0 0-96 96v96a32 32 0 1 1-64 0v-96a160 160 0 0 1 160-160h448a160 160 0 0 1 160 160v96a32 32 0 1 1-64 0"></path></svg>' | |
| 1654 | + ]; | |
| 1655 | + } else if (!get_current_user_id()) { | |
| 1656 | + $mobileMenuItems[] = [ | |
| 1657 | + 'name' => 'login', | |
| 1658 | + 'title' => __('Login', 'fluent-community'), | |
| 1659 | + 'permalink' => Helper::getAuthUrl(), | |
| 1660 | + 'icon_svg' => '<svg viewBox="0 0 1024 1024"><path fill="currentColor" d="M512 512a192 192 0 1 0 0-384 192 192 0 0 0 0 384m0 64a256 256 0 1 1 0-512 256 256 0 0 1 0 512m320 320v-96a96 96 0 0 0-96-96H288a96 96 0 0 0-96 96v96a32 32 0 1 1-64 0v-96a160 160 0 0 1 160-160h448a160 160 0 0 1 160 160v96a32 32 0 1 1-64 0"></path></svg>' | |
| 1661 | + ]; | |
| 1662 | + } | |
| 1663 | + | |
| 1664 | + return apply_filters('fluent_community/mobile_menu', $mobileMenuItems, $xprofile, $context); | |
| 1665 | + } | |
| 1666 | + | |
| 1253 | 1667 | public static function getFeedLinks() |
| 1254 | 1668 | { |
| 1255 | - return Utility::getFromCache('feed_links', function () { | |
| 1256 | - return Utility::getOption('feed_links', []); | |
| 1257 | - }, WEEK_IN_SECONDS); | |
| 1669 | + return Utility::getOption('feed_links', []); | |
| 1258 | 1670 | } |
| 1259 | 1671 | |
| 1260 | 1672 | public static function updateFeedLinks($links) |
| 1261 | 1673 | { |
| 1262 | 1674 | Utility::updateOption('feed_links', $links); |
| 1263 | - Utility::setCache('feed_links', $links, WEEK_IN_SECONDS); | |
| 1264 | 1675 | } |
| 1265 | 1676 | |
| 1266 | 1677 | /** |
| 1267 | 1678 | * Get the full name of a WordPress user. |
| @@ -1323,31 +1734,37 @@ | ||
| 1323 | 1734 | |
| 1324 | 1735 | /** |
| 1325 | 1736 | * Add a user to a space. |
| 1326 | 1737 | * |
| 1327 | - * @param Space | int $space space to add the user to. | |
| 1738 | + * @param BaseSpace|int $space space to add the user to. | |
| 1328 | 1739 | * @param int $userId The ID of the user to add. |
| 1329 | 1740 | * @param string $role The role of the user in the space. |
| 1330 | 1741 | * @param string $by The source of the action. |
| 1331 | 1742 | * @return bool True if the user was added, false otherwise. |
| 1332 | 1743 | */ |
| 1333 | - public static function addToSpace($space, $userId, $role = 'member', $by = 'self') | |
| 1744 | + public static function addToSpace($space, $userId, $role = 'member', $by = 'self', $skipSync = false) | |
| 1334 | 1745 | { |
| 1335 | 1746 | if (is_numeric($space)) { |
| 1336 | - $space = BaseSpace::withoutGlobalScopes()->find($space); | |
| 1747 | + $space = BaseSpace::onlyMain()->find($space); | |
| 1337 | 1748 | } |
| 1338 | 1749 | |
| 1339 | - if (!$space) { | |
| 1750 | + if (!$space || !$space instanceof BaseSpace) { | |
| 1340 | 1751 | return false; |
| 1341 | 1752 | } |
| 1342 | 1753 | |
| 1343 | - $user = User::find($userId); | |
| 1754 | + if (!$skipSync) { | |
| 1755 | + $user = User::find($userId); | |
| 1344 | 1756 | |
| 1345 | - if (!$user) { | |
| 1346 | - return false; | |
| 1757 | + if (!$user) { | |
| 1758 | + return false; | |
| 1759 | + } | |
| 1760 | + | |
| 1761 | + $user->syncXProfile(); | |
| 1347 | 1762 | } |
| 1348 | 1763 | |
| 1349 | - $user->syncXProfile(); | |
| 1764 | + if ($role == 'member' && $space->type == 'course') { | |
| 1765 | + $role = 'student'; | |
| 1766 | + } | |
| 1350 | 1767 | |
| 1351 | 1768 | $exist = SpaceUserPivot::where('user_id', $userId) |
| 1352 | 1769 | ->where('space_id', $space->id) |
| 1353 | 1770 | ->first(); |
| @@ -1354,9 +1771,13 @@ | ||
| 1354 | 1771 | |
| 1355 | 1772 | if ($exist) { |
| 1356 | 1773 | if ($exist->status != 'active') { |
| 1357 | 1774 | $exist->status = 'active'; |
| 1358 | - $exist->role = $role; | |
| 1775 | + | |
| 1776 | + if (!in_array($exist->role, ['admin', 'moderator'])) { | |
| 1777 | + $exist->role = $role; | |
| 1778 | + } | |
| 1779 | + | |
| 1359 | 1780 | $exist->save(); |
| 1360 | 1781 | |
| 1361 | 1782 | if ($space->type == 'course') { |
| 1362 | 1783 | do_action('fluent_community/course/enrolled', $space, $userId, $by); |
| @@ -1369,9 +1790,9 @@ | ||
| 1369 | 1790 | |
| 1370 | 1791 | return false; |
| 1371 | 1792 | } |
| 1372 | 1793 | |
| 1373 | - SpaceUserPivot::create([ | |
| 1794 | + $created = SpaceUserPivot::create([ | |
| 1374 | 1795 | 'space_id' => $space->id, |
| 1375 | 1796 | 'role' => $role, |
| 1376 | 1797 | 'user_id' => $userId |
| 1377 | 1798 | ]); |
| @@ -1376,13 +1797,18 @@ | ||
| 1376 | 1797 | 'user_id' => $userId |
| 1377 | 1798 | ]); |
| 1378 | 1799 | |
| 1379 | 1800 | if ($space->type == 'course') { |
| 1380 | - do_action('fluent_community/course/enrolled', $space, $userId, $by); | |
| 1801 | + if (!$space instanceof Course) { | |
| 1802 | + $space = Course::find($space->id); // we are renewing the model to have access to course relations | |
| 1803 | + } | |
| 1804 | + do_action('fluent_community/course/enrolled', $space, $userId, $by, $created); | |
| 1381 | 1805 | } else { |
| 1382 | - do_action('fluent_community/space/joined', $space, $userId, $by); | |
| 1806 | + if (!$space instanceof Space) { | |
| 1807 | + $space = Space::find($space->id); // we are renewing the model to have access to space relations | |
| 1808 | + } | |
| 1809 | + do_action('fluent_community/space/joined', $space, $userId, $by, $created); | |
| 1383 | 1810 | } |
| 1384 | - | |
| 1385 | 1811 | return true; |
| 1386 | 1812 | } |
| 1387 | 1813 | |
| 1388 | 1814 | /** |
| @@ -1400,28 +1826,37 @@ | ||
| 1400 | 1826 | return false; |
| 1401 | 1827 | } |
| 1402 | 1828 | |
| 1403 | 1829 | if (is_numeric($space)) { |
| 1404 | - $space = BaseSpace::query()->withoutGlobalScopes()->find($space); | |
| 1830 | + $space = BaseSpace::query()->onlyMain()->find($space); | |
| 1405 | 1831 | } |
| 1406 | 1832 | |
| 1407 | - if (!$space) { | |
| 1833 | + if (!$space || !$space instanceof BaseSpace) { | |
| 1408 | 1834 | return false; |
| 1409 | 1835 | } |
| 1410 | 1836 | |
| 1837 | + | |
| 1411 | 1838 | if (!self::isUserInSpace($userId, $space->id)) { |
| 1412 | 1839 | return false; |
| 1413 | 1840 | } |
| 1414 | 1841 | |
| 1415 | - SpaceUserPivot::bySpace($space->id) | |
| 1416 | - ->byUser($userId) | |
| 1842 | + SpaceUserPivot::where('space_id', $space->id) | |
| 1843 | + ->where('user_id', $userId) | |
| 1417 | 1844 | ->delete(); |
| 1418 | 1845 | |
| 1419 | 1846 | $user->cacheAccessSpaces(); |
| 1420 | 1847 | |
| 1421 | 1848 | if ($space->type == 'course') { |
| 1849 | + if (!$space instanceof Course) { | |
| 1850 | + $space = Course::find($space->id); // we are renewing the model to have access to course relations | |
| 1851 | + } | |
| 1852 | + | |
| 1422 | 1853 | do_action('fluent_community/course/student_left', $space, $userId, $by); |
| 1423 | 1854 | } else { |
| 1855 | + if (!$space instanceof Space) { | |
| 1856 | + $space = Space::find($space->id); | |
| 1857 | + } | |
| 1858 | + // we are renewing the model to have access to space relations | |
| 1424 | 1859 | do_action('fluent_community/space/user_left', $space, $userId, $by); |
| 1425 | 1860 | } |
| 1426 | 1861 | |
| 1427 | 1862 | return true; |
| @@ -1436,21 +1871,29 @@ | ||
| 1436 | 1871 | * @param bool $renderIcon Whether to render the icon or not. |
| 1437 | 1872 | */ |
| 1438 | 1873 | public static function renderLink($link, $linkClass = '', $fallback = '<span class="fcom_no_avatar"></span>', $renderIcon = true) |
| 1439 | 1874 | { |
| 1875 | + if (!$link || empty($link['permalink'])) { | |
| 1876 | + return; | |
| 1877 | + } | |
| 1878 | + | |
| 1879 | + $isCustom = Arr::get($link, 'is_custom') == 'yes'; | |
| 1880 | + | |
| 1440 | 1881 | $linkAtts = array_filter([ |
| 1441 | - 'class' => trim($linkClass . ' ' . Arr::get($link, 'link_classes')) . ' fcom_compt_link', | |
| 1882 | + 'class' => trim($linkClass . ' ' . Arr::get($link, 'link_classes')) . ' fcom_compt_link' . ($isCustom ? ' fcom_custom_link' : ''), | |
| 1442 | 1883 | 'target' => Arr::get($link, 'new_tab') === 'yes' ? '_blank' : '', |
| 1443 | 1884 | 'rel' => Arr::get($link, 'new_tab') === 'yes' ? 'noopener noreferrer' : '', |
| 1444 | 1885 | ]); |
| 1886 | + | |
| 1445 | 1887 | ?> |
| 1446 | - <a aria-label="Go to <?php echo esc_attr(Arr::get($link, 'title')); ?> page" | |
| 1447 | - href="<?php echo esc_url($link['permalink']); ?>" <?php foreach ($linkAtts as $key => $value) { | |
| 1448 | - echo esc_attr($key) . '="' . esc_attr($value) . '"'; | |
| 1888 | + <a data-fcom-hint="<?php echo esc_attr(Arr::get($link, 'title')); ?>" | |
| 1889 | + href="<?php echo esc_url($link['permalink']); ?>"<?php foreach ($linkAtts as $key => $value) { | |
| 1890 | + echo ' ' . esc_attr($key) . '="' . esc_attr($value) . '"'; | |
| 1449 | 1891 | } ?>> |
| 1450 | 1892 | <?php $renderIcon && self::printLinkIcon($link, $fallback); ?> |
| 1451 | - <span class="community_name"><?php echo wp_kses_post(Arr::get($link, 'title')); ?></span> | |
| 1452 | - <?php if (Arr::get($link, 'show_lock')): ?> | |
| 1893 | + <?php // The native title sits on the label span (not the anchor) so it can not duplicate the link's accessible name for screen readers. ?> | |
| 1894 | + <span class="community_name" title="<?php echo esc_attr(Arr::get($link, 'title')); ?>"><?php echo wp_kses_post((string) Arr::get($link, 'title', '')); ?></span> | |
| 1895 | + <?php if (Arr::get($link, 'show_lock')) : ?> | |
| 1453 | 1896 | <span class="fcom_space_lock"> |
| 1454 | 1897 | <i class="el-icon"> |
| 1455 | 1898 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"> |
| 1456 | 1899 | <path fill="currentColor" |
| @@ -1459,14 +1902,49 @@ | ||
| 1459 | 1902 | d="M512 544a32 32 0 0 1 32 32v192a32 32 0 1 1-64 0V576a32 32 0 0 1 32-32m192-160v-64a192 192 0 1 0-384 0v64zM512 64a256 256 0 0 1 256 256v128H256V320A256 256 0 0 1 512 64"></path> |
| 1460 | 1903 | </svg> |
| 1461 | 1904 | </i> |
| 1462 | 1905 | </span> |
| 1906 | + <?php elseif ($unreadBardge = Arr::get($link, 'unread_badge')) : ?> | |
| 1907 | + <span class="fcom_space_lock fcom_unread_count"> | |
| 1908 | + <?php echo wp_kses_post($unreadBardge); ?> | |
| 1909 | + </span> | |
| 1463 | 1910 | <?php endif; ?> |
| 1464 | - | |
| 1465 | 1911 | </a> |
| 1466 | 1912 | <?php |
| 1467 | 1913 | } |
| 1468 | 1914 | |
| 1915 | + public static function renderMenuItems($menuItems, $linkClass, $fallback = '', $renderIcon = false) | |
| 1916 | + { | |
| 1917 | + if (!$menuItems) { | |
| 1918 | + return; | |
| 1919 | + } | |
| 1920 | + | |
| 1921 | + $renderIcon = $renderIcon || Utility::isCustomizationEnabled('icon_on_header_menu'); | |
| 1922 | + | |
| 1923 | + foreach ($menuItems as $itemKey => $item): ?> | |
| 1924 | + <li class="<?php echo esc_attr('fcom_menu_item_' . $itemKey); ?>"> | |
| 1925 | + <?php self::renderLink($item, $linkClass, $fallback, $renderIcon); ?> | |
| 1926 | + </li> | |
| 1927 | + <?php endforeach; | |
| 1928 | + } | |
| 1929 | + | |
| 1930 | + public static function renderSettingsItems($settingsItems = []) | |
| 1931 | + { | |
| 1932 | + foreach ($settingsItems as $itemKey => $item): ?> | |
| 1933 | + <li class="<?php echo esc_attr('fcom_menu_item_' . $itemKey); ?>"> | |
| 1934 | + <a class="fcom_menu_link <?php echo esc_attr(Arr::get($item, 'link_classes')); ?>" | |
| 1935 | + href="<?php echo esc_url($item['permalink']); ?>"> | |
| 1936 | + <?php if (!empty($item['icon_svg'])): ?> | |
| 1937 | + <i class="el-icon"> | |
| 1938 | + <?php echo CustomSanitizer::sanitizeSvg(Arr::get($item, 'el-icon', '')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> | |
| 1939 | + </i> | |
| 1940 | + <?php endif; ?> | |
| 1941 | + <span class="community_name"><?php echo wp_kses_post($item['title']); ?></span> | |
| 1942 | + </a> | |
| 1943 | + </li> | |
| 1944 | + <?php endforeach; | |
| 1945 | + } | |
| 1946 | + | |
| 1469 | 1947 | /** |
| 1470 | 1948 | * Print a link icon. |
| 1471 | 1949 | * |
| 1472 | 1950 | * @param array $link The link data. |
| @@ -1524,9 +2002,12 @@ | ||
| 1524 | 2002 | // most probably it's local reverse proxy |
| 1525 | 2003 | if (isset($_SERVER["HTTP_CLIENT_IP"])) { |
| 1526 | 2004 | $ipAddress = sanitize_text_field(wp_unslash($_SERVER["HTTP_CLIENT_IP"])); |
| 1527 | 2005 | } else if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { |
| 1528 | - $ipAddress = (string)rest_is_ip_address(trim(current(preg_split('/,/', sanitize_text_field(wp_unslash($_SERVER['HTTP_X_FORWARDED_FOR'])))))); | |
| 2006 | + $forwardedIp = trim(current(preg_split('/,/', sanitize_text_field(wp_unslash($_SERVER['HTTP_X_FORWARDED_FOR']))))); | |
| 2007 | + if (rest_is_ip_address($forwardedIp)) { | |
| 2008 | + $ipAddress = $forwardedIp; | |
| 2009 | + } | |
| 1529 | 2010 | } |
| 1530 | 2011 | } |
| 1531 | 2012 | |
| 1532 | 2013 | if (!$ipAddress) { |
| @@ -1649,8 +2130,14 @@ | ||
| 1649 | 2130 | public static function getPortalRequestPath($requestUri) |
| 1650 | 2131 | { |
| 1651 | 2132 | $portalSlug = self::getPortalSlug(); |
| 1652 | 2133 | |
| 2134 | + // If portal is mounted at site root with empty requestUri, ignore query-only requests that do not relate to the community portal. | |
| 2135 | + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only routing check on $_GET, no state mutation | |
| 2136 | + if ($portalSlug === '' && $requestUri === '' && !empty($_GET) && !self::hasSupportedQueryParam()) { | |
| 2137 | + return false; | |
| 2138 | + } | |
| 2139 | + | |
| 1653 | 2140 | if ($portalSlug == $requestUri) { |
| 1654 | 2141 | return 'portal_home'; |
| 1655 | 2142 | } |
| 1656 | 2143 | |
| @@ -1665,25 +2152,422 @@ | ||
| 1665 | 2152 | |
| 1666 | 2153 | $parts = explode('/', $requestUri); |
| 1667 | 2154 | $start = $parts[0]; |
| 1668 | 2155 | |
| 2156 | + if (!$portalSlug && $start == 'fcom_route') { | |
| 2157 | + return $start; | |
| 2158 | + } | |
| 2159 | + | |
| 1669 | 2160 | $routeStats = self::portalRoutePaths(); |
| 1670 | 2161 | |
| 1671 | 2162 | if (in_array($start, $routeStats)) { |
| 1672 | 2163 | return $requestUri; |
| 1673 | 2164 | } |
| 2165 | + | |
| 1674 | 2166 | return false; |
| 1675 | 2167 | } |
| 1676 | 2168 | |
| 2169 | + /** | |
| 2170 | + * Check if the current request has any supported query parameter. | |
| 2171 | + * | |
| 2172 | + * @return bool | |
| 2173 | + */ | |
| 2174 | + private static function hasSupportedQueryParam() | |
| 2175 | + { | |
| 2176 | + $supportedParams = (array) apply_filters('fluent_community/portal_supported_query_params', [ | |
| 2177 | + 'customizer_panel', | |
| 2178 | + 'create_space' | |
| 2179 | + ]); | |
| 2180 | + | |
| 2181 | + // phpcs:ignore WordPress.Security.NonceVerification.Recommended | |
| 2182 | + foreach (array_keys($_GET) as $key) { | |
| 2183 | + if (strpos($key, 'fcom_') === 0) { | |
| 2184 | + return true; | |
| 2185 | + } | |
| 2186 | + if (in_array($key, $supportedParams, true)) { | |
| 2187 | + return true; | |
| 2188 | + } | |
| 2189 | + } | |
| 2190 | + return false; | |
| 2191 | + } | |
| 2192 | + | |
| 1677 | 2193 | public static function getTopicsConfig() |
| 1678 | 2194 | { |
| 1679 | - return Utility::getFromCache('topics_config', function () { | |
| 1680 | - $config = Utility::getOption('topics_config', []); | |
| 1681 | - $default = [ | |
| 1682 | - 'max_topics_per_post' => 1, | |
| 1683 | - 'max_topics_per_space' => 20, | |
| 1684 | - 'show_on_post_card' => 'yes' | |
| 1685 | - ]; | |
| 1686 | - return wp_parse_args($config, $default); | |
| 1687 | - }, WEEK_IN_SECONDS); | |
| 2195 | + $config = Utility::getOption('topics_config', []); | |
| 2196 | + $default = [ | |
| 2197 | + 'max_topics_per_post' => 1, | |
| 2198 | + 'max_topics_per_space' => 20, | |
| 2199 | + 'show_on_post_card' => 'yes' | |
| 2200 | + ]; | |
| 2201 | + return wp_parse_args($config, $default); | |
| 2202 | + } | |
| 2203 | + | |
| 2204 | + public static function getModerationConfig() | |
| 2205 | + { | |
| 2206 | + $config = Utility::getOption('moderation_config', []); | |
| 2207 | + | |
| 2208 | + $default = [ | |
| 2209 | + 'is_enabled' => 'no', | |
| 2210 | + 'profanity_filter' => "", | |
| 2211 | + 'flag_after_threshold' => 0, | |
| 2212 | + 'flag_all_new_posts' => 'no', | |
| 2213 | + 'first_post_approval' => 'no', | |
| 2214 | + 'first_comment_approval' => 'no', | |
| 2215 | + 'flag_all_new_posts_spaces' => [], | |
| 2216 | + 'auto_flag_user_reject_threshold' => 0, | |
| 2217 | + 'auto_flag_user_report_threshold' => 0, | |
| 2218 | + ]; | |
| 2219 | + | |
| 2220 | + return wp_parse_args($config, $default); | |
| 2221 | + } | |
| 2222 | + | |
| 2223 | + public static function getReportReasons() | |
| 2224 | + { | |
| 2225 | + return apply_filters('fluent_community/report_reasons', [ | |
| 2226 | + 'harassment' => __('Harassment', 'fluent-community'), | |
| 2227 | + 'spam' => __('Spam', 'fluent-community'), | |
| 2228 | + 'offensive' => __('Offensive', 'fluent-community'), | |
| 2229 | + 'incorrect_space' => __('Incorrect Space', 'fluent-community'), | |
| 2230 | + 'against_community' => __('Against Community Rules', 'fluent-community'), | |
| 2231 | + 'other' => __('Other', 'fluent-community'), | |
| 2232 | + ]); | |
| 2233 | + } | |
| 2234 | + | |
| 2235 | + public static function htmlToMd($html) | |
| 2236 | + { | |
| 2237 | + return preg_replace('/<a.*?href="(.*?)".*?>(.*?)<\/a>/', '[$2]($1)', $html); | |
| 2238 | + } | |
| 2239 | + | |
| 2240 | + public static function isProfanity($profanity, $text) | |
| 2241 | + { | |
| 2242 | + $profanity = explode(',', $profanity); | |
| 2243 | + if (empty($profanity)) { | |
| 2244 | + return false; | |
| 2245 | + } | |
| 2246 | + $profanity = array_map('trim', $profanity); | |
| 2247 | + $profanity = array_map(function ($word) { | |
| 2248 | + return mb_strtolower($word, 'UTF-8'); | |
| 2249 | + }, $profanity); | |
| 2250 | + $text = mb_strtolower($text, 'UTF-8'); | |
| 2251 | + | |
| 2252 | + // Convert words into a regex pattern (ensuring whole-word matching) | |
| 2253 | + $pattern = '/(?<!\p{L})(' . implode('|', array_map('preg_quote', $profanity)) . ')(?!\p{L})/iu'; | |
| 2254 | + | |
| 2255 | + if (preg_match($pattern, $text, $matches)) { | |
| 2256 | + return $matches[0]; | |
| 2257 | + } | |
| 2258 | + | |
| 2259 | + return false; | |
| 2260 | + } | |
| 2261 | + | |
| 2262 | + public static function getFullDayName($day) | |
| 2263 | + { | |
| 2264 | + $dayMap = [ | |
| 2265 | + 'sun' => 'sunday', | |
| 2266 | + 'mon' => 'monday', | |
| 2267 | + 'tue' => 'tuesday', | |
| 2268 | + 'wed' => 'wednesday', | |
| 2269 | + 'thu' => 'thursday', | |
| 2270 | + 'fri' => 'friday', | |
| 2271 | + 'sat' => 'saturday' | |
| 2272 | + ]; | |
| 2273 | + | |
| 2274 | + return isset($dayMap[$day]) ? $dayMap[$day] : $day . 'day'; | |
| 2275 | + } | |
| 2276 | + | |
| 2277 | + public static function getPostOrderOptions($context = 'feed') | |
| 2278 | + { | |
| 2279 | + $options = [ | |
| 2280 | + 'new_activity' => __('New Activity', 'fluent-community'), | |
| 2281 | + 'latest' => __('Latest', 'fluent-community'), | |
| 2282 | + 'oldest' => __('Oldest', 'fluent-community'), | |
| 2283 | + 'popular' => __('Popular', 'fluent-community'), | |
| 2284 | + 'likes' => __('Likes', 'fluent-community'), | |
| 2285 | + 'alphabetical' => __('Alphabetical', 'fluent-community'), | |
| 2286 | + 'unanswered' => __('Unanswered', 'fluent-community'), | |
| 2287 | + ]; | |
| 2288 | + | |
| 2289 | + return apply_filters('fluent_community/post_order_options', $options, $context); | |
| 2290 | + } | |
| 2291 | + | |
| 2292 | + public static function getCommentOrderOptions($context = 'comment') | |
| 2293 | + { | |
| 2294 | + $options = [ | |
| 2295 | + 'oldest' => __('Earliest', 'fluent-community'), | |
| 2296 | + 'latest' => __('Latest', 'fluent-community'), | |
| 2297 | + 'popular' => __('Popular', 'fluent-community'), | |
| 2298 | + 'most_replied' => __('Most Replied', 'fluent-community'), | |
| 2299 | + ]; | |
| 2300 | + | |
| 2301 | + return apply_filters('fluent_community/comment_order_options', $options, $context); | |
| 2302 | + } | |
| 2303 | + | |
| 2304 | + public static function convertPhpDateToDayJSFormay($phpFormat) | |
| 2305 | + { | |
| 2306 | + // Mapping PHP date format characters to Day.js format characters | |
| 2307 | + $replacements = [ | |
| 2308 | + // Day | |
| 2309 | + 'd' => 'DD', // Day of the month, 2 digits with leading zeros | |
| 2310 | + 'D' => 'ddd', // A textual representation of a day, three letters | |
| 2311 | + 'j' => 'D', // Day of the month without leading zeros | |
| 2312 | + 'l' => 'dddd', // A full textual representation of the day of the week | |
| 2313 | + 'N' => 'E', // ISO-8601 numeric representation of the day of the week | |
| 2314 | + 'S' => 'o', // English ordinal suffix for the day of the month, 2 characters | |
| 2315 | + 'w' => 'd', // Numeric representation of the day of the week | |
| 2316 | + 'z' => 'DDD', // The day of the year (starting from 0) | |
| 2317 | + | |
| 2318 | + // Week | |
| 2319 | + 'W' => 'W', // ISO-8601 week number of year, weeks starting on Monday | |
| 2320 | + | |
| 2321 | + // Month | |
| 2322 | + 'F' => 'MMMM', // A full textual representation of a month | |
| 2323 | + 'm' => 'MM', // Numeric representation of a month, with leading zeros | |
| 2324 | + 'M' => 'MMM', // A short textual representation of a month, three letters | |
| 2325 | + 'n' => 'M', // Numeric representation of a month, without leading zeros | |
| 2326 | + 't' => '', // Not supported in Day.js (Number of days in the given month) | |
| 2327 | + | |
| 2328 | + // Year | |
| 2329 | + 'L' => '', // Not supported in Day.js (Whether it's a leap year) | |
| 2330 | + 'o' => 'GGGG', // ISO-8601 week-numbering year | |
| 2331 | + 'Y' => 'YYYY', // A full numeric representation of a year, 4 digits | |
| 2332 | + 'y' => 'YY', // A two digit representation of a year | |
| 2333 | + | |
| 2334 | + // Time | |
| 2335 | + 'a' => 'a', // Lowercase Ante meridiem and Post meridiem | |
| 2336 | + 'A' => 'A', // Uppercase Ante meridiem and Post meridiem | |
| 2337 | + 'B' => '', // Not supported in Day.js (Swatch Internet time) | |
| 2338 | + 'g' => 'h', // 12-hour format of an hour without leading zeros | |
| 2339 | + 'G' => 'H', // 24-hour format of an hour without leading zeros | |
| 2340 | + 'h' => 'hh', // 12-hour format of an hour with leading zeros | |
| 2341 | + 'H' => 'HH', // 24-hour format of an hour with leading zeros | |
| 2342 | + 'i' => 'mm', // Minutes with leading zeros | |
| 2343 | + 's' => 'ss', // Seconds with leading zeros | |
| 2344 | + 'u' => 'SSS', // Milliseconds (Day.js uses SSS for fractional seconds) | |
| 2345 | + 'v' => 'SSS', // Milliseconds (Day.js uses SSS for fractional seconds) | |
| 2346 | + | |
| 2347 | + // Timezone | |
| 2348 | + 'e' => '', // Not supported in Day.js (Timezone identifier) | |
| 2349 | + 'I' => '', // Not supported in Day.js (Whether or not the date is in daylight saving time) | |
| 2350 | + 'O' => 'ZZ', // Difference to Greenwich time (GMT) in hours | |
| 2351 | + 'P' => 'Z', // Difference to Greenwich time (GMT) with colon between hours and minutes | |
| 2352 | + 'T' => '', // Not supported in Day.js (Timezone abbreviation) | |
| 2353 | + 'Z' => '', // Not supported in Day.js (Timezone offset in seconds) | |
| 2354 | + | |
| 2355 | + // Full Date/Time | |
| 2356 | + 'c' => 'YYYY-MM-DDTHH:mm:ssZ', // ISO 8601 date | |
| 2357 | + 'r' => 'ddd, DD MMM YYYY HH:mm:ss ZZ', // RFC 2822 formatted date | |
| 2358 | + 'U' => 'X', // Seconds since the Unix Epoch (January 1 1970 00:00:00 GMT) | |
| 2359 | + ]; | |
| 2360 | + | |
| 2361 | + // Replace each PHP date format character with Day.js equivalent | |
| 2362 | + $dayjsFormat = ""; | |
| 2363 | + | |
| 2364 | + for ($i = 0; $i < strlen($phpFormat); $i++) { | |
| 2365 | + $char = $phpFormat[$i]; | |
| 2366 | + | |
| 2367 | + // Special handling for G\hi pattern | |
| 2368 | + if ($char === 'G' && $i + 2 < strlen($phpFormat) && | |
| 2369 | + $phpFormat[$i + 1] === '\\' && $phpFormat[$i + 2] === 'h') { | |
| 2370 | + $dayjsFormat .= 'H[h]'; | |
| 2371 | + $i += 2; | |
| 2372 | + continue; | |
| 2373 | + } | |
| 2374 | + | |
| 2375 | + // Check if the character is escaped | |
| 2376 | + if ($char === "\\") { | |
| 2377 | + // Day.js escapes literal text with square brackets, not backslashes | |
| 2378 | + $i++; | |
| 2379 | + if ($i < strlen($phpFormat)) { | |
| 2380 | + $dayjsFormat .= "[" . $phpFormat[$i] . "]"; | |
| 2381 | + } | |
| 2382 | + continue; | |
| 2383 | + } | |
| 2384 | + | |
| 2385 | + // Add the mapped character or the character itself if not found in the mapping | |
| 2386 | + $dayjsFormat .= $replacements[$char] ?? $char; | |
| 2387 | + } | |
| 2388 | + | |
| 2389 | + return $dayjsFormat; | |
| 2390 | + } | |
| 2391 | + | |
| 2392 | + public static function getDateFormatter($isDayJs = false) | |
| 2393 | + { | |
| 2394 | + $format = get_option('date_format'); | |
| 2395 | + if ($isDayJs) { | |
| 2396 | + return self::convertPhpDateToDayJSFormay($format); | |
| 2397 | + } | |
| 2398 | + | |
| 2399 | + return $format; | |
| 2400 | + } | |
| 2401 | + | |
| 2402 | + public static function getTimeFormatter($isDayJs = false) | |
| 2403 | + { | |
| 2404 | + $format = get_option('time_format'); | |
| 2405 | + | |
| 2406 | + if ($isDayJs) { | |
| 2407 | + return self::convertPhpDateToDayJSFormay($format); | |
| 2408 | + } | |
| 2409 | + | |
| 2410 | + return $format; | |
| 2411 | + } | |
| 2412 | + | |
| 2413 | + public static function normalizeToAscii($text) | |
| 2414 | + { | |
| 2415 | + if (function_exists('transliterator_transliterate')) { | |
| 2416 | + $result = transliterator_transliterate('Any-Latin; Latin-ASCII', $text); | |
| 2417 | + if ($result !== false) { | |
| 2418 | + return $result; | |
| 2419 | + } | |
| 2420 | + } | |
| 2421 | + | |
| 2422 | + if (function_exists('iconv')) { | |
| 2423 | + $result = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $text); | |
| 2424 | + if ($result !== false) { | |
| 2425 | + return $result; | |
| 2426 | + } | |
| 2427 | + } | |
| 2428 | + | |
| 2429 | + return $text; | |
| 2430 | + } | |
| 2431 | + | |
| 2432 | + public static function getPathFromUrl($url) | |
| 2433 | + { | |
| 2434 | + return rtrim((string) wp_parse_url($url, PHP_URL_PATH), '/'); | |
| 2435 | + } | |
| 2436 | + | |
| 2437 | + // Return the ID of the group that contains the current page | |
| 2438 | + public static function getActiveSidebarGroupId($groups) | |
| 2439 | + { | |
| 2440 | + if (empty($_SERVER['REQUEST_URI'])) { | |
| 2441 | + return ''; | |
| 2442 | + } | |
| 2443 | + $url=esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated | |
| 2444 | + $currentPath = self::getPathFromUrl($url); | |
| 2445 | + if ($currentPath === '') { | |
| 2446 | + return ''; | |
| 2447 | + } | |
| 2448 | + | |
| 2449 | + foreach ($groups as $group) { | |
| 2450 | + $items = isset($group['children']) ? $group['children'] : ($group['items'] ?? []); | |
| 2451 | + foreach ($items as $item) { | |
| 2452 | + $item_path = isset($item['permalink']) ? self::getPathFromUrl($item['permalink']) : ''; | |
| 2453 | + | |
| 2454 | + if (is_array($item) && !empty($item['permalink']) && $item_path === $currentPath) { | |
| 2455 | + return sanitize_key((string) ($group['id'] ?? $group['slug'] ?? '')); | |
| 2456 | + } | |
| 2457 | + } | |
| 2458 | + } | |
| 2459 | + | |
| 2460 | + return ''; | |
| 2461 | + } | |
| 2462 | + | |
| 2463 | + // Explicit per-user choices from the cookie. Format: "c:1,2|e:3,4" (c = collapsed, e = expanded). | |
| 2464 | + public static function getSidebarGroupStates() | |
| 2465 | + { | |
| 2466 | + $collapsed = []; | |
| 2467 | + $expanded = []; | |
| 2468 | + $cookie = isset($_COOKIE['fcom_sidebar_group_states']) ? sanitize_text_field(wp_unslash($_COOKIE['fcom_sidebar_group_states'])) : ''; | |
| 2469 | + foreach (explode('|', $cookie) as $section) { | |
| 2470 | + list($flag, $ids) = array_pad(explode(':', $section, 2), 2, ''); | |
| 2471 | + $list = array_filter(array_map('sanitize_key', explode(',', $ids))); | |
| 2472 | + | |
| 2473 | + if ($flag === 'c') { | |
| 2474 | + $collapsed = $list; | |
| 2475 | + } elseif ($flag === 'e') { | |
| 2476 | + $expanded = $list; | |
| 2477 | + } | |
| 2478 | + } | |
| 2479 | + | |
| 2480 | + return [$collapsed, $expanded]; | |
| 2481 | + } | |
| 2482 | + | |
| 2483 | + public static function getCollapsedSidebarGroups($groups = []) | |
| 2484 | + { | |
| 2485 | + $isDefaultCollapse = Utility::isCustomizationEnabled('collapse_sidebar_groups'); | |
| 2486 | + list($collapsedByUser, $expandedByUser) = self::getSidebarGroupStates(); | |
| 2487 | + $activeGroupId = self::getActiveSidebarGroupId($groups); | |
| 2488 | + | |
| 2489 | + /** | |
| 2490 | + * Precendence. | |
| 2491 | + * 1. No group id or slug => Fallback to expanded | |
| 2492 | + * 2. Has Active Link => Always expanded | |
| 2493 | + * 3. Explicitly expanded by user => Always expanded | |
| 2494 | + * 4. Explicitly collapsed by user => Collapsed | |
| 2495 | + * 5. Default Collapse by setting => Collapsed | |
| 2496 | + * 6. Otherwise => Expanded | |
| 2497 | + */ | |
| 2498 | + | |
| 2499 | + $collapsed = []; | |
| 2500 | + foreach ($groups as $group) { | |
| 2501 | + $id = sanitize_key((string) ($group['id'] ?? $group['slug'] ?? '')); | |
| 2502 | + // Matching expanded condition (1,2,3) | |
| 2503 | + if ($id === '' || $id === $activeGroupId || in_array($id, $expandedByUser, true)) { | |
| 2504 | + continue; | |
| 2505 | + } | |
| 2506 | + // Matching collapsed condition (4,5) | |
| 2507 | + if ($isDefaultCollapse || in_array($id, $collapsedByUser, true)) { | |
| 2508 | + $collapsed[] = $id; | |
| 2509 | + } | |
| 2510 | + // else (6) => expanded, do nothing | |
| 2511 | + } | |
| 2512 | + | |
| 2513 | + return $collapsed; | |
| 2514 | + } | |
| 2515 | + | |
| 2516 | + public static function getUndeliverableEmails($emails) | |
| 2517 | + { | |
| 2518 | + if (!$emails || !defined('FLUENTCRM')) { | |
| 2519 | + return []; | |
| 2520 | + } | |
| 2521 | + | |
| 2522 | + if (Utility::getPrivacySetting('skip_crm_undeliverable_emails') != 'yes') { | |
| 2523 | + return []; | |
| 2524 | + } | |
| 2525 | + | |
| 2526 | + /** | |
| 2527 | + * FluentCRM contact statuses that FluentCommunity treats as undeliverable. | |
| 2528 | + * Emails to contacts with these statuses are skipped for notification emails. | |
| 2529 | + * | |
| 2530 | + * @param array $statuses Contact statuses to skip. Default: bounced, complained, spammed. | |
| 2531 | + */ | |
| 2532 | + $skippableStatuses = apply_filters('fluent_community/undeliverable_crm_contact_statuses', ['bounced', 'complained', 'spammed']); | |
| 2533 | + | |
| 2534 | + if (!$skippableStatuses) { | |
| 2535 | + return []; | |
| 2536 | + } | |
| 2537 | + | |
| 2538 | + $undeliverableEmails = Contact::whereIn('email', $emails) | |
| 2539 | + ->whereIn('status', $skippableStatuses) | |
| 2540 | + ->pluck('email') | |
| 2541 | + ->toArray(); | |
| 2542 | + | |
| 2543 | + return array_map('strtolower', $undeliverableEmails); | |
| 2544 | + } | |
| 2545 | + | |
| 2546 | + public static function getCrmUndeliverableStatus($email) | |
| 2547 | + { | |
| 2548 | + if (!$email || !defined('FLUENTCRM')) { | |
| 2549 | + return ''; | |
| 2550 | + } | |
| 2551 | + | |
| 2552 | + if (Utility::getPrivacySetting('skip_crm_undeliverable_emails') != 'yes') { | |
| 2553 | + return ''; | |
| 2554 | + } | |
| 2555 | + | |
| 2556 | + $skippableStatuses = apply_filters('fluent_community/undeliverable_crm_contact_statuses', ['bounced', 'complained', 'spammed']); | |
| 2557 | + | |
| 2558 | + if (!$skippableStatuses) { | |
| 2559 | + return ''; | |
| 2560 | + } | |
| 2561 | + | |
| 2562 | + $contact = Contact::where('email', $email) | |
| 2563 | + ->whereIn('status', $skippableStatuses) | |
| 2564 | + ->first(); | |
| 2565 | + | |
| 2566 | + return $contact ? $contact->status : ''; | |
| 2567 | + } | |
| 2568 | + | |
| 2569 | + public static function isUndeliverableEmail($email) | |
| 2570 | + { | |
| 2571 | + return (bool)self::getCrmUndeliverableStatus($email); | |
| 1688 | 2572 | } |
| 1689 | 2573 | } |