From 48721c85ebcfa973ee81719d2467ca80e4253dc9 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Fri, 01 May 2026 17:30:03 +0000
Subject: [PATCH] =Edmonton Ink hard test begins! Real testing of the managers and reset routes will commence. So far, just ensuring our classes are all loaded correctly: Site() and its sub-classes Membership, Login, etc. Care should be taken to load conditionally on 'init', as we finish defining most settings by 'plugins_loaded' at priority 5
---
inc/rest/routes/FeedRoutes.php | 1921 +++++++++++++++++++++++++++++++--------------------------
1 files changed, 1,052 insertions(+), 869 deletions(-)
diff --git a/inc/rest/routes/FeedRoutes.php b/inc/rest/routes/FeedRoutes.php
index eb7024e..4efc099 100644
--- a/inc/rest/routes/FeedRoutes.php
+++ b/inc/rest/routes/FeedRoutes.php
@@ -1,10 +1,12 @@
<?php
namespace JVBase\rest\routes;
-use JVBase\rest\RestRouteManager;
+use JVBase\meta\Meta;
+use JVBase\registrar\Registrar;
+use JVBase\rest\Rest;
use JVBase\integrations\Umami;
-use JVBase\meta\MetaManager;
-use JVBase\managers\TaxonomyRelationships;
+use JVBase\rest\Route;
+use JVBase\base\Site;
use WP_Query;
use WP_Post;
use WP_Term;
@@ -14,1004 +16,1185 @@
if (!defined('ABSPATH')) {
exit; // Exit if accessed directly
}
-/**
- * FeedRoutes - Optimized API endpoints for the feed block
- * TODO: Look at Content Routes' setup and make this more like that one; it's a bit more organized
- * Or NewsRoutes
- */
-class FeedRoutes extends RestRouteManager
+
+class FeedRoutes extends Rest
{
- protected int $per_page = 36;
- protected Umami $tracker;
+ protected int $per_page = 36;
+ protected ?Umami $tracker = null;
- public function __construct()
- {
- $this->cache_name = 'feed';
- $this->cache_ttl = 86400;
+ protected ?array $fields = null;
+ protected ?array $timelineSharedFields = null;
+ protected ?array $timelineUniqueFields = null;
- if (jvbSiteUsesUmami()) {
- $this->tracker = JVB()->connect('umami');
- }
- parent::__construct();
- }
+ public function __construct()
+ {
+ $this->cacheName = 'feed';
+ $this->cacheTtl = 86400;
+ parent::__construct();
+ $this->cache
+ ->connect('post', true)
+ ->connect('taxonomy', true)
+ ->connect('user', true);
- /**
- * Registers feed routes
- * @return void
- */
- public function registerRoutes():void
- {
- register_rest_route($this->namespace, '/feed', [
- 'methods' => ['GET', 'POST'],
- 'callback' => [$this, 'handleFeedRequest'],
- 'permission_callback' => [$this, 'checkPermission'],
- ]);
- }
+ if (JVB_TESTING) {
+ $this->cache->flush();
+ }
- /**
- * Formats an item
- * @param int $postID
- *
- * @return array
- */
- protected function formatItem(int $postID, string $type = 'post'):array
- {
- switch ($type) {
- case 'post':
- $post = get_post($postID);
- $type = jvbNoBase($post->post_type);
- $metaType = 'post';
- break;
- default:
- $post = get_term($postID, jvbCheckBase($type));
- $type = jvbNoBase($type);
- $metaType = 'term';
- break;
- }
- if (!$post || is_wp_error($post)) {
- return [];
- }
- $formatted = $this->cache->get($postID, $type);
-// $formatted = false;
- if ($formatted) {
- return $formatted;
- }
+ }
+
+ public function init():void
+ {
+ if (Site::hasIntegration('umami')) {
+ $this->tracker = JVB()->connect('umami');
+ }
+ }
+
+ /**
+ * Registers feed routes
+ * @return void
+ */
+ public function registerRoutes(): void
+ {
+ 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)
+ ->register();
+
+ // Feed types endpoint
+ Route::for('feed/types')
+ ->get([$this, 'getFeedTypes'])
+ ->auth('public')
+ ->rateLimit()
+ ->register();
+ }
+
+ /**
+ * Formats an item
+ * @param int $postID
+ *
+ * @return array
+ */
+ protected function formatItem(int $postID, string $type = 'post', $skip = false): array
+ {
+ switch ($type) {
+ case 'post':
+ $post = get_post($postID);
+ $type = jvbNoBase($post->post_type);
+ $metaType = 'post';
+ break;
+ default:
+ $post = get_term($postID, jvbCheckBase($type));
+ $type = jvbNoBase($type);
+ $metaType = 'term';
+ break;
+ }
+ if (!$post || is_wp_error($post)) {
+ return [];
+ }
+
+ return $this->cache->remember(
+ $postID,
+ function() use ($postID, $type, $metaType, $post, $skip) {
+ $registrar = null;
+ switch ($metaType) {
+ case 'post':
+ $registrar = Registrar::getInstance($type);
+ $meta = Meta::forPost($postID);
+ if (!$skip && $registrar->isTimeline()) {
+ return $this->formatTimeline($postID, $post);
+ }
+ break;
+ case 'term':
+
+ $meta = Meta::forTerm($postID);
+ $registrar = Registrar::getInstance($type);
+ break;
+ case 'user':
+ $meta = Meta::forUser($postID);
+ $registrar = Registrar::getInstance($type);
+ break;
+ }
+ if (!$registrar) {
+ return [];
+ }
+ $fields = $registrar->getFields();
+
+ //Allow custom filtering for public fields
+ if (!empty($registrar->getConfig('feed')['fields'])) {
+ $fields = array_filter($fields, function($field) use ($registrar) {
+ return in_array($field, $registrar->getConfig('feed')['fields']);
+ }, ARRAY_FILTER_USE_KEY);
+ }
+
+ $values = $meta->getAll(array_keys($fields));
+
+ $out = [
+ 'fields' => $values,
+ ];
+
+ //Format Taxonomies
+ $out['taxonomies'] = $this->extractTaxonomies($values, $postID, $type);
+
+ //Add images
+ $imgIDs = [];
+ $temp = array_filter($fields, function($field) {
+ return in_array($field['type'], [ 'upload', 'image', 'gallery']);
+ });
+
+ foreach ($temp as $key => $config) {
+ if (array_key_exists($key, $out['fields']) && $out['fields'][$key] !== '') {
+ $IDs = array_map('absint', explode(',',$out['fields'][$key]));
+ foreach ($IDs as $ID) {
+ $imgIDs[$ID] = jvbImageData($ID);
+ }
+ }
+ }
+ $out['images'] = $imgIDs;
+
+ $out['id'] = $postID;
+ $out['content'] = $type;
+ $out['icon'] = $registrar->getIcon()??jvbDefaultIcon();
+ if ($out['icon'] === '') {
+ $out['icon'] = jvbDefaultIcon();
+ }
+
+ if ($this->tracker) {
+ $args = ($metaType === 'post') ? ['owner_id' => $post->post_author] : [];
+ $out['umami_view'] = $this->tracker->trackFeedView($postID, $type, $args);
+ $out['umami_fav'] = $this->tracker->trackFavouriteToggle($postID, $type, false);
+ $out['umami_click'] = $this->tracker->trackClick($postID, $type);
+ }
+
+ switch ($metaType) {
+ case 'term':
+
+ $owner = $registrar->hasFeature('is_content') ? $meta->get('owner') : null;
+ if (!is_null($owner)) {
+ $out['user_id'] = $owner;
+ }
+ $out['url'] = get_term_link($postID, $type);
+ $out['title'] = html_entity_decode($post->name);
+ break;
+ case 'post':
+ $out['date'] = $post->post_date;
+ $out['modified'] = $post->post_modified;
+ $out['user_id'] = (int)$post->post_author;
+ $out['url'] = get_the_permalink($postID);
+ $out['title']= get_the_title($postID);
+ break;
+ }
+ return $out;
+ }
+ );
+ }
- $fields = apply_filters(
- 'jvbFeedFields',
- [],
- $type
- );
- $meta = new MetaManager($postID, $metaType);
- $formatted = [
- 'id' => $postID,
- 'icon' => $type,
- ];
+ protected function initTimelineFields(string $content):void
+ {
+ $registrar = Registrar::getInstance($content);
+ if (!$registrar || !$registrar->hasFeature('is_timeline')){
+ return;
+ }
+ $this->fields = $registrar->getFields();
- if (jvbSiteUsesUmami()) {
- $args = ($metaType === 'post') ? [ 'owner_id' => $post->post_author] : [];
- $formatted['umami_view'] = $this->tracker->trackFeedView($postID, $type, $args);
- $formatted['umami_fav'] = $this->tracker->trackFavouriteToggle($postID, $type, false);
- }
- switch ($metaType) {
- case 'term':
- if (jvbSiteUsesUmami()) {
- $formatted['umami_click'] = $this->tracker->trackTaxonomyClick($postID, $type);
- }
- $owner = (in_array($type, jvbContentTaxonomies()) ? $meta->getValue('owner') : null);
- if (!is_null($owner)) {
- $formatted['user_id'] = $owner;
- }
- $formatted['url'] = get_term_link($postID, $type);
- break;
- default:
- $formatted = array_merge($formatted, [
- 'date' => $post->post_date,
- 'user_id' => (int)$post->post_author,
- 'url' => get_the_permalink($postID),
- ]);
- break;
- }
- $order = array_keys($fields);
- foreach ($fields as $field => $config) {
- $value = [];
- if ($field === 'umami_click') {
- if ($config === 'profile') {
- $formatted['umami_click'] = $this->tracker->trackProfileClick($postID, $type);
- }
- if ($config === 'contentTaxonomy') {
- $formatted['umami_click'] = $this->tracker->trackContentTaxonomyClick($postID, $type);
- }
- } else {
- if (array_key_exists('field', $config)) {
- if ($field === 'image' && array_key_exists('gallery', $config)) {
- //Array === post types
- if (is_array($config['gallery'])) {
- $posts = get_posts([
- 'post_type' => array_map(function ($item) { return BASE.$item; }, $config['gallery']),
- 'author' => $post->post_author,
- 'posts_per_page' => 5,
- 'orderby' => 'date',
- 'order' => 'DESC',
- ]);
- $formatted['content'] = array_map(function ($content) {
- return [
- 'url' => get_permalink($content->ID),
- 'title' => $content->post_title,
- 'image' => jvbImageData((int)get_post_thumbnail_id($content->ID)),
- ];
- }, $posts);
- } else {
- //String === $meta
- $ids = explode(',', $meta->getValue($config['gallery']));
- $formatted['content'] = array_map(function ($id) {
- return [
- 'image' => jvbImageData((int)$id)
- ];
- }, $ids);
- }
- }
- switch ($config['field']) {
- case 'post_author':
- $author = $this->getAuthorData($post);
- $value = [
- 'value' => $author['value'],
- 'url' => $author['url']
- ];
- break;
- case 'name':
- $value = $post->name;
- break;
- case 'post_title':
- $value = $post->post_title;
- break;
- case 'image':
- case 'image_portrait':
- case 'featured_image':
- $value = $meta->getValue($config['field']);
- $value = jvbImageData((int)$value);
- break;
- case 'top_style':
- case 'city':
- case 'top_theme':
- $terms = explode(',', $meta->getValue($field));
- $terms = array_filter(array_map(function ($termID) use ($config, $postID, $type) {
- $term = get_term($termID, jvbCheckBase($config['icon']));
- if ($term && !is_wp_error($term)) {
- return $this->formatTaxonomy($term, $postID, $type);
- }
- return [];
- }, $terms));
- $value = [
- 'terms' => $terms
- ];
- break;
- default:
- $value = [
- 'value' => $meta->getValue($field)
- ];
- }
+ $this->timelineSharedFields = array_keys(array_filter($this->fields, function ($field) {
+ if (!array_key_exists('for_all', $field) || $field['for_all'] === false){
+ return true;
+ }
+ return false;
+ }));
+ array_unshift($this->timelineSharedFields, 'post_thumbnail');
+ array_unshift($this->timelineSharedFields, 'post_title');
- } elseif (array_key_exists('taxonomy', $config)) {
- $terms = get_the_terms($postID, BASE.$config['taxonomy']);
- if ($terms && !is_wp_error($terms)) {
- $terms = array_map(function ($term) use ($postID, $type) {
- return $this->formatTaxonomy($term, $postID, $type);
- }, $terms);
- $value = [
- 'terms' => $terms
- ];
- }
- }
+ $this->timelineUniqueFields = array_keys(array_filter($this->fields, function ($field) {
+ if (array_key_exists('for_all', $field) && $field['for_all'] === true) {
+ return true;
+ }
+ return false;
+ }));
+ }
- if (array_key_exists('label', $config)) {
- $value['label'] = $config['label'];
- }
- if (array_key_exists('icon', $config)) {
- $value['icon'] = $config['icon'];
- }
- $formatted[$field] = $value;
- }
- }
+ protected function formatTimeline(int $postID, WP_Post $post):array
+ {
+ if (!$this->timelineSharedFields || !$this->timelineUniqueFields){
+ $this->initTimelineFields($post->post_type);
+ }
+ $item = $this->formatItem($postID, 'post', true);
+ //Step 1: Get the fields that apply to all posts
+ $mainMeta = Meta::forPost($post->ID);
+ $item['fields'] = $mainMeta->getAll($this->timelineSharedFields);
- $formatted['order'] = $order;
- $this->cache->set($postID, $formatted, $type);
+ //Step 2: Get the fields for each individual posts
+ $children = get_children(['post_parent' => $post->ID, 'orderby' => 'date', 'order' => 'ASC', 'post_status' => ['publish'], 'fields'=> 'ids']);
+ array_unshift($children, $post->ID);
- return $formatted;
- }
+ $item['taxonomies'] = $this->extractTaxonomies($item['fields'], $postID, jvbNoBase($post->post_type));
- protected function formatTaxonomy(WP_Term $term, int $postID, string $type)
- {
- return [
- 'ID' => $term->term_id,
- 'title' => htmlspecialchars_decode($term->name),
- 'url' => get_term_link($term->term_id, $term->taxonomy),
- 'umami_click' => $this->tracker->trackTaxonomyClick($term->term_id, $term->taxonomy, [
- 'from' => $type.'_'.$postID
- ])
- ];
- }
- protected function getAuthorData(WP_Post $post)
- {
- $author = $this->cache->get($post->post_author, 'author_data');
- if (!$author) {
- $author = [
- 'id' => $post->post_author,
- 'label' => 'Artist',
- 'value' => get_the_author_meta('display_name', $post->post_author),
- 'icon' => 'artist',
- 'url' => get_the_permalink(get_user_meta($post->post_author, BASE.'link', true)),
- ];
- $this->cache->set($post->post_author, $author, 'author_data');
- }
- return $author;
- }
+ $subFields = [];
+ $images = [];
+ foreach ($children as $child) {
+ $meta = Meta::forPost($child);
+ $f = $meta->getAll($this->timelineUniqueFields);
+ $f = ['id' => $child] + $f;
+ $subFields[] = $f;
+ $item['taxonomies'] = array_merge($item['taxonomies'], $this->extractTaxonomies($f, $postID, jvbNoBase($post->post_type)));
+ $images[$f['post_thumbnail']] = jvbImageData((int) $f['post_thumbnail']);
+ }
+ $item['number'] = (int)get_post_meta($post->ID,BASE.'number', true);
+ $item['fields']['before'] = get_post_thumbnail_id($children[0]);
+ $item['fields']['after'] = get_post_thumbnail_id($children[array_key_last($children)]);
- protected function getTaxonomies(int $postID, string $content):array
- {
- $taxonomies = jvbTaxonomiesForContent($content);
- $out = [];
- foreach ($taxonomies as $tax) {
- $terms = get_the_terms($postID, $tax);
- $t = [];
- if ($terms && !is_wp_error($terms)) {
- $config = jvbNoBase($tax);
- $out[] = [
- 'icon' => $config,
- 'title' => JVB_TAXONOMY[$config]['plural'],
- 'terms' => array_map(function ($term) use ($tax, $postID, $content) {
- return [
- 'ID' => $term->term_id,
- 'title' => htmlspecialchars_decode($term->name),
- 'url' => get_term_link($term->term_id, $tax),
- 'umami_click' => $this->tracker->trackTaxonomyClick($term->term_id, $tax, [
- 'from' => $content.'_'.$postID
- ])
- ];
- }, $terms),
- ];
- }
- }
- return $out;
- }
+ $item['fields']['timeline'] = $subFields;
+ $item['images'] = $item['images'] + $images;
- protected function buildRequestArgs(WP_REST_Request $request):array
- {
- global $jvb_everything;
- $data = $request->get_params();
- error_log('Feed Request: '.print_r($data, true));
- $args = [
- 'post_type' => (array_key_exists($data['content'], $jvb_everything)) ?
- BASE.$data['content'] :
- BASE.array_key_first(JVB_CONTENT),
- 'paged' => intval($data['page'] ?? 1),
- 'posts_per_page' => $this->per_page,
- ];
- if (!empty($data['context'])) {
- $args = $this->applyContextFilters(
- $args,
- [
- 'id' => $data['source'],
- 'type'=>$data['context']
- ]
- );
- }
- $args = $this->applyContextFilters($args, $data);
- $args = $this->applyTaxonomyFilters($args, $data);
- $args = $this->applyOrderFilters($args, $data);
- $args = $this->applyDateFilters($args, $data);
+ return $item;
+ }
+ protected function extractTaxonomies(array $fields, int $postID, string $content):array {
+ $taxonomies = [];
+ foreach ($fields as $key => $value) {
+ if (empty($value)) {
+ continue;
+ }
- $args = $this->applyFavouritesFilter($args, $data);
+ $registrar = Registrar::getInstance($key);
+ if (!$registrar || $registrar->registrar->public === false){
+ continue;
+ }
- return $args;
- }
- /**
- * @param WP_REST_Request $request
- *
- * @return WP_REST_Response
- */
- public function handleFeedRequest(WP_REST_Request $request):WP_REST_Response
- {
- $args = $this->buildRequestArgs($request);
+ $terms = array_map('absint', explode(',', $value));
+ $terms = array_filter($terms); // Remove 0 values
- error_log('Final Args: '.print_r($args, true));
+ if (empty($terms)) {
+ continue;
+ }
+ foreach($terms as $termID) {
+ $term = get_term($termID, jvbCheckBase($key));
+ if ($term && !is_wp_error($term)) {
+ $taxonomies[$key][$termID] = $this->formatTaxonomy($term, $postID, $content);
+ }
+ }
+ }
+ return $taxonomies;
+ }
+
+ protected function formatTaxonomy(WP_Term|int $term, int $postID, string $type)
+ {
+ return $this->cache->remember(
+ $term->term_id,
+ function () use ($term, $postID, $type) {
+ $base = [
+ 'ID' => $term->term_id,
+ 'title' => html_entity_decode($term->name),
+ 'url' => get_term_link($term->term_id, $term->taxonomy),
+ ];
+ if ($this->tracker) {
+ $base['umami_click'] =$this->tracker->trackTaxonomyClick($term->term_id, $term->taxonomy, [
+ 'from' => $type . '_' . $postID
+ ]);
+ }
+ return $base;
+ }
+ );
+ }
+
+ protected function getAuthorData(WP_Post $post)
+ {
+ $author = $post->post_author;
+ $userLink = get_user_meta($author, BASE.'profile_link', true);
+ return $this->cache->remember(
+ $userLink,
+ function () use ($userLink, $author) {
+ $label = jvbUserRole($author);
+ $registrar = Registrar::getInstance($label);
+ if ($registrar) {
+ $label = $registrar->getSingular();
+ } else {
+ $label = 'Artist';
+ }
+ return [
+ 'id' => $userLink,
+ 'label' => $label,
+ 'value' => get_the_title($userLink),
+ 'icon' => 'user',
+ 'url' => get_the_permalink($userLink),
+ ];
+ }
+ );
+ }
+
+ protected function getTaxonomies(int $postID, string $content): array
+ {
+ $registrar = Registrar::getInstance($content)??false;
+ $taxonomies = $registrar->registrar->taxonomies;
+ $out = [];
+ foreach ($taxonomies as $tax) {
+ $terms = get_the_terms($postID, $tax);
+ $t = [];
+ if ($terms && !is_wp_error($terms)) {
+ $config = Registrar::getInstance($tax);
+ $out[] = [
+ 'icon' => $config->getIcon(),
+ 'title' => $config->getPlural(),
+ 'terms' => array_map(function ($term) use ($tax, $postID, $content) {
+ $item = $this->cache->remember(
+ $term->term_id,
+ function() use ($term, $tax, $content, $postID) {
+ return [
+ 'ID' => $term->term_id,
+ 'title' => html_entity_decode($term->name),
+ 'url' => get_term_link($term->term_id, $tax),
+ ];
+ }
+ );
+ $item['umami_click'] = $this->tracker->trackTaxonomyClick($term->term_id, $tax, [
+ 'from' => $content.'_'.$postID
+ ]);
+ return $item;
+ }, $terms),
+ ];
+
+ }
+ }
+ return $out;
+ }
- $key = $this->cache->generateKey($args);
- $cached = $this->cache->get($key);
- if ($cached) {
- if ($request->get_param('highlight')) {
- $highlight = json_decode($request->get_param('highlight'), true);
- $args['highlight'] = $highlight;
- }
- $cached['items'] = $this->processHighlightedItem($cached['items'], $args);
- return new WP_REST_Response($cached);
- }
- // Fetch and format items
- $items = $this->fetchFeedItems($args);
+ protected function buildRequestArgs(WP_REST_Request $request): array
+ {
+ $data = $request->get_params();
+ $args = [
+ 'post_type' => (array_key_exists($data['content'], $this->buildFeedTypesConfig())) ?
+ jvbCheckBase($data['content']) : null,
+ 'paged' => intval($data['page'] ?? 1),
+ 'posts_per_page' => $this->per_page,
+ ];
+ if (!empty($data['context'])) {
+ $args = $this->applyContextFilters(
+ $args,
+ [
+ 'id' => $data['source']??'0',
+ 'type' => $data['context']
+ ]
+ );
+ }
+ if (array_key_exists('taxonomy', $data) && is_string($data['taxonomy'])) {
+ $data['taxonomy'] = json_decode($data['taxonomy'], true);
+ }
+ $args = $this->applyContextFilters($args, $data);
+ $args = $this->applyTaxonomyFilters($args, $data);
+ $args = $this->applyOrderFilters($args, $data);
+ $args = $this->applyDateFilters($args, $data);
+ return $this->applyFavouritesFilter($args, $data);
+ }
- $ttl = (str_contains($args['orderby'], 'RAND')) ? 1800 : $this->cache_ttl;
- $this->cache->set($key, $items, $ttl);
+ /**
+ * @param WP_REST_Request $request
+ *
+ * @return WP_REST_Response
+ */
+ public function handleFeedRequest(WP_REST_Request $request): WP_REST_Response
+ {
+ $args = $this->buildRequestArgs($request);
+ $key = $this->cache->generateKey($args);
- if ($request->get_param('highlight')) {
- $highlight = json_decode($request->get_param('highlight'), true);
- $args['highlight'] = $highlight;
- }
+ // Check HTTP cache headers first
+ $cache_check = $this->checkHeaders(
+ $request,
+ $key
+ );
+ if ($cache_check) {
+ return $cache_check; // Returns 304 Not Modified
+ }
- $items['items'] = $this->processHighlightedItem($items['items'], $args);
- return new WP_REST_Response($items);
- }
+ $cached = $this->cache->get($key);
+ if ($cached) {
+ if ($request->get_param('highlight')) {
+ $highlight = json_decode($request->get_param('highlight'), true);
+ $args['highlight'] = $highlight;
+ }
+ $cached['items'] = $this->processHighlightedItem($cached['items'], $args);
+ $response = $this->success($cached);
+ return $this->addCacheHeaders($response);
+ }
+ // Fetch and format items
+ $items = $this->fetchFeedItems($args);
- /**
- * @param array $args Formatted Args for WP_Query
- * @param array $data parsed Request Data
- *
- * @return array|null
- */
- protected function processHighlightedItem(array $items, array $data):array
- {
- error_log('Data passed to processHighlightedItem:'.print_r($data, true));
- if (empty($data['highlight']??null)) {
- return $items;
- }
+ $ttl = (str_contains($args['orderby'], 'RAND')) ? 300 : $this->cacheTtl;
+ $this->cache->set($key, $items, $ttl);
- // Convert to array if string
- if (is_string($data['highlight'])) {
- $data['highlight'] = json_decode($data['highlight'], true);
- }
+ if ($request->get_param('highlight')) {
+ $highlight = json_decode($request->get_param('highlight'), true);
+ $args['highlight'] = $highlight;
+ }
- // Extract key and value
- $key = array_keys($data['highlight'])[0] ?? false;
- $value = array_values($data['highlight'])[0] ?? false;
- error_log('Highlighted item: '.$key);
- error_log('Highlighted item: '.$value);
- error_log('No Single Content Types: '.print_r(jvbNoSingleContentTypes(), true));
- error_log('Page: '.print_r($data['paged'], true));
- if (in_array($key, jvbNoSingleContentTypes()) && $value && $data['paged'] === 1) {
- error_log('Formatted Highlighted item: '.print_r($this->formatItem($value), true));
- error_log('Items: '.print_r($items, true));
- array_unshift($items, $this->formatItem($value));
- error_log('Items after unshift: '.print_r($items, true));
- }
- return $items;
- }
+ $items['items'] = $this->processHighlightedItem($items['items'], $args);
+ $response = $this->success($items);
+ return $this->addCacheHeaders($response);
+ }
- /**
- * @param array $args
- * @param array $context
- *
- * @return array
- */
- protected function applyContextFilters(array $args, array $context):array
- {
+ /**
+ * @param array $items Formatted Args for WP_Query
+ * @param array $data parsed Request Data
+ *
+ * @return array|null
+ */
+ protected function processHighlightedItem(array $items, array $data): array
+ {
+ if (empty($data['highlight'] ?? null)) {
+ return $items;
+ }
- error_log('Args at Context Filters: '.print_r($args, true));
- error_log('Request at Context Filters: '.print_r($context, true));
- if (!isset($context['type'])) {
- return $args;
- }
+ // Convert to array if string
+ if (is_string($data['highlight'])) {
+ $data['highlight'] = json_decode($data['highlight'], true);
+ }
- switch (true) {
- case contentIsJVBUserType($context['type']):
- $args['author'] = (int)get_post_meta($context['id'], BASE.'link', true);
- break;
- case taxIsJVBContentTax($context['type']):
- $args['post_type'] = (is_array($args['post_type'])) ? $args['post_type'] : explode(',',$args['post_type']);
- if (array_intersect($args['post_type'], array_map(function ($type) { return jvbCheckBase($type); },array_keys(jvbGlobalFeedContent())))) {
- $artists = jvbGetContentUsers($context['id']);
- if (!empty($artists)) {
- $args['author__in'] = $artists;
- }
- } 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'],
- ]
- ];
- break;
- }
- return $args;
- }
+ // Extract key and value
+ $key = array_keys($data['highlight'])[0] ?? false;
+ $value = array_values($data['highlight'])[0] ?? false;
+ if ($key && $data['paged'] === 1) {
+ array_unshift($items, $this->formatItem($value));
+ }
+ return $items;
+ }
- /**
- * @param array $args
- * @param array $filters
- *
- * @return array
- */
- protected function applyFavouritesFilter(array $args, array $filters):array
- {
- if (!array_key_exists('favourites', $filters)){
- return $args;
- }
- error_log('Proceeding to check for favourites:');
- global $wpdb;
+ /**
+ * @param array $args
+ * @param array $context
+ *
+ * @return array
+ */
+ protected function applyContextFilters(array $args, array $context): array
+ {
+ if (!isset($context['type'])) {
+ return $args;
+ }
- // Get post types for the current filter
- $post_types = explode(',', $args['post_type']);
+ $registrar = Registrar::getInstance($context['type']);
+ switch (true) {
+ case $registrar->hasFeature('profile_link'):
+ $args['author'] = (int)get_post_meta($context['id'], BASE . 'link', true);
+ break;
+ case $registrar->getType() === 'term' && $registrar->hasFeature('is_content'):
+ $args['post_type'] = is_array($args['post_type'])
+ ? $args['post_type']
+ : explode(',', $args['post_type']);
- $favourites_table = $wpdb->prefix . BASE . 'favourites';
- $placeholders = implode(',', array_fill(0, count($post_types), '%s'));
- error_log('CurrentUser ID: '.print_r(get_current_user_id(), true));
- $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
- )
- ));
+ // Check if filtering global feed content
+ if (in_array(jvbNoBase($context['type']), Registrar::getFeatured('is_content', 'term'))) {
+ // Global: show posts from any content type with this taxonomy
+ $for_content = Registrar::getInstance($context['type'])->registrar->for ?? [];
- if (empty($favourited_ids)) {
- // Force empty results
- $args['post__in'] = [0];
- return $args;
- }
+ // Convert to full post types with BASE prefix
+ $post_types = array_map(fn($type) => jvbCheckBase($type), $for_content);
- $args['post__in'] = isset($args['post__in'])
- ? array_intersect($args['post__in'], $favourited_ids)
- : $favourited_ids;
+ // Filter to only show_feed content types
+ $show_feed_types = Registrar::getFeatured('show_feed', 'post');
+ $args['post_type'] = array_intersect(
+ $post_types,
+ array_map(fn($type) => jvbCheckBase($type), $show_feed_types)
+ );
+ }
- return $args;
- }
+ // Add term to tax query
+ $args['tax_query'][] = [
+ 'taxonomy' => jvbCheckBase($context['type']),
+ 'field' => 'term_id',
+ 'terms' => [(int)$context['id']],
+ ];
+ break;
+ }
- /**
- * @param array $args
- *
- * @return array
- */
- protected function fetchFeedItems(array $args):array
- {
- if (in_array($args['post_type'], jvbContentTaxonomies())) {
- return $this->handleContentTaxonomies($args);
- }
- $args['fields'] = 'ids';
- // Get post IDs
- $query = new WP_Query($args);
+ return $args;
+ }
- // Batch prefetch related data
- update_meta_cache('post', $query->posts);
- update_object_term_cache($query->posts, $args['post_type']);
+ /**
+ * @param array $args
+ * @param array $filters
+ *
+ * @return array
+ */
+ protected function applyFavouritesFilter(array $args, array $data): array
+ {
+ if (empty($data['favourites']) || empty($data['user'])) {
+ return $args;
+ }
- // Format regular items
- $items = array_map(function ($post) {
- return $this->formatItem($post);
- }, $query->posts);
+ $user_id = (int)$data['user'];
+ $content = jvbNoBase($args['post_type']);
- wp_reset_postdata();
- return [
- 'items' => $items,
- 'has_more' => $query->max_num_pages > $args['paged'],
- 'total' => $query->found_posts
- ];
- }
+ // Get user's favourites for this content type
+ $fav_key = BASE . 'favourites_' . $content;
+ $favourites = get_user_meta($user_id, $fav_key, true);
- protected function handleContentTaxonomies(array $args):array
- {
+ if (empty($favourites)) {
+ // No favourites - return empty result
+ $args['post__in'] = [0]; // Will return no results
+ return $args;
+ }
- $taxonomy = jvbNoBase($args['post_type']);
- global $wpdb;
- $table = $wpdb->prefix.BASE.'content_'.$taxonomy;
+ $fav_ids = array_filter(array_map('intval', explode(',', $favourites)));
- // Check if table exists
- if ($wpdb->get_var("SHOW TABLES LIKE '{$table}'") !== $table) {
- return [
- 'items' => [],
- 'has_more' => false,
- 'total' => 0
- ];
- }
+ if (empty($fav_ids)) {
+ $args['post__in'] = [0];
+ return $args;
+ }
- // Build the query components
- $queryBuilder = $this->buildCustomTableQuery($args, $table, $taxonomy);
+ $args['post__in'] = $fav_ids;
+ $args['orderby'] = 'post__in'; // Preserve favourite order
- // Execute count query first
- $total = (int) $wpdb->get_var($queryBuilder['count_query']);
+ return $args;
+ }
- // Execute main query if we have results
- $items = [];
- if ($total > 0) {
- $results = $wpdb->get_results($queryBuilder['main_query'],ARRAY_A);
- $items = array_map(function ($ID) use ($taxonomy) {
- return $this->formatItem($ID['term_id'], $taxonomy);
- }, $results);
- }
+ /**
+ * @param array $args
+ *
+ * @return array
+ */
+ protected function fetchFeedItems(array $args): array
+ {
+ $postType = is_array($args['post_type']) ? $args['post_type'][0] : $args['post_type'];
+ $slug = jvbNoBase($postType);
+ $registrar = Registrar::getInstance($slug);
+ if ($registrar && $registrar->hasFeature('is_timeline')) {
+ $args['post_parent'] = 0;
+ }
+ if ($registrar && $registrar->hasFeature('is_content')) {
+ return $this->handleContentTaxonomies($args);
+ }
+ $args['fields'] = 'ids';
+ // Get post IDs
+ $query = new WP_Query($args);
- $page = $args['paged'] ?? 1;
- $per_page = $args['posts_per_page'] ?? $this->per_page;
- $has_more = ($page * $per_page) < $total;
+ // Batch prefetch related data
+ update_meta_cache('post', $query->posts);
+ update_object_term_cache($query->posts, $args['post_type']);
- return [
- 'items' => $items,
- 'has_more' => $has_more,
- 'total' => $total
- ];
- }
- /**
- * Build SQL query components for custom table
- * @param array $args WP_Query style arguments
- * @param string $table Table name
- * @param string $taxonomy Taxonomy type
- * @return array Query components
- */
- protected function buildCustomTableQuery(array $args, string $table, string $taxonomy): array
- {
- global $wpdb;
+ // Format regular items
+ $items = array_map(fn($post) => $this->formatItem($post), $query->posts);
- $where_conditions = ['1=1'];
- $joins = [];
- $params = [];
+ wp_reset_postdata();
+ return [
+ 'items' => $items,
+ 'has_more' => $query->max_num_pages > $args['paged'],
+ 'total' => $query->found_posts
+ ];
+ }
- // Handle search
- if (!empty($args['s'])) {
- $search = '%' . $wpdb->esc_like($args['s']) . '%';
- $where_conditions[] = "(ct.name LIKE %s OR ct.slug LIKE %s)";
- $params[] = $search;
- $params[] = $search;
- }
+ protected function handleContentTaxonomies(array $args): array
+ {
- // Handle context filters (e.g., filtering shops by style through relationships)
- if (!empty($args['context_filter'])) {
- $context_conditions = $this->buildContextConditions($args['context_filter'], $joins, $params, $taxonomy);
- if (!empty($context_conditions)) {
- $where_conditions[] = $context_conditions;
- }
- }
+ $taxonomy = jvbNoBase($args['post_type']);
+ global $wpdb;
+ $table = $wpdb->prefix . BASE . 'content_' . $taxonomy;
- // Handle taxonomy filters (tax_query)
- if (!empty($args['tax_query'])) {
- $tax_conditions = $this->buildTaxonomyConditions($args['tax_query'], $joins, $params, $taxonomy);
- if (!empty($tax_conditions)) {
- $where_conditions[] = $tax_conditions;
- }
- }
+ // Check if table exists
+ if ($wpdb->get_var("SHOW TABLES LIKE '{$table}'") !== $table) {
+ return [
+ 'items' => [],
+ 'has_more' => false,
+ 'total' => 0
+ ];
+ }
- // Handle meta queries (custom fields in the table)
- if (!empty($args['meta_query'])) {
- $meta_conditions = $this->buildMetaConditions($args['meta_query'], $joins, $params, $taxonomy);
- if (!empty($meta_conditions)) {
- $where_conditions[] = $meta_conditions;
- }
- }
+ // Build the query components
+ $queryBuilder = $this->buildCustomTableQuery($args, $table, $taxonomy);
- // Handle date queries
- if (!empty($args['date_query'])) {
- $date_conditions = $this->buildDateConditions($args['date_query'], $params);
- if (!empty($date_conditions)) {
- $where_conditions[] = $date_conditions;
- }
- }
+ // Execute count query first
+ $total = (int)$wpdb->get_var($queryBuilder['count_query']);
- // Handle specific IDs
- if (!empty($args['include'])) {
- $placeholders = implode(',', array_fill(0, count($args['include']), '%d'));
- $where_conditions[] = "ct.term_id IN ({$placeholders})";
- $params = array_merge($params, $args['include']);
- }
+ // Execute main query if we have results
+ $items = [];
+ if ($total > 0) {
+ $results = $wpdb->get_results($queryBuilder['main_query'], ARRAY_A);
+ $items = array_map(
+ fn($ID) => $this->formatItem($ID['term_id'], $taxonomy),
+ $results
+ );
+ }
- if (!empty($args['exclude'])) {
- $placeholders = implode(',', array_fill(0, count($args['exclude']), '%d'));
- $where_conditions[] = "ct.term_id NOT IN ({$placeholders})";
- $params = array_merge($params, $args['exclude']);
- }
+ $page = $args['paged'] ?? 1;
+ $per_page = $args['posts_per_page'] ?? $this->per_page;
+ $has_more = ($page * $per_page) < $total;
- // Build ORDER BY
- $order_by = $this->buildOrderBy($args, $taxonomy);
+ return [
+ 'items' => $items,
+ 'has_more' => $has_more,
+ 'total' => $total
+ ];
+ }
- // Build LIMIT
- $page = $args['paged'] ?? 1;
- $per_page = $args['posts_per_page'] ?? $this->per_page;
- $offset = ($page - 1) * $per_page;
+ /**
+ * Build SQL query components for custom table
+ * @param array $args WP_Query style arguments
+ * @param string $table Table name
+ * @param string $taxonomy Taxonomy type
+ * @return array Query components
+ */
+ protected function buildCustomTableQuery(array $args, string $table, string $taxonomy): array
+ {
+ global $wpdb;
- // Combine everything
- $joins_sql = !empty($joins) ? implode(' ', $joins) : '';
- $where_sql = implode(' AND ', $where_conditions);
+ $where_conditions = ['1=1'];
+ $joins = [];
+ $params = [];
- $base_query = "FROM {$table} ct
+ // Handle search
+ if (!empty($args['s'])) {
+ $search = '%' . $wpdb->esc_like($args['s']) . '%';
+ $where_conditions[] = "(ct.name LIKE %s OR ct.slug LIKE %s)";
+ $params[] = $search;
+ $params[] = $search;
+ }
+
+ // Handle context filters (e.g., filtering shops by style through relationships)
+ if (!empty($args['context_filter'])) {
+ $context_conditions = $this->buildContextConditions($args['context_filter'], $joins, $params, $taxonomy);
+ if (!empty($context_conditions)) {
+ $where_conditions[] = $context_conditions;
+ }
+ }
+
+ // Handle taxonomy filters (tax_query)
+ if (!empty($args['tax_query'])) {
+ $tax_conditions = $this->buildTaxonomyConditions($args['tax_query'], $joins, $params, $taxonomy);
+ if (!empty($tax_conditions)) {
+ $where_conditions[] = $tax_conditions;
+ }
+ }
+
+ // Handle meta queries (custom fields in the table)
+ if (!empty($args['meta_query'])) {
+ $meta_conditions = $this->buildMetaConditions($args['meta_query'], $joins, $params, $taxonomy);
+ if (!empty($meta_conditions)) {
+ $where_conditions[] = $meta_conditions;
+ }
+ }
+
+ // Handle date queries
+ if (!empty($args['date_query'])) {
+ $date_conditions = $this->buildDateConditions($args['date_query'], $params);
+ if (!empty($date_conditions)) {
+ $where_conditions[] = $date_conditions;
+ }
+ }
+
+ // Handle specific IDs
+ if (!empty($args['include'])) {
+ $placeholders = implode(',', array_fill(0, count($args['include']), '%d'));
+ $where_conditions[] = "ct.term_id IN ({$placeholders})";
+ $params = array_merge($params, $args['include']);
+ }
+
+ if (!empty($args['exclude'])) {
+ $placeholders = implode(',', array_fill(0, count($args['exclude']), '%d'));
+ $where_conditions[] = "ct.term_id NOT IN ({$placeholders})";
+ $params = array_merge($params, $args['exclude']);
+ }
+
+ // Build ORDER BY
+ $order_by = $this->buildOrderBy($args, $taxonomy);
+
+ // Build LIMIT
+ $page = $args['paged'] ?? 1;
+ $per_page = $args['posts_per_page'] ?? $this->per_page;
+ $offset = ($page - 1) * $per_page;
+
+ // Combine everything
+ $joins_sql = !empty($joins) ? implode(' ', $joins) : '';
+ $where_sql = implode(' AND ', $where_conditions);
+
+ $base_query = "FROM {$table} ct
LEFT JOIN {$wpdb->terms} t ON ct.term_id = t.term_id
LEFT JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
{$joins_sql}
WHERE {$where_sql}";
- $count_query = "SELECT COUNT(DISTINCT ct.term_id) {$base_query}";
+ $count_query = "SELECT COUNT(DISTINCT ct.term_id) {$base_query}";
- $main_query = "SELECT ct.term_id
+ $main_query = "SELECT ct.term_id
{$base_query}
{$order_by}
LIMIT %d OFFSET %d";
- // Add limit parameters
- $count_params = $params;
- $main_params = array_merge($params, [$per_page, $offset]);
+ // Add limit parameters
+ $count_params = $params;
+ $main_params = array_merge($params, [$per_page, $offset]);
- return [
- 'main_query' => $wpdb->prepare($main_query, $main_params),
- 'count_query' => $wpdb->prepare($count_query, $count_params)
- ];
- }
+ return [
+ 'main_query' => $wpdb->prepare($main_query, $main_params),
+ 'count_query' => $wpdb->prepare($count_query, $count_params)
+ ];
+ }
- /**
- * Build context-based filter conditions (e.g., shops filtered by style relationships)
- * @param array $context_filter Context filter data
- * @param array &$joins Reference to joins array
- * @param array &$params Reference to params array
- * @param string $taxonomy Current taxonomy type
- * @return string SQL condition
- */
- protected function buildContextConditions(array $context_filter, array &$joins, array &$params, string $taxonomy): string
- {
- global $wpdb;
+ /**
+ * Build context-based filter conditions (e.g., shops filtered by style relationships)
+ * @param array $context_filter Context filter data
+ * @param array &$joins Reference to joins array
+ * @param array &$params Reference to params array
+ * @param string $taxonomy Current taxonomy type
+ * @return string SQL condition
+ */
+ protected function buildContextConditions(array $context_filter, array &$joins, array &$params, string $taxonomy): string
+ {
+ global $wpdb;
- $context_type = $context_filter['type'] ?? '';
- $context_id = $context_filter['id'] ?? 0;
+ $context_type = $context_filter['type'] ?? '';
+ $context_id = $context_filter['id'] ?? 0;
- if (empty($context_type) || empty($context_id)) {
- return '';
- }
+ if (empty($context_type) || empty($context_id)) {
+ return '';
+ }
- $relationships_table = $wpdb->prefix . BASE . 'taxonomy_relationships';
+ $relationships_table = $wpdb->prefix . BASE . 'taxonomy_relationships';
- switch ($context_type) {
- case 'style':
- case 'theme':
- case 'pstyle':
- // For shops filtered by style/theme through relationships
- if ($taxonomy === 'shop') {
- $joins[] = "INNER JOIN {$relationships_table} tr ON ct.term_id = tr.term_id";
- $where_condition = "tr.related_term_id = %d AND tr.related_taxonomy = %s";
- $params[] = $context_id;
- $params[] = BASE . $context_type;
- return $where_condition;
- }
- break;
+ switch ($context_type) {
+ case 'style':
+ case 'theme':
+ case 'pstyle':
+ // For shops filtered by style/theme through relationships
+ if ($taxonomy === 'shop') {
+ $joins[] = "INNER JOIN {$relationships_table} tr ON ct.term_id = tr.term_id";
+ $where_condition = "tr.related_term_id = %d AND tr.related_taxonomy = %s";
+ $params[] = $context_id;
+ $params[] = BASE . $context_type;
+ return $where_condition;
+ }
+ break;
- case 'city':
- // Filter by city
- if (isset($this->getCustomTableFields($taxonomy)['city'])) {
- $params[] = $context_id;
- return "ct.city = %d";
- }
- break;
- }
+ case 'city':
+ // Filter by city
+ if (isset($this->getCustomTableFields($taxonomy)['city'])) {
+ $params[] = $context_id;
+ return "ct.city = %d";
+ }
+ break;
+ }
- return '';
- }
+ return '';
+ }
- /**
- * Build taxonomy filter conditions for custom table
- * @param array $tax_query Tax query array
- * @param array &$joins Reference to joins array
- * @param array &$params Reference to params array
- * @param string $taxonomy Current taxonomy type
- * @return string SQL condition
- */
- protected function buildTaxonomyConditions(array $tax_query, array &$joins, array &$params, string $taxonomy): string
- {
- global $wpdb;
+ /**
+ * Build taxonomy filter conditions for custom table
+ * @param array $tax_query Tax query array
+ * @param array &$joins Reference to joins array
+ * @param array &$params Reference to params array
+ * @param string $taxonomy Current taxonomy type
+ * @return string SQL condition
+ */
+ protected function buildTaxonomyConditions(array $tax_query, array &$joins, array &$params, string $taxonomy): string
+ {
+ global $wpdb;
- $conditions = [];
- $relation = $tax_query['relation'] ?? 'AND';
+ $conditions = [];
+ $relation = $tax_query['relation'] ?? 'AND';
- foreach ($tax_query as $key => $query) {
- if ($key === 'relation' || !is_array($query)) {
- continue;
- }
+ foreach ($tax_query as $key => $query) {
+ if ($key === 'relation' || !is_array($query)) {
+ continue;
+ }
- $query_taxonomy = $query['taxonomy'] ?? '';
- $terms = (array)($query['terms'] ?? []);
- $field = $query['field'] ?? 'term_id';
- $operator = $query['operator'] ?? 'IN';
+ $query_taxonomy = $query['taxonomy'] ?? '';
+ $terms = (array)($query['terms'] ?? []);
+ $field = $query['field'] ?? 'term_id';
+ $operator = $query['operator'] ?? 'IN';
- if (empty($query_taxonomy) || empty($terms)) {
- continue;
- }
+ if (empty($query_taxonomy) || empty($terms)) {
+ continue;
+ }
- // Check if this taxonomy field exists in our custom table
- $custom_fields = $this->getCustomTableFields($taxonomy);
- $taxonomy_clean = str_replace(BASE, '', $query_taxonomy);
+ // Check if this taxonomy field exists in our custom table
+ $custom_fields = $this->getCustomTableFields($taxonomy);
+ $taxonomy_clean = str_replace(BASE, '', $query_taxonomy);
- if (isset($custom_fields[$taxonomy_clean])) {
- // Field exists in custom table - direct query
- $field_column = "ct.{$taxonomy_clean}";
+ if (isset($custom_fields[$taxonomy_clean])) {
+ // Field exists in custom table - direct query
+ $field_column = "ct.{$taxonomy_clean}";
- if ($field === 'slug') {
- // Need to convert slugs to IDs first
- $term_ids = [];
- foreach ($terms as $slug) {
- $term = get_term_by('slug', $slug, $query_taxonomy);
- if ($term) {
- $term_ids[] = $term->term_id;
- }
- }
- $terms = $term_ids;
- }
+ if ($field === 'slug') {
+ // Need to convert slugs to IDs first
+ $term_ids = [];
+ foreach ($terms as $slug) {
+ $term = get_term_by('slug', $slug, $query_taxonomy);
+ if ($term) {
+ $term_ids[] = $term->term_id;
+ }
+ }
+ $terms = $term_ids;
+ }
- if (!empty($terms)) {
- $placeholders = implode(',', array_fill(0, count($terms), '%d'));
- $conditions[] = "{$field_column} {$operator} ({$placeholders})";
- $params = array_merge($params, $terms);
- }
- } else {
- // Need to join with term relationships
- $join_alias = "tr_{$key}";
- $joins[] = "LEFT JOIN {$wpdb->term_relationships} {$join_alias} ON ct.term_id = {$join_alias}.object_id";
- $joins[] = "LEFT JOIN {$wpdb->term_taxonomy} tt_{$key} ON {$join_alias}.term_taxonomy_id = tt_{$key}.term_taxonomy_id";
+ if (!empty($terms)) {
+ $placeholders = implode(',', array_fill(0, count($terms), '%d'));
+ $conditions[] = "{$field_column} {$operator} ({$placeholders})";
+ $params = array_merge($params, $terms);
+ }
+ } else {
+ // Need to join with term relationships
+ $join_alias = "tr_{$key}";
+ $joins[] = "LEFT JOIN {$wpdb->term_relationships} {$join_alias} ON ct.term_id = {$join_alias}.object_id";
+ $joins[] = "LEFT JOIN {$wpdb->term_taxonomy} tt_{$key} ON {$join_alias}.term_taxonomy_id = tt_{$key}.term_taxonomy_id";
- if ($field === 'slug') {
- $joins[] = "LEFT JOIN {$wpdb->terms} t_{$key} ON tt_{$key}.term_id = t_{$key}.term_id";
- $field_column = "t_{$key}.slug";
- $term_values = $terms; // Use slugs directly
- } else {
- $field_column = "tt_{$key}.term_id";
- $term_values = array_map('intval', $terms);
- }
+ if ($field === 'slug') {
+ $joins[] = "LEFT JOIN {$wpdb->terms} t_{$key} ON tt_{$key}.term_id = t_{$key}.term_id";
+ $field_column = "t_{$key}.slug";
+ $term_values = $terms; // Use slugs directly
+ } else {
+ $field_column = "tt_{$key}.term_id";
+ $term_values = array_map('intval', $terms);
+ }
- $placeholders = implode(',', array_fill(0, count($term_values), $field === 'slug' ? '%s' : '%d'));
- $taxonomy_condition = "tt_{$key}.taxonomy = %s";
- $terms_condition = "{$field_column} {$operator} ({$placeholders})";
+ $placeholders = implode(',', array_fill(0, count($term_values), $field === 'slug' ? '%s' : '%d'));
+ $taxonomy_condition = "tt_{$key}.taxonomy = %s";
+ $terms_condition = "{$field_column} {$operator} ({$placeholders})";
- $conditions[] = "({$taxonomy_condition} AND {$terms_condition})";
- $params[] = $query_taxonomy;
- $params = array_merge($params, $term_values);
- }
- }
+ $conditions[] = "({$taxonomy_condition} AND {$terms_condition})";
+ $params[] = $query_taxonomy;
+ $params = array_merge($params, $term_values);
+ }
+ }
- return !empty($conditions) ? '(' . implode(" {$relation} ", $conditions) . ')' : '';
- }
+ return !empty($conditions) ? '(' . implode(" {$relation} ", $conditions) . ')' : '';
+ }
- /**
- * Build meta query conditions for custom table fields
- * @param array $meta_query Meta query array
- * @param array &$joins Reference to joins array
- * @param array &$params Reference to params array
- * @param string $taxonomy Taxonomy type
- * @return string SQL condition
- */
- protected function buildMetaConditions(array $meta_query, array &$joins, array &$params, string $taxonomy): string
- {
- global $wpdb;
- $conditions = [];
- $relation = $meta_query['relation'] ?? 'AND';
+ /**
+ * Build meta query conditions for custom table fields
+ * @param array $meta_query Meta query array
+ * @param array &$joins Reference to joins array
+ * @param array &$params Reference to params array
+ * @param string $taxonomy Taxonomy type
+ * @return string SQL condition
+ */
+ protected function buildMetaConditions(array $meta_query, array &$joins, array &$params, string $taxonomy): string
+ {
+ global $wpdb;
+ $conditions = [];
+ $relation = $meta_query['relation'] ?? 'AND';
- // Get fields for this taxonomy to know which are in the custom table
- $custom_fields = $this->getCustomTableFields($taxonomy);
+ // Get fields for this taxonomy to know which are in the custom table
+ $custom_fields = $this->getCustomTableFields($taxonomy);
- foreach ($meta_query as $key => $query) {
- if ($key === 'relation' || !is_array($query)) {
- continue;
- }
+ foreach ($meta_query as $key => $query) {
+ if ($key === 'relation' || !is_array($query)) {
+ continue;
+ }
- $meta_key = $query['key'] ?? '';
- $meta_value = $query['value'] ?? '';
- $compare = $query['compare'] ?? '=';
+ $meta_key = $query['key'] ?? '';
+ $meta_value = $query['value'] ?? '';
+ $compare = $query['compare'] ?? '=';
- if (empty($meta_key)) {
- continue;
- }
+ if (empty($meta_key)) {
+ continue;
+ }
- // Remove BASE prefix if present
- $clean_key = str_replace(BASE, '', $meta_key);
+ // Remove BASE prefix if present
+ $clean_key = str_replace(BASE, '', $meta_key);
- // Check if this field exists in our custom table
- if (isset($custom_fields[$clean_key])) {
- // Field is in custom table, query directly
- $column = "ct.{$clean_key}";
- $condition = $this->buildMetaComparison($column, $meta_value, $compare, $params);
- if ($condition) {
- $conditions[] = $condition;
- }
- } else {
- // Field is in term meta, need to join
- $join_alias = "tm_{$key}";
- $joins[] = "LEFT JOIN {$wpdb->termmeta} {$join_alias} ON ct.term_id = {$join_alias}.term_id AND {$join_alias}.meta_key = %s";
- $params[] = $meta_key;
+ // Check if this field exists in our custom table
+ if (isset($custom_fields[$clean_key])) {
+ // Field is in custom table, query directly
+ $column = "ct.{$clean_key}";
+ $condition = $this->buildMetaComparison($column, $meta_value, $compare, $params);
+ if ($condition) {
+ $conditions[] = $condition;
+ }
+ } else {
+ // Field is in term meta, need to join
+ $join_alias = "tm_{$key}";
+ $joins[] = "LEFT JOIN {$wpdb->termmeta} {$join_alias} ON ct.term_id = {$join_alias}.term_id AND {$join_alias}.meta_key = %s";
+ $params[] = $meta_key;
- $column = "{$join_alias}.meta_value";
- $condition = $this->buildMetaComparison($column, $meta_value, $compare, $params);
- if ($condition) {
- $conditions[] = $condition;
- }
- }
- }
+ $column = "{$join_alias}.meta_value";
+ $condition = $this->buildMetaComparison($column, $meta_value, $compare, $params);
+ if ($condition) {
+ $conditions[] = $condition;
+ }
+ }
+ }
- return !empty($conditions) ? '(' . implode(" {$relation} ", $conditions) . ')' : '';
- }
+ return !empty($conditions) ? '(' . implode(" {$relation} ", $conditions) . ')' : '';
+ }
- /**
- * Build comparison condition for meta fields
- * @param string $column Column name
- * @param mixed $value Value to compare
- * @param string $compare Comparison operator
- * @param array &$params Reference to params array
- * @return string SQL condition
- */
- protected function buildMetaComparison(string $column, $value, string $compare, array &$params): string
- {
- switch (strtoupper($compare)) {
- case 'LIKE':
- $params[] = '%' . $value . '%';
- return "{$column} LIKE %s";
+ /**
+ * Build comparison condition for meta fields
+ * @param string $column Column name
+ * @param mixed $value Value to compare
+ * @param string $compare Comparison operator
+ * @param array &$params Reference to params array
+ * @return string SQL condition
+ */
+ protected function buildMetaComparison(string $column, $value, string $compare, array &$params): string
+ {
+ switch (strtoupper($compare)) {
+ case 'LIKE':
+ $params[] = '%' . $value . '%';
+ return "{$column} LIKE %s";
- case 'NOT LIKE':
- $params[] = '%' . $value . '%';
- return "{$column} NOT LIKE %s";
+ case 'NOT LIKE':
+ $params[] = '%' . $value . '%';
+ return "{$column} NOT LIKE %s";
- case 'IN':
- if (is_array($value)) {
- $placeholders = implode(',', array_fill(0, count($value), '%s'));
- $params = array_merge($params, $value);
- return "{$column} IN ({$placeholders})";
- }
- break;
+ case 'IN':
+ if (is_array($value)) {
+ $placeholders = implode(',', array_fill(0, count($value), '%s'));
+ $params = array_merge($params, $value);
+ return "{$column} IN ({$placeholders})";
+ }
+ break;
- case 'NOT IN':
- if (is_array($value)) {
- $placeholders = implode(',', array_fill(0, count($value), '%s'));
- $params = array_merge($params, $value);
- return "{$column} NOT IN ({$placeholders})";
- }
- break;
+ case 'NOT IN':
+ if (is_array($value)) {
+ $placeholders = implode(',', array_fill(0, count($value), '%s'));
+ $params = array_merge($params, $value);
+ return "{$column} NOT IN ({$placeholders})";
+ }
+ break;
- case 'BETWEEN':
- if (is_array($value) && count($value) === 2) {
- $params[] = $value[0];
- $params[] = $value[1];
- return "{$column} BETWEEN %s AND %s";
- }
- break;
+ case 'BETWEEN':
+ if (is_array($value) && count($value) === 2) {
+ $params[] = $value[0];
+ $params[] = $value[1];
+ return "{$column} BETWEEN %s AND %s";
+ }
+ break;
- case '!=':
- case '<>':
- $params[] = $value;
- return "{$column} != %s";
+ case '!=':
+ case '<>':
+ $params[] = $value;
+ return "{$column} != %s";
- case '>':
- $params[] = $value;
- return "{$column} > %s";
+ case '>':
+ $params[] = $value;
+ return "{$column} > %s";
- case '>=':
- $params[] = $value;
- return "{$column} >= %s";
+ case '>=':
+ $params[] = $value;
+ return "{$column} >= %s";
- case '<':
- $params[] = $value;
- return "{$column} < %s";
+ case '<':
+ $params[] = $value;
+ return "{$column} < %s";
- case '<=':
- $params[] = $value;
- return "{$column} <= %s";
+ case '<=':
+ $params[] = $value;
+ return "{$column} <= %s";
- case '=':
- default:
- $params[] = $value;
- return "{$column} = %s";
- }
+ case '=':
+ default:
+ $params[] = $value;
+ return "{$column} = %s";
+ }
- return '';
- }
+ return '';
+ }
- /**
- * Build date query conditions
- * @param array $date_query Date query array
- * @param array &$params Reference to params array
- * @return string SQL condition
- */
- protected function buildDateConditions(array $date_query, array &$params): string
- {
- $conditions = [];
+ /**
+ * Build date query conditions
+ * @param array $date_query Date query array
+ * @param array &$params Reference to params array
+ * @return string SQL condition
+ */
+ protected function buildDateConditions(array $date_query, array &$params): string
+ {
+ $conditions = [];
- foreach ($date_query as $query) {
- if (!is_array($query)) continue;
+ foreach ($date_query as $query) {
+ if (!is_array($query)) continue;
- $column = $query['column'] ?? 'updated_at';
- $year = $query['year'] ?? null;
- $month = $query['month'] ?? null;
- $day = $query['day'] ?? null;
- $after = $query['after'] ?? null;
- $before = $query['before'] ?? null;
+ $column = $query['column'] ?? 'updated_at';
+ $year = $query['year'] ?? null;
+ $month = $query['month'] ?? null;
+ $day = $query['day'] ?? null;
+ $after = $query['after'] ?? null;
+ $before = $query['before'] ?? null;
- if ($year) {
- $params[] = $year;
- $conditions[] = "YEAR(ct.{$column}) = %d";
- }
+ if ($year) {
+ $params[] = $year;
+ $conditions[] = "YEAR(ct.{$column}) = %d";
+ }
- if ($month) {
- $params[] = $month;
- $conditions[] = "MONTH(ct.{$column}) = %d";
- }
+ if ($month) {
+ $params[] = $month;
+ $conditions[] = "MONTH(ct.{$column}) = %d";
+ }
- if ($day) {
- $params[] = $day;
- $conditions[] = "DAY(ct.{$column}) = %d";
- }
+ if ($day) {
+ $params[] = $day;
+ $conditions[] = "DAY(ct.{$column}) = %d";
+ }
- if ($after) {
- $params[] = $after;
- $conditions[] = "ct.{$column} > %s";
- }
+ if ($after) {
+ $params[] = $after;
+ $conditions[] = "ct.{$column} > %s";
+ }
- if ($before) {
- $params[] = $before;
- $conditions[] = "ct.{$column} < %s";
- }
- }
+ if ($before) {
+ $params[] = $before;
+ $conditions[] = "ct.{$column} < %s";
+ }
+ }
- return !empty($conditions) ? '(' . implode(' AND ', $conditions) . ')' : '';
- }
+ return !empty($conditions) ? '(' . implode(' AND ', $conditions) . ')' : '';
+ }
- /**
- * Build ORDER BY clause
- * @param array $args Query arguments
- * @param string $taxonomy Taxonomy type
- * @return string ORDER BY clause
- */
- protected function buildOrderBy(array $args, string $taxonomy): string
- {
- $orderby = $args['orderby'] ?? 'name';
- $order = $args['order'] ?? 'ASC';
+ /**
+ * Build ORDER BY clause
+ * @param array $args Query arguments
+ * @param string $taxonomy Taxonomy type
+ * @return string ORDER BY clause
+ */
+ protected function buildOrderBy(array $args, string $taxonomy): string
+ {
+ $orderby = $args['orderby'] ?? 'name';
+ $order = $args['order'] ?? 'ASC';
- // Validate order direction
- if (!in_array($order, ['ASC', 'DESC'])) {
- $order = 'ASC';
- }
+ // Validate order direction
+ if (!in_array($order, ['ASC', 'DESC'])) {
+ $order = 'ASC';
+ }
- if (str_contains($orderby, 'RAND')) {
- return "ORDER BY {$orderby}";
- }
+ if (str_contains($orderby, 'RAND')) {
+ return "ORDER BY {$orderby}";
+ }
- switch ($orderby) {
- case 'name':
- return "ORDER BY ct.name {$order}";
+ switch ($orderby) {
+ case 'name':
+ return "ORDER BY ct.name {$order}";
- case 'count':
- return "ORDER BY tt.count {$order}";
+ case 'count':
+ return "ORDER BY tt.count {$order}";
- case 'term_id':
- case 'id':
- return "ORDER BY ct.term_id {$order}";
+ case 'term_id':
+ case 'id':
+ return "ORDER BY ct.term_id {$order}";
- case 'slug':
- return "ORDER BY t.slug {$order}";
+ case 'slug':
+ return "ORDER BY t.slug {$order}";
- case 'date':
- case 'updated':
- return "ORDER BY ct.updated_at {$order}";
+ case 'date':
+ case 'updated':
+ return "ORDER BY ct.updated_at {$order}";
- default:
- // Check if it's a custom field in our table
- $custom_fields = $this->getCustomTableFields($taxonomy);
- if (isset($custom_fields[$orderby])) {
- return "ORDER BY ct.{$orderby} {$order}";
- }
+ default:
+ // Check if it's a custom field in our table
+ $custom_fields = $this->getCustomTableFields($taxonomy);
+ if (isset($custom_fields[$orderby])) {
+ return "ORDER BY ct.{$orderby} {$order}";
+ }
- // Default to name
- return "ORDER BY ct.name {$order}";
- }
- }
+ // Default to name
+ return "ORDER BY ct.name {$order}";
+ }
+ }
- /**
- * Get custom table fields for a taxonomy
- * @param string $taxonomy Taxonomy type
- * @return array Field definitions
- */
- protected function getCustomTableFields(string $taxonomy): array
- {
- return jvbContentTaxonomiesTableFields($taxonomy)['fields'] ?? [];
- }
+
+ /**
+ * Get available feed types (for block editor)
+ * Returns structured data about content types that can be shown in feed
+ */
+ public function getFeedTypes(WP_REST_Request $request): WP_REST_Response
+ {
+ // Check HTTP cache
+ $cache_check = $this->checkHeaders($request, 'feed_types');
+ if ($cache_check) {
+ return $cache_check;
+ }
+
+ $feedTypes = $this->buildFeedTypesConfig();
+
+ $response = $this->success($feedTypes);
+ return $this->addCacheHeaders($response);
+ }
+
+ public function getFeedTypesConfig():array
+ {
+ return $this->buildFeedTypesConfig();
+ }
+ /**
+ * Build feed types configuration from Features
+ */
+ protected function buildFeedTypesConfig(): array
+ {
+ return $this->cache->remember(
+ 'contentTypes',
+ function () {
+ $config = [];
+
+ // Get content types with show_feed
+ $contentTypes = Registrar::getFeatured('show_feed', 'post');
+ foreach ($contentTypes as $slug) {
+ $this->cache->tag('content:'.$slug);
+ $registrar = Registrar::getInstance($slug);
+ if (!$registrar) continue;
+
+ $config[$slug] = [
+ 'type' => 'content',
+ 'singular' => $registrar->getSingular(),
+ 'plural' => $registrar->getPlural(),
+ 'icon' => $registrar->getIcon(),
+ 'taxonomies' => $registrar->registrar->taxonomies,
+ ];
+ }
+
+ // Get taxonomies with show_feed (content taxonomies)
+ $taxonomies = Registrar::getFeatured('show_feed', 'term');
+ foreach ($taxonomies as $slug) {
+ $registrar = Registrar::getInstance($slug);
+ if (!$registrar || !($registrar->hasFeature('is_content') ?? false)) {
+ continue;
+ }
+
+ $this->cache->tag('taxonomy:'.$slug);
+
+ $config[$slug] = [
+ 'type' => 'taxonomy',
+ 'singular' => $registrar->getSingular(),
+ 'plural' => $registrar->getPlural(),
+ 'icon' => $registrar->getIcon(),
+ 'taxonomies' => [], // Content taxonomies don't have sub-taxonomies
+ 'for_content' => $registrar->registrar->for ?? []
+ ];
+ }
+
+ return $config;
+ });
+ }
}
--
Gitblit v1.10.0