PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.10.0
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.10.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/Http/Controllers/CommentsController.php +97 -45 2.4.012.10.0 View file →
@@ -11,8 +11,9 @@
11 11 use FluentCommunity\Framework\Http\Request\Request;
12 12 use FluentCommunity\App\Models\Comment;
13 13 use FluentCommunity\App\Models\Feed;
14 14 use FluentCommunity\App\Models\Reaction;
15 +use FluentCommunity\App\Models\XProfile;
15 16 use FluentCommunity\Framework\Support\Arr;
16 17
17 18 class CommentsController extends Controller
18 19 {
@@ -17,11 +18,29 @@
17 18 class CommentsController extends Controller
18 19 {
19 20 public function getComments(Request $request, $feed_id)
20 21 {
21 - $feed = Feed::withoutGlobalScopes()->findOrFail($feed_id);
22 - $canViewComments = apply_filters('fluent_community/can_view_comments_' . $feed->type, true, $feed);
22 + $feed = Feed::withoutGlobalScopes()
23 + ->byUserAccess(get_current_user_id())
24 + ->findOrFail($feed_id);
23 25
26 + if (!in_array($feed->status, FeedsHelper::getViewableByLinkStatuses(), true) && !$feed->hasEditAccess($this->getUserId())) {
27 + return $this->sendError([
28 + 'message' => __('Sorry, you do not have permission to view this post', 'fluent-community')
29 + ], 404);
30 + }
31 +
32 + /*
33 + * The row's own setting is the default the filter gets handed, rather than a bare
34 + * true. Before this, meta.enable_comments was read nowhere on this path, so a page
35 + * with comments switched off still served its thread to anyone who asked for it.
36 + */
37 + $canViewComments = apply_filters(
38 + 'fluent_community/can_view_comments_' . $feed->type,
39 + FeedsHelper::commentsEnabled($feed),
40 + $feed
41 + );
42 +
24 43 if (!$canViewComments) {
25 44 return [
26 45 'comments' => []
27 46 ];
@@ -69,9 +88,9 @@
69 88
70 89 $text = $this->validateCommentText($request->all());
71 90 $feed = Feed::withoutGlobalScopes()->findOrFail($feedId);
72 91
73 - if ($feed->status != 'published') {
92 + if (!in_array($feed->status, FeedsHelper::getViewableByLinkStatuses(), true)) {
74 93 return $this->sendError([
75 94 'message' => __('This post is not published yet', 'fluent-community')
76 95 ]);
77 96 }
@@ -79,25 +98,8 @@
79 98 $this->verifyCreateCommentPermission($feed);
80 99
81 100 $requestData = $request->all();
82 101
83 - // Check for duplicate (only for comments with text)
84 - if ($text) {
85 - $skipDuplicateCheck = apply_filters('fluent_community/disable_duplicate_comment_check', false, get_current_user_id(), $feed->id);
86 - if (!$skipDuplicateCheck) {
87 - $exist = Comment::where('user_id', get_current_user_id())
88 - ->where('message', $text)
89 - ->where('post_id', $feed->id)
90 - ->first();
91 -
92 - if ($exist) {
93 - return $this->sendError([
94 - 'message' => __('No duplicate comment please!', 'fluent-community')
95 - ]);
96 - }
97 - }
98 - }
99 -
100 102 [$markdown, $inlineMedias] = FeedsHelper::replaceImageUrlsWithRealMediaArchive($text);
101 103 $mentions = FeedsHelper::getMentions($markdown, $feed->space_id, true);
102 104 $commentHtml = $this->generateCommentHtml($markdown, $mentions);
103 105
@@ -129,12 +131,33 @@
129 131 do_action('fluent_community/before_comment_create', $commentData, $feed);
130 132
131 133 $commentData = apply_filters('fluent_community/comment/comment_data', $commentData, $feed);
132 134
133 - $comment = Comment::create($commentData);
135 + // Only comments with text are duplicate checked
136 + $shouldCheckDuplicate = $text && !apply_filters('fluent_community/disable_duplicate_comment_check', false, get_current_user_id(), $feed->id);
134 137
138 + // Serialize a member's concurrent submissions by locking their profile row,
139 + // so parallel matching requests cannot pass the duplicate check and both insert.
140 + $comment = Helper::dbTransaction(function () use ($commentData, $feed, $text, $shouldCheckDuplicate) {
141 + XProfile::where('user_id', get_current_user_id())->lockForUpdate()->first();
142 +
143 + if ($shouldCheckDuplicate && Comment::where('user_id', get_current_user_id())->where('message', $text)->where('post_id', $feed->id)->first()) {
144 + return null;
145 + }
146 +
147 + $newComment = Comment::create($commentData);
148 + Feed::withoutGlobalScopes()->where('id', $feed->id)->increment('comments_count');
149 +
150 + return $newComment;
151 + });
152 +
153 + if (!$comment) {
154 + return $this->sendError([
155 + 'message' => __('No duplicate comment please!', 'fluent-community')
156 + ]);
157 + }
158 +
135 159 $feed->comments_count = $feed->comments_count + 1;
136 - $feed->save();
137 160
138 161
139 162 // Merge and save all media in one loop
140 163 $mediaItems = $mediaItems ? (is_array($mediaItems) ? $mediaItems : [$mediaItems]) : [];
@@ -160,12 +183,13 @@
160 183 if ($comment->status != 'published') {
161 184 do_action('fluent_community/comment/new_comment_' . $comment->status, $comment, $feed);
162 185 /* translators: %$s is replaced by the status of the comment */
163 186 $message = sprintf(__('Your comment has been marked as %s', 'fluent-community'), $comment->status);
164 - return [
187 + $response = [
165 188 'comment' => $comment,
166 189 'message' => $message
167 190 ];
191 + return apply_filters('fluent_community/comment/new_comment_response', $response, $comment);
168 192 }
169 193
170 194 do_action('fluent_community/comment_added_' . $feed->type, $comment, $feed);
171 195 do_action('fluent_community/comment_added', $comment, $feed, Arr::get($mentions, 'users', []));
@@ -184,8 +208,15 @@
184 208 $this->verifySpacePermission($feed);
185 209
186 210 $requestData = $request->all();
187 211 $comment = Comment::findOrFail($commentId);
212 +
213 + if ($comment->post_id != $feed->id) {
214 + return $this->sendError([
215 + 'message' => __('Invalid comment', 'fluent-community')
216 + ]);
217 + }
218 +
188 219 $user = $this->getUser(true);
189 220
190 221 $requestData['is_admin'] = $user->hasPermissionOrInCurrentSpace('community_moderator', $feed->space);
191 222
@@ -267,8 +298,14 @@
267 298 $feed = Feed::withoutGlobalScopes()->findOrFail($feedId);
268 299
269 300 $comment = Comment::findOrFail($commentId);
270 301
302 + if ($comment->post_id != $feed->id) {
303 + return $this->sendError([
304 + 'message' => __('Invalid comment', 'fluent-community')
305 + ]);
306 + }
307 +
271 308 $user = $this->getUser(true);
272 309
273 310 $isMod = $user && $user->hasPermissionOrInCurrentSpace('community_moderator', $feed->space);
274 311 $isAdmin = $user && $user->hasPermissionOrInCurrentSpace('community_admin', $feed->space);
@@ -387,14 +424,17 @@
387 424 return [$commentData, [$existingMedia]];
388 425 }
389 426 }
390 427
428 + // type/provider reach :class bindings and width/height a :style binding in
429 + // _MediaPreview.vue. Neither is an executable sink, but the stored values are
430 + // request-supplied so they are normalised here rather than trusted.
391 431 $commentData['meta']['media_preview'] = array_filter([
392 432 'image' => sanitize_url(Arr::get($requestData, 'meta.media_preview.image', '')),
393 - 'type' => Arr::get($requestData, 'meta.media_preview.type', 'image'),
394 - 'provider' => Arr::get($requestData, 'meta.media_preview.provider', ''),
395 - 'height' => Arr::get($requestData, 'meta.media_preview.height', 0),
396 - 'width' => Arr::get($requestData, 'meta.media_preview.width', 0),
433 + 'type' => sanitize_text_field(Arr::get($requestData, 'meta.media_preview.type', 'image')),
434 + 'provider' => sanitize_text_field(Arr::get($requestData, 'meta.media_preview.provider', '')),
435 + 'height' => (int) Arr::get($requestData, 'meta.media_preview.height', 0),
436 + 'width' => (int) Arr::get($requestData, 'meta.media_preview.width', 0),
397 437 ]);
398 438
399 439 return [$commentData, []];
400 440 }
@@ -400,9 +440,9 @@
400 440 }
401 441
402 442 private function validateCommentText($data)
403 443 {
404 - $text = trim(Arr::get($data, 'comment'));
444 + $text = trim((string) Arr::get($data, 'comment', ''));
405 445 $text = CustomSanitizer::unslashMarkdown($text);
406 446
407 447 // Decode HTML entities (e.g.,   for space) and strip all whitespace for validation
408 448 $textForValidation = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
@@ -420,9 +460,10 @@
420 460 }
421 461
422 462 $maxCommentLength = apply_filters('fluent_community/max_comment_char_length', 10000);
423 463 if ($text && strlen($text) > $maxCommentLength) {
424 - throw new \Exception(esc_html__('Comment text is too long', 'fluent-community'), 422);
464 + /* translators: %s is the maximum allowed character count */
465 + throw new \Exception(esc_html(sprintf(__('The comment is too long. Please keep it under %s characters.', 'fluent-community'), number_format($maxCommentLength))), 422);
425 466 }
426 467
427 468 return $text;
428 469 }
@@ -428,9 +469,9 @@
428 469 }
429 470
430 471 private function verifyCreateCommentPermission($feed)
431 472 {
432 - if (Arr::get($feed->meta, 'comments_disabled') === 'yes') {
473 + if (!FeedsHelper::commentsEnabled($feed)) {
433 474 throw new \Exception(esc_html__('Comments are disabled for this post', 'fluent-community'));
434 475 }
435 476
436 477 $this->verifySpacePermission($feed);
@@ -479,17 +520,18 @@
479 520 {
480 521 $userId = get_current_user_id();
481 522 $feed = Feed::withoutGlobalScopes()->byUserAccess($userId)->findOrFail($feed_id);
482 523 $type = $request->get('react_type', 'like');
524 + $type = in_array($type, ['like', 'bookmark'], true) ? $type : 'like';
483 525 $willRemove = $request->get('remove');
484 526
485 - if ($feed->status != 'published') {
527 + if (!in_array($feed->status, FeedsHelper::getViewableByLinkStatuses(), true)) {
486 528 return $this->sendError([
487 529 'message' => __('This post is not published yet', 'fluent-community')
488 530 ]);
489 531 }
490 532
491 - if ($userId === $feed->user_id && apply_filters('fluent_community/disable_self_post_react', false, $feed)) {
533 + if (!$willRemove && (int) $userId === (int) $feed->user_id && apply_filters('fluent_community/disable_self_post_react', false, $feed)) {
492 534 return $this->sendError([
493 535 'message' => __('You cannot react to your own post', 'fluent-community')
494 536 ]);
495 537 }
@@ -585,9 +627,9 @@
585 627 }
586 628
587 629 public function toggleReaction(Request $request, $feedId, $commentId)
588 630 {
589 - $feed = Feed::withoutGlobalScopes()->findOrFail($feedId);
631 + $feed = Feed::withoutGlobalScopes()->byUserAccess(get_current_user_id())->findOrFail($feedId);
590 632 $comment = Comment::findOrFail($commentId);
591 633
592 634 if ($comment->post_id != $feed->id) {
593 635 return $this->sendError([
@@ -601,28 +643,38 @@
601 643 $user->verifySpacePermission('registered', $feed->space);
602 644 }
603 645
604 646 $userId = get_current_user_id();
605 - if ($userId === $comment->user_id && apply_filters('fluent_community/disable_self_comment_react', false, $feed)) {
647 + $reactionState = !!$request->get('state', false);
648 +
649 + if ($reactionState && (int) $userId === (int) $comment->user_id && apply_filters('fluent_community/disable_self_comment_react', false, $feed)) {
606 650 return $this->sendError([
607 651 'message' => __('You cannot react to your own comment', 'fluent-community')
608 652 ]);
609 653 }
610 654
611 - $reactionState = !!$request->get('state', false);
655 + if ($reactionState) {
656 + // Serialize concurrent reactions on this comment by locking its row,
657 + // so parallel add requests cannot each insert a duplicate reaction.
658 + $reaction = Helper::dbTransaction(function () use ($comment, $feed) {
659 + XProfile::where('user_id', get_current_user_id())->lockForUpdate()->first();
612 660
613 - if ($reactionState) {
614 - // add or update the reaction
615 - $reaction = Reaction::firstOrCreate([
616 - 'user_id' => get_current_user_id(),
617 - 'object_id' => $comment->id,
618 - 'object_type' => 'comment',
619 - 'parent_id' => $feed->id
620 - ]);
661 + $reaction = Reaction::firstOrCreate([
662 + 'user_id' => get_current_user_id(),
663 + 'object_id' => $comment->id,
664 + 'object_type' => 'comment',
665 + 'parent_id' => $feed->id
666 + ]);
621 667
668 + if ($reaction->wasRecentlyCreated) {
669 + Comment::where('id', $comment->id)->increment('reactions_count');
670 + $comment->reactions_count = $comment->reactions_count + 1;
671 + }
672 +
673 + return $reaction;
674 + });
675 +
622 676 if ($reaction->wasRecentlyCreated) {
623 - $comment->reactions_count = $comment->reactions_count + 1;
624 - $comment->save();
625 677 do_action('fluent_community/comment/react_added', $reaction, $comment, $feed);
626 678 }
627 679 } else {
628 680 // remove the reaction