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; } /** * Create a new feed programmatically * @param array $allData * @return \FluentCommunity\App\Models\Feed|\WP_Error **/ public static function createFeed($allData) { if (!is_array($allData)) { return new \WP_Error('invalid_data', 'Invalid data. The data need to be array', ['status' => 400]); } $acceptedKeys = ['message', 'title', 'user_id', 'space_id', 'type']; $feedData = Arr::only($allData, $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()) { return new \WP_Error('validation_failed', 'Validation failed', $validation->errors()); } $sanitizedData = self::sanitizeAndValidateData($feedData); $feedData = wp_parse_args($sanitizedData, $feedData); $user = User::find($feedData['user_id']); if (!$user) { return new \WP_Error('user_not_found', 'User not found', ['status' => 400]); } $user->syncXProfile(); if ($user->xprofile->status != 'active') { return new \WP_Error('user_inactive', 'User status is not active', $validation->errors()); } $markdown = $feedData['message']; $mentions = null; // Extra Validaton for space_id if (!empty($feedData['space_id'])) { if (!Helper::isUserInSpace($feedData['user_id'], $feedData['space_id'])) { return new \WP_Error('invalid_space', 'User is not in the space', ['status' => 400]); } $mentions = FeedsHelper::getMentions($markdown, Arr::get($feedData, 'space_id')); if ($mentions) { $markdown = $mentions['text']; } } else if (!Helper::hasGlobalPost()) { return new \WP_Error('global_post_disabled', 'User is not allowed to post in global', ['status' => 400]); } $feedData['message_rendered'] = wp_kses_post(self::mdToHtml($markdown)); $feedData['status'] = 'published'; [$feedData, $mediaItems] = self::processFeedMetaData($feedData, $allData); $data = apply_filters('fluent_community/feed/new_feed_data', $feedData, $allData); $feed = new Feed(); $feed->fill($data); $feed->save(); if ($mentions) { do_action('fluent_community/feed_mentioned', $feed, $mentions['users']); } if ($mediaItems) { foreach ($mediaItems as $media) { $media->feed_id = $feed->id; $media->is_active = 1; $media->object_source = 'feed'; $media->save(); } } 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'))); $processedData = [ 'message' => $message, 'type' => 'text' ]; $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; } public static function transformForEdit($feed) { $topicsConfig = Helper::getTopicsConfig(); $terms = $feed->terms; $feed->topic_ids = $terms->where('taxonomy_name', 'post_topic')->pluck('id')->toArray(); if ($topicsConfig['max_topics_per_post'] == 1) { if ($feed->topic_ids) { $feed->topic_ids = Arr::first($feed->topic_ids); } else { $feed->topic_ids = ''; } } $surveyConfig = Arr::get($feed->meta, 'survey_config', []); if ($surveyConfig) { $feed->survey = [ 'type' => Arr::get($surveyConfig, 'type'), 'options' => Arr::get($surveyConfig, 'options', []) ]; } else { $mediaImages = Arr::get($feed->meta, 'media_items', []); $meta = $feed->meta; unset($feed->meta); if ($mediaImages) { $feed->media_images = $mediaImages; } else if ($mediaPreview = Arr::get($meta, 'media_preview')) { $type = Arr::get($mediaPreview, 'type'); if ($type == 'oembed') { $feed->media = $mediaPreview; } $feedMedias = Media::where('object_source', 'feed') ->where('feed_id', $feed->id) ->where('is_active', 1) ->get(); if (!$feedMedias->isEmpty()) { $mediaItems = []; foreach ($feedMedias as $media) { $mediaItems[] = [ 'url' => $media->public_url, 'type' => 'image', 'media_id' => $media->id, 'width' => Arr::get($media->settings, 'width'), 'height' => Arr::get($media->settings, 'height'), 'provider' => Arr::get($media->settings, 'provider', 'uploader') ]; } $feed->media_images = $mediaItems; } else if ($type != 'meta_data') { $feed->meta = $meta; } } } $feed->load('space'); return $feed; } public static function processFeedMetaData($data, $requestData, $existingFeed = null) { if (empty($data['meta'])) { $data['meta'] = []; } $uplaodedDocs = []; // Handle Survey if (!empty($data['survey'])) { $surveyConfig = $data['survey']; if ($existingFeed) { $surveyConfig = Arr::get($existingFeed->meta, 'survey_config', []); if ($surveyConfig) { $oldOptions = Arr::get($surveyConfig, 'options', []); $newOptions = Arr::get($data['survey'], 'options', []); $oldKeyedOptions = []; foreach ($oldOptions as $option) { $oldKeyedOptions[$option['slug']] = $option; } foreach ($newOptions as $index => $option) { $slug = Arr::get($option, 'slug', ''); if (isset($oldKeyedOptions[$slug])) { $newOptions[$index]['vote_counts'] = Arr::get($oldKeyedOptions[$slug], 'vote_counts', 0); } } $surveyConfig['options'] = $newOptions; } } $data['meta']['survey_config'] = $surveyConfig; $data['content_type'] = 'survey'; unset($data['survey']); return [$data, $uplaodedDocs]; } // Handle Giphy if (Arr::get($requestData, 'meta.media_preview.provider') == 'giphy') { $data['meta']['media_preview'] = array_filter([ 'image' => sanitize_url($requestData['meta']['media_preview']['image']), 'type' => sanitize_text_field(Arr::get($requestData, 'meta.media_preview.type', 'image')), 'provider' => 'giphy', 'height' => (int)Arr::get($requestData, 'meta.media_preview.height', 0), 'width' => (int)Arr::get($requestData, 'meta.media_preview.width', 0), ]); return [$data, $uplaodedDocs]; } // Handling Video Embed if (Arr::get($requestData, 'media') && Arr::get($requestData, 'media.type') == 'oembed') { $media = Arr::get($requestData, 'media'); $url = Arr::get($media, 'url'); $metaData = RemoteUrlParser::parse($url); if ($metaData && !is_wp_error($metaData) && (!empty($metaData['image']) || !empty($metaData['html']))) { $data['meta']['media_preview'] = $metaData; } return [$data, $uplaodedDocs]; } // Let's handle the uploaded media if ($mediaImages = Arr::get($requestData, 'media_images', [])) { $uploadedImages = Helper::getMediaByProvider($mediaImages); if (!$existingFeed) { $uploadedMediaItems = Helper::getMediaItemsFromUrl($uploadedImages); } else { $uploadedMediaItems = []; foreach ($mediaImages as $mediaImage) { $url = sanitize_url(Arr::get($mediaImage, 'url', '')); if (!$url) { continue; } $mediaItem = Helper::getMediaFromUrl($mediaImage); if ($mediaItem) { $uploadedMediaItems[] = $mediaItem; } else { // maybe this is a previously uploaded image $media = Media::where('media_url', $url) ->where('object_source', 'feed') ->where('feed_id', $existingFeed->id) ->where('is_active', 1) ->first(); if ($media) { $uploadedMediaItems[] = $media; } } } } if (count($uploadedMediaItems) == 1) { $singleMedia = $uploadedMediaItems[0]; $data['meta']['media_preview'] = [ 'is_uploaded' => true, 'image' => $singleMedia->public_url, 'type' => 'meta_data', 'provider' => 'uploader', 'width' => Arr::get($singleMedia->settings, 'width'), 'height' => Arr::get($singleMedia->settings, 'height'), 'media_id' => $singleMedia->id, ]; } else if ($uploadedMediaItems) { $mediaPreviews = []; foreach ($uploadedMediaItems as $mediaItem) { $mediaData = [ 'media_id' => $mediaItem->id, 'url' => $mediaItem->public_url, 'type' => 'image', 'width' => Arr::get($mediaItem->settings, 'width'), 'height' => Arr::get($mediaItem->settings, 'height'), 'provider' => Arr::get($mediaItem->settings, 'provider', 'uploader') ]; $mediaPreviews[] = array_filter($mediaData); } $data['meta']['media_items'] = $mediaPreviews; } return [$data, $uploadedMediaItems]; } // Let's handle the fallback here $firstUrl = FeedsHelper::findFirstUrl(Arr::get($data, 'message_rendered')); if ($firstUrl) { $metaData = RemoteUrlParser::parse($firstUrl); if ($metaData && !is_wp_error($metaData) && (!empty($metaData['image']) || !empty($metaData['html']))) { $data['meta']['media_preview'] = $metaData; } } return [$data, $uplaodedDocs]; } }