PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.4.01
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.4.01
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
fluent-community / app / Http / Controllers / CommentsController.php

CommentsController.php in FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses 2.4.01, at app/Http/Controllers/CommentsController.php

709 lines 25.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCommunity\App\Http\Controllers;
4
5 use FluentCommunity\App\Models\Media;
6 use FluentCommunity\App\Models\User;
7 use FluentCommunity\App\Services\CustomSanitizer;
8 use FluentCommunity\App\Services\FeedsHelper;
9 use FluentCommunity\App\Services\Helper;
10 use FluentCommunity\App\Services\ProfileHelper;
11 use FluentCommunity\Framework\Http\Request\Request;
12 use FluentCommunity\App\Models\Comment;
13 use FluentCommunity\App\Models\Feed;
14 use FluentCommunity\App\Models\Reaction;
15 use FluentCommunity\Framework\Support\Arr;
16
17 class CommentsController extends Controller
18 {
19 public function getComments(Request $request, $feed_id)
20 {
21 $feed = Feed::withoutGlobalScopes()->findOrFail($feed_id);
22 $canViewComments = apply_filters('fluent_community/can_view_comments_' . $feed->type, true, $feed);
23
24 if (!$canViewComments) {
25 return [
26 'comments' => []
27 ];
28 }
29
30 $comments = Comment::where('post_id', $feed->id)
31 ->byContentModerationAccessStatus($this->getUser())
32 ->orderBy('created_at', 'asc')
33 ->with([
34 'xprofile' => function ($q) {
35 $q->select(ProfileHelper::getXProfilePublicFields());
36 }
37 ])
38 ->whereHas('xprofile', function ($q) {
39 $q->where('status', 'active');
40 })
41 ->get();
42
43 $comments = apply_filters('fluent_community/comments_query_response', $comments, $request->all());
44
45 $userId = $this->getUserId();
46
47 if ($userId) {
48 $likedIds = FeedsHelper::getLikedIdsByUserFeedId($feed->id, get_current_user_id());
49 if ($likedIds) {
50 $comments->each(function ($comment) use ($likedIds) {
51 if (in_array($comment->id, $likedIds)) {
52 $comment->liked = 1;
53 }
54 });
55 }
56 }
57
58 $data = [
59 'comments' => $comments
60 ];
61
62 return apply_filters('fluent_community/comments_api_response', $data, $request->all());
63 }
64
65 public function store(Request $request, $feedId)
66 {
67 $user = $this->getUser(true);
68 do_action('fluent_community/check_rate_limit/create_comment', $user);
69
70 $text = $this->validateCommentText($request->all());
71 $feed = Feed::withoutGlobalScopes()->findOrFail($feedId);
72
73 if ($feed->status != 'published') {
74 return $this->sendError([
75 'message' => __('This post is not published yet', 'fluent-community')
76 ]);
77 }
78
79 $this->verifyCreateCommentPermission($feed);
80
81 $requestData = $request->all();
82
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 [$markdown, $inlineMedias] = FeedsHelper::replaceImageUrlsWithRealMediaArchive($text);
101 $mentions = FeedsHelper::getMentions($markdown, $feed->space_id, true);
102 $commentHtml = $this->generateCommentHtml($markdown, $mentions);
103
104 $commentData = $this->prepareCommentData($feed->id, $text, $commentHtml);
105
106 if (!empty($requestData['parent_id'])) {
107 $parentId = (int)$requestData['parent_id'];
108 $parentComment = Comment::where('id', $parentId)
109 ->where('post_id', $feed->id)
110 ->first();
111
112 if (!$parentComment) {
113 return $this->sendError([
114 'message' => __('Invalid parent comment', 'fluent-community')
115 ]);
116 }
117
118 $commentData['parent_id'] = $parentId;
119 }
120
121 [$commentData, $mediaItems] = $this->prepareCommentMedia($commentData, $requestData);
122
123 $commentData['is_admin'] = $user->hasSpacePermission('community_moderator', $feed->space);
124
125 if ($mentionUserIds = Arr::get($mentions, 'user_ids', [])) {
126 $commentData['meta']['mentioned_user_ids'] = $mentionUserIds;
127 }
128
129 do_action('fluent_community/before_comment_create', $commentData, $feed);
130
131 $commentData = apply_filters('fluent_community/comment/comment_data', $commentData, $feed);
132
133 $comment = Comment::create($commentData);
134
135 $feed->comments_count = $feed->comments_count + 1;
136 $feed->save();
137
138
139 // Merge and save all media in one loop
140 $mediaItems = $mediaItems ? (is_array($mediaItems) ? $mediaItems : [$mediaItems]) : [];
141
142 if ($inlineMedias) {
143 $mediaItems = array_merge($mediaItems, $inlineMedias);
144 }
145
146 if ($mediaItems) {
147 foreach ($mediaItems as $media) {
148 $media->fill([
149 'is_active' => 1,
150 'feed_id' => $feed->id,
151 'object_source' => 'comment',
152 'sub_object_id' => $comment->id
153 ]);
154 $media->save();
155 }
156 }
157
158 $this->loadCommentRelations($comment);
159
160 if ($comment->status != 'published') {
161 do_action('fluent_community/comment/new_comment_' . $comment->status, $comment, $feed);
162 /* translators: %$s is replaced by the status of the comment */
163 $message = sprintf(__('Your comment has been marked as %s', 'fluent-community'), $comment->status);
164 return [
165 'comment' => $comment,
166 'message' => $message
167 ];
168 }
169
170 do_action('fluent_community/comment_added_' . $feed->type, $comment, $feed);
171 do_action('fluent_community/comment_added', $comment, $feed, Arr::get($mentions, 'users', []));
172
173 return [
174 'comment' => $comment,
175 'message' => __('Comment has been added', 'fluent-community'),
176 ];
177 }
178
179 public function update(Request $request, $feedId, $commentId)
180 {
181 $text = $this->validateCommentText($request->all());
182
183 $feed = Feed::withoutGlobalScopes()->findOrFail($feedId);
184 $this->verifySpacePermission($feed);
185
186 $requestData = $request->all();
187 $comment = Comment::findOrFail($commentId);
188 $user = $this->getUser(true);
189
190 $requestData['is_admin'] = $user->hasPermissionOrInCurrentSpace('community_moderator', $feed->space);
191
192 if ($comment->user_id != get_current_user_id() && !$user->can('edit_any_comment', $feed->space)) {
193 return $this->sendError([
194 'message' => __('You are not allowed to edit this comment', 'fluent-community')
195 ]);
196 }
197
198 [$markdown, $inlineMedias] = FeedsHelper::replaceImageUrlsWithRealMediaArchive($text, $feed);
199
200 $mentions = FeedsHelper::getMentions($markdown, $feed->space_id);
201
202 $commentHtml = $this->generateCommentHtml($markdown, $mentions);
203
204 $commentData = $this->prepareCommentData($feed->id, $text, $commentHtml);
205
206 [$commentData, $mediaItems] = $this->prepareCommentMedia($commentData, $requestData, $comment);
207
208 $commentData = apply_filters('fluent_community/comment/update_comment_data', $commentData, $feed, $requestData, $comment);
209
210 $comment->fill($commentData);
211
212 $dirty = $comment->getDirty();
213
214 if ($dirty) {
215 $comment->save();
216 }
217
218 // Merge and save all media in one loop
219 $mediaItems = $mediaItems ? (is_array($mediaItems) ? $mediaItems : [$mediaItems]) : [];
220
221 if ($inlineMedias) {
222 $mediaItems = array_merge($mediaItems, $inlineMedias);
223 }
224
225 $allMediaIds = [];
226
227 if ($mediaItems) {
228 foreach ($mediaItems as $media) {
229 $media->fill([
230 'is_active' => 1,
231 'feed_id' => $feed->id,
232 'object_source' => 'comment',
233 'sub_object_id' => $comment->id
234 ]);
235 $media->save();
236 $allMediaIds[] = $media->id;
237 }
238 }
239
240 // Remove old media not in current list
241 $otherMedias = Media::where('object_source', 'comment')
242 ->when($allMediaIds, function ($q) use ($allMediaIds) {
243 $q->whereNotIn('id', $allMediaIds);
244 })
245 ->where('sub_object_id', $comment->id)
246 ->get();
247
248 if (!$otherMedias->isEmpty()) {
249 do_action('fluent_community/comment/media_deleted', $otherMedias);
250 }
251
252 $this->loadCommentRelations($comment);
253
254 if ($dirty) {
255 do_action('fluent_community/comment_updated', $comment, $feed);
256 do_action('fluent_community/comment_updated_' . $feed->type, $comment, $feed);
257 }
258
259 return [
260 'comment' => $comment,
261 'message' => __('Comment has been updated', 'fluent-community'),
262 ];
263 }
264
265 public function patchComment(Request $request, $feedId, $commentId)
266 {
267 $feed = Feed::withoutGlobalScopes()->findOrFail($feedId);
268
269 $comment = Comment::findOrFail($commentId);
270
271 $user = $this->getUser(true);
272
273 $isMod = $user && $user->hasPermissionOrInCurrentSpace('community_moderator', $feed->space);
274 $isAdmin = $user && $user->hasPermissionOrInCurrentSpace('community_admin', $feed->space);
275
276 if (!$isMod && !$isAdmin) {
277 return $this->sendError([
278 'message' => __('You do not have permission to perform this action', 'fluent-community')
279 ]);
280 }
281
282 $allData = $request->all();
283 $validKeys = ['is_sticky'];
284
285 $data = Arr::only($allData, $validKeys);
286
287 $data = array_map('intval', $data);
288
289 if (isset($data['is_sticky'])) {
290 if ($comment->parent_id) {
291 return $this->sendError([
292 'message' => __('You cannot pin a reply comment', 'fluent-community')
293 ]);
294 }
295
296 $data['is_sticky'] = $data['is_sticky'] ? 1 : 0;
297 if ($data['is_sticky']) {
298 Comment::where('post_id', $feed->id)->update(['is_sticky' => 0]);
299 }
300 }
301
302 if ($data) {
303 $comment->fill($data);
304 $dirty = $comment->getDirty();
305 if ($dirty) {
306 $comment->save();
307 do_action('fluent_community/comment/updated', $comment, $dirty);
308 }
309 }
310
311 return apply_filters('fluent_community/comment/patch_comment_response', [
312 'comment' => $comment,
313 'message' => __('Comment updated', 'fluent-community')
314 ], $comment, $feed, $request->all());
315 }
316
317 private function prepareCommentMedia($commentData, $requestData, $exisitngComment = null)
318 {
319 $mediaImages = Arr::get($requestData, 'media_images', []);
320
321 if ($mediaImages) {
322 if ($exisitngComment) {
323 $mediaItems = [];
324 $mediaData = [];
325 foreach ($mediaImages as $mediaImage) {
326 $id = Arr::get($mediaImage, 'media_id');
327 if ($id) {
328 $media = Media::where('sub_object_id', $exisitngComment->id)
329 ->where('object_source', 'comment')
330 ->find($id);
331 } else {
332 $media = Helper::getMediaFromUrl($mediaImage);
333 }
334
335 if ($media) {
336 $mediaItems[] = $media;
337 $mediaData[] = [
338 'media_id' => $media->id,
339 'url' => $media->public_url,
340 'type' => 'image',
341 'width' => Arr::get($media->settings, 'width'),
342 'height' => Arr::get($media->settings, 'height'),
343 'provider' => Arr::get($media->settings, 'provider', 'uploader')
344 ];
345 }
346 }
347 $commentData['meta']['media_items'] = $mediaData;
348 return [$commentData, $mediaItems];
349 }
350
351 $uploadedImages = Helper::getMediaByProvider($mediaImages);
352 if ($uploadedImages) {
353 $mediaItems = Helper::getMediaItemsFromUrl($uploadedImages);
354 if ($mediaItems) {
355 $mediaPreviews = [];
356 foreach ($mediaItems as $mediaItem) {
357 $mediaData = [
358 'media_id' => $mediaItem->id,
359 'url' => $mediaItem->public_url,
360 'type' => 'image',
361 'width' => Arr::get($mediaItem->settings, 'width'),
362 'height' => Arr::get($mediaItem->settings, 'height'),
363 'provider' => Arr::get($mediaItem->settings, 'provider', 'uploader')
364 ];
365
366 $mediaPreviews[] = array_filter($mediaData);
367 }
368 $commentData['meta']['media_items'] = $mediaPreviews;
369 return [$commentData, $mediaItems];
370 }
371 }
372 }
373
374 if (empty($requestData['meta']['media_preview']['image'])) {
375 return [$commentData, []];
376 }
377
378 if ($exisitngComment) {
379 $image = sanitize_url(Arr::get($requestData, 'meta.media_preview.image', ''));
380 $existingMedia = Media::where('media_url', $image)
381 ->where('object_source', 'comment')
382 ->where('sub_object_id', $exisitngComment->id)
383 ->first();
384
385 if ($existingMedia) {
386 $commentData['meta'] = $exisitngComment->meta;
387 return [$commentData, [$existingMedia]];
388 }
389 }
390
391 $commentData['meta']['media_preview'] = array_filter([
392 '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),
397 ]);
398
399 return [$commentData, []];
400 }
401
402 private function validateCommentText($data)
403 {
404 $text = trim(Arr::get($data, 'comment'));
405 $text = CustomSanitizer::unslashMarkdown($text);
406
407 // Decode HTML entities (e.g., &#x20; for space) and strip all whitespace for validation
408 $textForValidation = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
409 $textForValidation = preg_replace('/\s+/u', '', $textForValidation);
410
411 $hasMedia = Arr::get($data, 'media_images', []) || Arr::get($data, 'meta.media_preview.image', false);
412
413 $isReply = !empty($data['parent_id']);
414 if (!$textForValidation && !$hasMedia) {
415 if ($isReply) {
416 throw new \Exception(esc_html__('Reply cannot be empty.', 'fluent-community'), 422);
417 } else {
418 throw new \Exception(esc_html__('Comment cannot be empty.', 'fluent-community'), 422);
419 }
420 }
421
422 $maxCommentLength = apply_filters('fluent_community/max_comment_char_length', 10000);
423 if ($text && strlen($text) > $maxCommentLength) {
424 throw new \Exception(esc_html__('Comment text is too long', 'fluent-community'), 422);
425 }
426
427 return $text;
428 }
429
430 private function verifyCreateCommentPermission($feed)
431 {
432 if (Arr::get($feed->meta, 'comments_disabled') === 'yes') {
433 throw new \Exception(esc_html__('Comments are disabled for this post', 'fluent-community'));
434 }
435
436 $this->verifySpacePermission($feed);
437 }
438
439 private function verifySpacePermission($feed)
440 {
441 if ($feed->space_id && $feed->space) {
442 $user = $this->getUser(true);
443 $user->verifySpacePermission('can_comment', $feed->space);
444
445 if ($feed->space->type == 'course' && Arr::get($feed->space->settings, 'disable_comments') === 'yes') {
446 throw new \Exception(esc_html__('Comments are disabled for this course', 'fluent-community'));
447 }
448 }
449 }
450
451 private function generateCommentHtml($text, $mentions)
452 {
453 $htmlText = $mentions ? $mentions['text'] : $text;
454 return wp_kses_post(FeedsHelper::mdToHtml($htmlText));
455 }
456
457 private function prepareCommentData($feedId, $text, $commentHtml)
458 {
459 return [
460 'post_id' => $feedId,
461 'message' => $text,
462 'message_rendered' => $commentHtml,
463 'type' => 'comment',
464 'meta' => [],
465 ];
466 }
467
468 private function loadCommentRelations($comment)
469 {
470 $comment->load('media');
471 $comment->load([
472 'xprofile' => function ($q) {
473 $q->select(ProfileHelper::getXProfilePublicFields());
474 }
475 ]);
476 }
477
478 public function addOrRemovePostReact(Request $request, $feed_id)
479 {
480 $userId = get_current_user_id();
481 $feed = Feed::withoutGlobalScopes()->byUserAccess($userId)->findOrFail($feed_id);
482 $type = $request->get('react_type', 'like');
483 $willRemove = $request->get('remove');
484
485 if ($feed->status != 'published') {
486 return $this->sendError([
487 'message' => __('This post is not published yet', 'fluent-community')
488 ]);
489 }
490
491 if ($userId === $feed->user_id && apply_filters('fluent_community/disable_self_post_react', false, $feed)) {
492 return $this->sendError([
493 'message' => __('You cannot react to your own post', 'fluent-community')
494 ]);
495 }
496
497 $react = Reaction::where('user_id', $userId)
498 ->where('object_id', $feed->id)
499 ->where('type', $type)
500 ->objectType('feed')
501 ->first();
502
503 if ($willRemove) {
504 if ($react) {
505 $react->delete();
506 if ($type == 'like') {
507 $feed->reactions_count = $feed->reactions_count - 1;
508 $feed->timestamps = false; // Don't update the updated_at timestamp
509 $feed->save();
510 do_action('fluent_community/feed/react_removed', $feed);
511 }
512 }
513
514 return [
515 'message' => __('Reaction has been removed', 'fluent-community'),
516 'new_count' => $feed->reactions_count
517 ];
518 }
519
520 if ($react) {
521 return [
522 'message' => __('You have already reacted to this post', 'fluent-community'),
523 'new_count' => $feed->reactions_count
524 ];
525 }
526
527 $react = Reaction::create([
528 'user_id' => get_current_user_id(),
529 'object_id' => $feed->id,
530 'type' => $type,
531 'object_type' => 'feed'
532 ]);
533
534 if ($type == 'like') {
535 $feed->reactions_count = $feed->reactions_count + 1;
536 $feed->timestamps = false; // Don't update the updated_at timestamp
537 $feed->save();
538
539 $react->load('xprofile');
540 do_action('fluent_community/feed/react_added', $react, $feed);
541 }
542
543 return [
544 'message' => __('Reaction has been added', 'fluent-community'),
545 'new_count' => $feed->reactions_count
546 ];
547 }
548
549 public function deleteComment(Request $request, $feedId, $commentId)
550 {
551 $feed = Feed::withoutGlobalScopes()->findOrFail($feedId);
552 $comment = Comment::findOrFail($commentId);
553
554 if ($comment->post_id != $feed->id) {
555 return $this->sendError([
556 'message' => __('Invalid comment', 'fluent-community')
557 ]);
558 }
559
560 $user = User::find(get_current_user_id());
561 if ($comment->user_id != get_current_user_id() && !$user->can('delete_any_comment', $feed->space)) {
562 return $this->sendError([
563 'message' => __('You are not allowed to delete this comment', 'fluent-community')
564 ]);
565 }
566
567 do_action('fluent_community/before_comment_delete', $comment);
568
569 if ($comment->media) {
570 do_action('fluent_community/comment/media_deleted', $comment->media);
571 }
572
573 $comment->delete();
574
575 $feed->comments_count = Comment::where('post_id', $feed->id)->count();
576 $feed->timestamps = false; // Don't update the updated_at timestamp
577 $feed->save();
578
579 do_action('fluent_community/comment_deleted_' . $feed->type, $commentId, $feed);
580 do_action('fluent_community/comment_deleted', $commentId, $feed);
581
582 return [
583 'message' => __('Selected comment has been deleted', 'fluent-community')
584 ];
585 }
586
587 public function toggleReaction(Request $request, $feedId, $commentId)
588 {
589 $feed = Feed::withoutGlobalScopes()->findOrFail($feedId);
590 $comment = Comment::findOrFail($commentId);
591
592 if ($comment->post_id != $feed->id) {
593 return $this->sendError([
594 'message' => __('Invalid comment', 'fluent-community')
595 ]);
596 }
597
598 $user = User::findOrFail(get_current_user_id());
599
600 if ($feed->space_id) {
601 $user->verifySpacePermission('registered', $feed->space);
602 }
603
604 $userId = get_current_user_id();
605 if ($userId === $comment->user_id && apply_filters('fluent_community/disable_self_comment_react', false, $feed)) {
606 return $this->sendError([
607 'message' => __('You cannot react to your own comment', 'fluent-community')
608 ]);
609 }
610
611 $reactionState = !!$request->get('state', false);
612
613 if ($reactionState) {
614 // add or update the reaction
615 $reaction = Reaction::firstOrCreate([
616 'user_id' => get_current_user_id(),
617 'object_id' => $comment->id,
618 'object_type' => 'comment',
619 'parent_id' => $feed->id
620 ]);
621
622 if ($reaction->wasRecentlyCreated) {
623 $comment->reactions_count = $comment->reactions_count + 1;
624 $comment->save();
625 do_action('fluent_community/comment/react_added', $reaction, $comment, $feed);
626 }
627 } else {
628 // remove the reaction
629 $deleted = Reaction::where('user_id', get_current_user_id())
630 ->where('object_id', $comment->id)
631 ->where('object_type', 'comment')
632 ->delete();
633
634 if ($deleted) {
635 $comment->reactions_count = $comment->reactions_count - 1;
636 $comment->save();
637 do_action('fluent_community/comment/react_removed', $comment, $feed);
638 }
639 }
640
641 return [
642 'message' => __('Reaction has been toggled', 'fluent-community'),
643 'reactions_count' => $comment->reactions_count,
644 'liked' => $reactionState
645 ];
646 }
647
648 public function show(Request $request, $id)
649 {
650
651 $testComment = Comment::query()->findOrFail($id);
652
653 $comment = Comment::byContentModerationAccessStatus($this->getUser(), $testComment->space)
654 ->with([
655 'xprofile' => function ($q) {
656 return $q->select(ProfileHelper::getXProfilePublicFields());
657 }
658 ])->findOrFail($id);
659
660 // Just to verify the permission
661 Feed::withoutGlobalScopes()
662 ->byUserAccess($this->getUserId())
663 ->findOrFail($comment->post_id);
664
665 if ($request->get('context') == 'edit') {
666 $meta = $comment->meta;
667 unset($comment->meta);
668 $images = Arr::get($meta, 'media_items', []);
669 if ($images) {
670 $comment->media_images = $images;
671 } else {
672 $preview = Arr::get($meta, 'media_preview', []);
673 if ($preview) {
674 $previewUrl = Arr::get($preview, 'image');
675 $provider = Arr::get($preview, 'provider');
676 if ($previewUrl && $provider == 'uploader') {
677 $media = Media::where('media_url', $previewUrl)
678 ->where('object_source', 'comment')
679 ->where('sub_object_id', $comment->id)
680 ->first();
681 if ($media) {
682 $comment->media_images = [
683 [
684 'media_id' => $media->id,
685 'url' => $media->public_url,
686 'type' => $media->media_type,
687 'width' => Arr::get($media->settings, 'width'),
688 'height' => Arr::get($media->settings, 'height'),
689 'provider' => Arr::get($media->settings, 'provider', 'uploader')
690 ]
691 ];
692 }
693 } else {
694 $comment->meta = [
695 'media_preview' => $preview
696 ];
697 }
698 }
699 }
700 }
701
702 $data = [
703 'comment' => $comment
704 ];
705
706 return apply_filters('fluent_community/comment_api_response', $data, $request->all());
707 }
708 }
709