taxonomy = jvbNoBase($taxonomy); if ($taxonomy !== '' && Registrar::getInstance($this->taxonomy)) { $this->registrar = Registrar::getInstance($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 = Registrar::getFeatured('is_content', 'term'); foreach($taxonomies as $taxonomy) { $registry->register("{$taxonomy}_update", new TypeConfig( executor: $executor, )); $registrar = Registrar::getInstance($taxonomy); if ($registrar && $registrar->hasFeature('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->registrar->registrar->for; if ($this->registrar->hasFeature('track_changes') && !empty($content)) { foreach ($content as $contentType) { $tableName = "history_{$contentType}_{$this->taxonomy}"; $this->historyTable = CustomTable::for($tableName); break; // Only need one table } } if ($this->registrar->hasFeature('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 (!$this->registrar->hasFeature('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::combine([[$this, 'checkTermPermission']])) ->rateLimit(10) ->register(); // Member management (if track_changes enabled) if ($this->registrar->hasFeature('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::combine([[$this, 'checkTermPermission']])) ->rateLimit(5) ->register(); } // Membership requests (if verify_entry enabled) if ($this->registrar->hasFeature('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::combine([[$this, 'checkTermPermission']])) ->rateLimit(20) ->register(); 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) ->register(); } // Ownership/management (if is_ownable enabled) if ($this->registrar->hasFeature('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::combine([[$this, 'checkTermPermission']])) ->rateLimit(5) ->register(); } } /** * 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 ] ); } } }