<?php
|
namespace JVBase\rest\routes;
|
|
use JVBase\managers\queue\executors\ContentTermExecutor;
|
use JVBase\managers\queue\TypeConfig;
|
use JVBase\rest\Rest;
|
use JVBase\rest\Route;
|
use JVBase\rest\PermissionHandler;
|
use JVBase\utility\Features;
|
use JVBase\managers\CustomTable;
|
use WP_REST_Request;
|
use WP_REST_Response;
|
use Exception;
|
|
if (!defined('ABSPATH')) {
|
exit;
|
}
|
|
/**
|
* Generic routes for managing content taxonomies (shops, studios, etc.)
|
*
|
* Responsibilities:
|
* - Update term settings/metadata
|
* - Manage members (add/remove via history table)
|
* - Manage ownership/management permissions
|
* - Process membership requests (if verify_entry enabled)
|
*
|
* Note: Invitations handled by InvitationRoutes
|
* Note: Approvals handled by ApprovalRoutes
|
*/
|
class ContentTermsRoutes extends Rest
|
{
|
protected string $taxonomy;
|
protected array $config;
|
protected ?CustomTable $historyTable = null;
|
protected ?CustomTable $requestsTable = null;
|
|
public function __construct(string $taxonomy = '')
|
{
|
$this->taxonomy = jvbNoBase($taxonomy);
|
|
if ($taxonomy && isset(JVB_TAXONOMY[$this->taxonomy])) {
|
$this->config = JVB_TAXONOMY[$this->taxonomy];
|
$this->cacheName = $this->taxonomy;
|
parent::__construct();
|
$this->setupTables();
|
$this->setupCacheConnections();
|
}
|
|
add_action('init', [$this, 'registerContentTermsExecutors'], 5);
|
}
|
|
public function registerContentTermsExecutors():void
|
{
|
$registry = JVB()->queue()->registry();
|
$executor = new ContentTermExecutor();
|
$taxonomies = Features::getTypesWithFeature('is_content', 'taxonomy');
|
|
foreach($taxonomies as $taxonomy) {
|
$registry->register("{$taxonomy}_update", new TypeConfig(
|
executor: $executor,
|
));
|
|
if (Features::forTaxonomy($taxonomy)->has('track_changes')) {
|
$registry->register("{$taxonomy}_member_add", new TypeConfig(
|
executor: $executor
|
));
|
|
$registry->register("{$taxonomy}_member_remove", new TypeConfig(
|
executor: $executor
|
));
|
}
|
|
}
|
}
|
|
protected function setupTables(): void
|
{
|
$content = $this->config['for_content'] ?? [];
|
|
if (Features::forTaxonomy($this->taxonomy)->has('track_changes') && !empty($content)) {
|
foreach ($content as $contentType) {
|
$tableName = "history_{$contentType}_{$this->taxonomy}";
|
$this->historyTable = CustomTable::for($tableName);
|
break; // Only need one table
|
}
|
}
|
|
if (Features::forTaxonomy($this->taxonomy)->has('verify_entry') && !empty($content)) {
|
foreach ($content as $contentType) {
|
$tableName = "{$contentType}_{$this->taxonomy}_requests";
|
$this->requestsTable = CustomTable::for($tableName);
|
break;
|
}
|
}
|
}
|
|
protected function setupCacheConnections(): void
|
{
|
$this->cache
|
->connect('taxonomy')
|
->connect('user');
|
}
|
|
public function registerRoutes(): void
|
{
|
if (!Features::forTaxonomy($this->taxonomy)->has('is_content')) {
|
return;
|
}
|
|
$base = $this->taxonomy;
|
|
// Update term settings
|
Route::for("{$base}/:term_id/settings")
|
->post([$this, 'updateSettings'])
|
->args([
|
'user' => 'int|required',
|
'term_id' => 'int|required'
|
])
|
->auth(PermissionHandler::custom([$this, 'checkTermPermission']))
|
->rateLimit(10);
|
|
// Member management (if track_changes enabled)
|
if (Features::forTaxonomy($this->taxonomy)->has('track_changes')) {
|
Route::for("{$base}/:term_id/members")
|
->get([$this, 'getMembers'])
|
->args([
|
'term_id' => 'int|required',
|
'status' => 'string|enum:active,inactive,all|default:active',
|
'page' => 'int|default:1|min:1'
|
])
|
->auth('logged_in')
|
->rateLimit(30)
|
|
->post([$this, 'manageMember'])
|
->args([
|
'user' => 'int|required',
|
'term_id' => 'int|required',
|
'target_user' => 'int|required',
|
'action' => 'string|enum:add,remove|required'
|
])
|
->auth(PermissionHandler::custom([$this, 'checkTermPermission']))
|
->rateLimit(5);
|
}
|
|
// Membership requests (if verify_entry enabled)
|
if (Features::forTaxonomy($this->taxonomy)->has('verify_entry')) {
|
Route::for("{$base}/:term_id/requests")
|
->get([$this, 'getRequests'])
|
->args([
|
'user' => 'int|required',
|
'term_id' => 'int|required',
|
'status' => 'string|enum:requested,accepted,rejected,all|default:requested',
|
'page' => 'int|default:1|min:1'
|
])
|
->auth(PermissionHandler::custom([$this, 'checkTermPermission']))
|
->rateLimit(20);
|
|
Route::for("{$base}/request")
|
->post([$this, 'handleRequest'])
|
->args([
|
'user' => 'int|required',
|
'term_id' => 'int|required',
|
'action' => 'string|enum:create,accept,reject|required',
|
'request_id' => 'int',
|
'notes' => 'string'
|
])
|
->auth('verified')
|
->rateLimit(5);
|
}
|
|
// Ownership/management (if is_ownable enabled)
|
if (Features::forTaxonomy($this->taxonomy)->has('is_ownable')) {
|
Route::for("{$base}/:term_id/permissions")
|
->post([$this, 'updatePermissions'])
|
->args([
|
'user' => 'int|required',
|
'term_id' => 'int|required',
|
'target_user' => 'int|required',
|
'role' => 'string|enum:owner,manager|required',
|
'grant' => 'bool|required'
|
])
|
->auth(PermissionHandler::custom([$this, 'checkOwnerPermission']))
|
->rateLimit(5);
|
}
|
}
|
|
/**
|
* Permission Callbacks
|
*/
|
public function checkTermPermission(WP_REST_Request $request): bool
|
{
|
$userID = $request->get_param('user') ?? get_current_user_id();
|
$termID = (int)$request->get_param('term_id');
|
|
if (!$this->checkUser($userID) || !term_exists($termID, jvbCheckBase($this->taxonomy))) {
|
return false;
|
}
|
|
return user_can($userID, 'manage_options') ||
|
JVB()->roles()->isManager($userID, $termID);
|
}
|
|
public function checkOwnerPermission(WP_REST_Request $request): bool
|
{
|
$userID = $request->get_param('user') ?? get_current_user_id();
|
$termID = (int)$request->get_param('term_id');
|
|
if (!$this->checkUser($userID) || !term_exists($termID, jvbCheckBase($this->taxonomy))) {
|
return false;
|
}
|
|
return user_can($userID, 'manage_options') ||
|
JVB()->roles()->isOwner($userID, $termID);
|
}
|
|
/**
|
* Update term settings
|
*/
|
public function updateSettings(WP_REST_Request $request): WP_REST_Response
|
{
|
$termID = (int)$request->get_param('term_id');
|
$userID = $request->get_param('user');
|
|
$data = $request->get_params();
|
unset($data['user'], $data['term_id']);
|
|
// Queue the update
|
$op = JVB()->queue()->add(
|
"{$this->taxonomy}_update",
|
$userID,
|
array_merge(['term_id' => $termID], $data),
|
[
|
'priority' => 'high',
|
'notification' => true
|
]
|
);
|
|
return $this->queued($op['operation_id']);
|
}
|
|
/**
|
* Get term members
|
*/
|
public function getMembers(WP_REST_Request $request): WP_REST_Response
|
{
|
$termID = (int)$request->get_param('term_id');
|
$status = $request->get_param('status');
|
$page = (int)$request->get_param('page');
|
|
$cacheKey = $this->cache->generateKey(compact('termID', 'status', 'page'));
|
|
return $this->cache->remember($cacheKey, function() use ($termID, $status, $page) {
|
$perPage = 20;
|
$offset = ($page - 1) * $perPage;
|
|
$query = $this->historyTable->where(['term_id' => $termID]);
|
|
if ($status === 'active') {
|
$query = $query->where(['end_date' => null]);
|
} elseif ($status === 'inactive') {
|
$query = $query->whereNotNull('end_date');
|
}
|
|
$total = $query->countResults();
|
|
$members = $query
|
->orderBy('is_primary', 'DESC')
|
->orderBy('created_at', 'ASC')
|
->limit($perPage, $offset)
|
->getResults(ARRAY_A);
|
|
// Enrich with user data
|
$members = array_map(function($member) {
|
$user = get_userdata($member['user_id']);
|
$member['display_name'] = $user ? $user->display_name : 'Unknown';
|
$member['user_email'] = $user ? $user->user_email : '';
|
return $member;
|
}, $members);
|
|
return $this->success([
|
'members' => $members,
|
'total' => $total,
|
'pages' => ceil($total / $perPage),
|
'page' => $page,
|
'per_page' => $perPage
|
]);
|
});
|
}
|
|
/**
|
* Manage member (add/remove)
|
*/
|
public function manageMember(WP_REST_Request $request): WP_REST_Response
|
{
|
$action = $request->get_param('action');
|
$userID = $request->get_param('user');
|
$termID = (int)$request->get_param('term_id');
|
$targetUserID = (int)$request->get_param('target_user');
|
|
if (!$this->checkUser($targetUserID)) {
|
return $this->error('Invalid target user');
|
}
|
|
// Queue the operation
|
$op = JVB()->queue()->add(
|
"{$this->taxonomy}_member_{$action}",
|
$userID,
|
[
|
'term_id' => $termID,
|
'target_user' => $targetUserID
|
],
|
['priority' => 'high']
|
);
|
|
return $this->queued($op['operation_id']);
|
}
|
|
/**
|
* Get membership requests
|
*/
|
public function getRequests(WP_REST_Request $request): WP_REST_Response
|
{
|
$termID = (int)$request->get_param('term_id');
|
$status = $request->get_param('status');
|
$page = (int)$request->get_param('page');
|
|
$cacheKey = $this->cache->generateKey(compact('termID', 'status', 'page'));
|
|
return $this->cache->remember($cacheKey, function() use ($termID, $status, $page) {
|
$perPage = 20;
|
$offset = ($page - 1) * $perPage;
|
|
$query = $this->requestsTable->where(['term_id' => $termID]);
|
|
if ($status !== 'all') {
|
$query = $query->where(['status' => $status]);
|
}
|
|
$total = $query->countResults();
|
|
$requests = $query
|
->orderBy('created_date', 'DESC')
|
->limit($perPage, $offset)
|
->getResults(ARRAY_A);
|
|
return $this->success([
|
'requests' => $requests,
|
'total' => $total,
|
'pages' => ceil($total / $perPage),
|
'page' => $page,
|
'per_page' => $perPage
|
]);
|
});
|
}
|
|
/**
|
* Handle membership request (create/accept/reject)
|
*/
|
public function handleRequest(WP_REST_Request $request): WP_REST_Response
|
{
|
$action = $request->get_param('action');
|
$userID = $request->get_param('user');
|
$termID = (int)$request->get_param('term_id');
|
|
return match($action) {
|
'create' => $this->createRequest($userID, $termID),
|
'accept', 'reject' => $this->processRequest($request),
|
default => $this->error('Invalid action')
|
};
|
}
|
|
protected function createRequest(int $userID, int $termID): WP_REST_Response
|
{
|
if (!term_exists($termID, jvbCheckBase($this->taxonomy))) {
|
return $this->error('Invalid ' . $this->taxonomy);
|
}
|
|
// Check if request already exists
|
$existing = $this->requestsTable
|
->where([
|
'user_id' => $userID,
|
'term_id' => $termID
|
])
|
->first();
|
|
if ($existing) {
|
return $this->error('Request already exists');
|
}
|
|
$contentID = get_user_meta($userID, BASE . 'link', true);
|
if (!$contentID) {
|
return $this->error('User profile not found');
|
}
|
|
try {
|
$requestID = $this->requestsTable->create([
|
'user_id' => $userID,
|
'content_id' => $contentID,
|
'term_id' => $termID,
|
'status' => 'requested',
|
'created_date' => current_time('mysql')
|
]);
|
|
if ($requestID) {
|
$this->notifyTermManagers($termID, $userID, 'membership_request');
|
$this->cache->flush();
|
|
return $this->success([
|
'message' => 'Request submitted successfully',
|
'request_id' => $requestID
|
]);
|
}
|
|
return $this->error('Failed to create request');
|
} catch (Exception $e) {
|
$this->logError('createRequest', [
|
'error' => $e->getMessage(),
|
'user_id' => $userID,
|
'term_id' => $termID
|
]);
|
|
return $this->error('An error occurred');
|
}
|
}
|
|
protected function processRequest(WP_REST_Request $request): WP_REST_Response
|
{
|
$action = $request->get_param('action');
|
$requestID = (int)$request->get_param('request_id');
|
$notes = $request->get_param('notes') ?? '';
|
|
if (!$requestID) {
|
return $this->error('Request ID required');
|
}
|
|
$requestData = $this->requestsTable
|
->where(['id' => $requestID])
|
->first(ARRAY_A);
|
|
if (!$requestData) {
|
return $this->error('Request not found');
|
}
|
|
$status = $action === 'accept' ? 'accepted' : 'rejected';
|
|
try {
|
$this->requestsTable->transaction(function($table) use ($requestID, $status, $notes, $requestData) {
|
// Update request
|
$table->where(['id' => $requestID])->updateResults([
|
'status' => $status,
|
'notes' => $notes,
|
'updated_date' => current_time('mysql')
|
]);
|
|
// If accepted, add member via action
|
if ($status === 'accepted') {
|
do_action(
|
BASE . 'add_user_to_term',
|
$requestData['user_id'],
|
$requestData['term_id'],
|
$this->taxonomy,
|
'member'
|
);
|
}
|
});
|
|
// Notify user
|
JVB()->notification()->addNotification(
|
$requestData['user_id'],
|
'request_' . $status,
|
[
|
'term_id' => $requestData['term_id'],
|
'taxonomy' => $this->taxonomy,
|
'notes' => $notes
|
]
|
);
|
|
$this->cache->flush();
|
|
return $this->success(['message' => 'Request ' . $status]);
|
|
} catch (Exception $e) {
|
$this->logError('processRequest', [
|
'error' => $e->getMessage(),
|
'request_id' => $requestID,
|
'action' => $action
|
]);
|
|
return $this->error('Failed to process request');
|
}
|
}
|
|
/**
|
* Update ownership/management permissions
|
*/
|
public function updatePermissions(WP_REST_Request $request): WP_REST_Response
|
{
|
$termID = (int)$request->get_param('term_id');
|
$targetUserID = (int)$request->get_param('target_user');
|
$role = $request->get_param('role');
|
$grant = (bool)$request->get_param('grant');
|
|
if (!$this->checkUser($targetUserID)) {
|
return $this->error('Invalid target user');
|
}
|
|
$roleManager = JVB()->roles();
|
|
try {
|
$result = match($role) {
|
'owner' => $grant
|
? $roleManager->grantOwnership($targetUserID, $termID, $this->taxonomy)
|
: $roleManager->revokeOwnership($targetUserID, $termID, $this->taxonomy),
|
'manager' => $grant
|
? $roleManager->grantManagement($targetUserID, $termID, $this->taxonomy)
|
: $roleManager->revokeManagement($targetUserID, $termID, $this->taxonomy),
|
default => false
|
};
|
|
if ($result) {
|
$this->cache->flush();
|
|
return $this->success([
|
'message' => ucfirst($role) . ' permissions ' . ($grant ? 'granted' : 'revoked')
|
]);
|
}
|
|
return $this->error('Failed to update permissions');
|
|
} catch (Exception $e) {
|
$this->logError('updatePermissions', [
|
'error' => $e->getMessage(),
|
'term_id' => $termID,
|
'target_user' => $targetUserID,
|
'role' => $role
|
]);
|
|
return $this->error('An error occurred');
|
}
|
}
|
|
/**
|
* Notify term managers
|
*/
|
protected function notifyTermManagers(int $termID, int $actingUserID, string $notificationType): void
|
{
|
$roleManager = JVB()->roles();
|
$managers = $roleManager->getManagedTerms($actingUserID, $this->taxonomy);
|
|
$term = get_term($termID, jvbCheckBase($this->taxonomy));
|
|
foreach ($managers as $managerID) {
|
JVB()->notification()->addNotification(
|
$managerID,
|
$notificationType,
|
[
|
'user_id' => $actingUserID,
|
'term_id' => $termID,
|
'term_name' => $term->name ?? 'Unknown',
|
'taxonomy' => $this->taxonomy
|
]
|
);
|
}
|
}
|
}
|