<?php
|
namespace JVBase\rest\routes;
|
|
use JVBase\registrar\Registrar;
|
use JVBase\rest\Rest;
|
use JVBase\managers\UserTermsManager;
|
use JVBase\rest\Route;
|
use JVBase\utility\Features;
|
use WP_REST_Request;
|
use WP_REST_Response;
|
use Exception;
|
|
if (!defined('ABSPATH')) {
|
exit; // Exit if accessed directly
|
}
|
class TermRoutes extends Rest
|
{
|
protected object $term_index_manager;
|
protected int $per_page;
|
|
public function __construct()
|
{
|
$this->cacheName = 'terms';
|
parent::__construct();
|
if (JVB_TESTING) {
|
$this->cache->flush();
|
}
|
$this->cache->connect('taxonomy', true);
|
$this->per_page = 20;
|
|
add_action('edited_term', [$this, 'deleteTermPath']);
|
add_action('wp_login', [$this, 'clearUserTaxonomyCache'], 10, 2);
|
}
|
|
/**
|
* Registers term routes
|
* @return void
|
*/
|
public function registerRoutes():void
|
{
|
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();
|
|
Route::for('terms/check')
|
->get([$this,'getTermDetails'])
|
->auth('public')
|
->rateLimit()
|
->register();
|
}
|
|
/**
|
* @param WP_REST_Request $request
|
*
|
* @return WP_REST_Response
|
*/
|
public function handleSingleTerm(WP_REST_Request $request):WP_REST_Response
|
{
|
$term_id = (int)$request->get_param('id');
|
$term = get_term($term_id);
|
|
if (is_wp_error($term)) {
|
return $this->error('No term found');
|
}
|
|
$data = [
|
'id' => $term->term_id,
|
'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 = JVB()->termRelationships();
|
$related_taxonomies = $request->get_param('related_taxonomies') ?: ['jvb_style', 'jvb_theme', 'jvb_city'];
|
|
$data['relationships'] = [];
|
foreach ($related_taxonomies as $related_tax) {
|
$relationships = $relationship_manager->getRelatedTerms(
|
$term->term_id,
|
$related_tax
|
);
|
|
if (!empty($relationships)) {
|
$data['relationships'][$related_tax] = array_map(function ($rel) {
|
$term = get_term($rel->related_term_id, $rel->related_taxonomy);
|
return [
|
'id' => $rel->related_term_id,
|
'name' => $term ? html_entity_decode($term->name) : 'Unknown',
|
'count' => $rel->relationship_count
|
];
|
}, $relationships);
|
}
|
}
|
}
|
|
return $this->success($data);
|
}
|
|
/**
|
* @param WP_REST_Request $request
|
*
|
* @return WP_REST_Response
|
*/
|
public function getTermDetails(WP_REST_Request $request): WP_REST_Response
|
{
|
$data = $request->get_params();
|
$taxonomies = array_keys($data);
|
|
$cache_check = $this->checkHeaders($request, $this->cache->generateKey(['termDetails' => $data]));
|
if ($cache_check) {
|
return $cache_check;
|
}
|
|
$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
|
*
|
* @return WP_REST_Response
|
*/
|
public function handleTermSelectionRequest(WP_REST_Request $request):WP_REST_Response
|
{
|
$data = $request->get_params();
|
$taxonomy = sanitize_text_field($data['taxonomy'])??'';
|
// Check HTTP cache headers
|
$cache_check = $this->checkHeaders($request, $taxonomy);
|
if ($cache_check) {
|
error_log('Header Check failed');
|
return $cache_check;
|
}
|
|
// Handle batch request (multiple taxonomies)
|
if (str_contains($taxonomy, ',')) {
|
return $this->handleBatchTermRequest($taxonomy, $data, $request);
|
}
|
$taxonomy = jvbCheckBase($taxonomy);
|
|
if (array_key_exists('termIDs', $data)) {
|
$args = [
|
'taxonomy' => $taxonomy,
|
'include' => $data['termIDs'],
|
'hide_empty' => true,
|
];
|
$key = $this->cache->generateKey($args);
|
$cached = $this->cache->get($key);
|
if ($cached) {
|
$response = $this->success($cached);
|
return $this->addCacheHeaders($response);
|
}
|
|
$formatted = $this->formatTerms($args, $taxonomy);
|
$response = [
|
'items' => $formatted
|
];
|
$this->cache->set($key, $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');
|
$registrar = Registrar::getInstance($content_type);
|
if ($registrar->hasFeature('is_content')) {
|
$response = $this->getTermsForContentType($request);
|
return $this->addCacheHeaders($response);
|
}
|
}
|
|
$taxonomy = BASE.$request->get_param('taxonomy');
|
$search = $request->get_param('search');
|
|
$parent = (int)$data['parent']??0;
|
$page = max(1, (int)($data['page']??1));
|
$per_page = 25;
|
|
if (!taxonomy_exists($taxonomy)) {
|
return $this->emptyResult();
|
}
|
|
$tax_obj = get_taxonomy($taxonomy);
|
$is_hierarchical = $tax_obj->hierarchical;
|
|
// If searching, handle differently
|
if (!empty($search)) {
|
error_log('Handling search...');
|
$response = $this->handleTermSearch($request);
|
return $this->addCacheHeaders($response);
|
}
|
|
// Get terms for current level with child count
|
$args = [
|
'taxonomy' => $taxonomy,
|
'hide_empty' => true,
|
'parent' => $parent,
|
'number' => $per_page,
|
'orderby'=> 'name',
|
'offset' => ($page - 1) * $per_page,
|
'fields' => 'all'
|
];
|
|
|
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);
|
$manager = new UserTermsManager();
|
$related = $manager->fetchUserTerms($userID, $taxonomy);
|
|
if (empty($related)) {
|
$response = $this->emptyResult();
|
return $this->addCacheHeaders($response);
|
}
|
|
$args['include'] = $related;
|
|
} elseif ($request->get_param('main_context')) {
|
$main_context = json_decode($request->get_param('main_context'), true);
|
$thisTaxonomy = str_replace('taxonomy:', '', $main_context['context']);
|
$ID = (int)$main_context['id'];
|
$manager = JVB()->termRelationships();
|
$related = $manager->getRelatedTerms($ID, BASE.$request->get_param('taxonomy'));
|
|
if (empty($related)) {
|
$response = $this->emptyResult();
|
return $this->addCacheHeaders($response);
|
}
|
$args['tax_query'] = [
|
'taxonomy' => $taxonomy,
|
'terms' => array_filter($related, function ($ID) {
|
return (int)$ID;
|
})
|
];
|
}
|
if ($request->get_param('context')) {
|
$match = $request->get_param('match') ?? 'any';
|
$context = json_decode($request['context'], true);
|
|
$relationshipManager = JVB()->termRelationships();
|
// Prepare array to collect term IDs that match the context
|
$related_term_ids = [];
|
|
foreach ($context as $context_taxonomy => $term_ids) {
|
if (!is_array($term_ids) || empty($term_ids)) {
|
continue;
|
}
|
|
// For each context term, get related terms in the requested taxonomy
|
foreach ($term_ids as $term_id) {
|
$related_terms = $relationshipManager->getRelatedTerms($term_id, $taxonomy);
|
|
if (!empty($related_terms)) {
|
// Merge with existing term IDs
|
$related_term_ids = array_merge($related_term_ids, $related_terms);
|
}
|
}
|
}
|
|
// If we have related terms, filter our query to only include them
|
if (!empty($related_term_ids)) {
|
// For 'all' match type, we need to find intersection of all term sets
|
// This logic would need to be expanded based on your exact requirements
|
if ($match === 'all') {
|
// Complex logic for "all" matching goes here
|
// Would need to be implemented based on your exact requirements
|
}
|
|
// Remove duplicates
|
$related_term_ids = array_unique($related_term_ids);
|
|
// Include only these terms in our query
|
$args['include'] = $related_term_ids;
|
} else {
|
// No related terms found, return empty result
|
$response = $this->emptyResult();
|
|
return $this->addCacheHeaders($response);
|
}
|
}
|
|
|
|
$key = $this->cache->generateKey($args);
|
$cache = $this->cache->get($key);
|
|
if ($cache) {
|
$response = $this->success($cache);
|
return $this->addCacheHeaders($response);
|
}
|
|
$formatted_terms = $this->formatTerms($args, $taxonomy);
|
// Get total count for pagination
|
$total_terms = wp_count_terms([
|
'taxonomy' => $taxonomy,
|
'hide_empty' => false,
|
'parent' => $parent
|
]);
|
|
// Calculate pagination
|
$total_pages = ceil($total_terms / $per_page);
|
$has_more = $page < $total_pages;
|
|
$response = [
|
'items' => $formatted_terms,
|
'is_hierarchical' => $is_hierarchical,
|
'pagination' => [
|
'page' => $page,
|
'per_page' => $per_page,
|
'total_pages' => $total_pages,
|
'total_terms' => (int)$total_terms
|
],
|
'has_more' => $has_more
|
];
|
|
$this->cache->set($key, $response);
|
$response = $this->success($response);
|
return $this->addCacheHeaders($response);
|
}
|
|
protected function handleBatchTermRequest(string $taxonomy, array $data, WP_REST_Request $request):WP_REST_Response
|
{
|
$taxonomies = array_map('trim', explode(',', $taxonomy));
|
$all_terms = [];
|
$parent = (int)$data['parent']??0;
|
$page = max(1, (int)($data['page']??1));
|
$per_page = 25;
|
$mainArgs = [
|
'hide_empty'=> false,
|
'parent' => $parent,
|
'number' => $per_page,
|
'orderby' => 'name',
|
'offset' => ($page -1) * $per_page,
|
];
|
|
foreach ($taxonomies as $taxonomy) {
|
if (!taxonomy_exists(BASE.$taxonomy)) {
|
continue;
|
}
|
$args = $mainArgs;
|
$args['taxonomy'] = BASE.$taxonomy;
|
|
$all_terms = array_merge($all_terms, $this->formatTerms($args, $taxonomy));
|
}
|
|
$response = [
|
'items' => $all_terms,
|
'pagination'=> [
|
'page' => $page,
|
'per_page'=> $per_page
|
],
|
'has_more' => true,
|
];
|
|
$response = $this->success($response);
|
return $this->addCacheHeaders($response);
|
}
|
|
|
/**
|
* @param array $args
|
* @param string $taxonomy
|
*
|
* @return array
|
*/
|
protected function formatTerms(array $args, string $taxonomy): array
|
{
|
return $this->cache->remember(
|
$this->cache->generateKey($args),
|
function() use ($args, $taxonomy) {
|
$terms = get_terms($args);
|
|
if (is_wp_error($terms)) {
|
return [];
|
}
|
|
$formatted_terms = [];
|
foreach ($terms as $term) {
|
$formatted_terms[] = $this->formatSingleTerm($term, $taxonomy, true);
|
}
|
|
return $formatted_terms;
|
}
|
);
|
|
|
}
|
|
/**
|
* Format a single term with caching
|
*
|
* @param object $term WP_Term object
|
* @param string $taxonomy Full taxonomy name
|
*
|
* @return array Formatted term data
|
*/
|
protected function formatSingleTerm(object $term, string $taxonomy): array
|
{
|
$cache_key = "{$term->term_id}_{$taxonomy}";
|
|
return $this->cache->remember($cache_key, function() use ($term, $taxonomy) {
|
$data = [
|
'id' => $term->term_id,
|
'name' => html_entity_decode($term->name),
|
'slug' => $term->slug,
|
'parent' => $term->parent,
|
'path' => $this->getTermPath($term->term_id, $term->name, $taxonomy),
|
'taxonomy' => jvbNoBase($term->taxonomy),
|
'count' => $term->count,
|
];
|
|
$children_args = [
|
'taxonomy' => $taxonomy,
|
'parent' => $term->term_id,
|
'fields' => 'count',
|
'hide_empty' => false
|
];
|
$count = wp_count_terms($children_args);
|
$data['hasChildren'] = !is_wp_error($count) && $count > 0;
|
|
|
return $data;
|
});
|
}
|
|
/**
|
* @param WP_REST_Request $request
|
*
|
* @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;
|
|
$args = [
|
'taxonomy' => $taxonomy,
|
'hide_empty' => true,
|
'search' => $search,
|
'search_columns' => ['name', 'slug'],
|
'fields' => 'all',
|
'number' => $per_page,
|
'offset' => ($page - 1) * $per_page,
|
];
|
|
$data = $this->cache->remember(
|
$this->cache->generateKey($args),
|
function() use ($args, $taxonomy, $page, $per_page) {
|
$terms = get_terms($args);
|
|
if (is_wp_error($terms)) {
|
return $this->emptyResult($page, $per_page);
|
}
|
|
$formatted_terms = array_map(
|
fn($term) => $this->formatSingleTerm($term, $taxonomy),
|
$terms
|
);
|
|
$count_args = array_merge($args, ['fields' => 'count']);
|
$total_terms = wp_count_terms($count_args);
|
$total_pages = ceil($total_terms / $per_page);
|
|
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
|
];
|
}
|
);
|
|
return $this->addCacheHeaders($this->success($data));
|
}
|
|
/**
|
* @param int $termID
|
* @param string $name
|
* @param string $taxonomy
|
*
|
* @return string
|
*/
|
public function getTermPath(int $termID, string $name, string $taxonomy):string
|
{
|
$path = get_term_meta($termID, BASE.'term_path', true);
|
if ($path == '') {
|
// Get ancestor path for each term
|
$ancestors = get_ancestors($termID, $taxonomy, 'taxonomy');
|
$ancestor_names = [];
|
|
foreach (array_reverse($ancestors) as $ancestor_id) {
|
$ancestor = get_term($ancestor_id, $taxonomy);
|
if ($ancestor && !is_wp_error($ancestor)) {
|
$ancestor_names[] = $ancestor->name;
|
}
|
}
|
|
$path = (empty($ancestor_names)) ? $name : implode(' > ', $ancestor_names).' > '.$name;
|
update_term_meta($termID, BASE.'term_path', $path);
|
}
|
|
return $path;
|
}
|
|
/**
|
* @param int $termID
|
*
|
* @return void
|
*/
|
public function deleteTermPath($termID)
|
{
|
delete_term_meta($termID, BASE.'term_path');
|
}
|
|
/**
|
* @param string $username
|
* @param object $user
|
*
|
* @return void
|
*/
|
public function clearUserTaxonomyCache(string $username, object $user):void
|
{
|
global $wpdb;
|
|
// Get all transients for this user
|
$like_pattern = $wpdb->esc_like(BASE.'user_' . $user->ID) . '_%';
|
|
$transients = $wpdb->get_col($wpdb->prepare("
|
SELECT option_name
|
FROM {$wpdb->options}
|
WHERE option_name LIKE %s
|
AND option_name LIKE %s
|
", '_transient_' . $like_pattern, '%'));
|
|
// Delete all found transients
|
foreach ($transients as $transient) {
|
delete_transient(str_replace('_transient_', '', $transient));
|
}
|
}
|
|
/**
|
* @param WP_REST_Request $request
|
*
|
* @return WP_REST_Response
|
*/
|
public function getTermsForContentType(WP_REST_Request $request):WP_REST_Response
|
{
|
$manager = JVB()->termRelationships();
|
$content_type = BASE . $request->get_param('content');
|
$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') ?: 20;
|
|
// Create cache key
|
$cache_key = "terms_for_{$content_type}_{$taxonomy}_" . md5("{$search}_{$page}_{$per_page}");
|
|
$cache_key = $this->cache->generateKey([
|
'terms_for' => $content_type,
|
'taxonomy' => $taxonomy,
|
'search' => $search,
|
'page' => $page,
|
'per_page' => $per_page
|
]);
|
$cache = $this->cache->get($cache_key);
|
// Try cache first
|
if ($cache !== false) {
|
$response = $this->success($cache);
|
return $this->addCacheHeaders($response);
|
}
|
|
try {
|
global $wpdb;
|
|
// Starting the query building
|
$query_args = [];
|
$terms_query = "
|
SELECT DISTINCT t.term_id, t.name, t.slug, tt.count, tt.taxonomy,
|
(SELECT COUNT(DISTINCT tm.meta_key) FROM {$wpdb->termmeta} tm WHERE tm.term_id = t.term_id) AS meta_count
|
FROM {$wpdb->terms} t
|
JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
|
JOIN {$manager->getTableName()} tr ON (
|
(tr.primary_taxonomy = %s AND tr.primary_term_id = t.term_id)
|
OR (tr.related_taxonomy = %s AND tr.related_term_id = t.term_id)
|
)
|
";
|
|
$query_args[] = $taxonomy;
|
$query_args[] = $taxonomy;
|
|
// Add content type filter through posts
|
$query_where = " WHERE tt.taxonomy = %s";
|
$query_args[] = $taxonomy;
|
|
// Add search condition if provided
|
if ($search) {
|
$query_where .= " AND (t.name LIKE %s OR t.slug LIKE %s)";
|
$search_like = '%' . $wpdb->esc_like($search) . '%';
|
$query_args[] = $search_like;
|
$query_args[] = $search_like;
|
}
|
|
// Add relationship strength ordering
|
$query_order = " GROUP BY t.term_id";
|
$query_order .= " ORDER BY COUNT(tr.id) DESC, tt.count DESC";
|
|
// Add pagination
|
$query_limit = " LIMIT %d OFFSET %d";
|
$query_args[] = $per_page;
|
$query_args[] = ($page - 1) * $per_page;
|
|
// Combine the query
|
$final_query = $query_where . $query_order . $query_limit;
|
|
// Get terms
|
$terms = $wpdb->get_results(
|
$wpdb->prepare(
|
$terms_query . $final_query,
|
$query_args
|
)
|
);
|
// Get total count for pagination
|
$count_query = str_replace($query_limit, '', $query_where);
|
$count_query = "SELECT COUNT(DISTINCT t.term_id) FROM {$wpdb->terms} t
|
JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
|
JOIN {$manager->getTableName()} tr ON (
|
(tr.primary_taxonomy = %s AND tr.primary_term_id = t.term_id)
|
OR (tr.related_taxonomy = %s AND tr.related_term_id = t.term_id)
|
) " . $count_query;
|
|
// Remove pagination args
|
array_pop($query_args);
|
array_pop($query_args);
|
|
$total = $wpdb->get_var(
|
$wpdb->prepare(
|
$count_query,
|
$query_args
|
)
|
);
|
|
// Format terms
|
$formatted_terms = [];
|
$is_hierarchical = is_taxonomy_hierarchical($taxonomy);
|
|
foreach ($terms as $term) {
|
$formatted = $this->formatSingleTerm($term, $taxonomy, false);
|
// Add relationship strength which is unique to this method
|
$formatted['relationship_strength'] = $term->relationship_count ?? 0;
|
$formatted_terms[] = $formatted;
|
}
|
|
// Build response
|
$total_pages = ceil($total / $per_page);
|
$results = [
|
'items' => $formatted_terms,
|
'pagination' => [
|
'page' => (int)$page,
|
'per_page' => (int)$per_page,
|
'total_terms'=> $total,
|
'total_pages'=> $total_pages
|
],
|
'has_more' => $page < $total_pages
|
];
|
|
// Cache results
|
$this->cache->set($cache_key, $results);
|
$response = $this->success($results);
|
return $this->addCacheHeaders($response);
|
|
} catch (Exception $e) {
|
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 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 {
|
$existing = term_exists($name, jvbCheckBase($taxonomy), $parent);
|
|
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)
|
]]);
|
}
|
|
if (Features::forMembership()->has('term_approval')) {
|
$approval_routes = JVB()->routes('approvals');
|
$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 $this->success([
|
'message' => 'Term suggestion submitted for approval',
|
'term' => [
|
'id' => 'pending_' . $request_id,
|
'name' => $name,
|
'pending' => true,
|
'request_id' => $request_id
|
]
|
], 202); // 202 Accepted for pending approval
|
}
|
|
$termID = wp_insert_term(
|
$name,
|
jvbCheckBase($taxonomy),
|
['parent' => absint($parent)]
|
);
|
|
if (is_wp_error($termID)) {
|
throw new Exception($termID->get_error_message());
|
}
|
|
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
|
]);
|
}
|
}
|