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

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

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