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; } }