Jake Vanderwerf
2026-02-04 2127b1bdd73ecd2423e443992da4b442f5a3c1a3
inc/rest/routes/VoteRoutes.php
@@ -1,7 +1,11 @@
<?php
namespace JVBase\rest\routes;
use JVBase\JVB;
use JVBase\rest\RestRouteManager;
use JVBase\managers\CustomTable;
use JVBase\rest\Response;
use JVBase\rest\Rest;
use JVBase\rest\Route;
use JVBase\utility\Features;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
@@ -10,12 +14,12 @@
if (!defined('ABSPATH')) {
    exit; // Exit if accessed directly
}
class VoteRoutes extends RestRouteManager
class VoteRoutes extends Rest
{
    public function __construct()
    {
        $this->cache_name = 'karma';
        $this->cache_ttl = 86400;
        $this->cacheName = 'karma';
        $this->cacheTtl = DAY_IN_SECONDS;
        parent::__construct();
        add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
    }
@@ -26,18 +30,23 @@
     */
    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']
            ]
        ]);
      Route::for('vote')
         ->post([$this, 'handleVote'])
         ->args([
            'user' => 'integer|required',
            'id' => 'string|required',
            'item_id' => 'integer|required',
            'content' => 'string|required',
            'vote' => 'string|required|enum:up,down',
         ])
         ->auth('user')
         ->rateLimit(10)
         ->get([$this, 'getVotes'])
         ->args([
            'user' => 'integer',
         ])
         ->auth('user')
         ->rateLimit(30);
    }
    /**
@@ -47,42 +56,45 @@
     */
    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'),
            ]);
        $content = sanitize_text_field($request->get_param('content')??'');
      if ((!Features::forContent($content)->has('karma') && !Features::forTaxonomy($content)->has('karma') && !Features::forUser($content)->has('karma'))) {
         return Response::validationError(['message' => __('Invalid content', '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'),
            ]);
        $vote = sanitize_text_field($request->get_param('vote')??'');
      $itemID = absint($request->get_param('item_id')??0);
        if ($itemID === 0 || !in_array($vote, ['up', 'down'])) {
         return Response::validationError(['message' => __('Invalid item or vote attempt', 'jvb')]);
        }
      $user = absint($request->get_param('user'));
      if (!$this->userCheck($user)) {
         return Response::validationError(['message' => __('User doesn\'t match. Bot?', 'jvb')]);
      }
        $operation = sanitize_text_field($request->get_param('id'));
      $type = match(true) {
         array_key_exists($content, JVB_CONTENT) => 'post',
         array_key_exists($content, JVB_TAXONOMY) => 'term',
         array_key_exists($content, JVB_USER) => 'user',
         default => false
      };
      if (!$type) {
         return Response::validationError(['message' => __('Invalid content type', 'jvb')]);
      }
        $data = [
            'user'      => $user,
            'item_id'   => (int) $request->get_param('item_id'),
            'content'   => sanitize_text_field($request->get_param('content')),
            'vote'      => sanitize_text_field($vote),
            'item_id'   => $itemID,
            'content'   => $content,
         'type'      => $type,
            'vote'      => $vote,
        ];
        error_log('Final Vote Data: '.print_r($data, true));
        error_log('Operation: '.print_r($operation, true));
        $queue = JVB()->queue();
        $queue->queueOperation(
        $operationID = JVB()->queue()->add(
            'karmic',
            $user,
            $data,
@@ -91,12 +103,7 @@
                'operation_id'  => $operation,
            ]
        );
        return new WP_REST_Response([
            'success' => true,
            'message' => __('Operation queued', 'jvb'),
            'operation_id' => $operation
        ]);
      return $this->queued($operationID['operation_id']);
    }
    /**
@@ -105,19 +112,21 @@
     * @param array $data
     *
     * @return WP_Error|array
    * @throws Exception
     */
    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']);
      $item = match ($data['type']) {
         'post'   => get_post($data['item_id']),
         'term'   => get_term($data['item_id'], jvbCheckBase($data['content'])),
         'user'   => get_userdata($data['item_id']),
         default => false
      };
        if (!$item || is_wp_error($item)) {
            return [
                'success' => false,
@@ -125,122 +134,79 @@
            ];
        }
        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']
            )
        );
      $table = CustomTable::for('karma_' . $data['content']);
        // Begin transaction for data integrity
        $wpdb->query('START TRANSACTION');
        try {
            // Initialize response data
            $response_data = [
      return $table->transaction(function($table) use ($data) {
         // Check existing vote
         $existing = $table->where([
                'item_id' => $data['item_id'],
                'previous_vote' => $existing_vote,
                'new_vote' => $data['vote'],
                'updated' => false
            ];
            'user_id' => $data['user']
         ])->first();
            error_log('Existing: '.print_r($existing_vote, true));
            error_log('New: '.print_r($data['vote'], true));
         $existing_vote = $existing->vote ?? null;
         $new_vote = $data['vote'];
            // If user hasn't voted before
         // No previous vote - insert new
            if ($existing_vote === null) {
                // Insert new vote
                $inserted = $wpdb->insert(
                    $table_name,
                    [
            $inserted = $table->create([
                        'item_id' => $data['item_id'],
                        'user_id' => $data['user'],
                        'vote' => $data['vote'],
                        'date' => current_time('mysql')
                    ],
                    ['%d', '%d', '%s', '%s']
                );
               'vote' => $new_vote,
            ]);
                if (!$inserted) {
                    throw new Exception('Failed to record vote');
                }
                // Increment the appropriate vote counter
                $this->updateVoteCount($data['content'], $data['item_id'], $data['vote'], 1);
            $this->updateVoteCount($data['content'], $data['type'], $data['item_id'], $new_vote, 1);
            $this->cache->invalidate($data['user']);
                $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'],
                    ],
                    [
            return [
               'success' => true,
               'result' => __('Vote recorded', 'jvb'),
            ];
         }
         // Changing vote
         if ($existing_vote !== $new_vote) {
            $updated = $table->where([
                        'item_id' => $data['item_id'],
                        'user_id' => $data['user'],
                    ],
                    ['%s'],
                    ['%d', '%d']
                );
               'user_id' => $data['user']
            ])->updateResults(['vote' => $new_vote]);
                if (!$updated) {
                    throw new Exception('Failed to update vote');
                }
                $this->updateVoteCount($data['content'], $data['item_id'], $existing_vote, -1);
            // Decrement old, increment new
            $this->updateVoteCount($data['content'], $data['type'], $data['item_id'], $existing_vote, -1);
            $this->updateVoteCount($data['content'], $data['type'], $data['item_id'], $new_vote, 1);
            $this->cache->invalidate($data['user']);
                // Increment new vote type
                $this->updateVoteCount($data['content'], $data['item_id'], $data['vote'], 1);
            return [
               'success' => true,
               'result' => __('Vote updated', 'jvb'),
            ];
         }
                $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,
                    [
         // Toggle off - remove vote
         $deleted = $table->where([
                        'item_id' => $data['item_id'],
                        'user_id' => $data['user']
                    ],
                    ['%d', '%d']
                );
         ])->deleteResults();
                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');
         $this->updateVoteCount($data['content'], $data['type'], $data['item_id'], $existing_vote, -1);
         $this->cache->delete($data['user']);
            return [
                'success'   => true,
                'result'   => __('Vote handled', 'jvb'),
            'result' => __('Vote removed', 'jvb'),
            ];
        } catch (Exception $e) {
            $wpdb->query('ROLLBACK');
            return [
                'success'   => false,
                'result'   => $e->getMessage()
            ];
        }
      });
    }
    /**
@@ -251,49 +217,50 @@
     *
     * @return void
     */
    protected function updateVoteCount(string $content, int $ID, string $vote, int $value):void
    protected function updateVoteCount(string $content, string $type, int $ID, string $vote, int $value):void
    {
        global $karma;
        $key = ($vote==='down') ? BASE.'downvotes' : BASE.'upvotes';
        switch ($karma[$content]) {
      switch ($type) {
            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
         case 'response':
                $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
                ));
            CustomTable::for('responses')->query(
               "UPDATE {table}
                SET $field = GREATEST(0, $field + %d),
                    karma = (upvotes - downvotes)
                WHERE id = %d",
               [$value, $ID]
            );
                break;
        }
    }
