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/FeedsHelper.php +94 -5 2.8.02.11.0 View file →
@@ -9,8 +9,9 @@
9 9 use FluentCommunity\App\Models\Reaction;
10 10 use FluentCommunity\App\Models\Term;
11 11 use FluentCommunity\App\Models\User;
12 12 use FluentCommunity\App\Models\XProfile;
13 +use FluentCommunity\Framework\Foundation\Exceptions\UnprocessableEntityHttpException;
13 14 use FluentCommunity\Framework\Support\Arr;
14 15 use FluentCommunity\Framework\Validator\Validator;
15 16
16 17 class FeedsHelper
@@ -58,8 +59,71 @@
58 59
59 60 return $user->spaces()->pluck('slug')->toArray();
60 61 }
61 62
63 + /**
64 + * Statuses where a post is fully reachable by its direct link. An unlisted post is
65 + * hidden from listings only, so it stays commentable and reactable like a published one.
66 + *
67 + * @return array
68 + */
69 + public static function getViewableByLinkStatuses()
70 + {
71 + return ['published', 'unlisted'];
72 + }
73 +
74 + /**
75 + * Row types that opt IN to comments through meta.enable_comments, mapped to the value
76 + * assumed when the key is absent.
77 + *
78 + * A feed post uses the opposite convention - meta.comments_disabled, absent meaning on -
79 + * so it is deliberately not listed here and falls through to the permissive default.
80 + *
81 + * The fallbacks match each model's getDefaultMeta(): a lesson written before the
82 + * setting existed keeps its thread, a page does not. Guessing one value for both
83 + * would silently switch off every legacy lesson discussion.
84 + *
85 + * @return array<string, string>
86 + */
87 + public static function getOptInCommentTypes()
88 + {
89 + return apply_filters('fluent_community/opt_in_comment_types', [
90 + 'course_lesson' => 'yes',
91 + 'space_page' => 'no',
92 + ]);
93 + }
94 +
95 + /**
96 + * Whether a row accepts comments at all, by its own settings.
97 + *
98 + * This is the setting check only - it says nothing about who the current user is.
99 + * Space membership and the course level kill switch are separate, in
100 + * CommentsController::verifySpacePermission().
101 + *
102 + * Both the read and the write path go through here so they cannot disagree. They used
103 + * to: the write path only ever read meta.comments_disabled, which pages and lessons
104 + * do not set, so a POST landed a comment on a page whose thread the UI was hiding.
105 + *
106 + * @param \FluentCommunity\App\Models\Feed $feed
107 + * @return bool
108 + */
109 + public static function commentsEnabled($feed)
110 + {
111 + $meta = $feed->meta;
112 +
113 + if (Arr::get($meta, 'comments_disabled') === 'yes') {
114 + return false;
115 + }
116 +
117 + $optIn = self::getOptInCommentTypes();
118 +
119 + if (isset($optIn[$feed->type])) {
120 + return Arr::get($meta, 'enable_comments', $optIn[$feed->type]) === 'yes';
121 + }
122 +
123 + return true;
124 + }
125 +
62 126 public static function getLastFeedId()
63 127 {
64 128 $lastItem = Feed::where('status', 'published')
65 129 ->byUserAccess(get_current_user_id())
@@ -547,9 +611,9 @@
547 611 }
548 612
549 613 public static function sanitizeAndValidateData($data)
550 614 {
551 - $message = CustomSanitizer::unslashMarkdown(trim(Arr::get($data, 'message')));
615 + $message = CustomSanitizer::unslashMarkdown(trim((string) Arr::get($data, 'message', '')));
552 616
553 617 // Decode HTML entities and strip all whitespace for validation
554 618 $messageForValidation = html_entity_decode($message, ENT_QUOTES | ENT_HTML5, 'UTF-8');
555 619 $messageForValidation = preg_replace('/\s+/u', '', $messageForValidation);
@@ -554,9 +618,12 @@
554 618 $messageForValidation = html_entity_decode($message, ENT_QUOTES | ENT_HTML5, 'UTF-8');
555 619 $messageForValidation = preg_replace('/\s+/u', '', $messageForValidation);
556 620
557 621 if (!$messageForValidation) {
558 - throw new \Exception(esc_html__('Message is required', 'fluent-community'));
622 + throw new UnprocessableEntityHttpException(
623 + esc_html__('Message is required', 'fluent-community'),
624 + 'feed_message_required'
625 + );
559 626 }
560 627
561 628 $processedData = [
562 629 'message' => $message,
@@ -596,10 +663,13 @@
596 663 }
597 664
598 665 $maxlen = apply_filters('fluent_community/max_post_length', 15000);
599 666 if (\strlen($message) > $maxlen) {
600 - /* translators: %s is the maximum allowed character count */
601 - throw new \Exception(esc_html(sprintf(__('The post is too long. Please keep it under %s characters.', 'fluent-community'), number_format($maxlen))));
667 + throw new UnprocessableEntityHttpException(
668 + /* translators: %s is the maximum allowed character count */
669 + esc_html(sprintf(__('The post is too long. Please keep it under %s characters.', 'fluent-community'), number_format($maxlen))),
670 + 'feed_message_too_long'
671 + );
602 672 }
603 673
604 674 $titlePref = Utility::postTitlePref();
605 675
@@ -605,9 +675,12 @@
605 675
606 676 if ($titlePref) {
607 677 $processedData['title'] = sanitize_text_field(Arr::get($data, 'title'));
608 678 if ($titlePref == 'required' && empty($processedData['title'])) {
609 - throw new \Exception(esc_html__('Title is required. Please provide a title', 'fluent-community'));
679 + throw new UnprocessableEntityHttpException(
680 + esc_html__('Title is required. Please provide a title', 'fluent-community'),
681 + 'feed_title_required'
682 + );
610 683 }
611 684 // trim the title if it's too long to 192 chars (multibyte-safe; column is VARCHAR(192) characters)
612 685 if (mb_strlen($processedData['title']) > 192) {
613 686 $processedData['title'] = mb_substr($processedData['title'], 0, 192, 'UTF-8');
@@ -1054,8 +1127,24 @@
1054 1127 }
1055 1128
1056 1129 $spaceSettings = $feed->space ? $feed->space->settings : [];
1057 1130 $feed->default_comment_sort_by = Arr::get($spaceSettings, 'default_comment_sort_by', '');
1131 +
1132 + // Feed::withPublicRelations() eager-loads the space with its raw settings, and
1133 + // those settings carry links scoped to logged-in members or to specific
1134 + // memberships. BaseSpace::formatSpaceData() filters them for the space
1135 + // endpoints; nothing filtered them here, so every feed response handed all of
1136 + // a space's links - titles and URLs - to any caller, anonymous included.
1137 + if ($feed->space && Arr::get($spaceSettings, 'links')) {
1138 + $currentUser = Helper::getCurrentUser();
1139 +
1140 + $spaceSettings['links'] = Helper::filterAccessibleLinks(
1141 + Arr::get($spaceSettings, 'links', []),
1142 + $currentUser ? $currentUser : null
1143 + );
1144 +
1145 + $feed->space->settings = $spaceSettings;
1146 + }
1058 1147
1059 1148 self::setCurrentRelatedUserId($feed->user_id);
1060 1149
1061 1150 return apply_filters('fluent_community/rendering_feed_model', $feed, $config);