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 +77 -43 2.7.52.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 {
@@ -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,12 +131,33 @@
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
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 +
144 159 $feed->comments_count = $feed->comments_count + 1;
145 - $feed->save();
146 160
147 161
148 162 // Merge and save all media in one loop
149 163 $mediaItems = $mediaItems ? (is_array($mediaItems) ? $mediaItems : [$mediaItems]) : [];
@@ -284,8 +298,14 @@
284 298 $feed = Feed::withoutGlobalScopes()->findOrFail($feedId);
285 299
286 300 $comment = Comment::findOrFail($commentId);
287 301
302 + if ($comment->post_id != $feed->id) {
303 + return $this->sendError([
304 + 'message' => __('Invalid comment', 'fluent-community')
305 + ]);
306 + }
307 +
288 308 $user = $this->getUser(true);
289 309
290 310 $isMod = $user && $user->hasPermissionOrInCurrentSpace('community_moderator', $feed->space);
291 311 $isAdmin = $user && $user->hasPermissionOrInCurrentSpace('community_admin', $feed->space);
@@ -404,14 +424,17 @@
404 424 return [$commentData, [$existingMedia]];
405 425 }
406 426 }
407 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.
408 431 $commentData['meta']['media_preview'] = array_filter([
409 432 '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),
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),
414 437 ]);
415 438
416 439 return [$commentData, []];
417 440 }
@@ -417,9 +440,9 @@
417 440 }
418 441
419 442 private function validateCommentText($data)
420 443 {
421 - $text = trim(Arr::get($data, 'comment'));
444 + $text = trim((string) Arr::get($data, 'comment', ''));
422 445 $text = CustomSanitizer::unslashMarkdown($text);
423 446
424 447 // Decode HTML entities (e.g.,   for space) and strip all whitespace for validation
425 448 $textForValidation = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
@@ -446,9 +469,9 @@
446 469 }
447 470
448 471 private function verifyCreateCommentPermission($feed)
449 472 {
450 - if (Arr::get($feed->meta, 'comments_disabled') === 'yes') {
473 + if (!FeedsHelper::commentsEnabled($feed)) {
451 474 throw new \Exception(esc_html__('Comments are disabled for this post', 'fluent-community'));
452 475 }
453 476
454 477 $this->verifySpacePermission($feed);
@@ -497,17 +520,18 @@
497 520 {
498 521 $userId = get_current_user_id();
499 522 $feed = Feed::withoutGlobalScopes()->byUserAccess($userId)->findOrFail($feed_id);
500 523 $type = $request->get('react_type', 'like');
524 + $type = in_array($type, ['like', 'bookmark'], true) ? $type : 'like';
501 525 $willRemove = $request->get('remove');
502 526
503 - if ($feed->status != 'published') {
527 + if (!in_array($feed->status, FeedsHelper::getViewableByLinkStatuses(), true)) {
504 528 return $this->sendError([
505 529 'message' => __('This post is not published yet', 'fluent-community')
506 530 ]);
507 531 }
508 532
509 - 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)) {
510 534 return $this->sendError([
511 535 'message' => __('You cannot react to your own post', 'fluent-community')
512 536 ]);
513 537 }
@@ -603,9 +627,9 @@
603 627 }
604 628
605 629 public function toggleReaction(Request $request, $feedId, $commentId)
606 630 {
607 - $feed = Feed::withoutGlobalScopes()->findOrFail($feedId);
631 + $feed = Feed::withoutGlobalScopes()->byUserAccess(get_current_user_id())->findOrFail($feedId);
608 632 $comment = Comment::findOrFail($commentId);
609 633
610 634 if ($comment->post_id != $feed->id) {
611 635 return $this->sendError([
@@ -619,28 +643,38 @@
619 643 $user->verifySpacePermission('registered', $feed->space);
620 644 }
621 645
622 646 $userId = get_current_user_id();
623 - 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)) {
624 650 return $this->sendError([
625 651 'message' => __('You cannot react to your own comment', 'fluent-community')
626 652 ]);
627 653 }
628 654
629 - $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();
630 660
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 - ]);
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 + ]);
639 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 +
640 676 if ($reaction->wasRecentlyCreated) {
641 - $comment->reactions_count = $comment->reactions_count + 1;
642 - $comment->save();
643 677 do_action('fluent_community/comment/react_added', $reaction, $comment, $feed);
644 678 }
645 679 } else {
646 680 // remove the reaction