<?php
|
namespace JVBase\rest;
|
|
use JVBase\JVB;
|
use JVBase\rest\RateLimiter;
|
use JVBase\managers\OperationQueue;
|
use JVBase\managers\CacheManager;
|
use JVBase\managers\NotificationManager;
|
use WP_REST_Request;
|
use WP_Error;
|
use Exception;
|
use WP_REST_Response;
|
|
if (!defined('ABSPATH')) {
|
exit; // Exit if accessed directly
|
}
|
|
/**
|
* Handles route registration and high-level coordination
|
*/
|
abstract class RestRouteManager
|
{
|
protected string $namespace = 'jvb/v1';
|
|
protected RateLimiter|null $rate_limiter;
|
protected array $types;
|
protected string $route;
|
protected string $base;
|
protected string $content_type; //the registered post type
|
protected string $type; //post, user, term, for MetaManager
|
protected string $action = ''; //optional additional nonce to check
|
protected array $callback; //route->callback array
|
protected string $operation_type; // from QueueManager.js and OperationQueue.php
|
protected OperationQueue $queue;
|
protected CacheManager $cache;
|
protected NotificationManager $notifications;
|
protected string $cache_name ='';
|
protected int $cache_ttl = 3600; //1 hour default
|
|
|
// Error code constants for consistency
|
const ERROR_MISSING_PARAMS = 'missing_parameters';
|
const ERROR_ACCESS_DENIED = 'access_denied';
|
const ERROR_NOT_FOUND = 'not_found';
|
const ERROR_INVALID_OPERATION = 'invalid_operation';
|
const ERROR_PROCESSING = 'processing_error';
|
|
public function __construct()
|
{
|
$this->base = BASE;
|
$this->rate_limiter = new RateLimiter();
|
if ($this->cache_name !== '') {
|
$this->cache = new CacheManager($this->cache_name, $this->cache_ttl);
|
}
|
add_action('rest_api_init', [$this, 'registerRoutes']);
|
}
|
|
public function init()
|
{
|
//Replace in child classes, if necessary
|
}
|
|
|
/**
|
* @param WP_REST_Request $request Rest Request object
|
* @param string $nonceName nonce name to check
|
* @param string|null $nonce optional additional nonce to check
|
*
|
* @return void|WP_Error
|
*/
|
public function verifyNonce(WP_REST_Request $request, string $nonceName = 'wp_rest', string|null $nonce = null):mixed
|
{
|
$nonce = (!is_null($nonce)) ? $nonce : $request->get_header('X-WP-Nonce');
|
|
if (!wp_verify_nonce($nonce, $nonceName)) {
|
return new WP_Error(
|
'invalid_nonce',
|
'Invalid nonce',
|
['status' => 403]
|
);
|
}
|
return true;
|
}
|
|
abstract public function registerRoutes();
|
|
|
/**
|
* @param WP_REST_Request $request the Request Object
|
*
|
* @return bool|WP_Error Whether or not we can proceed
|
*/
|
public function checkPermission(WP_REST_Request $request):bool
|
{
|
// Check rate limits first
|
if (!$this->rate_limiter->checkLimit($request)) {
|
return new WP_Error(
|
'rate_limit_reached',
|
'Rate limit reached',
|
['status' => 403]
|
);
|
}
|
$user_id = $request->get_param('user');
|
if (!empty($user_id) && !$this->userCheck($user_id)) {
|
return false;
|
}
|
// Verify nonces
|
$this->verifyNonce($request, 'wp_rest');
|
if ($this->action!=='') {
|
$this->verifyNonce($request, $this->action . $user_id, $request->get_header('action_nonce'));
|
}
|
return true;
|
}
|
|
protected function checkContent(string $content, bool $bool = false):string|bool
|
{
|
$result = JVB_CONTENT[$content]??JVB_TAXONOMY[$content]??JVB_USER[$content]??'';
|
if ($bool) {
|
return $result !== '';
|
}
|
return $result;
|
}
|
|
|
public function formatError($code, $message, $status = 400)
|
{
|
return new WP_Error($code, $message, ['status' => $status]);
|
}
|
|
/**
|
* @param int $userID the userID to check
|
*
|
* @return bool whether or not the $userID matches the currently logged in user
|
*/
|
protected function userCheck(int $userID)
|
{
|
return $userID === get_current_user_id();
|
}
|
|
/**
|
* Makes sure the value is in the format it needs to be
|
* @param mixed $value
|
*
|
* @return mixed
|
*/
|
protected function getMetaValues(mixed $value):mixed
|
{
|
//get the repeater out of there
|
$temp = json_decode($value);
|
if (is_array($temp)) {
|
$value = [];
|
foreach ($temp as $t) {
|
if (is_object($t)) {
|
$t = (array)$t;
|
}
|
$value[] = $t;
|
}
|
}
|
return $value;
|
}
|
/**
|
* Log errors with proper context
|
* @param string $message The error message
|
* @param array $context Additional context
|
* @param string $severity
|
* @return void
|
*/
|
protected function logError(string $message, array $context = [], string $severity = 'error')
|
{
|
try {
|
JVB()->error()->log(
|
'artist_invitations', // component
|
$message,
|
$context,
|
$severity
|
);
|
} catch (Exception $e) {
|
// Fallback if error handler fails
|
error_log("Invitations Error: {$message} - " . json_encode($context));
|
}
|
}
|
|
/**
|
* @param int $userID The user ID to check
|
*
|
* @return bool whether user exists
|
*/
|
protected function checkUser(int $userID):bool
|
{
|
$checked = $this->cache->get($userID, 'checked_users');
|
if ($checked) {
|
return $checked;
|
}
|
$test = (bool)get_userdata($userID);
|
|
$this->cache->set($userID, $test, null, 'checked_users');
|
return $test;
|
}
|
|
/**
|
* @param int $shopID the shop ID to check
|
*
|
* @return bool whether the shop exists
|
*/
|
protected function checkShop(int $shopID):bool
|
{
|
$checked = $this->cache->get($shopID, 'checked_shops');
|
if ($checked) {
|
return (bool)$checked;
|
}
|
$test = term_exists($shopID, BASE . 'shop');
|
$this->cache->set($shopID, (int)$test, null, 'checked_shops');
|
return $test;
|
}
|
|
protected function checkTerm(array $args) {
|
$termID = $args['to_term']??$args['term_id']??false;
|
if (!$termID) {
|
return false;
|
}
|
$taxonomy = $args['taxonomy']??false;
|
if (!$taxonomy) {
|
return false;
|
}
|
$checked = $this->cache->get($termID, 'checked_'.$taxonomy);
|
if ($checked) {
|
return (bool) $checked;
|
}
|
$test = term_exists($termID, jvbCheckBase($taxonomy));
|
$this->cache->set($termID, (int)$test, null, 'checked_'.$taxonomy);
|
return (bool)$test;
|
}
|
|
/**
|
* Check if an artist is verified
|
*
|
* @param int $user_id User ID
|
* @return bool True if verified
|
*/
|
public function isVerifiedUser(int $user_id):bool
|
{
|
// Cache result to avoid repeated checks
|
$cache_key = "verified_users";
|
$verified = $this->cache->get($cache_key, 'users');
|
$verified = ($verified) ?: [];
|
if (array_key_exists($user_id, $verified)) {
|
return (bool) $verified[$user_id];
|
}
|
|
// Check if user has the skip_moderation capability
|
$is_verified = user_can($user_id, 'skip_moderation');
|
|
$verified[$user_id] = $is_verified;
|
// Cache for a day
|
$this->cache->set($cache_key, $verified, DAY_IN_SECONDS, 'users');
|
|
return $is_verified;
|
}
|
|
protected function applyTaxonomyFilters(array $args, array $data):array
|
{
|
$taxQuery = [];
|
foreach($data['taxonomies']??[] as $taxonomy => $terms) {
|
if (array_key_exists(jvbNoBase($taxonomy), JVB_TAXONOMY)) {
|
$taxQuery[] = [
|
'taxonomy' => jvbCheckBase($taxonomy),
|
'terms' => array_map(
|
'absint',
|
is_array($terms) ? $terms : explode(',', $terms)
|
)
|
];
|
}
|
}
|
|
if (!empty($taxQuery)) {
|
$args['tax_query'] = array_merge([
|
'relation' => (array_key_exists('match', $data)) ? 'AND' : 'OR',
|
], $taxQuery);
|
}
|
|
$authorQuery = [];
|
foreach (jvbAuthorUsers() as $type) {
|
if (array_key_exists($type, $data)) {
|
$artist_ids = array_map(
|
'absint',
|
is_array($data[$type]) ?
|
$data[$type] :
|
explode(',', $data[$type])
|
);
|
$authorQuery = array_merge($authorQuery, $artist_ids);
|
}
|
}
|
if (!empty($authorQuery)) {
|
$args['author__in'] = array_unique($authorQuery);
|
}
|
|
return $args;
|
}
|
|
protected function applyOrderFilters(array $args, array $data):array
|
{
|
if (array_key_exists('orderby', $data) && $data['orderby'] === 'random') {
|
$current_seed = jvbGetRandomSeed();
|
$args['orderby'] = 'RAND(' . $current_seed . ')';
|
unset($args['order']);
|
return $args;
|
}
|
if (in_array($data['orderby'], ['date', 'title', 'alphabetical'])) {
|
$args['orderby'] = ($data['orderby'] === 'alphabetical') ? 'title' : $data['orderby'];
|
} else {
|
switch ($data['orderby']) {
|
case 'popularity':
|
$args['meta_key'] = BASE.'upvotes';
|
$args['orderby'] = 'meta_value_num';
|
break;
|
case 'karma':
|
$args['meta_key'] = BASE.'karma';
|
$args['orderby'] = 'meta_value_num';
|
break;
|
default:
|
$args['orderby'] = 'date';
|
}
|
}
|
$order = (array_key_exists('order', $data)) ? strtoupper($data['order']) : 'DESC';
|
$args['order'] = (in_array($order, ['ASC', 'DESC'])) ? $order : 'DESC';
|
|
return $args;
|
}
|
|
protected function applyDateFilters(array $args, array $data):array
|
{
|
if (!array_key_exists('date-filter', $data) && !array_key_exists('dateFrom', $data)) {
|
return $args;
|
}
|
if (array_key_exists('dateFrom', $data)) {
|
$dateFrom = strtotime(sanitize_text_field($data['dateFrom']));
|
$dateTo = strtotime(sanitize_text_field($data['dateTo']));
|
if ($dateFrom && $dateTo) {
|
$args['date_query'] = [
|
[
|
'after' => date('c', $dateFrom),
|
'before' => date('c', $dateTo),
|
'inclusive' => true,
|
]
|
];
|
}
|
} else {
|
switch ($data['date-filter']) {
|
case 'today':
|
$args['date_query'] = [['after' => '1 day ago']];
|
break;
|
case 'week':
|
$args['date_query'] = [['after' => '1 week ago']];
|
break;
|
case 'month':
|
$args['date_query'] = [['after' => '1 month ago']];
|
break;
|
case 'year':
|
$args['date_query'] = [['after' => '1 year ago']];
|
break;
|
}
|
}
|
return $args;
|
}
|
|
protected function applyCalendarFilters(array $args, array $data):array
|
{
|
$meta_query = [];
|
$today = date('Y-m-d');
|
if (in_array('future', $args['post_status'])) {
|
$meta_query[] = [
|
'key' => 'jvb_start_date',
|
'value' => $today,
|
'compare' => '>=',
|
'type' => 'DATE'
|
];
|
}
|
if (in_array('past', $args['post_status'])) {
|
$meta_query[] = [
|
'key' => 'jvb_end_date',
|
'value' => $today,
|
'compare' => '<',
|
'type' => 'DATE'
|
];
|
}
|
if (in_array('recurring', $args['post_status'])) {
|
$meta_query[] = [
|
'key' => 'jvb_is_recurring',
|
'value' => true,
|
'compare' => '='
|
];
|
}
|
if (!empty($meta_query)) {
|
$args['meta_query'] = (array_key_exists('meta_query', $args)) ? array_merge($args['meta_query'], $meta_query) : $meta_query;
|
}
|
return $args;
|
|
}
|
|
protected function table_exists($tableName, $wpdb = null)
|
{
|
if (!$wpdb) {
|
global $wpdb;
|
}
|
return $wpdb->get_var("SHOW TABLES LIKE '{$tableName}'") !== $tableName;
|
}
|
|
protected function ifModifiedSince($lastModified, $params, $request):WP_REST_Response|null {
|
$etag = '"' . md5(serialize($params)) . '"';
|
// Check ETag
|
$if_none_match = $request->get_header('If-None-Match');
|
if ($if_none_match && $if_none_match === $etag) {
|
return new WP_REST_Response(null, 304);
|
}
|
|
$if_modified_since = $request->get_header('If-Modified-Since');
|
if ($if_modified_since && $lastModified) {
|
$if_modified_timestamp = strtotime($if_modified_since);
|
if ($lastModified <= $if_modified_timestamp) {
|
return new WP_REST_Response(null, 304);
|
}
|
}
|
|
header('ETag: ' . $etag); // Add this line
|
if ($lastModified) {
|
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $lastModified) . ' GMT');
|
}
|
header('Cache-Control: private, max-age=30');
|
return null;
|
}
|
}
|