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 +95 -44 2.5.02.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');
@@ -429,9 +469,9 @@
429 469 }
430 470
431 471 private function verifyCreateCommentPermission($feed)
432 472 {
433 - if (Arr::get($feed->meta, 'comments_disabled') === 'yes') {
473 + if (!FeedsHelper::commentsEnabled($feed)) {
434 474 throw new \Exception(esc_html__('Comments are disabled for this post', 'fluent-community'));
435 475 }
436 476
437 477 $this->verifySpacePermission($feed);
@@ -480,17 +520,18 @@
480 520 {
481 521 $userId = get_current_user_id();
482 522 $feed = Feed::withoutGlobalScopes()->byUserAccess($userId)->findOrFail($feed_id);
483 523 $type = $request->get('react_type', 'like');
524 + $type = in_array($type, ['like', 'bookmark'], true) ? $type : 'like';
484 525 $willRemove = $request->get('remove');
485 526
486 - if ($feed->status != 'published') {
527 + if (!in_array($feed->status, FeedsHelper::getViewableByLinkStatuses(), true)) {
487 528 return $this->sendError([
488 529 'message' => __('This post is not published yet', 'fluent-community')
489 530 ]);
490 531 }
491 532
492 - 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)) {
493 534 return $this->sendError([
494 535 'message' => __('You cannot react to your own post', 'fluent-community')
495 536 ]);
496 537 }
@@ -586,9 +627,9 @@
586 627 }
587 628
588 629 public function toggleReaction(Request $request, $feedId, $commentId)
589 630 {
590 - $feed = Feed::withoutGlobalScopes()->findOrFail($feedId);
631 + $feed = Feed::withoutGlobalScopes()->byUserAccess(get_current_user_id())->findOrFail($feedId);
591 632 $comment = Comment::findOrFail($commentId);
592 633
593 634 if ($comment->post_id != $feed->id) {
594 635 return $this->sendError([
@@ -602,28 +643,38 @@
602 643 $user->verifySpacePermission('registered', $feed->space);
603 644 }
604 645
605 646 $userId = get_current_user_id();
606 - 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)) {
607 650 return $this->sendError([
608 651 'message' => __('You cannot react to your own comment', 'fluent-community')
609 652 ]);
610 653 }
611 654
612 - $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();
613 660
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 - ]);
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 + ]);
622 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 +
623 676 if ($reaction->wasRecentlyCreated) {
624 - $comment->reactions_count = $comment->reactions_count + 1;
625 - $comment->save();
626 677 do_action('fluent_community/comment/react_added', $reaction, $comment, $feed);
627 678 }
628 679 } else {
629 680 // remove the reaction