PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.4.01
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.4.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.4.01, at app/Services/FeedsHelper.php

1,067 lines 37.4 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 throw new \Exception(esc_html__('Post message is too long', 'fluent-community'));
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 transformForEdit($feed)
570 {
571 $topicsConfig = Helper::getTopicsConfig();
572
573 $terms = $feed->terms;
574 $feed->topic_ids = $terms->where('taxonomy_name', 'post_topic')->pluck('id')->toArray();
575 if ($topicsConfig['max_topics_per_post'] == 1) {
576 if ($feed->topic_ids) {
577 $feed->topic_ids = Arr::first($feed->topic_ids);
578 } else {
579 $feed->topic_ids = '';
580 }
581 }
582
583 if (Arr::get($feed->meta, 'send_announcement_email') == 'yes') {
584 $feed->send_announcement_email = 'yes';
585 }
586
587 if ($feed->content_type == 'document') {
588 $documents = Media::where('object_source', 'space_document')
589 ->where('feed_id', $feed->id)
590 ->where('is_active', 1)
591 ->get();
592 $mediaIds = [];
593 foreach ($documents as $document) {
594 $mediaIds[] = $document->getPrivateFileMeta();
595 }
596 $feed->document_ids = $mediaIds;
597 $feed->load('space');
598 return $feed;
599 }
600
601 $surveyConfig = Arr::get($feed->meta, 'survey_config', []);
602
603 if ($surveyConfig) {
604 $feed->survey = [
605 'type' => Arr::get($surveyConfig, 'type'),
606 'options' => Arr::get($surveyConfig, 'options', []),
607 'end_date' => Arr::get($surveyConfig, 'end_date', '')
608 ];
609 }
610
611 $mediaImages = Arr::get($feed->meta, 'media_items', []);
612 $meta = $feed->meta;
613 unset($feed->meta);
614
615 if ($mediaImages) {
616 $feed->media_images = $mediaImages;
617 } else if ($mediaPreview = Arr::get($meta, 'media_preview')) {
618 $type = Arr::get($mediaPreview, 'type');
619 if ($type == 'oembed' || $type == 'iframe_html') {
620 $feed->media = $mediaPreview;
621 }
622
623 // Only fetch the specific attached media, not all media (which would include inline images)
624 $mediaId = Arr::get($mediaPreview, 'media_id');
625 if ($mediaId) {
626 $media = Media::where('id', $mediaId)
627 ->where('feed_id', $feed->id)
628 ->where('is_active', 1)
629 ->first();
630
631 if ($media) {
632 $feed->media_images = [[
633 'url' => $media->public_url,
634 'type' => 'image',
635 'media_id' => $media->id,
636 'width' => Arr::get($media->settings, 'width'),
637 'height' => Arr::get($media->settings, 'height'),
638 'provider' => Arr::get($media->settings, 'provider', 'uploader')
639 ]];
640 }
641 } else if ($type != 'meta_data') {
642 $feed->meta = $meta;
643 }
644 }
645
646 $feed->load('space');
647 return $feed;
648 }
649
650 public static function processFeedMetaData($data, $requestData, $existingFeed = null)
651 {
652 if (empty($data['meta'])) {
653 $data['meta'] = [];
654 }
655
656 $uplaodedDocs = [];
657 // Handle Survey
658 if (!empty($data['survey'])) {
659 $surveyConfig = $data['survey'];
660 if ($existingFeed) {
661 $surveyConfig = Arr::get($existingFeed->meta, 'survey_config', []);
662 if ($surveyConfig) {
663 $oldOptions = Arr::get($surveyConfig, 'options', []);
664 $newOptions = Arr::get($data['survey'], 'options', []);
665 $oldKeyedOptions = [];
666 foreach ($oldOptions as $option) {
667 $oldKeyedOptions[$option['slug']] = $option;
668 }
669 foreach ($newOptions as $index => $option) {
670 $slug = Arr::get($option, 'slug', '');
671 if (isset($oldKeyedOptions[$slug])) {
672 $newOptions[$index]['vote_counts'] = Arr::get($oldKeyedOptions[$slug], 'vote_counts', 0);
673 }
674 }
675 $surveyConfig['options'] = $newOptions;
676 } else {
677 $surveyConfig = $data['survey'];
678 }
679 }
680
681 if ($endDate = Arr::get($data['survey'], 'end_date', '')) {
682 $surveyConfig['end_date'] = gmdate('Y-m-d H:i:s', strtotime($endDate));
683 } else {
684 $surveyConfig['end_date'] = '';
685 }
686
687 $data['meta']['survey_config'] = $surveyConfig;
688 $data['content_type'] = 'survey';
689 unset($data['survey']);
690 }
691
692 // Handle Giphy
693 if (Arr::get($requestData, 'meta.media_preview.provider') == 'giphy') {
694 $url = Arr::get($requestData, 'meta.media_preview.image');
695 if (!$url || !filter_var($url, FILTER_VALIDATE_URL)) {
696 return [$data, $uplaodedDocs];
697 }
698
699 $data['meta']['media_preview'] = array_filter([
700 'image' => sanitize_url($url),
701 'type' => sanitize_text_field(Arr::get($requestData, 'meta.media_preview.type', 'image')),
702 'provider' => 'giphy',
703 'height' => (int)Arr::get($requestData, 'meta.media_preview.height', 0),
704 'width' => (int)Arr::get($requestData, 'meta.media_preview.width', 0),
705 ]);
706
707 return [$data, $uplaodedDocs];
708 }
709
710 // Handling Video Embed
711 if (
712 Arr::get($requestData, 'media') &&
713 (
714 (Arr::get($requestData, 'media.type') == 'oembed' && Arr::get($requestData, 'media.player') != 'fluent_player') ||
715 Arr::get($requestData, 'media.type') == 'iframe_html'
716 )
717 ) {
718 if (Arr::get($requestData, 'media.type') == 'iframe_html') {
719 $data['meta']['media_preview'] = array_filter(Arr::get($requestData, 'media', []));
720 return [$data, $uplaodedDocs];
721 }
722
723 $media = Arr::get($requestData, 'media');
724 $url = Arr::get($media, 'url');
725 $metaData = RemoteUrlParser::parse($url);
726 if ($metaData && !is_wp_error($metaData) && (!empty($metaData['image']) || !empty($metaData['html']))) {
727 $data['meta']['media_preview'] = $metaData;
728 }
729
730 return [$data, $uplaodedDocs];
731 }
732
733 // Let's handle the uploaded media
734 $mediaImages = Arr::get($requestData, 'media_images', []);
735 if ($mediaImages) {
736 $uploadedImages = Helper::getMediaByProvider($mediaImages);
737 if (!$existingFeed) {
738 $uploadedMediaItems = Helper::getMediaItemsFromUrl($uploadedImages);
739 } else {
740 $uploadedMediaItems = [];
741 foreach ($mediaImages as $mediaImage) {
742 $url = sanitize_url(Arr::get($mediaImage, 'url', ''));
743 if (!$url) {
744 continue;
745 }
746 $mediaItem = Helper::getMediaFromUrl($mediaImage);
747 if ($mediaItem) {
748 $uploadedMediaItems[] = $mediaItem;
749 } else {
750 // maybe this is a previously uploaded image
751 $media = Media::where('media_url', $url)
752 ->where('object_source', 'feed')
753 ->where('feed_id', $existingFeed->id)
754 ->where('is_active', 1)
755 ->first();
756
757 if ($media) {
758 $uploadedMediaItems[] = $media;
759 }
760 }
761 }
762 }
763
764 if (count($uploadedMediaItems) == 1) {
765 $singleMedia = $uploadedMediaItems[0];
766 $data['meta']['media_preview'] = [
767 'is_uploaded' => true,
768 'image' => $singleMedia->public_url,
769 'type' => 'meta_data',
770 'provider' => 'uploader',
771 'width' => Arr::get($singleMedia->settings, 'width'),
772 'height' => Arr::get($singleMedia->settings, 'height'),
773 'media_id' => $singleMedia->id,
774 ];
775 } else if ($uploadedMediaItems) {
776 $mediaPreviews = [];
777 foreach ($uploadedMediaItems as $mediaItem) {
778 $mediaData = [
779 'media_id' => $mediaItem->id,
780 'url' => $mediaItem->public_url,
781 'type' => 'image',
782 'width' => Arr::get($mediaItem->settings, 'width'),
783 'height' => Arr::get($mediaItem->settings, 'height'),
784 'provider' => Arr::get($mediaItem->settings, 'provider', 'uploader')
785 ];
786 $mediaPreviews[] = array_filter($mediaData);
787 }
788 $data['meta']['media_items'] = $mediaPreviews;
789 }
790
791 $maxMediaPerPost = apply_filters('fluent_community/max_media_per_post', Utility::getCustomizationSetting('max_media_per_post'));
792
793 $allMediaItems = array_slice($uploadedMediaItems, 0, (int)$maxMediaPerPost);
794
795 return [$data, $allMediaItems];
796 }
797
798 if ($existingFeed && Arr::get($existingFeed->meta, 'auto_flagged') == 'yes') {
799 $data['meta']['auto_flagged'] = 'yes';
800 $data['meta']['prevent_published'] = 'yes';
801 $data['meta']['reports_count'] = Arr::get($existingFeed->meta, 'reports_count', 0);
802 }
803
804 // Let's handle the fallback here
805 $firstUrl = FeedsHelper::findFirstUrl(Arr::get($data, 'message_rendered'));
806
807 // check if this is another post or not
808 if (strpos($firstUrl, Helper::baseUrl()) === 0) {
809 // this is an internal URL
810 if (Helper::getRouteNameByRequestPath($firstUrl) === 'feed_view') {
811 $uriParts = explode('/', $firstUrl);
812 if (count($uriParts) >= 2) {
813 $postSlug = end($uriParts);
814 $feed = Feed::where('slug', $postSlug)->first();
815 if ($feed) {
816 $firstUrl = null;
817 $data['meta']['custom_app_preview'] = [
818 'app_name' => 'child_post',
819 'feed_id' => $feed->id
820 ];
821 }
822 }
823 }
824 }
825
826 if ($firstUrl) {
827 $metaData = RemoteUrlParser::parse($firstUrl);
828 if ($metaData && !is_wp_error($metaData) && (!empty($metaData['image']) || !empty($metaData['html']))) {
829 $data['meta']['media_preview'] = $metaData;
830 }
831 }
832
833 $uplaodedDocs = apply_filters('fluent_community/feed/uploaded_feed_medias', $uplaodedDocs, $requestData);
834 return [$data, $uplaodedDocs];
835 }
836
837 protected static function tranformFeedData(Feed $feed, $config = [])
838 {
839 $userId = Arr::get($config, 'user_id', 0);
840 $commentLikeIds = $userId ? Arr::get($config, 'comment_like_ids', []) : [];
841
842 $feed->comments->each(function ($comment) use ($commentLikeIds) {
843 self::setCurrentRelatedUserId($comment->user_id);
844 if ($commentLikeIds && in_array($comment->id, $commentLikeIds)) {
845 $comment->liked = 1;
846 }
847 });
848
849 // User-specific processing
850 if ($userId) {
851 $interactions = Arr::get($config, 'interactions', []);
852
853 if ($interactions) {
854 $feed->has_user_react = Arr::get($interactions, 'like', false);
855 $feed->bookmarked = Arr::get($interactions, 'bookmark', false);
856 }
857
858 if ($feed->content_type == 'survey') {
859 $votedOptions = $feed->getSurveyCastsByUserId($userId);
860 if ($votedOptions) {
861 $surveyConfig = Arr::get($feed->meta, 'survey_config', []);
862 foreach ($surveyConfig['options'] as $index => $option) {
863 if (in_array($option['slug'], $votedOptions)) {
864 $surveyConfig['options'][$index]['voted'] = true;
865 }
866 }
867 $meta = $feed->meta;
868 $meta['survey_config'] = $surveyConfig;
869 $feed->meta = $meta;
870 }
871 }
872 }
873
874 if ($feed->content_type == 'document') {
875 $feedMeta = $feed->meta;
876 $documentLists = Arr::get($feedMeta, 'document_lists', []);
877 foreach ($documentLists as $index => $document) {
878 $documentLists[$index]['url'] = Helper::baseUrl('?fcom_action=download_document&media_key=' . $document['media_key'] . '&media_id=' . $document['id']);
879 }
880 $feedMeta['document_lists'] = $documentLists;
881 $feed->meta = $feedMeta;
882 }
883
884 $spaceSettings = Space::where('id', $feed->space_id)->value('settings');
885 $feed->default_comment_sort_by = Arr::get($spaceSettings, 'default_comment_sort_by', '');
886
887 self::setCurrentRelatedUserId($feed->user_id);
888
889 return apply_filters('fluent_community/rendering_feed_model', $feed, $config);
890 }
891
892 public static function transformFeed(Feed $feed)
893 {
894 $userId = get_current_user_id();
895
896 $config = apply_filters('fluent_community/feed_general_config', [
897 'user_id' => $userId,
898 'interactions' => [],
899 'comment_like_ids' => [],
900 'is_collection' => false
901 ], $feed, $userId);
902
903 if ($userId) {
904 $config['interactions'] = [
905 'like' => $feed->hasUserReact($userId, 'like'),
906 'bookmark' => $feed->hasUserReact($userId, 'bookmark'),
907 ];
908 $config['comment_like_ids'] = self::getLikedIdsByUserFeedId($feed->id, $userId);
909 }
910
911 return self::tranformFeedData($feed, $config);
912 }
913
914 public static function transformFeedsCollection($feeds)
915 {
916 if ($feeds->isEmpty()) {
917 return $feeds;
918 }
919
920 $userId = get_current_user_id();
921 $commentLikeIds = [];
922 $formattedInteractions = [];
923 $feedIds = $feeds->pluck('id')->toArray();
924
925 if ($userId) {
926 $interactions = Reaction::query()
927 ->select(['user_id', 'type', 'object_id'])
928 ->whereIn('object_id', $feedIds)
929 ->where('object_type', 'feed')
930 ->where('user_id', $userId)
931 ->whereIn('type', ['like', 'bookmark'])
932 ->get();
933
934 $formattedInteractions = [];
935 foreach ($interactions as $interaction) {
936 $objectId = (int)$interaction->object_id;
937
938 if (!isset($formattedInteractions[$objectId])) {
939 $formattedInteractions[$objectId] = [];
940 }
941 $formattedInteractions[$objectId][$interaction->type] = true;
942 }
943
944 $commentLikeIds = Reaction::select('object_id')
945 ->where('object_type', 'comment')
946 ->whereIn('parent_id', $feedIds)
947 ->where('user_id', $userId)
948 ->get()
949 ->pluck('object_id')
950 ->toArray();
951 }
952
953 $generalConfig = apply_filters('fluent_community/feed_general_config', [
954 'user_id' => $userId,
955 'interactions' => [],
956 'comment_like_ids' => $commentLikeIds,
957 'is_collection' => true
958 ], $feeds, $feedIds);
959
960 $feeds->each(function ($feed) use ($userId, $generalConfig, $formattedInteractions) {
961 $config = $generalConfig;
962 if ($userId) {
963 $config['interactions'] = Arr::get($formattedInteractions, $feed->id, []);
964 }
965 return self::tranformFeedData($feed, $config);
966 });
967
968 return $feeds;
969 }
970
971 public static function getMediaHtml($meta, $postPermalink)
972 {
973 $mediaImage = Arr::get($meta, 'media_preview.image');
974 $mediaCount = 0;
975 if (!$mediaImage) {
976 $mediaItems = Arr::get($meta, 'media_items', []);
977 if ($mediaItems) {
978 $mediaImage = Arr::get($mediaItems[0], 'url');
979 $mediaCount = count($mediaItems);
980 }
981 }
982
983 $feedHtml = '';
984
985 if ($mediaImage) {
986 $feedHtml .= '<div class="fcom_media" style="margin-top: 20px;">';
987 $feedHtml .= '<a href="' . $postPermalink . '"><img src="' . $mediaImage . '" style="max-width: 100%; height: auto; display: block; margin: 0 auto 0px;" /></a>';
988 if ($mediaCount > 1) {
989 /* translators: %d is the number of additional images not shown in the preview. */
990 $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>';
991 }
992 $feedHtml .= '</div>';
993 }
994
995 return $feedHtml;
996 }
997
998 public static function hasEveryoneTag($message)
999 {
1000 // Updated regular expression to match @everyone with more flexibility
1001 $pattern = '/(?<=^|\W)@everyone(?=\W|\z)/iu';
1002
1003 return preg_match($pattern, $message) === 1;
1004 }
1005
1006 public static function replaceImageUrlsWithRealMediaArchive($markdown, $existingFeed = null)
1007 {
1008 $imageUrls = self::getInlineImageUrls($markdown);
1009
1010 if (!$imageUrls) {
1011 return [$markdown, []];
1012 }
1013
1014 $mediaItems = [];
1015
1016 foreach ($imageUrls as $url) {
1017 $url = sanitize_url($url);
1018 $media = Helper::getMediaFromUrl($url);
1019 if ($media) {
1020 if ($media->is_active && (!$existingFeed || $media->feed_id != $existingFeed->id) ) {
1021 continue;
1022 }
1023
1024 $realUrl = $media->public_url;
1025 $markdown = str_replace($url, $realUrl, $markdown);
1026 $mediaItems[] = $media;
1027 }
1028 }
1029
1030 return [$markdown, $mediaItems];
1031 }
1032
1033 private static function getInlineImageUrls($markdown)
1034 {
1035 $urls = [];
1036 // Match ![alt](url) and ![alt](url "title")
1037 if (preg_match_all('/!\[[^\]]*\]\(([^\s\)]+)(?:\s+"[^"]*")?\)/', $markdown, $matches)) {
1038 $urls = array_merge($urls, $matches[1]);
1039 }
1040
1041 // Match reference-style image usage: ![alt][id] and map only those IDs to their definitions [id]: url
1042 if (preg_match_all('/!\[[^\]]*\]\[([^\]]+)\]/', $markdown, $imageRefMatches)) {
1043 $usedIds = array_unique($imageRefMatches[1]);
1044
1045 if (preg_match_all('/^\[([^\]]+)\]:\s+(\S+)/m', $markdown, $refMatches)) {
1046 $refMap = [];
1047 foreach ($refMatches[1] as $index => $id) {
1048 $refMap[$id] = $refMatches[2][$index];
1049 }
1050
1051 foreach ($usedIds as $id) {
1052 if (isset($refMap[$id])) {
1053 $urls[] = $refMap[$id];
1054 }
1055 }
1056 }
1057 }
1058
1059 // Match HTML <img> tags
1060 if (preg_match_all('/<img[^>]+src=["\']([^"\']+)["\'][^>]*>/i', $markdown, $matches)) {
1061 $urls = array_merge($urls, $matches[1]);
1062 }
1063
1064 return array_unique($urls);
1065 }
1066 }
1067