WP_REST_Server::READABLE, // GET 'callback' => 'aitepc_rest_unsubscribe_callback', 'permission_callback' => '__return_true', // Token-verified; no auth required. 'args' => array( 'user_id' => array( 'required' => true, 'sanitize_callback' => 'absint', 'validate_callback' => static function ( $param ) { return is_numeric( $param ) && (int) $param > 0; }, 'description' => __( 'The user ID to unsubscribe.', 'ait-easy-post-customization' ), ), 'token' => array( 'required' => true, 'sanitize_callback' => 'sanitize_text_field', 'description' => __( 'HMAC verification token.', 'ait-easy-post-customization' ), ), ), ) ); } add_action( 'rest_api_init', 'aitepc_register_rest_routes' ); /** * Handle an unsubscribe request. * * Validates the HMAC token derived from user ID + email (stateless — * no token DB table needed). Uses hash_equals() for timing-safe comparison. * Sets user meta 'aitepc_email_unsubscribe' = '1' on success. * * @param WP_REST_Request $request Incoming REST request. * @return WP_REST_Response|WP_Error JSON response. */ function aitepc_rest_unsubscribe_callback( WP_REST_Request $request ) { $user_id = $request->get_param( 'user_id' ); $token = $request->get_param( 'token' ); $user = get_userdata( $user_id ); if ( ! $user ) { return new WP_Error( 'aitepc_invalid_user', __( 'User not found.', 'ait-easy-post-customization' ), array( 'status' => 404 ) ); } // Build the expected token — same algorithm used when generating the link. $expected = wp_hash( $user->ID . $user->user_email ); // Timing-safe comparison prevents brute-force token guessing. if ( ! hash_equals( $expected, $token ) ) { return new WP_Error( 'aitepc_invalid_token', __( 'Invalid or expired unsubscribe token.', 'ait-easy-post-customization' ), array( 'status' => 403 ) ); } // Mark the user as unsubscribed. update_user_meta( $user_id, 'aitepc_email_unsubscribe', '1' ); return new WP_REST_Response( array( 'success' => true, 'message' => __( 'You have been unsubscribed from expiry notifications.', 'ait-easy-post-customization' ), ), 200 ); }