PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.7.7
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.7.7
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.7.7, at app/Http/Controllers/FeedsController.php

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