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 +400 -115 1.0.982.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,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 ];
@@ -27,8 +46,9 @@
27 46 ];
28 47 }
29 48
30 49 $comments = Comment::where('post_id', $feed->id)
50 + ->byContentModerationAccessStatus($this->getUser())
31 51 ->orderBy('created_at', 'asc')
32 52 ->with([
33 53 'xprofile' => function ($q) {
34 54 $q->select(ProfileHelper::getXProfilePublicFields());
@@ -33,15 +53,19 @@
33 53 'xprofile' => function ($q) {
34 54 $q->select(ProfileHelper::getXProfilePublicFields());
35 55 }
36 56 ])
57 + ->whereHas('xprofile', function ($q) {
58 + $q->where('status', 'active');
59 + })
37 60 ->get();
38 61
62 + $comments = apply_filters('fluent_community/comments_query_response', $comments, $request->all());
63 +
39 64 $userId = $this->getUserId();
40 65
41 66 if ($userId) {
42 67 $likedIds = FeedsHelper::getLikedIdsByUserFeedId($feed->id, get_current_user_id());
43 -
44 68 if ($likedIds) {
45 69 $comments->each(function ($comment) use ($likedIds) {
46 70 if (in_array($comment->id, $likedIds)) {
47 71 $comment->liked = 1;
@@ -49,11 +73,13 @@
49 73 });
50 74 }
51 75 }
52 76
53 - return [
54 - 'comments' => $comments
77 + $data = [
78 + 'comments' => $comments
55 79 ];
80 +
81 + return apply_filters('fluent_community/comments_api_response', $data, $request->all());
56 82 }
57 83
58 84 public function store(Request $request, $feedId)
59 85 {
@@ -62,30 +88,26 @@
62 88
63 89 $text = $this->validateCommentText($request->all());
64 90 $feed = Feed::withoutGlobalScopes()->findOrFail($feedId);
65 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 +
66 98 $this->verifyCreateCommentPermission($feed);
67 99
68 100 $requestData = $request->all();
69 101
70 - // Check for duplicate
71 - $exist = Comment::where('user_id', get_current_user_id())
72 - ->where('message', $text)
73 - ->where('post_id', $feed->id)
74 - ->first();
102 + [$markdown, $inlineMedias] = FeedsHelper::replaceImageUrlsWithRealMediaArchive($text);
103 + $mentions = FeedsHelper::getMentions($markdown, $feed->space_id, true);
104 + $commentHtml = $this->generateCommentHtml($markdown, $mentions);
75 105
76 - if ($exist) {
77 - return $this->sendError([
78 - 'message' => __('No duplicate comment please!', 'fluent-community')
79 - ]);
80 - }
81 -
82 - $mentions = FeedsHelper::getMentions($text, $feed->space_id);
83 - $commentHtml = $this->generateCommentHtml($text, $mentions);
84 106 $commentData = $this->prepareCommentData($feed->id, $text, $commentHtml);
85 107
86 - if ($parentId = $request->get('parent_id')) {
87 - $parentId = (int) $parentId;
108 + if (!empty($requestData['parent_id'])) {
109 + $parentId = (int)$requestData['parent_id'];
88 110 $parentComment = Comment::where('id', $parentId)
89 111 ->where('post_id', $feed->id)
90 112 ->first();
91 113
@@ -97,35 +119,88 @@
97 119
98 120 $commentData['parent_id'] = $parentId;
99 121 }
100 122
101 - [$commentData, $media] = $this->prepareCommentMedia($commentData, $requestData);
123 + [$commentData, $mediaItems] = $this->prepareCommentMedia($commentData, $requestData);
102 124
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 +
103 131 do_action('fluent_community/before_comment_create', $commentData, $feed);
104 132
105 - $commentData = apply_filters('fluent_community/comment/comment_data', $commentData, $feed, $requestData);
133 + $commentData = apply_filters('fluent_community/comment/comment_data', $commentData, $feed);
106 134
107 - $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);
108 137
109 - $feed->comments_count = $feed->comments_count + 1;
110 - $feed->save();
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();
111 142
112 - if ($media) {
113 - $media->fill([
114 - 'is_active' => 1,
115 - 'feed_id' => $feed->id,
116 - 'object_source' => 'comment',
117 - 'sub_object_id' => $comment->id
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')
118 160 ]);
119 - $media->save();
120 161 }
121 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 +
122 187 $this->loadCommentRelations($comment);
123 188
124 - $mentionedUsers = $mentions ? $mentions['users'] : null;
125 - do_action('fluent_community/comment_added_' . $feed->type, $comment, $feed, $mentionedUsers);
126 - 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 + }
127 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 +
128 203 return [
129 204 'comment' => $comment,
130 205 'message' => __('Comment has been added', 'fluent-community'),
131 206 ];
@@ -133,29 +208,42 @@
133 208
134 209 public function update(Request $request, $feedId, $commentId)
135 210 {
136 211 $text = $this->validateCommentText($request->all());
212 +
137 213 $feed = Feed::withoutGlobalScopes()->findOrFail($feedId);
138 214 $this->verifySpacePermission($feed);
139 215
140 216 $requestData = $request->all();
141 217 $comment = Comment::findOrFail($commentId);
142 - $user = User::find(get_current_user_id());
143 218
144 - if (!$user->can('edit_any_comment') && $comment->user_id != get_current_user_id()) {
219 + if ($comment->post_id != $feed->id) {
145 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([
146 231 'message' => __('You are not allowed to edit this comment', 'fluent-community')
147 232 ]);
148 233 }
149 234
150 - $mentions = FeedsHelper::getMentions($text, $feed->space_id);
151 - $commentHtml = $this->generateCommentHtml($text, $mentions);
235 + [$markdown, $inlineMedias] = FeedsHelper::replaceImageUrlsWithRealMediaArchive($text, $feed);
152 236
237 + $mentions = FeedsHelper::getMentions($markdown, $feed->space_id);
238 +
239 + $commentHtml = $this->generateCommentHtml($markdown, $mentions);
240 +
153 241 $commentData = $this->prepareCommentData($feed->id, $text, $commentHtml);
154 242
155 - [$commentData, $media] = $this->prepareCommentMedia($commentData, $requestData, $comment);
243 + [$commentData, $mediaItems] = $this->prepareCommentMedia($commentData, $requestData, $comment);
156 244
157 - $commentData = apply_filters('fluent_community/comment/update_comment_data', $commentData, $feed, $requestData);
245 + $commentData = apply_filters('fluent_community/comment/update_comment_data', $commentData, $feed, $requestData, $comment);
158 246
159 247 $comment->fill($commentData);
160 248
161 249 $dirty = $comment->getDirty();
@@ -163,35 +251,40 @@
163 251 if ($dirty) {
164 252 $comment->save();
165 253 }
166 254
167 - if ($media) {
168 - $media->fill([
169 - 'is_active' => 1,
170 - 'feed_id' => $feed->id,
171 - 'object_source' => 'comment',
172 - 'sub_object_id' => $comment->id
173 - ]);
174 - $media->save();
255 + // Merge and save all media in one loop
256 + $mediaItems = $mediaItems ? (is_array($mediaItems) ? $mediaItems : [$mediaItems]) : [];
175 257
176 - // remove other media
177 - $otherMedias = Media::where('object_source', 'comment')
178 - ->where('sub_object_id', $comment->id)
179 - ->where('id', '!=', $media->id)
180 - ->get();
258 + if ($inlineMedias) {
259 + $mediaItems = array_merge($mediaItems, $inlineMedias);
260 + }
181 261
182 - if (!$otherMedias->isEmpty()) {
183 - // do_action('fluent_community/comment/media_deleted', $otherMedias);
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;
184 274 }
185 - } else {
186 - // remove other media
187 - $otherMedias = Media::where('object_source', 'comment')
188 - ->where('sub_object_id', $comment->id)
189 - ->get();
275 + }
190 276
191 - if (!$otherMedias->isEmpty()) {
192 - // do_action('fluent_community/comment/media_deleted', $otherMedias);
193 - }
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);
194 287 }
195 288
196 289 $this->loadCommentRelations($comment);
197 290
@@ -205,32 +298,125 @@
205 298 'message' => __('Comment has been updated', 'fluent-community'),
206 299 ];
207 300 }
208 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 +
209 360 private function prepareCommentMedia($commentData, $requestData, $exisitngComment = null)
210 361 {
211 362 $mediaImages = Arr::get($requestData, 'media_images', []);
212 363
213 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 +
214 394 $uploadedImages = Helper::getMediaByProvider($mediaImages);
215 395 if ($uploadedImages) {
216 396 $mediaItems = Helper::getMediaItemsFromUrl($uploadedImages);
217 397 if ($mediaItems) {
218 - $firstMedia = $mediaItems[0];
219 - $commentData['meta']['media_preview'] = [
220 - 'image' => $firstMedia->public_url,
221 - 'type' => 'image',
222 - 'provider' => 'upload',
223 - 'height' => $firstMedia->settings ? Arr::get($firstMedia->settings, 'height', 0) : 0,
224 - 'width' => $firstMedia->settings ? Arr::get($firstMedia->settings, 'width', 0) : 0,
225 - ];
226 - 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];
227 413 }
228 414 }
229 415 }
230 416
231 417 if (empty($requestData['meta']['media_preview']['image'])) {
232 - return [$commentData, null];
418 + return [$commentData, []];
233 419 }
234 420
235 421 if ($exisitngComment) {
236 422 $image = sanitize_url(Arr::get($requestData, 'meta.media_preview.image', ''));
@@ -240,37 +426,50 @@
240 426 ->first();
241 427
242 428 if ($existingMedia) {
243 429 $commentData['meta'] = $exisitngComment->meta;
244 - return [$commentData, $existingMedia];
430 + return [$commentData, [$existingMedia]];
245 431 }
246 432 }
247 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.
248 437 $commentData['meta']['media_preview'] = array_filter([
249 438 'image' => sanitize_url(Arr::get($requestData, 'meta.media_preview.image', '')),
250 - 'type' => Arr::get($requestData, 'meta.media_preview.type', 'image'),
251 - 'provider' => Arr::get($requestData, 'meta.media_preview.provider', ''),
252 - 'height' => Arr::get($requestData, 'meta.media_preview.height', 0),
253 - '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),
254 443 ]);
255 444
256 - return [$commentData, null];
445 + return [$commentData, []];
257 446 }
258 447
259 448 private function validateCommentText($data)
260 449 {
261 - $text = trim(Arr::get($data, 'comment'));
450 + $text = trim((string) Arr::get($data, 'comment', ''));
262 451 $text = CustomSanitizer::unslashMarkdown($text);
263 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 +
264 457 $hasMedia = Arr::get($data, 'media_images', []) || Arr::get($data, 'meta.media_preview.image', false);
265 458
266 - if (!$text && !$hasMedia) {
267 - 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 + }
268 466 }
269 467
270 468 $maxCommentLength = apply_filters('fluent_community/max_comment_char_length', 10000);
271 469 if ($text && strlen($text) > $maxCommentLength) {
272 - 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);
273 472 }
274 473
275 474 return $text;
276 475 }
@@ -276,9 +475,9 @@
276 475 }
277 476
278 477 private function verifyCreateCommentPermission($feed)
279 478 {
280 - if (Arr::get($feed->meta, 'comments_disabled') === 'yes') {
479 + if (!FeedsHelper::commentsEnabled($feed)) {
281 480 throw new \Exception(esc_html__('Comments are disabled for this post', 'fluent-community'));
282 481 }
283 482
284 483 $this->verifySpacePermission($feed);
@@ -287,9 +486,13 @@
287 486 private function verifySpacePermission($feed)
288 487 {
289 488 if ($feed->space_id && $feed->space) {
290 489 $user = $this->getUser(true);
291 - $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 + }
292 495 }
293 496 }
294 497
295 498 private function generateCommentHtml($text, $mentions)
@@ -320,13 +523,27 @@
320 523 }
321 524
322 525 public function addOrRemovePostReact(Request $request, $feed_id)
323 526 {
324 - $feed = Feed::withoutGlobalScopes()->findOrFail($feed_id);
527 + $userId = get_current_user_id();
528 + $feed = Feed::withoutGlobalScopes()->byUserAccess($userId)->findOrFail($feed_id);
325 529 $type = $request->get('react_type', 'like');
530 + $type = in_array($type, ['like', 'bookmark'], true) ? $type : 'like';
326 531 $willRemove = $request->get('remove');
327 532
328 - $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)
329 546 ->where('object_id', $feed->id)
330 547 ->where('type', $type)
331 548 ->objectType('feed')
332 549 ->first();
@@ -335,14 +552,16 @@
335 552 if ($react) {
336 553 $react->delete();
337 554 if ($type == 'like') {
338 555 $feed->reactions_count = $feed->reactions_count - 1;
556 + $feed->timestamps = false; // Don't update the updated_at timestamp
339 557 $feed->save();
558 + do_action('fluent_community/feed/react_removed', $feed);
340 559 }
341 560 }
342 561
343 562 return [
344 - 'message' => 'Reaction has been removed',
563 + 'message' => __('Reaction has been removed', 'fluent-community'),
345 564 'new_count' => $feed->reactions_count
346 565 ];
347 566 }
348 567
@@ -347,9 +566,9 @@
347 566 }
348 567
349 568 if ($react) {
350 569 return [
351 - 'message' => 'You have already reacted to this post',
570 + 'message' => __('You have already reacted to this post', 'fluent-community'),
352 571 'new_count' => $feed->reactions_count
353 572 ];
354 573 }
355 574
@@ -361,8 +580,9 @@
361 580 ]);
362 581
363 582 if ($type == 'like') {
364 583 $feed->reactions_count = $feed->reactions_count + 1;
584 + $feed->timestamps = false; // Don't update the updated_at timestamp
365 585 $feed->save();
366 586
367 587 $react->load('xprofile');
368 588 do_action('fluent_community/feed/react_added', $react, $feed);
@@ -368,9 +588,9 @@
368 588 do_action('fluent_community/feed/react_added', $react, $feed);
369 589 }
370 590
371 591 return [
372 - 'message' => 'Reaction has been added',
592 + 'message' => __('Reaction has been added', 'fluent-community'),
373 593 'new_count' => $feed->reactions_count
374 594 ];
375 595 }
376 596
@@ -380,16 +600,16 @@
380 600 $comment = Comment::findOrFail($commentId);
381 601
382 602 if ($comment->post_id != $feed->id) {
383 603 return $this->sendError([
384 - 'message' => 'Invalid comment'
604 + 'message' => __('Invalid comment', 'fluent-community')
385 605 ]);
386 606 }
387 607
388 608 $user = User::find(get_current_user_id());
389 - 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)) {
390 610 return $this->sendError([
391 - 'message' => 'You are not allowed to delete this comment'
611 + 'message' => __('You are not allowed to delete this comment', 'fluent-community')
392 612 ]);
393 613 }
394 614
395 615 do_action('fluent_community/before_comment_delete', $comment);
@@ -399,9 +619,12 @@
399 619 }
400 620
401 621 $comment->delete();
402 622
403 - $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
404 627 $feed->save();
405 628
406 629 do_action('fluent_community/comment_deleted_' . $feed->type, $commentId, $feed);
407 630 do_action('fluent_community/comment_deleted', $commentId, $feed);
@@ -410,16 +633,16 @@
410 633 'message' => __('Selected comment has been deleted', 'fluent-community')
411 634 ];
412 635 }
413 636
414 - public function toggoleReaction(Request $request, $feedId, $commentId)
637 + public function toggleReaction(Request $request, $feedId, $commentId)
415 638 {
416 - $feed = Feed::withoutGlobalScopes()->findOrFail($feedId);
639 + $feed = Feed::withoutGlobalScopes()->byUserAccess(get_current_user_id())->findOrFail($feedId);
417 640 $comment = Comment::findOrFail($commentId);
418 641
419 642 if ($comment->post_id != $feed->id) {
420 643 return $this->sendError([
421 - 'message' => 'Invalid comment'
644 + 'message' => __('Invalid comment', 'fluent-community')
422 645 ]);
423 646 }
424 647
425 648 $user = User::findOrFail(get_current_user_id());
@@ -427,22 +650,40 @@
427 650 if ($feed->space_id) {
428 651 $user->verifySpacePermission('registered', $feed->space);
429 652 }
430 653
654 + $userId = get_current_user_id();
431 655 $reactionState = !!$request->get('state', false);
432 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 +
433 663 if ($reactionState) {
434 - // add or update the reaction
435 - $reaction = Reaction::firstOrCreate([
436 - 'user_id' => get_current_user_id(),
437 - 'object_id' => $comment->id,
438 - 'object_type' => 'comment',
439 - 'parent_id' => $feed->id
440 - ]);
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();
441 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 +
442 684 if ($reaction->wasRecentlyCreated) {
443 - $comment->reactions_count = $comment->reactions_count + 1;
444 - $comment->save();
685 + do_action('fluent_community/comment/react_added', $reaction, $comment, $feed);
445 686 }
446 687 } else {
447 688 // remove the reaction
448 689 $deleted = Reaction::where('user_id', get_current_user_id())
@@ -452,13 +693,14 @@
452 693
453 694 if ($deleted) {
454 695 $comment->reactions_count = $comment->reactions_count - 1;
455 696 $comment->save();
697 + do_action('fluent_community/comment/react_removed', $comment, $feed);
456 698 }
457 699 }
458 700
459 701 return [
460 - 'message' => 'Reaction has been toggled',
702 + 'message' => __('Reaction has been toggled', 'fluent-community'),
461 703 'reactions_count' => $comment->reactions_count,
462 704 'liked' => $reactionState
463 705 ];
464 706 }
@@ -464,20 +706,63 @@
464 706 }
465 707
466 708 public function show(Request $request, $id)
467 709 {
468 - $comment = Comment::with([
469 - 'xprofile' => function ($q) {
470 - return $q->select(ProfileHelper::getXProfilePublicFields());
471 - }
472 - ])->findOrFail($id);
473 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 +
474 720 // Just to verify the permission
475 - $feed = Feed::withoutGlobalScopes()
721 + Feed::withoutGlobalScopes()
476 722 ->byUserAccess($this->getUserId())
477 723 ->findOrFail($comment->post_id);
478 724
479 - 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 = [
480 763 'comment' => $comment
481 764 ];
765 +
766 + return apply_filters('fluent_community/comment_api_response', $data, $request->all());
482 767 }
483 768 }