two-factor-extended/includes/class-rest-api.php
2026-03-28 10:06:01 +00:00

506 lines
12 KiB
PHP

<?php
/**
* REST API endpoints
*
* @package TwoFactorExtended
* @since 0.1.0
*/
// Prevent direct access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class Two_Factor_Extended_REST_API
*
* Provides REST API endpoints for Two-Factor Extended.
*
* @since 0.1.0
*/
class Two_Factor_Extended_REST_API {
/**
* API namespace.
*
* @since 0.1.0
* @var string
*/
const NAMESPACE = 'two-factor-extended/v1';
/**
* Constructor.
*
* @since 0.1.0
*/
public function __construct() {
$this->init_hooks();
}
/**
* Initialize WordPress hooks.
*
* @since 0.1.0
*/
private function init_hooks(): void {
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
}
/**
* Register REST API routes.
*
* @since 0.1.0
*/
public function register_routes(): void {
// GET /two-factor-extended/v1/status.
register_rest_route(
self::NAMESPACE,
'/status',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_status' ),
'permission_callback' => array( $this, 'check_manage_options_permission' ),
'args' => array(
'role' => array(
'type' => 'string',
'sanitize_callback' => 'sanitize_key',
'default' => '',
),
),
)
);
// GET /two-factor-extended/v1/users.
register_rest_route(
self::NAMESPACE,
'/users',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_users' ),
'permission_callback' => array( $this, 'check_manage_options_permission' ),
'args' => array(
'role' => array(
'type' => 'string',
'sanitize_callback' => 'sanitize_key',
'default' => '',
),
'non_compliant_only' => array(
'type' => 'boolean',
'default' => false,
),
),
)
);
// POST /two-factor-extended/v1/enforce.
register_rest_route(
self::NAMESPACE,
'/enforce',
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'enforce_requirements' ),
'permission_callback' => array( $this, 'check_manage_options_permission' ),
'args' => array(
'user_ids' => array(
'type' => 'array',
'required' => true,
'items' => array(
'type' => 'integer',
),
),
'reset_grace' => array(
'type' => 'boolean',
'default' => false,
),
),
)
);
// POST /two-factor-extended/v1/reset.
register_rest_route(
self::NAMESPACE,
'/reset',
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'reset_grace_period' ),
'permission_callback' => array( $this, 'check_manage_options_permission' ),
'args' => array(
'user_ids' => array(
'type' => 'array',
'required' => true,
'items' => array(
'type' => 'integer',
),
),
),
)
);
// GET /two-factor-extended/v1/report.
register_rest_route(
self::NAMESPACE,
'/report',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_report' ),
'permission_callback' => array( $this, 'check_manage_options_permission' ),
'args' => array(
'role' => array(
'type' => 'string',
'sanitize_callback' => 'sanitize_key',
'default' => '',
),
'format' => array(
'type' => 'string',
'enum' => array( 'json', 'csv' ),
'default' => 'json',
),
),
)
);
}
/**
* Check if user has manage_options permission.
*
* @since 0.1.0
*
* @return bool True if user has permission.
*/
public function check_manage_options_permission(): bool {
return current_user_can( 'manage_options' );
}
/**
* Get compliance status.
*
* @since 0.1.0
*
* @param WP_REST_Request $request Request object.
*
* @return WP_REST_Response Response object.
*/
public function get_status( WP_REST_Request $request ): WP_REST_Response {
try {
$role = $request->get_param( 'role' );
$compliance = two_factor_extended()->get_compliance_report();
if ( null === $compliance ) {
return new WP_REST_Response(
array(
'success' => false,
'message' => __( 'Compliance report module is not available.', 'two-factor-extended' ),
),
500
);
}
$stats = $compliance->get_compliance_stats( array( 'role' => is_string( $role ) ? $role : '' ) );
return new WP_REST_Response(
array(
'success' => true,
'data' => $stats,
),
200
);
} catch ( Exception $e ) {
return new WP_REST_Response(
array(
'success' => false,
'message' => __( 'Unable to retrieve compliance status.', 'two-factor-extended' ),
),
500
);
}
}
/**
* Get users list.
*
* @since 0.1.0
*
* @param WP_REST_Request $request Request object.
*
* @return WP_REST_Response Response object.
*/
public function get_users( WP_REST_Request $request ): WP_REST_Response {
$role = $request->get_param( 'role' );
$non_compliant_only = $request->get_param( 'non_compliant_only' );
$role_string = is_string( $role ) ? $role : '';
$compliance = two_factor_extended()->get_compliance_report();
if ( null === $compliance ) {
return new WP_REST_Response(
array(
'success' => false,
'message' => __( 'Compliance report module is not available.', 'two-factor-extended' ),
),
500
);
}
if ( $non_compliant_only ) {
$users = $compliance->get_non_compliant_users( array( 'role' => $role_string ) );
} else {
// Get all users with compliance status.
$user_args = array( 'fields' => 'all' );
if ( ! empty( $role_string ) ) {
$user_args['role'] = $role_string;
}
$all_users = get_users( $user_args );
$enforcement = two_factor_extended()->get_enforcement();
$users = array();
if ( null === $enforcement ) {
return new WP_REST_Response(
array(
'success' => false,
'message' => __( 'Enforcement module is not available.', 'two-factor-extended' ),
),
500
);
}
foreach ( $all_users as $user ) {
$required = $enforcement->get_required_providers_for_user( $user->ID );
$users[] = array(
'user_id' => $user->ID,
'user_login' => $user->user_login,
'user_email' => $user->user_email,
'roles' => Two_Factor_Extended_Role_Manager::get_user_roles( $user->ID ),
'compliant' => empty( $required ) || $enforcement->user_meets_requirements( $user->ID, $required ),
'in_grace' => $enforcement->is_in_grace_period( $user->ID ),
'grace_remaining' => $enforcement->get_grace_period_remaining_days( $user->ID ),
);
}
}
return new WP_REST_Response(
array(
'success' => true,
'data' => $users,
'total' => count( $users ),
),
200
);
}
/**
* Enforce 2FA requirements.
*
* @since 0.1.0
*
* @param WP_REST_Request $request Request object.
*
* @return WP_REST_Response Response object.
*/
public function enforce_requirements( WP_REST_Request $request ): WP_REST_Response {
$user_ids = $request->get_param( 'user_ids' );
$reset_grace = $request->get_param( 'reset_grace' );
if ( ! is_array( $user_ids ) ) {
return new WP_REST_Response(
array(
'success' => false,
'message' => __( 'Invalid user_ids parameter.', 'two-factor-extended' ),
),
400
);
}
$enforcement = two_factor_extended()->get_enforcement();
if ( null === $enforcement ) {
return new WP_REST_Response(
array(
'success' => false,
'message' => __( 'Enforcement module is not available.', 'two-factor-extended' ),
),
500
);
}
$processed = 0;
$errors = array();
foreach ( $user_ids as $user_id ) {
if ( ! is_numeric( $user_id ) ) {
$errors[] = 'Invalid user_id value provided.';
continue;
}
$user_id_int = (int) $user_id;
$user = get_userdata( $user_id_int );
if ( ! $user ) {
$errors[] = sprintf( 'User not found: %d', $user_id_int );
continue;
}
$required = $enforcement->get_required_providers_for_user( $user_id_int );
if ( empty( $required ) ) {
$errors[] = sprintf( 'No 2FA requirements for user: %s', $user->user_login );
continue;
}
// Set enforcement start date.
update_user_meta( $user_id_int, Two_Factor_Extended_Enforcement::META_ENFORCEMENT_START, time() );
if ( $reset_grace ) {
delete_user_meta( $user_id_int, Two_Factor_Extended_Enforcement::META_GRACE_NOTIFIED );
}
// Log the action.
$audit_log = two_factor_extended()->get_audit_log();
if ( null !== $audit_log ) {
$audit_log->log_event(
'api_enforce',
sprintf( 'REST API: Enforced 2FA for user %s', $user->user_login ),
$user_id_int,
array( 'required_providers' => $required )
);
}
++$processed;
}
return new WP_REST_Response(
array(
'success' => true,
'processed' => $processed,
'errors' => $errors,
),
200
);
}
/**
* Reset grace period.
*
* @since 0.1.0
*
* @param WP_REST_Request $request Request object.
*
* @return WP_REST_Response Response object.
*/
public function reset_grace_period( WP_REST_Request $request ): WP_REST_Response {
$user_ids = $request->get_param( 'user_ids' );
$processed = 0;
$errors = array();
if ( ! is_array( $user_ids ) ) {
return new WP_REST_Response(
array(
'success' => false,
'message' => __( 'Invalid user_ids parameter.', 'two-factor-extended' ),
),
400
);
}
foreach ( $user_ids as $user_id ) {
if ( ! is_numeric( $user_id ) ) {
$errors[] = 'Invalid user_id value provided.';
continue;
}
$user_id_int = (int) $user_id;
$user = get_userdata( $user_id_int );
if ( ! $user ) {
$errors[] = sprintf( 'User not found: %d', $user_id_int );
continue;
}
// Reset enforcement start date.
update_user_meta( $user_id_int, Two_Factor_Extended_Enforcement::META_ENFORCEMENT_START, time() );
// Clear grace period notified flag.
delete_user_meta( $user_id_int, Two_Factor_Extended_Enforcement::META_GRACE_NOTIFIED );
// Log the action.
$audit_log = two_factor_extended()->get_audit_log();
if ( null !== $audit_log ) {
$audit_log->log_event(
'api_reset_grace',
sprintf( 'REST API: Reset grace period for user %s', $user->user_login ),
$user_id_int
);
}
++$processed;
}
return new WP_REST_Response(
array(
'success' => true,
'processed' => $processed,
'errors' => $errors,
),
200
);
}
/**
* Get compliance report.
*
* @since 0.1.0
*
* @param WP_REST_Request $request Request object.
*
* @return WP_REST_Response Response object.
*/
public function get_report( WP_REST_Request $request ): WP_REST_Response {
$role = $request->get_param( 'role' );
$format = $request->get_param( 'format' );
$role_string = is_string( $role ) ? $role : '';
$compliance = two_factor_extended()->get_compliance_report();
if ( null === $compliance ) {
return new WP_REST_Response(
array(
'success' => false,
'message' => __( 'Compliance report module is not available.', 'two-factor-extended' ),
),
500
);
}
if ( 'csv' === $format ) {
$csv = $compliance->export_to_csv( array( 'role' => $role_string ) );
return new WP_REST_Response(
array(
'success' => true,
'data' => $csv,
'format' => 'csv',
),
200,
array(
'Content-Type' => 'text/csv',
)
);
}
// JSON format.
$non_compliant = $compliance->get_non_compliant_users( array( 'role' => $role_string ) );
$stats = $compliance->get_compliance_stats( array( 'role' => $role_string ) );
return new WP_REST_Response(
array(
'success' => true,
'statistics' => $stats,
'non_compliant' => $non_compliant,
),
200
);
}
}