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