<?php
|
namespace JVBase\rest\routes;
|
|
use JVBase\JVB;
|
use JVBase\rest\RestRouteManager;
|
use JVBase\managers\Cache;
|
use WP_REST_Request;
|
use WP_REST_Response;
|
use WP_Error;
|
|
if (!defined('ABSPATH')) {
|
exit; // Exit if accessed directly
|
}
|
class ResponseRoutes extends RestRouteManager
|
{
|
protected int $per_page;
|
protected false|object $manager = false;
|
|
public function __construct()
|
{
|
$this->cache_name = 'responses';
|
parent::__construct();
|
$this->action = 'dash-';
|
$this->per_page = 20;
|
|
add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
|
add_action('deleted_user', [$this, 'handleUserDeletion'], 10, 1);
|
}
|
|
/**
|
* 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']
|
]
|
]);
|
}
|
|
/**
|
* 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'
|
]);
|
}
|
|
// 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));
|
|
// Return formatted response
|
return new WP_REST_Response($responses);
|
}
|
|
/**
|
* @param int $ID
|
* @param string $postType
|
*
|
* @return void
|
*/
|
protected function clearItemCache(int $ID, string $postType):void
|
{
|
$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);
|
}
|
|
/**
|
* @param int $ID
|
* @param array $args
|
*
|
* @return array|WP_Error
|
*/
|
public function getItemResponse(int $ID, array $args = []):array|WP_Error
|
{
|
|
$default = [
|
'post_type' => BASE.'news',
|
'page' => 1,
|
'per_page' => 20,
|
'orderby' => 'created_at',
|
'order' => 'DESC'
|
];
|
|
$args = wp_parse_args($default, $args);
|
|
$key = $this->cache->generateKey(array_merge([$args['post_type'] => $ID], $args));
|
$check = $this->cache->get($key);
|
|
if ($check) {
|
return $check;
|
}
|
|
// 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]
|
);
|
}
|
|
// Execute the query
|
global $wpdb;
|
$table = $wpdb->prefix . BASE . 'responses';
|
|
// Get total count
|
$total_query = "SELECT COUNT(*) FROM $table WHERE item_id = %d";
|
$total_args = [$ID];
|
|
// 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'];
|
}
|
}
|
|
$total_items = $wpdb->get_var($wpdb->prepare($total_query, $total_args));
|
|
// 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";
|
|
$query_args = [$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'];
|
}
|
}
|
|
// Apply order
|
$order_column = in_array($args['orderby'], ['created_at', 'updated_at'])
|
? "r." . $args['orderby']
|
: $args['orderby'];
|
|
$query .= " ORDER BY $order_column " . $args['order'];
|
|
// Apply pagination
|
$query .= " LIMIT %d OFFSET %d";
|
$query_args[] = $args['per_page'];
|
$query_args[] = ($args['page'] - 1) * $args['per_page'];
|
|
// Get responses
|
$responses = $wpdb->get_results($wpdb->prepare($query, $query_args));
|
|
// Format responses
|
$items = array_map([$this, 'formatItem'], $responses);
|
|
// Calculate pagination
|
$total_pages = ceil($total_items / $args['per_page']);
|
|
$return = [
|
'items' => $items,
|
'has_more' => $args['page'] < $total_pages,
|
'total_items' => (int) $total_items,
|
'total_pages' => $total_pages
|
];
|
|
$this->cache->set($key, $return);
|
return $return;
|
}
|
|
/**
|
* 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_');
|
|
|
// 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.'
|
]);
|
}
|
|
// 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'])
|
];
|
|
error_log('Queue Data: '.print_r($queue_data, true));
|
|
$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!');
|
|
// 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');
|
|
return new WP_REST_Response([
|
'success' => true,
|
'message' => 'Item queued for processing'
|
]);
|
}
|
|
/**
|
* 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_');
|
|
|
// Verify response exists
|
global $wpdb;
|
$table = $wpdb->prefix . BASE . 'responses';
|
$response = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE id = %d", $id));
|
|
if (!$response) {
|
return new WP_REST_Response([
|
'success' => false,
|
'message' => 'Item not found.'
|
]);
|
}
|
|
// 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'
|
]);
|
}
|
|
// 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
|
];
|
|
// Add to queue
|
$operation = JVB()->queue()->queueOperation(
|
'update_response',
|
$user_id,
|
$queue_data,
|
[
|
'operation_id' => 'u' . $user_id . '_' . $operation_id,
|
'priority' => 'high',
|
'notification' => false,
|
]
|
);
|
|
return new WP_REST_Response($operation);
|
}
|
|
/**
|
* 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_');
|
|
// Verify response exists
|
global $wpdb;
|
$table = $wpdb->prefix . BASE . 'responses';
|
$response = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE id = %d", $id));
|
|
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'];
|
}
|
|
$updated = $wpdb->update(
|
$table,
|
$update_data,
|
['id' => $data['response_id']],
|
$update_format,
|
['%d']
|
);
|
|
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'
|
];
|
}
|
|
$this->cache->forget($data['item_id']);
|
$this->cache->flush();
|
return ['success' => true, 'result' => $updated];
|
|
case 'delete_response':
|
// Get response info before deleting
|
$response = $wpdb->get_row($wpdb->prepare(
|
"SELECT * FROM $table WHERE id = %d",
|
$data['response_id']
|
));
|
|
if (!$response) {
|
return ['success' => false, 'result' => 'Response not found'];
|
}
|
|
// 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;
|
|
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']
|
);
|
|
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'
|
];
|
}
|
|
$this->cache->forget($data['item_id']);
|
$this->cache->flush();
|
return ['success' => true, 'result' => $deleted];
|
}
|
}
|
|
|
return $result;
|
}
|
|
/**
|
* 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));
|
|
$args = [
|
'page' => $page,
|
'per_page' => $per_page,
|
'orderby' => 'created_at',
|
'order' => 'DESC'
|
];
|
|
// 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;
|
}
|
|
// Apply ordering
|
if ($request->has_param('orderby')) {
|
$orderby = $request->get_param('orderby');
|
$valid_orderby = ['created_at', 'updated_at', 'upvotes', 'downvotes'];
|
|
if (in_array($orderby, $valid_orderby)) {
|
$args['orderby'] = $orderby;
|
}
|
}
|
|
if ($request->has_param('order')) {
|
$order = strtoupper($request->get_param('order'));
|
if (in_array($order, ['ASC', 'DESC'])) {
|
$args['order'] = $order;
|
}
|
}
|
|
return $args;
|
}
|
|
/**
|
* @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);
|
|
$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)
|
|
];
|
|
// Add artist info if available
|
if (!empty($artist)) {
|
$formatted['artist'] = [
|
'name' => $artist['name'],
|
'url' => $artist['url'],
|
'shop' => $artist['shop'] ?? null
|
];
|
}
|
}
|
|
// Add reply count if available (for both deleted and non-deleted users)
|
if (isset($response->reply_count)) {
|
$formatted['reply_count'] = (int) $response->reply_count;
|
}
|
|
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';
|
|
// 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']
|
);
|
|
JVB()->error()->log(
|
'news_responses',
|
'Anonymized responses for deleted user',
|
['user_id' => $user_id, 'count' => $wpdb->rows_affected],
|
'info'
|
);
|
}
|
}
|