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 +86 -43 2.7.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]) : [];
@@ -275,8 +298,14 @@
275 298 $feed = Feed::withoutGlobalScopes()->findOrFail($feedId);
276 299
277 300 $comment = Comment::findOrFail($commentId);
278 301
302 + if ($comment->post_id != $feed->id) {
303 + return $this->sendError([
304 + 'message' => __('Invalid comment', 'fluent-community')
305 + ]);
306 + }
307 +
279 308 $user = $this->getUser(true);
280 309
281 310 $isMod = $user && $user->hasPermissionOrInCurrentSpace('community_moderator', $feed->space);
282 311 $isAdmin = $user && $user->hasPermissionOrInCurrentSpace('community_admin', $feed->space);
@@ -395,14 +424,17 @@
395 424 return [$commentData, [$existingMedia]];
396 425 }
397 426 }
398 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.
399 431 $commentData['meta']['media_preview'] = array_filter([
400 432 'image' => sanitize_url(Arr::get($requestData, 'meta.media_preview.image', '')),
401 - 'type' => Arr::get($requestData, 'meta.media_preview.type', 'image'),
402 - 'provider' => Arr::get($requestData, 'meta.media_preview.provider', ''),
403 - 'height' => Arr::get($requestData, 'meta.media_preview.height', 0),
404 - '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),
405 437 ]);
406 438
407 439 return [$commentData, []];
408 440 }
@@ -408,9 +440,9 @@
408 440 }
409 441
410 442 private function validateCommentText($data)
411 443 {
412 - $text = trim(Arr::get($data, 'comment'));
444 + $text = trim((string) Arr::get($data, 'comment', ''));
413 445 $text = CustomSanitizer::unslashMarkdown($text);
414 446
415 447 // Decode HTML entities (e.g.,   for space) and strip all whitespace for validation
416 448 $textForValidation = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
@@ -437,9 +469,9 @@
437 469 }
438 470
439 471 private function verifyCreateCommentPermission($feed)
440 472 {
441 - if (Arr::get($feed->meta, 'comments_disabled') === 'yes') {
473 + if (!FeedsHelper::commentsEnabled($feed)) {
442 474 throw new \Exception(esc_html__('Comments are disabled for this post', 'fluent-community'));
443 475 }
444 476
445 477 $this->verifySpacePermission($feed);
@@ -488,17 +520,18 @@
488 520 {
489 521 $userId = get_current_user_id();
490 522 $feed = Feed::withoutGlobalScopes()->byUserAccess($userId)->findOrFail($feed_id);
491 523 $type = $request->get('react_type', 'like');
524 + $type = in_array($type, ['like', 'bookmark'], true) ? $type : 'like';
492 525 $willRemove = $request->get('remove');
493 526
494 - if ($feed->status != 'published') {
527 + if (!in_array($feed->status, FeedsHelper::getViewableByLinkStatuses(), true)) {
495 528 return $this->sendError([
496 529 'message' => __('This post is not published yet', 'fluent-community')
497 530 ]);
498 531 }
499 532
500 - 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)) {
501 534 return $this->sendError([
502 535 'message' => __('You cannot react to your own post', 'fluent-community')
503 536 ]);
504 537 }
@@ -594,9 +627,9 @@
594 627 }
595 628
596 629 public function toggleReaction(Request $request, $feedId, $commentId)
597 630 {
598 - $feed = Feed::withoutGlobalScopes()->findOrFail($feedId);
631 + $feed = Feed::withoutGlobalScopes()->byUserAccess(get_current_user_id())->findOrFail($feedId);
599 632 $comment = Comment::findOrFail($commentId);
600 633
601 634 if ($comment->post_id != $feed->id) {
602 635 return $this->sendError([
@@ -610,28 +643,38 @@
610 643 $user->verifySpacePermission('registered', $feed->space);
611 644 }
612 645
613 646 $userId = get_current_user_id();
614 - 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)) {
615 650 return $this->sendError([
616 651 'message' => __('You cannot react to your own comment', 'fluent-community')
617 652 ]);
618 653 }
619 654
620 - $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();
621 660
622 - if ($reactionState) {
623 - // add or update the reaction
624 - $reaction = Reaction::firstOrCreate([
625 - 'user_id' => get_current_user_id(),
626 - 'object_id' => $comment->id,
627 - 'object_type' => 'comment',
628 - 'parent_id' => $feed->id
629 - ]);
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 + ]);
630 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 +
631 676 if ($reaction->wasRecentlyCreated) {
632 - $comment->reactions_count = $comment->reactions_count + 1;
633 - $comment->save();
634 677 do_action('fluent_community/comment/react_added', $reaction, $comment, $feed);
635 678 }
636 679 } else {
637 680 // remove the reaction