Jake Vanderwerf
5 days ago a9b3b28d001941921aa70d37fdc87c758a163a44
inc/rest/routes/TermRoutes.php
@@ -1,11 +1,11 @@
<?php
namespace JVBase\rest\routes;
use JVBase\JVB;
use JVBase\rest\RestRouteManager;
use JVBase\managers\TaxonomyRelationships;
use JVBase\registrar\Registrar;
use JVBase\rest\Rest;
use JVBase\managers\UserTermsManager;
use JVBase\utility\Features;
use JVBase\rest\Route;
use JVBase\base\Site;
use WP_REST_Request;
use WP_REST_Response;
use Exception;
@@ -13,16 +13,19 @@
if (!defined('ABSPATH')) {
    exit; // Exit if accessed directly
}
class TermRoutes extends RestRouteManager
class TermRoutes extends Rest
{
    protected object $term_index_manager;
    protected int $per_page;
    public function __construct()
    {
        $this->cache_name = 'terms';
        $this->cacheName = 'terms';
        parent::__construct();
//    $this->cache->invalidateGroup('terms');
      if (JVB_TESTING) {
         $this->cache->flush();
      }
      $this->cache->connect('taxonomy', true);
        $this->per_page = 20;
        add_action('edited_term', [$this, 'deleteTermPath']);
@@ -35,60 +38,31 @@
     */
    public function registerRoutes():void
    {
        register_rest_route($this->namespace, '/terms', [
            [
                'methods'   => 'GET',
                'callback'  => [$this, 'handleTermSelectionRequest'],
                'permission_callback'   => [$this, 'checkPermission'],
                'args' => [
                    'page'  => [
                        'required'  => true,
                        'type'      => 'integer',
                        'default'   => 1,
                    ],
                    'taxonomy' => [
                        'required' => true,
                        'type' => 'string'
                    ],
                    'search' => [
                        'required' => false,
                        'type' => 'string'
                    ],
                    'parent' => [
                        'required' => false,
                        'type' => 'integer',
                        'default'   => 0
                    ]
                ]
            ],
            [
                'methods'   => 'POST',
                'callback' => [$this, 'createTermRequest'],
                'permission_callback' => [$this, 'checkPermission'],
                'args' => [
                    'taxonomy' => [
                        'required' => true,
                        'type' => 'string'
                    ],
                    'name' => [
                        'required' => true,
                        'type' => 'string'
                    ],
                    'parent' => [
                        'required' => false,
                        'type' => 'integer',
                        'default' => 0
                    ]
                ]
            ]
        ]);
      Route::for('terms')
         ->get([$this, 'handleTermSelectionRequest'])
         ->auth('public')
         ->rateLimit()
         ->args([
            'page'      => 'int|required',
            'taxonomy'  => 'string|required',
            'search' => 'string',
            'parent' => 'int'
         ])
         ->post([$this, 'createTermRequest'])
         ->auth('isVerified')
         ->rateLimit(30)
         ->args([
            'taxonomy'  => 'string|required',
            'name'      => 'string|required',
            'parent' => 'int|default:0',
         ])
         ->register();
        //TODO: Just wrap this up to the normal GET request
        register_rest_route($this->namespace, '/terms/check', [
            'methods' => 'GET',
            'callback' => [$this, 'getTermDetails'],
            'permission_callback' => [$this, 'checkPermission'],
        ]);
      Route::for('terms/check')
         ->get([$this,'getTermDetails'])
         ->auth('public')
         ->rateLimit()
         ->register();
    }
    /**
@@ -102,22 +76,19 @@
        $term = get_term($term_id);
        if (is_wp_error($term)) {
            return new WP_REST_Response([
                'success'   => false,
                'message'   => $term
            ]);
         return $this->error('No term found');
        }
        $data = [
            'id' => $term->term_id,
            'name' => $term->name,
            'name' => html_entity_decode($term->name),
            'slug' => $term->slug,
            'taxonomy' => $term->taxonomy,
        ];
        // Add relationship data if requested
        if ($request->get_param('include_relationships')) {
            $relationship_manager = new TaxonomyRelationships();
            $relationship_manager = JVB()->termRelationships();
            $related_taxonomies = $request->get_param('related_taxonomies') ?: ['jvb_style', 'jvb_theme', 'jvb_city'];
            $data['relationships'] = [];
@@ -132,7 +103,7 @@
                        $term = get_term($rel->related_term_id, $rel->related_taxonomy);
                        return [
                            'id' => $rel->related_term_id,
                            'name' => $term ? $term->name : 'Unknown',
                            'name' => $term ? html_entity_decode($term->name) : 'Unknown',
                            'count' => $rel->relationship_count
                        ];
                    }, $relationships);
@@ -140,7 +111,7 @@
            }
        }
        return new WP_REST_Response($data);
        return $this->success($data);
    }
    /**
@@ -148,31 +119,33 @@
     *
     * @return WP_REST_Response
     */
    public function getTermDetails(WP_REST_Request $request):WP_REST_Response
    {
        $data = $request->get_params();
      // Collect all taxonomies being queried
   public function getTermDetails(WP_REST_Request $request): WP_REST_Response
   {
      $data = $request->get_params();
      $taxonomies = array_keys($data);
      // Check HTTP cache headers
      $cache_check = $this->checkHeaders($request, $taxonomies);
      $cache_check = $this->checkHeaders($request, $this->cache->generateKey(['termDetails' => $data]));
      if ($cache_check) {
         return $cache_check;
      }
        $terms = [];
        foreach ($data as $tax => $IDs) {
            $args = [
                'taxonomy'  => BASE.$tax,
                'include'   => $IDs
            ];
            $terms[$tax] = $this->formatTerms($args, BASE.$tax);
        }
        $response = new WP_REST_Response([
            'items' => $terms,
        ]);
      return $this->addCacheHeaders($response);
    }
      $terms = $this->cache->remember(
         $this->cache->generateKey(['termDetails' => $data]),
         function() use ($data) {
            $result = [];
            foreach ($data as $tax => $IDs) {
               $args = [
                  'taxonomy' => BASE . $tax,
                  'include' => $IDs
               ];
               $result[$tax] = $this->formatTerms($args, BASE . $tax);
            }
            return $result;
         }
      );
      return $this->addCacheHeaders($this->success(['items' => $terms]));
   }
    /**
     * @param WP_REST_Request $request
@@ -200,12 +173,12 @@
         $args = [
            'taxonomy'  => $taxonomy,
            'include'   => $data['termIDs'],
            'hide_empty'   => false,
            'hide_empty'   => true,
         ];
         $key = $this->cache->generateKey($args);
         $cached = $this->cache->get($key);
         if ($cached) {
            $response = new WP_REST_Response($cached);
            $response = $this->success($cached);
            return $this->addCacheHeaders($response);
         }
@@ -214,14 +187,14 @@
            'items'  => $formatted
         ];
         $this->cache->set($key, $response);
         $response = new WP_REST_Response($response);
         $response = $this->success($response);
         return $this->addCacheHeaders($response);
      }
      if (array_key_exists('content', $data)) {
         // If content_type is provided, use the specialized endpoint
         $content_type = $request->get_param('content');
         global $feed_types;
         if (taxIsJVBContentTax($content_type)) {
         $registrar = Registrar::getInstance($content_type);
         if ($registrar->hasFeature('is_content')) {
            $response = $this->getTermsForContentType($request);
            return $this->addCacheHeaders($response);
         }
@@ -235,16 +208,7 @@
      $per_page = 25;
        if (!taxonomy_exists($taxonomy)) {
            return new WP_REST_Response([
                'items' => [],
                'pagination' => [
                    'page' => 1,
                    'per_page' => $per_page,
                    'total_pages' => 0,
                    'total_terms' => 0,
                    'has_more' => false
                ]
            ]);
         return $this->emptyResult();
        }
        $tax_obj = get_taxonomy($taxonomy);
@@ -260,7 +224,7 @@
        // Get terms for current level with child count
        $args = [
            'taxonomy' => $taxonomy,
            'hide_empty' => false,
            'hide_empty' => true,
            'parent' => $parent,
            'number' => $per_page,
         'orderby'=> 'name',
@@ -272,21 +236,12 @@
        if ($request->get_param('main_context') && in_array(jvbCheckBase(json_decode($request->get_param('main_context'), true)['context']), jvbUserTypes())) {
            $main_context = json_decode($request->get_param('main_context'), true);
            $userID = get_post_meta($main_context['id'], BASE.'link', true);
            $userID = get_post_meta($main_context['id'], BASE.'profile_link', true);
            $manager = new UserTermsManager();
            $related = $manager->getUserTermIDs($userID, $taxonomy);
            $related = $manager->fetchUserTerms($userID, $taxonomy);
            if (empty($related)) {
                $response = new WP_REST_Response([
                    'items' => [],
                    'pagination' => [
                        'page' => 1,
                        'per_page' => $per_page,
                        'total_pages' => 0,
                        'total_terms' => 0,
                        'has_more' => false
                    ]
                ]);
                $response = $this->emptyResult();
            return $this->addCacheHeaders($response);
            }
@@ -296,20 +251,11 @@
            $main_context = json_decode($request->get_param('main_context'), true);
            $thisTaxonomy = str_replace('taxonomy:', '', $main_context['context']);
            $ID = (int)$main_context['id'];
            $manager = new TaxonomyRelationships();
            $manager = JVB()->termRelationships();
            $related = $manager->getRelatedTerms($ID, BASE.$request->get_param('taxonomy'));
            if (empty($related)) {
                $response = new WP_REST_Response([
                    'items' => [],
                    'pagination' => [
                        'page' => 1,
                        'per_page' => $per_page,
                        'total_pages' => 0,
                        'total_terms' => 0,
                        'has_more' => false
                    ]
                ]);
                $response = $this->emptyResult();
            return $this->addCacheHeaders($response);
            }
            $args['tax_query'] = [
@@ -323,7 +269,7 @@
            $match = $request->get_param('match') ?? 'any';
            $context = json_decode($request['context'], true);
            $relationshipManager = new TaxonomyRelationships();
            $relationshipManager = JVB()->termRelationships();
            // Prepare array to collect term IDs that match the context
            $related_term_ids = [];
@@ -359,16 +305,7 @@
                $args['include'] = $related_term_ids;
            } else {
                // No related terms found, return empty result
                $response =  new WP_REST_Response([
                    'items' => [],
                    'pagination' => [
                        'page' => 1,
                        'per_page' => $per_page,
                        'total_pages' => 0,
                        'total_terms' => 0,
                        'has_more' => false
                    ]
                ]);
                $response = $this->emptyResult();
            return $this->addCacheHeaders($response);
            }
@@ -380,7 +317,7 @@
        $cache = $this->cache->get($key);
        if ($cache) {
            $response = new WP_REST_Response($cache);
            $response = $this->success($cache);
         return $this->addCacheHeaders($response);
        }
@@ -403,13 +340,13 @@
                'page' => $page,
                'per_page' => $per_page,
                'total_pages' => $total_pages,
                'total_terms' => (int)$total_terms,
                'has_more' => $has_more
            ]
                'total_terms' => (int)$total_terms
            ],
         'has_more' => $has_more
        ];
        $this->cache->set($key, $response);
        $response = new WP_REST_Response($response);
        $response = $this->success($response);
      return $this->addCacheHeaders($response);
    }
@@ -442,12 +379,12 @@
         'items'  => $all_terms,
         'pagination'=> [
            'page' => $page,
            'per_page'=> $per_page,
            'has_more' => true,
         ]
            'per_page'=> $per_page
         ],
         'has_more' => true,
      ];
      $response = new WP_REST_Response($response);
      $response = $this->success($response);
      return $this->addCacheHeaders($response);
   }
@@ -496,7 +433,7 @@
      return $this->cache->remember($cache_key, function() use ($term, $taxonomy) {
         $data = [
            'id' => $term->term_id,
            'name' => $term->name,
            'name' => html_entity_decode($term->name),
            'slug' => $term->slug,
            'parent' => $term->parent,
            'path' => $this->getTermPath($term->term_id, $term->name, $taxonomy),
@@ -523,74 +460,56 @@
     *
     * @return WP_REST_Response
     */
    public function handleTermSearch(WP_REST_Request $request):WP_REST_Response
    {
        $taxonomy = BASE.$request->get_param('taxonomy');
        $search = $request->get_param('search');
        $page = $request->get_param('page') ?? 1;
        $per_page = $request->get_param('per_page') ?? 20;
   public function handleTermSearch(WP_REST_Request $request): WP_REST_Response
   {
      $taxonomy = BASE . $request->get_param('taxonomy');
      $search = $request->get_param('search');
      $page = $request->get_param('page') ?? 1;
      $per_page = $request->get_param('per_page') ?? 20;
        // When searching, we want to search across all terms regardless of hierarchy
        $args = [
            'taxonomy' => $taxonomy,
            'hide_empty' => false,
            'search' => $search,
            'search_columns' => ['name', 'slug'],
            'fields' => 'all',
            'number' => $per_page,
            'offset' => ($page - 1) * $per_page,
        ];
      $args = [
         'taxonomy' => $taxonomy,
         'hide_empty' => true,
         'search' => $search,
         'search_columns' => ['name', 'slug'],
         'fields' => 'all',
         'number' => $per_page,
         'offset' => ($page - 1) * $per_page,
      ];
        $key = $this->cache->generateKey($args);
        $cache = $this->cache->get($key);
        if ($cache) {
            return new WP_REST_Response($cache);
        }
      $data = $this->cache->remember(
         $this->cache->generateKey($args),
         function() use ($args, $taxonomy, $page, $per_page) {
            $terms = get_terms($args);
        $terms = get_terms($args);
            if (is_wp_error($terms)) {
               return $this->emptyResult($page, $per_page);
            }
        if (is_wp_error($terms)) {
            return new WP_REST_Response([
                'items' => [],
                'pagination' => [
                    'page' => 0,
                    'per_page' => 20,
                    'total_pages' => 0,
                    'total_terms' => 0,
                    'has_more' => false
                ]
            ]);
        }
            $formatted_terms = array_map(
               fn($term) => $this->formatSingleTerm($term, $taxonomy),
               $terms
            );
        // Get total count for pagination
        $count_args = array_merge($args, ['fields' => 'count']);
        $total_terms = wp_count_terms($count_args);
            $count_args = array_merge($args, ['fields' => 'count']);
            $total_terms = wp_count_terms($count_args);
            $total_pages = ceil($total_terms / $per_page);
        $formatted_terms = [];
      foreach ($terms as $term) {
         // Search results show path, so includeChildren = false for performance
         $formatted_terms[] = $this->formatSingleTerm($term, $taxonomy, false);
      }
            return [
               'items' => $formatted_terms,
               'pagination' => [
                  'page' => (int)$page,
                  'per_page' => (int)$per_page,
                  'total_pages' => $total_pages,
                  'total_terms' => (int)$total_terms
               ],
               'has_more' => $page < $total_pages
            ];
         }
      );
        // Calculate pagination info
        $total_pages = ceil($total_terms / $per_page);
        $has_more = $page < $total_pages;
        $response = [
            'items' => $formatted_terms,
            'pagination' => [
                'page' => (int)$page,
                'per_page' => (int)$per_page,
                'total_pages' => $total_pages,
                'total_terms' => (int)$total_terms,
                'has_more' => $has_more
            ]
        ];
        $this->cache->set($key, $response);
        return new WP_REST_Response($response);
    }
      return $this->addCacheHeaders($this->success($data));
   }
    /**
     * @param int $termID
@@ -664,7 +583,7 @@
     */
    public function getTermsForContentType(WP_REST_Request $request):WP_REST_Response
    {
        $manager = new TaxonomyRelationships();
        $manager = JVB()->termRelationships();
        $content_type = BASE . $request->get_param('content');
        $taxonomy = BASE . $request->get_param('taxonomy');
        $search = $request->get_param('search');
@@ -684,7 +603,8 @@
        $cache = $this->cache->get($cache_key);
        // Try cache first
        if ($cache !== false) {
            return new WP_REST_Response($cache);
         $response = $this->success($cache);
            return $this->addCacheHeaders($response);
        }
        try {
@@ -776,163 +696,61 @@
                    'page'      => (int)$page,
                    'per_page'  => (int)$per_page,
                    'total_terms'=> $total,
                    'total_pages'=> $total_pages,
                    'has_more'  => $page < $total_pages
                ]
                    'total_pages'=> $total_pages
                ],
            'has_more'  => $page < $total_pages
            ];
            // Cache results
            $this->cache->set($cache_key, $results);
            return new WP_REST_Response($results);
         $response = $this->success($results);
            return $this->addCacheHeaders($response);
        } catch (Exception $e) {
            return new WP_REST_Response([
                'success'   => false,
                'message'   => $e->getMessage()
            ]);
         return $this->error('Error getting terms for content: '.$e->getMessage(), 'get_terms_for_content');
        }
    }
    /**
     * @param WP_REST_Request $request
     *
     * @return WP_REST_Response
     */
    public function handleTermsRequest(WP_REST_Request $request):WP_REST_Response
    {
        $taxonomy = BASE.$request->get_param('taxonomy');
        $search = $request->get_param('search');
        $page = max(1, intval($request->get_param('page')));
        $per_page = $request->get_param('per_page') ?: $this->per_page;
   public function createTermRequest(WP_REST_Request $request): WP_REST_Response
   {
      $user_id = get_current_user_id();
      $taxonomy = $request->get_param('taxonomy');
      $name = sanitize_text_field($request->get_param('name'));
      $parent = (int)$request->get_param('parent') ?: 0;
        // Create cache key
        $cache_key = "terms_{$taxonomy}_" . md5("{$search}_{$page}_{$per_page}");
        $cache = $this->cache->get($cache_key);
        if ($cache) {
            return new WP_REST_Response($cache);
        }
      try {
         $existing = term_exists($name, jvbCheckBase($taxonomy), $parent);
        try {
            // Build query args
            $args = [
                'taxonomy' => $taxonomy,
                'hide_empty' => false,
                'orderby' => $search ? 'name' : 'count',
                'order' => $search ? 'ASC' : 'DESC',
                'number' => $per_page,
                'offset' => ($page - 1) * $per_page,
                'fields' => 'all'
            ];
            // Add search if provided
            if ($search) {
                $args['search'] = $search;
            }
            // Get terms
            $terms = get_terms($args);
            if (is_wp_error($terms)) {
                return new WP_REST_Response([
                    'items' => [],
                    'pagination' => [
                        'page' => 0,
                        'per_page' => 20,
                        'total_pages' => 0,
                        'total_terms' => 0,
                        'has_more' => 0
                    ]
                ]);
            }
            // Check if taxonomy is hierarchical
            $is_hierarchical = is_taxonomy_hierarchical($taxonomy);
            // Format terms
         $formatted_terms = [];
         foreach ($terms as $term) {
            $formatted_terms[] = $this->formatSingleTerm($term, $taxonomy, false);
         if ($existing) {
            $term = get_term($existing['term_id'], jvbCheckBase($taxonomy));
            return $this->success(['message' => 'Term already exists', 'term' => [
               'id' => $term->term_id,
               'name' => html_entity_decode($term->name),
               'path' => $this->getTermPath($term->term_id, $term->name, $taxonomy)
            ]]);
         }
            // Get total for pagination
            $total_args = array_merge($args, ['fields' => 'count', 'number' => '']);
            $total = wp_count_terms($taxonomy, $total_args);
            $total_pages = ceil($total / $per_page);
            $results = [
                'items' => $formatted_terms,
                'pagination' => [
                    'page' => (int)$page,
                    'per_page' => (int)$per_page,
                    'total_pages' => $total_pages,
                    'total_terms' => (int)$total,
                    'has_more' => $page < $total_pages
                ]
            ];
            // Cache results
            $this->cache->set($cache_key, $results);
            return new WP_REST_Response($results);
        } catch (Exception $e) {
            return new WP_REST_Response([
                'success'       => false,
                'message'       =>  $e->getMessage()
            ]);
        }
    }
    /**
     * @param WP_REST_Request $request
     *
     * @return WP_REST_Response
     */
    public function createTermRequest(WP_REST_Request $request):WP_REST_Response
    {
        $user_id = get_current_user_id();
        $taxonomy = $request->get_param('taxonomy');
        $name = sanitize_text_field($request->get_param('name'));
        $parent = (int)$request->get_param('parent') ?: 0;
        try {
            // Check if term already exists
            $existing = term_exists($name, jvbCheckBase($taxonomy), $parent);
            if ($existing) {
                $term = get_term($existing['term_id'], jvbCheckBase($taxonomy));
            error_log('Existing Term: '.print_r($term, true));
                return new WP_REST_Response([
                    'success' => false,
                    'message' => 'Term already exists',
                    'term' => [
                        'id' => $term->term_id,
                        'name' => $term->name,
                        'path' => $this->getTermPath($term->term_id, $term->name, $taxonomy)
                    ]
                ]);
            }
         if (Features::forMembership()->has('term_approval')) {
            error_log('Term Approval required');
            // Get approval routes instance
         $membership = Site::membership();
         if ($membership && $membership->has('term_approval')) {
            $approval_routes = JVB()->routes('approvals');
            // Create approval request
            $request_id = $approval_routes->createTermApprovalRequest(
               $user_id,
               $taxonomy,
               sanitize_title($name),
               absint($parent)
            );
            if (!$request_id) {
               throw new Exception('Failed to create approval request');
            }
            $return = [
               'success' => true,
            return $this->success([
               'message' => 'Term suggestion submitted for approval',
               'term' => [
                  'id' => 'pending_' . $request_id,
@@ -940,50 +758,50 @@
                  'pending' => true,
                  'request_id' => $request_id
               ]
            ];
         } else {
            error_log('Creating new Term: ');
            $termID = wp_insert_term(
               $name,
               jvbCheckBase($taxonomy),
               [
                  'parent' => absint($parent)
               ]
            );
            error_log('Result: '.print_r($termID, true));
            if (is_wp_error($termID)) {
               throw new Exception('Failed to create new term');
            }
            $return = [
               'success'   => true,
               'message'   => $name.' created successfully',
               'term'      => [
                  'id'  => $termID['term_id'],
                  'name'   => $name,
                  'path'   => $this->getTermPath($termID['term_id'], $name, $taxonomy)
               ]
            ];
            ], 202); // 202 Accepted for pending approval
         }
            return new WP_REST_Response($return);
         $termID = wp_insert_term(
            $name,
            jvbCheckBase($taxonomy),
            ['parent' => absint($parent)]
         );
        } catch (Exception $e) {
            JVB()->error()->log(
                'terms',
                'Term creation failed: ' . $e->getMessage(),
                [
                    'user_id' => $user_id,
                    'taxonomy' => $taxonomy,
                    'name' => $name
                ]
            );
         if (is_wp_error($termID)) {
            throw new Exception($termID->get_error_message());
         }
            return new WP_REST_Response([
                'success' => false,
                'message' => $e->getMessage()
            ], 500);
        }
    }
         return $this->success([
            'message' => $name . ' created successfully',
            'term' => [
               'id' => $termID['term_id'],
               'name' => $name,
               'path' => $this->getTermPath($termID['term_id'], $name, $taxonomy)
            ]
         ], 201); // 201 Created
      } catch (Exception $e) {
         JVB()->error()->log(
            'terms',
            'Term creation failed: ' . $e->getMessage(),
            ['user_id' => $user_id, 'taxonomy' => $taxonomy, 'name' => $name]
         );
         return $this->error($e->getMessage(), 'term_creation_failed', 500);
      }
   }
   protected function emptyResult(int $page = 1, int $per_page = 20):WP_REST_Response
   {
      return $this->success([
         'items' => [],
         'pagination' => [
            'page' => $page,
            'per_page' => $per_page,
            'total_pages' => 0,
            'total_terms' => 0
         ],
         'has_more' => false
      ]);
   }
}