Please review the account details below and confirm that this activity was expected. If you do not recognize it, follow your site’s incident-response process.
', 'aegis-user-guard' ),
);
}
/**
* Handle the settings callback.
*/
public static function settings() {
if ( null === self::$settings ) {
self::$settings = wp_parse_args( (array) get_option( self::OPTION, array() ), self::defaults() );
}
return self::$settings;
}
/**
* Handle the register_admin_menu callback.
*/
public static function register_admin_menu() {
add_menu_page( __( 'Aegis User Guard', 'aegis-user-guard' ), __( 'Aegis User Guard', 'aegis-user-guard' ), 'manage_options', self::PAGE_SLUG, array( __CLASS__, 'render_settings_page' ), 'dashicons-shield', 80 );
}
/**
* Handle the add_settings_link callback.
*
* @param mixed $links Parameter passed to the callback.
*/
public static function add_settings_link( $links ) {
if ( ! current_user_can( 'manage_options' ) ) {
return $links;
}
$links['settings'] = sprintf( '%s', esc_url( admin_url( 'admin.php?page=' . self::PAGE_SLUG ) ), esc_html__( 'Settings', 'aegis-user-guard' ) );
return $links;
}
/**
* Handle the register_settings callback.
*/
public static function register_settings() {
register_setting(
self::OPTION_GROUP,
self::OPTION,
array(
'type' => 'array',
'sanitize_callback' => array( __CLASS__, 'sanitize_settings' ),
'default' => self::defaults(),
)
);
}
/**
* Sanitize and normalize submitted plugin settings.
*
* @param mixed $input Submitted settings.
* @return array
*/
public static function sanitize_settings( $input ) {
$input = is_array( $input ) ? $input : array();
$old_settings = wp_parse_args( (array) get_option( self::OPTION, array() ), self::defaults() );
$settings = self::defaults();
$settings['password_enabled'] = empty( $input['password_enabled'] ) ? 0 : 1;
$settings['inactivity_enabled'] = empty( $input['inactivity_enabled'] ) ? 0 : 1;
$settings['throttle_enabled'] = empty( $input['throttle_enabled'] ) ? 0 : 1;
$settings['twofa_required'] = empty( $input['twofa_required'] ) ? 0 : 1;
$settings['complexity_enabled'] = empty( $input['complexity_enabled'] ) ? 0 : 1;
$settings['complexity_min_length'] = max( 6, min( 64, absint( $input['complexity_min_length'] ?? 10 ) ) );
$settings['complexity_require_mixed_case'] = empty( $input['complexity_require_mixed_case'] ) ? 0 : 1;
$settings['complexity_require_number'] = empty( $input['complexity_require_number'] ) ? 0 : 1;
$settings['complexity_require_symbol'] = empty( $input['complexity_require_symbol'] ) ? 0 : 1;
$settings['login_alert_enabled'] = empty( $input['login_alert_enabled'] ) ? 0 : 1;
$settings['rest_restrict_users'] = empty( $input['rest_restrict_users'] ) ? 0 : 1;
$settings['notify_enabled'] = empty( $input['notify_enabled'] ) ? 0 : 1;
$settings['notify_user_created'] = empty( $input['notify_user_created'] ) ? 0 : 1;
$settings['notify_user_login'] = empty( $input['notify_user_login'] ) ? 0 : 1;
$settings['notify_username_changed'] = empty( $input['notify_username_changed'] ) ? 0 : 1;
$settings['notify_email_changed'] = empty( $input['notify_email_changed'] ) ? 0 : 1;
$settings['notify_password_changed'] = empty( $input['notify_password_changed'] ) ? 0 : 1;
$settings['notify_recipients'] = ( isset( $input['notify_recipients'] ) && 'selected' === sanitize_key( (string) $input['notify_recipients'] ) ) ? 'selected' : 'all';
$settings['notify_recipient_ids'] = array();
if ( 'selected' === $settings['notify_recipients'] ) {
$recipient_ids = ! empty( $input['notify_recipient_ids'] ) && is_array( $input['notify_recipient_ids'] ) ? array_filter(
array_map( 'absint', $input['notify_recipient_ids'] ),
static function ( $id ) {
return $id > 0;
}
) : array();
$administrator_ids = get_users(
array(
'role' => 'administrator',
'fields' => 'ID',
)
);
$administrator_ids = array_map( 'absint', is_array( $administrator_ids ) ? $administrator_ids : array() );
$settings['notify_recipient_ids'] = array_values( array_intersect( array_unique( $recipient_ids ), $administrator_ids ) );
if ( empty( $settings['notify_recipient_ids'] ) ) {
$settings['notify_recipients'] = 'all';
}
}
$settings['password_days'] = max( 1, min( 3650, absint( $input['password_days'] ?? 180 ) ) );
$settings['inactivity_days'] = max( 1, min( 3650, absint( $input['inactivity_days'] ?? 90 ) ) );
$settings['throttle_max_attempts'] = max( 3, min( 20, absint( $input['throttle_max_attempts'] ?? 5 ) ) );
$settings['throttle_lockout_minutes'] = max( 1, min( 1440, absint( $input['throttle_lockout_minutes'] ?? 15 ) ) );
$settings['email_subject'] = sanitize_text_field( $input['email_subject'] ?? self::defaults()['email_subject'] );
$settings['email_body'] = wp_kses_post( $input['email_body'] ?? self::defaults()['email_body'] );
self::$settings = $settings;
self::log_activity( 'policy_updated', 0, self::diff_settings( $old_settings, $settings ) );
return $settings;
}
/**
* Handle the diff_settings callback.
*
* @param mixed $old Parameter passed to the callback.
* @param mixed $new_settings New plugin settings.
*/
private static function diff_settings( $old, $new_settings ) {
$fields = array(
'password_enabled' => array( __( 'Password freshness rule', 'aegis-user-guard' ), 'bool' ),
'password_days' => array( __( 'Password freshness age', 'aegis-user-guard' ), 'days' ),
'inactivity_enabled' => array( __( 'Inactivity lockout rule', 'aegis-user-guard' ), 'bool' ),
'inactivity_days' => array( __( 'Inactivity lockout age', 'aegis-user-guard' ), 'days' ),
'throttle_enabled' => array( __( 'Brute-force lockout rule', 'aegis-user-guard' ), 'bool' ),
'throttle_max_attempts' => array( __( 'Brute-force attempt limit', 'aegis-user-guard' ), 'text' ),
'throttle_lockout_minutes' => array( __( 'Brute-force lockout length', 'aegis-user-guard' ), 'minutes' ),
'twofa_required' => array( __( 'Two-factor requirement', 'aegis-user-guard' ), 'bool' ),
'complexity_enabled' => array( __( 'Password complexity rule', 'aegis-user-guard' ), 'bool' ),
'complexity_min_length' => array( __( 'Password minimum length', 'aegis-user-guard' ), 'text' ),
'complexity_require_mixed_case' => array( __( 'Require mixed case', 'aegis-user-guard' ), 'bool' ),
'complexity_require_number' => array( __( 'Require a number', 'aegis-user-guard' ), 'bool' ),
'complexity_require_symbol' => array( __( 'Require a symbol', 'aegis-user-guard' ), 'bool' ),
'login_alert_enabled' => array( __( 'New-device sign-in alerts', 'aegis-user-guard' ), 'bool' ),
'rest_restrict_users' => array( __( 'REST user list restriction', 'aegis-user-guard' ), 'bool' ),
'notify_enabled' => array( __( 'Administrator alerts', 'aegis-user-guard' ), 'bool' ),
'notify_user_created' => array( __( 'New account alerts', 'aegis-user-guard' ), 'bool' ),
'notify_user_login' => array( __( 'Sign-in alerts', 'aegis-user-guard' ), 'bool' ),
'notify_username_changed' => array( __( 'Username change alerts', 'aegis-user-guard' ), 'bool' ),
'notify_email_changed' => array( __( 'Email change alerts', 'aegis-user-guard' ), 'bool' ),
'notify_password_changed' => array( __( 'Password change alerts', 'aegis-user-guard' ), 'bool' ),
'notify_recipients' => array( __( 'Alert recipients mode', 'aegis-user-guard' ), 'text' ),
'notify_recipient_ids' => array( __( 'Selected alert recipients', 'aegis-user-guard' ), 'count' ),
'email_subject' => array( __( 'Email subject', 'aegis-user-guard' ), 'changed' ),
'email_body' => array( __( 'Email message', 'aegis-user-guard' ), 'changed' ),
);
$changes = array();
foreach ( $fields as $key => $meta ) {
list( $label, $type ) = $meta;
$old_value = $old[ $key ] ?? null;
$new_value = $new_settings[ $key ] ?? null;
if ( 'count' === $type ) {
$old_value = is_array( $old_value ) ? $old_value : array();
$new_value = is_array( $new_value ) ? $new_value : array();
if ( $old_value === $new_value ) {
continue;
}
$changes[ $key ] = sprintf( '%s: %d → %d', $label, count( $old_value ), count( $new_value ) );
continue;
}
if ( $old_value === $new_value ) {
continue;
}
switch ( $type ) {
case 'bool':
$changes[ $key ] = sprintf( '%s: %s → %s', $label, $old_value ? __( 'on', 'aegis-user-guard' ) : __( 'off', 'aegis-user-guard' ), $new_value ? __( 'on', 'aegis-user-guard' ) : __( 'off', 'aegis-user-guard' ) );
break;
case 'days':
$changes[ $key ] = sprintf( '%s: %d → %d %s', $label, $old_value, $new_value, __( 'days', 'aegis-user-guard' ) );
break;
case 'minutes':
$changes[ $key ] = sprintf( '%s: %d → %d %s', $label, $old_value, $new_value, __( 'minutes', 'aegis-user-guard' ) );
break;
case 'changed':
$changes[ $key ] = sprintf( '%s %s', $label, __( 'updated', 'aegis-user-guard' ) );
break;
default:
$changes[ $key ] = sprintf( '%s: %s → %s', $label, $old_value, $new_value );
}
}
return $changes;
}
/**
* Handle the ajax_save_settings callback.
*/
public static function ajax_save_settings() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => __( 'You are not allowed to update Aegis settings.', 'aegis-user-guard' ) ), 403 );
}
check_ajax_referer( 'aegis_user_guard_save', 'nonce' );
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitize_settings() validates and sanitizes each setting according to its type.
$input = isset( $_POST[ self::OPTION ] ) && is_array( $_POST[ self::OPTION ] ) ? wp_unslash( $_POST[ self::OPTION ] ) : array();
$settings = self::sanitize_settings( $input );
update_option( self::OPTION, $settings );
wp_send_json_success(
array(
'message' => __( 'Aegis settings saved.', 'aegis-user-guard' ),
'version' => self::VERSION,
)
);
}
/**
* Handle the ajax_get_administrators callback.
*/
public static function ajax_get_administrators() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => __( 'You are not allowed to view Administrator accounts.', 'aegis-user-guard' ) ), 403 );
}
check_ajax_referer( 'aegis_user_guard_directory', 'nonce' );
$page = isset( $_POST['page'] ) ? max( 1, absint( $_POST['page'] ) ) : 1;
$per_page = isset( $_POST['per_page'] ) ? absint( $_POST['per_page'] ) : 10;
$per_page = in_array( $per_page, array( 5, 10, 25, 50 ), true ) ? $per_page : 10;
$search = isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : '';
$query_args = array(
'role' => 'administrator',
'number' => $per_page,
'offset' => ( $page - 1 ) * $per_page,
'orderby' => 'display_name',
'order' => 'ASC',
'count_total' => true,
);
if ( '' !== $search ) {
$query_args['search'] = '*' . $search . '*';
$query_args['search_columns'] = array( 'user_login', 'user_email', 'display_name' );
}
$user_query = new WP_User_Query( $query_args );
$total = (int) $user_query->get_total();
$total_pages = max( 1, (int) ceil( $total / $per_page ) );
$page = min( $page, $total_pages );
$expected_offset = ( $page - 1 ) * $per_page;
if ( $expected_offset !== (int) $query_args['offset'] ) {
$query_args['offset'] = $expected_offset;
$user_query = new WP_User_Query( $query_args );
}
$rows = array();
foreach ( (array) $user_query->get_results() as $administrator ) {
$rows[] = self::administrator_directory_row( $administrator );
}
wp_send_json_success(
array(
'rows' => implode( '', $rows ),
'total' => $total,
'page' => $page,
'per_page' => $per_page,
'total_pages' => $total_pages,
/* translators: %1$s is the first result, %2$s is the last result, and %3$s is the total. */
'count_text' => sprintf( __( 'Showing %1$s–%2$s of %3$s', 'aegis-user-guard' ), $total ? ( ( $page - 1 ) * $per_page ) + 1 : 0, min( $page * $per_page, $total ), $total ),
)
);
}
/**
* Handle the get_pending_administrators callback.
*/
private static function get_pending_administrators() {
return get_users(
array(
'role' => 'administrator',
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- The pending-account policy intentionally queries user metadata.
'meta_query' => array(
array(
'key' => self::META_PENDING_ADMIN,
'value' => 1,
'compare' => '=',
),
),
'orderby' => 'registered',
'order' => 'DESC',
)
);
}
/**
* Handle the is_pending_admin_review callback.
*
* @param mixed $user_id Parameter passed to the callback.
*/
private static function is_pending_admin_review( $user_id ) {
return self::user_is_administrator( $user_id ) && (bool) get_user_meta( $user_id, self::META_PENDING_ADMIN, true );
}
/**
* Handle the set_pending_admin_review callback.
*
* @param mixed $user_id Parameter passed to the callback.
* @param mixed $pending Parameter passed to the callback.
*/
private static function set_pending_admin_review( $user_id, $pending = true ) {
if ( $pending ) {
update_user_meta( $user_id, self::META_PENDING_ADMIN, 1 );
self::set_account_disabled( $user_id, true );
return;
}
delete_user_meta( $user_id, self::META_PENDING_ADMIN );
self::set_account_disabled( $user_id, false );
}
/**
* Handle the ajax_get_pending_admin callback.
*/
public static function ajax_get_pending_admin() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => __( 'You are not allowed to review Administrator accounts.', 'aegis-user-guard' ) ), 403 );
}
check_ajax_referer( 'aegis_user_guard_pending', 'nonce' );
$user_id = isset( $_POST['user_id'] ) ? absint( $_POST['user_id'] ) : 0;
$user = $user_id ? get_userdata( $user_id ) : false;
if ( ! $user || ! self::is_pending_admin_review( $user_id ) ) {
wp_send_json_error( array( 'message' => __( 'This Administrator is no longer awaiting review.', 'aegis-user-guard' ) ), 404 );
}
$registered = $user->user_registered ? strtotime( $user->user_registered ) : 0;
wp_send_json_success(
array(
'id' => $user->ID,
'name' => $user->display_name ? $user->display_name : $user->user_login,
'username' => $user->user_login,
'email' => $user->user_email,
'registered' => $registered ? wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $registered ) : __( 'Not recorded', 'aegis-user-guard' ),
'role' => __( 'Administrator', 'aegis-user-guard' ),
'status' => __( 'Pending review', 'aegis-user-guard' ),
'login_status' => __( 'Blocked until access is granted', 'aegis-user-guard' ),
'profile_url' => admin_url( 'user-edit.php?user_id=' . $user->ID ),
)
);
}
/**
* Handle the ajax_grant_admin_access callback.
*/
public static function ajax_grant_admin_access() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => __( 'You are not allowed to grant Administrator access.', 'aegis-user-guard' ) ), 403 );
}
check_ajax_referer( 'aegis_user_guard_pending', 'nonce' );
$user_id = isset( $_POST['user_id'] ) ? absint( $_POST['user_id'] ) : 0;
$user = $user_id ? get_userdata( $user_id ) : false;
if ( ! $user || ! self::is_pending_admin_review( $user_id ) ) {
wp_send_json_error( array( 'message' => __( 'This Administrator is no longer awaiting review.', 'aegis-user-guard' ) ), 404 );
}
if ( get_current_user_id() === $user_id ) {
wp_send_json_error( array( 'message' => __( 'You cannot grant access to your own pending account.', 'aegis-user-guard' ) ), 400 );
}
delete_user_meta( $user_id, self::META_PENDING_ADMIN );
delete_user_meta( $user_id, self::META_DISABLED );
self::log_activity(
'administrator_access_granted',
$user_id,
array(
'source' => 'pending_review',
'status' => 'enabled',
'login' => 'available',
)
);
wp_send_json_success(
array(
'id' => $user_id,
/* translators: %s is the display name or login of the administrator. */
'message' => sprintf( __( 'Access granted to %s.', 'aegis-user-guard' ), $user->display_name ? $user->display_name : $user->user_login ),
'pending' => count( self::get_pending_administrators() ),
)
);
}
/**
* Record a security event in the activity log.
*
* @param mixed $event Event identifier.
* @param mixed $user_id Related user ID.
* @param mixed $context Additional event data.
*/
private static function log_activity( $event, $user_id = 0, $context = array() ) {
$log = get_option( self::OPTION_ACTIVITY, array() );
$log = is_array( $log ) ? $log : array();
$actor_id = is_user_logged_in() ? get_current_user_id() : 0;
$entry = array(
'id' => wp_generate_uuid4(),
'time' => time(),
'event' => sanitize_key( $event ),
'user_id' => absint( $user_id ),
'actor_id' => absint( $actor_id ),
'context' => is_array( $context ) ? array_map( 'sanitize_text_field', $context ) : array(),
);
array_unshift( $log, $entry );
update_option( self::OPTION_ACTIVITY, array_slice( $log, 0, self::MAX_ACTIVITY ), false );
}
/**
* Handle the get_activity_log callback.
*/
private static function get_activity_log() {
$log = get_option( self::OPTION_ACTIVITY, array() );
return is_array( $log ) ? array_slice( $log, 0, 50 ) : array();
}
/**
* Get presentation metadata for an activity event.
*
* @param mixed $event Event identifier.
*/
private static function activity_event_meta( $event ) {
$events = array(
'administrator_access_granted' => array(
'label' => __( 'Administrator access granted', 'aegis-user-guard' ),
'tone' => 'success',
'priority' => 'high',
),
'login_success' => array(
'label' => __( 'Successful sign-in', 'aegis-user-guard' ),
'tone' => 'success',
'priority' => 'low',
),
'login_blocked' => array(
'label' => __( 'Sign-in blocked', 'aegis-user-guard' ),
'tone' => 'critical',
'priority' => 'critical',
),
'password_changed' => array(
'label' => __( 'Password changed', 'aegis-user-guard' ),
'tone' => 'success',
'priority' => 'medium',
),
'user_created' => array(
'label' => __( 'User created', 'aegis-user-guard' ),
'tone' => 'info',
'priority' => 'medium',
),
'administrator_created' => array(
'label' => __( 'Administrator created', 'aegis-user-guard' ),
'tone' => 'warning',
'priority' => 'high',
),
'administrator_promoted' => array(
'label' => __( 'Administrator role granted', 'aegis-user-guard' ),
'tone' => 'warning',
'priority' => 'high',
),
'access_paused' => array(
'label' => __( 'Account access paused', 'aegis-user-guard' ),
'tone' => 'warning',
'priority' => 'high',
),
'access_reactivated' => array(
'label' => __( 'Account access reactivated', 'aegis-user-guard' ),
'tone' => 'success',
'priority' => 'medium',
),
'notification_sent' => array(
'label' => __( 'Administrator email sent', 'aegis-user-guard' ),
'tone' => 'info',
'priority' => 'low',
),
'policy_updated' => array(
'label' => __( 'Security policy updated', 'aegis-user-guard' ),
'tone' => 'info',
'priority' => 'medium',
),
'2fa_enabled' => array(
'label' => __( 'Two-factor authentication enabled', 'aegis-user-guard' ),
'tone' => 'success',
'priority' => 'high',
),
'2fa_disabled' => array(
'label' => __( 'Two-factor authentication disabled', 'aegis-user-guard' ),
'tone' => 'danger',
'priority' => 'critical',
),
'2fa_backup_code_used' => array(
'label' => __( 'Two-factor backup code used', 'aegis-user-guard' ),
'tone' => 'warning',
'priority' => 'high',
),
'2fa_backup_codes_regenerated' => array(
'label' => __( 'Two-factor backup codes regenerated', 'aegis-user-guard' ),
'tone' => 'info',
'priority' => 'medium',
),
'new_device_login' => array(
'label' => __( 'Sign-in from a new device', 'aegis-user-guard' ),
'tone' => 'warning',
'priority' => 'high',
),
'password_reset_forced' => array(
'label' => __( 'Password reset forced by administrator', 'aegis-user-guard' ),
'tone' => 'danger',
'priority' => 'critical',
),
'sessions_revoked' => array(
'label' => __( 'Sessions signed out', 'aegis-user-guard' ),
'tone' => 'warning',
'priority' => 'high',
),
'ip_rule_added' => array(
'label' => __( 'IP rule added', 'aegis-user-guard' ),
'tone' => 'warning',
'priority' => 'high',
),
'ip_rule_removed' => array(
'label' => __( 'IP rule removed', 'aegis-user-guard' ),
'tone' => 'info',
'priority' => 'medium',
),
'activity_exported' => array(
'label' => __( 'Activity log exported', 'aegis-user-guard' ),
'tone' => 'info',
'priority' => 'low',
),
'administrators_exported' => array(
'label' => __( 'Administrator directory exported', 'aegis-user-guard' ),
'tone' => 'info',
'priority' => 'low',
),
'login_failed' => array(
'label' => __( 'Sign-in attempt failed', 'aegis-user-guard' ),
'tone' => 'danger',
'priority' => 'high',
),
);
return $events[ $event ] ?? array(
'label' => __( 'Security event', 'aegis-user-guard' ),
'tone' => 'neutral',
'priority' => 'low',
);
}
/**
* Handle the activity_label callback.
*
* @param mixed $event Parameter passed to the callback.
*/
private static function activity_label( $event ) {
$meta = self::activity_event_meta( $event );
return $meta['label'];
}
/**
* Format an activity context for the admin table and CSV export.
*
* @param mixed $event Event identifier.
* @param mixed $context Event context values.
*/
private static function activity_context( $event, $context ) {
$context = is_array( $context ) ? $context : array();
$summary = '';
$detail = '';
switch ( $event ) {
case 'administrator_access_granted':
$summary = __( 'Access approved from the pending Administrator review queue.', 'aegis-user-guard' );
$detail = __( 'The account can sign in now.', 'aegis-user-guard' );
break;
case 'login_success':
$summary = __( 'Authentication completed successfully.', 'aegis-user-guard' );
break;
case 'login_blocked':
$reasons = array(
'ip_lockout_active' => __( 'Sign-in blocked because this network is temporarily locked.', 'aegis-user-guard' ),
'account_lockout_active' => __( 'Sign-in blocked because this account is temporarily locked.', 'aegis-user-guard' ),
'pending_administrator_review' => __( 'Sign-in blocked while Administrator access awaits review.', 'aegis-user-guard' ),
'manual_pause' => __( 'Sign-in blocked because account access is paused.', 'aegis-user-guard' ),
'inactivity_lockout' => __( 'Sign-in blocked because the account was inactive for too long.', 'aegis-user-guard' ),
'ip_lockout_triggered' => __( 'Sign-in blocked after repeated failures from this IP address.', 'aegis-user-guard' ),
'account_lockout_triggered' => __( 'Sign-in blocked after repeated failures for this account.', 'aegis-user-guard' ),
'invalid_2fa_code' => __( 'Sign-in blocked because the two-factor code was invalid.', 'aegis-user-guard' ),
'ip_blocklist' => __( 'Sign-in blocked because this IP address is on the block list.', 'aegis-user-guard' ),
);
$summary = $reasons[ $context['reason'] ?? '' ] ?? __( 'Sign-in was blocked by a security rule.', 'aegis-user-guard' );
if ( ! empty( $context['ip'] ) ) {
/* translators: %s is an IP address. */
$detail = sprintf( __( 'Source IP: %s.', 'aegis-user-guard' ), sanitize_text_field( $context['ip'] ) );
}
break;
case 'password_changed':
$summary = __( 'The account password was changed successfully.', 'aegis-user-guard' );
break;
case 'user_created':
$summary = __( 'A new WordPress user account was created.', 'aegis-user-guard' );
break;
case 'administrator_created':
case 'administrator_promoted':
$summary = __( 'Administrator access is awaiting approval.', 'aegis-user-guard' );
$detail = __( 'Sign-in is blocked until another Administrator grants access.', 'aegis-user-guard' );
break;
case 'access_paused':
$summary = __( 'Account access was paused from the Administrator directory.', 'aegis-user-guard' );
$detail = __( 'All active sessions were terminated.', 'aegis-user-guard' );
break;
case 'access_reactivated':
$summary = __( 'Account access was restored from the Administrator directory.', 'aegis-user-guard' );
break;
case 'notification_sent':
$event_label = sanitize_text_field( $context['event'] ?? __( 'Security event', 'aegis-user-guard' ) );
$recipient_count = absint( $context['recipients'] ?? 0 );
$recipient_mode = 'selected' === ( $context['recipient_mode'] ?? '' ) ? __( 'selected administrators', 'aegis-user-guard' ) : __( 'all administrators', 'aegis-user-guard' );
$recipient_label = 1 === $recipient_count ? __( 'recipient', 'aegis-user-guard' ) : __( 'recipients', 'aegis-user-guard' );
$delivery_status = 'accepted' === ( $context['delivery'] ?? '' ) ? __( 'delivery accepted', 'aegis-user-guard' ) : __( 'delivery failed', 'aegis-user-guard' );
/* translators: %s is the notification event label. */
$summary = sprintf( __( 'Notification sent for: %s.', 'aegis-user-guard' ), $event_label );
/* translators: %1$d is the number of recipients, %2$s is singular or plural recipient text, %3$s is the recipient group, and %4$s is the delivery status. */
$detail = sprintf( __( '%1$d %2$s · %3$s · %4$s.', 'aegis-user-guard' ), $recipient_count, $recipient_label, $recipient_mode, $delivery_status );
break;
case 'policy_updated':
$changes = array();
foreach ( $context as $change ) {
if ( '' !== (string) $change ) {
$changes[] = sanitize_text_field( $change );
}
}
$summary = __( 'Security policy settings were updated.', 'aegis-user-guard' );
$detail = ! empty( $changes ) ? implode( ' · ', $changes ) : __( 'No individual setting changes were recorded.', 'aegis-user-guard' );
break;
case '2fa_enabled':
$summary = __( 'Two-factor authentication was enabled for this account.', 'aegis-user-guard' );
break;
case '2fa_disabled':
$summary = __( 'Two-factor authentication was disabled for this account.', 'aegis-user-guard' );
break;
case '2fa_backup_code_used':
$summary = __( 'A backup code was used to complete two-factor authentication.', 'aegis-user-guard' );
break;
case '2fa_backup_codes_regenerated':
$summary = __( 'A new set of two-factor backup codes was generated.', 'aegis-user-guard' );
break;
case 'new_device_login':
$summary = __( 'A sign-in came from a device or IP address not seen before.', 'aegis-user-guard' );
if ( ! empty( $context['ip'] ) ) {
/* translators: %s is an IP address. */
$detail = sprintf( __( 'New source IP: %s.', 'aegis-user-guard' ), sanitize_text_field( $context['ip'] ) );
}
break;
case 'password_reset_forced':
$summary = __( 'An administrator required this account to set a new password.', 'aegis-user-guard' );
$detail = __( 'All active sessions were terminated.', 'aegis-user-guard' );
break;
case 'sessions_revoked':
$summary = 'self' === ( $context['source'] ?? '' ) ? __( 'The user signed out all active sessions from their own account.', 'aegis-user-guard' ) : __( 'An administrator signed out all active sessions from the Administrator directory.', 'aegis-user-guard' );
break;
case 'ip_rule_added':
$mode = 'allow' === ( $context['mode'] ?? '' ) ? __( 'allow', 'aegis-user-guard' ) : __( 'block', 'aegis-user-guard' );
$summary = __( 'A network access rule was added.', 'aegis-user-guard' );
/* translators: %1$s is the access mode, %2$s is an IP address. */
$detail = sprintf( __( 'This rule will %1$s IP address %2$s.', 'aegis-user-guard' ), $mode, sanitize_text_field( $context['ip'] ?? __( 'the recorded address', 'aegis-user-guard' ) ) );
break;
case 'ip_rule_removed':
$summary = __( 'A network access rule was removed.', 'aegis-user-guard' );
if ( ! empty( $context['ip'] ) ) {
/* translators: %s is an IP address. */
$detail = sprintf( __( 'Removed rule for IP address: %s.', 'aegis-user-guard' ), sanitize_text_field( $context['ip'] ) );
}
break;
case 'activity_exported':
$row_count = absint( $context['rows'] ?? 0 );
$row_label = 1 === $row_count ? __( 'activity record', 'aegis-user-guard' ) : __( 'activity records', 'aegis-user-guard' );
/* translators: %1$d is the number of records, and %2$s is singular or plural activity record text. */
$summary = sprintf( __( '%1$d %2$s were exported as a CSV file.', 'aegis-user-guard' ), $row_count, $row_label );
break;
case 'administrators_exported':
$row_count = absint( $context['rows'] ?? 0 );
$row_label = 1 === $row_count ? __( 'administrator record', 'aegis-user-guard' ) : __( 'administrator records', 'aegis-user-guard' );
/* translators: %1$d is the number of records, and %2$s is singular or plural administrator record text. */
$summary = sprintf( __( '%1$d %2$s were exported as a CSV file.', 'aegis-user-guard' ), $row_count, $row_label );
break;
case 'login_failed':
$summary = __( 'A sign-in attempt failed before authentication completed.', 'aegis-user-guard' );
if ( ! empty( $context['ip'] ) ) {
/* translators: %s is an IP address. */
$detail = sprintf( __( 'Source IP: %s.', 'aegis-user-guard' ), sanitize_text_field( $context['ip'] ) );
}
if ( ! empty( $context['username'] ) ) {
/* translators: %s is a submitted username. */
$detail = sprintf( __( 'Submitted username: %s.', 'aegis-user-guard' ), sanitize_text_field( $context['username'] ) );
}
break;
}
if ( '' === $summary ) {
$summary = __( 'A security event was recorded.', 'aegis-user-guard' );
}
return array(
'summary' => $summary,
'detail' => $detail,
);
}
/**
* Convert an activity context into a plain-text string.
*
* @param mixed $event Event identifier.
* @param mixed $context Event context values.
*/
private static function activity_context_text( $event, $context ) {
$parts = self::activity_context( $event, $context );
return implode( ' · ', array_filter( $parts ) );
}
/**
* Handle the clear_activity_log callback.
*/
public static function clear_activity_log() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You are not allowed to clear the activity log.', 'aegis-user-guard' ), 403 );
}
check_admin_referer( self::NONCE );
update_option( self::OPTION_ACTIVITY, array(), false );
wp_safe_redirect( add_query_arg( 'aegis_notice', 'activity_cleared', admin_url( 'admin.php?page=' . self::PAGE_SLUG ) ) );
exit;
}
/**
* Handle the enqueue_admin_assets callback.
*
* @param mixed $hook Parameter passed to the callback.
*/
public static function enqueue_admin_assets( $hook ) {
if ( ! in_array( $hook, array( 'toplevel_page_' . self::PAGE_SLUG, 'users.php', 'profile.php' ), true ) ) {
return;
}
$asset_url = plugin_dir_url( AEGIS_USER_GUARD_FILE ) . 'admin/';
if ( 'toplevel_page_' . self::PAGE_SLUG === $hook ) {
wp_enqueue_editor();
}
wp_enqueue_style( 'aegis-user-guard-admin', $asset_url . 'css/admin.css', array(), self::VERSION );
wp_enqueue_script( 'aegis-user-guard-admin', $asset_url . 'js/admin.js', array(), self::VERSION, true );
wp_localize_script(
'aegis-user-guard-admin',
'AegisUserGuard',
array(
'confirmToggle' => __( 'Change this user’s account access?', 'aegis-user-guard' ),
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'aegis_user_guard_save' ),
'directoryNonce' => wp_create_nonce( 'aegis_user_guard_directory' ),
'pendingNonce' => wp_create_nonce( 'aegis_user_guard_pending' ),
'twoFaNonce' => wp_create_nonce( 'aegis_user_guard_2fa' ),
'savingLabel' => __( 'Saving changes…', 'aegis-user-guard' ),
'savedLabel' => __( 'Changes saved', 'aegis-user-guard' ),
'errorLabel' => __( 'Unable to save changes. Please try again.', 'aegis-user-guard' ),
'i18n' => self::admin_script_i18n(),
)
);
}
/**
* Handle the admin_script_i18n callback.
*/
private static function admin_script_i18n() {
return array(
'cancel' => __( 'Cancel', 'aegis-user-guard' ),
'working' => __( 'Working…', 'aegis-user-guard' ),
'close' => __( 'Close', 'aegis-user-guard' ),
'openProfile' => __( 'Open profile', 'aegis-user-guard' ),
'administratorAccountDetails' => __( 'Administrator account details', 'aegis-user-guard' ),
'reviewBeforeGrant' => __( 'Review this account before granting sign-in access.', 'aegis-user-guard' ),
'name' => __( 'Name', 'aegis-user-guard' ),
'username' => __( 'Username', 'aegis-user-guard' ),
'email' => __( 'Email', 'aegis-user-guard' ),
'role' => __( 'Role', 'aegis-user-guard' ),
'created' => __( 'Created', 'aegis-user-guard' ),
'loginStatus' => __( 'Login status', 'aegis-user-guard' ),
'grantAccessNote' => __( 'Granting access will remove the pending review hold and allow this Administrator to sign in through the normal WordPress login flow.', 'aegis-user-guard' ),
'forcePasswordResetTitle' => __( 'Force a password reset?', 'aegis-user-guard' ),
'forcePasswordResetMessage' => __( '{{account}} will be required to set a new password before they can use wp-admin again, and their active sessions will be signed out.', 'aegis-user-guard' ),
'forcePasswordResetConfirm' => __( 'Force password reset', 'aegis-user-guard' ),
'signOutEverywhereTitle' => __( 'Sign out everywhere?', 'aegis-user-guard' ),
'signOutEverywhereMessage' => __( 'All active sessions for {{account}} will be ended immediately. They will need to sign in again.', 'aegis-user-guard' ),
'signOutEverywhereConfirm' => __( 'Sign out everywhere', 'aegis-user-guard' ),
'removeIpRuleTitle' => __( 'Remove IP rule?', 'aegis-user-guard' ),
'removeIpRuleMessage' => __( 'The rule for {{rule}} will be removed.', 'aegis-user-guard' ),
'removeRule' => __( 'Remove rule', 'aegis-user-guard' ),
'disableAccountTitle' => __( 'Disable account access?', 'aegis-user-guard' ),
'enableAccountTitle' => __( 'Enable account access?', 'aegis-user-guard' ),
'disableAccountMessage' => __( 'Access for {{account}} will be paused at login. You can restore it later.', 'aegis-user-guard' ),
'enableAccountMessage' => __( 'Access for {{account}} will be restored for future sign-ins.', 'aegis-user-guard' ),
'disableAccess' => __( 'Disable access', 'aegis-user-guard' ),
'enableAccess' => __( 'Enable access', 'aegis-user-guard' ),
'clearActivityTitle' => __( 'Clear activity log?', 'aegis-user-guard' ),
'clearActivityMessage' => __( 'This permanently removes the recorded Aegis security events. This action cannot be undone.', 'aegis-user-guard' ),
'clearLog' => __( 'Clear log', 'aegis-user-guard' ),
'disableTwoFactorTitle' => __( 'Disable two-factor authentication?', 'aegis-user-guard' ),
'disableTwoFactorMessage' => __( 'Your account will only require a password to sign in. Your saved backup codes will be deleted.', 'aegis-user-guard' ),
'disableTwoFactor' => __( 'Disable two-factor', 'aegis-user-guard' ),
'thisAccount' => __( 'this account', 'aegis-user-guard' ),
'thisRule' => __( 'this rule', 'aegis-user-guard' ),
/* translators: %d is the number of accounts. */
'accountOne' => __( '%d account', 'aegis-user-guard' ),
/* translators: %d is the number of accounts. */
'accountMany' => __( '%d accounts', 'aegis-user-guard' ),
'unableCompleteReview' => __( 'Unable to complete this Administrator review action.', 'aegis-user-guard' ),
'loading' => __( 'Loading…', 'aegis-user-guard' ),
'tryAgain' => __( 'Try again', 'aegis-user-guard' ),
'grantAdministratorAccessTitle' => __( 'Grant Administrator access?', 'aegis-user-guard' ),
'grantAdministratorAccessMessage' => __( 'This will allow {{account}} to sign in through WordPress. Confirm that you reviewed this account and expected the change.', 'aegis-user-guard' ),
'grantAccess' => __( 'Grant access', 'aegis-user-guard' ),
'somethingWentWrong' => __( 'Something went wrong. Please try again.', 'aegis-user-guard' ),
'enterSixDigitCode' => __( 'Enter the 6-digit code from your authenticator app.', 'aegis-user-guard' ),
'customizeEmailTemplateIntro' => __( 'Customize the subject and message used for every enabled Administrator alert event.', 'aegis-user-guard' ),
'chooseEmailRecipientsIntro' => __( 'Choose which administrators receive enabled account security alerts.', 'aegis-user-guard' ),
'restoreDefaultTemplateConfirm' => __( 'Restore the default Administrator email template?', 'aegis-user-guard' ),
/* translators: %d is the number of selected administrators. */
'recipientOne' => __( '%d administrator selected', 'aegis-user-guard' ),
/* translators: %d is the number of selected administrators. */
'recipientMany' => __( '%d administrators selected', 'aegis-user-guard' ),
'selectAdministrator' => __( 'Select at least one administrator', 'aegis-user-guard' ),
'selectAdministratorOrAll' => __( 'Select at least one Administrator, or choose All administrators.', 'aegis-user-guard' ),
'unableLoadAdministrators' => __( 'Unable to load Administrator accounts.', 'aegis-user-guard' ),
'noAdministrators' => __( 'No Administrator accounts found.', 'aegis-user-guard' ),
'showingActivity' => __( 'Showing {{start}}–{{end}} of {{total}}', 'aegis-user-guard' ),
'noMatchingEvents' => __( 'No matching events', 'aegis-user-guard' ),
'previous' => __( 'Previous', 'aegis-user-guard' ),
'next' => __( 'Next', 'aegis-user-guard' ),
);
}
/**
* Handle the protect_authentication callback.
*
* @param mixed $user Parameter passed to the callback.
* @param mixed $username Parameter passed to the callback.
* @param mixed $password Parameter passed to the callback.
*/
public static function protect_authentication( $user, $username, $password ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- WordPress authenticate filter requires the complete callback signature.
if ( $user instanceof WP_Error || ! ( $user instanceof WP_User ) ) {
return $user;
}
$user_id = $user->ID;
$settings = self::settings();
if ( $settings['throttle_enabled'] && self::is_ip_locked_out( self::get_client_ip() ) ) {
self::log_activity( 'login_blocked', $user_id, array( 'reason' => 'ip_lockout_active' ) );
return new WP_Error( 'aegis_ip_locked', __( 'Too many failed sign-in attempts. Access from this network is temporarily locked. Please try again later.', 'aegis-user-guard' ) );
}
if ( $settings['throttle_enabled'] && self::is_user_locked_out( $user_id ) ) {
self::log_activity( 'login_blocked', $user_id, array( 'reason' => 'account_lockout_active' ) );
return new WP_Error( 'aegis_login_locked', __( 'Too many failed sign-in attempts. This account is temporarily locked. Please try again later.', 'aegis-user-guard' ) );
}
if ( self::is_pending_admin_review( $user_id ) ) {
self::log_activity( 'login_blocked', $user_id, array( 'reason' => 'pending_administrator_review' ) );
return new WP_Error( 'aegis_admin_pending_review', __( 'Administrator access pending review. An existing Administrator must grant access before this account can sign in.', 'aegis-user-guard' ) );
}
if ( self::is_account_disabled( $user_id ) ) {
self::log_activity( 'login_blocked', $user_id, array( 'reason' => 'manual_pause' ) );
return new WP_Error( 'aegis_account_disabled', __( 'Account access paused. Please contact an administrator to reactivate your account.', 'aegis-user-guard' ) );
}
if ( $settings['inactivity_enabled'] && self::is_inactive( $user_id, $settings['inactivity_days'] ) ) {
self::set_account_disabled( $user_id, true );
self::log_activity( 'login_blocked', $user_id, array( 'reason' => 'inactivity_lockout' ) );
return new WP_Error( 'aegis_account_inactive', __( 'Account access paused. This account has been locked after an extended period of inactivity. Please contact an administrator.', 'aegis-user-guard' ) );
}
return $user;
}
/**
* Handle the redirect_expired_password_user callback.
*
* @param mixed $redirect_to Parameter passed to the callback.
* @param mixed $requested_redirect_to Parameter passed to the callback.
* @param mixed $user Parameter passed to the callback.
*/
public static function redirect_expired_password_user( $redirect_to, $requested_redirect_to, $user ) {
if ( ! $user instanceof WP_User || ! self::must_reset_password( $user->ID ) ) {
return $redirect_to;
}
$key = get_password_reset_key( $user );
if ( is_wp_error( $key ) ) {
return $redirect_to;
}
return network_site_url( 'wp-login.php?action=rp&key=' . rawurlencode( $key ) . '&login=' . rawurlencode( $user->user_login ), 'login' );
}
/**
* Handle the guard_expired_admin_session callback.
*/
public static function guard_expired_admin_session() {
if ( ! is_user_logged_in() || ! self::must_reset_password( get_current_user_id() ) ) {
return;
}
global $pagenow;
if ( in_array( $pagenow, array( 'profile.php', 'admin-ajax.php', 'async-upload.php' ), true ) || wp_doing_ajax() ) {
return;
}
wp_safe_redirect( admin_url( 'profile.php#aegis-password-expired' ) );
exit;
}
/**
* Handle the render_expired_password_notice callback.
*/
public static function render_expired_password_notice() {
$user_id = get_current_user_id();
if ( ! is_user_logged_in() || ! self::must_reset_password( $user_id ) ) {
return;
}
$forced = (bool) get_user_meta( $user_id, self::META_FORCE_PASSWORD_RESET, true );
printf( '
%s %s
', esc_html__( 'Password update required.', 'aegis-user-guard' ), $forced ? esc_html__( 'An administrator has required a password reset on this account. Update it from this profile screen to restore normal access.', 'aegis-user-guard' ) : esc_html__( 'Your password has reached the configured age limit. Update it from this profile screen to restore normal access.', 'aegis-user-guard' ) );
}
/**
* Handle the record_password_change callback.
*
* @param mixed $user Parameter passed to the callback.
* @param mixed $new_pass Parameter passed to the callback.
*/
public static function record_password_change( $user, $new_pass ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- WordPress password_reset action supplies the new password for compatibility.
if ( $user instanceof WP_User ) {
self::record_password_event( $user->ID );
}
}
/**
* Handle the record_password_set callback.
*
* @param mixed $password Parameter passed to the callback.
* @param mixed $user_id Parameter passed to the callback.
*/
public static function record_password_set( $password, $user_id ) {
if ( $user_id ) {
self::record_password_event( $user_id );
}
}
/**
* Handle the record_password_event callback.
*
* @param mixed $user_id Parameter passed to the callback.
*/
private static function record_password_event( $user_id ) {
$user_id = absint( $user_id );
if ( ! $user_id || isset( self::$password_events_recorded[ $user_id ] ) ) {
return;
}
self::$password_events_recorded[ $user_id ] = true;
update_user_meta( $user_id, self::META_LAST_PASSWORD, time() );
delete_user_meta( $user_id, self::META_FORCE_PASSWORD_RESET );
self::log_activity( 'password_changed', $user_id );
self::notify_administrators( $user_id, 'password_changed' );
}
/**
* Handle the handle_profile_update callback.
*
* @param mixed $user_id Parameter passed to the callback.
* @param mixed $old_user_data Parameter passed to the callback.
* @param mixed $userdata Parameter passed to the callback.
*/
public static function handle_profile_update( $user_id, $old_user_data, $userdata ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- WordPress profile_update action supplies the complete hook payload.
if ( ! $old_user_data instanceof WP_User ) {
return;
}
$user = get_userdata( $user_id );
if ( ! $user instanceof WP_User ) {
return;
}
if ( $old_user_data->user_login !== $user->user_login ) {
self::notify_administrators( $user_id, 'username_changed' );
}
if ( strtolower( $old_user_data->user_email ) !== strtolower( $user->user_email ) ) {
self::notify_administrators( $user_id, 'email_changed' );
}
if ( $old_user_data->user_pass !== $user->user_pass ) {
self::record_password_event( $user_id );
}
}
/**
* Handle the record_successful_login callback.
*
* @param mixed $user_login Parameter passed to the callback.
* @param mixed $user Parameter passed to the callback.
*/
public static function record_successful_login( $user_login, $user ) {
if ( ! $user instanceof WP_User ) {
return;
}
$now = time();
update_user_meta( $user->ID, self::META_LAST_LOGIN, $now );
self::log_activity( 'login_success', $user->ID );
if ( ! get_user_meta( $user->ID, self::META_LAST_PASSWORD, true ) ) {
update_user_meta( $user->ID, self::META_LAST_PASSWORD, $now );
}
delete_user_meta( $user->ID, self::META_LOGIN_ATTEMPTS );
delete_user_meta( $user->ID, self::META_LOGIN_LOCKOUT_UNTIL );
delete_transient( self::TRANSIENT_IP_LOCKOUT . md5( self::get_client_ip() ) );
self::notify_administrators( $user->ID, 'login' );
self::maybe_alert_new_device( $user, self::get_client_ip() );
}
/**
* Handle the record_failed_login callback.
*
* @param mixed $username Parameter passed to the callback.
* @param mixed $error Parameter passed to the callback.
*/
public static function record_failed_login( $username, $error = null ) {
$settings = self::settings();
$is_aegis_block = $error instanceof WP_Error && 0 === strpos( $error->get_error_code(), 'aegis_' );
$ip = self::get_client_ip();
$user = get_user_by( 'login', $username );
if ( ! $is_aegis_block ) {
self::log_activity(
'login_failed',
$user instanceof WP_User ? $user->ID : 0,
array_filter(
array(
'username' => $user instanceof WP_User ? '' : sanitize_text_field( $username ),
'ip' => $ip,
)
)
);
}
if ( ! $settings['throttle_enabled'] || $is_aegis_block ) {
return;
}
$max_attempts = (int) $settings['throttle_max_attempts'];
$lockout_seconds = (int) $settings['throttle_lockout_minutes'] * MINUTE_IN_SECONDS;
if ( '' !== $ip && ! self::is_ip_allowlisted( $ip ) ) {
$ip_key = self::TRANSIENT_IP_LOCKOUT . md5( $ip );
$ip_state = get_transient( $ip_key );
$ip_state = is_array( $ip_state ) ? $ip_state : array( 'count' => 0 );
$ip_state['count'] = (int) $ip_state['count'] + 1;
if ( $ip_state['count'] >= $max_attempts ) {
$ip_state['locked_until'] = time() + $lockout_seconds;
self::log_activity(
'login_blocked',
0,
array(
'reason' => 'ip_lockout_triggered',
'ip' => $ip,
)
);
}
set_transient( $ip_key, $ip_state, $lockout_seconds );
}
if ( $user instanceof WP_User ) {
$attempts = (int) get_user_meta( $user->ID, self::META_LOGIN_ATTEMPTS, true ) + 1;
update_user_meta( $user->ID, self::META_LOGIN_ATTEMPTS, $attempts );
if ( $attempts >= $max_attempts ) {
update_user_meta( $user->ID, self::META_LOGIN_LOCKOUT_UNTIL, time() + $lockout_seconds );
self::log_activity( 'login_blocked', $user->ID, array( 'reason' => 'account_lockout_triggered' ) );
}
}
}
/**
* Handle the is_user_locked_out callback.
*
* @param mixed $user_id Parameter passed to the callback.
*/
private static function is_user_locked_out( $user_id ) {
$locked_until = absint( get_user_meta( $user_id, self::META_LOGIN_LOCKOUT_UNTIL, true ) );
if ( $locked_until && $locked_until <= time() ) {
delete_user_meta( $user_id, self::META_LOGIN_LOCKOUT_UNTIL );
delete_user_meta( $user_id, self::META_LOGIN_ATTEMPTS );
return false;
}
return $locked_until > time();
}
/**
* Handle the is_ip_locked_out callback.
*
* @param mixed $ip Parameter passed to the callback.
*/
private static function is_ip_locked_out( $ip ) {
if ( '' === $ip ) {
return false;
}
$state = get_transient( self::TRANSIENT_IP_LOCKOUT . md5( $ip ) );
return is_array( $state ) && ! empty( $state['locked_until'] ) && $state['locked_until'] > time();
}
/**
* Handle the get_client_ip callback.
*/
private static function get_client_ip() {
$ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';
return apply_filters( 'aegis_client_ip', $ip );
}
/**
* Handle the handle_user_created callback.
*
* @param mixed $user_id Parameter passed to the callback.
*/
public static function handle_user_created( $user_id ) {
if ( ! get_user_meta( $user_id, self::META_LAST_PASSWORD, true ) ) {
update_user_meta( $user_id, self::META_LAST_PASSWORD, time() );
}
$is_administrator = self::user_is_administrator( $user_id );
$creation_context = get_transient( self::TRANSIENT_ADMIN_CREATION_GUARD . $user_id );
if ( $is_administrator ) {
self::set_pending_admin_review( $user_id, true );
if ( 'awaiting_registration_hook' === $creation_context ) {
delete_transient( self::TRANSIENT_ADMIN_CREATION_GUARD . $user_id );
} else {
set_transient( self::TRANSIENT_ADMIN_CREATION_GUARD . $user_id, 'administrator', MINUTE_IN_SECONDS );
}
self::log_activity(
'administrator_created',
$user_id,
array(
'status' => 'pending_review',
'login' => 'blocked',
)
);
self::notify_administrators( $user_id, 'created' );
} else {
set_transient( self::TRANSIENT_ADMIN_CREATION_GUARD . $user_id, 'non_administrator', MINUTE_IN_SECONDS );
self::log_activity( 'user_created', $user_id );
self::notify_administrators( $user_id, 'created' );
}
}
/**
* Handle the handle_role_change callback.
*
* @param mixed $user_id Parameter passed to the callback.
* @param mixed $role Parameter passed to the callback.
* @param mixed $old_roles Parameter passed to the callback.
*/
public static function handle_role_change( $user_id, $role, $old_roles ) {
if ( 'administrator' !== $role || in_array( 'administrator', (array) $old_roles, true ) ) {
return;
}
$creation_context = get_transient( self::TRANSIENT_ADMIN_CREATION_GUARD . $user_id );
if ( 'administrator' === $creation_context ) {
delete_transient( self::TRANSIENT_ADMIN_CREATION_GUARD . $user_id );
return;
}
if ( 'non_administrator' === $creation_context ) {
delete_transient( self::TRANSIENT_ADMIN_CREATION_GUARD . $user_id );
} else {
$user = get_userdata( $user_id );
$registered = $user && $user->user_registered ? strtotime( $user->user_registered ) : 0;
if ( $registered && $registered >= ( time() - 10 ) ) {
self::set_pending_admin_review( $user_id, true );
set_transient( self::TRANSIENT_ADMIN_CREATION_GUARD . $user_id, 'awaiting_registration_hook', MINUTE_IN_SECONDS );
return;
}
}
self::set_pending_admin_review( $user_id, true );
self::log_activity(
'administrator_promoted',
$user_id,
array(
'status' => 'pending_review',
'login' => 'blocked',
)
);
self::notify_administrators( $user_id, 'promoted' );
}
/**
* Handle the render_notification_email callback.
*
* @param mixed $site_name Parameter passed to the callback.
* @param mixed $action_label Parameter passed to the callback.
* @param mixed $user Parameter passed to the callback.
* @param mixed $actor_name Parameter passed to the callback.
* @param mixed $date Parameter passed to the callback.
* @param mixed $user_url Parameter passed to the callback.
* @param mixed $custom_message Parameter passed to the callback.
*/
private static function render_notification_email( $site_name, $action_label, $user, $actor_name, $date, $user_url, $custom_message ) {
$site_name = esc_html( $site_name );
$action_label = esc_html( $action_label );
$username = esc_html( $user->user_login );
$email = esc_html( $user->user_email );
$actor_name = esc_html( $actor_name );
$date = esc_html( $date );
$user_url = esc_url( $user_url );
$content = wp_kses_post( $custom_message );
if ( false === strpos( $content, '<' ) ) {
$content = nl2br( esc_html( $content ) );
}
$language = esc_attr( get_bloginfo( 'language' ) );
$security_alert = esc_html__( 'Security alert', 'aegis-user-guard' );
$account_activity = esc_html__( 'Account activity', 'aegis-user-guard' );
/* translators: %s is the site name. */
$detected_event = sprintf( __( 'Aegis detected a user account event on %s. Review the details below to confirm this activity was expected.', 'aegis-user-guard' ), $site_name );
$account_label = esc_html__( 'Account', 'aegis-user-guard' );
$email_label = esc_html__( 'Email', 'aegis-user-guard' );
$occurred_label = esc_html__( 'Occurred', 'aegis-user-guard' );
$initiated_by_label = esc_html__( 'Initiated by', 'aegis-user-guard' );
$review_account = esc_html__( 'Review account securely', 'aegis-user-guard' );
$copy_url = esc_html__( 'If the button does not work, copy and paste this URL into your browser:', 'aegis-user-guard' );
/* translators: %s is the site name. */
$footer_message = sprintf( __( 'This notification was sent by Aegis User Guard for %s. Keep this message for your security records.', 'aegis-user-guard' ), $site_name );
$automated_notice = esc_html__( 'Automated security notification', 'aegis-user-guard' );
return '' . $action_label . ' · ' . $site_name . '