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/Http/Controllers/CommentsController.php +103 -45 2.6.02.11.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,14 +131,41 @@
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
135 - $feed->comments_count = $feed->comments_count + 1;
136 - $feed->save();
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();
137 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 + }
138 146
147 + $newComment = Comment::create($commentData);
148 +
149 + // A held comment is not visible yet, so it must not be counted until it is approved.
150 + if ($newComment->status === 'published') {
151 + Feed::withoutGlobalScopes()->where('id', $feed->id)->increment('comments_count');
152 + }
153 +
154 + return $newComment;
155 + });
156 +
157 + if (!$comment) {
158 + return $this->sendError([
159 + 'message' => __('No duplicate comment please!', 'fluent-community')
160 + ]);
161 + }
162 +
163 + if ($comment->status === 'published') {
164 + $feed->comments_count = $feed->comments_count + 1;
165 + }
166 +
167 +
139 168 // Merge and save all media in one loop
140 169 $mediaItems = $mediaItems ? (is_array($mediaItems) ? $mediaItems : [$mediaItems]) : [];
141 170
142 171 if ($inlineMedias) {
@@ -185,8 +214,15 @@
185 214 $this->verifySpacePermission($feed);
186 215
187 216 $requestData = $request->all();
188 217 $comment = Comment::findOrFail($commentId);
218 +
219 + if ($comment->post_id != $feed->id) {
220 + return $this->sendError([
221 + 'message' => __('Invalid comment', 'fluent-community')
222 + ]);
223 + }
224 +
189 225 $user = $this->getUser(true);
190 226
191 227 $requestData['is_admin'] = $user->hasPermissionOrInCurrentSpace('community_moderator', $feed->space);
192 228
@@ -268,8 +304,14 @@
268 304 $feed = Feed::withoutGlobalScopes()->findOrFail($feedId);
269 305
270 306 $comment = Comment::findOrFail($commentId);
271 307
308 + if ($comment->post_id != $feed->id) {
309 + return $this->sendError([
310 + 'message' => __('Invalid comment', 'fluent-community')
311 + ]);
312 + }
313 +
272 314 $user = $this->getUser(true);
273 315
274 316 $isMod = $user && $user->hasPermissionOrInCurrentSpace('community_moderator', $feed->space);
275 317 $isAdmin = $user && $user->hasPermissionOrInCurrentSpace('community_admin', $feed->space);
@@ -388,14 +430,17 @@
388 430 return [$commentData, [$existingMedia]];
389 431 }
390 432 }
391 433
434 + // type/provider reach :class bindings and width/height a :style binding in
435 + // _MediaPreview.vue. Neither is an executable sink, but the stored values are
436 + // request-supplied so they are normalised here rather than trusted.
392 437 $commentData['meta']['media_preview'] = array_filter([
393 438 '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),
439 + 'type' => sanitize_text_field(Arr::get($requestData, 'meta.media_preview.type', 'image')),
440 + 'provider' => sanitize_text_field(Arr::get($requestData, 'meta.media_preview.provider', '')),
441 + 'height' => (int) Arr::get($requestData, 'meta.media_preview.height', 0),
442 + 'width' => (int) Arr::get($requestData, 'meta.media_preview.width', 0),
398 443 ]);
399 444
400 445 return [$commentData, []];
401 446 }
@@ -401,9 +446,9 @@
401 446 }
402 447
403 448 private function validateCommentText($data)
404 449 {
405 - $text = trim(Arr::get($data, 'comment'));
450 + $text = trim((string) Arr::get($data, 'comment', ''));
406 451 $text = CustomSanitizer::unslashMarkdown($text);
407 452
408 453 // Decode HTML entities (e.g.,   for space) and strip all whitespace for validation
409 454 $textForValidation = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
@@ -430,9 +475,9 @@
430 475 }
431 476
432 477 private function verifyCreateCommentPermission($feed)
433 478 {
434 - if (Arr::get($feed->meta, 'comments_disabled') === 'yes') {
479 + if (!FeedsHelper::commentsEnabled($feed)) {
435 480 throw new \Exception(esc_html__('Comments are disabled for this post', 'fluent-community'));
436 481 }
437 482
438 483 $this->verifySpacePermission($feed);
@@ -481,17 +526,18 @@
481 526 {
482 527 $userId = get_current_user_id();
483 528 $feed = Feed::withoutGlobalScopes()->byUserAccess($userId)->findOrFail($feed_id);
484 529 $type = $request->get('react_type', 'like');
530 + $type = in_array($type, ['like', 'bookmark'], true) ? $type : 'like';
485 531 $willRemove = $request->get('remove');
486 532
487 - if ($feed->status != 'published') {
533 + if (!in_array($feed->status, FeedsHelper::getViewableByLinkStatuses(), true)) {
488 534 return $this->sendError([
489 535 'message' => __('This post is not published yet', 'fluent-community')
490 536 ]);
491 537 }
492 538
493 - if ($userId === $feed->user_id && apply_filters('fluent_community/disable_self_post_react', false, $feed)) {
539 + if (!$willRemove && (int) $userId === (int) $feed->user_id && apply_filters('fluent_community/disable_self_post_react', false, $feed)) {
494 540 return $this->sendError([
495 541 'message' => __('You cannot react to your own post', 'fluent-community')
496 542 ]);
497 543 }
@@ -573,9 +619,11 @@
573 619 }
574 620
575 621 $comment->delete();
576 622
577 - $feed->comments_count = Comment::where('post_id', $feed->id)->count();
623 + $feed->comments_count = Comment::where('post_id', $feed->id)
624 + ->where('status', 'published')
625 + ->count();
578 626 $feed->timestamps = false; // Don't update the updated_at timestamp
579 627 $feed->save();
580 628
581 629 do_action('fluent_community/comment_deleted_' . $feed->type, $commentId, $feed);
@@ -587,9 +635,9 @@
587 635 }
588 636
589 637 public function toggleReaction(Request $request, $feedId, $commentId)
590 638 {
591 - $feed = Feed::withoutGlobalScopes()->findOrFail($feedId);
639 + $feed = Feed::withoutGlobalScopes()->byUserAccess(get_current_user_id())->findOrFail($feedId);
592 640 $comment = Comment::findOrFail($commentId);
593 641
594 642 if ($comment->post_id != $feed->id) {
595 643 return $this->sendError([
@@ -603,28 +651,38 @@
603 651 $user->verifySpacePermission('registered', $feed->space);
604 652 }
605 653
606 654 $userId = get_current_user_id();
607 - if ($userId === $comment->user_id && apply_filters('fluent_community/disable_self_comment_react', false, $feed)) {
655 + $reactionState = !!$request->get('state', false);
656 +
657 + if ($reactionState && (int) $userId === (int) $comment->user_id && apply_filters('fluent_community/disable_self_comment_react', false, $feed)) {
608 658 return $this->sendError([
609 659 'message' => __('You cannot react to your own comment', 'fluent-community')
610 660 ]);
611 661 }
612 662
613 - $reactionState = !!$request->get('state', false);
663 + if ($reactionState) {
664 + // Serialize concurrent reactions on this comment by locking its row,
665 + // so parallel add requests cannot each insert a duplicate reaction.
666 + $reaction = Helper::dbTransaction(function () use ($comment, $feed) {
667 + XProfile::where('user_id', get_current_user_id())->lockForUpdate()->first();
614 668
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 - ]);
669 + $reaction = Reaction::firstOrCreate([
670 + 'user_id' => get_current_user_id(),
671 + 'object_id' => $comment->id,
672 + 'object_type' => 'comment',
673 + 'parent_id' => $feed->id
674 + ]);
623 675
676 + if ($reaction->wasRecentlyCreated) {
677 + Comment::where('id', $comment->id)->increment('reactions_count');
678 + $comment->reactions_count = $comment->reactions_count + 1;
679 + }
680 +
681 + return $reaction;
682 + });
683 +
624 684 if ($reaction->wasRecentlyCreated) {
625 - $comment->reactions_count = $comment->reactions_count + 1;
626 - $comment->save();
627 685 do_action('fluent_community/comment/react_added', $reaction, $comment, $feed);
628 686 }
629 687 } else {
630 688 // remove the reaction