'sanitize_text_field', ) ); register_setting( 'wpuhc_settings_group', 'wpuhc_allowed_ips', array( 'sanitize_callback' => 'sanitize_text_field', ) ); } function wpuhc_settings_page() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Unauthorized capability.', 'absoluit-user-hub' ) ); } $api_key = get_option( 'wpuhc_api_key', '' ); if ( empty( $api_key ) ) { $api_key = wp_generate_password( 48, false ); update_option( 'wpuhc_api_key', $api_key ); } $site_label = get_option( 'wpuhc_site_label', get_bloginfo( 'name' ) ); $allowed_ips = get_option( 'wpuhc_allowed_ips', '' ); // Handle regenerate key if ( isset( $_POST['wpuhc_regenerate_key'] ) && check_admin_referer( 'wpuhc_regenerate_key_nonce' ) ) { $api_key = wp_generate_password( 48, false ); update_option( 'wpuhc_api_key', $api_key ); echo '

' . esc_html__( 'API Key regenerated successfully!', 'absoluit-user-hub' ) . '

'; } ?>


get_param( 'api_key' ); $provided_timestamp = $request->get_param( 'timestamp' ); $provided_nonce = $request->get_param( 'nonce' ); $provided_signature = $request->get_param( 'signature' ); $stored_key = get_option( 'wpuhc_api_key', '' ); // --- Layer 1: API Key Validation --- if ( empty( $stored_key ) ) { return new WP_Error( 'rest_forbidden', __( 'API Key is not configured on this site.', 'absoluit-user-hub' ), array( 'status' => 403 ) ); } if ( empty( $provided_key ) || ! hash_equals( $stored_key, $provided_key ) ) { return new WP_Error( 'rest_forbidden', __( 'Invalid or missing API key.', 'absoluit-user-hub' ), array( 'status' => 401 ) ); } // --- Layer 2: IP Whitelist Validation --- $allowed_ips_str = get_option( 'wpuhc_allowed_ips', '' ); if ( ! empty( $allowed_ips_str ) ) { $allowed_ips = array_map( 'trim', explode( ',', $allowed_ips_str ) ); $client_ip = wpuhc_get_client_ip(); if ( ! in_array( $client_ip, $allowed_ips, true ) ) { wpuhc_log_security_failure( 'IP not in whitelist: ' . $client_ip, $request ); return new WP_Error( 'rest_forbidden', __( 'Your IP address is not whitelisted.', 'absoluit-user-hub' ), array( 'status' => 403 ) ); } } // --- Layer 3: Timestamp Validation (5 Minute Expiry) --- if ( empty( $provided_timestamp ) || abs( time() - (int)$provided_timestamp ) > 300 ) { wpuhc_log_security_failure( 'Timestamp expired or invalid', $request ); return new WP_Error( 'rest_forbidden', __( 'Request expired or timestamp invalid.', 'absoluit-user-hub' ), array( 'status' => 403 ) ); } // --- Layer 4: Nonce Validation (Replay Attack Prevention) --- if ( empty( $provided_nonce ) ) { return new WP_Error( 'rest_forbidden', __( 'Missing request nonce.', 'absoluit-user-hub' ), array( 'status' => 403 ) ); } $nonce_key = 'wpuhc_nonce_' . $provided_nonce; if ( get_transient( $nonce_key ) ) { wpuhc_log_security_failure( 'Duplicate nonce (replay)', $request ); return new WP_Error( 'rest_forbidden', __( 'Duplicate request detected.', 'absoluit-user-hub' ), array( 'status' => 403 ) ); } // Store nonce for 5 minutes (same as timestamp expiry) set_transient( $nonce_key, true, 300 ); // --- Layer 5: HMAC Signature Verification --- if ( empty( $provided_signature ) ) { return new WP_Error( 'rest_forbidden', __( 'Missing request signature.', 'absoluit-user-hub' ), array( 'status' => 403 ) ); } $raw_body = $request->get_body(); // Reconstruct payload: timestamp + nonce + body $payload = $provided_timestamp . $provided_nonce . $raw_body; $expected_signature = hash_hmac( 'sha256', $payload, $stored_key ); if ( ! hash_equals( $expected_signature, $provided_signature ) ) { wpuhc_log_security_failure( 'Invalid signature', $request ); return new WP_Error( 'rest_forbidden', __( 'Invalid request signature.', 'absoluit-user-hub' ), array( 'status' => 403 ) ); } return true; } /** * Log security failures for debugging purposes. */ function wpuhc_log_security_failure( $reason, WP_REST_Request $request ) { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { $log_entry = sprintf( '[WPUHC Security Failure] Reason: %s | IP: %s | Nonce: %s | TS: %s', $reason, wpuhc_get_client_ip(), $request->get_param( 'nonce' ), $request->get_param( 'timestamp' ) ); error_log( $log_entry ); } } function wpuhc_get_client_ip() { // Check Cloudflare header if ( ! empty( $_SERVER['HTTP_CF_CONNECTING_IP'] ) ) { $ip = trim( sanitize_text_field( wp_unslash( $_SERVER['HTTP_CF_CONNECTING_IP'] ) ) ); if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) { return $ip; } } // Check X-Forwarded-For header if ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) { $ips = explode( ',', $_SERVER['HTTP_X_FORWARDED_FOR'] ); foreach ( $ips as $ip ) { $ip = trim( sanitize_text_field( wp_unslash( $ip ) ) ); if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) { return $ip; } } } // Check X-Real-IP header if ( ! empty( $_SERVER['HTTP_X_REAL_IP'] ) ) { $ip = trim( sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_REAL_IP'] ) ) ); if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) { return $ip; } } // Default to REMOTE_ADDR if ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) { $ip = trim( sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) ); if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) { return $ip; } } return '0.0.0.0'; } /** * ============================================================= * REST API ENDPOINTS * ============================================================= */ add_action( 'rest_api_init', 'wpuhc_register_rest_routes' ); function wpuhc_register_rest_routes() { $namespace = 'wp-user-hub/v1'; // Health check / Ping register_rest_route( $namespace, '/ping', array( 'methods' => 'GET', 'callback' => 'wpuhc_endpoint_ping', 'permission_callback' => 'wpuhc_verify_api_key', ) ); // List users register_rest_route( $namespace, '/users', array( 'methods' => 'GET', 'callback' => 'wpuhc_endpoint_list_users', 'permission_callback' => 'wpuhc_verify_api_key', ) ); // Create user register_rest_route( $namespace, '/users', array( 'methods' => 'POST', 'callback' => 'wpuhc_endpoint_create_user', 'permission_callback' => 'wpuhc_verify_api_key', ) ); // Delete user register_rest_route( $namespace, '/users/(?P[^/]+)', array( 'methods' => 'DELETE', 'callback' => 'wpuhc_endpoint_delete_user', 'permission_callback' => 'wpuhc_verify_api_key', ) ); // Update user register_rest_route( $namespace, '/users/(?P[a-zA-Z0-9_.@+-]+)', array( 'methods' => 'PUT', 'callback' => 'wpuhc_endpoint_update_user', 'permission_callback' => 'wpuhc_verify_api_key', ) ); // Search user by email register_rest_route( $namespace, '/users/search', array( 'methods' => 'GET', 'callback' => 'wpuhc_endpoint_search_user', 'permission_callback' => 'wpuhc_verify_api_key', ) ); } /** * PING — Health check */ function wpuhc_endpoint_ping( WP_REST_Request $request ) { return rest_ensure_response( array( 'status' => 'ok', 'site' => get_bloginfo( 'name' ), 'label' => get_option( 'wpuhc_site_label', get_bloginfo( 'name' ) ), 'url' => home_url(), 'wp' => get_bloginfo( 'version' ), 'php' => phpversion(), 'plugin' => WPUHC_VERSION, 'time' => current_time( 'mysql' ), 'users' => count_users(), ) ); } /** * LIST USERS — Get all users on this site */ function wpuhc_endpoint_list_users( WP_REST_Request $request ) { $role = $request->get_param( 'role' ); $search = $request->get_param( 'search' ); $args = array( 'number' => 500, 'orderby' => 'display_name', 'order' => 'ASC', ); if ( ! empty( $role ) ) { $args['role'] = sanitize_text_field( $role ); } if ( ! empty( $search ) ) { $args['search'] = '*' . sanitize_text_field( $search ) . '*'; $args['search_columns'] = array( 'user_login', 'user_email', 'display_name' ); } $users = get_users( $args ); $output = array(); foreach ( $users as $user ) { $output[] = array( 'id' => $user->ID, 'username' => $user->user_login, 'email' => $user->user_email, 'first_name' => $user->first_name, 'last_name' => $user->last_name, 'display_name' => $user->display_name, 'role' => ! empty( $user->roles ) ? $user->roles[0] : 'none', 'registered' => $user->user_registered, ); } return rest_ensure_response( array( 'site' => get_bloginfo( 'name' ), 'url' => home_url(), 'count' => count( $output ), 'users' => $output, ) ); } /** * CREATE USER */ function wpuhc_endpoint_create_user( WP_REST_Request $request ) { $params = $request->get_json_params(); $username = isset( $params['username'] ) ? sanitize_user( $params['username'] ) : ''; $email = isset( $params['email'] ) ? sanitize_email( $params['email'] ) : ''; $password = isset( $params['password'] ) ? $params['password'] : wp_generate_password( 16, true ); $first_name = isset( $params['first_name'] ) ? sanitize_text_field( $params['first_name'] ) : ''; $last_name = isset( $params['last_name'] ) ? sanitize_text_field( $params['last_name'] ) : ''; $role = isset( $params['role'] ) ? sanitize_text_field( $params['role'] ) : 'subscriber'; // Validation if ( empty( $username ) || empty( $email ) ) { return new WP_Error( 'missing_fields', __( 'Username and email are required.', 'absoluit-user-hub' ), array( 'status' => 400 ) ); } if ( ! is_email( $email ) ) { return new WP_Error( 'invalid_email', __( 'Invalid email address.', 'absoluit-user-hub' ), array( 'status' => 400 ) ); } // Input Bounds Checking if ( strlen( $username ) > 60 ) { return new WP_Error( 'invalid_username_length', __( 'Username must be 60 characters or less.', 'absoluit-user-hub' ), array( 'status' => 400 ) ); } if ( strlen( $email ) > 100 ) { return new WP_Error( 'invalid_email_length', __( 'Email must be 100 characters or less.', 'absoluit-user-hub' ), array( 'status' => 400 ) ); } if ( strlen( $password ) > 100 ) { return new WP_Error( 'invalid_password_length', __( 'Password must be 100 characters or less.', 'absoluit-user-hub' ), array( 'status' => 400 ) ); } if ( strlen( $first_name ) > 50 ) { return new WP_Error( 'invalid_firstname_length', __( 'First name must be 50 characters or less.', 'absoluit-user-hub' ), array( 'status' => 400 ) ); } if ( strlen( $last_name ) > 50 ) { return new WP_Error( 'invalid_lastname_length', __( 'Last name must be 50 characters or less.', 'absoluit-user-hub' ), array( 'status' => 400 ) ); } // Strict Role Whitelisting $allowed_roles = array( 'subscriber', 'contributor', 'author', 'editor', 'administrator' ); if ( ! in_array( $role, $allowed_roles, true ) ) { return new WP_Error( 'invalid_role', __( 'Invalid user role requested.', 'absoluit-user-hub' ), array( 'status' => 400 ) ); } // Check if user already exists if ( username_exists( $username ) ) { // If the email matches, just update their role $existing = get_user_by( 'login', $username ); if ( $existing && $existing->user_email === $email ) { $existing->set_role( $role ); return rest_ensure_response( array( 'status' => 'exists', 'message' => __( 'User already exists. Role updated.', 'absoluit-user-hub' ), 'user_id' => $existing->ID, ) ); } return new WP_Error( 'user_exists', __( 'Username already exists with a different email.', 'absoluit-user-hub' ), array( 'status' => 409 ) ); } if ( email_exists( $email ) ) { $existing = get_user_by( 'email', $email ); $existing->set_role( $role ); return rest_ensure_response( array( 'status' => 'exists', 'message' => __( 'Email already exists. Role updated.', 'absoluit-user-hub' ), 'user_id' => $existing->ID, ) ); } // Create user $user_id = wp_insert_user( array( 'user_login' => $username, 'user_email' => $email, 'user_pass' => $password, 'first_name' => $first_name, 'last_name' => $last_name, 'role' => $role, ) ); if ( is_wp_error( $user_id ) ) { return new WP_Error( 'create_failed', $user_id->get_error_message(), array( 'status' => 500 ) ); } return rest_ensure_response( array( 'status' => 'created', 'message' => __( 'User created successfully.', 'absoluit-user-hub' ), 'user_id' => $user_id, ) ); } /** * DELETE USER — by email or username */ function wpuhc_endpoint_delete_user( WP_REST_Request $request ) { require_once ABSPATH . 'wp-admin/includes/user.php'; $identifier = sanitize_text_field( $request->get_param( 'identifier' ) ); $reassign = $request->get_param( 'reassign' ); // Try to find user by ID first if provided $user_id = $request->get_param( 'user_id' ); if ( ! empty( $user_id ) ) { $user = get_userdata( absint( $user_id ) ); } if ( empty( $user ) ) { // Find user by email or username (try both, no restrictive checks) $user = get_user_by( 'email', $identifier ); if ( ! $user ) { $user = get_user_by( 'login', $identifier ); } } // Last ditch effort: try decoding the identifier (in case of double encoding) if ( empty( $user ) && strpos( $identifier, '%' ) !== false ) { $decoded = urldecode( $identifier ); $user = get_user_by( 'email', $decoded ); if ( ! $user ) { $user = get_user_by( 'login', $decoded ); } } if ( ! $user ) { return rest_ensure_response( array( 'status' => 'not_found', 'message' => sprintf( __( 'User "%s" not found on this site.', 'absoluit-user-hub' ), $identifier ), ) ); } // Don't allow deletion of user ID 1 (main admin) if ( $user->ID === 1 ) { return new WP_Error( 'cannot_delete', __( 'Cannot delete the primary administrator.', 'absoluit-user-hub' ), array( 'status' => 403 ) ); } // Reassign posts to specified user or fallback to first available administrator $reassign_to = ! empty( $reassign ) ? absint( $reassign ) : 0; if ( ! $reassign_to || ! get_userdata( $reassign_to ) ) { // Fallback to ID 1 if it exists if ( get_userdata( 1 ) ) { $reassign_to = 1; } else { // Fallback to first available administrator $admins = get_users( array( 'role' => 'administrator', 'number' => 1 ) ); if ( ! empty( $admins ) ) { $reassign_to = $admins[0]->ID; } else { // Last resort: don't reassign (delete posts) $reassign_to = null; } } } $result = wp_delete_user( $user->ID, $reassign_to ); if ( ! $result ) { return new WP_Error( 'delete_failed', __( 'Failed to delete user. This might be due to database permissions or reassignment issues.', 'absoluit-user-hub' ), array( 'status' => 500 ) ); } return rest_ensure_response( array( 'status' => 'deleted', 'message' => sprintf( __( 'User "%s" deleted successfully.', 'absoluit-user-hub' ), $identifier ), 'reassigned' => $reassign_to, ) ); } /** * UPDATE USER — change role or info */ function wpuhc_endpoint_update_user( WP_REST_Request $request ) { $identifier = sanitize_text_field( $request->get_param( 'identifier' ) ); $params = $request->get_json_params(); // Try to find user by ID first if provided $user_id = $request->get_param( 'user_id' ); if ( ! empty( $user_id ) ) { $user = get_userdata( absint( $user_id ) ); } if ( empty( $user ) ) { // Find user by email or username $user = get_user_by( 'email', $identifier ); if ( ! $user ) { $user = get_user_by( 'login', $identifier ); } } if ( ! $user ) { return new WP_Error( 'not_found', sprintf( __( 'User "%s" not found on this site.', 'absoluit-user-hub' ), $identifier ), array( 'status' => 404 ) ); } // Input validation & strict whitelisting for update parameters if ( isset( $params['role'] ) ) { $role = sanitize_text_field( $params['role'] ); $allowed_roles = array( 'subscriber', 'contributor', 'author', 'editor', 'administrator' ); if ( ! in_array( $role, $allowed_roles, true ) ) { return new WP_Error( 'invalid_role', __( 'Invalid user role requested.', 'absoluit-user-hub' ), array( 'status' => 400 ) ); } } if ( isset( $params['first_name'] ) ) { $first_name = sanitize_text_field( $params['first_name'] ); if ( strlen( $first_name ) > 50 ) { return new WP_Error( 'invalid_firstname_length', __( 'First name must be 50 characters or less.', 'absoluit-user-hub' ), array( 'status' => 400 ) ); } } if ( isset( $params['last_name'] ) ) { $last_name = sanitize_text_field( $params['last_name'] ); if ( strlen( $last_name ) > 50 ) { return new WP_Error( 'invalid_lastname_length', __( 'Last name must be 50 characters or less.', 'absoluit-user-hub' ), array( 'status' => 400 ) ); } } if ( isset( $params['email'] ) ) { $email = sanitize_email( $params['email'] ); if ( strlen( $email ) > 100 ) { return new WP_Error( 'invalid_email_length', __( 'Email must be 100 characters or less.', 'absoluit-user-hub' ), array( 'status' => 400 ) ); } if ( ! is_email( $email ) ) { return new WP_Error( 'invalid_email', __( 'Invalid email address.', 'absoluit-user-hub' ), array( 'status' => 400 ) ); } } if ( isset( $params['password'] ) ) { $password = $params['password']; if ( strlen( $password ) > 100 ) { return new WP_Error( 'invalid_password_length', __( 'Password must be 100 characters or less.', 'absoluit-user-hub' ), array( 'status' => 400 ) ); } } $update_data = array( 'ID' => $user->ID ); if ( isset( $params['role'] ) ) { $user->set_role( sanitize_text_field( $params['role'] ) ); } if ( isset( $params['first_name'] ) ) { $update_data['first_name'] = sanitize_text_field( $params['first_name'] ); } if ( isset( $params['last_name'] ) ) { $update_data['last_name'] = sanitize_text_field( $params['last_name'] ); } if ( isset( $params['email'] ) ) { $update_data['user_email'] = sanitize_email( $params['email'] ); } if ( isset( $params['password'] ) ) { $update_data['user_pass'] = $params['password']; } if ( count( $update_data ) > 1 ) { $result = wp_update_user( $update_data ); if ( is_wp_error( $result ) ) { return new WP_Error( 'update_failed', $result->get_error_message(), array( 'status' => 500 ) ); } } return rest_ensure_response( array( 'status' => 'updated', 'message' => sprintf( __( 'User "%s" updated successfully.', 'absoluit-user-hub' ), $identifier ), ) ); } /** * SEARCH USER — Find if a user exists by email or username */ function wpuhc_endpoint_search_user( WP_REST_Request $request ) { $query = $request->get_param( 'q' ); if ( empty( $query ) ) { return new WP_Error( 'missing_query', __( 'Search query is required.', 'absoluit-user-hub' ), array( 'status' => 400 ) ); } $query = sanitize_text_field( $query ); // Search by email first, then username $user = get_user_by( 'email', $query ); if ( ! $user ) { $user = get_user_by( 'login', $query ); } if ( ! $user ) { return rest_ensure_response( array( 'found' => false, 'site' => get_bloginfo( 'name' ), 'url' => home_url(), ) ); } return rest_ensure_response( array( 'found' => true, 'site' => get_bloginfo( 'name' ), 'url' => home_url(), 'user' => array( 'id' => $user->ID, 'username' => $user->user_login, 'email' => $user->user_email, 'first_name' => $user->first_name, 'last_name' => $user->last_name, 'display_name' => $user->display_name, 'role' => ! empty( $user->roles ) ? $user->roles[0] : 'none', 'registered' => $user->user_registered, ), ) ); }