| @@ -5,8 +5,9 @@ | ||
| 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; | |
| 9 | 10 | use FluentCommunity\App\Models\Feed; |
| 10 | 11 | use FluentCommunity\App\Models\Space; |
| 11 | 12 | use FluentCommunity\App\Models\Media; |
| 12 | 13 | use FluentCommunity\App\Models\Meta; |
| @@ -246,8 +247,9 @@ | ||
| 246 | 247 | /** |
| 247 | 248 | * Check if the user is a site admin. |
| 248 | 249 | * |
| 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. | |
| 250 | 252 | * @return bool True if the user is a site admin, false otherwise. |
| 251 | 253 | */ |
| 252 | 254 | public static function isSiteAdmin($userId = null, $user = null) |
| 253 | 255 | { |
| @@ -255,9 +257,11 @@ | ||
| 255 | 257 | return true; |
| 256 | 258 | } |
| 257 | 259 | |
| 258 | 260 | if (!$user) { |
| 259 | - $user = self::getCurrentUser(); | |
| 261 | + $user = ($userId && (int)$userId !== get_current_user_id()) | |
| 262 | + ? User::find($userId) | |
| 263 | + : self::getCurrentUser(); | |
| 260 | 264 | } |
| 261 | 265 | |
| 262 | 266 | return $user && Arr::get($user->getPermissions(), 'community_admin'); |
| 263 | 267 | } |
| @@ -794,12 +798,12 @@ | ||
| 794 | 798 | // Blockquotes: remove '>' symbol |
| 795 | 799 | '/^\s*>\s?/m' => '', |
| 796 | 800 | // Horizontal rules: replace with empty line |
| 797 | 801 | '/^\s*([-*_])\1{2,}\s*$/m' => "\n", |
| 802 | + // Images: keep only the alt text (run before links) | |
| 803 | + '/!\[([^\]]*)\]\([^\)]+\)/' => '$1', | |
| 798 | 804 | // Links: keep only the link text |
| 799 | - '/\[([^\]]+)\]\([^\)]+\)/' => '$1', | |
| 800 | - // Images: keep only the alt text | |
| 801 | - '/!\[([^\]]+)\]\([^\)]+\)/' => '$1', | |
| 805 | + '/\[([^\]]*)\]\([^\)]+\)/' => '$1', | |
| 802 | 806 | // Strikethrough: remove '~~' symbols |
| 803 | 807 | '/~~(.*?)~~/' => '$1', |
| 804 | 808 | // Task lists: remove checkbox syntax |
| 805 | 809 | '/^\s*[-*+]\s+\[[ xX]\]\s+/m' => '', |
| @@ -806,8 +810,10 @@ | ||
| 806 | 810 | ]; |
| 807 | 811 | |
| 808 | 812 | $content = preg_replace(array_keys($patterns), array_values($patterns), $content); |
| 809 | 813 | |
| 814 | + $content = html_entity_decode($content, ENT_QUOTES | ENT_HTML5, 'UTF-8'); | |
| 815 | + | |
| 810 | 816 | // remove all tags |
| 811 | 817 | $content = wp_strip_all_tags($content); |
| 812 | 818 | // remove new lines and tabs |
| 813 | 819 | $content = str_replace(["\r", "\n", "\t"], ' ', $content); |
| @@ -1343,8 +1349,30 @@ | ||
| 1343 | 1349 | |
| 1344 | 1350 | return $menuGroups; |
| 1345 | 1351 | } |
| 1346 | 1352 | |
| 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 | + | |
| 1347 | 1375 | public static function isLinkAccessible($link, $currentUser = null) |
| 1348 | 1376 | { |
| 1349 | 1377 | $isEnabled = Arr::get($link, 'enabled', 'yes') === 'yes'; |
| 1350 | 1378 | $isUnavailable = Arr::get($link, 'is_unavailable') === 'yes'; |
| @@ -1856,15 +1884,15 @@ | ||
| 1856 | 1884 | 'rel' => Arr::get($link, 'new_tab') === 'yes' ? 'noopener noreferrer' : '', |
| 1857 | 1885 | ]); |
| 1858 | 1886 | |
| 1859 | 1887 | ?> |
| 1860 | - <a aria-label="Go to <?php echo esc_attr(Arr::get($link, 'title')); ?> page" | |
| 1861 | - data-fcom-tip="<?php echo esc_attr(Arr::get($link, 'title')); ?>" | |
| 1888 | + <a data-fcom-hint="<?php echo esc_attr(Arr::get($link, 'title')); ?>" | |
| 1862 | 1889 | href="<?php echo esc_url($link['permalink']); ?>"<?php foreach ($linkAtts as $key => $value) { |
| 1863 | - echo esc_attr($key) . '="' . esc_attr($value) . '"'; | |
| 1890 | + echo ' ' . esc_attr($key) . '="' . esc_attr($value) . '"'; | |
| 1864 | 1891 | } ?>> |
| 1865 | 1892 | <?php $renderIcon && self::printLinkIcon($link, $fallback); ?> |
| 1866 | - <span class="community_name"><?php echo wp_kses_post(Arr::get($link, 'title')); ?></span> | |
| 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> | |
| 1867 | 1895 | <?php if (Arr::get($link, 'show_lock')) : ?> |
| 1868 | 1896 | <span class="fcom_space_lock"> |
| 1869 | 1897 | <i class="el-icon"> |
| 1870 | 1898 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"> |
| @@ -2482,6 +2510,64 @@ | ||
| 2482 | 2510 | // else (6) => expanded, do nothing |
| 2483 | 2511 | } |
| 2484 | 2512 | |
| 2485 | 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); | |
| 2486 | 2572 | } |
| 2487 | 2573 | } |