cache_name = 'karma'; $this->cache_ttl = 86400; parent::__construct(); add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3); } /** * Registers vote routes * @return void */ public function registerRoutes():void { register_rest_route($this->namespace, 'vote', [ [ 'methods' => 'POST', 'callback' => [$this, 'handleVote'], 'permission_callback' => [$this, 'checkPermission'] ], [ 'methods' => 'GET', 'callback' => [$this, 'getVotes'], 'permission_callback' => [$this, 'checkPermission'] ] ]); } /** * @param WP_REST_Request $request * * @return WP_REST_Response */ public function handleVote(WP_REST_Request $request):WP_REST_Response { global $karma; if (!array_key_exists($request->get_param('content'), $karma)) { return new WP_REST_Response([ 'success' => false, 'message' => __('Invalid content', 'jvb'), ]); } $vote = $request->get_param('vote'); if (!$request->get_param('item_id') || !in_array($vote, ['up', 'down'])) { return new WP_REST_Response([ 'success' => false, 'message' => __('Invalid item or vote attempt', 'jvb'), ]); } //cursory sanitization $user = (int) $request->get_param('user'); if (!$this->userCheck($user)) { return new WP_REST_Response([ 'success' => false, 'message' => __('User doesn\'t match. Bot?', 'jvb'), ]); } $operation = sanitize_text_field($request->get_param('id')); $data = [ 'user' => $user, 'item_id' => (int) $request->get_param('item_id'), 'content' => sanitize_text_field($request->get_param('content')), 'vote' => sanitize_text_field($vote), ]; error_log('Final Vote Data: '.print_r($data, true)); error_log('Operation: '.print_r($operation, true)); $queue = JVB()->queue(); $queue->queueOperation( 'karmic', $user, $data, [ 'priority' => 'high', 'operation_id' => $operation, ] ); return new WP_REST_Response([ 'success' => true, 'message' => __('Operation queued', 'jvb'), 'operation_id' => $operation ]); } /** * @param WP_Error|array $result * @param object $operation * @param array $data * * @return WP_Error|array */ public function processOperation(WP_Error|array $result, object $operation, array $data):WP_Error|array { if ($operation->type !== 'karmic') { return $result; } // Get parameters from request global $karma; //Check if item exists $item = ($karma[$data['content']] === 'term') ? get_term($data['item_id'], BASE.$data['content']) : get_post($data['item_id']); if (!$item || is_wp_error($item)) { return [ 'success' => false, 'result' => __('Invalid item', 'jvb') ]; } global $wpdb; $table_name = $wpdb->prefix . BASE . 'karma_' . $data['content']; $key = $data['user']; error_log('Processing: '.print_r($data, true)); // Check if user has already voted on this post $existing_vote = $wpdb->get_var( $wpdb->prepare( "SELECT vote FROM {$table_name} WHERE item_id = %d AND user_id = %d", $data['item_id'], $data['user'] ) ); // Begin transaction for data integrity $wpdb->query('START TRANSACTION'); try { // Initialize response data $response_data = [ 'item_id' => $data['item_id'], 'previous_vote' => $existing_vote, 'new_vote' => $data['vote'], 'updated' => false ]; error_log('Existing: '.print_r($existing_vote, true)); error_log('New: '.print_r($data['vote'], true)); // If user hasn't voted before if ($existing_vote === null) { // Insert new vote $inserted = $wpdb->insert( $table_name, [ 'item_id' => $data['item_id'], 'user_id' => $data['user'], 'vote' => $data['vote'], 'date' => current_time('mysql') ], ['%d', '%d', '%s', '%s'] ); if (!$inserted) { throw new Exception('Failed to record vote'); } // Increment the appropriate vote counter $this->updateVoteCount($data['content'], $data['item_id'], $data['vote'], 1); $response_data['updated'] = true; $this->cache->invalidate($key); } elseif ($existing_vote !== $data['vote']) { // If user is changing their vote // Update existing vote $updated = $wpdb->update( $table_name, [ 'vote' => $data['vote'], ], [ 'item_id' => $data['item_id'], 'user_id' => $data['user'], ], ['%s'], ['%d', '%d'] ); if (!$updated) { throw new Exception('Failed to update vote'); } $this->updateVoteCount($data['content'], $data['item_id'], $existing_vote, -1); // Increment new vote type $this->updateVoteCount($data['content'], $data['item_id'], $data['vote'], 1); $response_data['updated'] = true; $this->cache->invalidate($key); } else { // If user is clicking the same vote again (toggle off) // Remove the vote $deleted = $wpdb->delete( $table_name, [ 'item_id' => $data['item_id'], 'user_id' => $data['user'] ], ['%d', '%d'] ); if (!$deleted) { throw new Exception('Failed to remove vote'); } // Decrement the vote counter $this->updateVoteCount($data['content'], $data['item_id'], $data['vote'], -1); $response_data['new_vote'] = null; $response_data['updated'] = true; $this->cache->invalidate($key); } $wpdb->query('COMMIT'); return [ 'success' => true, 'result' => __('Vote handled', 'jvb'), ]; } catch (Exception $e) { $wpdb->query('ROLLBACK'); return [ 'success' => false, 'result' => $e->getMessage() ]; } } /** * @param string $content * @param int $ID * @param string $vote * @param int $value * * @return void */ protected function updateVoteCount(string $content, int $ID, string $vote, int $value):void { global $karma; $key = ($vote==='down') ? BASE.'downvotes' : BASE.'upvotes'; switch ($karma[$content]) { case 'post': $old = (int) get_post_meta($ID, $key, true); $new = max(0, $old + $value); update_post_meta($ID, $key, $new); $up = (int) get_post_meta($ID, BASE.'upvotes', true); $down = (int) get_post_meta($ID, BASE.'downvotes', true); update_post_meta($ID, BASE.'karma', $up - $down); break; case 'term': $old = (int) get_term_meta($ID, $key, true); $new = max(0, $old + $value); update_term_meta($ID, $key, $new); $up = (int) get_term_meta($ID, BASE.'upvotes', true); $down = (int) get_term_meta($ID, BASE.'downvotes', true); update_term_meta($ID, BASE.'karma', $up - $down); break; case 'user': $old = (int) get_user_meta($ID, $key, true); $new = max(0, $old + $value); update_user_meta($ID, $key, $new); $up = (int) get_user_meta($ID, BASE.'upvotes', true); $down = (int) get_user_meta($ID, BASE.'downvotes', true); update_user_meta($ID, BASE.'karma', $up - $down); break; case 'response': // Direct table update for responses global $wpdb; $table = $wpdb->prefix . BASE . 'responses'; // Update vote count $field = str_replace(BASE, '', $key); $wpdb->query($wpdb->prepare( "UPDATE $table SET $field = GREATEST(0, $field + %d), karma = (upvotes - downvotes) WHERE id = %d", $value, $ID )); break; } } /** * @param WP_REST_Request $request * * @return WP_REST_Response */ public function getVotes(WP_REST_Request $request):WP_REST_Response { $user = $request->get_param('user')??get_current_user_id(); $cache = $this->cache->get($user); if ($cache) { return new WP_REST_Response($cache); } global $wpdb; $votes = []; foreach (jvbGlobalKarma() as $type => $content_types) { foreach ($content_types as $content_type) { $table_name = $wpdb->prefix . BASE . 'karma_'. $content_type; // Skip if table doesn't exist if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") != $table_name) { continue; } $results = $wpdb->get_results( $wpdb->prepare( "SELECT item_id, vote, date FROM {$table_name} WHERE user_id = %d", $user ) ); if ($results && !is_wp_error($results)) { foreach ($results as $vote) { $votes[$content_type][$vote->item_id] = $vote->vote; } } } } // Store in cache $this->cache->set($user, $votes); return new WP_REST_Response($votes); } }