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

606 lines 20.6 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\Feed;
7 use FluentCommunity\App\Models\Media;
8 use FluentCommunity\App\Models\Reaction;
9 use FluentCommunity\App\Models\Term;
10 use FluentCommunity\App\Models\User;
11 use FluentCommunity\App\Models\XProfile;
12 use FluentCommunity\Framework\Support\Arr;
13 use FluentCommunity\Framework\Validator\Validator;
14
15 class FeedsHelper
16 {
17 public static function getSpaceSlugsByUserId($userId)
18 {
19 if (!$userId) {
20 $userId = get_current_user_id();
21 }
22
23 if (!$userId) {
24 return [];
25 }
26
27 $user = User::find($userId);
28
29 return $user->spaces()->pluck('slug')->toArray();
30 }
31
32 public static function getLastFeedId()
33 {
34 $lastItem = Feed::where('status', 'published')
35 ->byUserAccess(get_current_user_id())
36 ->orderBy('id', 'DESC')
37 ->first();
38
39 if ($lastItem) {
40 return $lastItem->id;
41 }
42
43 return 1;
44 }
45
46 public static function mdToHtml($text, $options = [])
47 {
48 if (!$text) {
49 return '';
50 }
51 $text = str_replace('&#x20;', '', $text); // hide markdown empty content
52
53 $html = (new \FluentCommunity\App\Services\Parsedown([
54 ]))
55 ->setBreaksEnabled(true)
56 ->text($text);
57
58 if (!Arr::get($options, 'disable_link_process')) {
59 // add nofollow to all links. But check if nofollow is already there
60 $html = self::addNoFollowToLinks($html);
61 }
62
63 return $html;
64 }
65
66 public static function addNoFollowToLinks($html)
67 {
68 if (!$html) {
69 return '';
70 }
71
72 $current_domain = parse_url(home_url(), PHP_URL_HOST);
73
74 // Regular expression to match <a> tags
75 $pattern = '/<a\s[^>]*href=("|\')(https?:\/\/(?!' . preg_quote($current_domain, '/') . ').*?)("|\')\s?([^>]*)>/i';
76
77 // Callback function to modify each matched <a> tag
78 $callback = function ($matches) {
79 $url = $matches[2];
80 $attr = $matches[4];
81
82 // Remove existing rel attribute if present
83 $attr = preg_replace('/\srel=("|\').*?("|\')/i', '', $attr);
84
85 // Add nofollow
86 return '<a href="' . $url . '" rel="nofollow" ' . trim($attr) . '>';
87 };
88
89 // Perform the replacement
90 return preg_replace_callback($pattern, $callback, $html);
91 }
92
93 public static function findFirstUrl($html)
94 {
95 // use regular expression to find the first URL in a href tag
96 // do not take the url which contains /u/ in it
97 $pattern = '/<a\s+(?:[^>]*?\s+)?href=([\'"])(?!.*\/u\/)(.*?)\1/';
98 preg_match($pattern, $html, $matches);
99
100 if (isset($matches[2])) {
101 return $matches[2];
102 }
103
104 return '';
105 }
106
107 public static function extractHashTags($text, $limit = 5)
108 {
109 // Extract hashtag including - and _
110 preg_match_all('/#([a-zA-Z0-9_-]+)/', $text, $matches);
111
112 $tags = array_unique($matches[1]);
113
114 if (!$tags) {
115 return [];
116 }
117
118 $tags = array_slice($tags, 0, $limit);
119
120 $lowerCaseTags = array_map('strtolower', $tags);
121
122 $terms = Term::whereIn('slug', $lowerCaseTags)
123 ->where('taxonomy_name', 'hashtag')
124 ->get();
125
126 $termIds = [];
127
128 foreach ($terms as $term) {
129 $termIds[$term->slug] = $term->id;
130 }
131
132 if (count($termIds) == count($tags)) {
133 return array_values($termIds);
134 }
135
136 $excepts = array_diff($tags, array_keys($termIds));
137
138 foreach ($excepts as $except) {
139 $term = Term::create([
140 'taxonomy_name' => 'hashtag',
141 'slug' => strtolower($except),
142 'title' => $except
143 ]);
144
145 $termIds[$term->slug] = $term->id;
146 }
147
148 return array_values($termIds);
149 }
150
151 public static function getMentions($text, $spaceId = null)
152 {
153 // the mention may have . or _ or - in the username
154 preg_match_all('/@([a-zA-Z0-9_.-]+)/', $text, $matches);
155 $mentions = array_unique($matches[1]);
156
157 if (!$mentions) {
158 return null;
159 }
160
161 if ($spaceId) {
162 $xProfiles = XProfile::whereIn('username', $mentions)
163 ->whereHas('spaces', function ($query) use ($spaceId) {
164 $query->where('space_id', $spaceId);
165 })
166 ->get();
167 } else {
168 $xProfiles = XProfile::whereIn('username', $mentions)
169 ->get();
170 }
171
172 if ($xProfiles->isEmpty()) {
173 return null;
174 }
175
176 $userMentions = [];
177
178 $userIds = [];
179
180 foreach ($xProfiles as $xProfile) {
181 $userIds[] = $xProfile->user_id;
182 $html = '<a data-user_name="' . $xProfile->username . '" class="fcom_mention fcom_route" href="' . Helper::baseUrl('u/' . $xProfile->username . '/') . '">' . $xProfile->display_name . '</a>';
183 $userMentions['@' . $xProfile->username] = $html;
184 }
185
186 $users = User::whereIn('ID', $userIds)->get();
187
188 return [
189 'users' => $users,
190 'text' => strtr($text, $userMentions)
191 ];
192 }
193
194 public static function getLikedIdsByUserFeedId($feedId, $userId)
195 {
196 return Reaction::select('object_id')
197 ->where('object_type', 'comment')
198 ->where('parent_id', $feedId)
199 ->where('user_id', $userId)
200 ->get()
201 ->pluck('object_id')
202 ->toArray();
203 }
204
205 public static function castSurveyVote($newVoteIndexes, Feed $feed, $userId)
206 {
207 $surveyConfig = Arr::get($feed->meta, 'survey_config', []);
208
209 $slugs = array_map(function ($item) {
210 return $item['slug'];
211 }, $surveyConfig['options']);
212
213 $newVoteIndexes = array_filter(array_intersect($slugs, $newVoteIndexes));
214
215 $previousVotes = Reaction::where('type', 'survey_vote')
216 ->where('user_id', $userId)
217 ->where('object_id', $feed->id)
218 ->get();
219
220 $removedIndexes = [];
221 $alreadyIndexes = [];
222
223 foreach ($previousVotes as $previousVote) {
224 if (!in_array($previousVote->object_type, $newVoteIndexes)) {
225 // This vote need to be deleted
226 $removedIndexes[] = $previousVote->object_type;
227 $previousVote->delete();
228 } else {
229 $alreadyIndexes[] = $previousVote->object_type;
230 }
231 }
232
233 $newSyncIndexes = array_diff($newVoteIndexes, $alreadyIndexes);
234
235 foreach ($newSyncIndexes as $newSyncIndex) {
236 Reaction::create([
237 'user_id' => $userId,
238 'object_id' => $feed->id,
239 'type' => 'survey_vote',
240 'object_type' => $newSyncIndex
241 ]);
242 }
243
244 foreach ($surveyConfig['options'] as $index => $option) {
245 $slug = $option['slug'];
246
247 if (in_array($slug, $removedIndexes)) {
248 $newCount = (int)Arr::get($option, 'vote_counts', 0) - 1;
249 $option['vote_counts'] = $newCount > 0 ? $newCount : 0;
250 } else if (in_array($slug, $newSyncIndexes)) {
251 $newCount = (int)Arr::get($option, 'vote_counts', 0) + 1;
252 $option['vote_counts'] = $newCount > 0 ? $newCount : 0;
253 }
254
255 $surveyConfig['options'][$index] = $option;
256 }
257
258 $meta = $feed->meta;
259 $meta['survey_config'] = $surveyConfig;
260 $feed->meta = $meta;
261 $feed->save();
262
263 Utility::forgetCache('survey_cast_' . $feed->id . '_' . $userId);
264
265 return $feed;
266 }
267
268 /**
269 * Create a new feed programmatically
270 * @param array $allData
271 * @return \FluentCommunity\App\Models\Feed|\WP_Error
272 **/
273 public static function createFeed($allData)
274 {
275 if (!is_array($allData)) {
276 return new \WP_Error('invalid_data', 'Invalid data. The data need to be array', ['status' => 400]);
277 }
278
279 $acceptedKeys = ['message', 'title', 'user_id', 'space_id', 'type'];
280 $feedData = Arr::only($allData, $acceptedKeys);
281
282 // Let's validate the data
283 $validation = Validator::make($feedData, [
284 'message' => 'required',
285 'title' => 'nullable|string',
286 'user_id' => 'required|integer|exists:users,ID',
287 'space_id' => 'nullable|integer|exists:fcom_spaces,id'
288 ]);
289
290 if ($validation->fails()) {
291 return new \WP_Error('validation_failed', 'Validation failed', $validation->errors());
292 }
293
294 $sanitizedData = self::sanitizeAndValidateData($feedData);
295
296 $feedData = wp_parse_args($sanitizedData, $feedData);
297
298 $user = User::find($feedData['user_id']);
299
300 if (!$user) {
301 return new \WP_Error('user_not_found', 'User not found', ['status' => 400]);
302 }
303 $user->syncXProfile();
304 if ($user->xprofile->status != 'active') {
305 return new \WP_Error('user_inactive', 'User status is not active', $validation->errors());
306 }
307
308 $markdown = $feedData['message'];
309 $mentions = null;
310
311 // Extra Validaton for space_id
312 if (!empty($feedData['space_id'])) {
313 if (!Helper::isUserInSpace($feedData['user_id'], $feedData['space_id'])) {
314 return new \WP_Error('invalid_space', 'User is not in the space', ['status' => 400]);
315 }
316 $mentions = FeedsHelper::getMentions($markdown, Arr::get($feedData, 'space_id'));
317 if ($mentions) {
318 $markdown = $mentions['text'];
319 }
320 } else if (!Helper::hasGlobalPost()) {
321 return new \WP_Error('global_post_disabled', 'User is not allowed to post in global', ['status' => 400]);
322 }
323
324 $feedData['message_rendered'] = wp_kses_post(self::mdToHtml($markdown));
325 $feedData['status'] = 'published';
326
327 [$feedData, $mediaItems] = self::processFeedMetaData($feedData, $allData);
328
329 $data = apply_filters('fluent_community/feed/new_feed_data', $feedData, $allData);
330 $feed = new Feed();
331 $feed->fill($data);
332 $feed->save();
333
334 if ($mentions) {
335 do_action('fluent_community/feed_mentioned', $feed, $mentions['users']);
336 }
337
338 if ($mediaItems) {
339 foreach ($mediaItems as $media) {
340 $media->feed_id = $feed->id;
341 $media->is_active = 1;
342 $media->object_source = 'feed';
343 $media->save();
344 }
345 }
346
347 do_action('fluent_community/feed/created', $feed);
348
349 if ($feed->space_id) {
350 do_action('fluent_community/space_feed/created', $feed);
351 }
352
353 return $feed;
354 }
355
356 public static function sanitizeAndValidateData($data)
357 {
358 $message = CustomSanitizer::unslashMarkdown(trim(Arr::get($data, 'message')));
359
360 $processedData = [
361 'message' => $message,
362 'type' => 'text'
363 ];
364
365 $survey = Arr::get($data, 'survey', []);
366
367 if ($survey) {
368 $options = Arr::get($survey, 'options', []);
369 $formattedOptions = [];
370 foreach ($options as $index => $option) {
371 if (empty($option['label'])) {
372 continue;
373 }
374
375 $formattedOptions[] = [
376 'label' => sanitize_text_field($option['label']),
377 'slug' => 'opt_' . ($index + 1)
378 ];
379 }
380
381 if ($formattedOptions) {
382 $processedData['survey'] = [
383 'type' => Arr::get($survey, 'type') == 'single_choice' ? 'single_choice' : 'multi_choice',
384 'options' => $formattedOptions
385 ];
386 }
387 }
388
389 $maxlen = apply_filters('fluent_community/max_post_length', 15000);
390 if (\strlen($message) > $maxlen) {
391 throw new \Exception(esc_html__('Post message is too long', 'fluent-community'));
392 }
393
394 $titlePref = Utility::postTitlePref();
395
396 if ($titlePref) {
397 $processedData['title'] = sanitize_text_field(Arr::get($data, 'title'));
398 if ($titlePref == 'required' && empty($processedData['title'])) {
399 throw new \Exception(esc_html__('Title is required. Please provide a title', 'fluent-community'));
400 }
401 // trim the title if it's too long to 150 char
402 if (\strlen($processedData['title']) > 192) {
403 $processedData['title'] = substr($processedData['title'], 0, 192);
404 }
405 }
406
407 return $processedData;
408 }
409
410 public static function transformForEdit($feed)
411 {
412 $topicsConfig = Helper::getTopicsConfig();
413
414 $terms = $feed->terms;
415 $feed->topic_ids = $terms->where('taxonomy_name', 'post_topic')->pluck('id')->toArray();
416 if ($topicsConfig['max_topics_per_post'] == 1) {
417 if ($feed->topic_ids) {
418 $feed->topic_ids = Arr::first($feed->topic_ids);
419 } else {
420 $feed->topic_ids = '';
421 }
422 }
423
424 $surveyConfig = Arr::get($feed->meta, 'survey_config', []);
425
426 if ($surveyConfig) {
427 $feed->survey = [
428 'type' => Arr::get($surveyConfig, 'type'),
429 'options' => Arr::get($surveyConfig, 'options', [])
430 ];
431 } else {
432 $mediaImages = Arr::get($feed->meta, 'media_items', []);
433 $meta = $feed->meta;
434 unset($feed->meta);
435
436 if ($mediaImages) {
437 $feed->media_images = $mediaImages;
438 } else if ($mediaPreview = Arr::get($meta, 'media_preview')) {
439 $type = Arr::get($mediaPreview, 'type');
440 if ($type == 'oembed') {
441 $feed->media = $mediaPreview;
442 }
443
444 $feedMedias = Media::where('object_source', 'feed')
445 ->where('feed_id', $feed->id)
446 ->where('is_active', 1)
447 ->get();
448
449 if (!$feedMedias->isEmpty()) {
450 $mediaItems = [];
451 foreach ($feedMedias as $media) {
452 $mediaItems[] = [
453 'url' => $media->public_url,
454 'type' => 'image',
455 'media_id' => $media->id,
456 'width' => Arr::get($media->settings, 'width'),
457 'height' => Arr::get($media->settings, 'height'),
458 'provider' => Arr::get($media->settings, 'provider', 'uploader')
459 ];
460 }
461 $feed->media_images = $mediaItems;
462 } else if ($type != 'meta_data') {
463 $feed->meta = $meta;
464 }
465 }
466 }
467
468 $feed->load('space');
469 return $feed;
470 }
471
472 public static function processFeedMetaData($data, $requestData, $existingFeed = null)
473 {
474 if (empty($data['meta'])) {
475 $data['meta'] = [];
476 }
477
478 $uplaodedDocs = [];
479
480 // Handle Survey
481 if (!empty($data['survey'])) {
482 $surveyConfig = $data['survey'];
483 if ($existingFeed) {
484 $surveyConfig = Arr::get($existingFeed->meta, 'survey_config', []);
485 if ($surveyConfig) {
486 $oldOptions = Arr::get($surveyConfig, 'options', []);
487 $newOptions = Arr::get($data['survey'], 'options', []);
488 $oldKeyedOptions = [];
489 foreach ($oldOptions as $option) {
490 $oldKeyedOptions[$option['slug']] = $option;
491 }
492 foreach ($newOptions as $index => $option) {
493 $slug = Arr::get($option, 'slug', '');
494 if (isset($oldKeyedOptions[$slug])) {
495 $newOptions[$index]['vote_counts'] = Arr::get($oldKeyedOptions[$slug], 'vote_counts', 0);
496 }
497 }
498
499 $surveyConfig['options'] = $newOptions;
500 }
501 }
502
503 $data['meta']['survey_config'] = $surveyConfig;
504 $data['content_type'] = 'survey';
505 unset($data['survey']);
506 return [$data, $uplaodedDocs];
507 }
508
509 // Handle Giphy
510 if (Arr::get($requestData, 'meta.media_preview.provider') == 'giphy') {
511 $data['meta']['media_preview'] = array_filter([
512 'image' => sanitize_url($requestData['meta']['media_preview']['image']),
513 'type' => sanitize_text_field(Arr::get($requestData, 'meta.media_preview.type', 'image')),
514 'provider' => 'giphy',
515 'height' => (int)Arr::get($requestData, 'meta.media_preview.height', 0),
516 'width' => (int)Arr::get($requestData, 'meta.media_preview.width', 0),
517 ]);
518 return [$data, $uplaodedDocs];
519 }
520
521 // Handling Video Embed
522 if (Arr::get($requestData, 'media') && Arr::get($requestData, 'media.type') == 'oembed') {
523 $media = Arr::get($requestData, 'media');
524 $url = Arr::get($media, 'url');
525 $metaData = RemoteUrlParser::parse($url);
526 if ($metaData && !is_wp_error($metaData) && (!empty($metaData['image']) || !empty($metaData['html']))) {
527 $data['meta']['media_preview'] = $metaData;
528 }
529
530 return [$data, $uplaodedDocs];
531 }
532
533 // Let's handle the uploaded media
534 if ($mediaImages = Arr::get($requestData, 'media_images', [])) {
535 $uploadedImages = Helper::getMediaByProvider($mediaImages);
536 if (!$existingFeed) {
537 $uploadedMediaItems = Helper::getMediaItemsFromUrl($uploadedImages);
538 } else {
539 $uploadedMediaItems = [];
540 foreach ($mediaImages as $mediaImage) {
541 $url = sanitize_url(Arr::get($mediaImage, 'url', ''));
542 if (!$url) {
543 continue;
544 }
545 $mediaItem = Helper::getMediaFromUrl($mediaImage);
546 if ($mediaItem) {
547 $uploadedMediaItems[] = $mediaItem;
548 } else {
549 // maybe this is a previously uploaded image
550 $media = Media::where('media_url', $url)
551 ->where('object_source', 'feed')
552 ->where('feed_id', $existingFeed->id)
553 ->where('is_active', 1)
554 ->first();
555
556 if ($media) {
557 $uploadedMediaItems[] = $media;
558 }
559 }
560 }
561 }
562
563 if (count($uploadedMediaItems) == 1) {
564 $singleMedia = $uploadedMediaItems[0];
565 $data['meta']['media_preview'] = [
566 'is_uploaded' => true,
567 'image' => $singleMedia->public_url,
568 'type' => 'meta_data',
569 'provider' => 'uploader',
570 'width' => Arr::get($singleMedia->settings, 'width'),
571 'height' => Arr::get($singleMedia->settings, 'height'),
572 'media_id' => $singleMedia->id,
573 ];
574
575 } else if ($uploadedMediaItems) {
576 $mediaPreviews = [];
577 foreach ($uploadedMediaItems as $mediaItem) {
578 $mediaData = [
579 'media_id' => $mediaItem->id,
580 'url' => $mediaItem->public_url,
581 'type' => 'image',
582 'width' => Arr::get($mediaItem->settings, 'width'),
583 'height' => Arr::get($mediaItem->settings, 'height'),
584 'provider' => Arr::get($mediaItem->settings, 'provider', 'uploader')
585 ];
586 $mediaPreviews[] = array_filter($mediaData);
587 }
588 $data['meta']['media_items'] = $mediaPreviews;
589 }
590
591 return [$data, $uploadedMediaItems];
592 }
593
594 // Let's handle the fallback here
595 $firstUrl = FeedsHelper::findFirstUrl(Arr::get($data, 'message_rendered'));
596 if ($firstUrl) {
597 $metaData = RemoteUrlParser::parse($firstUrl);
598 if ($metaData && !is_wp_error($metaData) && (!empty($metaData['image']) || !empty($metaData['html']))) {
599 $data['meta']['media_preview'] = $metaData;
600 }
601 }
602
603 return [$data, $uplaodedDocs];
604 }
605 }
606