From d7dbe7fee362d587dfc334135d9581b6216a4295 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 23 Nov 2025 04:13:56 +0000
Subject: [PATCH] =Timeline block, and feed block updated. DataStore.js refactored to not block rendering

---
 inc/rest/RestRouteManager.php |  436 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 435 insertions(+), 1 deletions(-)

diff --git a/inc/rest/RestRouteManager.php b/inc/rest/RestRouteManager.php
index 8f1b1ba..3d9b2a4 100644
--- a/inc/rest/RestRouteManager.php
+++ b/inc/rest/RestRouteManager.php
@@ -6,6 +6,7 @@
 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 +37,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 +115,19 @@
         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;
+	}
+
 	protected function checkContent(string $content, bool $bool = false):string|bool
 	{
 		$result = JVB_CONTENT[$content]??JVB_TAXONOMY[$content]??JVB_USER[$content]??'';
@@ -520,6 +534,392 @@
 
 		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 = [
+			'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
+	{
+		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 +1005,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