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.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 1.1.0 All 77 releases
← All changes | app/Http/Controllers/CommentsController.php +93 -43 2.6.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]) : [];
@@ -185,8 +208,15 @@
185 208 $this->verifySpacePermission($feed);
186 209
187 210 $requestData = $request->all();
188 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 +
189 219 $user = $this->getUser(true);
190 220
191 221 $requestData['is_admin'] = $user->hasPermissionOrInCurrentSpace('community_moderator', $feed->space);
192 222
@@ -268,8 +298,14 @@
268 298 $feed = Feed::withoutGlobalScopes()->findOrFail($feedId);
269 299
270 300 $comment = Comment::findOrFail($commentId);
271 301
302 + if ($comment->post_id != $feed->id) {
303 + return $this->sendError([
304 + 'message' => __('Invalid comment', 'fluent-community')
305 + ]);
306 + }
307 +
272 308 $user = $this->getUser(true);
273 309
274 310 $isMod = $user && $user->hasPermissionOrInCurrentSpace('community_moderator', $feed->space);
275 311 $isAdmin = $user && $user->hasPermissionOrInCurrentSpace('community_admin', $feed->space);
@@ -388,14 +424,17 @@
388 424 return [$commentData, [$existingMedia]];
389 425 }
390 426 }
391 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.
392 431 $commentData['meta']['media_preview'] = array_filter([
393 432 'image' => sanitize_url(Arr::get($requestData, 'meta.media_preview.image', '')),
394 - 'type' => Arr::get($requestData, 'meta.media_preview.type', 'image'),
395 - 'provider' => Arr::get($requestData, 'meta.media_preview.provider', ''),
396 - 'height' => Arr::get($requestData, 'meta.media_preview.height', 0),
397 - '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),
398 437 ]);
399 438
400 439 return [$commentData, []];
401 440 }
@@ -401,9 +440,9 @@
401 440 }
402 441
403 442 private function validateCommentText($data)
404 443 {
405 - $text = trim(Arr::get($data, 'comment'));
444 + $text = trim((string) Arr::get($data, 'comment', ''));
406 445 $text = CustomSanitizer::unslashMarkdown($text);
407 446
408 447 // Decode HTML entities (e.g.,   for space) and strip all whitespace for validation
409 448 $textForValidation = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
@@ -430,9 +469,9 @@
430 469 }
431 470
432 471 private function verifyCreateCommentPermission($feed)
433 472 {
434 - if (Arr::get($feed->meta, 'comments_disabled') === 'yes') {
473 + if (!FeedsHelper::commentsEnabled($feed)) {
435 474 throw new \Exception(esc_html__('Comments are disabled for this post', 'fluent-community'));
436 475 }
437 476
438 477 $this->verifySpacePermission($feed);
@@ -481,17 +520,18 @@
481 520 {
482 521 $userId = get_current_user_id();
483 522 $feed = Feed::withoutGlobalScopes()->byUserAccess($userId)->findOrFail($feed_id);
484 523 $type = $request->get('react_type', 'like');
524 + $type = in_array($type, ['like', 'bookmark'], true) ? $type : 'like';
485 525 $willRemove = $request->get('remove');
486 526
487 - if ($feed->status != 'published') {
527 + if (!in_array($feed->status, FeedsHelper::getViewableByLinkStatuses(), true)) {
488 528 return $this->sendError([
489 529 'message' => __('This post is not published yet', 'fluent-community')
490 530 ]);
491 531 }
492 532
493 - 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)) {
494 534 return $this->sendError([
495 535 'message' => __('You cannot react to your own post', 'fluent-community')
496 536 ]);
497 537 }
@@ -587,9 +627,9 @@
587 627 }
588 628
589 629 public function toggleReaction(Request $request, $feedId, $commentId)
590 630 {
591 - $feed = Feed::withoutGlobalScopes()->findOrFail($feedId);
631 + $feed = Feed::withoutGlobalScopes()->byUserAccess(get_current_user_id())->findOrFail($feedId);
592 632 $comment = Comment::findOrFail($commentId);
593 633
594 634 if ($comment->post_id != $feed->id) {
595 635 return $this->sendError([
@@ -603,28 +643,38 @@
603 643 $user->verifySpacePermission('registered', $feed->space);
604 644 }
605 645
606 646 $userId = get_current_user_id();
607 - 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)) {
608 650 return $this->sendError([
609 651 'message' => __('You cannot react to your own comment', 'fluent-community')
610 652 ]);
611 653 }
612 654
613 - $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();
614 660
615 - if ($reactionState) {
616 - // add or update the reaction
617 - $reaction = Reaction::firstOrCreate([
618 - 'user_id' => get_current_user_id(),
619 - 'object_id' => $comment->id,
620 - 'object_type' => 'comment',
621 - 'parent_id' => $feed->id
622 - ]);
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 + ]);
623 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 +
624 676 if ($reaction->wasRecentlyCreated) {
625 - $comment->reactions_count = $comment->reactions_count + 1;
626 - $comment->save();
627 677 do_action('fluent_community/comment/react_added', $reaction, $comment, $feed);
628 678 }
629 679 } else {
630 680 // remove the reaction