From 474109a5df0a06f5343ab184838fe2d80e3872a8 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 11 Jan 2026 19:23:20 +0000
Subject: [PATCH] =Fixed timeline CRUD.js issue where this.activeItem was set null when we still needed it
---
inc/rest/RestRouteManager.php | 466 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 465 insertions(+), 1 deletions(-)
diff --git a/inc/rest/RestRouteManager.php b/inc/rest/RestRouteManager.php
index 8f1b1ba..f017e9c 100644
--- a/inc/rest/RestRouteManager.php
+++ b/inc/rest/RestRouteManager.php
@@ -1,11 +1,14 @@
<?php
namespace JVBase\rest;
+use DateTime;
+use DateTimeZone;
use JVBase\JVB;
use JVBase\rest\RateLimiter;
use JVBase\managers\OperationQueue;
use JVBase\managers\CacheManager;
use JVBase\managers\NotificationManager;
+use JVBase\utility\Features;
use WP_REST_Request;
use WP_Error;
use Exception;
@@ -36,7 +39,7 @@
protected NotificationManager $notifications;
protected string $cache_name ='';
protected int $cache_ttl = 3600; //1 hour default
-
+ protected array $response_headers = [];
// Error code constants for consistency
const ERROR_MISSING_PARAMS = 'missing_parameters';
@@ -114,6 +117,45 @@
return true;
}
+ public function checkRateLimit(WP_REST_Request $request):bool|WP_Error
+ {
+ if (!$this->rate_limiter->checkLimit($request)) {
+ error_log('Rate Limit Reached');
+ return new WP_Error(
+ 'rate_limit',
+ 'Too many attempts. Please wait a moment before trying again.',
+ ['status' => 429]
+ );
+ }
+ return true;
+ }
+
+ /**
+ * Convert MySQL datetime to ISO 8601 timestamp with proper timezone
+ */
+ public function formatTimestamp(?string $mysql_datetime): ?string
+ {
+ if (empty($mysql_datetime)) {
+ return null;
+ }
+
+ try {
+ // Get WordPress timezone - dates are stored in this timezone
+ $wp_timezone = wp_timezone();
+
+ // Parse the datetime in WordPress timezone
+ $date = new DateTime($mysql_datetime, $wp_timezone);
+
+ // Convert to UTC for API consistency
+ $date->setTimezone(new DateTimeZone('UTC'));
+
+ // Return ISO 8601 format
+ return $date->format('c');
+ } catch (Exception $e) {
+ return null;
+ }
+ }
+
protected function checkContent(string $content, bool $bool = false):string|bool
{
$result = JVB_CONTENT[$content]??JVB_TAXONOMY[$content]??JVB_USER[$content]??'';
@@ -520,6 +562,394 @@
return $this->checkHeaders($request, $types, ['user_id' => $user_id]);
}
+
+ /**
+ * Helper to return error response
+ */
+ protected function error(string $message, string $code, int $status = 400, ?string $field = null): WP_REST_Response
+ {
+ $data = [
+ 'success' => false,
+ 'message' => $message,
+ 'code' => $code
+ ];
+
+ if ($field) {
+ $data['field'] = $field;
+ }
+
+ return new WP_REST_Response($data, $status);
+ }
+ /**
+ * Helper to return success response
+ */
+ protected function success(array $data, int $status = 200): WP_REST_Response
+ {
+ $data['success'] = true;
+ return new WP_REST_Response($data, $status);
+ }
+
+ /************************************************************
+ SESSION FINGERPRINT
+ ************************************************************/
+ /**
+ * Store session fingerprint for hijacking detection
+ */
+ protected function storeSessionFingerprint(int $user_id, WP_REST_Request $request): void
+ {
+ if (!defined('JVB_SESSION_FINGERPRINT') || !JVB_SESSION_FINGERPRINT) {
+ return;
+ }
+
+ $fingerprint = $this->generateSessionFingerprint($request);
+ update_user_meta($user_id, BASE . 'session_fingerprint', $fingerprint);
+ update_user_meta($user_id, BASE . 'session_timestamp', time());
+ }
+ /**
+ * Generate session fingerprint for hijacking detection
+ *
+ * @param WP_REST_Request $request The REST request
+ * @return string Hashed fingerprint
+ */
+ protected function generateSessionFingerprint(WP_REST_Request $request): string
+ {
+ return hash('sha256', implode('|', [
+ $request->get_header('User-Agent') ?? '',
+ // Use IP class instead of full IP to allow for mobile network changes
+ $this->getIPClass(
+ $request->get_header('X-Forwarded-For')
+ ?: $request->get_header('X-Real-IP')
+ ?: $_SERVER['REMOTE_ADDR'] ?? ''
+ )
+ ]));
+ }
+
+ /**
+ * Get IP class (first 3 octets) for session validation
+ * This allows for minor IP changes (common with mobile networks)
+ *
+ * @param string $ip IP address
+ * @return string First 3 octets
+ */
+ protected function getIPClass(string $ip): string
+ {
+ $parts = explode('.', $ip);
+ return implode('.', array_slice($parts, 0, 3));
+ }
+
+ /**
+ * Validate session fingerprint against stored value
+ *
+ * @param int $user_id User ID to validate
+ * @param WP_REST_Request $request Current request
+ * @return bool True if valid, false if potential hijacking
+ */
+ protected function validateSessionFingerprint(int $user_id, WP_REST_Request $request): bool
+ {
+ // Only enforce if enabled in config
+ if (!defined('JVB_SESSION_FINGERPRINT') || !JVB_SESSION_FINGERPRINT) {
+ return true;
+ }
+
+ $stored = get_user_meta($user_id, BASE . 'session_fingerprint', true);
+ $current = $this->generateSessionFingerprint($request);
+
+ if (empty($stored)) {
+ // First request - store fingerprint
+ update_user_meta($user_id, BASE . 'session_fingerprint', $current);
+ update_user_meta($user_id, BASE . 'session_timestamp', time());
+ return true;
+ }
+
+ // Compare using timing-safe comparison
+ return hash_equals($stored, $current);
+ }
+
+ /**
+ * Clear session fingerprint (call on logout)
+ *
+ * @param int $user_id User ID
+ * @return void
+ */
+ protected function clearSessionFingerprint(int $user_id): void
+ {
+ delete_user_meta($user_id, BASE . 'session_fingerprint');
+ delete_user_meta($user_id, BASE . 'session_timestamp');
+ }
+
+ /******************************************************************
+ * CSRF PROTECTION
+ ******************************************************************/
+ /**
+ * Generate CSRF token for a user
+ *
+ * @param int $user_id User ID
+ * @return string CSRF token
+ */
+ protected function generateCSRFToken(int $user_id): string
+ {
+ $token = wp_generate_password(32, false);
+ set_transient(BASE . 'csrf_' . $user_id, $token, HOUR_IN_SECONDS);
+ return $token;
+ }
+
+ /**
+ * Validate CSRF token from request header
+ *
+ * @param WP_REST_Request $request The REST request
+ * @return bool True if valid or not required
+ */
+ protected function validateCSRFToken(WP_REST_Request $request): bool
+ {
+ // Only for authenticated requests
+ if (!is_user_logged_in()) {
+ return true;
+ }
+
+ // Only for state-changing operations
+ if (in_array($request->get_method(), ['GET', 'HEAD', 'OPTIONS'])) {
+ return true;
+ }
+
+ $user_id = get_current_user_id();
+ $token = $request->get_header('X-CSRF-Token');
+ $stored = get_transient(BASE . 'csrf_' . $user_id);
+
+ if (empty($stored) || empty($token)) {
+ return false;
+ }
+
+ return hash_equals($stored, $token);
+ }
+
+ /**
+ * Get current CSRF token for user (for frontend to use)
+ *
+ * @param int $user_id User ID
+ * @return string|null CSRF token or null
+ */
+ protected function getCSRFToken(int $user_id): ?string
+ {
+ $token = get_transient(BASE . 'csrf_' . $user_id);
+
+ if (!$token) {
+ $token = $this->generateCSRFToken($user_id);
+ }
+
+ return $token;
+ }
+
+ /***********************************************************
+ * REQUEST SIGNATURE VERIFICATION
+ ***********************************************************/
+ /**
+ * Verify request signature for sensitive operations
+ * Adds an additional layer of security for critical endpoints
+ *
+ * @param WP_REST_Request $request The REST request
+ * @param array $sensitive_params Params to include in signature
+ * @return bool True if valid signature
+ */
+ protected function verifyRequestSignature(WP_REST_Request $request, array $sensitive_params): bool
+ {
+ $signature = $request->get_header('X-Request-Signature');
+ if (empty($signature)) {
+ return false;
+ }
+
+ // Get user's signing key (rotated on each login)
+ $user_id = get_current_user_id();
+ if (!$user_id) {
+ return false;
+ }
+
+ $signing_key = get_user_meta($user_id, BASE . 'signing_key', true);
+ if (empty($signing_key)) {
+ // Generate new key if missing
+ $signing_key = wp_generate_password(64, false);
+ update_user_meta($user_id, BASE . 'signing_key', $signing_key);
+ }
+
+ // Build signature from sensitive params
+ ksort($sensitive_params);
+ $message = json_encode($sensitive_params);
+ $expected = hash_hmac('sha256', $message, $signing_key);
+
+ return hash_equals($expected, $signature);
+ }
+
+ /**
+ * Rotate signing key (call on login)
+ *
+ * @param int $user_id User ID
+ * @return string New signing key
+ */
+ protected function rotateSigningKey(int $user_id): string
+ {
+ $new_key = wp_generate_password(64, false);
+ update_user_meta($user_id, BASE . 'signing_key', $new_key);
+ return $new_key;
+ }
+ /**********************************************************
+ * AUDIT LOGGING
+ **********************************************************/
+ /**
+ * Log security-relevant events
+ *
+ * @param string $event Event name
+ * @param array $data Additional event data
+ * @return void
+ */
+ protected function auditLog(string $event, array $data = []): void
+ {
+ // Add standard context
+ $context = array_merge($data, [
+ 'timestamp' => current_time('mysql'),
+ 'user_id' => get_current_user_id() ?: 0,
+ 'ip' => $_SERVER['REMOTE_ADDR'] ?? '',
+ 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
+ ]);
+
+ // Use existing error handler
+ try {
+ JVB()->error()->log(
+ 'security_audit',
+ $event,
+ $context,
+ 'info'
+ );
+ } catch (\Exception $e) {
+ // Fallback to error_log if handler fails
+ error_log("Security Audit: {$event} - " . json_encode($context));
+ }
+ }
+
+ /***************************************************************
+ * SANITIZATION HELPERS
+ ***************************************************************/
+ /**
+ * Sanitize array of IDs
+ *
+ * @param array $ids Array of IDs
+ * @return array Sanitized array of integers
+ */
+ protected function sanitizeIDs(array $ids): array
+ {
+ return array_map('absint', array_filter($ids, function($id) {
+ return is_numeric($id) && $id > 0;
+ }));
+ }
+
+ /***************************************************************
+ * RESPONSE HELPERS
+ ***************************************************************/
+ /**
+ * Return validation error response
+ *
+ * @param array $errors Validation errors (field => message)
+ * @return WP_REST_Response 422 response with errors
+ */
+ protected function validationError(array $errors): WP_REST_Response
+ {
+ return new WP_REST_Response([
+ 'success' => false,
+ 'message' => 'Validation failed',
+ 'errors' => $errors
+ ], 422);
+ }
+
+ /**
+ * Return unauthorized error response
+ *
+ * @param string $message Error message
+ * @return WP_REST_Response 401 response
+ */
+ protected function unauthorized(string $message = 'Unauthorized'): WP_REST_Response
+ {
+ return new WP_REST_Response([
+ 'success' => false,
+ 'message' => $message
+ ], 401);
+ }
+
+ /**
+ * Return forbidden error response
+ *
+ * @param string $message Error message
+ * @return WP_REST_Response 403 response
+ */
+ protected function forbidden(string $message = 'Forbidden'): WP_REST_Response
+ {
+ return new WP_REST_Response([
+ 'success' => false,
+ 'message' => $message
+ ], 403);
+ }
+
+ /**
+ * Return not found error response
+ *
+ * @param string $message Error message
+ * @return WP_REST_Response 404 response
+ */
+ protected function notFound(string $message = 'Not found'): WP_REST_Response
+ {
+ return new WP_REST_Response([
+ 'success' => false,
+ 'message' => $message
+ ], 404);
+ }
+
+ /*****************************************************************
+ * CONCURRENT CHECKS
+ *****************************************************************/
+ /**
+ * Prevent concurrent requests for the same operation
+ * Useful for preventing double-submissions
+ *
+ * @param string $operation_key Unique key for this operation
+ * @param int $lock_duration Lock duration in seconds
+ * @return bool True if lock acquired, false if already locked
+ */
+ protected function acquireOperationLock(string $operation_key, int $lock_duration = 5): bool
+ {
+ $lock_key = 'op_lock_' . md5($operation_key);
+
+ // Try to acquire lock
+ $locked = get_transient($lock_key);
+ if ($locked) {
+ return false; // Already locked
+ }
+
+ // Set lock
+ set_transient($lock_key, true, $lock_duration);
+ return true;
+ }
+
+ /**
+ * Release operation lock
+ *
+ * @param string $operation_key Unique key for this operation
+ * @return void
+ */
+ protected function releaseOperationLock(string $operation_key): void
+ {
+ $lock_key = 'op_lock_' . md5($operation_key);
+ delete_transient($lock_key);
+ }
+
+ protected function verifyTurnstile(string $token): bool
+ {
+ if (!Features::hasIntegration('cloudflare') || !JVB()->connect('cloudflare')->isSetUp()) {
+ return true;
+ }
+
+ if (empty($token)) {
+ return false;
+ }
+
+ return JVB()->connect('cloudflare')->verifyTurnstile($token);
+ }
}
//
//Simple example:
@@ -605,3 +1035,37 @@
// $response = new WP_REST_Response($data);
// return $this->addCacheHeaders($response);
//}
+
+
+
+/**
+ * Use operation lock in your methods like this:
+ *
+ * public function updateContent(WP_REST_Request $request): WP_REST_Response
+ * {
+ * $user_id = get_current_user_id();
+ * $content_id = $request->get_param('content_id');
+ *
+ * // Prevent concurrent updates
+ * $lock_key = "update_{$user_id}_{$content_id}";
+ * if (!$this->acquireOperationLock($lock_key)) {
+ * return $this->error(
+ * 'An update is already in progress. Please wait.',
+ * 'concurrent_operation',
+ * 409
+ * );
+ * }
+ *
+ * try {
+ * // Do your operation
+ * $result = $this->doUpdate($content_id);
+ *
+ * $this->releaseOperationLock($lock_key);
+ * return $this->success($result);
+ *
+ * } catch (\Exception $e) {
+ * $this->releaseOperationLock($lock_key);
+ * return $this->error($e->getMessage(), 'operation_failed');
+ * }
+ * }
+ */
--
Gitblit v1.10.0