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

FeedsHelper.php in FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses 2.7.5, at app/Services/FeedsHelper.php

1,157 lines 40.7 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\Services;
4
5 use FluentCommunity\App\Functions\Utility;
6 use FluentCommunity\App\Models\BaseSpace;
7 use FluentCommunity\App\Models\Feed;
8 use FluentCommunity\App\Models\Media;
9 use FluentCommunity\App\Models\Reaction;
10 use FluentCommunity\App\Models\Term;
11 use FluentCommunity\App\Models\User;
12 use FluentCommunity\App\Models\XProfile;
13 use FluentCommunity\Framework\Support\Arr;
14 use FluentCommunity\Framework\Validator\Validator;
15
16 class FeedsHelper
17 {
18 static protected $currentRelatedUserIds = [];
19
20 public static function setCurrentRelatedUserId($userId)
21 {
22 self::$currentRelatedUserIds[] = $userId;
23 }
24
25 public static function getCurrentRelatedUserIds()
26 {
27 return array_values(array_unique(self::$currentRelatedUserIds));
28 }
29
30 /**
31 * Resolve who should receive the "post author" notification for a feed.
32 * Course lessons notify the COURSE creator (whoever created the course),
33 * not the user who uploaded the individual lesson.
34 */
35 public static function getNotificationAuthorId($feed)
36 {
37 if ($feed->type === 'course_lesson' && $feed->space_id) {
38 $course = BaseSpace::withoutGlobalScopes()->find($feed->space_id);
39 if ($course && $course->created_by) {
40 return (int) $course->created_by;
41 }
42 }
43
44 return (int) $feed->user_id;
45 }
46
47 public static function getSpaceSlugsByUserId($userId)
48 {
49 if (!$userId) {
50 $userId = get_current_user_id();
51 }
52
53 if (!$userId) {
54 return [];
55 }
56
57 $user = User::find($userId);
58
59 return $user->spaces()->pluck('slug')->toArray();
60 }
61
62 public static function getLastFeedId()
63 {
64 $lastItem = Feed::where('status', 'published')
65 ->byUserAccess(get_current_user_id())
66 ->orderBy('id', 'DESC')
67 ->first();
68
69 if ($lastItem) {
70 return $lastItem->id;
71 }
72
73 return 1;
74 }
75
76 public static function mdToHtml($text, $options = [])
77 {
78 if (!$text) {
79 return '';
80 }
81
82 $text = str_replace('&#x20;', '', $text); // hide markdown empty content
83
84 $html = (new \FluentCommunity\App\Services\Parsedown([
85 ]))
86 ->setBreaksEnabled(true)
87 ->setUrlsLinked(false)
88 // ->setSafeMode(true)
89 ->text($text);
90
91 if (!Arr::get($options, 'disable_link_process')) {
92 // add nofollow to all links. But check if nofollow is already there
93 $html = self::addNoFollowToLinks($html);
94 }
95
96 $html = wp_kses($html, array(
97 'p' => array(),
98 'br' => array(),
99 'strong' => array(),
100 'em' => array(),
101 'hr' => array(),
102 'h1' => array(),
103 'h2' => array(),
104 'h3' => array(),
105 'h4' => array(),
106 'h5' => array(),
107 'h6' => array(),
108 'ul' => array(),
109 'b' => array(),
110 'ol' => array(),
111 'li' => array(),
112 'span' => array(),
113 'a' => array(
114 'href' => true,
115 'title' => true,
116 'rel' => true,
117 'target' => true,
118 ),
119 'img' => array(
120 'src' => true,
121 'alt' => true,
122 ),
123 'code' => array(),
124 'pre' => array(),
125 'blockquote' => array(),
126 'del' => array(),
127 'table' => array(),
128 'thead' => array(),
129 'tbody' => array(),
130 'tfoot' => array(),
131 'tr' => array(),
132 'th' => array(
133 'align' => true,
134 'style' => true,
135 'colspan' => true,
136 'rowspan' => true,
137 ),
138 'td' => array(
139 'align' => true,
140 'style' => true,
141 'colspan' => true,
142 'rowspan' => true,
143 ),
144 ));
145
146 return self::maybeTransformDynamicCodes($html);
147 }
148
149 public static function maybeTransformDynamicCodes($html)
150 {
151 // check if there has {{
152 if (strpos($html, '{{') === false) {
153 return $html;
154 }
155
156 return preg_replace_callback(
157 '/{{utc:(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})}}/',
158 function ($match) {
159 // Extract the datetime string (e.g., 2025-06-01 15:06:59)
160 $datetimeStr = $match[1];
161
162 try {
163 // Create a DateTime object from the UTC string
164 $date = new \DateTime($datetimeStr, new \DateTimeZone('UTC'));
165 // Get the Unix timestamp for the data-timestamp attribute
166 $timestamp = $date->getTimestamp();
167 // Format the display string
168 $displayFormat = $date->format('d F Y, H:i') . ' (UTC)';
169
170 // Return the formatted HTML
171 return '<span class="fcom_dynamic_prop" data-type="timestamp" data-timestamp="' . $timestamp . '">' . $displayFormat . '</span>';
172 } catch (\Exception $e) {
173 // Return original match if parsing fails
174 return $match[0];
175 }
176 },
177 $html
178 );
179 }
180
181 public static function addNoFollowToLinks($html)
182 {
183 if (!$html) {
184 return '';
185 }
186
187 $current_domain = wp_parse_url(home_url(), PHP_URL_HOST);
188
189 // Regular expression to match <a> tags
190 $pattern = '/<a\s[^>]*href=("|\')(https?:\/\/(?!' . preg_quote($current_domain, '/') . ').*?)("|\')\s?([^>]*)>/i';
191
192 // Callback function to modify each matched <a> tag
193 $callback = function ($matches) {
194 $url = $matches[2];
195 $attr = $matches[4];
196
197 // Remove existing rel attribute if present
198 $attr = preg_replace('/\srel=("|\').*?("|\')/i', '', $attr);
199
200 // Add nofollow
201 return '<a href="' . $url . '" rel="nofollow" ' . trim($attr) . '>';
202 };
203
204 // Perform the replacement
205 return preg_replace_callback($pattern, $callback, $html);
206 }
207
208 public static function addNewTabToLinks($html)
209 {
210 if (empty($html) || !is_string($html)) {
211 return '';
212 }
213
214 // return is there has no href
215 if (strpos($html, 'href=') === false) {
216 return $html;
217 }
218
219 // More comprehensive regex to capture existing attributes
220 $pattern = '/<a\s+([^>]*)>/i';
221
222 // Callback function to modify each matched <a> tag
223 $callback = function ($matches) {
224 $full_tag = $matches[0];
225 $attributes = $matches[1];
226
227 // Extract href
228 preg_match('/href=("|\')([^"\']+)("|\')/', $full_tag, $href_matches);
229 if (empty($href_matches)) {
230 return $full_tag;
231 }
232 $url = $href_matches[2];
233
234 // Check if it's an external URL and not an image
235 if (preg_match('/^https?:\/\//i', $url) && !preg_match('/\.(jpg|jpeg|png|gif|svg)$/i', $url)) {
236 // Check if target already exists
237 if (!preg_match('/\btarget=/i', $full_tag)) {
238 // Preserve existing attributes, add target="_blank"
239 return '<a ' . $attributes . ' target="_blank" rel="noopener noreferrer">';
240 }
241 }
242
243 // Return original tag if no modification needed
244 return $full_tag;
245 };
246
247 // Perform the replacement
248 return preg_replace_callback($pattern, $callback, $html);
249 }
250
251 public static function findFirstUrl($html)
252 {
253 if (!preg_match_all('/<a\s+(?:[^>]*?\s+)?href=([\'"])(.*?)\1/i', $html, $matches)) {
254 return '';
255 }
256
257 $profileUrlPrefix = Helper::baseUrl('u/');
258
259 foreach ($matches[2] as $href) {
260 if (strpos($href, $profileUrlPrefix) === 0) {
261 continue;
262 }
263 return $href;
264 }
265
266 return '';
267 }
268
269 public static function extractHashTags($text, $limit = 5)
270 {
271 // Extract hashtag including - and _
272 preg_match_all('/#([a-zA-Z0-9_-]+)/', $text, $matches);
273
274 $tags = array_unique($matches[1]);
275
276 if (!$tags) {
277 return [];
278 }
279
280 $tags = array_slice($tags, 0, $limit);
281
282 $lowerCaseTags = array_map('strtolower', $tags);
283
284 $terms = Term::whereIn('slug', $lowerCaseTags)
285 ->where('taxonomy_name', 'hashtag')
286 ->get();
287
288 $termIds = [];
289
290 foreach ($terms as $term) {
291 $termIds[$term->slug] = $term->id;
292 }
293
294 if (count($termIds) == count($tags)) {
295 return array_values($termIds);
296 }
297
298 $excepts = array_diff($tags, array_keys($termIds));
299
300 foreach ($excepts as $except) {
301 $term = Term::create([
302 'taxonomy_name' => 'hashtag',
303 'slug' => strtolower($except),
304 'title' => $except
305 ]);
306
307 $termIds[$term->slug] = $term->id;
308 }
309
310 return array_values($termIds);
311 }
312
313 public static function getMentions($text, $spaceId = null, $withUsers = false)
314 {
315 // the mention may have . or _ or - in the username
316 preg_match_all('/@([a-zA-Z0-9_.-]+)/', $text, $matches);
317 $mentions = array_unique($matches[1]);
318
319 if (!$mentions) {
320 return null;
321 }
322
323 if ($spaceId) {
324 $xProfiles = XProfile::whereIn('username', $mentions)
325 ->whereHas('spaces', function ($query) use ($spaceId) {
326 $query->withoutGlobalScopes()->where('space_id', $spaceId);
327 })
328 ->get();
329 } else {
330 $xProfiles = XProfile::whereIn('username', $mentions)
331 ->get();
332 }
333
334 if ($xProfiles->isEmpty()) {
335 return null;
336 }
337
338 $userMentions = [];
339
340 $userIds = [];
341
342 foreach ($xProfiles as $xProfile) {
343 $userIds[] = $xProfile->user_id;
344 $html = '<a data-user_name="' . $xProfile->username . '" class="fcom_mention fcom_route" href="' . Helper::baseUrl('u/' . $xProfile->username . '/') . '">' . $xProfile->display_name . '</a>';
345 $userMentions['@' . $xProfile->username] = $html;
346 }
347
348 $data = [
349 'user_ids' => $userIds,
350 'text' => strtr($text, $userMentions)
351 ];
352
353 if ($withUsers) {
354 $data['users'] = User::whereIn('ID', $userIds)->get();
355 }
356
357 return $data;
358 }
359
360 public static function getLikedIdsByUserFeedId($feedId, $userId)
361 {
362 return Reaction::select('object_id')
363 ->where('object_type', 'comment')
364 ->where('parent_id', $feedId)
365 ->where('user_id', $userId)
366 ->get()
367 ->pluck('object_id')
368 ->toArray();
369 }
370
371 public static function castSurveyVote($newVoteIndexes, Feed $feed, $userId)
372 {
373 $surveyConfig = Arr::get($feed->meta, 'survey_config', []);
374
375 $slugs = array_map(function ($item) {
376 return $item['slug'];
377 }, $surveyConfig['options']);
378
379 $newVoteIndexes = array_filter(array_intersect($slugs, $newVoteIndexes));
380
381 $previousVotes = Reaction::where('type', 'survey_vote')
382 ->where('user_id', $userId)
383 ->where('object_id', $feed->id)
384 ->get();
385
386 $removedIndexes = [];
387 $alreadyIndexes = [];
388
389 foreach ($previousVotes as $previousVote) {
390 if (!in_array($previousVote->object_type, $newVoteIndexes)) {
391 // This vote need to be deleted
392 $removedIndexes[] = $previousVote->object_type;
393 $previousVote->delete();
394 } else {
395 $alreadyIndexes[] = $previousVote->object_type;
396 }
397 }
398
399 $newSyncIndexes = array_diff($newVoteIndexes, $alreadyIndexes);
400
401 foreach ($newSyncIndexes as $newSyncIndex) {
402 Reaction::create([
403 'user_id' => $userId,
404 'object_id' => $feed->id,
405 'type' => 'survey_vote',
406 'object_type' => $newSyncIndex
407 ]);
408 }
409
410 if (!empty($newSyncIndexes)) {
411 do_action('fluent_community/feed/cast_survey_vote', $newSyncIndexes, $feed, $userId);
412 }
413
414 foreach ($surveyConfig['options'] as $index => $option) {
415 $slug = $option['slug'];
416
417 if (in_array($slug, $removedIndexes)) {
418 $newCount = (int)Arr::get($option, 'vote_counts', 0) - 1;
419 $option['vote_counts'] = $newCount > 0 ? $newCount : 0;
420 } else if (in_array($slug, $newSyncIndexes)) {
421 $newCount = (int)Arr::get($option, 'vote_counts', 0) + 1;
422 $option['vote_counts'] = $newCount > 0 ? $newCount : 0;
423 }
424
425 $surveyConfig['options'][$index] = $option;
426 }
427
428 $surveyConfig = apply_filters('fluent_community/feed/updated_survey_config', $surveyConfig, $feed, $userId);
429
430 $meta = $feed->meta;
431 $meta['survey_config'] = $surveyConfig;
432 $feed->meta = $meta;
433 $feed->save();
434
435 Utility::forgetCache('survey_cast_' . $feed->id . '_' . $userId);
436
437 return $feed;
438 }
439
440 /**
441 * Create a new feed programmatically
442 * @param array $allData
443 * @return \FluentCommunity\App\Models\Feed|\WP_Error
444 **/
445 public static function createFeed($allData)
446 {
447 if (!is_array($allData)) {
448 return new \WP_Error('invalid_data', __('Invalid data. The data need to be array', 'fluent-community'), ['status' => 400]);
449 }
450
451 $acceptedKeys = ['message', 'title', 'user_id', 'space_id', 'type'];
452 $feedData = Arr::only($allData, $acceptedKeys);
453
454 // Let's validate the data
455 $validation = Validator::make($feedData, [
456 'message' => 'required',
457 'title' => 'nullable|string',
458 'user_id' => 'required|integer|exists:users,ID',
459 'space_id' => 'nullable|integer|exists:fcom_spaces,id'
460 ]);
461
462 if ($validation->fails()) {
463 return new \WP_Error('validation_failed', __('Validation failed', 'fluent-community'), $validation->errors());
464 }
465
466 $sanitizedData = self::sanitizeAndValidateData($feedData);
467
468 $feedData = wp_parse_args($sanitizedData, $feedData);
469
470 $user = User::find($feedData['user_id']);
471
472 if (!$user) {
473 return new \WP_Error('user_not_found', __('User not found', 'fluent-community'), ['status' => 400]);
474 }
475 $user->syncXProfile();
476 if ($user->xprofile->status != 'active') {
477 return new \WP_Error('user_inactive', __('User status is not active', 'fluent-community'), $validation->errors());
478 }
479
480 $markdown = $feedData['message'];
481 $mentions = null;
482
483 // Extra Validaton for space_id
484 if (!empty($feedData['space_id'])) {
485 if (!Helper::isUserInSpace($feedData['user_id'], $feedData['space_id'])) {
486 return new \WP_Error('invalid_space', __('User is not in the space', 'fluent-community'), ['status' => 400]);
487 }
488 $mentions = FeedsHelper::getMentions($markdown, Arr::get($feedData, 'space_id'), true);
489 if ($mentions) {
490 $markdown = $mentions['text'];
491 }
492 } else if (!Helper::hasGlobalPost()) {
493 return new \WP_Error('global_post_disabled', 'User is not allowed to post in global', ['status' => 400]);
494 }
495
496 $feedData['message_rendered'] = wp_kses_post(self::mdToHtml($markdown));
497 $feedData['status'] = 'published';
498
499 if (Arr::get($allData, 'meta.media_preview.provider') == 'inline') {
500 $allData['meta']['media_preview']['provider'] = 'giphy';
501 }
502
503 [$feedData, $mediaItems] = self::processFeedMetaData($feedData, $allData);
504
505 if ($mentions) {
506 $feedData['meta']['mentioned_user_ids'] = Arr::get($mentions, 'user_ids', []);
507 }
508
509 $data = apply_filters('fluent_community/feed/new_feed_data', $feedData, $allData);
510
511 if (is_wp_error($data)) {
512 return $data;
513 }
514
515 $feed = new Feed();
516 $feed->fill($data);
517 $feed->save();
518
519 if ($mentions) {
520 do_action('fluent_community/feed_mentioned', $feed, $mentions['users']);
521 }
522
523 if ($mediaItems) {
524 foreach ($mediaItems as $media) {
525 $media->feed_id = $feed->id;
526 $media->is_active = 1;
527 $media->object_source = 'feed';
528 $media->save();
529 }
530 }
531
532 do_action('fluent_community/feed/created', $feed);
533
534 if ($feed->space_id) {
535 do_action('fluent_community/space_feed/created', $feed);
536 }
537
538 return $feed;
539 }
540
541 public static function sanitizeAndValidateData($data)
542 {
543 $message = CustomSanitizer::unslashMarkdown(trim(Arr::get($data, 'message')));
544
545 // Decode HTML entities and strip all whitespace for validation
546 $messageForValidation = html_entity_decode($message, ENT_QUOTES | ENT_HTML5, 'UTF-8');
547 $messageForValidation = preg_replace('/\s+/u', '', $messageForValidation);
548
549 if (!$messageForValidation) {
550 throw new \Exception(esc_html__('Message is required', 'fluent-community'));
551 }
552
553 $processedData = [
554 'message' => $message,
555 'type' => 'text'
556 ];
557
558 $survey = Arr::get($data, 'survey', []);
559
560 if ($survey) {
561 $options = Arr::get($survey, 'options', []);
562 $formattedOptions = [];
563 foreach ($options as $index => $option) {
564 if (empty($option['label'])) {
565 continue;
566 }
567
568 $formattedOptions[] = [
569 'label' => sanitize_text_field($option['label']),
570 'slug' => Arr::get($option, 'slug') ?: 'opt_' . ($index + 1)
571 ];
572 }
573
574 $endDate = Arr::get($survey, 'end_date', '');
575 if ($endDate) {
576 $endDate = gmdate('Y-m-d H:i:s', strtotime($endDate));
577 } else {
578 $endDate = '';
579 }
580
581 if ($formattedOptions) {
582 $processedData['survey'] = [
583 'type' => Arr::get($survey, 'type') == 'single_choice' ? 'single_choice' : 'multi_choice',
584 'options' => $formattedOptions,
585 'end_date' => $endDate
586 ];
587 }
588 }
589
590 $maxlen = apply_filters('fluent_community/max_post_length', 15000);
591 if (\strlen($message) > $maxlen) {
592 /* translators: %s is the maximum allowed character count */
593 throw new \Exception(esc_html(sprintf(__('The post is too long. Please keep it under %s characters.', 'fluent-community'), number_format($maxlen))));
594 }
595
596 $titlePref = Utility::postTitlePref();
597
598 if ($titlePref) {
599 $processedData['title'] = sanitize_text_field(Arr::get($data, 'title'));
600 if ($titlePref == 'required' && empty($processedData['title'])) {
601 throw new \Exception(esc_html__('Title is required. Please provide a title', 'fluent-community'));
602 }
603 // trim the title if it's too long to 192 chars (multibyte-safe; column is VARCHAR(192) characters)
604 if (mb_strlen($processedData['title']) > 192) {
605 $processedData['title'] = mb_substr($processedData['title'], 0, 192, 'UTF-8');
606 }
607 }
608
609 return $processedData;
610 }
611
612 public static function getSurveyOptionsUpdateError($existingSurveyOptions, $submittedSurvey)
613 {
614 if (empty($existingSurveyOptions) || empty($submittedSurvey)) {
615 return null;
616 }
617
618 $submittedLabelsBySlug = [];
619 foreach (Arr::get($submittedSurvey, 'options', []) as $option) {
620 $slug = Arr::get($option, 'slug', '');
621 if ($slug !== '') {
622 $submittedLabelsBySlug[$slug] = trim((string)Arr::get($option, 'label', ''));
623 }
624 }
625
626 foreach ($existingSurveyOptions as $existingOption) {
627 $slug = Arr::get($existingOption, 'slug', '');
628 if ($slug === '') {
629 continue;
630 }
631
632 if (!isset($submittedLabelsBySlug[$slug]) || $submittedLabelsBySlug[$slug] === '') {
633 return __('Existing poll options cannot be removed or left empty.', 'fluent-community');
634 }
635 }
636
637 return null;
638 }
639
640 public static function transformForEdit($feed)
641 {
642 $topicsConfig = Helper::getTopicsConfig();
643
644 $terms = $feed->terms;
645 $feed->topic_ids = $terms->where('taxonomy_name', 'post_topic')->pluck('id')->toArray();
646 if ($topicsConfig['max_topics_per_post'] == 1) {
647 if ($feed->topic_ids) {
648 $feed->topic_ids = Arr::first($feed->topic_ids);
649 } else {
650 $feed->topic_ids = '';
651 }
652 }
653
654 if (Arr::get($feed->meta, 'send_announcement_email') == 'yes') {
655 $feed->send_announcement_email = 'yes';
656 }
657
658 if ($feed->content_type == 'document') {
659 $documents = Media::where('object_source', 'space_document')
660 ->where('feed_id', $feed->id)
661 ->where('is_active', 1)
662 ->get();
663 $mediaIds = [];
664 foreach ($documents as $document) {
665 /** @var Media $document */
666 $mediaIds[] = $document->getPrivateFileMeta();
667 }
668 $feed->document_ids = $mediaIds;
669 $feed->load('space');
670 return $feed;
671 }
672
673 $surveyConfig = Arr::get($feed->meta, 'survey_config', []);
674
675 if ($surveyConfig) {
676 $feed->survey = [
677 'type' => Arr::get($surveyConfig, 'type'),
678 'options' => Arr::get($surveyConfig, 'options', []),
679 'end_date' => Arr::get($surveyConfig, 'end_date', '')
680 ];
681 }
682
683 $mediaImages = Arr::get($feed->meta, 'media_items', []);
684 $meta = $feed->meta;
685 unset($feed->meta);
686
687 if ($mediaImages) {
688 $feed->media_images = $mediaImages;
689 } else if ($mediaPreview = Arr::get($meta, 'media_preview')) {
690 $type = Arr::get($mediaPreview, 'type');
691 if ($type == 'oembed' || $type == 'iframe_html') {
692 $feed->media = $mediaPreview;
693 }
694
695 // Only fetch the specific attached media, not all media (which would include inline images)
696 $mediaId = Arr::get($mediaPreview, 'media_id');
697 if ($mediaId) {
698 $media = Media::where('id', $mediaId)
699 ->where('feed_id', $feed->id)
700 ->where('is_active', 1)
701 ->first();
702
703 if ($media) {
704 $feed->media_images = [[
705 'url' => $media->public_url,
706 'type' => 'image',
707 'media_id' => $media->id,
708 'width' => Arr::get($media->settings, 'width'),
709 'height' => Arr::get($media->settings, 'height'),
710 'provider' => Arr::get($media->settings, 'provider', 'uploader')
711 ]];
712 }
713 } else if ($type != 'meta_data') {
714 $feed->meta = $meta;
715 }
716 }
717
718 // Preserve multi-audio so the edit composer can load, edit/remove, and re-save them
719 // (transformForEdit otherwise drops meta for audio-only posts).
720 $audioMedias = Arr::get($meta, 'audio_medias', []);
721 if ($audioMedias) {
722 $editMeta = (isset($feed->meta) && is_array($feed->meta)) ? $feed->meta : [];
723 $editMeta['audio_medias'] = $audioMedias;
724 $feed->meta = $editMeta;
725 }
726
727 $feed->load('space');
728 return $feed;
729 }
730
731 public static function processFeedMetaData($data, $requestData, $existingFeed = null)
732 {
733 if (empty($data['meta'])) {
734 $data['meta'] = [];
735 }
736
737 $uplaodedDocs = [];
738 // Handle Survey
739 if (!empty($data['survey'])) {
740 $surveyConfig = $data['survey'];
741 if ($existingFeed) {
742 $surveyConfig = Arr::get($existingFeed->meta, 'survey_config', []);
743 if ($surveyConfig) {
744 $oldOptions = Arr::get($surveyConfig, 'options', []);
745 $newOptions = Arr::get($data['survey'], 'options', []);
746 $oldKeyedOptions = [];
747 foreach ($oldOptions as $option) {
748 $oldKeyedOptions[$option['slug']] = $option;
749 }
750 foreach ($newOptions as $index => $option) {
751 $slug = Arr::get($option, 'slug', '');
752 if (isset($oldKeyedOptions[$slug])) {
753 $newOptions[$index]['vote_counts'] = Arr::get($oldKeyedOptions[$slug], 'vote_counts', 0);
754 }
755 }
756 $surveyConfig['options'] = $newOptions;
757 } else {
758 $surveyConfig = $data['survey'];
759 }
760 }
761
762 if ($endDate = Arr::get($data['survey'], 'end_date', '')) {
763 $surveyConfig['end_date'] = gmdate('Y-m-d H:i:s', strtotime($endDate));
764 } else {
765 $surveyConfig['end_date'] = '';
766 }
767
768 $data['meta']['survey_config'] = $surveyConfig;
769 $data['content_type'] = 'survey';
770 unset($data['survey']);
771 }
772
773 // Handle Giphy
774 if (Arr::get($requestData, 'meta.media_preview.provider') == 'giphy') {
775 $url = Arr::get($requestData, 'meta.media_preview.image');
776 if (!$url || !filter_var($url, FILTER_VALIDATE_URL)) {
777 return [$data, $uplaodedDocs];
778 }
779
780 $data['meta']['media_preview'] = array_filter([
781 'image' => sanitize_url($url),
782 'type' => sanitize_text_field(Arr::get($requestData, 'meta.media_preview.type', 'image')),
783 'provider' => 'giphy',
784 'height' => (int)Arr::get($requestData, 'meta.media_preview.height', 0),
785 'width' => (int)Arr::get($requestData, 'meta.media_preview.width', 0),
786 ]);
787
788 return [$data, $uplaodedDocs];
789 }
790
791 // Handling Video Embed
792 if (
793 Arr::get($requestData, 'media') &&
794 (
795 (Arr::get($requestData, 'media.type') == 'oembed' && Arr::get($requestData, 'media.player') != 'fluent_player') ||
796 Arr::get($requestData, 'media.type') == 'iframe_html'
797 )
798 ) {
799 if (Arr::get($requestData, 'media.type') == 'iframe_html') {
800 $mediaPreview = array_filter(Arr::get($requestData, 'media', []));
801
802 if (empty($mediaPreview['image']) && !empty($mediaPreview['html'])) {
803 $thumb = RemoteUrlParser::extractIframeThumbnail($mediaPreview['html']);
804 if ($thumb) {
805 $mediaPreview['image'] = $thumb;
806 }
807 }
808
809 $data['meta']['media_preview'] = $mediaPreview;
810 return [$data, $uplaodedDocs];
811 }
812
813 $media = Arr::get($requestData, 'media');
814 $url = Arr::get($media, 'url');
815 $metaData = RemoteUrlParser::parse($url);
816 if ($metaData && !is_wp_error($metaData) && (!empty($metaData['image']) || !empty($metaData['html']))) {
817 $data['meta']['media_preview'] = $metaData;
818 }
819
820 return [$data, $uplaodedDocs];
821 }
822
823 // Let's handle the uploaded media
824 $mediaImages = Arr::get($requestData, 'media_images', []);
825 if ($mediaImages) {
826 $uploadedImages = Helper::getMediaByProvider($mediaImages);
827 if (!$existingFeed) {
828 $uploadedMediaItems = Helper::getMediaItemsFromUrl($uploadedImages);
829 } else {
830 $uploadedMediaItems = [];
831 foreach ($mediaImages as $mediaImage) {
832 $url = sanitize_url(Arr::get($mediaImage, 'url', ''));
833 if (!$url) {
834 continue;
835 }
836 $mediaItem = Helper::getMediaFromUrl($mediaImage);
837 if ($mediaItem) {
838 $uploadedMediaItems[] = $mediaItem;
839 } else {
840 // maybe this is a previously uploaded image
841 $media = Media::where('media_url', $url)
842 ->where('object_source', 'feed')
843 ->where('feed_id', $existingFeed->id)
844 ->where('is_active', 1)
845 ->first();
846
847 if ($media) {
848 $uploadedMediaItems[] = $media;
849 }
850 }
851 }
852 }
853
854 if (count($uploadedMediaItems) == 1) {
855 $singleMedia = $uploadedMediaItems[0];
856 $data['meta']['media_preview'] = [
857 'is_uploaded' => true,
858 'image' => $singleMedia->public_url,
859 'type' => 'meta_data',
860 'provider' => 'uploader',
861 'width' => Arr::get($singleMedia->settings, 'width'),
862 'height' => Arr::get($singleMedia->settings, 'height'),
863 'media_id' => $singleMedia->id,
864 ];
865 } else if ($uploadedMediaItems) {
866 $mediaPreviews = [];
867 foreach ($uploadedMediaItems as $mediaItem) {
868 $mediaData = [
869 'media_id' => $mediaItem->id,
870 'url' => $mediaItem->public_url,
871 'type' => 'image',
872 'width' => Arr::get($mediaItem->settings, 'width'),
873 'height' => Arr::get($mediaItem->settings, 'height'),
874 'provider' => Arr::get($mediaItem->settings, 'provider', 'uploader')
875 ];
876 $mediaPreviews[] = array_filter($mediaData);
877 }
878 $data['meta']['media_items'] = $mediaPreviews;
879 }
880
881 $maxMediaPerPost = apply_filters('fluent_community/max_media_per_post', Utility::getCustomizationSetting('max_media_per_post'));
882
883 $allMediaItems = array_slice($uploadedMediaItems, 0, (int)$maxMediaPerPost);
884
885 return [$data, $allMediaItems];
886 }
887
888 if ($existingFeed && Arr::get($existingFeed->meta, 'auto_flagged') == 'yes') {
889 $data['meta']['auto_flagged'] = 'yes';
890 $data['meta']['prevent_published'] = 'yes';
891 $data['meta']['reports_count'] = Arr::get($existingFeed->meta, 'reports_count', 0);
892 }
893
894 // Let's handle the fallback here
895 $firstUrl = FeedsHelper::findFirstUrl(Arr::get($data, 'message_rendered'));
896
897 // check if this is another post or not
898 if (strpos($firstUrl, Helper::baseUrl()) === 0) {
899 // this is an internal URL
900 if (Helper::getRouteNameByRequestPath($firstUrl) === 'feed_view') {
901 $uriParts = explode('/', $firstUrl);
902 if (count($uriParts) >= 2) {
903 $postSlug = end($uriParts);
904 $feed = Feed::where('slug', $postSlug)->first();
905 if ($feed) {
906 $firstUrl = null;
907 $data['meta']['custom_app_preview'] = [
908 'app_name' => 'child_post',
909 'feed_id' => $feed->id
910 ];
911 }
912 }
913 }
914 }
915
916 if ($firstUrl) {
917 $metaData = RemoteUrlParser::parse($firstUrl);
918 if ($metaData && !is_wp_error($metaData) && (!empty($metaData['image']) || !empty($metaData['html']))) {
919 $data['meta']['media_preview'] = $metaData;
920 }
921 }
922
923 $uplaodedDocs = apply_filters('fluent_community/feed/uploaded_feed_medias', $uplaodedDocs, $requestData);
924 return [$data, $uplaodedDocs];
925 }
926
927 protected static function tranformFeedData(Feed $feed, $config = [])
928 {
929 $userId = Arr::get($config, 'user_id', 0);
930 $commentLikeIds = $userId ? Arr::get($config, 'comment_like_ids', []) : [];
931
932 $feed->comments->each(function ($comment) use ($commentLikeIds) {
933 self::setCurrentRelatedUserId($comment->user_id);
934 if ($commentLikeIds && in_array($comment->id, $commentLikeIds)) {
935 $comment->liked = 1;
936 }
937 });
938
939 // User-specific processing
940 if ($userId) {
941 $interactions = Arr::get($config, 'interactions', []);
942
943 if ($interactions) {
944 $feed->has_user_react = Arr::get($interactions, 'like', false);
945 $feed->bookmarked = Arr::get($interactions, 'bookmark', false);
946 }
947
948 if ($feed->content_type == 'survey') {
949 $votedOptions = $feed->getSurveyCastsByUserId($userId);
950 if ($votedOptions) {
951 $surveyConfig = Arr::get($feed->meta, 'survey_config', []);
952 foreach ($surveyConfig['options'] as $index => $option) {
953 if (in_array($option['slug'], $votedOptions)) {
954 $surveyConfig['options'][$index]['voted'] = true;
955 }
956 }
957 $meta = $feed->meta;
958 $meta['survey_config'] = $surveyConfig;
959 $feed->meta = $meta;
960 }
961 }
962 }
963
964 if ($feed->content_type == 'document') {
965 $feedMeta = $feed->meta;
966 $documentLists = Arr::get($feedMeta, 'document_lists', []);
967 foreach ($documentLists as $index => $document) {
968 $documentLists[$index]['url'] = Helper::baseUrl('?fcom_action=download_document&media_key=' . $document['media_key'] . '&media_id=' . $document['id']);
969 }
970 $feedMeta['document_lists'] = $documentLists;
971 $feed->meta = $feedMeta;
972 }
973
974 $spaceSettings = $feed->space ? $feed->space->settings : [];
975 $feed->default_comment_sort_by = Arr::get($spaceSettings, 'default_comment_sort_by', '');
976
977 self::setCurrentRelatedUserId($feed->user_id);
978
979 return apply_filters('fluent_community/rendering_feed_model', $feed, $config);
980 }
981
982 public static function transformFeed(Feed $feed)
983 {
984 $userId = get_current_user_id();
985
986 $config = apply_filters('fluent_community/feed_general_config', [
987 'user_id' => $userId,
988 'interactions' => [],
989 'comment_like_ids' => [],
990 'is_collection' => false
991 ], $feed, $userId);
992
993 if ($userId) {
994 $config['interactions'] = [
995 'like' => $feed->hasUserReact($userId, 'like'),
996 'bookmark' => $feed->hasUserReact($userId, 'bookmark'),
997 ];
998 $config['comment_like_ids'] = self::getLikedIdsByUserFeedId($feed->id, $userId);
999 }
1000
1001 return self::tranformFeedData($feed, $config);
1002 }
1003
1004 public static function transformFeedsCollection($feeds)
1005 {
1006 if ($feeds->isEmpty()) {
1007 return $feeds;
1008 }
1009
1010 $userId = get_current_user_id();
1011 $commentLikeIds = [];
1012 $formattedInteractions = [];
1013 $feedIds = $feeds->pluck('id')->toArray();
1014
1015 if ($userId) {
1016 $interactions = Reaction::query()
1017 ->select(['user_id', 'type', 'object_id'])
1018 ->whereIn('object_id', $feedIds)
1019 ->where('object_type', 'feed')
1020 ->where('user_id', $userId)
1021 ->whereIn('type', ['like', 'bookmark'])
1022 ->get();
1023
1024 $formattedInteractions = [];
1025 foreach ($interactions as $interaction) {
1026 $objectId = (int)$interaction->object_id;
1027
1028 if (!isset($formattedInteractions[$objectId])) {
1029 $formattedInteractions[$objectId] = [];
1030 }
1031 $formattedInteractions[$objectId][$interaction->type] = true;
1032 }
1033
1034 $commentLikeIds = Reaction::select('object_id')
1035 ->where('object_type', 'comment')
1036 ->whereIn('parent_id', $feedIds)
1037 ->where('user_id', $userId)
1038 ->get()
1039 ->pluck('object_id')
1040 ->toArray();
1041 }
1042
1043 $generalConfig = apply_filters('fluent_community/feed_general_config', [
1044 'user_id' => $userId,
1045 'interactions' => [],
1046 'comment_like_ids' => $commentLikeIds,
1047 'is_collection' => true
1048 ], $feeds, $feedIds);
1049
1050 $feeds->each(function ($feed) use ($userId, $generalConfig, $formattedInteractions) {
1051 $config = $generalConfig;
1052 if ($userId) {
1053 $config['interactions'] = Arr::get($formattedInteractions, $feed->id, []);
1054 }
1055 return self::tranformFeedData($feed, $config);
1056 });
1057
1058 return $feeds;
1059 }
1060
1061 public static function getMediaHtml($meta, $postPermalink)
1062 {
1063 $mediaImage = Arr::get($meta, 'media_preview.image');
1064 $mediaCount = 0;
1065 if (!$mediaImage) {
1066 $mediaItems = Arr::get($meta, 'media_items', []);
1067 if ($mediaItems) {
1068 $mediaImage = Arr::get($mediaItems[0], 'url');
1069 $mediaCount = count($mediaItems);
1070 }
1071 }
1072
1073 $feedHtml = '';
1074
1075 if ($mediaImage) {
1076 $feedHtml .= '<div class="fcom_media" style="margin-top: 20px;">';
1077 $feedHtml .= '<a href="' . $postPermalink . '"><img src="' . $mediaImage . '" style="max-width: 100%; height: auto; display: block; margin: 0 auto 0px;" /></a>';
1078 if ($mediaCount > 1) {
1079 /* translators: %d is the number of additional images not shown in the preview. */
1080 $feedHtml .= '<p style="text-align: center; font-size: 14px; color: #666; margin-top: 10px;">' . sprintf(_n('+%d more image', '+%d more images', $mediaCount - 1, 'fluent-community'), $mediaCount - 1) . '</p>';
1081 }
1082 $feedHtml .= '</div>';
1083 }
1084
1085 return $feedHtml;
1086 }
1087
1088 public static function hasEveryoneTag($message)
1089 {
1090 // Updated regular expression to match @everyone with more flexibility
1091 $pattern = '/(?<=^|\W)@everyone(?=\W|\z)/iu';
1092
1093 return preg_match($pattern, $message) === 1;
1094 }
1095
1096 public static function replaceImageUrlsWithRealMediaArchive($markdown, $existingFeed = null)
1097 {
1098 $imageUrls = self::getInlineImageUrls($markdown);
1099
1100 if (!$imageUrls) {
1101 return [$markdown, []];
1102 }
1103
1104 $mediaItems = [];
1105
1106 foreach ($imageUrls as $url) {
1107 $url = sanitize_url($url);
1108 $media = Helper::getMediaFromUrl($url);
1109 if ($media) {
1110 if ($media->is_active && (!$existingFeed || $media->feed_id != $existingFeed->id) ) {
1111 continue;
1112 }
1113
1114 $realUrl = $media->public_url;
1115 $markdown = str_replace($url, $realUrl, $markdown);
1116 $mediaItems[] = $media;
1117 }
1118 }
1119
1120 return [$markdown, $mediaItems];
1121 }
1122
1123 private static function getInlineImageUrls($markdown)
1124 {
1125 $urls = [];
1126 // Match ![alt](url) and ![alt](url "title")
1127 if (preg_match_all('/!\[[^\]]*\]\(([^\s\)]+)(?:\s+"[^"]*")?\)/', $markdown, $matches)) {
1128 $urls = array_merge($urls, $matches[1]);
1129 }
1130
1131 // Match reference-style image usage: ![alt][id] and map only those IDs to their definitions [id]: url
1132 if (preg_match_all('/!\[[^\]]*\]\[([^\]]+)\]/', $markdown, $imageRefMatches)) {
1133 $usedIds = array_unique($imageRefMatches[1]);
1134
1135 if (preg_match_all('/^\[([^\]]+)\]:\s+(\S+)/m', $markdown, $refMatches)) {
1136 $refMap = [];
1137 foreach ($refMatches[1] as $index => $id) {
1138 $refMap[$id] = $refMatches[2][$index];
1139 }
1140
1141 foreach ($usedIds as $id) {
1142 if (isset($refMap[$id])) {
1143 $urls[] = $refMap[$id];
1144 }
1145 }
1146 }
1147 }
1148
1149 // Match HTML <img> tags
1150 if (preg_match_all('/<img[^>]+src=["\']([^"\']+)["\'][^>]*>/i', $markdown, $matches)) {
1151 $urls = array_merge($urls, $matches[1]);
1152 }
1153
1154 return array_unique($urls);
1155 }
1156 }
1157