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

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

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