spaces()->pluck('slug')->toArray(); } public static function getLastFeedId() { $lastItem = Feed::where('status', 'published') ->byUserAccess(get_current_user_id()) ->orderBy('id', 'DESC') ->first(); if ($lastItem) { return $lastItem->id; } return 1; } public static function mdToHtml($text, $options = []) { if (!$text) { return ''; } $text = str_replace(' ', '', $text); // hide markdown empty content $html = (new \FluentCommunity\App\Services\Parsedown([ ])) ->setBreaksEnabled(true) ->text($text); if (!Arr::get($options, 'disable_link_process')) { // add nofollow to all links. But check if nofollow is already there $html = self::addNoFollowToLinks($html); } return $html; } public static function addNoFollowToLinks($html) { if (!$html) { return ''; } $current_domain = parse_url(home_url(), PHP_URL_HOST); // Regular expression to match tags $pattern = '/]*href=("|\')(https?:\/\/(?!' . preg_quote($current_domain, '/') . ').*?)("|\')\s?([^>]*)>/i'; // Callback function to modify each matched tag $callback = function ($matches) { $url = $matches[2]; $attr = $matches[4]; // Remove existing rel attribute if present $attr = preg_replace('/\srel=("|\').*?("|\')/i', '', $attr); // Add nofollow return ''; }; // Perform the replacement return preg_replace_callback($pattern, $callback, $html); } public static function findFirstUrl($html) { // use regular expression to find the first URL in a href tag // do not take the url which contains /u/ in it $pattern = '/]*?\s+)?href=([\'"])(?!.*\/u\/)(.*?)\1/'; preg_match($pattern, $html, $matches); if (isset($matches[2])) { return $matches[2]; } return ''; } public static function extractHashTags($text, $limit = 5) { // Extract hashtag including - and _ preg_match_all('/#([a-zA-Z0-9_-]+)/', $text, $matches); $tags = array_unique($matches[1]); if (!$tags) { return []; } $tags = array_slice($tags, 0, $limit); $lowerCaseTags = array_map('strtolower', $tags); $terms = Term::whereIn('slug', $lowerCaseTags) ->where('taxonomy_name', 'hashtag') ->get(); $termIds = []; foreach ($terms as $term) { $termIds[$term->slug] = $term->id; } if (count($termIds) == count($tags)) { return array_values($termIds); } $excepts = array_diff($tags, array_keys($termIds)); foreach ($excepts as $except) { $term = Term::create([ 'taxonomy_name' => 'hashtag', 'slug' => strtolower($except), 'title' => $except ]); $termIds[$term->slug] = $term->id; } return array_values($termIds); } public static function getMentions($text, $spaceId = null) { // the mention may have . or _ or - in the username preg_match_all('/@([a-zA-Z0-9_.-]+)/', $text, $matches); $mentions = array_unique($matches[1]); if (!$mentions) { return null; } if ($spaceId) { $xProfiles = XProfile::whereIn('username', $mentions) ->whereHas('spaces', function ($query) use ($spaceId) { $query->where('space_id', $spaceId); }) ->get(); } else { $xProfiles = XProfile::whereIn('username', $mentions) ->get(); } if ($xProfiles->isEmpty()) { return null; } $userMentions = []; $userIds = []; foreach ($xProfiles as $xProfile) { $userIds[] = $xProfile->user_id; $html = '' . $xProfile->display_name . ''; $userMentions['@' . $xProfile->username] = $html; } $users = User::whereIn('ID', $userIds)->get(); return [ 'users' => $users, 'text' => strtr($text, $userMentions) ]; } public static function getLikedIdsByUserFeedId($feedId, $userId) { return Reaction::select('object_id') ->where('object_type', 'comment') ->where('parent_id', $feedId) ->where('user_id', $userId) ->get() ->pluck('object_id') ->toArray(); } public static function castSurveyVote($newVoteIndexes, Feed $feed, $userId) { $surveyConfig = Arr::get($feed->meta, 'survey_config', []); $slugs = array_map(function ($item) { return $item['slug']; }, $surveyConfig['options']); $newVoteIndexes = array_filter(array_intersect($slugs, $newVoteIndexes)); $previousVotes = Reaction::where('type', 'survey_vote') ->where('user_id', $userId) ->where('object_id', $feed->id) ->get(); $removedIndexes = []; $alreadyIndexes = []; foreach ($previousVotes as $previousVote) { if (!in_array($previousVote->object_type, $newVoteIndexes)) { // This vote need to be deleted $removedIndexes[] = $previousVote->object_type; $previousVote->delete(); } else { $alreadyIndexes[] = $previousVote->object_type; } } $newSyncIndexes = array_diff($newVoteIndexes, $alreadyIndexes); foreach ($newSyncIndexes as $newSyncIndex) { Reaction::create([ 'user_id' => $userId, 'object_id' => $feed->id, 'type' => 'survey_vote', 'object_type' => $newSyncIndex ]); } foreach ($surveyConfig['options'] as $index => $option) { $slug = $option['slug']; if (in_array($slug, $removedIndexes)) { $newCount = (int)Arr::get($option, 'vote_counts', 0) - 1; $option['vote_counts'] = $newCount > 0 ? $newCount : 0; } else if (in_array($slug, $newSyncIndexes)) { $newCount = (int)Arr::get($option, 'vote_counts', 0) + 1; $option['vote_counts'] = $newCount > 0 ? $newCount : 0; } $surveyConfig['options'][$index] = $option; } $meta = $feed->meta; $meta['survey_config'] = $surveyConfig; $feed->meta = $meta; $feed->save(); Utility::forgetCache('survey_cast_' . $feed->id . '_' . $userId); return $feed; } public static function createFeed($feedData) { if(!is_array($feedData)){ throw new \Exception('Invalid data, The provided data must be an array'); } $acceptedKeys = ['message', 'title', 'user_id', 'space_id', 'type', 'media_preview']; $feedData = Arr::only($feedData, $acceptedKeys); // Let's validate the data $validation = Validator::make($feedData, [ 'message' => 'required', 'title' => 'nullable|string', 'user_id' => 'required|integer|exists:users,ID', 'space_id' => 'nullable|integer|exists:fcom_spaces,id' ]); if ($validation->fails()) { throw new \Exception('Validation failed', $validation->errors()); } $sanitizedData = self::sanitizeAndValidateData($feedData); $feedData = wp_parse_args($sanitizedData, $feedData); $user = User::findOrFail($feedData['user_id']); $user->syncXProfile(); if($user->xprofile->status != 'active'){ throw new \Exception(esc_html__('User status is not active', 'fluent-community')); } $markdown = $feedData['message']; $mentions = null; // Extra Validaton for space_id if (!empty($feedData['space_id'])) { if (!Helper::isUserInSpace($feedData['user_id'], $feedData['space_id'])) { throw new \Exception(esc_html__('User is not in the space', 'fluent-community')); } $mentions = FeedsHelper::getMentions($markdown, Arr::get($feedData, 'space_id')); if ($mentions) { $markdown = $mentions['text']; } } else if (!Helper::hasGlobalPost()) { throw new \Exception(esc_html__('User is not allowed to post in global', 'fluent-community')); } $feedData['message_rendered'] = wp_kses_post(selff::mdToHtml($markdown)); $feedData['status'] = 'published'; $firstUrl = FeedsHelper::findFirstUrl($feedData['message_rendered']); if ($firstUrl) { $metaData = RemoteUrlParser::parse($firstUrl); if ($metaData && !is_wp_error($metaData) && (!empty($metaData['image']) || !empty($metaData['html']))) { $feedData['meta'] = [ 'media_preview' => $metaData ]; } } $data = apply_filters('fluent_community/feed/new_feed_data', $feedData, $feedData); $feed = new Feed(); $feed->fill($data); $feed->save(); if ($mentions) { do_action('fluent_community/feed_mentioned', $feed, $mentions['users']); } do_action('fluent_community/feed/created', $feed); if ($feed->space_id) { do_action('fluent_community/space_feed/created', $feed); } return $feed; } public static function sanitizeAndValidateData($data) { $message = CustomSanitizer::unslashMarkdown(trim(Arr::get($data, 'message'))); $type = sanitize_text_field(Arr::get($data, 'type', 'text')); $processedData = [ 'message' => $message, 'type' => $type ]; $survey = Arr::get($data, 'survey', []); if ($survey) { $options = Arr::get($survey, 'options', []); $formattedOptions = []; foreach ($options as $index => $option) { if (empty($option['label'])) { continue; } $formattedOptions[] = [ 'label' => sanitize_text_field($option['label']), 'slug' => 'opt_' . ($index + 1) ]; } if ($formattedOptions) { $processedData['survey'] = [ 'type' => Arr::get($survey, 'type') == 'single_choice' ? 'single_choice' : 'multi_choice', 'options' => $formattedOptions ]; } } $maxlen = apply_filters('fluent_community/max_post_length', 15000); if (\strlen($message) > $maxlen) { throw new \Exception(esc_html__('Post message is too long', 'fluent-community')); } $titlePref = Utility::postTitlePref(); if ($titlePref) { $processedData['title'] = sanitize_text_field(Arr::get($data, 'title')); if ($titlePref == 'required' && empty($processedData['title'])) { throw new \Exception(esc_html__('Title is required. Please provide a title', 'fluent-community')); } // trim the title if it's too long to 150 char if (\strlen($processedData['title']) > 192) { $processedData['title'] = substr($processedData['title'], 0, 192); } } return $processedData; } }