defineTables(); if (empty($this->tables)) { return; } } protected function defineTables():void { $types = Registrar::getFeatured('approve_new'); foreach ($types as $type) { $requests = CustomTable::for("approval_{$type}_requests"); $requests->setColumns([ 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', 'user_id' => "{$requests->getUserIDType()} NOT NULL", 'name' => 'varchar(255) NOT NULL', 'parent_id' => 'bigint(20) unsigned DEFAULT 0', 'status' => "ENUM('pending', 'approved', 'rejected', 'appealed', 'expired') DEFAULT 'pending'", 'required_approvals'=> 'int unsigned DEFAULT 3', 'approvals' => 'int unsigned DEFAULT 0', 'rejections'=> 'int unsigned DEFAULT 0', 'expires_at' => 'datetime DEFAULT NULL', 'created_at' => 'datetime DEFAULT CURRENT_TIMESTAMP', 'updated_at' => 'datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', 'approved_by' => 'json DEFAULT NULL', 'rejected_by' => 'json DEFAULT NULL', 'created_item_id' => 'bigint(20) unsigned DEFAULT NULL', ]); $requests->setKeys([ ['key' => 'PRIMARY', 'value' => 'id'], ['key' => 'UNIQUE', 'value' => '`unique_request` (`user_id`, `name`)'], '`name` (`name`)', '`name_parent` (`name`, `parent_id`)', '`status` (`status`)', '`expiring_requests` (`status`, `expires_at`)' ]); $base = BASE; $requests->setConstraints([ "CONSTRAINTS `{$base}{$type}_approval_requester` FOREIGN KEY (`user_id`) REFERENCES `{$requests->getUserTable()}` (`ID`) ON DELETE CASCADE", "CONSTRAINTS `{$base}{$type}_approval_parent_term` FOREIGN KEY (`parent_id`) REFERENCES `{$requests->getTermTable()}` (`term_id`) ON DELETE CASCADE" ]); $requests->defineTable(); $votes = CustomTable::for("approval_{$type}_votes"); $votes->setColumns([ 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', 'request_id' => 'bigint(20) unsigned NOT NULL', 'user_id' => "{$votes->getUserIDType()} NOT NULL", 'vote' => "ENUM('approve', 'reject', 'dismiss') NOT NULL", 'notes' => 'text DEFAULT NULL', 'created_at' => 'datetime DEFAULT CURRENT_TIMESTAMP', 'updated_at' => 'datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP' ]); $votes->setKeys([ ['key' => 'PRIMARY', 'value' => 'id'], ['key' => 'UNIQUE', 'value' => '`unique_vote` (`request_id`, `user_id`)'], '`user_votes` (`user_id`)' ]); $votes->setConstraints([ "CONSTRAINT `{$base}{$type}_user_approval_request` FOREIGN KEY (`request_id`) REFERENCES `{$requests->getFullTableName()} (`id`) ON DELETE CASCADE", "CONSTRAINT `{$base}{$type}_user_approval_voter` FOREIGN KEY (`user_id`) REFERENCES `{$votes->getUserTable()}` (`ID`) ON DELETE CASCADE" ]); $votes->defineTable(); $this->tables[$type] = [ 'requests' => $requests, 'votes' => $votes ]; } } public function canApprove(int $userID, string $type):bool { $type = jvbNoBase($type); if (!array_key_exists($type, $this->tables)) { return false; } return user_can($userID, "approve_{$type}"); } protected function requests(string $type):?CustomTable { $type = jvbNoBase($type); if (!array_key_exists($type, $this->tables)){ return null; } return $this->tables[$type]['requests']??null; } protected function votes(string $type):?CustomTable { $type = jvbNoBase($type); if (!array_key_exists($type, $this->tables)){ return null; } return $this->tables[$type]['votes']??null; } public function response(bool $success, string $msg = ''):array { $return = ['success' => $success]; if ($msg !== '') { $return['message'] = $msg; } return $return; } public function createApproval(int $userID, string $type, string $name, int $parentID = 0):array { if (!$this->canApprove($userID, $type)) { return $this->response(false, 'Cannot create request'); } $type = jvbNoBase($type); if (!array_key_exists($type, $this->tables)) { $this->response(false, 'Invalid type'); } $table = $this->requests($type); if (!$table) { return $this->response(false, 'Invalid type'); } //Check for existing request $existing = $table->get([ 'name' => sanitize_text_field($name) ]); if (!$existing) { $this->approve($userID, $existing->id); return $this->response(true, 'Existing request - added your vote!'); } if ($parentID > 0) { $parent = get_term($parentID, jvbCheckBase($type)); if (!$parent || is_wp_error($parent)) { $parent = 0; } } $date = new DateTime('+' . $this->expiresAt . ' days'); $expires = $date->format('Y-m-d H:i:s'); $result = $table->create([ 'user_id' => $userID, 'name' => sanitize_text_field($name), 'parent_id' => $parentID, 'expires_at'=> $expires ]); return $this->response((bool)$result, $result?'Request created successfully' : 'Request could not be created'); } public function markApproval(int $request_id, int $userID, string $type, string $vote, ?string $note = null):array { if (!$this->canApprove($userID, $type)) { return $this->response(false, 'Sorry, you cannot do this.'); } $requests = $this->requests($type); if (!$requests) { return $this->response(false, 'Invalid type'); } $request = $requests->get(['request_id' => $request_id]); if (!$request) { return $this->response(false, 'Invalid request id'); } $votes = $this->votes($type); if (!$votes) { return $this->response(false, 'Invalid type'); } if (!in_array($vote, ['approve', 'reject', 'dismiss'])) { return $this->response(false, 'Invalid vote'); } $response = $votes->findOrCreate([ 'request_id' => $request_id, 'user_id' => $userID ], [ 'vote' => 'approve', 'notes' => $note ? sanitize_text_field($note) : $note ]); if (!$response) { return $this->response(false, 'Could not store vote for some reason.'); } switch ($vote) { case 'reject': $request['rejections']++; break; case 'approve': $request['approvals']++; break; default: return $this->response(true, 'Successfully dismissed approval request.'); } if ($request['rejections'] === $request['required_approvals']) { $this->finalizeDenial($request_id, $type); } else if ($request['approvals'] === $request['required_approvals']) { $this->finalizeApproval($request_id, $type); } $updated = $requests->update([ 'rejections'=> $request['rejections'], 'approvals' => $request['approvals'] ], [ 'request_id' => $request_id ]); if ($updated) { return $this->response(true, 'Successfully voted'); } return $this->response(false, 'Could not finalize vote for some reason'); } public function approveRequest(int $request_id, int $userID, string $type, ?string $note = null):array { return $this->markApproval($request_id, $userID, $type, 'approve', $note); } public function denyRequest(int $request_id, int $userID, string $type, string $note = ''):array { return $this->markApproval($request_id, $userID, $type, 'reject', $note); } public function dismissRequest(int $request_id, int $userID, string $type, string $note = ''):array { return $this->markApproval($request_id, $userID, $type, 'dismiss', $note); } public function isApproved(int $request_id, string $type):bool { $table = $this->requests($type); if (!$table) { return false; } $request = $table->get(['request_id' => $request_id]); if (!$request) { return false; } $votes = $this->votes($type); if (!$votes) { return false; } $total = $votes->getMany([ 'request_id' => $request_id, 'status' => ['IN' => ['approve', 'reject']] ]); $approvals = count(array_filter($total, function($item) { return $item->status === 'approve'; })); $denials = count(array_filter($total, function($item) { return $item->status === 'reject'; })); if ($denials === $this->requiredVotes) { return false; } return $approvals === $this->requiredVotes; } public function finalizeApproval(int $request_id, string $type):void { $requests = $this->requests($type); $request = $requests->get(['request_id' => $request_id]); if (!$request){ error_log('Could not finalize denial for request: '.$request_id); return; } $updatedID = null; $registrar = Registrar::getInstance($type); if ($registrar->getType() === 'term') { $term = wp_insert_term($request['name'], $registrar->getBased(), ['parent' => $request['parent_id']]); if (!is_wp_error($term)) { $updatedID = $term['term_id']; } } elseif ($registrar->getType() === 'user') { } $updates = [ 'status' => 'approved' ]; if ($updatedID) { $updates['created_item_id'] = $updatedID; } $updated = $requests->update( $updates, [ 'request_id' => $request_id ]); JVB()->notification()->addNotification($request['user_id'], $type.'approved', null, 'Your suggestion "'.$request['name'].'" was denied.'); } public function finalizeDenial(int $request_id, string $type):void { $requests = $this->requests($type); $request = $requests->get(['request_id' => $request_id]); if (!$request){ error_log('Could not finalize denial for request: '.$request_id); return; } $updated = $requests->update([ 'status' => 'rejected' ], [ 'request_id' => $request_id ]); JVB()->notification()->addNotification($request['user_id'], $type.'denied', null, 'Your suggestion "'.$request['name'].'" was denied.'); } public function getApprovalRequests(int $userID, ?string $type = null):array { $return = []; if ($type) { if (!array_key_exists($type, $this->tables)) { return $return; } $requests = $this->requests($type); $active = $requests->pluck('request_id', [ 'status' => 'pending' ]); $votes = $this->votes($type); $voted = $votes->pluck('request_id',[ 'user_id' => $userID ]); $all = array_diff($active, $voted); $allRequests = $requests->getMany([ 'where' => [ 'request_id' => ['IN' => $all] ] ]); $return = array_map(function ($request) use ($type) { return $this->format_for_notification($request, $type); }, $allRequests); } else { foreach ($this->tables as $type => $tables) { $requests = $tables['requests'];; $active = $requests->pluck('request_id', [ 'status' => 'pending', ]); $votes = $tables['votes']; $voted = $votes->pluck('request_id', [ 'user_id' => $userID ]); $all = array_diff($active, $voted); $allRequests = $requests->getMany([ 'where' => [ 'request_id' => ['IN' => $all] ] ]); $return = array_merge($return, array_map(function ($request) use ($type) { return $this->format_for_notification($request, $type); }, $allRequests)); } } return $return; } protected function format_for_notification(array $request, string $type): array { $registrar = Registrar::getInstance($type); $message = ''; $user = get_userdata($request['user_id']); if ($user) { $message = $user->first_name; } else { $message = 'Someone'; } $message .= ' thinks we need a new '.$registrar->getSingular().'. They think: "'.$request['name'].'" '; if ($request['parent_id'] > 0) { $parent = get_term($request['parent_id'], $registrar->getBased()); if ($parent && !is_wp_error($parent)) { $message .= 'And nest it under: '.$parent->name.' '; } } $message .= ' What say you?'; return [ 'id' => $request['id'], 'type' => $type.'_new', 'message' => $message, 'created_at'=> $request['created_at'], 'status' => 'pending', 'requires_action' => true, 'action_taken' => false, 'icon' => $registrar->getIcon(), 'priority' => 'normal', 'target' => [ 'id' => $request['id'], 'type' => 'approval' ], 'actions' => [ 'approve' => 'Approve', 'deny' => 'Deny', 'dismiss' => 'Dismiss' ] ]; } }