| | |
| | | namespace JVBase\rest\routes; |
| | | |
| | | use JVBase\JVB; |
| | | use JVBase\rest\RestRouteManager; |
| | | use JVBase\managers\Cache; |
| | | use JVBase\rest\Rest; |
| | | use JVBase\managers\CustomTable; |
| | | use WP_REST_Request; |
| | | use WP_REST_Response; |
| | | use WP_Error; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | exit; |
| | | } |
| | | class ResponseRoutes extends RestRouteManager |
| | | |
| | | /** |
| | | * Response Routes |
| | | * |
| | | * Handles threaded responses/comments for news items |
| | | */ |
| | | class ResponseRoutes extends Rest |
| | | { |
| | | protected int $per_page; |
| | | protected false|object $manager = false; |
| | | protected int $perPage = 20; |
| | | protected CustomTable $table; |
| | | protected CustomTable $karmaTable; |
| | | |
| | | public function __construct() |
| | | { |
| | | $this->cache_name = 'responses'; |
| | | parent::__construct(); |
| | | $this->action = 'dash-'; |
| | | $this->per_page = 20; |
| | | public function __construct() |
| | | { |
| | | $this->cacheName = 'responses'; |
| | | $this->cacheTtl = 1800; // 30 minutes |
| | | parent::__construct(); |
| | | |
| | | add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3); |
| | | add_action('deleted_user', [$this, 'handleUserDeletion'], 10, 1); |
| | | } |
| | | $this->table = CustomTable::for('responses'); |
| | | $this->karmaTable = CustomTable::for('karma_response'); |
| | | |
| | | /** |
| | | * Registers response routes |
| | | * @return void |
| | | */ |
| | | public function registerRoutes():void |
| | | { |
| | | register_rest_route($this->namespace, '/response', [ |
| | | [ |
| | | 'methods' => 'GET', |
| | | 'callback' => [$this, 'getResponses'], |
| | | 'permission_callback' => [$this, 'checkPermission'] |
| | | ], |
| | | [ |
| | | 'methods' => 'POST', |
| | | 'callback' => [$this, 'handleResponseActions'], |
| | | 'permission_callback' => [$this, 'checkPermission'] |
| | | ] |
| | | ]); |
| | | } |
| | | add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3); |
| | | add_action('deleted_user', [$this, 'handleUserDeletion'], 10, 1); |
| | | } |
| | | |
| | | /** |
| | | * Get responses for a news post |
| | | * @param WP_REST_Request $request |
| | | * @return WP_REST_Response |
| | | */ |
| | | public function getResponses(WP_REST_Request $request):WP_REST_Response |
| | | { |
| | | $item_id = (int) $request->get_param('item_id'); |
| | | error_log('Item ID: '.print_r($item_id, true)); |
| | | if (!$item_id) { |
| | | return new WP_REST_Response([ |
| | | 'success'=> false, |
| | | 'message' => 'Missing item ID' |
| | | ]); |
| | | } |
| | | public function registerRoutes(): void |
| | | { |
| | | register_rest_route($this->namespace, '/response', [ |
| | | [ |
| | | 'methods' => 'GET', |
| | | 'callback' => [$this, 'getResponses'], |
| | | 'permission_callback' => 'is_user_logged_in' |
| | | ], |
| | | [ |
| | | 'methods' => 'POST', |
| | | 'callback' => [$this, 'handleResponseActions'], |
| | | 'permission_callback' => 'is_user_logged_in' |
| | | ] |
| | | ]); |
| | | } |
| | | |
| | | // Build query args |
| | | $args = $this->buildQueryArgs($request); |
| | | error_log('Args: '.print_r($args, true)); |
| | | $responses = $this->getItemResponse($item_id, $args); |
| | | error_log('Responses: '.print_r($responses, true)); |
| | | /** |
| | | * Get responses for an item |
| | | */ |
| | | public function getResponses(WP_REST_Request $request): WP_REST_Response |
| | | { |
| | | $item_id = (int) $request->get_param('item_id'); |
| | | |
| | | // Return formatted response |
| | | return new WP_REST_Response($responses); |
| | | } |
| | | if (!$item_id) { |
| | | return $this->error('Missing item ID', 'missing_item_id'); |
| | | } |
| | | |
| | | /** |
| | | * @param int $ID |
| | | * @param string $postType |
| | | * |
| | | * @return void |
| | | */ |
| | | protected function clearItemCache(int $ID, string $postType):void |
| | | { |
| | | $args = [ |
| | | // Build query args |
| | | $args = $this->buildQueryArgs($request); |
| | | $cacheKey = $this->cache->generateKey(array_merge(['item_id' => $item_id], $args)); |
| | | |
| | | BASE.'news' => $ID, |
| | | 'post_type' => BASE.$postType, |
| | | 'page' => 1, |
| | | 'per_page' => 20, |
| | | 'orderby' => 'created_at', |
| | | 'order' => 'DESC' |
| | | ]; |
| | | $key = $this->cache->generateKey($args); |
| | | $this->cache->invalidate($key); |
| | | } |
| | | // Check headers for 304 Not Modified |
| | | $headerCheck = $this->checkHeaders($request, $cacheKey); |
| | | if ($headerCheck) { |
| | | return $headerCheck; |
| | | } |
| | | |
| | | /** |
| | | * @param int $ID |
| | | * @param array $args |
| | | * |
| | | * @return array|WP_Error |
| | | */ |
| | | public function getItemResponse(int $ID, array $args = []):array|WP_Error |
| | | { |
| | | // Check cache |
| | | $cached = $this->cache->get($cacheKey); |
| | | if ($cached) { |
| | | return $this->success($cached); |
| | | } |
| | | |
| | | $default = [ |
| | | 'post_type' => BASE.'news', |
| | | 'page' => 1, |
| | | 'per_page' => 20, |
| | | 'orderby' => 'created_at', |
| | | 'order' => 'DESC' |
| | | ]; |
| | | // Get responses |
| | | $responses = $this->getItemResponse($item_id, $args); |
| | | |
| | | $args = wp_parse_args($default, $args); |
| | | if (is_wp_error($responses)) { |
| | | return $this->notFound($responses->get_error_message()); |
| | | } |
| | | |
| | | $key = $this->cache->generateKey(array_merge([$args['post_type'] => $ID], $args)); |
| | | $check = $this->cache->get($key); |
| | | // Cache and return |
| | | $this->cache->set($cacheKey, $responses); |
| | | return $this->success($responses); |
| | | } |
| | | |
| | | if ($check) { |
| | | return $check; |
| | | } |
| | | /** |
| | | * Handle response actions (create/update/delete) |
| | | */ |
| | | public function handleResponseActions(WP_REST_Request $request): WP_REST_Response |
| | | { |
| | | $data = $request->get_params(); |
| | | $user_id = (int) ($data['user'] ?? 0); |
| | | |
| | | // Verify post exists and is of correct type |
| | | $post = get_post($ID); |
| | | if (!$post || $post->post_type !== $args['post_type']) { |
| | | return new WP_Error( |
| | | self::ERROR_NOT_FOUND, |
| | | 'Item not found', |
| | | ['status' => 404] |
| | | ); |
| | | } |
| | | if (!$this->userCheck($user_id)) { |
| | | return $this->unauthorized('User verification failed'); |
| | | } |
| | | |
| | | // Execute the query |
| | | global $wpdb; |
| | | $table = $wpdb->prefix . BASE . 'responses'; |
| | | $operation_id = $data['id'] ?? uniqid('response_'); |
| | | $action = sanitize_text_field($data['action'] ?? ''); |
| | | |
| | | // Get total count |
| | | $total_query = "SELECT COUNT(*) FROM $table WHERE item_id = %d"; |
| | | $total_args = [$ID]; |
| | | if (!in_array($action, ['create', 'delete', 'update'])) { |
| | | return $this->error('Invalid action', 'invalid_action'); |
| | | } |
| | | |
| | | // Add parent filter if specified |
| | | if (isset($args['parent_id'])) { |
| | | $total_query .= " AND parent_id " . ($args['parent_id'] === null ? "IS NULL" : "= %d"); |
| | | if ($args['parent_id'] !== null) { |
| | | $total_args[] = $args['parent_id']; |
| | | } |
| | | } |
| | | // Validate required fields for create |
| | | if ($action === 'create') { |
| | | if (!isset($data['item_id'], $data['response'], $data['content'])) { |
| | | return $this->error('Missing required fields', 'missing_fields'); |
| | | } |
| | | } |
| | | |
| | | $total_items = $wpdb->get_var($wpdb->prepare($total_query, $total_args)); |
| | | // Prepare data for queue |
| | | $queue_data = match($action) { |
| | | 'create' => [ |
| | | 'item_id' => (int) $data['item_id'], |
| | | 'parent_id' => isset($data['parent_id']) ? (int) $data['parent_id'] : null, |
| | | 'response' => wp_kses_post($data['response']), |
| | | 'content' => sanitize_text_field($data['content']) |
| | | ], |
| | | 'update' => [ |
| | | 'response_id' => (int) $data['response_id'], |
| | | 'response' => isset($data['response']) ? wp_kses_post($data['response']) : null, |
| | | 'status' => isset($data['status']) && in_array($data['status'], ['published', 'hidden', 'flagged']) |
| | | ? $data['status'] |
| | | : null |
| | | ], |
| | | 'delete' => [ |
| | | 'response_id' => (int) $data['response_id'] |
| | | ] |
| | | }; |
| | | |
| | | // Build main query |
| | | $query = "SELECT r.*, |
| | | COALESCE((SELECT COUNT(*) FROM $table WHERE parent_id = r.id), 0) as reply_count, |
| | | COALESCE((SELECT SUM(CASE WHEN vote = 'up' THEN 1 ELSE 0 END) FROM {$wpdb->prefix}" . BASE . "karma_response WHERE item_id = r.id), 0) as upvotes, |
| | | COALESCE((SELECT SUM(CASE WHEN vote = 'down' THEN 1 ELSE 0 END) FROM {$wpdb->prefix}" . BASE . "karma_response WHERE item_id = r.id), 0) as downvotes |
| | | FROM $table r |
| | | WHERE r.item_id = %d"; |
| | | // Queue the operation |
| | | JVB()->queue()->queueOperation( |
| | | $action . '_response', |
| | | $user_id, |
| | | $queue_data, |
| | | [ |
| | | 'operation_id' => 'u' . $user_id . '_' . $operation_id, |
| | | 'priority' => 'high' |
| | | ] |
| | | ); |
| | | |
| | | $query_args = [$ID]; |
| | | return $this->queued($operation_id); |
| | | } |
| | | |
| | | // Apply parent filter |
| | | if (isset($args['parent_id'])) { |
| | | $query .= " AND r.parent_id " . ($args['parent_id'] === null ? "IS NULL" : "= %d"); |
| | | if ($args['parent_id'] !== null) { |
| | | $query_args[] = $args['parent_id']; |
| | | } |
| | | } |
| | | /** |
| | | * Get responses with karma calculations |
| | | */ |
| | | protected function getItemResponse(int $item_id, array $args = []): array|WP_Error |
| | | { |
| | | $defaults = [ |
| | | 'page' => 1, |
| | | 'per_page' => $this->perPage, |
| | | 'orderby' => 'created_at', |
| | | 'order' => 'DESC', |
| | | 'parent_id' => null |
| | | ]; |
| | | |
| | | // Apply order |
| | | $order_column = in_array($args['orderby'], ['created_at', 'updated_at']) |
| | | ? "r." . $args['orderby'] |
| | | : $args['orderby']; |
| | | $args = wp_parse_args($args, $defaults); |
| | | |
| | | $query .= " ORDER BY $order_column " . $args['order']; |
| | | // Verify post exists |
| | | $post = get_post($item_id); |
| | | if (!$post) { |
| | | return new WP_Error('not_found', 'Item not found'); |
| | | } |
| | | |
| | | // Apply pagination |
| | | $query .= " LIMIT %d OFFSET %d"; |
| | | $query_args[] = $args['per_page']; |
| | | $query_args[] = ($args['page'] - 1) * $args['per_page']; |
| | | // Build query with karma calculations |
| | | $query = " |
| | | SELECT r.*, |
| | | COALESCE((SELECT COUNT(*) FROM {$this->table->getFullTableName()} WHERE parent_id = r.id), 0) as reply_count, |
| | | COALESCE((SELECT COUNT(*) FROM {$this->karmaTable->getFullTableName()} WHERE item_id = r.id AND vote = 'up'), 0) as upvotes, |
| | | COALESCE((SELECT COUNT(*) FROM {$this->karmaTable->getFullTableName()} WHERE item_id = r.id AND vote = 'down'), 0) as downvotes |
| | | FROM {$this->table->getFullTableName()} r |
| | | WHERE r.item_id = %d |
| | | "; |
| | | |
| | | // Get responses |
| | | $responses = $wpdb->get_results($wpdb->prepare($query, $query_args)); |
| | | $query_args = [$item_id]; |
| | | |
| | | // Format responses |
| | | $items = array_map([$this, 'formatItem'], $responses); |
| | | // Filter by parent_id |
| | | if (isset($args['parent_id'])) { |
| | | $query .= " AND r.parent_id " . ($args['parent_id'] === null ? "IS NULL" : "= %d"); |
| | | if ($args['parent_id'] !== null) { |
| | | $query_args[] = $args['parent_id']; |
| | | } |
| | | } |
| | | |
| | | // Calculate pagination |
| | | $total_pages = ceil($total_items / $args['per_page']); |
| | | // Get total count for pagination |
| | | $count_query = str_replace( |
| | | ['SELECT r.*,', 'COALESCE((SELECT COUNT(*) FROM', 'as reply_count,', 'as upvotes,', 'as downvotes'], |
| | | ['SELECT COUNT(*)', '', '', '', ''], |
| | | $query |
| | | ); |
| | | $count_query = preg_replace('/COALESCE\([^)]+\)[^,]*,?/', '', $count_query); |
| | | $total_items = (int) $this->table->queryVar($count_query, $query_args); |
| | | |
| | | $return = [ |
| | | 'items' => $items, |
| | | 'has_more' => $args['page'] < $total_pages, |
| | | 'total_items' => (int) $total_items, |
| | | 'total_pages' => $total_pages |
| | | ]; |
| | | // Add ordering |
| | | $order_column = in_array($args['orderby'], ['created_at', 'updated_at']) |
| | | ? "r.{$args['orderby']}" |
| | | : $args['orderby']; |
| | | |
| | | $this->cache->set($key, $return); |
| | | return $return; |
| | | } |
| | | $query .= " ORDER BY {$order_column} {$args['order']}"; |
| | | |
| | | /** |
| | | * Create a new response |
| | | * @param WP_REST_Request $request |
| | | * @return WP_REST_Response |
| | | */ |
| | | public function handleResponseActions(WP_REST_Request $request):WP_REST_Response |
| | | { |
| | | $data = $request->get_params(); |
| | | $user_id = (int)$data['user']; |
| | | if (!$this->userCheck($data['user'])) { |
| | | return new WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => 'User doesn\'t match. Bot?' |
| | | ]); |
| | | } |
| | | $operation_id = $data['id'] ?? uniqid('response_'); |
| | | // Add pagination |
| | | $query .= " LIMIT %d OFFSET %d"; |
| | | $query_args[] = $args['per_page']; |
| | | $query_args[] = ($args['page'] - 1) * $args['per_page']; |
| | | |
| | | // Get responses |
| | | $responses = $this->table->queryResults($query, $query_args); |
| | | |
| | | // Validate required fields |
| | | if (!array_key_exists('item_id', $data) || !array_key_exists('response', $data) || !array_key_exists('content', $data)) { |
| | | error_log('Not enough data'); |
| | | return new WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => 'Missing required information.' |
| | | ]); |
| | | } |
| | | // Format responses |
| | | $items = array_map([$this, 'formatItem'], $responses); |
| | | |
| | | // Prepare data for queue |
| | | $queue_data = [ |
| | | 'item_id' => (int) $data['item_id'], |
| | | 'parent_id' => array_key_exists('parent_id', $data) ? (int) $data['parent_id'] : null, |
| | | 'response' => wp_kses_post($data['response']), |
| | | 'content'=> sanitize_text_field($data['content']) |
| | | ]; |
| | | return [ |
| | | 'items' => $items, |
| | | 'has_more' => $args['page'] < ceil($total_items / $args['per_page']), |
| | | 'total_items' => $total_items, |
| | | 'total_pages' => (int) ceil($total_items / $args['per_page']) |
| | | ]; |
| | | } |
| | | |
| | | error_log('Queue Data: '.print_r($queue_data, true)); |
| | | /** |
| | | * Process queue operations |
| | | */ |
| | | public function processOperation(WP_Error|array $result, object $operation, array $data): WP_Error|array |
| | | { |
| | | if (!in_array($operation->type, ['create_response', 'update_response', 'delete_response'])) { |
| | | return $result; |
| | | } |
| | | |
| | | $action = sanitize_text_field($data['action']); |
| | | error_log('Action: '.print_r($action, true)); |
| | | if (!in_array($action, ['create', 'delete', 'update'])) { |
| | | return new WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => 'Invalid action' |
| | | ]); |
| | | } |
| | | error_log('Sanitized action. Here we go!'); |
| | | return match($operation->type) { |
| | | 'create_response' => $this->createResponse($operation, $data), |
| | | 'update_response' => $this->updateResponse($data), |
| | | 'delete_response' => $this->deleteResponse($data), |
| | | default => $result |
| | | }; |
| | | } |
| | | |
| | | // Add to queue |
| | | $operation = JVB()->queue()->queueOperation( |
| | | $action.'_response', |
| | | $user_id, |
| | | $queue_data, |
| | | [ |
| | | 'operation_id' => 'u' . $user_id . '_' . $operation_id, |
| | | 'priority' => 'high' |
| | | ] |
| | | ); |
| | | error_log('Queued for processing'); |
| | | /** |
| | | * Create a new response |
| | | */ |
| | | protected function createResponse(object $operation, array $data): array |
| | | { |
| | | $response_id = $this->table->insert([ |
| | | 'item_id' => $data['item_id'], |
| | | 'content' => $data['content'], |
| | | 'user_id' => $operation->user_id, |
| | | 'parent_id' => $data['parent_id'], |
| | | 'response' => $data['response'], |
| | | 'status' => 'published' |
| | | ]); |
| | | |
| | | return new WP_REST_Response([ |
| | | 'success' => true, |
| | | 'message' => 'Item queued for processing' |
| | | ]); |
| | | } |
| | | if (!$response_id) { |
| | | $this->logError('Failed to insert response', [ |
| | | 'data' => $data, |
| | | 'error' => $this->table->getLastError() |
| | | ]); |
| | | |
| | | /** |
| | | * Update a response |
| | | * @param WP_REST_Request $request |
| | | * @return WP_REST_Response |
| | | */ |
| | | public function updateResponse(WP_REST_Request $request):WP_REST_Response |
| | | { |
| | | $id = (int) $request->get_param('id'); |
| | | $data = $request->get_params(); |
| | | $user_id = (int) $data['user'] ?? get_current_user_id(); |
| | | $operation_id = $data['id'] ?? uniqid('response_update_'); |
| | | return ['success' => false, 'result' => 'Failed to create response']; |
| | | } |
| | | |
| | | // Send notifications |
| | | $this->sendNotifications($data['item_id'], $data['parent_id'], $response_id, $operation->user_id); |
| | | |
| | | // Verify response exists |
| | | global $wpdb; |
| | | $table = $wpdb->prefix . BASE . 'responses'; |
| | | $response = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE id = %d", $id)); |
| | | // Clear cache |
| | | $this->cache->flush(); |
| | | |
| | | if (!$response) { |
| | | return new WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => 'Item not found.' |
| | | ]); |
| | | } |
| | | return ['success' => true, 'result' => $response_id]; |
| | | } |
| | | |
| | | // Check ownership or admin rights |
| | | if ($response->user_id != $user_id) { |
| | | return new WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => 'You do not have permission to delete this response' |
| | | ]); |
| | | } |
| | | /** |
| | | * Update a response |
| | | */ |
| | | protected function updateResponse(array $data): array |
| | | { |
| | | $update_data = []; |
| | | |
| | | // Prepare data for queue |
| | | $queue_data = [ |
| | | 'response_id' => $id, |
| | | 'response' => !empty($data['response']) ? wp_kses_post($data['response']) : null, |
| | | 'status' => !empty($data['status']) && in_array($data['status'], ['published', 'hidden', 'flagged']) |
| | | ? $data['status'] |
| | | : null |
| | | ]; |
| | | if (isset($data['response'])) { |
| | | $update_data['response'] = $data['response']; |
| | | $update_data['updated_at'] = current_time('mysql'); |
| | | } |
| | | |
| | | // Add to queue |
| | | $operation = JVB()->queue()->queueOperation( |
| | | 'update_response', |
| | | $user_id, |
| | | $queue_data, |
| | | [ |
| | | 'operation_id' => 'u' . $user_id . '_' . $operation_id, |
| | | 'priority' => 'high', |
| | | 'notification' => false, |
| | | ] |
| | | ); |
| | | if (isset($data['status'])) { |
| | | $update_data['status'] = $data['status']; |
| | | } |
| | | |
| | | return new WP_REST_Response($operation); |
| | | } |
| | | if (empty($update_data)) { |
| | | return ['success' => false, 'result' => 'No fields to update']; |
| | | } |
| | | |
| | | /** |
| | | * Delete a response |
| | | * @param WP_REST_Request $request |
| | | * @return WP_REST_Response |
| | | */ |
| | | public function deleteResponse(WP_REST_Request $request):WP_REST_Response |
| | | { |
| | | $id = (int) $request->get_param('id'); |
| | | $user_id = get_current_user_id(); |
| | | $operation_id = $request->get_param('id') ?? uniqid('response_delete_'); |
| | | $updated = $this->table->update( |
| | | $update_data, |
| | | ['id' => $data['response_id']] |
| | | ); |
| | | |
| | | // Verify response exists |
| | | global $wpdb; |
| | | $table = $wpdb->prefix . BASE . 'responses'; |
| | | $response = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE id = %d", $id)); |
| | | if ($updated === false) { |
| | | $this->logError('Failed to update response', [ |
| | | 'data' => $data, |
| | | 'error' => $this->table->getLastError() |
| | | ]); |
| | | |
| | | if (!$response) { |
| | | return new WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => 'Response not found', |
| | | ]); |
| | | } |
| | | |
| | | // Check ownership or admin rights |
| | | if ($response->user_id != $user_id) { |
| | | return new WP_REST_Response([ |
| | | 'success' => false, |
| | | 'msesage' => 'You do not have permission to delete this resposne', |
| | | ]); |
| | | } |
| | | |
| | | // Add to queue |
| | | $operation = JVB()->queue()->queueOperation( |
| | | 'delete_response', |
| | | $user_id, |
| | | ['response_id' => $id], |
| | | [ |
| | | 'operation_id' => 'u' . $user_id . '_' . $operation_id, |
| | | 'priority' => 'high', |
| | | 'notification' => false, |
| | | ] |
| | | ); |
| | | |
| | | return new WP_REST_Response([ |
| | | 'success' => true, |
| | | 'message' => 'Queued for processing' |
| | | ]); |
| | | } |
| | | |
| | | /** |
| | | * Process operations from the queue |
| | | * @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 (!in_array($operation->type, ['create_response', 'update_response', 'delete_response'])) { |
| | | return $result; |
| | | } |
| | | |
| | | global $wpdb; |
| | | $table = $wpdb->prefix . BASE . 'responses'; |
| | | |
| | | switch ($operation->type) { |
| | | case 'create_response': |
| | | // Create new response |
| | | $inserted = $wpdb->insert( |
| | | $table, |
| | | [ |
| | | 'item_id' => $data['item_id'], |
| | | 'content' => $data['content'], |
| | | 'user_id' => $operation->user_id, |
| | | 'parent_id' => $data['parent_id'], |
| | | 'response' => $data['response'], |
| | | 'status' => 'published', |
| | | 'created_at' => current_time('mysql'), |
| | | 'updated_at' => current_time('mysql') |
| | | ], |
| | | ['%d', '%d', '%d', '%s', '%s', '%s', '%s'] |
| | | ); |
| | | |
| | | if (!$inserted) { |
| | | error_log('Did not insert'.print_r($wpdb->last_error, true)); |
| | | JVB()->error()->log( |
| | | '[ResponseRoutes]:processOperation', |
| | | 'Failed to insert response', |
| | | ['data' => $data, 'error' => $wpdb->last_error], |
| | | 'error' |
| | | ); |
| | | return [ |
| | | 'success' => false, |
| | | 'result' => 'Failed to create response' |
| | | ]; |
| | | } |
| | | |
| | | $response_id = $wpdb->insert_id; |
| | | error_log('Response ID: '.print_r($response_id, true)); |
| | | |
| | | // Send notification to post author |
| | | $post = get_post($data['item_id']); |
| | | if ($post && $post->post_author != $operation->user_id) { |
| | | JVB()->notification()->addNotification( |
| | | $post->post_author, |
| | | 'new_response', |
| | | [ |
| | | 'message' => 'Someone responded to your post', |
| | | 'item_id' => $data['item_id'], |
| | | 'response_id' => $response_id |
| | | ] |
| | | ); |
| | | } |
| | | |
| | | // Send notification to parent response author if this is a reply |
| | | if ($data['parent_id']) { |
| | | $parent = $wpdb->get_row($wpdb->prepare( |
| | | "SELECT user_id FROM $table WHERE id = %d", |
| | | $data['parent_id'] |
| | | )); |
| | | |
| | | if ($parent && $parent->user_id != $operation->user_id) { |
| | | JVB()->notification()->addNotification( |
| | | $parent->user_id, |
| | | 'response_reply', |
| | | [ |
| | | 'message' => 'Someone replied to your response', |
| | | 'item_id' => $data['item_id'], |
| | | 'response_id' => $response_id |
| | | ] |
| | | ); |
| | | } |
| | | } |
| | | |
| | | $this->cache->forget($data['item_id']); |
| | | return ['success' => true, 'result' => $response_id]; |
| | | |
| | | case 'update_response': |
| | | $update_data = []; |
| | | $update_format = []; |
| | | |
| | | if (array_key_exists('response', $data)) { |
| | | $update_data['response'] = $data['response']; |
| | | $update_data['updated_at'] = current_time('mysql'); |
| | | $update_format[] = '%s'; |
| | | $update_format[] = '%s'; |
| | | } |
| | | |
| | | if (array_key_exists('status', $data)) { |
| | | $update_data['status'] = $data['status']; |
| | | $update_format[] = '%s'; |
| | | } |
| | | |
| | | if (empty($update_data)) { |
| | | return ['success' => false, 'result' => 'No fields to update']; |
| | | } |
| | | return ['success' => false, 'result' => 'Failed to update response']; |
| | | } |
| | | |
| | | $updated = $wpdb->update( |
| | | $table, |
| | | $update_data, |
| | | ['id' => $data['response_id']], |
| | | $update_format, |
| | | ['%d'] |
| | | ); |
| | | $this->cache->flush(); |
| | | return ['success' => true, 'result' => $updated]; |
| | | } |
| | | |
| | | if ($updated === false) { |
| | | JVB()->error()->log( |
| | | '[ResponseRoutes]:processOperation', |
| | | 'Failed to update response', |
| | | ['data' => $data, 'error' => $wpdb->last_error], |
| | | 'error' |
| | | ); |
| | | return [ |
| | | 'success' => false, |
| | | 'result' => 'Failed to update response' |
| | | ]; |
| | | } |
| | | /** |
| | | * Delete a response (or mark as deleted if it has replies) |
| | | */ |
| | | protected function deleteResponse(array $data): array |
| | | { |
| | | $response = $this->table->get(['id' => $data['response_id']]); |
| | | |
| | | $this->cache->forget($data['item_id']); |
| | | $this->cache->flush(); |
| | | return ['success' => true, 'result' => $updated]; |
| | | if (!$response) { |
| | | return ['success' => false, 'result' => 'Response not found']; |
| | | } |
| | | |
| | | case 'delete_response': |
| | | // Get response info before deleting |
| | | $response = $wpdb->get_row($wpdb->prepare( |
| | | "SELECT * FROM $table WHERE id = %d", |
| | | $data['response_id'] |
| | | )); |
| | | // Check if it has replies |
| | | $has_replies = $this->table->where(['parent_id' => $data['response_id']])->countResults() > 0; |
| | | |
| | | if (!$response) { |
| | | return ['success' => false, 'result' => 'Response not found']; |
| | | } |
| | | if ($has_replies) { |
| | | // Don't delete, just mark as deleted |
| | | $updated = $this->table->update( |
| | | [ |
| | | 'response' => '[ deleted ]', |
| | | 'status' => 'deleted', |
| | | 'updated_at' => current_time('mysql') |
| | | ], |
| | | ['id' => $data['response_id']] |
| | | ); |
| | | |
| | | // Check if this response has replies |
| | | $has_replies = $wpdb->get_var($wpdb->prepare( |
| | | "SELECT COUNT(*) FROM $table WHERE parent_id = %d", |
| | | $data['response_id'] |
| | | )) > 0; |
| | | $this->cache->flush(); |
| | | return ['success' => true, 'result' => $updated]; |
| | | } |
| | | |
| | | if ($has_replies) { |
| | | // Don't delete, just mark as deleted and replace content |
| | | $updated = $wpdb->update( |
| | | $table, |
| | | [ |
| | | 'response' => '[ deleted ]', |
| | | 'status' => 'deleted', |
| | | 'updated_at' => current_time('mysql') |
| | | ], |
| | | ['id' => $data['response_id']], |
| | | ['%s', '%s', '%s'], |
| | | ['%d'] |
| | | ); |
| | | $this->cache->flush(); |
| | | return ['success' => true, 'result' => $updated ]; |
| | | } else { |
| | | // No replies, safe to actually delete |
| | | $deleted = $wpdb->delete( |
| | | $table, |
| | | ['id' => $data['response_id']], |
| | | ['%d'] |
| | | ); |
| | | // No replies, safe to delete |
| | | $deleted = $this->table->delete(['id' => $data['response_id']]); |
| | | |
| | | if ($deleted === false) { |
| | | JVB()->error()->log( |
| | | '[ResponseRoutes]:processOperation', |
| | | 'Failed to delete response', |
| | | ['data' => $data, 'error' => $wpdb->last_error], |
| | | 'error' |
| | | ); |
| | | return [ |
| | | 'success' => false, |
| | | 'result' => 'Failed to delete response' |
| | | ]; |
| | | } |
| | | if ($deleted === false) { |
| | | $this->logError('Failed to delete response', [ |
| | | 'data' => $data, |
| | | 'error' => $this->table->getLastError() |
| | | ]); |
| | | |
| | | $this->cache->forget($data['item_id']); |
| | | $this->cache->flush(); |
| | | return ['success' => true, 'result' => $deleted]; |
| | | } |
| | | } |
| | | return ['success' => false, 'result' => 'Failed to delete response']; |
| | | } |
| | | |
| | | $this->cache->flush(); |
| | | return ['success' => true, 'result' => $deleted]; |
| | | } |
| | | |
| | | return $result; |
| | | } |
| | | /** |
| | | * Send notifications for new responses |
| | | */ |
| | | protected function sendNotifications(int $item_id, ?int $parent_id, int $response_id, int $user_id): void |
| | | { |
| | | // Notify post author |
| | | $post = get_post($item_id); |
| | | if ($post && $post->post_author != $user_id) { |
| | | JVB()->notification()->addNotification( |
| | | $post->post_author, |
| | | 'new_response', |
| | | [ |
| | | 'message' => 'Someone responded to your post', |
| | | 'item_id' => $item_id, |
| | | 'response_id' => $response_id |
| | | ] |
| | | ); |
| | | } |
| | | |
| | | /** |
| | | * Build query arguments from request parameters |
| | | * @param WP_REST_Request $request |
| | | * @return array |
| | | */ |
| | | protected function buildQueryArgs(WP_REST_Request $request):array |
| | | { |
| | | $page = max(1, (int) $request->get_param('page') ?? 1); |
| | | $per_page = min(100, max(1, (int) $request->get_param('per_page') ?? $this->per_page)); |
| | | // Notify parent response author if this is a reply |
| | | if ($parent_id) { |
| | | $parent = $this->table->get(['id' => $parent_id]); |
| | | |
| | | $args = [ |
| | | 'page' => $page, |
| | | 'per_page' => $per_page, |
| | | 'orderby' => 'created_at', |
| | | 'order' => 'DESC' |
| | | ]; |
| | | if ($parent && $parent->user_id != $user_id) { |
| | | JVB()->notification()->addNotification( |
| | | $parent->user_id, |
| | | 'response_reply', |
| | | [ |
| | | 'message' => 'Someone replied to your response', |
| | | 'item_id' => $item_id, |
| | | 'response_id' => $response_id |
| | | ] |
| | | ); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // Apply parent filter (null means top-level responses) |
| | | if ($request->has_param('parent_id')) { |
| | | $parent_id = $request->get_param('parent_id'); |
| | | $args['parent_id'] = $parent_id === '0' ? null : (int) $parent_id; |
| | | } |
| | | /** |
| | | * Build query arguments from request |
| | | */ |
| | | protected function buildQueryArgs(WP_REST_Request $request): array |
| | | { |
| | | $page = max(1, (int) ($request->get_param('page') ?? 1)); |
| | | $per_page = min(100, max(1, (int) ($request->get_param('per_page') ?? $this->perPage))); |
| | | |
| | | // Apply ordering |
| | | if ($request->has_param('orderby')) { |
| | | $orderby = $request->get_param('orderby'); |
| | | $valid_orderby = ['created_at', 'updated_at', 'upvotes', 'downvotes']; |
| | | $args = [ |
| | | 'page' => $page, |
| | | 'per_page' => $per_page, |
| | | 'orderby' => 'created_at', |
| | | 'order' => 'DESC' |
| | | ]; |
| | | |
| | | if (in_array($orderby, $valid_orderby)) { |
| | | $args['orderby'] = $orderby; |
| | | } |
| | | } |
| | | // Parent filter (null = top-level only) |
| | | if ($request->has_param('parent_id')) { |
| | | $parent_id = $request->get_param('parent_id'); |
| | | $args['parent_id'] = $parent_id === '0' ? null : (int) $parent_id; |
| | | } |
| | | |
| | | if ($request->has_param('order')) { |
| | | $order = strtoupper($request->get_param('order')); |
| | | if (in_array($order, ['ASC', 'DESC'])) { |
| | | $args['order'] = $order; |
| | | } |
| | | } |
| | | // Ordering |
| | | if ($request->has_param('orderby')) { |
| | | $orderby = $request->get_param('orderby'); |
| | | $valid = ['created_at', 'updated_at', 'upvotes', 'downvotes']; |
| | | |
| | | return $args; |
| | | } |
| | | if (in_array($orderby, $valid)) { |
| | | $args['orderby'] = $orderby; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * @param int $itemID |
| | | * @param int $ID |
| | | * |
| | | * @return array |
| | | */ |
| | | protected function getChildren(int $itemID, int $ID):array |
| | | { |
| | | return $this->getItemResponse($itemID, ['parent_id' => $ID]); |
| | | } |
| | | /** |
| | | * Format a response object for API output |
| | | * @param object $response |
| | | * @return array |
| | | */ |
| | | protected function formatItem(object $response):array |
| | | { |
| | | if ($response->is_user_deleted) { |
| | | // For deleted users, show anonymous info |
| | | $formatted = [ |
| | | 'id' => (int) $response->id, |
| | | 'item_id' => (int) $response->item_id, |
| | | 'parent_id' => $response->parent_id ? (int) $response->parent_id : null, |
| | | 'response' => $response->response, |
| | | 'status' => $response->status, |
| | | 'created_at' => $response->created_at, |
| | | 'updated_at' => $response->updated_at, |
| | | 'children' => $this->getChildren($response->item_id, $response->id), |
| | | 'user' => [ |
| | | 'id' => null, |
| | | 'name' => '[deleted user]', |
| | | 'is_deleted' => true |
| | | ], |
| | | 'upvotes' => (int) ($response->upvotes ?? 0), |
| | | 'downvotes' => (int) ($response->downvotes ?? 0), |
| | | 'karma' => (int) ($response->upvotes ?? 0) - (int) ($response->downvotes ?? 0) |
| | | ]; |
| | | } else { |
| | | // Normal user processing as before |
| | | error_log('Response: '.print_r($response, true)); |
| | | $artist = jvbContentFromUser($response->user_id); |
| | | if ($request->has_param('order')) { |
| | | $order = strtoupper($request->get_param('order')); |
| | | if (in_array($order, ['ASC', 'DESC'])) { |
| | | $args['order'] = $order; |
| | | } |
| | | } |
| | | |
| | | $formatted = [ |
| | | 'id' => (int) $response->id, |
| | | 'item_id' => (int) $response->item_id, |
| | | 'parent_id' => $response->parent_id ? (int) $response->parent_id : null, |
| | | 'response' => $response->response, |
| | | 'status' => $response->status, |
| | | 'created_at' => $response->created_at, |
| | | 'updated_at' => $response->updated_at, |
| | | 'children' => $this->getChildren($response->item_id, $response->id), |
| | | 'upvotes' => (int) ($response->upvotes ?? 0), |
| | | 'downvotes' => (int) ($response->downvotes ?? 0), |
| | | 'karma' => (int) ($response->upvotes ?? 0) - (int) ($response->downvotes ?? 0) |
| | | return $args; |
| | | } |
| | | |
| | | ]; |
| | | /** |
| | | * Get child responses recursively |
| | | */ |
| | | protected function getChildren(int $item_id, int $parent_id): array |
| | | { |
| | | return $this->getItemResponse($item_id, ['parent_id' => $parent_id]); |
| | | } |
| | | |
| | | // Add artist info if available |
| | | if (!empty($artist)) { |
| | | $formatted['artist'] = [ |
| | | 'name' => $artist['name'], |
| | | 'url' => $artist['url'], |
| | | 'shop' => $artist['shop'] ?? null |
| | | ]; |
| | | } |
| | | } |
| | | /** |
| | | * Format response for API output |
| | | */ |
| | | protected function formatItem(object $response): array |
| | | { |
| | | $formatted = [ |
| | | 'id' => (int) $response->id, |
| | | 'item_id' => (int) $response->item_id, |
| | | 'parent_id' => $response->parent_id ? (int) $response->parent_id : null, |
| | | 'response' => $response->response, |
| | | 'status' => $response->status, |
| | | 'created_at' => $response->created_at, |
| | | 'updated_at' => $response->updated_at, |
| | | 'children' => $this->getChildren($response->item_id, $response->id), |
| | | 'upvotes' => (int) ($response->upvotes ?? 0), |
| | | 'downvotes' => (int) ($response->downvotes ?? 0), |
| | | 'karma' => (int) ($response->upvotes ?? 0) - (int) ($response->downvotes ?? 0), |
| | | 'reply_count' => (int) ($response->reply_count ?? 0) |
| | | ]; |
| | | |
| | | // Add reply count if available (for both deleted and non-deleted users) |
| | | if (isset($response->reply_count)) { |
| | | $formatted['reply_count'] = (int) $response->reply_count; |
| | | } |
| | | // Handle deleted users |
| | | if ($response->is_user_deleted) { |
| | | $formatted['user'] = [ |
| | | 'id' => null, |
| | | 'name' => '[deleted user]', |
| | | 'is_deleted' => true |
| | | ]; |
| | | } else { |
| | | // Add artist info |
| | | $artist = jvbContentFromUser($response->user_id); |
| | | if (!empty($artist)) { |
| | | $formatted['artist'] = [ |
| | | 'name' => $artist['name'], |
| | | 'url' => $artist['url'], |
| | | 'shop' => $artist['shop'] ?? null |
| | | ]; |
| | | } |
| | | } |
| | | |
| | | return $formatted; |
| | | } |
| | | return $formatted; |
| | | } |
| | | |
| | | /** |
| | | * Handle user deletion by anonymizing their responses |
| | | * @param int $user_id The deleted user ID |
| | | */ |
| | | public function handleUserDeletion(int $user_id):void |
| | | { |
| | | global $wpdb; |
| | | $table = $wpdb->prefix . BASE . 'news_responses'; |
| | | /** |
| | | * Handle user deletion by anonymizing responses |
| | | */ |
| | | public function handleUserDeletion(int $user_id): void |
| | | { |
| | | $updated = $this->table->update( |
| | | [ |
| | | 'is_user_deleted' => 1, |
| | | 'updated_at' => current_time('mysql') |
| | | ], |
| | | ['user_id' => $user_id] |
| | | ); |
| | | |
| | | // Anonymize all responses by this user |
| | | $wpdb->update( |
| | | $table, |
| | | [ |
| | | 'is_user_deleted' => 1, |
| | | // Keep user_id intact for internal tracking, but mark as deleted |
| | | 'updated_at' => current_time('mysql') |
| | | ], |
| | | ['user_id' => $user_id], |
| | | ['%d', '%s'], |
| | | ['%d'] |
| | | ); |
| | | $this->logError( |
| | | 'Anonymized responses for deleted user', |
| | | ['user_id' => $user_id, 'count' => $updated], |
| | | 'info' |
| | | ); |
| | | |
| | | JVB()->error()->log( |
| | | 'news_responses', |
| | | 'Anonymized responses for deleted user', |
| | | ['user_id' => $user_id, 'count' => $wpdb->rows_affected], |
| | | 'info' |
| | | ); |
| | | } |
| | | $this->cache->flush(); |
| | | } |
| | | } |