Jake Vanderwerf
5 days ago a9b3b28d001941921aa70d37fdc87c758a163a44
inc/rest/routes/VoteRoutes.php
@@ -1,7 +1,12 @@
<?php
namespace JVBase\rest\routes;
use JVBase\JVB;
use JVBase\rest\RestRouteManager;
use JVBase\managers\CustomTable;
use JVBase\managers\KarmaManager;
use JVBase\registrar\Registrar;
use JVBase\rest\Response;
use JVBase\rest\Rest;
use JVBase\rest\Route;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
@@ -10,12 +15,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 +31,24 @@
     */
    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)
         ->register();
    }
    /**
@@ -47,303 +58,62 @@
     */
    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')??'');
      $registrar = Registrar::getInstance($content);
      if (!$registrar || !$registrar->hasFeature('karma')) {
         return Response::validationError(['message' => __('Invalid content', 'jvb')]);
      }
        $vote = sanitize_text_field($request->get_param('vote')??'');
      $itemID = absint($request->get_param('item_id')??0);
        if ($itemID === 0 || !in_array($vote, [true, false, null])) {
         return Response::validationError(['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'));
      $user = absint($request->get_param('user'));
      if (!$this->userCheck($user)) {
         return Response::validationError(['message' => __('User doesn\'t match. Bot?', '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),
        ];
      $type = $registrar->getType()??false;
      if (!$type) {
         return Response::validationError(['message' => __('Invalid content type', 'jvb')]);
      }
        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
        ]);
      $man = KarmaManager::getInstance($type);
      if (!$man) {
         return Response::validationError(['message' => __('Karma not set up', 'jvb')]);
      }
      [$success, $message] = $man->vote($user, $itemID, $content, $vote);
      return match ($success) {
         true, 'partial' => Response::success(['message' => $message]),
         default => Response::error($message),
      };
    }
    /**
     * @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);
        }
   public function getVotes(WP_REST_Request $request): WP_REST_Response
   {
      $user = absint($request->get_param('user') ?? get_current_user_id());
      $cache = $this->cache->get($user);
      if ($cache) {
         return Response::success($cache);
      }
        global $wpdb;
        $votes = [];
      $votes = KarmaManager::getUserVotes($user);
        foreach (jvbGlobalKarma() as $type => $content_types) {
            foreach ($content_types as $content_type) {
                $table_name = $wpdb->prefix . BASE . 'karma_'. $content_type;
      $this->cache->set($user, $votes);
                // 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);
    }
      return Response::success($votes);
   }
}