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 +105 -46 2.5.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) {
@@ -160,12 +189,13 @@
160 189 if ($comment->status != 'published') {
161 190 do_action('fluent_community/comment/new_comment_' . $comment->status, $comment, $feed);
162 191 /* translators: %$s is replaced by the status of the comment */
163 192 $message = sprintf(__('Your comment has been marked as %s', 'fluent-community'), $comment->status);
164 - return [
193 + $response = [
165 194 'comment' => $comment,
166 195 'message' => $message
167 196 ];
197 + return apply_filters('fluent_community/comment/new_comment_response', $response, $comment);
168 198 }
169 199
170 200 do_action('fluent_community/comment_added_' . $feed->type, $comment, $feed);
171 201 do_action('fluent_community/comment_added', $comment, $feed, Arr::get($mentions, 'users', []));
@@ -184,8 +214,15 @@
184 214 $this->verifySpacePermission($feed);
185 215
186 216 $requestData = $request->all();
187 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 +
188 225 $user = $this->getUser(true);
189 226
190 227 $requestData['is_admin'] = $user->hasPermissionOrInCurrentSpace('community_moderator', $feed->space);
191 228
@@ -267,8 +304,14 @@
267 304 $feed = Feed::withoutGlobalScopes()->findOrFail($feedId);
268 305
269 306 $comment = Comment::findOrFail($commentId);
270 307
308 + if ($comment->post_id != $feed->id) {
309 + return $this->sendError([
310 + 'message' => __('Invalid comment', 'fluent-community')
311 + ]);
312 + }
313 +
271 314 $user = $this->getUser(true);
272 315
273 316 $isMod = $user && $user->hasPermissionOrInCurrentSpace('community_moderator', $feed->space);
274 317 $isAdmin = $user && $user->hasPermissionOrInCurrentSpace('community_admin', $feed->space);
@@ -387,14 +430,17 @@
387 430 return [$commentData, [$existingMedia]];
388 431 }
389 432 }
390 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.
391 437 $commentData['meta']['media_preview'] = array_filter([
392 438 '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),
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),
397 443 ]);
398 444
399 445 return [$commentData, []];
400 446 }
@@ -400,9 +446,9 @@
400 446 }
401 447
402 448 private function validateCommentText($data)
403 449 {
404 - $text = trim(Arr::get($data, 'comment'));
450 + $text = trim((string) Arr::get($data, 'comment', ''));
405 451 $text = CustomSanitizer::unslashMarkdown($text);
406 452
407 453 // Decode HTML entities (e.g.,   for space) and strip all whitespace for validation
408 454 $textForValidation = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
@@ -429,9 +475,9 @@
429 475 }
430 476
431 477 private function verifyCreateCommentPermission($feed)
432 478 {
433 - if (Arr::get($feed->meta, 'comments_disabled') === 'yes') {
479 + if (!FeedsHelper::commentsEnabled($feed)) {
434 480 throw new \Exception(esc_html__('Comments are disabled for this post', 'fluent-community'));
435 481 }
436 482
437 483 $this->verifySpacePermission($feed);
@@ -480,17 +526,18 @@
480 526 {
481 527 $userId = get_current_user_id();
482 528 $feed = Feed::withoutGlobalScopes()->byUserAccess($userId)->findOrFail($feed_id);
483 529 $type = $request->get('react_type', 'like');
530 + $type = in_array($type, ['like', 'bookmark'], true) ? $type : 'like';
484 531 $willRemove = $request->get('remove');
485 532
486 - if ($feed->status != 'published') {
533 + if (!in_array($feed->status, FeedsHelper::getViewableByLinkStatuses(), true)) {
487 534 return $this->sendError([
488 535 'message' => __('This post is not published yet', 'fluent-community')
489 536 ]);
490 537 }
491 538
492 - 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)) {
493 540 return $this->sendError([
494 541 'message' => __('You cannot react to your own post', 'fluent-community')
495 542 ]);
496 543 }
@@ -572,9 +619,11 @@
572 619 }
573 620
574 621 $comment->delete();
575 622
576 - $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();
577 626 $feed->timestamps = false; // Don't update the updated_at timestamp
578 627 $feed->save();
579 628
580 629 do_action('fluent_community/comment_deleted_' . $feed->type, $commentId, $feed);
@@ -586,9 +635,9 @@
586 635 }
587 636
588 637 public function toggleReaction(Request $request, $feedId, $commentId)
589 638 {
590 - $feed = Feed::withoutGlobalScopes()->findOrFail($feedId);
639 + $feed = Feed::withoutGlobalScopes()->byUserAccess(get_current_user_id())->findOrFail($feedId);
591 640 $comment = Comment::findOrFail($commentId);
592 641
593 642 if ($comment->post_id != $feed->id) {
594 643 return $this->sendError([
@@ -602,28 +651,38 @@
602 651 $user->verifySpacePermission('registered', $feed->space);
603 652 }
604 653
605 654 $userId = get_current_user_id();
606 - 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)) {
607 658 return $this->sendError([
608 659 'message' => __('You cannot react to your own comment', 'fluent-community')
609 660 ]);
610 661 }
611 662
612 - $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();
613 668
614 - if ($reactionState) {
615 - // add or update the reaction
616 - $reaction = Reaction::firstOrCreate([
617 - 'user_id' => get_current_user_id(),
618 - 'object_id' => $comment->id,
619 - 'object_type' => 'comment',
620 - 'parent_id' => $feed->id
621 - ]);
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 + ]);
622 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 +
623 684 if ($reaction->wasRecentlyCreated) {
624 - $comment->reactions_count = $comment->reactions_count + 1;
625 - $comment->save();
626 685 do_action('fluent_community/comment/react_added', $reaction, $comment, $feed);
627 686 }
628 687 } else {
629 688 // remove the reaction