| | |
| | | <?php |
| | | namespace JVBase\rest\routes; |
| | | |
| | | use JVBase\managers\Cache; |
| | | use JVBase\rest\RestRouteManager; |
| | | use JVBase\meta\Meta; |
| | | use JVBase\rest\Rest; |
| | | use JVBase\integrations\Umami; |
| | | use JVBase\meta\MetaManager; |
| | | use JVBase\managers\TaxonomyRelationships; |
| | | use JVBase\rest\Route; |
| | | use JVBase\utility\Checker; |
| | | use JVBase\utility\Features; |
| | | use WP_Query; |
| | |
| | | exit; // Exit if accessed directly |
| | | } |
| | | |
| | | class FeedRoutes extends RestRouteManager |
| | | class FeedRoutes extends Rest |
| | | { |
| | | protected int $per_page = 36; |
| | | protected ?Umami $tracker = null; |
| | |
| | | |
| | | public function __construct() |
| | | { |
| | | $this->cache_name = 'feed'; |
| | | $this->cache_ttl = 86400; |
| | | $this->cacheName = 'feed'; |
| | | $this->cacheTtl = 86400; |
| | | parent::__construct(); |
| | | $this->cache |
| | | ->connect('post', true) |
| | |
| | | */ |
| | | public function registerRoutes(): void |
| | | { |
| | | register_rest_route($this->namespace, '/feed', [ |
| | | 'methods' => ['GET', 'POST'], |
| | | 'callback' => [$this, 'handleFeedRequest'], |
| | | 'permission_callback' => [$this, 'checkPermission'], |
| | | ]); |
| | | Route::for('feed') |
| | | ->get([$this, 'handleFeedRequest']) |
| | | ->args([ |
| | | 'content' => 'string', |
| | | 'page' => 'integer|default:1|min:1', |
| | | 'taxonomy' => 'string', |
| | | 'match' => 'string|enum:all,any|default:all', |
| | | 'orderby' => 'string', |
| | | 'order' => 'string|enum:ASC,DESC', |
| | | 'date-filter' => 'string', |
| | | 'dateFrom' => 'string', |
| | | 'dateTo' => 'string', |
| | | 'context' => 'string', |
| | | 'source' => 'string', |
| | | 'favourites' => 'boolean', |
| | | 'user' => 'integer', |
| | | 'highlight' => 'string', |
| | | ]) |
| | | ->auth('public') |
| | | ->rateLimit(30, 60) |
| | | ->post([$this, 'handleFeedRequest']) |
| | | ->args([ |
| | | 'content' => 'string', |
| | | 'page' => 'integer|default:1|min:1', |
| | | 'taxonomy' => 'string', |
| | | 'match' => 'string|enum:all,any|default:all', |
| | | 'orderby' => 'string', |
| | | 'order' => 'string|enum:ASC,DESC', |
| | | 'date-filter' => 'string', |
| | | 'dateFrom' => 'string', |
| | | 'dateTo' => 'string', |
| | | 'context' => 'string', |
| | | 'source' => 'string', |
| | | 'favourites' => 'boolean', |
| | | 'user' => 'integer', |
| | | 'highlight' => 'string', |
| | | ]) |
| | | ->auth('public') |
| | | ->rateLimit(30, 60); |
| | | |
| | | register_rest_route($this->namespace, 'feed/types', [ |
| | | 'permission_callback' => [$this, 'checkPermission'], |
| | | 'methods' => 'GET', |
| | | 'callback' => [$this, 'getFeedTypes'] |
| | | ]); |
| | | // Feed types endpoint |
| | | Route::for('feed/types') |
| | | ->get([$this, 'getFeedTypes']) |
| | | ->auth('public') |
| | | ->rateLimit(60, 60); |
| | | } |
| | | |
| | | /** |
| | |
| | | switch ($metaType) { |
| | | case 'post': |
| | | $config = JVB_CONTENT[$type]; |
| | | |
| | | $meta = Meta::forPost($postID); |
| | | if (!$skip && array_key_exists('is_timeline', $config) && $config['is_timeline']) { |
| | | return $this->formatTimeline($postID, $post); |
| | | } |
| | | break; |
| | | case 'term': |
| | | |
| | | $meta = Meta::forTerm($postID); |
| | | $config = JVB_TAXONOMY[$type]; |
| | | break; |
| | | } |
| | |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | } |
| | | |
| | | $meta = new MetaManager($postID, $metaType); |
| | | $values = $meta->getAll(array_keys($fields)); |
| | | |
| | | $out = [ |
| | |
| | | } |
| | | $item = $this->formatItem($postID, 'post', true); |
| | | //Step 1: Get the fields that apply to all posts |
| | | $mainMeta = new MetaManager($post->ID, 'post'); |
| | | $mainMeta = Meta::forPost($post->ID); |
| | | $item['fields'] = $mainMeta->getAll($this->timelineSharedFields); |
| | | |
| | | //Step 2: Get the fields for each individual posts |
| | |
| | | $subFields = []; |
| | | $images = []; |
| | | foreach ($children as $child) { |
| | | $meta = new MetaManager($child, 'post'); |
| | | $meta = Meta::forPost($child); |
| | | $f = $meta->getAll($this->timelineUniqueFields); |
| | | $f = ['id' => $child] + $f; |
| | | $subFields[] = $f; |
| | |
| | | return $this->applyFavouritesFilter($args, $data); |
| | | } |
| | | |
| | | // protected function applyTaxonomyFilters(array $args, array $data): array |
| | | // { |
| | | // if (!array_key_exists('taxonomy', $data) || empty($data['taxonomy'])) { |
| | | // return $args; |
| | | // } |
| | | // |
| | | // $taxonomyFilters = $data['taxonomy']; |
| | | // |
| | | // // Validate taxonomies exist and sanitize |
| | | // $validFilters = []; |
| | | // foreach ($taxonomyFilters as $taxonomy => $terms) { |
| | | // if (!taxonomy_exists(jvbCheckBase($taxonomy))) { |
| | | // continue; |
| | | // } |
| | | // |
| | | // $validFilters[] = [ |
| | | // 'taxonomy' => jvbCheckBase($taxonomy), |
| | | // 'field' => 'term_id', |
| | | // 'terms' => array_map('absint', (array)$terms), |
| | | // 'operator' => 'IN' |
| | | // ]; |
| | | // } |
| | | // |
| | | // if (empty($validFilters)) { |
| | | // return $args; |
| | | // } |
| | | // |
| | | // // Determine relation based on match filter |
| | | // $relation = ($data['match'] ?? 'all') === 'all' ? 'AND' : 'OR'; |
| | | // |
| | | // $args['tax_query'] = array_merge( |
| | | // ['relation' => $relation], |
| | | // $validFilters |
| | | // ); |
| | | // |
| | | // return $args; |
| | | // } |
| | | |
| | | /** |
| | | * @param WP_REST_Request $request |
| | | * |
| | |
| | | $args['highlight'] = $highlight; |
| | | } |
| | | $cached['items'] = $this->processHighlightedItem($cached['items'], $args); |
| | | $response = new WP_REST_Response($cached); |
| | | $response = $this->success($cached); |
| | | return $this->addCacheHeaders($response); |
| | | } |
| | | // Fetch and format items |
| | | $items = $this->fetchFeedItems($args); |
| | | |
| | | $ttl = (str_contains($args['orderby'], 'RAND')) ? 300 : $this->cache_ttl; |
| | | $ttl = (str_contains($args['orderby'], 'RAND')) ? 300 : $this->cacheTtl; |
| | | $this->cache->set($key, $items, $ttl); |
| | | |
| | | if ($request->get_param('highlight')) { |
| | |
| | | } |
| | | |
| | | $items['items'] = $this->processHighlightedItem($items['items'], $args); |
| | | $response = new WP_REST_Response($items); |
| | | $response = $this->success($items); |
| | | return $this->addCacheHeaders($response); |
| | | } |
| | | |
| | | /** |
| | | * Build cache context from query args |
| | | * Extracts content types and parameters needed for proper cache checking |
| | | * |
| | | * @param array $args Built WP_Query arguments |
| | | * @param WP_REST_Request $request Original request |
| | | * @return array Cache context with content_types and additional_params |
| | | */ |
| | | protected function buildCacheContext(array $args, WP_REST_Request $request): array |
| | | { |
| | | // Extract content types from post_type in args |
| | | $post_types = is_array($args['post_type']) |
| | | ? $args['post_type'] |
| | | : [$args['post_type']]; |
| | | |
| | | $content_types = array_map('jvbNoBase', $post_types); |
| | | $content_types[] = 'feed'; // Always include base feed type |
| | | |
| | | // Build additional params for ETag uniqueness |
| | | $additional_params = [ |
| | | 'order' => $args['orderby'] ?? 'date', |
| | | 'direction' => $args['order'] ?? 'DESC', |
| | | 'page' => $args['paged'] ?? 1, |
| | | ]; |
| | | |
| | | if ($request->get_param('favourites')) { |
| | | $additional_params['user'] = (int)$request->get_param('user'); |
| | | } |
| | | |
| | | // Include author filter if present (from context or favourites) |
| | | if (!empty($args['author'])) { |
| | | $additional_params['author'] = $args['author']; |
| | | } |
| | | |
| | | if (!empty($args['author__in'])) { |
| | | $additional_params['author__in'] = $args['author__in']; |
| | | } |
| | | |
| | | // Include taxonomy filters if present |
| | | if (!empty($args['tax_query'])) { |
| | | $tax_filters = []; |
| | | foreach ($args['tax_query'] as $key => $query) { |
| | | if ($key === 'relation' || !is_array($query)) { |
| | | continue; |
| | | } |
| | | |
| | | $taxonomy = jvbNoBase($query['taxonomy'] ?? ''); |
| | | if ($taxonomy) { |
| | | $tax_filters[$taxonomy] = $query['terms'] ?? []; |
| | | // Also add taxonomy to content_types for timestamp checking |
| | | $content_types[] = $taxonomy; |
| | | } |
| | | } |
| | | if (!empty($tax_filters)) { |
| | | $additional_params['taxonomies'] = $tax_filters; |
| | | } |
| | | } |
| | | |
| | | // Include date filters if present |
| | | if (!empty($args['date_query'])) { |
| | | $additional_params['date_filter'] = md5(serialize($args['date_query'])); |
| | | } |
| | | |
| | | // Include meta queries if present |
| | | if (!empty($args['meta_query'])) { |
| | | $additional_params['meta_filter'] = md5(serialize($args['meta_query'])); |
| | | } |
| | | |
| | | return [ |
| | | 'content_types' => array_unique($content_types), |
| | | 'additional_params' => $additional_params |
| | | ]; |
| | | } |
| | | |
| | | /** |
| | | * @param array $args Formatted Args for WP_Query |
| | | * @param array $items Formatted Args for WP_Query |
| | | * @param array $data parsed Request Data |
| | | * |
| | | * @return array|null |
| | |
| | | : explode(',', $args['post_type']); |
| | | |
| | | // Check if filtering global feed content |
| | | $globalFeedTypes = array_map('jvbCheckBase', |
| | | array_keys(Features::getTypesWithFeature('show_feed', 'content')) |
| | | if (in_array($context['type'], jvbGlobalFeedContentTaxonomies())) { |
| | | // Global: show posts from any content type with this taxonomy |
| | | $for_content = JVB_TAXONOMY[$context['type']]['for_content'] ?? []; |
| | | if (empty($for_content)) { |
| | | // Fall back to any content that has this taxonomy registered |
| | | $for_content = array_keys( |
| | | array_filter( |
| | | JVB_CONTENT, |
| | | fn($c) => in_array($context['type'], $c['taxonomies'] ?? []) |
| | | ) |
| | | ); |
| | | } |
| | | |
| | | if (array_intersect($args['post_type'], $globalFeedTypes)) { |
| | | $artists = jvbGetContentUsers($context['id']); |
| | | if (!empty($artists)) { |
| | | $args['author__in'] = $artists; |
| | | // Convert to full post types with BASE prefix |
| | | $post_types = array_map(fn($type) => BASE . $type, $for_content); |
| | | |
| | | // Filter to only show_feed content types |
| | | $show_feed_types = Features::getTypesWithFeature('show_feed', 'content'); |
| | | $args['post_type'] = array_intersect( |
| | | $post_types, |
| | | array_map(fn($type) => BASE . $type, $show_feed_types) |
| | | ); |
| | | } |
| | | } else { |
| | | $args['tax_query'] = [ |
| | | 'relation' => 'AND', |
| | | [ |
| | | 'taxonomy' => BASE . $context['type'], |
| | | 'terms' => $context['id'], |
| | | ] |
| | | ]; |
| | | } |
| | | break; |
| | | case taxonomy_exists(jvbCheckBase($context['type'])): |
| | | $args['tax_query'] = [ |
| | | 'relation' => 'AND', |
| | | [ |
| | | 'taxonomy' => BASE . $context['type'], |
| | | 'terms' => $context['id'], |
| | | ] |
| | | |
| | | // Add term to tax query |
| | | $args['tax_query'][] = [ |
| | | 'taxonomy' => jvbCheckBase($context['type']), |
| | | 'field' => 'term_id', |
| | | 'terms' => [(int)$context['id']], |
| | | ]; |
| | | break; |
| | | } |
| | | |
| | | return $args; |
| | | } |
| | | |
| | |
| | | * |
| | | * @return array |
| | | */ |
| | | protected function applyFavouritesFilter(array $args, array $filters): array |
| | | protected function applyFavouritesFilter(array $args, array $data): array |
| | | { |
| | | if (!array_key_exists('favourites', $filters)) { |
| | | if (empty($data['favourites']) || empty($data['user'])) { |
| | | return $args; |
| | | } |
| | | global $wpdb; |
| | | |
| | | // Get post types for the current filter |
| | | $post_types = is_array($args['post_type']) |
| | | ? $args['post_type'] |
| | | : [$args['post_type']]; |
| | | $user_id = (int)$data['user']; |
| | | $content = jvbNoBase($args['post_type']); |
| | | |
| | | $favourites_table = $wpdb->prefix . BASE . 'favourites'; |
| | | $placeholders = implode(',', array_fill(0, count($post_types), '%s')); |
| | | $favourited_ids = $wpdb->get_col($wpdb->prepare( |
| | | "SELECT target_id FROM {$favourites_table} |
| | | WHERE user_id = %d AND type IN ($placeholders)", |
| | | array_merge( |
| | | [get_current_user_id()], |
| | | $post_types |
| | | ) |
| | | )); |
| | | // Get user's favourites for this content type |
| | | $fav_key = BASE . 'favourites_' . $content; |
| | | $favourites = get_user_meta($user_id, $fav_key, true); |
| | | |
| | | if (empty($favourited_ids)) { |
| | | // Force empty results |
| | | if (empty($favourites)) { |
| | | // No favourites - return empty result |
| | | $args['post__in'] = [0]; // Will return no results |
| | | return $args; |
| | | } |
| | | |
| | | $fav_ids = array_filter(array_map('intval', explode(',', $favourites))); |
| | | |
| | | if (empty($fav_ids)) { |
| | | $args['post__in'] = [0]; |
| | | return $args; |
| | | } |
| | | |
| | | $args['post__in'] = isset($args['post__in']) |
| | | ? array_intersect($args['post__in'], $favourited_ids) |
| | | : $favourited_ids; |
| | | $args['post__in'] = $fav_ids; |
| | | $args['orderby'] = 'post__in'; // Preserve favourite order |
| | | |
| | | return $args; |
| | | } |
| | |
| | | |
| | | $feedTypes = $this->buildFeedTypesConfig(); |
| | | |
| | | $response = new WP_REST_Response($feedTypes); |
| | | $response = $this->success($feedTypes); |
| | | return $this->addCacheHeaders($response); |
| | | } |
| | | |