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