'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,
),
) );
}