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

404 lines 12.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\Feed;
7 use FluentCommunity\App\Models\Reaction;
8 use FluentCommunity\App\Models\Term;
9 use FluentCommunity\App\Models\User;
10 use FluentCommunity\App\Models\XProfile;
11 use FluentCommunity\Framework\Support\Arr;
12 use FluentCommunity\Framework\Validator\Validator;
13
14 class FeedsHelper
15 {
16 public static function getSpaceSlugsByUserId($userId)
17 {
18 if (!$userId) {
19 $userId = get_current_user_id();
20 }
21
22 if (!$userId) {
23 return [];
24 }
25
26 $user = User::find($userId);
27
28 return $user->spaces()->pluck('slug')->toArray();
29 }
30
31 public static function getLastFeedId()
32 {
33 $lastItem = Feed::where('status', 'published')
34 ->byUserAccess(get_current_user_id())
35 ->orderBy('id', 'DESC')
36 ->first();
37
38 if ($lastItem) {
39 return $lastItem->id;
40 }
41
42 return 1;
43 }
44
45 public static function mdToHtml($text, $options = [])
46 {
47 if (!$text) {
48 return '';
49 }
50 $text = str_replace('&#x20;', '', $text); // hide markdown empty content
51
52 $html = (new \FluentCommunity\App\Services\Parsedown([
53 ]))
54 ->setBreaksEnabled(true)
55 ->text($text);
56
57 if (!Arr::get($options, 'disable_link_process')) {
58 // add nofollow to all links. But check if nofollow is already there
59 $html = self::addNoFollowToLinks($html);
60 }
61
62 return $html;
63 }
64
65 public static function addNoFollowToLinks($html)
66 {
67 if (!$html) {
68 return '';
69 }
70
71 $current_domain = parse_url(home_url(), PHP_URL_HOST);
72
73 // Regular expression to match <a> tags
74 $pattern = '/<a\s[^>]*href=("|\')(https?:\/\/(?!' . preg_quote($current_domain, '/') . ').*?)("|\')\s?([^>]*)>/i';
75
76 // Callback function to modify each matched <a> tag
77 $callback = function ($matches) {
78 $url = $matches[2];
79 $attr = $matches[4];
80
81 // Remove existing rel attribute if present
82 $attr = preg_replace('/\srel=("|\').*?("|\')/i', '', $attr);
83
84 // Add nofollow
85 return '<a href="' . $url . '" rel="nofollow" ' . trim($attr) . '>';
86 };
87
88 // Perform the replacement
89 return preg_replace_callback($pattern, $callback, $html);
90 }
91
92 public static function findFirstUrl($html)
93 {
94 // use regular expression to find the first URL in a href tag
95 // do not take the url which contains /u/ in it
96 $pattern = '/<a\s+(?:[^>]*?\s+)?href=([\'"])(?!.*\/u\/)(.*?)\1/';
97 preg_match($pattern, $html, $matches);
98
99 if (isset($matches[2])) {
100 return $matches[2];
101 }
102
103 return '';
104 }
105
106 public static function extractHashTags($text, $limit = 5)
107 {
108 // Extract hashtag including - and _
109 preg_match_all('/#([a-zA-Z0-9_-]+)/', $text, $matches);
110
111 $tags = array_unique($matches[1]);
112
113 if (!$tags) {
114 return [];
115 }
116
117 $tags = array_slice($tags, 0, $limit);
118
119 $lowerCaseTags = array_map('strtolower', $tags);
120
121 $terms = Term::whereIn('slug', $lowerCaseTags)
122 ->where('taxonomy_name', 'hashtag')
123 ->get();
124
125 $termIds = [];
126
127 foreach ($terms as $term) {
128 $termIds[$term->slug] = $term->id;
129 }
130
131 if (count($termIds) == count($tags)) {
132 return array_values($termIds);
133 }
134
135 $excepts = array_diff($tags, array_keys($termIds));
136
137 foreach ($excepts as $except) {
138 $term = Term::create([
139 'taxonomy_name' => 'hashtag',
140 'slug' => strtolower($except),
141 'title' => $except
142 ]);
143
144 $termIds[$term->slug] = $term->id;
145 }
146
147 return array_values($termIds);
148 }
149
150 public static function getMentions($text, $spaceId = null)
151 {
152 // the mention may have . or _ or - in the username
153 preg_match_all('/@([a-zA-Z0-9_.-]+)/', $text, $matches);
154 $mentions = array_unique($matches[1]);
155
156 if (!$mentions) {
157 return null;
158 }
159
160 if ($spaceId) {
161 $xProfiles = XProfile::whereIn('username', $mentions)
162 ->whereHas('spaces', function ($query) use ($spaceId) {
163 $query->where('space_id', $spaceId);
164 })
165 ->get();
166 } else {
167 $xProfiles = XProfile::whereIn('username', $mentions)
168 ->get();
169 }
170
171 if ($xProfiles->isEmpty()) {
172 return null;
173 }
174
175 $userMentions = [];
176
177 $userIds = [];
178
179 foreach ($xProfiles as $xProfile) {
180 $userIds[] = $xProfile->user_id;
181 $html = '<a data-user_name="' . $xProfile->username . '" class="fcom_mention fcom_route" href="' . Helper::baseUrl('u/' . $xProfile->username . '/') . '">' . $xProfile->display_name . '</a>';
182 $userMentions['@' . $xProfile->username] = $html;
183 }
184
185 $users = User::whereIn('ID', $userIds)->get();
186
187 return [
188 'users' => $users,
189 'text' => strtr($text, $userMentions)
190 ];
191 }
192
193 public static function getLikedIdsByUserFeedId($feedId, $userId)
194 {
195 return Reaction::select('object_id')
196 ->where('object_type', 'comment')
197 ->where('parent_id', $feedId)
198 ->where('user_id', $userId)
199 ->get()
200 ->pluck('object_id')
201 ->toArray();
202 }
203
204 public static function castSurveyVote($newVoteIndexes, Feed $feed, $userId)
205 {
206 $surveyConfig = Arr::get($feed->meta, 'survey_config', []);
207
208 $slugs = array_map(function ($item) {
209 return $item['slug'];
210 }, $surveyConfig['options']);
211
212 $newVoteIndexes = array_filter(array_intersect($slugs, $newVoteIndexes));
213
214 $previousVotes = Reaction::where('type', 'survey_vote')
215 ->where('user_id', $userId)
216 ->where('object_id', $feed->id)
217 ->get();
218
219 $removedIndexes = [];
220 $alreadyIndexes = [];
221
222 foreach ($previousVotes as $previousVote) {
223 if (!in_array($previousVote->object_type, $newVoteIndexes)) {
224 // This vote need to be deleted
225 $removedIndexes[] = $previousVote->object_type;
226 $previousVote->delete();
227 } else {
228 $alreadyIndexes[] = $previousVote->object_type;
229 }
230 }
231
232 $newSyncIndexes = array_diff($newVoteIndexes, $alreadyIndexes);
233
234 foreach ($newSyncIndexes as $newSyncIndex) {
235 Reaction::create([
236 'user_id' => $userId,
237 'object_id' => $feed->id,
238 'type' => 'survey_vote',
239 'object_type' => $newSyncIndex
240 ]);
241 }
242
243 foreach ($surveyConfig['options'] as $index => $option) {
244 $slug = $option['slug'];
245
246 if (in_array($slug, $removedIndexes)) {
247 $newCount = (int)Arr::get($option, 'vote_counts', 0) - 1;
248 $option['vote_counts'] = $newCount > 0 ? $newCount : 0;
249 } else if (in_array($slug, $newSyncIndexes)) {
250 $newCount = (int)Arr::get($option, 'vote_counts', 0) + 1;
251 $option['vote_counts'] = $newCount > 0 ? $newCount : 0;
252 }
253
254 $surveyConfig['options'][$index] = $option;
255 }
256
257 $meta = $feed->meta;
258 $meta['survey_config'] = $surveyConfig;
259 $feed->meta = $meta;
260 $feed->save();
261
262 Utility::forgetCache('survey_cast_' . $feed->id . '_' . $userId);
263
264 return $feed;
265 }
266
267 public static function createFeed($feedData)
268 {
269 if(!is_array($feedData)){
270 throw new \Exception('Invalid data, The provided data must be an array');
271 }
272
273 $acceptedKeys = ['message', 'title', 'user_id', 'space_id', 'type', 'media_preview'];
274
275 $feedData = Arr::only($feedData, $acceptedKeys);
276
277 // Let's validate the data
278 $validation = Validator::make($feedData, [
279 'message' => 'required',
280 'title' => 'nullable|string',
281 'user_id' => 'required|integer|exists:users,ID',
282 'space_id' => 'nullable|integer|exists:fcom_spaces,id'
283 ]);
284
285 if ($validation->fails()) {
286 throw new \Exception('Validation failed', $validation->errors());
287 }
288
289 $sanitizedData = self::sanitizeAndValidateData($feedData);
290
291 $feedData = wp_parse_args($sanitizedData, $feedData);
292
293 $user = User::findOrFail($feedData['user_id']);
294 $user->syncXProfile();
295
296 if($user->xprofile->status != 'active'){
297 throw new \Exception(esc_html__('User status is not active', 'fluent-community'));
298 }
299
300 $markdown = $feedData['message'];
301
302 $mentions = null;
303
304 // Extra Validaton for space_id
305 if (!empty($feedData['space_id'])) {
306 if (!Helper::isUserInSpace($feedData['user_id'], $feedData['space_id'])) {
307 throw new \Exception(esc_html__('User is not in the space', 'fluent-community'));
308 }
309 $mentions = FeedsHelper::getMentions($markdown, Arr::get($feedData, 'space_id'));
310 if ($mentions) {
311 $markdown = $mentions['text'];
312 }
313 } else if (!Helper::hasGlobalPost()) {
314 throw new \Exception(esc_html__('User is not allowed to post in global', 'fluent-community'));
315 }
316
317 $feedData['message_rendered'] = wp_kses_post(selff::mdToHtml($markdown));
318 $feedData['status'] = 'published';
319
320 $firstUrl = FeedsHelper::findFirstUrl($feedData['message_rendered']);
321
322 if ($firstUrl) {
323 $metaData = RemoteUrlParser::parse($firstUrl);
324 if ($metaData && !is_wp_error($metaData) && (!empty($metaData['image']) || !empty($metaData['html']))) {
325 $feedData['meta'] = [
326 'media_preview' => $metaData
327 ];
328 }
329 }
330
331 $data = apply_filters('fluent_community/feed/new_feed_data', $feedData, $feedData);
332 $feed = new Feed();
333 $feed->fill($data);
334 $feed->save();
335
336 if ($mentions) {
337 do_action('fluent_community/feed_mentioned', $feed, $mentions['users']);
338 }
339
340 do_action('fluent_community/feed/created', $feed);
341
342 if ($feed->space_id) {
343 do_action('fluent_community/space_feed/created', $feed);
344 }
345
346 return $feed;
347 }
348
349 public static function sanitizeAndValidateData($data)
350 {
351 $message = CustomSanitizer::unslashMarkdown(trim(Arr::get($data, 'message')));
352 $type = sanitize_text_field(Arr::get($data, 'type', 'text'));
353
354 $processedData = [
355 'message' => $message,
356 'type' => $type
357 ];
358
359 $survey = Arr::get($data, 'survey', []);
360
361 if ($survey) {
362 $options = Arr::get($survey, 'options', []);
363 $formattedOptions = [];
364 foreach ($options as $index => $option) {
365 if (empty($option['label'])) {
366 continue;
367 }
368
369 $formattedOptions[] = [
370 'label' => sanitize_text_field($option['label']),
371 'slug' => 'opt_' . ($index + 1)
372 ];
373 }
374
375 if ($formattedOptions) {
376 $processedData['survey'] = [
377 'type' => Arr::get($survey, 'type') == 'single_choice' ? 'single_choice' : 'multi_choice',
378 'options' => $formattedOptions
379 ];
380 }
381 }
382
383 $maxlen = apply_filters('fluent_community/max_post_length', 15000);
384 if (\strlen($message) > $maxlen) {
385 throw new \Exception(esc_html__('Post message is too long', 'fluent-community'));
386 }
387
388 $titlePref = Utility::postTitlePref();
389
390 if ($titlePref) {
391 $processedData['title'] = sanitize_text_field(Arr::get($data, 'title'));
392 if ($titlePref == 'required' && empty($processedData['title'])) {
393 throw new \Exception(esc_html__('Title is required. Please provide a title', 'fluent-community'));
394 }
395 // trim the title if it's too long to 150 char
396 if (\strlen($processedData['title']) > 192) {
397 $processedData['title'] = substr($processedData['title'], 0, 192);
398 }
399 }
400
401 return $processedData;
402 }
403 }
404