@@ -305,35 +272,28 @@
     */
    public function getVotes(WP_REST_Request $request):WP_REST_Response
    {
        $user = $request->get_param('user')??get_current_user_id();
      $user = absint($request->get_param('user') ?? get_current_user_id());
        $cache = $this->cache->get($user);
        if ($cache) {
            return new WP_REST_Response($cache);
         return Response::success($cache);
        }
        global $wpdb;
        $votes = [];
        foreach (jvbGlobalKarma() as $type => $content_types) {
            foreach ($content_types as $content_type) {
                $table_name = $wpdb->prefix . BASE . 'karma_'. $content_type;
            $table = CustomTable::for('karma_' . $content_type);
                // Skip if table doesn't exist
                if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") != $table_name) {
            global $wpdb;
            if ($wpdb->get_var("SHOW TABLES LIKE '{$table->getFullTableName()}'") != $table->getFullTableName()) {
                    continue;
                }
                $results = $wpdb->get_results(
                    $wpdb->prepare(
                        "SELECT item_id, vote, date
                FROM {$table_name}
                WHERE user_id = %d",
                        $user
                    )
                );
            $results = $table->where(['user_id' => $user])->getResults();
                if ($results && !is_wp_error($results)) {
            if (!empty($results)) {
                    foreach ($results as $vote) {
                        $votes[$content_type][$vote->item_id] = $vote->vote;
                    }
@@ -341,9 +301,8 @@
            }
        }
        // Store in cache
        $this->cache->set($user, $votes);
        return new WP_REST_Response($votes);
      return Response::success($votes);
    }
}