| | |
| | | |
| | | namespace JVBase\rest\routes; |
| | | |
| | | use JVBase\JVB; |
| | | use JVBase\rest\RestRouteManager; |
| | | use JVBase\managers\CacheManager; |
| | | use WP_User; |
| | | use JVBase\managers\CustomTable; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\rest\PermissionHandler; |
| | | use JVBase\rest\Rest; |
| | | use JVBase\rest\Route; |
| | | use JVBase\rest\Response; |
| | | use JVBase\base\Site; |
| | | use WP_REST_Request; |
| | | use WP_REST_Response; |
| | | use Exception; |
| | |
| | | exit; // Exit if accessed directly |
| | | } |
| | | |
| | | class ApprovalRoutes extends RestRouteManager |
| | | class ApprovalRoutes extends Rest |
| | | { |
| | | protected array $userTypes; |
| | | protected array $termTypes; |
| | | protected array $allTypes; |
| | | protected array $requestTables; |
| | | protected array $voteTables; |
| | | |
| | | protected int $expiryDays = 7; |
| | | protected bool $hasMemberApproval = false; |
| | | |
| | | public function __construct() |
| | | { |
| | | $this->cache_name = 'approvals'; |
| | | $this->cacheName = 'approvals'; |
| | | $this->hasMemberApproval = Site::membership() && Site::membership()->has('member_verified'); |
| | | parent::__construct(); |
| | | |
| | | $this->initTypes(); |
| | | |
| | | if (jvbSiteHasMemberApproval()) { |
| | | add_action('user_register', [$this, 'handleNewUserRegistration'], 10, 2); |
| | | } |
| | | |
| | | add_action('jvb_cleanup_expired_approvals', [$this, 'cleanupExpiredApprovals']); |
| | | } |
| | | |
| | | protected function initTypes():void |
| | | { |
| | | $approvals = jvbApprovalTypes(); |
| | | $this->userTypes = []; |
| | | $this->termTypes = []; |
| | | if (jvbSiteHasMemberApproval()) { |
| | | $this->userTypes = array_filter( |
| | | array_keys($approvals), |
| | | function ($item) { |
| | | return $item !== 'term'; |
| | | } |
| | | ); |
| | | $this->allTypes = []; |
| | | if ($this->hasMemberApproval) { |
| | | $this->userTypes = Registrar::withFeature('approve_new', 'user'); |
| | | $this->allTypes = $this->userTypes; |
| | | } |
| | | if (jvbSiteHasTermApproval()) { |
| | | $this->termTypes = $approvals['term']??[]; |
| | | if (Site::has('term_approval')) { |
| | | $this->termTypes = Registrar::withFeature('approve_new', 'term'); |
| | | $this->allTypes[] = 'term'; |
| | | } |
| | | } |
| | | |
| | | public function registerRoutes():void |
| | | { |
| | | register_rest_route($this->namespace, '/approvals', [ |
| | | [ |
| | | 'methods' => 'GET', |
| | | 'callback' => [ $this, 'getApprovals' ], |
| | | 'permission_callback' => [ $this, 'checkPermission' ] |
| | | ], |
| | | [ |
| | | 'methods' => 'POST', |
| | | 'callback' => [ $this, 'handleApprovalAction' ], |
| | | 'permission_callback' => [ $this, 'checkPermission' ] |
| | | ] |
| | | ]); |
| | | } |
| | | |
| | | /** |
| | | * @param WP_REST_Request $request The REST request |
| | | * |
| | | * @return bool |
| | | */ |
| | | public function checkPermission(WP_REST_Request $request):bool |
| | | { |
| | | $userID = get_current_user_id(); |
| | | if (!user_can($userID, 'skip_moderation')) { |
| | | return false; |
| | | } |
| | | |
| | | return parent::checkPermission($request); |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 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( |
| | | function ($role) { |
| | | return BASE.$role; |
| | | }, |
| | | $this->userTypes |
| | | ), |
| | | (array) $user->roles |
| | | ); |
| | | if (!empty($intersect)) { |
| | | // Mark as unverified initially |
| | | $user->add_cap('skip_moderation', false); |
| | | // Create approval request |
| | | $this->createArtistApprovalRequest($user_id); |
| | | } |
| | | Route::for('approvals') |
| | | ->get([$this, 'getApprovals']) |
| | | ->args([ |
| | | 'user' => 'integer|required', |
| | | 'type' => 'string', |
| | | 'status' => 'string|enum:pending,approved,rejected,expired', |
| | | ]) |
| | | ->auth(PermissionHandler::combine(['user', 'verified'])) |
| | | ->rateLimit(30) |
| | | ->post([$this, 'handleAction']) |
| | | ->args([ |
| | | 'user' => 'integer|required', |
| | | 'request_id' => 'integer|required', |
| | | 'action' => 'string|required|enum:approve,reject', |
| | | 'type' => 'string|required', |
| | | 'notes' => 'string', |
| | | ]) |
| | | ->auth(PermissionHandler::combine(['user', 'verified'])) |
| | | ->rateLimit(3) |
| | | ->register(); |
| | | } |
| | | |
| | | |
| | |
| | | * |
| | | * @return WP_REST_Response |
| | | */ |
| | | public function handleApprovalAction(WP_REST_Request $request):WP_REST_Response |
| | | public function handleAction(WP_REST_Request $request):WP_REST_Response |
| | | { |
| | | $data = $request->get_params(); |
| | | $request_id = $data['request_id'] ?? 0; |
| | | $user_id = (array_key_exists('user', $data) && |
| | | is_numeric($data['user'])) ? |
| | | (int) $data['user'] : get_current_user_id(); |
| | | $action = (array_key_exists('action', $data) && in_array($data['action'], [ |
| | | 'approve', |
| | | 'reject' |
| | | ])) ? $data['action'] : false; |
| | | $data = $request->get_params(); |
| | | $request_id = absint($data['request_id']); |
| | | $user_id = absint($data['user']); |
| | | $action = sanitize_text_field($data['action']); |
| | | $type = sanitize_text_field($data['type']); |
| | | $notes = sanitize_text_field($data['notes'] ?? ''); |
| | | |
| | | $type = (array_key_exists('type', $data) && |
| | | in_array($data['type'], $this->allTypes)) ? |
| | | $data['type'] : |
| | | false; |
| | | $notes = (array_key_exists('notes', $data)) ? sanitize_text_field($data['notes']) : ''; |
| | | if (!in_array($type, $this->allTypes)) { |
| | | return Response::validationError(['message' => 'Invalid type']); |
| | | } |
| | | |
| | | if ($action && $request_id !== 0 && $type) { |
| | | $result = $this->handleVote($type, $action, $request_id, $user_id, $notes); |
| | | return new WP_REST_Response([ |
| | | 'success' => $result, |
| | | 'message' => $result ? 'Vote recorded successfully' : 'Failed to record vote' |
| | | ], $result ? 200 : 500); |
| | | } |
| | | return new WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => 'Invalid action or request ID' |
| | | ], 400); |
| | | $result = $this->handleVote($type, $action, $request_id, $user_id, $notes); |
| | | |
| | | return $result |
| | | ? Response::success(['message' => 'Vote recorded successfully']) |
| | | : Response::error('Failed to record vote'); |
| | | } |
| | | |
| | | protected function getRequestTable(string $type, string $prefix):string |
| | | { |
| | | return match ($type) { |
| | | 'term' => $prefix . BASE . 'approval_term_requests', |
| | | default => $prefix . BASE . 'approval_' . $type . '_requests', |
| | | }; |
| | | } |
| | | protected function getVoteTable(string $type, string $prefix):string |
| | | { |
| | | return match ($type) { |
| | | 'term' => $prefix . BASE . 'approval_term_votes', |
| | | default => $prefix . BASE . 'approval_' . $type . '_votes', |
| | | }; |
| | | } |
| | | /** |
| | | * Artist and Term Approvals |
| | | */ |
| | | protected function handleVote(string $type, string $vote, int $request_id, int $user_id, string $notes = ''):bool |
| | | { |
| | | if (!in_array($vote, ['approve', 'reject'])) { |
| | | return false; |
| | | } |
| | | global $wpdb; |
| | | $table = $this->getRequestTable($type, $wpdb->prefix); |
| | | $votes = $this->getVoteTable($type, $wpdb->prefix); |
| | | protected function handleVote(string $type, string $vote, int $request_id, int $user_id, string $notes = ''): bool |
| | | { |
| | | if (!in_array($vote, ['approve', 'reject', 'dismiss'])) { |
| | | return false; |
| | | } |
| | | $result = JVB()->approvals()->markApproval($request_id, $user_id, $type, $vote, $notes); |
| | | return $result['success']; |
| | | } |
| | | |
| | | |
| | | try { |
| | | $request = $wpdb->get_row($wpdb->prepare( |
| | | "SELECT * FROM $table WHERE id = %d", |
| | | $request_id |
| | | )); |
| | | if (!$request || $request->status !== 'pending') { |
| | | throw new Exception("Invalid approval request"); |
| | | } |
| | | |
| | | $already_voted = $wpdb->get_row($wpdb->prepare( |
| | | "SELECT * FROM $votes WHERE request_id = %d AND user_id = %d", |
| | | $request_id, |
| | | $user_id |
| | | )); |
| | | |
| | | if ($already_voted && $already_voted->vote !== $vote) { |
| | | $wpdb->update( |
| | | $votes, |
| | | [ |
| | | 'vote' => $vote, |
| | | ], |
| | | [ |
| | | 'id' => $already_voted->id |
| | | ] |
| | | ); |
| | | return true; |
| | | } elseif ($already_voted) { |
| | | throw new Exception("User has already voted on this request"); |
| | | } |
| | | |
| | | $result = $wpdb->insert( |
| | | $votes, |
| | | [ |
| | | 'request_id' => $request_id, |
| | | 'user_id' => $user_id, |
| | | 'vote' => $vote, |
| | | 'notes' => $notes, |
| | | 'created_at' => current_time('mysql') |
| | | ] |
| | | ); |
| | | if (!$result) { |
| | | throw new Exception("Failed to record vote"); |
| | | } |
| | | |
| | | $user = get_userdata($user_id); |
| | | if ($vote === 'approve') { |
| | | $approvers = json_decode($request->approved_by, true)?:[]; |
| | | |
| | | $approvers[$user_id] = [ |
| | | 'name' => $user->display_name, |
| | | 'voted' => current_time('mysql') |
| | | ]; |
| | | $wpdb->update( |
| | | $table, |
| | | [ |
| | | 'current_approvals' => $request->current_approvals + 1, |
| | | 'updated_at' => current_time('mysql'), |
| | | 'approved_by' => $approvers, |
| | | 'expires_at' => $this->rebuildExpiryDate() |
| | | ], |
| | | [ |
| | | 'id' => $request_id |
| | | ] |
| | | ); |
| | | if ($request->current_approvals + 1 >= $request->required_approvals) { |
| | | switch ($type) { |
| | | case 'user': |
| | | case 'artist': |
| | | $this->completeVerification($request_id); |
| | | break; |
| | | case 'term': |
| | | $this->makeTermLive($request); |
| | | break; |
| | | } |
| | | } |
| | | } elseif ($vote === 'reject') { |
| | | $rejecters = json_decode($request->rejected_by, true)?:[]; |
| | | |
| | | $rejecters[$user_id] = [ |
| | | 'name' => $user->display_name, |
| | | 'voted' => current_time('mysql') |
| | | ]; |
| | | $wpdb->update( |
| | | $table, |
| | | [ |
| | | 'current_rejections' => $request->current_rejections + 1, |
| | | 'rejected_by' => $rejecters, |
| | | 'updated_at' => current_time('mysql'), |
| | | 'expires_at' => $this->rebuildExpiryDate() |
| | | ], |
| | | [ |
| | | 'id' => $request_id |
| | | ] |
| | | ); |
| | | if ($request->current_rejections + 1 >= $request->required_approvals) { |
| | | switch ($type) { |
| | | case 'user': |
| | | case 'artist': |
| | | $this->denyVerification($request_id); |
| | | break; |
| | | case 'term': |
| | | $this->makeTermUnalive($request); |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | |
| | | $wpdb->query('COMMIT'); |
| | | |
| | | return true; |
| | | } catch (Exception $e) { |
| | | $wpdb->query('ROLLBACK'); |
| | | |
| | | JVB()->error() |
| | | ->log( |
| | | '[ApprovalRoutes]:handleVote', |
| | | "Error creating '.$type.' approval request: " . $e->getMessage(), |
| | | [ |
| | | 'user_id' => $user_id, |
| | | 'request_id' => $request_id, |
| | | 'vote' => $vote |
| | | ] |
| | | ); |
| | | return false; |
| | | } |
| | | } |
| | | 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):bool|int |
| | | { |
| | | global $wpdb; |
| | | |
| | | $table = $this->getRequestTable($type, $wpdb->prefix); |
| | | |
| | | $result = $wpdb->insert( |
| | | $table, |
| | | $request |
| | | ); |
| | | |
| | | if (!$result) { |
| | | throw new Exception($wpdb->last_error); |
| | | } |
| | | return $wpdb->insert_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 |
| | | */ |
| | | public function createArtistApprovalRequest(int $user_id):int|false |
| | | { |
| | | global $wpdb; |
| | | $wpdb->query('START TRANSACTION'); |
| | | |
| | | try { |
| | | //Check for existing first |
| | | $table = $this->getRequestTable(jvbUserRole($user_id), $wpdb->prefix); |
| | | |
| | | // Verify this is not a duplicate request |
| | | $existing = $wpdb->get_var($wpdb->prepare( |
| | | "SELECT id FROM $table |
| | | WHERE user_id = %d", |
| | | $user_id |
| | | )); |
| | | |
| | | if ($existing) { |
| | | return $existing; |
| | | } |
| | | |
| | | $user_data = get_userdata($user_id); |
| | | $request = [ |
| | | 'user_id' => $user_id, |
| | | 'status' => 'pending', |
| | | 'expires_at' => date('Y-m-d H:i:s', strtotime('+30 days')), |
| | | 'created_at' => current_time('mysql'), |
| | | 'updated_at' => current_time('mysql'), |
| | | 'name' => $user_data->display_name, |
| | | 'email' => $user_data->user_email, |
| | | ]; |
| | | |
| | | $result = $this->createApprovalRequest('user', $request); |
| | | |
| | | if (!$result) { |
| | | throw new Exception($wpdb->last_error); |
| | | } |
| | | |
| | | $wpdb->query('COMMIT'); |
| | | |
| | | return $result; |
| | | } catch (Exception $e) { |
| | | $wpdb->query('ROLLBACK'); |
| | | JVB()->error() |
| | | ->log( |
| | | '[ApprovalRoutes]:createArtistApprovalRequest', |
| | | "Error creating artist approval request: " . $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); |
| | | } |
| | | |
| | | /** |
| | | * Mark an artist as verified after receiving required approvals |
| | | * |
| | | * @param int $request_id The approval request ID |
| | | * |
| | | * @return bool Success status |
| | | */ |
| | | public function completeVerification(int $request_id):bool |
| | | { |
| | | global $wpdb; |
| | | $approval_table = $wpdb->prefix . $this->userRequests; |
| | | |
| | | // Get the request details |
| | | $request = $wpdb->get_row($wpdb->prepare( |
| | | "SELECT * FROM $approval_table WHERE id = %d", |
| | | $request_id |
| | | )); |
| | | |
| | | if (!$request || $request->status !== 'pending') { |
| | | return false; |
| | | } |
| | | |
| | | // Check if enough approvals have been collected |
| | | if ($request->current_approvals < $request->required_approvals) { |
| | | return false; |
| | | } |
| | | |
| | | // Start a transaction |
| | | $wpdb->query('START TRANSACTION'); |
| | | |
| | | try { |
| | | // Get the user ID from the request |
| | | $user_id = $request->user_id; |
| | | |
| | | $this->verifyArtist($user_id, $request->current_approvals); |
| | | |
| | | // Update the request status |
| | | $updated = $wpdb->update( |
| | | $approval_table, |
| | | [ |
| | | 'status' => 'approved', |
| | | 'updated_at' => current_time('mysql') |
| | | ], |
| | | [ 'id' => $request_id ] |
| | | ); |
| | | |
| | | if ($updated === false) { |
| | | throw new Exception("Failed to update approval request status"); |
| | | } |
| | | |
| | | // Notify the user they've been verified |
| | | JVB()->notification()->addNotification( |
| | | $user_id, |
| | | 'artist_approved', |
| | | [ |
| | | 'request_id' => $request_id, |
| | | 'approval_date' => current_time('mysql') |
| | | ] |
| | | ); |
| | | |
| | | $wpdb->query('COMMIT'); |
| | | |
| | | return true; |
| | | } catch (Exception $e) { |
| | | $wpdb->query('ROLLBACK'); |
| | | JVB()->error() |
| | | ->log( |
| | | '[ApprovalRoutes]:completeVerification', |
| | | "Error verifying user: " . $e->getMessage(), |
| | | [ |
| | | 'user_id' => $user_id, |
| | | ] |
| | | ); |
| | | |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | public function denyVerification(int $request_id):bool |
| | | { |
| | | global $wpdb; |
| | | $approval_table = $wpdb->prefix . $this->userRequests; |
| | | |
| | | // Get the request details |
| | | $request = $wpdb->get_row($wpdb->prepare( |
| | | "SELECT * FROM $approval_table WHERE id = %d", |
| | | $request_id |
| | | )); |
| | | |
| | | if (!$request || $request->status !== 'pending') { |
| | | return false; |
| | | } |
| | | |
| | | // Check if enough approvals have been collected |
| | | if ($request->current_rejections < $request->required_approvals) { |
| | | return false; |
| | | } |
| | | |
| | | // Start a transaction |
| | | $wpdb->query('START TRANSACTION'); |
| | | |
| | | try { |
| | | // Get the user ID from the request |
| | | $user_id = $request->user_id; |
| | | |
| | | $this->unverifyArtist($user_id, $request->rejected_by); |
| | | |
| | | // Update the request status |
| | | $updated = $wpdb->update( |
| | | $approval_table, |
| | | [ |
| | | 'status' => 'rejected', |
| | | 'updated_at' => current_time('mysql') |
| | | ], |
| | | [ 'id' => $request_id ] |
| | | ); |
| | | |
| | | if ($updated === false) { |
| | | throw new Exception("Failed to update approval request status"); |
| | | } |
| | | |
| | | // Notify the user they've been verified |
| | | JVB()->notification()->addNotification( |
| | | $user_id, |
| | | 'artist_rejected', |
| | | [ |
| | | 'request_id' => $request_id, |
| | | 'approval_date' => current_time('mysql') |
| | | ] |
| | | ); |
| | | |
| | | $wpdb->query('COMMIT'); |
| | | |
| | | return true; |
| | | } catch (Exception $e) { |
| | | $wpdb->query('ROLLBACK'); |
| | | JVB()->error() |
| | | ->log( |
| | | '[ApprovalRoutes]:denyVerification', |
| | | "Error removing artist verification status: " . $e->getMessage(), |
| | | [ |
| | | 'user_id' => $user_id |
| | | ] |
| | | ); |
| | | |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Get verification details for a request |
| | |
| | | * |
| | | * @return array|false Verification details or false if not verified |
| | | */ |
| | | public function getVerificationDetails(int $requestID, string $type):array|false |
| | | { |
| | | global $wpdb; |
| | | public function getVerificationDetails(int $requestID, string $type): array|false |
| | | { |
| | | $request = JVB()->approvals()->getRequest($requestID, $type); |
| | | if (!$request) { |
| | | return false; |
| | | } |
| | | $votes = JVB()->approvals()->getVotes($requestID, $type); |
| | | |
| | | $approval_table = $this->getRequestTable($type, $wpdb->prefix); |
| | | $votes_table = $this->getVoteTable($type, $wpdb->prefix); |
| | | // Join with user data for display names |
| | | foreach ($votes as &$vote) { |
| | | $user = get_userdata($vote['user_id']); |
| | | $vote['approver_name'] = $user ? jvbGetUsername($vote['user_id']) : 'Someone'; |
| | | } |
| | | |
| | | // Get the approval request |
| | | $request = $wpdb->get_row($wpdb->prepare( |
| | | "SELECT * FROM $approval_table |
| | | WHERE id = %d |
| | | ORDER BY updated_at DESC", |
| | | $requestID |
| | | ), ARRAY_A); |
| | | return [ |
| | | 'request' => $request, |
| | | 'votes' => $votes, |
| | | 'verification_date' => $request['updated_at'], |
| | | ]; |
| | | } |
| | | |
| | | if (!$request) { |
| | | return false; |
| | | } |
| | | |
| | | // Get the votes for this request |
| | | $votes = $wpdb->get_results($wpdb->prepare( |
| | | "SELECT v.*, u.display_name as approver_name |
| | | FROM $votes_table v |
| | | WHERE v.request_id = %d |
| | | ORDER BY v.created_at", |
| | | $request['id'] |
| | | ), ARRAY_A); |
| | | |
| | | return [ |
| | | 'request' => $request, |
| | | 'votes' => $votes, |
| | | 'verification_date' => $request['updated_at'], |
| | | ]; |
| | | } |
| | | |
| | | /************* |
| | | * 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 |
| | | { |
| | | global $wpdb; |
| | | |
| | | try { |
| | | // Get term data from request |
| | | $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']; |
| | | |
| | | $table = $this->getRequestTable('term', $wpdb); |
| | | // Update request status |
| | | $wpdb->update( |
| | | $table, |
| | | [ |
| | | 'status' => 'approved', |
| | | 'updated_at' => current_time('mysql'), |
| | | 'created_term' => $term_id |
| | | ], |
| | | [ 'id' => $request->id ] |
| | | ); |
| | | |
| | | $userIDs = []; |
| | | $approvedBy = []; |
| | | $approvors = json_decode($request->approved_by, true) ?: []; |
| | | $requesters = json_decode($request->requested_by, true) ?: []; |
| | | $rejectors = json_decode($request->rejected_by, true) ?: []; |
| | | foreach (array_merge($requesters, $approvors, $rejectors) as $user_id => $info) { |
| | | $userIDs[] = $user_id; |
| | | } |
| | | foreach ($approvors as $user_id => $info) { |
| | | $approvedBy[] = $info['name']; |
| | | } |
| | | |
| | | $approvedBy = jvbCommaList($approvedBy); |
| | | |
| | | // Notify the requester |
| | | JVB()->notification()->addNotification( |
| | | $userIDs, |
| | | 'term_approved', |
| | | [ |
| | | 'term_id' => $term_id, |
| | | 'term_name' => $term_name, |
| | | 'taxonomy' => $taxonomy, |
| | | 'approved_by' => $approvedBy |
| | | ] |
| | | ); |
| | | |
| | | return true; |
| | | } catch (Exception $e) { |
| | | JVB()->error() |
| | | ->log( |
| | | '[ApprovalRoutes]:makeTermLive', |
| | | "Error making term live: " . $e->getMessage(), |
| | | [ |
| | | 'request_id' => $request->id, |
| | | 'requester' => $request->requested_by, |
| | | '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 |
| | | { |
| | | global $wpdb; |
| | | |
| | | try { |
| | | // Update request status |
| | | $wpdb->update( |
| | | $this->getRequestTable('term', $wpdb), |
| | | [ |
| | | 'status' => 'rejected', |
| | | 'updated_at' => current_time('mysql'), |
| | | ], |
| | | [ 'id' => $request->id ] |
| | | ); |
| | | |
| | | $userIDs = []; |
| | | $rejectedBy = []; |
| | | |
| | | $approvors = json_decode($request->approved_by, true) ?: []; |
| | | $requesters = json_decode($request->requested_by, true) ?: []; |
| | | $rejectors = json_decode($request->rejected_by, true) ?: []; |
| | | foreach (array_merge($requesters, $approvors, $rejectors) as $user_id => $info) { |
| | | $userIDs[] = $user_id; |
| | | } |
| | | foreach ($rejectors as $user_id => $info) { |
| | | $rejectedBy[] = $info['name']; |
| | | } |
| | | |
| | | $rejectedBy = jvbCommaList($rejectedBy); |
| | | |
| | | // Notify the requester |
| | | JVB()->notification()->addNotification( |
| | | $userIDs, |
| | | 'term_rejected', |
| | | [ |
| | | 'term_name' => $request->name, |
| | | 'taxonomy' => $request->taxonomy, |
| | | 'rejected_by' => $rejectedBy |
| | | ] |
| | | ); |
| | | |
| | | return true; |
| | | } catch (Exception $e) { |
| | | JVB()->error() |
| | | ->log( |
| | | '[ApprovalRoutes]:makeTermUnalive', |
| | | "Error rejecting term: " . $e->getMessage(), |
| | | [ |
| | | 'request_id' => $request->id, |
| | | 'requester' => $request->requested_by, |
| | | 'term_name' => $request->name, |
| | | 'taxonomy' => $request->taxonomy |
| | | ] |
| | | ); |
| | | |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Create a new term approval request |
| | |
| | | * |
| | | * @return int|false Request ID or false on failure |
| | | */ |
| | | public function createTermApprovalRequest( |
| | | int $user_id, |
| | | string $taxonomy, |
| | | string $name, |
| | | int $parent = 0, |
| | | int $required_approvals = 3 |
| | | ):int|false { |
| | | global $wpdb; |
| | | $table = $this->getRequestTable('term', $wpdb); |
| | | public function createTermApprovalRequest( |
| | | int $user_id, |
| | | string $taxonomy, |
| | | string $name, |
| | | int $parent = 0, |
| | | int $required_approvals = 3 |
| | | ): int|false { |
| | | |
| | | try { |
| | | $wpdb->query('START TRANSACTION'); |
| | | // Step 1: Check if user already has a pending request for this term |
| | | $existing = $wpdb->get_row($wpdb->prepare( |
| | | "SELECT id, requested_by FROM $table |
| | | WHERE name = %s |
| | | AND taxonomy = %s |
| | | AND parent = %d |
| | | AND status = 'pending'", |
| | | $name, |
| | | $taxonomy, |
| | | $parent |
| | | )); |
| | | $result = JVB()->approvals()->createApproval( |
| | | $user_id, |
| | | $taxonomy, |
| | | $name, |
| | | $parent |
| | | ); |
| | | return $result['success']; |
| | | } |
| | | |
| | | if ($existing) { |
| | | // Decode the requested_by JSON field |
| | | $requestedBy = json_decode($existing->requested_by, true) ?: []; |
| | | protected function getTableName(string $type, string $suffix): string |
| | | { |
| | | return match ($type) { |
| | | 'term' => "approval_term_{$suffix}", |
| | | default => "approval_{$type}_{$suffix}", |
| | | }; |
| | | } |
| | | |
| | | // Check if this user has already requested this term |
| | | if (isset($requestedBy[$user_id])) { |
| | | $wpdb->query('COMMIT'); |
| | | return (int)$existing->id; |
| | | } |
| | | public function getApprovals(WP_REST_Request $request): WP_REST_Response |
| | | { |
| | | $user_id = absint($request->get_param('user')); |
| | | $type = sanitize_text_field($request->get_param('type') ?? 'all'); |
| | | $status = sanitize_text_field($request->get_param('status') ?? 'pending'); |
| | | |
| | | // Add this user to the requesters |
| | | $requestedBy[$user_id] = get_userdata($user_id)->display_name; |
| | | if (!$this->checkUser($user_id)) { |
| | | return $this->unauthorized(); |
| | | } |
| | | |
| | | // Update the request with the new requester |
| | | $updated = $wpdb->update( |
| | | $table, |
| | | ['requested_by' => json_encode($requestedBy)], |
| | | ['id' => $existing->id] |
| | | ); |
| | | $cacheKey = compact('user_id', 'type', 'status'); |
| | | |
| | | if (!$updated) { |
| | | throw new Exception($wpdb->last_error); |
| | | } |
| | | $result = $this->cache->remember($cacheKey, function() use ($type, $status) { |
| | | $data = []; |
| | | |
| | | $wpdb->query('COMMIT'); |
| | | return (int)$existing->id; |
| | | } |
| | | if ($type === 'user' || $type === 'all') { |
| | | $data['user_approvals'] = $this->getUserApprovals($status); |
| | | } |
| | | |
| | | $request = [ |
| | | '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')), |
| | | 'created_at' => current_time('mysql'), |
| | | 'updated_at' => current_time('mysql') |
| | | ]; |
| | | $result = $this->createApprovalRequest('term', $request); |
| | | if ($type === 'term' || $type === 'all') { |
| | | $data['term_approvals'] = $this->getTermApprovals($status); |
| | | } |
| | | |
| | | if (!$result) { |
| | | throw new Exception($wpdb->last_error); |
| | | } |
| | | return $data; |
| | | }); |
| | | |
| | | $request_id = $wpdb->insert_id; |
| | | $wpdb->query('COMMIT'); |
| | | return $request_id; |
| | | } catch (Exception $e) { |
| | | $wpdb->query('ROLLBACK'); |
| | | JVB()->error() |
| | | ->log( |
| | | '[ApprovalRoutes]:createTermApprovalRequest', |
| | | "Error creating term approval request: " . $e->getMessage(), |
| | | [ |
| | | 'user_id' => $user_id, |
| | | 'taxonomy' => $taxonomy, |
| | | 'name' => $name |
| | | ] |
| | | ); |
| | | return $this->success($result); |
| | | } |
| | | |
| | | return false; |
| | | } |
| | | } |
| | | /** |
| | | * Clean up expired approval requests and notify admin |
| | | * |
| | | * @return void |
| | | */ |
| | | public function cleanupExpiredApprovals(): void |
| | | { |
| | | global $wpdb; |
| | | $tables = array_map(function ($table) use ($wpdb){ |
| | | return $wpdb->prefix . BASE . 'approval_'.$table.'_requests'; |
| | | }, $this->allTypes); |
| | | private function getUserApprovals(string $status = 'pending'): array |
| | | { |
| | | |
| | | $table = CustomTable::for($this->getTableName('artist', 'requests')); |
| | | |
| | | foreach ($tables as $table) { |
| | | $wpdb->query($wpdb->prepare( |
| | | "UPDATE $table SET status = 'expired', updated_at = %s |
| | | WHERE status = 'pending' AND expires_at < %s", |
| | | current_time('mysql'), |
| | | current_time('mysql') |
| | | )); |
| | | } |
| | | $query = $table; |
| | | |
| | | // Clear caches |
| | | CacheManager::invalidateGroup('approvals'); |
| | | } |
| | | if ($status !== 'all') { |
| | | $query = $query->where(['status' => $status]); |
| | | } |
| | | |
| | | public function getApprovals(WP_REST_Request $request) |
| | | { |
| | | $user_id = get_current_user_id(); |
| | | $params = $request->get_params(); |
| | | $type = $params['type'] ?? 'all'; |
| | | $status = $params['status'] ?? 'pending'; |
| | | return $query->orderBy('created_at', 'DESC')->getResults(ARRAY_A); |
| | | } |
| | | |
| | | // Get appropriate approvals based on type |
| | | if ($type === 'user' || $type === 'all') { |
| | | $user_approvals = $this->getUserApprovals($status); |
| | | } else { |
| | | $user_approvals = []; |
| | | } |
| | | private function getTermApprovals(string $status = 'pending'): array |
| | | { |
| | | $table = CustomTable::for($this->getTableName('term', 'requests')); |
| | | |
| | | if ($type === 'term' || $type === 'all') { |
| | | $term_approvals = $this->getTermApprovals($status); |
| | | } else { |
| | | $term_approvals = []; |
| | | } |
| | | if ($status === 'all') { |
| | | return $table->orderBy('created_at', 'DESC')->getResults(ARRAY_A); |
| | | } |
| | | |
| | | return new WP_REST_Response([ |
| | | 'user_approvals' => $user_approvals, |
| | | 'term_approvals' => $term_approvals |
| | | ]); |
| | | } |
| | | |
| | | private function getUserApprovals(string $status = 'pending'): array |
| | | { |
| | | global $wpdb; |
| | | $table = $wpdb->prefix . $this->userRequests; |
| | | |
| | | // Build the status condition |
| | | $status_condition = ($status === 'all') ? |
| | | "status IN ('pending', 'approved', 'rejected', 'expired')" : |
| | | $wpdb->prepare("status = %s", $status); |
| | | |
| | | return $wpdb->get_results( |
| | | "SELECT * FROM $table |
| | | WHERE $status_condition |
| | | ORDER BY created_at DESC" |
| | | ); |
| | | } |
| | | |
| | | private function getTermApprovals(string $status = 'pending'): array |
| | | { |
| | | global $wpdb; |
| | | $table = $wpdb->prefix . $this->termRequests; |
| | | |
| | | // Build the status condition |
| | | $status_condition = ($status === 'all') ? |
| | | "status IN ('pending', 'approved', 'rejected', 'expired')" : |
| | | $wpdb->prepare("status = %s", $status); |
| | | |
| | | return $wpdb->get_results( |
| | | "SELECT * FROM $table |
| | | WHERE $status_condition |
| | | ORDER BY created_at DESC" |
| | | ); |
| | | } |
| | | return $table |
| | | ->where(['status' => $status]) |
| | | ->orderBy('created_at', 'DESC') |
| | | ->getResults(ARRAY_A); |
| | | } |
| | | } |