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 +87 -45 2.7.52.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 {
@@ -21,15 +22,24 @@
21 22 $feed = Feed::withoutGlobalScopes()
22 23 ->byUserAccess(get_current_user_id())
23 24 ->findOrFail($feed_id);
24 25
25 - if ($feed->status != 'published' && !$feed->hasEditAccess($this->getUserId())) {
26 + if (!in_array($feed->status, FeedsHelper::getViewableByLinkStatuses(), true) && !$feed->hasEditAccess($this->getUserId())) {
26 27 return $this->sendError([
27 28 'message' => __('Sorry, you do not have permission to view this post', 'fluent-community')
28 29 ], 404);
29 30 }
30 31
31 - $canViewComments = apply_filters('fluent_community/can_view_comments_' . $feed->type, true, $feed);
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 + );
32 42
33 43 if (!$canViewComments) {
34 44 return [
35 45 'comments' => []
@@ -78,9 +88,9 @@
78 88
79 89 $text = $this->validateCommentText($request->all());
80 90 $feed = Feed::withoutGlobalScopes()->findOrFail($feedId);
81 91
82 - if ($feed->status != 'published') {
92 + if (!in_array($feed->status, FeedsHelper::getViewableByLinkStatuses(), true)) {
83 93 return $this->sendError([
84 94 'message' => __('This post is not published yet', 'fluent-community')
85 95 ]);
86 96 }
@@ -88,25 +98,8 @@
88 98 $this->verifyCreateCommentPermission($feed);
89 99
90 100 $requestData = $request->all();
91 101
92 - // Check for duplicate (only for comments with text)
93 - if ($text) {
94 - $skipDuplicateCheck = apply_filters('fluent_community/disable_duplicate_comment_check', false, get_current_user_id(), $feed->id);
95 - if (!$skipDuplicateCheck) {
96 - $exist = Comment::where('user_id', get_current_user_id())
97 - ->where('message', $text)
98 - ->where('post_id', $feed->id)
99 - ->first();
100 -
101 - if ($exist) {
102 - return $this->sendError([
103 - 'message' => __('No duplicate comment please!', 'fluent-community')
104 - ]);
105 - }
106 - }
107 - }
108 -
109 102 [$markdown, $inlineMedias] = FeedsHelper::replaceImageUrlsWithRealMediaArchive($text);
110 103 $mentions = FeedsHelper::getMentions($markdown, $feed->space_id, true);
111 104 $commentHtml = $this->generateCommentHtml($markdown, $mentions);
112 105
@@ -138,14 +131,41 @@
138 131 do_action('fluent_community/before_comment_create', $commentData, $feed);
139 132
140 133 $commentData = apply_filters('fluent_community/comment/comment_data', $commentData, $feed);
141 134
142 - $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);
143 137
144 - $feed->comments_count = $feed->comments_count + 1;
145 - $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();
146 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 + }
147 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 +
148 168 // Merge and save all media in one loop
149 169 $mediaItems = $mediaItems ? (is_array($mediaItems) ? $mediaItems : [$mediaItems]) : [];
150 170
151 171 if ($inlineMedias) {
@@ -284,8 +304,14 @@
284 304 $feed = Feed::withoutGlobalScopes()->findOrFail($feedId);
285 305
286 306 $comment = Comment::findOrFail($commentId);
287 307
308 + if ($comment->post_id != $feed->id) {
309 + return $this->sendError([
310 + 'message' => __('Invalid comment', 'fluent-community')
311 + ]);
312 + }
313 +
288 314 $user = $this->getUser(true);
289 315
290 316 $isMod = $user && $user->hasPermissionOrInCurrentSpace('community_moderator', $feed->space);
291 317 $isAdmin = $user && $user->hasPermissionOrInCurrentSpace('community_admin', $feed->space);
@@ -404,14 +430,17 @@
404 430 return [$commentData, [$existingMedia]];
405 431 }
406 432 }
407 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.
408 437 $commentData['meta']['media_preview'] = array_filter([
409 438 'image' => sanitize_url(Arr::get($requestData, 'meta.media_preview.image', '')),
410 - 'type' => Arr::get($requestData, 'meta.media_preview.type', 'image'),
411 - 'provider' => Arr::get($requestData, 'meta.media_preview.provider', ''),
412 - 'height' => Arr::get($requestData, 'meta.media_preview.height', 0),
413 - '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),
414 443 ]);
415 444
416 445 return [$commentData, []];
417 446 }
@@ -417,9 +446,9 @@
417 446 }
418 447
419 448 private function validateCommentText($data)
420 449 {
421 - $text = trim(Arr::get($data, 'comment'));
450 + $text = trim((string) Arr::get($data, 'comment', ''));
422 451 $text = CustomSanitizer::unslashMarkdown($text);
423 452
424 453 // Decode HTML entities (e.g.,   for space) and strip all whitespace for validation
425 454 $textForValidation = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
@@ -446,9 +475,9 @@
446 475 }
447 476
448 477 private function verifyCreateCommentPermission($feed)
449 478 {
450 - if (Arr::get($feed->meta, 'comments_disabled') === 'yes') {
479 + if (!FeedsHelper::commentsEnabled($feed)) {
451 480 throw new \Exception(esc_html__('Comments are disabled for this post', 'fluent-community'));
452 481 }
453 482
454 483 $this->verifySpacePermission($feed);
@@ -497,17 +526,18 @@
497 526 {
498 527 $userId = get_current_user_id();
499 528 $feed = Feed::withoutGlobalScopes()->byUserAccess($userId)->findOrFail($feed_id);
500 529 $type = $request->get('react_type', 'like');
530 + $type = in_array($type, ['like', 'bookmark'], true) ? $type : 'like';
501 531 $willRemove = $request->get('remove');
502 532
503 - if ($feed->status != 'published') {
533 + if (!in_array($feed->status, FeedsHelper::getViewableByLinkStatuses(), true)) {
504 534 return $this->sendError([
505 535 'message' => __('This post is not published yet', 'fluent-community')
506 536 ]);
507 537 }
508 538
509 - 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)) {
510 540 return $this->sendError([
511 541 'message' => __('You cannot react to your own post', 'fluent-community')
512 542 ]);
513 543 }
@@ -589,9 +619,11 @@
589 619 }
590 620
591 621 $comment->delete();
592 622
593 - $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();
594 626 $feed->timestamps = false; // Don't update the updated_at timestamp
595 627 $feed->save();
596 628
597 629 do_action('fluent_community/comment_deleted_' . $feed->type, $commentId, $feed);
@@ -603,9 +635,9 @@
603 635 }
604 636
605 637 public function toggleReaction(Request $request, $feedId, $commentId)
606 638 {
607 - $feed = Feed::withoutGlobalScopes()->findOrFail($feedId);
639 + $feed = Feed::withoutGlobalScopes()->byUserAccess(get_current_user_id())->findOrFail($feedId);
608 640 $comment = Comment::findOrFail($commentId);
609 641
610 642 if ($comment->post_id != $feed->id) {
611 643 return $this->sendError([
@@ -619,28 +651,38 @@
619 651 $user->verifySpacePermission('registered', $feed->space);
620 652 }
621 653
622 654 $userId = get_current_user_id();
623 - 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)) {
624 658 return $this->sendError([
625 659 'message' => __('You cannot react to your own comment', 'fluent-community')
626 660 ]);
627 661 }
628 662
629 - $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();
630 668
631 - if ($reactionState) {
632 - // add or update the reaction
633 - $reaction = Reaction::firstOrCreate([
634 - 'user_id' => get_current_user_id(),
635 - 'object_id' => $comment->id,
636 - 'object_type' => 'comment',
637 - 'parent_id' => $feed->id
638 - ]);
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 + ]);
639 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 +
640 684 if ($reaction->wasRecentlyCreated) {
641 - $comment->reactions_count = $comment->reactions_count + 1;
642 - $comment->save();
643 685 do_action('fluent_community/comment/react_added', $reaction, $comment, $feed);
644 686 }
645 687 } else {
646 688 // remove the reaction