| | |
| | | parent::__construct(); |
| | | |
| | | $this->initTypes(); |
| | | |
| | | if ($this->hasMemberApproval) { |
| | | add_action('user_register', [$this, 'handleNewUserRegistration'], 10, 2); |
| | | } |
| | | |
| | | add_action('jvb_cleanup_expired_approvals', [$this, 'cleanupExpiredApprovals']); |
| | | } |
| | | |
| | | protected function initTypes():void |
| | | { |
| | | $this->userTypes = []; |
| | | $this->termTypes = []; |
| | | $this->allTypes = []; |
| | | if ($this->hasMemberApproval) { |
| | | $this->userTypes = Registrar::getFeatured('approve_new', 'user'); |
| | | $this->allTypes = $this->userTypes; |
| | |
| | | ->register(); |
| | | } |
| | | |
| | | /** |
| | | * Handler for user registration |
| | | * |
| | | * @param int $user_id New user ID |
| | | * @param object $user the new user object |
| | | * |
| | | * @return void |
| | | */ |
| | | public function handleNewUserRegistration(int $user_id, object $user): void |
| | | { |
| | | $intersect = array_intersect( |
| | | array_map(fn($role) => BASE.$role, $this->userTypes), |
| | | (array) $user->roles |
| | | ); |
| | | |
| | | if (!empty($intersect)) { |
| | | $user->add_cap('skip_moderation', false); |
| | | $this->createArtistApprovalRequest($user_id); |
| | | } |
| | | } |
| | | |
| | | |
| | | /** |
| | | * @param WP_REST_Request $request |
| | |
| | | */ |
| | | protected function handleVote(string $type, string $vote, int $request_id, int $user_id, string $notes = ''): bool |
| | | { |
| | | if (!in_array($vote, ['approve', 'reject'])) { |
| | | if (!in_array($vote, ['approve', 'reject', 'dismiss'])) { |
| | | return false; |
| | | } |
| | | |
| | | $requestTable = $this->getTableName($type, 'requests'); |
| | | $voteTable = $this->getTableName($type, 'votes'); |
| | | |
| | | $requests = CustomTable::for($requestTable); |
| | | $votes = CustomTable::for($voteTable); |
| | | |
| | | try { |
| | | return $requests->transaction(function($requests) use ($votes, $request_id, $user_id, $vote, $notes, $type) { |
| | | // Get the approval request |
| | | $request = $requests->where(['id' => $request_id])->first(); |
| | | |
| | | if (!$request || $request->status !== 'pending') { |
| | | throw new Exception("Invalid approval request"); |
| | | } |
| | | |
| | | // Check if user already voted |
| | | $existingVote = $votes->where([ |
| | | 'request_id' => $request_id, |
| | | 'user_id' => $user_id |
| | | ])->first(); |
| | | |
| | | if ($existingVote) { |
| | | if ($existingVote->vote !== $vote) { |
| | | // Update vote |
| | | $votes->where(['id' => $existingVote->id]) |
| | | ->updateResults(['vote' => $vote]); |
| | | return true; |
| | | } |
| | | throw new Exception("User has already voted on this request"); |
| | | } |
| | | |
| | | // Insert new vote |
| | | $votes->create([ |
| | | 'request_id' => $request_id, |
| | | 'user_id' => $user_id, |
| | | 'vote' => $vote, |
| | | 'notes' => $notes, |
| | | ]); |
| | | |
| | | // Update request based on vote type |
| | | $user = get_userdata($user_id); |
| | | |
| | | if ($vote === 'approve') { |
| | | $this->handleApproval($requests, $request, $request_id, $user, $type); |
| | | } else { |
| | | $this->handleRejection($requests, $request, $request_id, $user, $type); |
| | | } |
| | | |
| | | return true; |
| | | }); |
| | | } catch (Exception $e) { |
| | | $this->logError('handleVote', [ |
| | | 'error' => $e->getMessage(), |
| | | 'user_id' => $user_id, |
| | | 'request_id' => $request_id, |
| | | 'vote' => $vote |
| | | ]); |
| | | return false; |
| | | } |
| | | $result = JVB()->approvals()->markApproval($request_id, $user_id, $type, $vote, $notes); |
| | | return $result['success']; |
| | | } |
| | | |
| | | /** |
| | | * Handle approval vote logic |
| | | */ |
| | | protected function handleApproval(CustomTable $table, object $request, int $request_id, $user, string $type): void |
| | | { |
| | | $approvers = json_decode($request->approved_by, true) ?: []; |
| | | $approvers[$user->ID] = [ |
| | | 'name' => $user->display_name, |
| | | 'voted' => current_time('mysql') |
| | | ]; |
| | | |
| | | $table->where(['id' => $request_id])->updateResults([ |
| | | 'current_approvals' => $request->current_approvals + 1, |
| | | 'approved_by' => json_encode($approvers), |
| | | 'expires_at' => $this->rebuildExpiryDate() |
| | | ]); |
| | | |
| | | // Check if threshold met |
| | | if ($request->current_approvals + 1 >= $request->required_approvals) { |
| | | match ($type) { |
| | | 'term' => $this->makeTermLive($request), |
| | | default => $this->completeVerification($request_id, $type), |
| | | }; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Handle rejection vote logic |
| | | */ |
| | | protected function handleRejection(CustomTable $table, object $request, int $request_id, $user, string $type): void |
| | | { |
| | | $rejecters = json_decode($request->rejected_by, true) ?: []; |
| | | $rejecters[$user->ID] = [ |
| | | 'name' => $user->display_name, |
| | | 'voted' => current_time('mysql') |
| | | ]; |
| | | |
| | | $table->where(['id' => $request_id])->updateResults([ |
| | | 'current_rejections' => $request->current_rejections + 1, |
| | | 'rejected_by' => json_encode($rejecters), |
| | | 'expires_at' => $this->rebuildExpiryDate() |
| | | ]); |
| | | |
| | | // Check if threshold met |
| | | if ($request->current_rejections + 1 >= $request->required_approvals) { |
| | | match ($type) { |
| | | 'term' => $this->makeTermUnalive($request), |
| | | default => $this->denyVerification($request_id, $type), |
| | | }; |
| | | } |
| | | } |
| | | protected function rebuildExpiryDate() |
| | | { |
| | | return date('Y-m-d H:i:s', strtotime("+{$this->expiryDays} days", time())); |
| | | } |
| | | |
| | | /** |
| | | * @param string $type user/artist or term |
| | | * @param array $request |
| | | * |
| | | * @return bool|int |
| | | */ |
| | | protected function createApprovalRequest(string $type, array $request): int |
| | | { |
| | | $tableName = $this->getTableName($type, 'requests'); |
| | | |
| | | $id = CustomTable::for($tableName)->create($request); |
| | | |
| | | if (!$id) { |
| | | throw new Exception('Failed to create approval request'); |
| | | } |
| | | |
| | | return $id; |
| | | } |
| | | |
| | | /************* |
| | | * Artist Approvals |
| | | ************/ |
| | | /** |
| | | * Create artist approval request |
| | | * |
| | | * @param int $user_id User ID to be approved |
| | | * |
| | | * @return int|false Request ID or false on failure |
| | | */ |
| | | /** |
| | | * Create artist approval request - REFACTORED |
| | | */ |
| | | public function createArtistApprovalRequest(int $user_id): int|false |
| | | { |
| | | $userRole = jvbUserRole($user_id); |
| | | $tableName = $this->getTableName($userRole, 'requests'); |
| | | $table = CustomTable::for($tableName); |
| | | |
| | | try { |
| | | return $table->transaction(function($table) use ($user_id) { |
| | | // Check for existing request |
| | | $existing = $table->where(['user_id' => $user_id])->first(); |
| | | |
| | | if ($existing) { |
| | | return $existing->id; |
| | | } |
| | | |
| | | $user_data = get_userdata($user_id); |
| | | |
| | | return $table->create([ |
| | | 'user_id' => $user_id, |
| | | 'status' => 'pending', |
| | | 'expires_at' => date('Y-m-d H:i:s', strtotime('+30 days')), |
| | | 'current_approvals' => 0, |
| | | 'current_rejections' => 0, |
| | | 'required_approvals' => 3, // From config |
| | | 'approved_by' => json_encode([]), |
| | | 'rejected_by' => json_encode([]), |
| | | ]); |
| | | }); |
| | | } catch (Exception $e) { |
| | | $this->logError('createArtistApprovalRequest', [ |
| | | 'error' => $e->getMessage(), |
| | | 'user_id' => $user_id |
| | | ]); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Mark an artist as verified |
| | | * |
| | | * @param int $user_id The user to verify |
| | | * @param int $verified_by ID of user who verified them (optional) |
| | | * |
| | | * @return bool Success status |
| | | */ |
| | | public function verifyArtist(int $user_id, int $verified_by = 0):bool |
| | | { |
| | | $user = get_userdata($user_id); |
| | | |
| | | // Check if user has the artist role |
| | | if (!array_intersect(array_map(function ($role) { return BASE.$role; }, $this->userTypes), $user->roles)) { |
| | | return false; |
| | | } |
| | | |
| | | // Add the capability |
| | | $user->add_cap('skip_moderation', true); |
| | | |
| | | // Store verification metadata |
| | | update_user_meta($user_id, BASE . 'verification_date', current_time('mysql')); |
| | | if ($verified_by) { |
| | | update_user_meta($user_id, BASE . 'verified_by', $verified_by); |
| | | } |
| | | |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * Mark an artist as verified |
| | | * |
| | | * @param int $user_id The user to verify |
| | | * @param int $verified_by ID of user who verified them (optional) |
| | | * |
| | | * @return bool Success status |
| | | */ |
| | | public function unverifyArtist(int $user_id, int $verified_by = 0):bool |
| | | { |
| | | $user = get_userdata($user_id); |
| | | |
| | | // Check if user has the artist role |
| | | if (!array_intersect(array_map(function ($role) { return BASE.$role; }, $this->userTypes), $user->roles)) { |
| | | return false; |
| | | } |
| | | |
| | | // Add the capability |
| | | $user->add_cap('skip_moderation', false); |
| | | |
| | | // Store verification metadata |
| | | update_user_meta($user_id, BASE . 'unverification_date', current_time('mysql')); |
| | | if ($verified_by) { |
| | | update_user_meta($user_id, BASE . 'unverified_by', $verified_by); |
| | | } |
| | | |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * Record an approval vote for an artist |
| | | * |
| | | * @param int $user_id User casting the approval vote |
| | |
| | | return $this->handleVote(jvbUserRole($user_id), $vote, $request_id, $user_id, $notes); |
| | | } |
| | | |
| | | /** |
| | | * Complete verification - REFACTORED |
| | | */ |
| | | protected function completeVerification(int $request_id, string $type = 'artist'): void |
| | | { |
| | | $tableName = $this->getTableName($type, 'requests'); |
| | | $table = CustomTable::for($tableName); |
| | | |
| | | $table->where(['id' => $request_id])->updateResults([ |
| | | 'status' => 'approved', |
| | | 'approved_at' => current_time('mysql') |
| | | ]); |
| | | |
| | | $request = $table->where(['id' => $request_id])->first(); |
| | | |
| | | if ($request && $request->user_id) { |
| | | $user = new \WP_User($request->user_id); |
| | | $user->add_cap('skip_moderation', true); |
| | | |
| | | JVB()->notification()->addNotification( |
| | | $request->user_id, |
| | | 'approval_granted', |
| | | ['message' => 'Your account has been verified!'] |
| | | ); |
| | | } |
| | | |
| | | $this->cache->flush(); |
| | | } |
| | | |
| | | protected function denyVerification(int $request_id, string $type = 'artist'): void |
| | | { |
| | | $tableName = $this->getTableName($type, 'requests'); |
| | | $table = CustomTable::for($tableName); |
| | | |
| | | $table->where(['id' => $request_id])->updateResults([ |
| | | 'status' => 'rejected', |
| | | 'rejected_at' => current_time('mysql') |
| | | ]); |
| | | |
| | | $request = $table->where(['id' => $request_id])->first(); |
| | | |
| | | if ($request && $request->user_id) { |
| | | JVB()->notification()->addNotification( |
| | | $request->user_id, |
| | | 'approval_denied', |
| | | ['message' => 'Your verification request was not approved.'] |
| | | ); |
| | | } |
| | | |
| | | $this->cache->flush(); |
| | | } |
| | | |
| | | /** |
| | | * Get verification details for a request |
| | |
| | | */ |
| | | public function getVerificationDetails(int $requestID, string $type): array|false |
| | | { |
| | | $requestTable = CustomTable::for($this->getTableName($type, 'requests')); |
| | | $voteTable = CustomTable::for($this->getTableName($type, 'votes')); |
| | | |
| | | $request = $requestTable->where(['id' => $requestID])->first(ARRAY_A); |
| | | |
| | | $request = JVB()->approvals()->getRequest($requestID, $type); |
| | | if (!$request) { |
| | | return false; |
| | | } |
| | | |
| | | // Get the votes for this request |
| | | $votes = $voteTable |
| | | ->where(['request_id' => $request['id']]) |
| | | ->orderBy('created_at', 'ASC') |
| | | ->getResults(ARRAY_A); |
| | | $votes = JVB()->approvals()->getVotes($requestID, $type); |
| | | |
| | | // Join with user data for display names |
| | | foreach ($votes as &$vote) { |
| | | $user = get_userdata($vote['user_id']); |
| | | $vote['approver_name'] = $user ? $user->display_name : 'Unknown'; |
| | | $vote['approver_name'] = $user ? jvbGetUsername($vote['user_id']) : 'Someone'; |
| | | } |
| | | |
| | | return [ |
| | |
| | | ]; |
| | | } |
| | | |
| | | /************* |
| | | * Term Approvals |
| | | ************/ |
| | | public function voteForTerm(int $user_id, int $request_id, string $vote, string $notes = ''):bool |
| | | { |
| | | return $this->handleVote('term', $vote, $user_id, $request_id, $notes); |
| | | } |
| | | |
| | | /** |
| | | * Publish an approved term |
| | | * |
| | | * @param object $request Approval request object |
| | | * |
| | | * @return boolean Success or failure |
| | | */ |
| | | protected function makeTermLive(object $request): bool |
| | | { |
| | | try { |
| | | $taxonomy = $request->taxonomy; |
| | | $term_name = $request->name; |
| | | $parent = $request->parent; |
| | | |
| | | $result = wp_insert_term($term_name, $taxonomy, [ |
| | | 'parent' => $parent |
| | | ]); |
| | | |
| | | if (is_wp_error($result)) { |
| | | throw new Exception($result->get_error_message()); |
| | | } |
| | | |
| | | $term_id = $result['term_id']; |
| | | |
| | | // Update request status |
| | | CustomTable::for($this->getTableName('term', 'requests')) |
| | | ->where(['id' => $request->id]) |
| | | ->updateResults([ |
| | | 'status' => 'approved', |
| | | 'created_term' => $term_id |
| | | ]); |
| | | |
| | | $userIDs = []; |
| | | $approvedBy = []; |
| | | $approvers = json_decode($request->approved_by, true) ?: []; |
| | | $requesters = json_decode($request->requested_by, true) ?: []; |
| | | $rejectors = json_decode($request->rejected_by, true) ?: []; |
| | | |
| | | foreach (array_merge($requesters, $approvers, $rejectors) as $user_id => $info) { |
| | | $userIDs[] = $user_id; |
| | | } |
| | | foreach ($approvers as $user_id => $info) { |
| | | $approvedBy[] = $info['name']; |
| | | } |
| | | |
| | | $approvedBy = jvbCommaList($approvedBy); |
| | | |
| | | JVB()->notification()->addNotification( |
| | | $userIDs, |
| | | 'term_approved', |
| | | [ |
| | | 'term_id' => $term_id, |
| | | 'term_name' => $term_name, |
| | | 'taxonomy' => $taxonomy, |
| | | 'approved_by' => $approvedBy |
| | | ] |
| | | ); |
| | | |
| | | return true; |
| | | } catch (Exception $e) { |
| | | $this->logError('makeTermLive', [ |
| | | 'error' => $e->getMessage(), |
| | | 'request_id' => $request->id, |
| | | 'term_name' => $term_name ?? '', |
| | | 'taxonomy' => $taxonomy ?? '' |
| | | ]); |
| | | |
| | | return false; |
| | | } |
| | | } |
| | | /** |
| | | * Reject a proposed term |
| | | * |
| | | * @param object $request request object |
| | | * |
| | | * @return boolean Success or failure |
| | | */ |
| | | protected function makeTermUnalive(object $request): bool |
| | | { |
| | | try { |
| | | // Update request status |
| | | CustomTable::for($this->getTableName('term', 'requests')) |
| | | ->where(['id' => $request->id]) |
| | | ->updateResults([ |
| | | 'status' => 'rejected' |
| | | ]); |
| | | |
| | | $userIDs = []; |
| | | $rejectedBy = []; |
| | | |
| | | $approvers = json_decode($request->approved_by, true) ?: []; |
| | | $requesters = json_decode($request->requested_by, true) ?: []; |
| | | $rejectors = json_decode($request->rejected_by, true) ?: []; |
| | | |
| | | foreach (array_merge($requesters, $approvers, $rejectors) as $user_id => $info) { |
| | | $userIDs[] = $user_id; |
| | | } |
| | | foreach ($rejectors as $user_id => $info) { |
| | | $rejectedBy[] = $info['name']; |
| | | } |
| | | |
| | | $rejectedBy = jvbCommaList($rejectedBy); |
| | | |
| | | JVB()->notification()->addNotification( |
| | | $userIDs, |
| | | 'term_rejected', |
| | | [ |
| | | 'term_name' => $request->name, |
| | | 'taxonomy' => $request->taxonomy, |
| | | 'rejected_by' => $rejectedBy |
| | | ] |
| | | ); |
| | | |
| | | return true; |
| | | } catch (Exception $e) { |
| | | $this->logError('makeTermUnalive', [ |
| | | 'error' => $e->getMessage(), |
| | | 'request_id' => $request->id, |
| | | 'term_name' => $request->name ?? '', |
| | | 'taxonomy' => $request->taxonomy ?? '' |
| | | ]); |
| | | |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Create a new term approval request |
| | |
| | | int $parent = 0, |
| | | int $required_approvals = 3 |
| | | ): int|false { |
| | | $table = CustomTable::for($this->getTableName('term', 'requests')); |
| | | |
| | | try { |
| | | return $table->transaction(function($table) use ($user_id, $taxonomy, $name, $parent, $required_approvals) { |
| | | // Check for existing request |
| | | $existing = $table->where([ |
| | | 'name' => $name, |
| | | 'taxonomy' => $taxonomy, |
| | | 'parent' => $parent, |
| | | 'status' => 'pending' |
| | | ])->first(); |
| | | |
| | | if ($existing) { |
| | | $requestedBy = json_decode($existing->requested_by, true) ?: []; |
| | | |
| | | if (isset($requestedBy[$user_id])) { |
| | | return (int)$existing->id; |
| | | } |
| | | |
| | | $requestedBy[$user_id] = get_userdata($user_id)->display_name; |
| | | |
| | | $table->where(['id' => $existing->id])->updateResults([ |
| | | 'requested_by' => json_encode($requestedBy) |
| | | ]); |
| | | |
| | | return (int)$existing->id; |
| | | } |
| | | |
| | | // Create new request |
| | | return $this->createApprovalRequest('term', [ |
| | | 'taxonomy' => $taxonomy, |
| | | 'name' => $name, |
| | | 'parent' => $parent ?: null, |
| | | 'status' => 'pending', |
| | | 'required_approvals' => $required_approvals, |
| | | 'current_approvals' => 0, |
| | | 'current_rejections' => 0, |
| | | 'requested_by' => json_encode([$user_id => get_userdata($user_id)->display_name]), |
| | | 'expires_at' => date('Y-m-d H:i:s', strtotime('+30 days')), |
| | | ]); |
| | | }); |
| | | } catch (Exception $e) { |
| | | $this->logError('createTermApprovalRequest', [ |
| | | 'error' => $e->getMessage(), |
| | | 'user_id' => $user_id, |
| | | 'taxonomy' => $taxonomy, |
| | | 'name' => $name |
| | | ]); |
| | | |
| | | return false; |
| | | } |
| | | } |
| | | /** |
| | | * Clean up expired approval requests and notify admin |
| | | * |
| | | * @return void |
| | | */ |
| | | public function cleanupExpiredApprovals(): void |
| | | { |
| | | $now = current_time('mysql'); |
| | | |
| | | foreach ($this->allTypes as $type) { |
| | | $tableName = $this->getTableName($type, 'requests'); |
| | | |
| | | CustomTable::for($tableName)->query( |
| | | "UPDATE {table} |
| | | SET status = 'expired' |
| | | WHERE status = 'pending' |
| | | AND expires_at < %s", |
| | | [$now] |
| | | ); |
| | | } |
| | | |
| | | $this->cache->flush(); |
| | | $result = JVB()->approvals()->createApproval( |
| | | $user_id, |
| | | $taxonomy, |
| | | $name, |
| | | $parent |
| | | ); |
| | | return $result['success']; |
| | | } |
| | | |
| | | protected function getTableName(string $type, string $suffix): string |
| | |
| | | |
| | | private function getUserApprovals(string $status = 'pending'): array |
| | | { |
| | | |
| | | $table = CustomTable::for($this->getTableName('artist', 'requests')); |
| | | |
| | | $query = $table; |