PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.11.0
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.11.0
2.11.0 2.10.0 2.10.01 2.9.1 2.9.0 2.8.1 2.8.0 2.7.7 2.7.5 2.7.0 2.6.01 2.6.0 2.5.0 2.4.01 trunk 1.0.90 1.0.91 1.0.92 1.0.93 1.0.94 1.0.95 1.0.96 1.0.97 1.0.98 1.0.99 All 78 releases
← All changes | app/Services/Helper.php +117 -8 2.7.52.11.0 View file →
@@ -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 }
@@ -749,8 +753,31 @@
749 753 return false;
750 754 }
751 755
752 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 + /**
753 780 * Get a human-readable excerpt from content.
754 781 *
755 782 * @param string $content The content to extract from.
756 783 * @param int $length The maximum length of the excerpt.
@@ -771,12 +798,12 @@
771 798 // Blockquotes: remove '>' symbol
772 799 '/^\s*>\s?/m' => '',
773 800 // Horizontal rules: replace with empty line
774 801 '/^\s*([-*_])\1{2,}\s*$/m' => "\n",
802 + // Images: keep only the alt text (run before links)
803 + '/!\[([^\]]*)\]\([^\)]+\)/' => '$1',
775 804 // Links: keep only the link text
776 - '/\[([^\]]+)\]\([^\)]+\)/' => '$1',
777 - // Images: keep only the alt text
778 - '/!\[([^\]]+)\]\([^\)]+\)/' => '$1',
805 + '/\[([^\]]*)\]\([^\)]+\)/' => '$1',
779 806 // Strikethrough: remove '~~' symbols
780 807 '/~~(.*?)~~/' => '$1',
781 808 // Task lists: remove checkbox syntax
782 809 '/^\s*[-*+]\s+\[[ xX]\]\s+/m' => '',
@@ -783,8 +810,10 @@
783 810 ];
784 811
785 812 $content = preg_replace(array_keys($patterns), array_values($patterns), $content);
786 813
814 + $content = html_entity_decode($content, ENT_QUOTES | ENT_HTML5, 'UTF-8');
815 +
787 816 // remove all tags
788 817 $content = wp_strip_all_tags($content);
789 818 // remove new lines and tabs
790 819 $content = str_replace(["\r", "\n", "\t"], ' ', $content);
@@ -1320,8 +1349,30 @@
1320 1349
1321 1350 return $menuGroups;
1322 1351 }
1323 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 +
1324 1375 public static function isLinkAccessible($link, $currentUser = null)
1325 1376 {
1326 1377 $isEnabled = Arr::get($link, 'enabled', 'yes') === 'yes';
1327 1378 $isUnavailable = Arr::get($link, 'is_unavailable') === 'yes';
@@ -1833,15 +1884,15 @@
1833 1884 'rel' => Arr::get($link, 'new_tab') === 'yes' ? 'noopener noreferrer' : '',
1834 1885 ]);
1835 1886
1836 1887 ?>
1837 - <a aria-label="Go to <?php echo esc_attr(Arr::get($link, 'title')); ?> page"
1838 - data-fcom-tip="<?php echo esc_attr(Arr::get($link, 'title')); ?>"
1888 + <a data-fcom-hint="<?php echo esc_attr(Arr::get($link, 'title')); ?>"
1839 1889 href="<?php echo esc_url($link['permalink']); ?>"<?php foreach ($linkAtts as $key => $value) {
1840 - echo esc_attr($key) . '="' . esc_attr($value) . '"';
1890 + echo ' ' . esc_attr($key) . '="' . esc_attr($value) . '"';
1841 1891 } ?>>
1842 1892 <?php $renderIcon && self::printLinkIcon($link, $fallback); ?>
1843 - <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>
1844 1895 <?php if (Arr::get($link, 'show_lock')) : ?>
1845 1896 <span class="fcom_space_lock">
1846 1897 <i class="el-icon">
1847 1898 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
@@ -2459,6 +2510,64 @@
2459 2510 // else (6) => expanded, do nothing
2460 2511 }
2461 2512
2462 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);
2463 2572 }
2464 2573 }