| | |
| | | <?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 |
| | | { |
| | | // TODO: Implement registerRoutes() method. |
| | | 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 |
| | | ] |
| | | ); |
| | | } |
| | | } |
| | | } |