PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.8.0
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.8.0
2.10.0 2.10.01 2.9.1 2.9.0 2.8.1 2.8.0 2.7.7 2.7.5 2.7.0 2.6.01 2.6.0 2.5.0 2.4.01 trunk 1.0.90 1.0.91 1.0.92 1.0.93 1.0.94 1.0.95 1.0.96 1.0.97 1.0.98 1.0.99 1.1.0 All 77 releases
fluent-community / app / Http / Controllers / FeedsController.php

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

1,343 lines 48.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCommunity\App\Http\Controllers;
4
5 use FluentCommunity\App\Functions\Utility;
6 use FluentCommunity\App\Models\Media;
7 use FluentCommunity\App\Models\NotificationSubscriber;
8 use FluentCommunity\App\Models\Space;
9 use FluentCommunity\App\Models\User;
10 use FluentCommunity\App\Services\CustomSanitizer;
11 use FluentCommunity\App\Services\FeedsHelper;
12 use FluentCommunity\App\Services\Helper;
13 use FluentCommunity\App\Services\Libs\FileSystem;
14 use FluentCommunity\App\Services\UploadHelper;
15 use FluentCommunity\App\Services\RemoteUrlParser;
16 use FluentCommunity\Framework\Http\Request\Request;
17 use FluentCommunity\App\Models\Feed;
18 use FluentCommunity\App\Models\BaseSpace;
19 use FluentCommunity\Framework\Support\Arr;
20
21 class FeedsController extends Controller
22 {
23 public function get(Request $request)
24 {
25 $start = microtime(true);
26 $space = null;
27 $bySpace = $request->get('space');
28 $userId = $request->getSafe('user_id', 'intval', '');
29 $selectedTopic = $request->getSafe('topic_slug', 'sanitize_text_field', '');
30 $search = $request->getSafe('search', 'sanitize_text_field', '');
31 if ($bySpace) {
32 // just for validation
33 $space = BaseSpace::where('slug', $bySpace)->first();
34 if (!$space) {
35 return $this->sendError(['message' => __('Invalid space slug', 'fluent-community')]);
36 }
37 }
38
39 $currentUserModel = $this->getUser();
40 $currentUserId = get_current_user_id();
41
42 $isOwnProfile = $userId && (int)$userId === (int)$currentUserId;
43
44 $filterableStatuses = apply_filters('fluent_community/feed/filterable_statuses', []);
45
46 $statusFilter = $request->getSafe('status', 'sanitize_text_field', '');
47
48 $applyStatusFilter = $statusFilter
49 && in_array($statusFilter, $filterableStatuses, true)
50 && (Helper::isModerator() || $isOwnProfile);
51
52 $maxPerPage = (int) apply_filters('fluent_community/max_per_page', 100) ?: 100;
53
54 $queryArgs = [
55 'selected_topic' => $selectedTopic,
56 'per_page' => min($maxPerPage, max(1, (int)$request->get('per_page', 10))),
57 'page' => max(1, (int)$request->get('page', 1)),
58 'search' => $search,
59 ];
60
61 $feedsQuery = Feed::select(Feed::$publicColumns)
62 ->with(Feed::withPublicRelations($currentUserModel, $space))
63 ->searchBy($search, (array)$request->get('search_in', ['post_content']))
64 ->byTopicSlug($selectedTopic)
65 ->customOrderBy($request->getSafe('order_by_type'));
66
67 if ($applyStatusFilter) {
68 $feedsQuery->byStatus($statusFilter);
69 } else {
70 $feedsQuery->byContentModerationAccessStatus($currentUserModel, $space);
71 }
72
73 $stickyFeed = null;
74
75 $disableSticky = $request->get('disable_sticky', '') == 'yes' || !!$search || !!$selectedTopic;
76
77 if ($bySpace) {
78 $feedsQuery = $feedsQuery->filterBySpaceSlug($bySpace);
79 $queryArgs['space_slug'] = $bySpace;
80 }
81
82 if ($bySpace && !$disableSticky) {
83 $feedsQuery = $feedsQuery->where('is_sticky', 0);
84 if ($queryArgs['page'] === 1) {
85 $stickyFeed = Feed::where('space_id', $space->id)
86 ->where('is_sticky', 1)
87 ->byUserAccess($currentUserId)
88 ->byContentModerationAccessStatus($currentUserModel, $space)
89 ->with(Feed::withPublicRelations($this->getUser(), $space))
90 ->first();
91 }
92 }
93
94 if ($userId) {
95 $feedsQuery = $feedsQuery->where('user_id', $userId);
96
97 if (!Helper::isModerator()) {
98 $feedsQuery = $feedsQuery->whereHas('xprofile', function ($q) {
99 $q->where('status', 'active');
100 });
101 }
102
103 if ($userId != $currentUserId) {
104 $feedsQuery = $feedsQuery->byUserAccess($currentUserId);
105 }
106
107 $queryArgs['user_id'] = $userId;
108 } else {
109 $feedsQuery->byUserAccess($currentUserId)->whereHas('xprofile', function ($q) {
110 $q->where('status', 'active');
111 });
112 }
113
114 $queryArgs = array_filter($queryArgs);
115 $queryArgs['is_main_query'] = empty($queryArgs['space_slug']) && empty($queryArgs['user_id']) && empty($queryArgs['search']);
116
117 do_action_ref_array('fluent_community/feeds_query', [&$feedsQuery, $request->all(), $queryArgs]);
118
119 $feedsQuery->limit($queryArgs['per_page'])->offset(($queryArgs['page'] - 1) * $queryArgs['per_page']);
120 $feeds = $feedsQuery->get();
121
122 // add $stickyFeed to the first page
123 if ($stickyFeed) {
124 $stickyFeed = FeedsHelper::transformFeed($stickyFeed);
125 }
126
127 $feeds = FeedsHelper::transformFeedsCollection($feeds);
128
129 $currentCount = $feeds->count();
130 $to = ($queryArgs['page'] - 1) * $queryArgs['per_page'] + $currentCount;
131
132 $hasMore = $currentCount == $queryArgs['per_page'];
133
134 $data = [
135 'feeds' => [
136 'data' => $feeds,
137 'current_page' => $queryArgs['page'],
138 'per_page' => $queryArgs['per_page'],
139 'from' => $currentCount ? ($queryArgs['page'] - 1) * $queryArgs['per_page'] + 1 : 0,
140 'to' => $to,
141 'has_more' => $hasMore,
142 'total' => $currentCount == $queryArgs['per_page'] ? $to + $currentCount : $to
143 ],
144 'sticky' => $stickyFeed
145 ];
146
147 $isMainFeed = $queryArgs['page'] === 1 && !$search && !$userId;
148 if ($isMainFeed && $currentUserId) {
149 $data['last_fetched_timestamp'] = current_time('timestamp');
150 }
151
152 $data['execution_time'] = microtime(true) - $start;
153
154 $data = apply_filters('fluent_community/feeds_api_response', $data, $request->all());
155
156 return $data;
157 }
158
159 public function getFeedBySlug(Request $request, $feed_slug)
160 {
161 $start = microtime(true);
162 if ($request->get('context') == 'edit') {
163 $feed = Feed::where('slug', $feed_slug)->first();
164
165 if (!$feed || !$feed->hasEditAccess(get_current_user_id())) {
166 return $this->sendError([
167 'message' => __('You do not have permission to edit this feed', 'fluent-community')
168 ]);
169 }
170
171 $data = [
172 'feed' => FeedsHelper::transformForEdit($feed)
173 ];
174
175 return apply_filters('fluent_community/feed_api_response', $data, $request->all());
176 }
177
178 $feed = Feed::where('slug', $feed_slug)
179 ->select(Feed::$publicColumns)
180 ->with(Feed::withPublicRelations($this->getUser()))
181 ->whereHas('xprofile', function ($q) {
182 $q->where('status', 'active');
183 })
184 ->byUserAccess($this->getUserId())
185 ->first();
186
187 if (!$feed) {
188 return $this->sendError([
189 'message' => __('The feed could not be found', 'fluent-community')
190 ], 404);
191 }
192
193 $viewableByLinkStatuses = ['published', 'unlisted'];
194
195 if (!in_array($feed->status, $viewableByLinkStatuses, true) && !$feed->hasEditAccess($this->getUserId())) {
196 return $this->sendError([
197 'message' => __('Sorry, you do not have permission to view this post', 'fluent-community')
198 ], 404);
199 }
200
201 $feed = FeedsHelper::transformFeed($feed);
202
203 return apply_filters('fluent_community/feed_api_response', [
204 'feed' => $feed,
205 'execution_time' => microtime(true) - $start
206 ], $request->all());
207
208 }
209
210 public function getFeedById(Request $request, $feedId)
211 {
212 $feed = Feed::findOrFail($feedId);
213 return $this->getFeedBySlug($request, $feed->slug);
214 }
215
216 public function getBookmarks(Request $request)
217 {
218 $userId = $this->getUserId();
219
220 $feedsQuery = Feed::where('status', 'published')
221 ->select(Feed::$publicColumns)
222 ->with(Feed::withPublicRelations($this->getUser()))
223 ->byBookMarked($userId)
224 ->byUserAccess($userId)
225 ->byTopicSlug($request->getSafe('topic_slug'))
226 ->customOrderBy($request->getSafe('order_by_type'))
227 ->searchBy($request->getSafe('search'));
228
229 if ($type = $request->get('type')) {
230 $feedsQuery = $feedsQuery->where('type', $type);
231 }
232
233 $queryArgs = [
234 'per_page' => (int)$request->get('per_page', 10),
235 'page' => (int)$request->get('page', 1)
236 ];
237
238 $feeds = $feedsQuery->orderBy('id', 'DESC')
239 ->limit($queryArgs['per_page'])
240 ->offset(($queryArgs['page'] - 1) * $queryArgs['per_page'])
241 ->get();
242
243 $currentCount = $feeds->count();
244 $to = ($queryArgs['page'] - 1) * $queryArgs['per_page'] + $currentCount;
245
246 $hasMore = $currentCount == $queryArgs['per_page'];
247
248 $feeds = FeedsHelper::transformFeedsCollection($feeds);
249
250 $data = [
251 'feeds' => [
252 'data' => $feeds,
253 'current_page' => $queryArgs['page'],
254 'per_page' => $queryArgs['per_page'],
255 'from' => $currentCount ? ($queryArgs['page'] - 1) * $queryArgs['per_page'] + 1 : 0,
256 'to' => $to,
257 'has_more' => $hasMore,
258 'total' => $currentCount == $queryArgs['per_page'] ? $to + $currentCount : $to
259 ]
260 ];
261
262 if ($queryArgs['page'] === 1) {
263 $lastItem = FeedsHelper::getLastFeedId();
264 if ($lastItem) {
265 $data['last_id'] = $lastItem;
266 }
267 }
268
269 return apply_filters('fluent_community/bookmarks_api_response', $data, $request->all());
270 }
271
272 public function store(Request $request)
273 {
274 $user = $this->getUser(true);
275
276 do_action('fluent_community/check_rate_limit/create_post', $user);
277
278 $requestData = $request->all();
279
280 $data = $this->sanitizeAndValidateData($requestData);
281 $data['user_id'] = $user->ID;
282 $data['status'] = 'published';
283
284 $data['status'] = apply_filters('fluent_community/feed/save_status', $data['status'], $requestData, null);
285
286 $feed = new Feed();
287 $feed->user_id = $user->ID;
288 $space = null;
289
290 if ($spaceSlug = $request->get('space')) {
291 $data['space_id'] = $this->validateAndSetSpace($spaceSlug, $user);
292 if ($data['space_id']) {
293 $space = Space::where('id', $data['space_id'])->first();
294 if (!$space) {
295 return $this->sendError([
296 'message' => __('Please select a valid space to post in.', 'fluent-community')
297 ]);
298 }
299 }
300
301 if ($space && Arr::get($space->settings, 'topic_required') == 'yes') {
302 $topicIds = (array)$request->get('topic_ids', []);
303 $spaceTopics = Utility::getTopicsBySpaceId($space->id);
304 $spaceTopicsIds = [];
305
306 foreach ($spaceTopics as $topic) {
307 $spaceTopicsIds[] = $topic['id'];
308 }
309
310 $validTopicIds = array_intersect($topicIds, $spaceTopicsIds);
311
312 if (!$validTopicIds) {
313 return $this->sendError([
314 'message' => __('Please select at least one topic to post in this space.', 'fluent-community'),
315 'shakes' => [
316 'topic_ids' => true
317 ]
318 ]);
319 }
320 }
321
322 } else if (!Helper::hasGlobalPost()) {
323 return $this->sendError([
324 'message' => __('Please select a valid space to post in.', 'fluent-community')
325 ]);
326 }
327
328 $spaceId = Arr::get($data, 'space_id');
329 $message = Arr::get($data, 'message');
330
331 if ($isDulicate = $this->checkForDuplicatePost($user->ID, $message, $spaceId)) {
332 return $isDulicate;
333 }
334
335 $mentions = FeedsHelper::getMentions($data['message'], Arr::get($data, 'space_id'), true);
336 if ($mentions) {
337 $data['message'] = $message;
338 $message = $mentions['text'];
339 }
340
341 [$message, $inlineMedias] = FeedsHelper::replaceImageUrlsWithRealMediaArchive($message);
342
343 // replace new line with br
344 $data['message_rendered'] = wp_kses_post(FeedsHelper::mdToHtml($message));
345
346 $requestData['is_admin'] = $user->hasPermissionOrInCurrentSpace('community_moderator', $space);
347
348 [$data, $mediaItems] = FeedsHelper::processFeedMetaData($data, $requestData);
349
350 if ($inlineMedias) {
351 $mediaItems = array_merge($mediaItems, $inlineMedias);
352 }
353
354 if (Arr::get($requestData, 'send_announcement_email') == 'yes' && $requestData['is_admin']) {
355 $data['meta']['send_announcement_email'] = 'yes';
356 } else if (isset($data['meta']['send_announcement_email'])) {
357 $data['meta']['send_announcement_email'] = 'no';
358 }
359
360 if ($mentions) {
361 $data['meta']['mentioned_user_ids'] = Arr::get($mentions, 'user_ids', []);
362 }
363
364 $data = apply_filters('fluent_community/feed/new_feed_data', $data, $requestData);
365
366 $formContentType = (string)Arr::get($requestData, 'content_type', '');
367
368 if ($formContentType) {
369 $data = apply_filters('fluent_community/feed/new_feed_data_type_' . $formContentType, $data, $requestData);
370 }
371
372 if (is_wp_error($data)) {
373 return $this->sendError([
374 'message' => $data->get_error_message(),
375 'errors' => $data->get_error_data()
376 ]);
377 }
378
379 $feed->fill($data);
380 $feed->save();
381
382 $feed = Feed::find($feed->id); // just renewing the feed
383 /** @var Feed $feed */
384
385 if ($mentions) {
386 do_action('fluent_community/feed_mentioned', $feed, Arr::get($mentions, 'users'));
387 }
388
389 if ($formContentType) {
390 do_action('fluent_community/feed/just_created_type_' . $formContentType, $feed, $requestData);
391 }
392
393 if ($mediaItems) {
394 $this->saveMediaItems($feed, $mediaItems);
395 }
396
397 $feed->load(['xprofile', 'comments.xprofile']);
398 if ($feed->space_id) {
399 $feed->load(['space']);
400 $topicIds = (array)$request->get('topic_ids', []);
401 // take only max topics per post
402 if ($topicIds) {
403 $topicsConfig = Helper::getTopicsConfig();
404 $topicIds = array_slice($topicIds, 0, $topicsConfig['max_topics_per_post']);
405 $feed->attachTopics($topicIds, false);
406 $feed->load(['terms']);
407 }
408 }
409
410
411 if ($feed->status == 'scheduled') {
412 do_action('fluent_community/feed/scheduled', $feed);
413 /* translators: %s: The scheduled date and time for the post */
414 $message = sprintf(__('Your post has been scheduled for %s', 'fluent-community'), $feed->scheduled_at);
415 return [
416 'feed' => FeedsHelper::transformFeed($feed),
417 'scheduled_at' => $feed->scheduled_at,
418 'message' => $message,
419 'last_fetched_timestamp' => current_time('timestamp')
420 ];
421 }
422
423 if (!in_array($feed->status, ['published', 'unlisted'])) {
424 do_action('fluent_community/feed/new_feed_' . $feed->status, $feed);
425 /* translators: %s: The status of the post */
426 $message = sprintf(__('Your post has been marked as %s', 'fluent-community'), $feed->status);
427 return apply_filters('fluent_community/feed/new_feed_response', [
428 'feed' => FeedsHelper::transformFeed($feed),
429 'message' => $message,
430 'last_fetched_timestamp' => current_time('timestamp')
431 ], $feed, $request->all());
432 }
433
434 do_action('fluent_community/feed/created', $feed);
435
436 if ($feed->space_id) {
437 do_action('fluent_community/space_feed/created', $feed);
438 } else {
439 do_action('fluent_community/profile_feed/created', $feed);
440 }
441
442 $message = __('Your post has been published', 'fluent-community');
443
444 return apply_filters('fluent_community/feed/new_feed_response', [
445 'feed' => FeedsHelper::transformFeed($feed),
446 'message' => $message,
447 'last_fetched_timestamp' => current_time('timestamp')
448 ], $feed, $request->all());
449 }
450
451 public function update(Request $request, $feedId)
452 {
453 $requestData = $request->all();
454 $data = $this->sanitizeAndValidateData($requestData);
455 $user = $this->getUser(true);
456 $existingFeed = Feed::findOrFail($feedId);
457 /** @var Feed $existingFeed */
458
459 $editableStatuses = ['published', 'unlisted', 'scheduled', 'pending'];
460
461 if (!in_array($existingFeed->status, $editableStatuses)) {
462 return $this->sendError([
463 'message' => __('Sorry, this post is not in an editable state.', 'fluent-community')
464 ]);
465 }
466
467 $user->canEditFeed($existingFeed, true);
468
469 if ($surveyOptionError = FeedsHelper::getSurveyOptionsUpdateError(
470 Arr::get($existingFeed->meta, 'survey_config.options', []),
471 Arr::get($requestData, 'survey', [])
472 )) {
473 return $this->sendError([
474 'message' => $surveyOptionError
475 ]);
476 }
477
478 if ($status = Arr::get($requestData, 'status')) {
479 if (in_array($status, $editableStatuses, true)) {
480 $fallbackStatus = $status === 'unlisted' ? $existingFeed->status : $status;
481 $data['status'] = apply_filters('fluent_community/feed/save_status', $fallbackStatus, $requestData, $existingFeed);
482 }
483 }
484
485 $message = $data['message'];
486 $mentions = FeedsHelper::getMentions($data['message'], Arr::get($data, 'space_id'));
487 if ($mentions) {
488 $data['message'] = $message;
489 $message = $mentions['text'];
490 }
491
492 [$message, $inlineMedias] = FeedsHelper::replaceImageUrlsWithRealMediaArchive($message, $existingFeed);
493
494 // replace new line with br
495 $data['message_rendered'] = wp_kses_post(FeedsHelper::mdToHtml($message));
496
497 [$data, $mediaItems] = FeedsHelper::processFeedMetaData($data, $requestData, $existingFeed);
498
499 if($inlineMedias) {
500 $mediaItems = array_merge($mediaItems, $inlineMedias);
501 }
502
503 if (isset($existingFeed->meta['comments_disabled'])) {
504 $data['meta']['comments_disabled'] = $existingFeed->meta['comments_disabled'];
505 }
506
507 $requestData['is_admin'] = $user->hasPermissionOrInCurrentSpace('community_moderator', $existingFeed->space);
508
509 if (Arr::get($requestData, 'send_announcement_email') == 'yes' && $requestData['is_admin']) {
510 $data['meta']['send_announcement_email'] = 'yes';
511 } else if (Arr::get($existingFeed->meta, 'send_announcement_email')) {
512 $data['meta']['send_announcement_email'] = Arr::get($existingFeed->meta, 'send_announcement_email');
513 }
514
515 $data = apply_filters('fluent_community/feed/update_feed_data', $data, $requestData);
516
517 if (is_wp_error($data)) {
518 return $this->sendError([
519 'message' => $data->get_error_message(),
520 'errors' => $data->get_error_data()
521 ]);
522 }
523
524 $newContentType = Arr::get($requestData, 'content_type', '');
525 $existingContentType = $existingFeed->content_type;
526
527 if (($newContentType === 'document' && empty($requestData['document_ids'])) || ($newContentType === '' && $existingContentType === 'document' && empty($requestData['survey']))) {
528 $newContentType = $data['content_type'] = 'text';
529 }
530
531 if ($newContentType != $existingContentType) {
532 // Content Type Changed
533 do_action('fluent_community/feed/updating_content_type_old_' . $existingContentType, $existingFeed, $newContentType, $requestData);
534 }
535
536 if ($newContentType != 'text') {
537 $data = apply_filters('fluent_community/feed/update_feed_data_type_' . $newContentType, $data, $requestData, $existingFeed);
538 if (is_wp_error($data)) {
539 return $this->sendError([
540 'message' => $data->get_error_message(),
541 'errors' => $data->get_error_data()
542 ]);
543 }
544 }
545
546 if ($message != $existingFeed->message) {
547 $data['meta']['last_edited'] = [
548 'user_id' => $user->ID,
549 'time' => current_time('mysql')
550 ];
551 }
552
553 if ($newSpaceId = $request->get('new_space_id')) {
554 if (!Helper::isUserInSpace($existingFeed->user_id, $newSpaceId)) {
555 return $this->sendError([
556 'message' => __('The author is not a member of the selected space', 'fluent-community')
557 ]);
558 }
559
560 $newSpace = Space::findOrFail($newSpaceId);
561
562 // check if the current user is admin
563 if (!$user->hasPermissionOrInCurrentSpace('community_admin', $newSpace)) {
564 return $this->sendError([
565 'message' => __('Sorry, you do not have permission to change the space for this post', 'fluent-community')
566 ]);
567 }
568
569 $data['space_id'] = $newSpaceId;
570
571 \FluentCommunity\App\Models\Activity::where('feed_id', $existingFeed->id)
572 ->update(['space_id' => $newSpaceId]);
573 } else if ($request->get('move_to_profile')) {
574 if (!$user->hasPermissionOrInCurrentSpace('community_admin', $existingFeed->space)) {
575 return $this->sendError([
576 'message' => __('Sorry, you do not have permission to move this post to a profile', 'fluent-community')
577 ]);
578 }
579
580 $data['space_id'] = null;
581
582 \FluentCommunity\App\Models\Activity::where('feed_id', $existingFeed->id)
583 ->update(['space_id' => null]);
584 }
585
586 $data = apply_filters('fluent_community/feed/update_data', $data, $existingFeed);
587 $existingFeed->fill($data);
588 $dirty = $existingFeed->getDirty();
589
590 $existingFeed->fill($data);
591 $existingFeed->save();
592
593 if ($message != $existingFeed->message) {
594 $editHistory = $existingFeed->getCustomMeta('_edit_history', []);
595 if (!$editHistory) {
596 $editHistory = [];
597 }
598
599 $editHistory[] = array_filter([
600 'user_id' => $user->ID,
601 'time' => current_time('mysql'),
602 'prev_message' => $existingFeed->message,
603 'prev_title' => $existingFeed->title
604 ]);
605
606 // get last 5 edit history
607 $editHistory = array_slice($editHistory, -5);
608 $existingFeed->updateCustomMeta('_edit_history', $editHistory);
609 }
610
611 $mediaItemIds = [];
612 foreach ($mediaItems as $mediaItem) {
613 $mediaItemIds[] = $mediaItem->id;
614 }
615
616 if (Arr::has($requestData, 'media_images')) {
617 $deactivateQuery = Media::where('object_source', 'feed')
618 ->where('feed_id', $existingFeed->id)
619 ->whereNotIn('id', $mediaItemIds);
620
621 if (empty(Arr::get($requestData, 'media_images'))) {
622 $deactivateQuery->where('media_type', '!=', 'fluent_player');
623 }
624
625 $deactivateQuery->update(['is_active' => 0]);
626 }
627
628 if ($mediaItems) {
629 $this->saveMediaItems($existingFeed, $mediaItems);
630 }
631
632 $existingFeed->load(['xprofile', 'comments.xprofile']);
633
634 if ($existingFeed->space_id) {
635 $existingFeed->load(['space']);
636 $space = $existingFeed->space;
637 $topicIds = (array)Arr::get($requestData, 'topic_ids', []);
638 $topicsConfig = Helper::getTopicsConfig();
639 // take only max topics per post
640 if ($topicIds) {
641 $topicIds = array_slice($topicIds, 0, $topicsConfig['max_topics_per_post']);
642 $existingFeed->attachTopics($topicIds, true);
643 } else {
644 if ($space && Arr::get($space->settings, 'topic_required') != 'yes') {
645 $existingFeed->terms()->where('taxonomy_name', 'post_topic')->detach();
646 }
647 }
648 }
649
650 if ($dirty) {
651 do_action('fluent_community/feed/updated', $existingFeed, $dirty);
652 if ($existingFeed->space_id) {
653 do_action('fluent_community/space_feed/updated', $existingFeed);
654 }
655 }
656
657 $data = [
658 'feed' => FeedsHelper::transformFeed($existingFeed),
659 'message' => __('Your post has been updated', 'fluent-community')
660 ];
661
662 return apply_filters('fluent_community/feed/update_feed_response', $data, $request->all());
663 }
664
665 public function patchFeed(Request $request, $feedId)
666 {
667 $feed = Feed::findOrFail($feedId);
668 $user = $this->getUser(true);
669
670 $isAuthor = $feed->user_id == $user->ID;
671 $isMod = $user->hasPermissionOrInCurrentSpace('community_moderator', $feed->space);
672 $isAdmin = $user->hasPermissionOrInCurrentSpace('community_admin', $feed->space);
673
674 if (!$isMod && !$isAuthor && !$isAdmin) {
675 return $this->sendError([
676 'message' => __('You do not have permission to perform this action', 'fluent-community')
677 ]);
678 }
679
680 $allData = $request->all();
681 $validKeys = ['is_sticky', 'priority', 'comments_disabled'];
682
683 if (!$isMod) {
684 $validKeys = ['comments_disabled'];
685 }
686
687 $data = Arr::only($allData, $validKeys);
688
689 $data = array_map('intval', $data);
690
691 // List/unlist toggle — community-moderator only, routed through the shared save_status filter.
692 if (Helper::isModerator($user)
693 && ($reqStatus = Arr::get($allData, 'status'))
694 && in_array($reqStatus, ['published', 'unlisted'], true)
695 && in_array($feed->status, ['published', 'unlisted'], true)
696 ) {
697 $fallbackStatus = $reqStatus === 'unlisted' ? $feed->status : $reqStatus;
698 $data['status'] = apply_filters('fluent_community/feed/save_status', $fallbackStatus, $allData, $feed);
699 }
700
701 if (isset($data['is_sticky'])) {
702 $data['is_sticky'] = $data['is_sticky'] ? 1 : 0;
703 if ($data['is_sticky'] && $feed->space_id) {
704 // remove all the sticky posts from the space
705 Feed::where('space_id', $feed->space_id)
706 ->where('is_sticky', 1)
707 ->update(['is_sticky' => 0]);
708 }
709 }
710
711 if (isset($data['comments_disabled'])) {
712 $meta = $feed->meta;
713 $meta['comments_disabled'] = $data['comments_disabled'] ? 'yes' : 'no';
714 $data['meta'] = $meta;
715 }
716
717 if ($data) {
718 $feed->fill($data);
719 $dirty = $feed->getDirty();
720 if ($dirty) {
721 $feed->save();
722 do_action('fluent_community/feed/updated', $feed, $dirty);
723 }
724 }
725
726 return apply_filters('fluent_community/feed/patch_feed_response', [
727 'feed' => $feed,
728 'message' => __('Feed updated', 'fluent-community')
729 ], $feed, $request->all());
730 }
731
732 public function getWelcomeBanner(Request $request)
733 {
734 $scope = get_current_user_id() ? 'login' : 'logout';
735
736 $data = [
737 'welcome_banner' => Helper::getWelcomeBanner($scope)
738 ];
739
740 return apply_filters('fluent_community/welcome_banner_api_response', $data, $request->all());
741 }
742
743 public function getLinks(Request $request)
744 {
745 $scope = $request->getSafe('scope');
746
747 if ($scope == 'view') {
748 $data = [
749 'links' => Helper::getEnabledFeedLinks()
750 ];
751
752 return apply_filters('fluent_community/feed_links_api_response', $data, $request->all());
753 }
754
755 $data = [
756 'links' => Helper::getFeedLinks()
757 ];
758
759 return apply_filters('fluent_community/feed_links_api_response', $data, $request->all());
760 }
761
762 public function updateLinks(Request $request)
763 {
764 $links = $request->get('links', []);
765
766 $links = array_map(function ($link) {
767 return CustomSanitizer::santizeLinkItem($link);
768 }, $links);
769
770 Helper::updateFeedLinks($links);
771
772 return [
773 'message' => __('Links have been updated.', 'fluent-community'),
774 'links' => $links
775 ];
776 }
777
778 private function saveMediaItems($feed, $mediaItems)
779 {
780 foreach ($mediaItems as $media) {
781 $media->feed_id = $feed->id;
782 $media->is_active = 1;
783 $media->object_source = 'feed';
784 $media->save();
785 }
786 }
787
788 private function sanitizeAndValidateData($data)
789 {
790 $data['type'] = 'text';
791
792 $this->validate($data, [
793 'message' => 'required'
794 ], [
795 'message.required' => __('Message is required', 'fluent-community'),
796 ]);
797
798 return FeedsHelper::sanitizeAndValidateData($data);
799 }
800
801 private function checkForDuplicatePost($userId, $message, $spaceId = null)
802 {
803 if (apply_filters('fluent_community/disable_duplicate_post_check', false, $userId, $spaceId)) {
804 return false;
805 }
806
807 $message = trim($message);
808
809 $exist = Feed::where('user_id', $userId)
810 ->where('message', $message)
811 ->where('created_at', '>', gmdate('Y-m-d H:i:s', current_time('timestamp') - 7 * 24 * 60 * 60))
812 ->when($spaceId, function ($query) use ($spaceId) {
813 $query->where('space_id', $spaceId);
814 })
815 ->first();
816
817 if ($exist) {
818 return $this->sendError(['message' => __('No duplicate post please!', 'fluent-community')]);
819 }
820
821 return false;
822 }
823
824 private function validateAndSetSpace($spaceSlug, $user)
825 {
826 if ($spaceSlug == '__self__post__') {
827 if (!Helper::hasGlobalPost()) {
828 throw new \Exception(esc_html__('Please select a valid space to post in', 'fluent-community'));
829 }
830
831 return null;
832 }
833
834 $space = Space::where('slug', $spaceSlug)->first();
835
836 if (!$space) {
837 throw new \Exception(esc_html__('Please select a valid space to post in', 'fluent-community'));
838 }
839
840 $user->verifySpacePermission('can_create_post', $space);
841
842 return $space->id;
843 }
844
845 public function deleteFeed(Request $request, $feed_id)
846 {
847 $feed = Feed::findOrFail($feed_id);
848
849 $user = User::find(get_current_user_id());
850 $user->canDeleteFeed($feed, true);
851 do_action('fluent_community/feed/before_deleted', $feed);
852 $feed->delete();
853
854 do_action('fluent_community/feed/deleted', $feed_id);
855
856 return [
857 'message' => __('Feed has been deleted successfully', 'fluent-community')
858 ];
859 }
860
861 public function deleteMediaPreview(Request $request, $feed_id)
862 {
863 $feed = Feed::findOrFail($feed_id);
864 $user = User::find(get_current_user_id());
865 $user->canDeleteFeed($feed, true);
866
867 //do_action('fluent_community/feed/media_deleted', $feed->media);
868
869 $meta = $feed->meta;
870 $meta['media_preview'] = null;
871
872 $feed->meta = $meta;
873 $feed->save();
874
875 return [
876 'message' => __('Media preview image has been removed successfully.', 'fluent-community')
877 ];
878 }
879
880 public function handleMediaUpload(Request $request)
881 {
882 if ($error = Helper::checkUploadSizeError()) {
883 return $this->sendError($error, 413);
884 }
885
886 $user = $this->getUser(true);
887
888 do_action('fluent_community/check_rate_limit/media_upload', $user);
889
890 $allowedMimeTypesArray = apply_filters('fluent_community/support_attachment_types', [
891 'image/jpeg',
892 'image/pjpeg',
893 'image/png',
894 'image/gif',
895 'image/webp',
896 'image/heic',
897 ]);
898
899 $allowedTypes = implode(',', $allowedMimeTypesArray);
900
901 // Extensions eligible for WebP conversion (from allowed MIME types, excluding webp)
902 $convertibleExtensions = [];
903 foreach ($allowedMimeTypesArray as $mime) {
904 $element = explode('/', $mime);
905 $ext = end($element);
906 if ($ext === 'pjpeg') {
907 $ext = 'jpeg';
908 }
909 if ($ext && $ext !== 'webp' && !in_array($ext, $convertibleExtensions)) {
910 $convertibleExtensions[] = $ext;
911 }
912 }
913 // jpg is a common alias for jpeg — add only if jpeg is allowed
914 if (in_array('jpeg', $convertibleExtensions)) {
915 $convertibleExtensions[] = 'jpg';
916 }
917
918 $maxFileUnit = apply_filters('fluent_community/media_upload_max_file_unit', 'MB');
919 $maxFileSize = apply_filters('fluent_community/media_upload_max_file_size', 100);
920
921 $allowedFileSize = $maxFileSize;
922 if (strtoupper($maxFileUnit) == 'MB') {
923 $allowedFileSize = $maxFileSize * 1024;
924 } else if (strtoupper($maxFileUnit) == 'GB') {
925 $allowedFileSize = $maxFileSize * 1024 * 1024;
926 }
927
928 $files = $this->validate($this->request->files(), [
929 'file' => 'mimetypes:' . $allowedTypes . '|max:' . $allowedFileSize,
930 ], [
931 'file.mimetypes' => __('The file must be an image type.', 'fluent-community'),
932 /* translators: %$1s is replaced by the maximum allowed file size, %2$s is replaced by the file size unit (e.g. MB) */
933 'file.max' => sprintf(__('The file size must be less than %1$s%2$s.', 'fluent-community'), $maxFileSize, $maxFileUnit)
934 ]);
935
936 if (Arr::get($files, 'file.type') === 'image/heic'
937 && (!extension_loaded('imagick') || !class_exists('Imagick') || !in_array('HEIC', \Imagick::queryFormats('HEIC')))
938 ) {
939 return $this->sendError([
940 'message' => __('HEIC image format is not supported on this system.', 'fluent-community')
941 ]);
942 }
943
944 add_filter('wp_handle_upload', [UploadHelper::class, 'fixImageOrientation']);
945 $uploadedFiles = FileSystem::put($files);
946 remove_filter('wp_handle_upload', [UploadHelper::class, 'fixImageOrientation']);
947
948 $file = $uploadedFiles[0];
949
950 $upload_dir = wp_upload_dir();
951
952 $originalUrl = $file['url'];
953 $orginalPath = $upload_dir['basedir'] . '/fluent-community/' . $file['file'];
954 $originalFileType = $file['type'];
955 $originalFileName = $file['file'];
956
957 $willWebPConvert = $request->get('disable_convert') != 'yes';
958
959 $willWebPConvert = apply_filters('fluent_community/convert_image_to_webp', $willWebPConvert, $file);
960 $willResize = $request->get('resize');
961 $maxWidth = $request->get('max_width');
962
963 $willResize = apply_filters('fluent_community/media_upload_resize', $willResize, $file);
964
965 if ($context = $request->get('context')) {
966 $maxWidth = apply_filters('fluent_community/media_upload_max_width_' . $context, $maxWidth, $file);
967 }
968
969 if ($willResize && $maxWidth) {
970 $upload_dir = wp_upload_dir();
971 $fileUrl = str_replace($upload_dir['baseurl'], $upload_dir['basedir'], $file['url']);
972
973 $editor = wp_get_image_editor($fileUrl);
974
975 if (!is_wp_error($editor) && $editor->get_size()['width'] > $maxWidth) {
976 // Current file extension
977 $ext = pathinfo($file['url'], PATHINFO_EXTENSION);
978 $willConvert = in_array($ext, $convertibleExtensions) && $willWebPConvert;
979
980 if ($willConvert) {
981 $dottedExtensions = array_map(function ($ext) {
982 return '.' . $ext;
983 }, $convertibleExtensions);
984
985 $fileUrl = str_replace($dottedExtensions, '.webp', $fileUrl);
986 $file['file'] = str_replace($dottedExtensions, '.webp', $file['file']);
987 $file['url'] = str_replace($dottedExtensions, '.webp', $file['url']);
988 $file['type'] = 'image/webp';
989 }
990
991 // resize the image
992 $editor->resize($maxWidth, null, false);
993 $editor->set_quality(90);
994 if ($willConvert) {
995 $result = $editor->save($fileUrl, 'image/webp');
996 if ($result['mime-type'] == 'image/webp') {
997 // remove original file now
998 wp_delete_file(str_replace('.webp', '.' . $ext, $fileUrl));
999 }
1000 $file['is_converted'] = true;
1001 } else {
1002 $result = $editor->save($fileUrl);
1003 }
1004
1005 if ($result['mime-type'] != 'image/webp') {
1006 $file['file'] = $originalFileName;
1007 $file['url'] = $originalUrl;
1008 $file['type'] = $result['mime-type'];
1009 }
1010
1011 $file['meta'] = [
1012 'width' => $editor->get_size()['width'],
1013 'height' => $editor->get_size()['height']
1014 ];
1015 }
1016 $file['path'] = $upload_dir['basedir'] . '/fluent-community/' . $file['file'];
1017 } else {
1018 $upload_dir = wp_upload_dir();
1019 $file['path'] = $upload_dir['basedir'] . '/fluent-community/' . $file['file'];
1020 }
1021
1022 if ($willWebPConvert && empty($file['is_converted']) && !$request->get('skip_convert')) {
1023 $path = $file['path'];
1024 $extension = pathinfo($path, PATHINFO_EXTENSION);
1025
1026 if ($extension != 'webp' && in_array($extension, $convertibleExtensions)) {
1027 // Let's convert to webp
1028 $editor = wp_get_image_editor($file['path']);
1029 if (!is_wp_error($editor)) {
1030 $file['path'] = str_replace('.' . $extension, '.webp', $file['path']);
1031 $file['url'] = str_replace('.' . $extension, '.webp', $file['url']);
1032 $file['type'] = 'image/webp';
1033 $result = $editor->save($file['path'], 'image/webp');
1034
1035 if ($result['mime-type'] != 'image/webp') {
1036 $file['path'] = $orginalPath;
1037 $file['url'] = $originalUrl;
1038 $file['type'] = $result['mime-type'];
1039 } else {
1040 wp_delete_file($orginalPath);
1041 }
1042
1043 $file['meta'] = [
1044 'width' => $editor->get_size()['width'],
1045 'height' => $editor->get_size()['height']
1046 ];
1047 }
1048 }
1049 }
1050
1051 $mediaData = [
1052 'media_type' => $file['type'],
1053 'driver' => 'local',
1054 'media_path' => $file['path'],
1055 'media_url' => $file['url'],
1056 'settings' => Arr::get($file, 'meta', [])
1057 ];
1058
1059 $mediaData = apply_filters('fluent_community/media_upload_data', $mediaData, $file);
1060
1061 if (is_wp_error($mediaData)) {
1062 return $this->sendError([
1063 'message' => $mediaData->get_error_message(),
1064 'errors' => $mediaData->get_error_data()
1065 ]);
1066 }
1067
1068 if (!$mediaData) {
1069 return $this->sendError([
1070 'message' => __('Error while uploading the media', 'fluent-community')
1071 ]);
1072 }
1073
1074 // Let's create the media now
1075 $media = Media::create($mediaData);
1076
1077 $mediaUrl = $media->public_url;
1078
1079 $mediaUrl = add_query_arg([
1080 'media_key' => $media->media_key,
1081 ], $mediaUrl);
1082
1083 return [
1084 'media' => [
1085 'url' => $mediaUrl,
1086 'media_key' => $media->media_key,
1087 'type' => $media->media_type,
1088 'width' => Arr::get($media->settings, 'width'),
1089 'height' => Arr::get($media->settings, 'height')
1090 ]
1091 ];
1092 }
1093
1094 public function getTicker(Request $request)
1095 {
1096 $start = microtime(true);
1097
1098 $userId = get_current_user_id();
1099 if (!$userId) {
1100 return [
1101 'timestamp' => current_time('mysql', true),
1102 'has_changes' => false,
1103 'error' => __('User not authenticated', 'fluent-community'),
1104 'feeds' => []
1105 ];
1106 }
1107
1108 do_action('fluent_community/track_activity');
1109
1110
1111 // Support both old and new format
1112 $since = $request->get('since');
1113 if (!$since) {
1114 $since = date('Y-m-d H:i:s', current_time('timestamp') - 60); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
1115 } else {
1116 $timestamp = strtotime($since);
1117 if (current_time('timestamp') - $timestamp > 300) {
1118 $since = date('Y-m-d H:i:s', current_time('timestamp') - 60); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
1119 }
1120 }
1121
1122 $feedUpdates = [];
1123 $hasChanges = false;
1124
1125 // Get feed updates if since timestamp provided
1126 if ($since) {
1127 // Get all updated/created feeds with full data (including relationships)
1128 $currentUserModel = Helper::getCurrentUser();
1129 $updatedFeeds = Feed::where('updated_at', '>', $since)
1130 ->where('status', 'published')
1131 ->byUserAccess($userId)
1132 ->with(Feed::withPublicRelations($currentUserModel, null))
1133 ->orderBy('updated_at', 'desc')
1134 ->limit(20) // Reduced limit since we're sending full data
1135 ->get();
1136
1137 // Transform feeds to include all necessary data
1138 $transformedFeeds = FeedsHelper::transformFeedsCollection($updatedFeeds);
1139
1140 foreach ($transformedFeeds as $feed) {
1141 $isNew = $feed->created_at >= $since;
1142
1143 // Determine context (primary context)
1144 $context = 'global';
1145 if ($feed->space_id && $feed->space) {
1146 $context = 'space-' . $feed->space->slug;
1147 }
1148
1149 $feedUpdates[] = [
1150 'id' => $feed->id,
1151 'updated_at' => $feed->updated_at,
1152 'action' => $isNew ? 'created' : 'updated',
1153 'context' => $context,
1154 'user_id' => $feed->user_id,
1155 'feed_data' => $feed // Include full feed data
1156 ];
1157 }
1158
1159 $hasChanges = !empty($feedUpdates);
1160 }
1161
1162 // Get notification count
1163 $notificationCount = NotificationSubscriber::unread()->where('user_id', $userId)->count();
1164
1165 $response = [
1166 'timestamp' => current_time('mysql'),
1167 'has_changes' => $hasChanges,
1168 'feeds' => $feedUpdates,
1169 'notifications' => [
1170 'unread_count' => $notificationCount,
1171 'new_count' => 0 // Could track new since last check
1172 ],
1173 'spaces' => [], // For future use
1174 'execution_time' => microtime(true) - $start
1175 ];
1176
1177 return apply_filters('fluent_community/feed_ticker', $response, $request->all());
1178 }
1179
1180 public function batchFetch(Request $request)
1181 {
1182 $feedIds = $request->get('feed_ids', []);
1183
1184 if (empty($feedIds) || !is_array($feedIds)) {
1185 return [
1186 'feeds' => [],
1187 'error' => __('No feed IDs provided', 'fluent-community')
1188 ];
1189 }
1190
1191 $userId = get_current_user_id();
1192
1193 // Limit to 20 feeds per batch to prevent abuse
1194 $feedIds = array_slice($feedIds, 0, 20);
1195
1196 // Build query based on context
1197 $query = Feed::whereIn('id', $feedIds)
1198 ->where('status', 'published')
1199 ->byUserAccess($userId);
1200
1201 $currentUserModel = $this->getUser();
1202
1203 $feeds = $query
1204 ->with(Feed::withPublicRelations($currentUserModel))
1205 ->get();
1206
1207 $feeds = FeedsHelper::transformFeedsCollection($feeds);
1208
1209 return [
1210 'feeds' => $feeds,
1211 'count' => $feeds->count()
1212 ];
1213 }
1214
1215 public function getTickerUpdates(Request $request)
1216 {
1217 $context = $request->get('context', 'global');
1218 $since = $request->get('since'); // ISO 8601 timestamp
1219
1220 $userId = get_current_user_id();
1221 if (!$userId) {
1222 return [
1223 'updates' => [],
1224 'timestamp' => current_time('mysql', true),
1225 'has_changes' => false,
1226 'error' => __('User not authenticated', 'fluent-community')
1227 ];
1228 }
1229
1230 // Parse since timestamp
1231 try {
1232 $sinceDate = $since ? new \DateTime($since) : null;
1233 } catch (\Exception $e) {
1234 return [
1235 'updates' => [],
1236 'timestamp' => current_time('mysql', true),
1237 'has_changes' => false,
1238 'error' => __('Invalid timestamp format', 'fluent-community')
1239 ];
1240 }
1241
1242 // Build query based on context
1243 $query = Feed::query();
1244
1245 if (strpos($context, 'space-') === 0) {
1246 $spaceSlug = str_replace('space-', '', $context);
1247 $space = Space::where('slug', $spaceSlug)->first();
1248 if ($space) {
1249 $query->where('space_id', $space->id);
1250 }
1251 } elseif (strpos($context, 'user-') === 0) {
1252 $targetUserId = str_replace('user-', '', $context);
1253 $query->where('user_id', $targetUserId);
1254 }
1255
1256 // Apply access control
1257 $query->byUserAccess($userId);
1258
1259 $updates = [];
1260
1261 // Get updated feeds (updated_at changed)
1262 if ($sinceDate) {
1263 $updatedFeeds = (clone $query)
1264 ->where('updated_at', '>', $sinceDate->format('Y-m-d H:i:s'))
1265 ->where('status', 'published')
1266 ->select(['id', 'updated_at', 'created_at'])
1267 ->orderBy('updated_at', 'desc')
1268 ->limit(100)
1269 ->get();
1270
1271 foreach ($updatedFeeds as $feed) {
1272 $isNew = $feed->created_at >= $sinceDate->format('Y-m-d H:i:s');
1273
1274 $updates[] = [
1275 'id' => $feed->id,
1276 'updated_at' => gmdate('c', strtotime($feed->updated_at)),
1277 'action' => $isNew ? 'created' : 'updated'
1278 ];
1279 }
1280
1281 // Check for deleted feeds (status changed to deleted)
1282 $deletedFeeds = (clone $query)
1283 ->where('updated_at', '>', $sinceDate->format('Y-m-d H:i:s'))
1284 ->whereIn('status', ['deleted', 'draft'])
1285 ->select(['id', 'updated_at'])
1286 ->limit(50)
1287 ->get();
1288
1289 foreach ($deletedFeeds as $feed) {
1290 $updates[] = [
1291 'id' => $feed->id,
1292 'updated_at' => gmdate('c', strtotime($feed->updated_at)),
1293 'action' => 'deleted'
1294 ];
1295 }
1296 }
1297
1298 return [
1299 'updates' => $updates,
1300 'timestamp' => current_time('mysql', true),
1301 'has_changes' => !empty($updates)
1302 ];
1303 }
1304
1305 public function getOembed(Request $request)
1306 {
1307 $url = $request->get('url');
1308 // check if the url is valid
1309 $metaData = RemoteUrlParser::parse($url);
1310
1311 if ($metaData && !is_wp_error($metaData)) {
1312 $data = [
1313 'oembed' => $metaData
1314 ];
1315 return apply_filters('fluent_community/feed_oembed_api_response', $data, $request->all());
1316 }
1317
1318 return $this->sendError([
1319 'message' => __('No oembed data found', 'fluent-community'),
1320 'url' => $url
1321 ]);
1322 }
1323
1324 public function markdownToHtml(Request $request)
1325 {
1326 $message = CustomSanitizer::unslashMarkdown($request->get('text', ''));
1327
1328 $html = wp_kses_post(FeedsHelper::mdToHtml($message));
1329
1330 $data = [
1331 'html' => $html
1332 ];
1333
1334 $data['message_rendered'] = $html;
1335
1336 if (in_array('meta', $request->get('with', [])) && $request->get('feed')) {
1337 [$data,] = FeedsHelper::processFeedMetaData($data, $request->get('feed'));
1338 }
1339
1340 return $data;
1341 }
1342 }
1343