page_slug ) )
: admin_url( 'options-general.php?page=' . $this->page_slug );
wp_safe_redirect( $redirect_to );
exit;
}
/**
* Determine if current admin screen is this plugin settings page.
*
* @return bool
*/
private function is_plugin_settings_screen() {
if ( ! function_exists( 'get_current_screen' ) ) {
return false;
}
$screen = get_current_screen();
if ( ! $screen || empty( $screen->id ) ) {
return false;
}
return 'settings_page_' . $this->page_slug === $screen->id;
}
/**
* Replace left admin footer text on plugin settings screen.
*
* @param string $text Existing footer text.
* @return string
*/
public function filter_admin_footer_text( $text ) {
if ( ! $this->is_plugin_settings_screen() ) {
return $text;
}
return sprintf(
'%1$s ! Thank you in advance! 🙌',
esc_html__( 'Maintained by wppasskey.', 'advanced-passkey-login' ),
esc_url( 'https://wordpress.org/support/plugin/advanced-passkey-login/reviews/#new-post' ),
esc_html__( 'Please rate this plugin', 'advanced-passkey-login' )
);
}
/**
* Replace right admin footer text on plugin settings screen.
*
* @param string $text Existing footer text.
* @return string
*/
public function filter_update_footer( $text ) {
if ( ! $this->is_plugin_settings_screen() ) {
return $text;
}
$wordpress_link = sprintf(
'',
esc_url( 'https://wordpress.org/plugins/advanced-passkey-login' ),
esc_html__( 'WordPress.org', 'advanced-passkey-login' )
);
$github_link = sprintf(
'',
esc_url( 'https://github.com/mbuiux/advanced-passkey-login' ),
esc_html__( 'GitHub', 'advanced-passkey-login' )
);
return $wordpress_link . ' | ' . $github_link;
}
/**
* Register the plugin settings page under Settings.
*/
public function add_admin_menu() {
add_options_page(
__( 'Advanced Passkeys for Secure Login', 'advanced-passkey-login' ),
__( 'Advanced Passkeys for Secure Login', 'advanced-passkey-login' ),
'manage_options',
$this->page_slug,
array( $this, 'render_settings_page' )
);
}
/**
* Enqueue admin styles/scripts for this plugin settings screen.
*
* @param string $hook Current admin page hook.
*/
public function enqueue_assets( $hook ) {
if ( 'settings_page_' . $this->page_slug !== $hook ) {
return;
}
$version = defined( 'ADVAPAFO_VERSION' ) ? ADVAPAFO_VERSION : '1.0.0';
$css_url = '';
/*
* Support the common Advanced Passkeys for Secure Login plugin structure first:
* /admin/css/advapafo-admin.css. The other paths are fallbacks for simple
* copy/paste installs and older generated bundles.
*/
if ( file_exists( plugin_dir_path( __DIR__ ) . 'admin/css/advapafo-admin.css' ) ) {
$css_url = ADVAPAFO_PLUGIN_URL . 'admin/css/advapafo-admin.css';
} elseif ( file_exists( __DIR__ . '/advapafo-admin.css' ) ) {
$css_url = ADVAPAFO_PLUGIN_URL . 'includes/advapafo-admin.css';
} elseif ( file_exists( plugin_dir_path( __DIR__ ) . 'assets/css/advapafo-admin.css' ) ) {
$css_url = ADVAPAFO_PLUGIN_URL . 'assets/css/advapafo-admin.css';
} elseif ( file_exists( plugin_dir_path( __DIR__ ) . 'advapafo-admin.css' ) ) {
$css_url = ADVAPAFO_PLUGIN_URL . 'advapafo-admin.css';
}
if ( $css_url ) {
wp_enqueue_style( 'advapafo-admin', $css_url, array(), $version );
}
$active_tab = $this->resolve_active_tab();
if ( in_array( $active_tab, array( 'settings', 'advanced' ), true ) ) {
$autosave_js = plugin_dir_path( __DIR__ ) . 'admin/js/advapafo-settings-autosave.js';
if ( file_exists( $autosave_js ) ) {
wp_enqueue_script(
'advapafo-settings-autosave',
ADVAPAFO_PLUGIN_URL . 'admin/js/advapafo-settings-autosave.js',
array(),
$version,
true
);
wp_localize_script(
'advapafo-settings-autosave',
'ADVAPAFOSettingsAutosave',
array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'advapafo_autosave_setting' ),
'messages' => array(
'saved' => __( 'Setting saved.', 'advanced-passkey-login' ),
'failed' => __( 'Setting could not be saved.', 'advanced-passkey-login' ),
'network' => __( 'Network error. Please check your connection and try again.', 'advanced-passkey-login' ),
),
)
);
}
}
if ( ! in_array( $active_tab, array( 'dashboard', 'audit' ), true ) ) {
return;
}
$dashboard_css = plugin_dir_path( __DIR__ ) . 'admin/css/advapafo-dashboard.css';
if ( file_exists( $dashboard_css ) ) {
wp_enqueue_style(
'advapafo-dashboard',
ADVAPAFO_PLUGIN_URL . 'admin/css/advapafo-dashboard.css',
array( 'advapafo-admin' ),
$version
);
}
if ( 'audit' === $active_tab ) {
$admin_table_js = plugin_dir_path( __DIR__ ) . 'admin/js/advapafo-admin-table.js';
if ( file_exists( $admin_table_js ) ) {
wp_enqueue_script(
'advapafo-admin-table',
ADVAPAFO_PLUGIN_URL . 'admin/js/advapafo-admin-table.js',
array(),
$version,
true
);
}
return;
}
$apexcharts_js = plugin_dir_path( __DIR__ ) . 'admin/vendor/apexcharts/apexcharts.min.js';
$apex_dependency = '';
if ( file_exists( $apexcharts_js ) ) {
wp_enqueue_script(
'advapafo-apexcharts',
ADVAPAFO_PLUGIN_URL . 'admin/vendor/apexcharts/apexcharts.min.js',
array(),
'3.49.1',
true
);
$apex_dependency = 'advapafo-apexcharts';
} elseif ( wp_script_is( 'apexcharts', 'registered' ) || wp_script_is( 'apexcharts', 'enqueued' ) ) {
$apex_dependency = 'apexcharts';
}
$dashboard_js = plugin_dir_path( __DIR__ ) . 'admin/js/advapafo-dashboard.js';
if ( file_exists( $dashboard_js ) ) {
$dashboard_deps = array();
if ( '' !== $apex_dependency ) {
$dashboard_deps[] = $apex_dependency;
}
wp_enqueue_script(
'advapafo-dashboard',
ADVAPAFO_PLUGIN_URL . 'admin/js/advapafo-dashboard.js',
$dashboard_deps,
$version,
true
);
}
}
/**
* Register plugin options and sanitization callbacks.
*/
public function register_settings() {
register_setting(
$this->option_group,
'advapafo_enabled',
array(
'type' => 'boolean',
'sanitize_callback' => array( $this, 'sanitize_checkbox' ),
'default' => true,
)
);
register_setting(
$this->option_group,
'advapafo_show_separator',
array(
'type' => 'boolean',
'sanitize_callback' => array( $this, 'sanitize_show_separator' ),
'default' => true,
)
);
register_setting(
$this->option_group,
'advapafo_conditional_ui_enabled',
array(
'type' => 'boolean',
'sanitize_callback' => array( $this, 'sanitize_checkbox' ),
'default' => false,
)
);
register_setting(
$this->option_group,
'advapafo_activity_logging_enabled',
array(
'type' => 'boolean',
'sanitize_callback' => array( $this, 'sanitize_checkbox' ),
'default' => true,
)
);
register_setting(
$this->option_group,
'advapafo_show_setup_notice',
array(
'type' => 'boolean',
'sanitize_callback' => array( $this, 'sanitize_checkbox' ),
'default' => true,
)
);
register_setting(
$this->option_group,
'advapafo_enable_woocommerce_support',
array(
'type' => 'boolean',
'sanitize_callback' => array( $this, 'sanitize_checkbox' ),
'default' => true,
)
);
register_setting(
$this->option_group,
'advapafo_enable_edd_support',
array(
'type' => 'boolean',
'sanitize_callback' => array( $this, 'sanitize_checkbox' ),
'default' => true,
)
);
register_setting(
$this->option_group,
'advapafo_enable_memberpress_support',
array(
'type' => 'boolean',
'sanitize_callback' => array( $this, 'sanitize_checkbox' ),
'default' => true,
)
);
register_setting(
$this->option_group,
'advapafo_enable_ultimate_member_support',
array(
'type' => 'boolean',
'sanitize_callback' => array( $this, 'sanitize_checkbox' ),
'default' => true,
)
);
register_setting(
$this->option_group,
'advapafo_enable_learndash_support',
array(
'type' => 'boolean',
'sanitize_callback' => array( $this, 'sanitize_checkbox' ),
'default' => true,
)
);
register_setting(
$this->option_group,
'advapafo_enable_buddyboss_support',
array(
'type' => 'boolean',
'sanitize_callback' => array( $this, 'sanitize_checkbox' ),
'default' => true,
)
);
register_setting(
$this->option_group,
'advapafo_enable_gravityforms_support',
array(
'type' => 'boolean',
'sanitize_callback' => array( $this, 'sanitize_checkbox' ),
'default' => true,
)
);
register_setting(
$this->option_group,
'advapafo_enable_pmp_support',
array(
'type' => 'boolean',
'sanitize_callback' => array( $this, 'sanitize_checkbox' ),
'default' => true,
)
);
register_setting(
$this->option_group,
'advapafo_eligible_roles',
array(
'type' => 'array',
'sanitize_callback' => array( $this, 'sanitize_roles' ),
'default' => array( 'administrator' ),
)
);
register_setting(
$this->option_group,
'advapafo_max_passkeys_per_user',
array(
'type' => 'integer',
'sanitize_callback' => array( $this, 'sanitize_max_passkeys' ),
'default' => 0,
)
);
register_setting(
$this->option_group,
'advapafo_user_verification',
array(
'type' => 'string',
'sanitize_callback' => array( $this, 'sanitize_user_verification' ),
'default' => 'required',
)
);
register_setting(
$this->option_group,
'advapafo_button_style',
array(
'type' => 'string',
'sanitize_callback' => array( $this, 'sanitize_button_style' ),
'default' => 'black',
)
);
register_setting(
$this->option_group,
'advapafo_rp_name',
array(
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'default' => '',
)
);
register_setting(
$this->option_group,
'advapafo_rp_id',
array(
'type' => 'string',
'sanitize_callback' => array( $this, 'sanitize_rp_id' ),
'default' => '',
)
);
register_setting(
$this->option_group,
'advapafo_login_challenge_ttl',
array(
'type' => 'integer',
'sanitize_callback' => array( $this, 'sanitize_challenge_ttl' ),
'default' => 300,
)
);
register_setting(
$this->option_group,
'advapafo_registration_challenge_ttl',
array(
'type' => 'integer',
'sanitize_callback' => array( $this, 'sanitize_challenge_ttl' ),
'default' => 300,
)
);
register_setting(
$this->option_group,
'advapafo_rate_limit_window',
array(
'type' => 'integer',
'sanitize_callback' => array( $this, 'sanitize_rate_limit_window' ),
'default' => 300,
)
);
register_setting(
$this->option_group,
'advapafo_rate_limit_max_failures',
array(
'type' => 'integer',
'sanitize_callback' => array( $this, 'sanitize_rate_limit_max_failures' ),
'default' => 5,
)
);
register_setting(
$this->option_group,
'advapafo_rate_limit_lockout',
array(
'type' => 'integer',
'sanitize_callback' => array( $this, 'sanitize_rate_limit_lockout' ),
'default' => 900,
)
);
}
/**
* Render the full settings page shell and active tab content.
*/
public function render_settings_page() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You do not have permission to access this page.', 'advanced-passkey-login' ) );
}
$active_tab = $this->resolve_active_tab();
$base_url = admin_url( 'options-general.php?page=' . $this->page_slug );
$show_quick_setup = ! $this->is_quick_setup_dismissed();
?>
render_dashboard_tab(); ?>
render_audit_tab(); ?>
render_shortcodes_tab(); ?>
render_advanced_tab() : $this->render_settings_tab(); ?>
%3$s',
esc_attr( $classes ),
esc_url( $tab_url ),
esc_html( $label )
);
}
/**
* Resolve active settings tab with nonce verification.
*
* @return string
*/
private function resolve_active_tab() {
$allowed_tabs = array( 'dashboard', 'audit', 'settings', 'advanced', 'shortcodes' );
$raw_tab = filter_input( INPUT_GET, 'tab', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
if ( ! is_string( $raw_tab ) || '' === $raw_tab ) {
return 'dashboard';
}
$active_tab = sanitize_key( $raw_tab );
if ( ! in_array( $active_tab, $allowed_tabs, true ) ) {
return 'settings';
}
$raw_nonce = filter_input( INPUT_GET, 'advapafo_tab_nonce', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
if ( ! is_string( $raw_nonce ) || '' === $raw_nonce ) {
return 'settings';
}
$nonce = sanitize_text_field( $raw_nonce );
if ( ! wp_verify_nonce( $nonce, 'advapafo_tab_' . $active_tab ) ) {
return 'settings';
}
return $active_tab;
}
/**
* Option names eligible for AJAX autosave, keyed by option name for O(1) lookup.
*
* Kept in sync manually with register_settings() — every option registered there
* must have a matching entry here or autosave will reject it with a 400.
*
* @return array
*/
private function get_autosave_allowed_options() {
return array(
'advapafo_enabled' => true,
'advapafo_show_separator' => true,
'advapafo_conditional_ui_enabled' => true,
'advapafo_activity_logging_enabled' => true,
'advapafo_show_setup_notice' => true,
'advapafo_enable_woocommerce_support' => true,
'advapafo_enable_edd_support' => true,
'advapafo_enable_memberpress_support' => true,
'advapafo_enable_ultimate_member_support' => true,
'advapafo_enable_learndash_support' => true,
'advapafo_enable_buddyboss_support' => true,
'advapafo_enable_gravityforms_support' => true,
'advapafo_enable_pmp_support' => true,
'advapafo_eligible_roles' => true,
'advapafo_max_passkeys_per_user' => true,
'advapafo_user_verification' => true,
'advapafo_button_style' => true,
'advapafo_rp_name' => true,
'advapafo_rp_id' => true,
'advapafo_login_challenge_ttl' => true,
'advapafo_registration_challenge_ttl' => true,
'advapafo_rate_limit_window' => true,
'advapafo_rate_limit_max_failures' => true,
'advapafo_rate_limit_lockout' => true,
);
}
/**
* Handle a single-setting AJAX autosave request.
*/
public function ajax_autosave_setting() {
if ( ! isset( $_SERVER['REQUEST_METHOD'] ) || 'POST' !== strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) ) {
wp_send_json_error( array( 'message' => __( 'Invalid request method.', 'advanced-passkey-login' ) ), 405 );
}
if ( ! check_ajax_referer( 'advapafo_autosave_setting', 'nonce', false ) ) {
wp_send_json_error( array( 'message' => __( 'Security check failed. Please refresh the page and try again.', 'advanced-passkey-login' ) ), 403 );
}
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => __( 'You do not have permission to change this setting.', 'advanced-passkey-login' ) ), 403 );
}
$option_name = isset( $_POST['option_name'] ) ? sanitize_key( wp_unslash( $_POST['option_name'] ) ) : '';
$allowed = $this->get_autosave_allowed_options();
if ( '' === $option_name || ! isset( $allowed[ $option_name ] ) ) {
wp_send_json_error( array( 'message' => __( 'Unknown setting.', 'advanced-passkey-login' ) ), 400 );
}
if ( advapafo_is_setting_overridden( $option_name ) ) {
wp_send_json_error( array( 'message' => __( 'This setting is managed by site configuration and cannot be changed here.', 'advanced-passkey-login' ) ), 409 );
}
if ( isset( $_POST['option_value'] ) && is_array( $_POST['option_value'] ) ) {
$raw_value = wp_unslash( $_POST['option_value'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized below via sanitize_option().
} else {
$raw_value = isset( $_POST['option_value'] ) ? wp_unslash( $_POST['option_value'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized below via sanitize_option().
}
// Reuses the sanitize_callback already registered for this option in register_settings().
$sanitized = sanitize_option( $option_name, $raw_value );
update_option( $option_name, $sanitized );
// Mirrors sanitize_show_separator()'s force-off rule for the case where only
// conditional_ui_enabled is submitted (autosave never posts both fields at once).
if ( 'advapafo_conditional_ui_enabled' === $option_name && $sanitized ) {
update_option( 'advapafo_show_separator', 0 );
}
if ( get_option( $option_name ) != $sanitized ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual -- intentionally loose; WP options are stored/retrieved as strings for scalars (e.g. int 1 vs "1"), and update_option() returning false for an unchanged value is not itself an error.
wp_send_json_error( array( 'message' => __( 'Setting could not be saved.', 'advanced-passkey-login' ) ), 500 );
}
$response = array(
'option_name' => $option_name,
'value' => $sanitized,
'message' => __( 'Setting saved.', 'advanced-passkey-login' ),
);
if ( 'advapafo_conditional_ui_enabled' === $option_name ) {
$response['show_separator_effective'] = (bool) get_option( 'advapafo_show_separator', true );
}
wp_send_json_success( $response );
}
/**
* Render the everyday settings tab.
*/
private function render_settings_tab() {
$enabled = (bool) advapafo_get_setting( 'enabled', true );
$show_setup_notice = (bool) advapafo_get_setting( 'show_setup_notice', true );
$eligible_roles = (array) advapafo_get_setting( 'eligible_roles', array( 'administrator' ) );
$max_passkeys = absint( advapafo_get_setting( 'max_passkeys_per_user', 0 ) );
$verification = advapafo_get_setting( 'user_verification', 'required' );
$roles = wp_roles()->roles;
$enabled_overridden = advapafo_is_setting_overridden( 'enabled' );
$show_setup_notice_overridden = advapafo_is_setting_overridden( 'show_setup_notice' );
$eligible_roles_overridden = advapafo_is_setting_overridden( 'eligible_roles' );
$max_passkeys_overridden = advapafo_is_setting_overridden( 'max_passkeys_per_user' );
$verification_overridden = advapafo_is_setting_overridden( 'user_verification' );
?>
get_credentials_table_for_audit();
$users_with_passkeys = 0;
$passkeys_total = 0;
if ( '' !== $credentials_table ) {
$credentials_table_sql = $this->quote_table_name( $credentials_table );
if ( '' !== $credentials_table_sql ) {
$users_with_passkeys = (int) $wpdb->get_var( 'SELECT COUNT(DISTINCT user_id) FROM ' . $credentials_table_sql . ' WHERE revoked_at IS NULL' ); // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared -- table identifier is strict-validated by quote_table_name().
$passkeys_total = (int) $wpdb->get_var( 'SELECT COUNT(*) FROM ' . $credentials_table_sql . ' WHERE revoked_at IS NULL' ); // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared -- table identifier is strict-validated by quote_table_name().
}
}
$activities_total = $this->count_combined_audit_login_rows();
$blocked_attempts = $this->count_combined_login_event( 'login_password_blocked_passkey_only' ) + $this->count_combined_login_event( 'login_rate_limited' );
$authenticator_totals = array();
if ( '' !== $credentials_table ) {
$credentials_table_sql = $this->quote_table_name( $credentials_table );
if ( '' === $credentials_table_sql ) {
$rows = array();
} else {
$rows = $wpdb->get_results( // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared -- table identifier is strict-validated by quote_table_name().
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- table identifier is strict-validated by quote_table_name().
'SELECT COALESCE(NULLIF(TRIM(credential_label), ""), "") AS credential_label, COALESCE(NULLIF(TRIM(credential_id_hash), ""), "") AS credential_hash, COUNT(*) AS total FROM ' . $credentials_table_sql . ' WHERE revoked_at IS NULL GROUP BY credential_label, credential_id_hash ORDER BY total DESC LIMIT 300',
ARRAY_A
);
}
foreach ( $rows as $row ) {
$raw_label = (string) ( $row['credential_label'] ?? '' );
$credential_hash = strtolower( trim( (string) ( $row['credential_hash'] ?? '' ) ) );
$raw_provider = '';
$raw_aaguid = '';
if ( '' !== $credential_hash ) {
$raw_provider = $this->lookup_credential_provider_hint_for_hash( $credential_hash );
$raw_aaguid = $this->lookup_credential_aaguid_for_hash( $credential_hash );
}
$provider_meta = $this->resolve_authenticator_metadata_for_reporting( $raw_provider, $raw_label, $raw_aaguid );
$provider = (string) ( $provider_meta['label'] ?? 'Unknown Authenticator' );
if ( ! isset( $authenticator_totals[ $provider ] ) ) {
$authenticator_totals[ $provider ] = 0;
}
$authenticator_totals[ $provider ] += (int) ( $row['total'] ?? 0 );
}
}
arsort( $authenticator_totals );
$authenticator_rows = array();
foreach ( $authenticator_totals as $provider => $total ) {
$authenticator_rows[] = array(
'provider' => (string) $provider,
'provider_key' => $this->normalize_authenticator_provider_key( (string) $provider ),
'total' => (int) $total,
);
if ( count( $authenticator_rows ) >= 8 ) {
break;
}
}
$authenticator_chart_labels = array();
$authenticator_chart_series = array();
$authenticator_chart_other_total = 0;
$authenticator_chart_limit = 5;
$authenticator_chart_index = 0;
foreach ( $authenticator_totals as $provider => $total ) {
if ( $authenticator_chart_index < $authenticator_chart_limit ) {
$authenticator_chart_labels[] = (string) $provider;
$authenticator_chart_series[] = (int) $total;
} else {
$authenticator_chart_other_total += (int) $total;
}
++$authenticator_chart_index;
}
if ( $authenticator_chart_other_total > 0 ) {
$authenticator_chart_labels[] = __( 'Other', 'advanced-passkey-login' );
$authenticator_chart_series[] = (int) $authenticator_chart_other_total;
}
$authenticator_chart_payload = array(
'labels' => $authenticator_chart_labels,
'series' => $authenticator_chart_series,
);
$login_rows = $this->get_combined_audit_login_rows_for_last_days( 14, 600 );
$dashboard_allowed_statuses = array( 'success', 'blocked', 'failed' );
$dashboard_activity_rows = array();
foreach ( $login_rows as $login_row ) {
$event_type = (string) ( $login_row['event_type'] ?? '' );
$status_meta = $this->classify_audit_event_for_activity_feed( $event_type );
$status_key = (string) ( $status_meta['key'] ?? '' );
if ( in_array( $status_key, $dashboard_allowed_statuses, true ) ) {
$dashboard_activity_rows[] = $login_row;
}
}
$dashboard_activity_rows = array_slice( $dashboard_activity_rows, 0, 8 );
$chart_days = 14;
$chart_success_points = array();
$chart_blocked_points = array();
$chart_failed_points = array();
$chart_labels = array();
$chart_timezone = wp_timezone();
$day_success_buckets = array();
$day_blocked_buckets = array();
$day_failed_buckets = array();
for ( $offset = $chart_days - 1; $offset >= 0; $offset-- ) {
$bucket_ts = strtotime( '-' . $offset . ' days', time() );
$bucket_key = wp_date( 'Y-m-d', $bucket_ts, $chart_timezone );
$day_success_buckets[ $bucket_key ] = 0;
$day_blocked_buckets[ $bucket_key ] = 0;
$day_failed_buckets[ $bucket_key ] = 0;
}
$chart_source_rows = $this->get_combined_audit_login_rows( 300 );
foreach ( $chart_source_rows as $row ) {
$event_type = (string) ( $row['event_type'] ?? '' );
$event_bucket = $this->classify_audit_event_for_activity_trend( $event_type );
if ( '' === $event_bucket ) {
continue;
}
$timestamp_raw = (string) ( $row['log_timestamp'] ?? '' );
$timestamp_ts = $this->utc_datetime_to_timestamp( $timestamp_raw );
if ( $timestamp_ts <= 0 ) {
continue;
}
$day_key = wp_date( 'Y-m-d', $timestamp_ts, $chart_timezone );
if ( 'success' === $event_bucket && array_key_exists( $day_key, $day_success_buckets ) ) {
++$day_success_buckets[ $day_key ];
} elseif ( 'blocked' === $event_bucket && array_key_exists( $day_key, $day_blocked_buckets ) ) {
++$day_blocked_buckets[ $day_key ];
} elseif ( 'failed' === $event_bucket && array_key_exists( $day_key, $day_failed_buckets ) ) {
++$day_failed_buckets[ $day_key ];
}
}
foreach ( $day_success_buckets as $day_key => $count ) {
$chart_success_points[] = (int) $count;
$chart_blocked_points[] = (int) ( $day_blocked_buckets[ $day_key ] ?? 0 );
$chart_failed_points[] = (int) ( $day_failed_buckets[ $day_key ] ?? 0 );
$chart_labels[] = wp_date( 'M j', strtotime( $day_key ), $chart_timezone );
}
$activity_chart_payload = array(
'labels' => $chart_labels,
'success' => $chart_success_points,
'blocked' => $chart_blocked_points,
'failed' => $chart_failed_points,
);
?>
-
render_authenticator_provider_badge( (string) $row['provider'], array( 'provider_key' => (string) ( $row['provider_key'] ?? '' ) ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- render_authenticator_provider_badge() returns fully escaped, internally-controlled markup. ?>
classify_audit_event_for_activity_feed( $event );
$status_key = (string) ( $status_meta['key'] ?? 'info' );
$status_label = (string) ( $status_meta['label'] ?? __( 'Info', 'advanced-passkey-login' ) );
$method = __( 'Other', 'advanced-passkey-login' );
$method_key = 'other';
if ( 'login_success' === $event ) {
$method = __( 'Passkey', 'advanced-passkey-login' );
$method_key = 'passkey';
} elseif ( 'login_password_success' === $event || 'login_password_blocked_passkey_only' === $event || 'login_bypass_cookie_used' === $event ) {
$method = __( 'Password', 'advanced-passkey-login' );
$method_key = 'password';
} elseif ( 'login_rate_limited' === $event ) {
$method = __( 'Passkey', 'advanced-passkey-login' );
$method_key = 'passkey';
} elseif ( in_array( $event, array( 'login_failed', 'login_credential_mismatch', 'login_begin_failed' ), true ) ) {
$method = __( 'Passkey', 'advanced-passkey-login' );
$method_key = 'passkey';
} elseif ( in_array( $event, array( 'attestation_policy_pass', 'trusted_device_match', 'trusted_device_first_seen', 'trusted_device_marked_trusted', 'session_hardening_other_sessions_terminated', 'attestation_policy_dry_run_block', 'attestation_policy_aaguid_dry_run_block', 'trusted_device_mismatch', 'attestation_policy_blocked', 'attestation_policy_aaguid_blocked', 'trusted_device_revoked' ), true ) ) {
$method = __( 'Security Policy', 'advanced-passkey-login' );
$method_key = 'security';
}
$authenticator = '—';
$authenticator_key = 'unknown';
if ( ! in_array( $method_key, array( 'password', 'security' ), true ) ) {
$credential_hash = isset( $data['credential_hash'] ) ? (string) $data['credential_hash'] : '';
$raw_authenticator = isset( $data['authenticator'] ) ? (string) $data['authenticator'] : '';
$raw_label = '';
$raw_aaguid = isset( $data['aaguid'] ) ? (string) $data['aaguid'] : '';
if ( '' === $raw_authenticator && '' !== $credential_hash ) {
$raw_authenticator = $this->lookup_credential_provider_hint_for_hash( $credential_hash );
}
if ( '' === $raw_authenticator && '' !== $credential_hash ) {
$raw_authenticator = $this->lookup_credential_label_for_hash( $credential_hash );
}
if ( '' === $raw_aaguid && '' !== $credential_hash ) {
$raw_aaguid = $this->lookup_credential_aaguid_for_hash( $credential_hash );
}
if ( isset( $data['authenticator_label'] ) ) {
$raw_label = (string) $data['authenticator_label'];
}
$meta = $this->resolve_authenticator_metadata_for_reporting( $raw_authenticator, $raw_label, $raw_aaguid );
$authenticator = (string) ( $meta['label'] ?? '' );
$authenticator_key = (string) ( $meta['key'] ?? 'unknown' );
if ( '' === $authenticator ) {
$authenticator = __( 'Unknown Authenticator', 'advanced-passkey-login' );
}
}
$user_label = isset( $data['user_login'] ) ? (string) $data['user_login'] : '';
if ( '' === $user_label && isset( $data['user_id'] ) ) {
$activity_user = get_userdata( (int) $data['user_id'] );
if ( $activity_user instanceof WP_User ) {
$user_label = (string) $activity_user->user_login;
}
}
if ( '' === $user_label ) {
$user_label = __( 'User activity', 'advanced-passkey-login' );
}
$timestamp_raw = (string) ( $row['log_timestamp'] ?? '' );
$timestamp_display = $this->format_utc_datetime_for_display( $timestamp_raw );
$timestamp_relative = $this->format_utc_relative_for_display( $timestamp_raw );
?>
|
—
render_authenticator_provider_badge( $authenticator, array( 'provider_key' => $authenticator_key ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- render_authenticator_provider_badge() returns fully escaped, internally-controlled markup. ?>
|
get_credentials_table_for_audit();
$activity_logging_enabled = $this->is_activity_logging_enabled();
$users_with_passkeys = 0;
$passkeys_total = 0;
if ( '' !== $credentials_table ) {
$credentials_table_sql = $this->quote_table_name( $credentials_table );
if ( '' !== $credentials_table_sql ) {
$users_with_passkeys = (int) $wpdb->get_var( 'SELECT COUNT(DISTINCT user_id) FROM ' . $credentials_table_sql . ' WHERE revoked_at IS NULL' ); // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared -- table identifier is strict-validated by quote_table_name().
$passkeys_total = (int) $wpdb->get_var( 'SELECT COUNT(*) FROM ' . $credentials_table_sql . ' WHERE revoked_at IS NULL' ); // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared -- table identifier is strict-validated by quote_table_name().
}
}
$passkey_logins = $this->count_combined_login_event( 'login_success' );
$password_logins = $this->count_combined_login_event( 'login_password_success' );
$blocked_attempts = $this->count_combined_login_event( 'login_password_blocked_passkey_only' );
$bypassed_logins = $this->count_combined_login_event( 'login_bypass_cookie_used' );
$authenticator_rows = array();
if ( '' !== $credentials_table ) {
$credentials_table_sql = $this->quote_table_name( $credentials_table );
$rows = array();
if ( '' !== $credentials_table_sql ) {
$rows = $wpdb->get_results( // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared -- table identifier is strict-validated by quote_table_name().
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- table identifier is strict-validated by quote_table_name().
'SELECT COALESCE(NULLIF(TRIM(credential_label), ""), "") AS credential_label, COALESCE(NULLIF(TRIM(credential_id_hash), ""), "") AS credential_hash, COUNT(*) AS total FROM ' . $credentials_table_sql . ' WHERE revoked_at IS NULL GROUP BY credential_label, credential_id_hash ORDER BY total DESC LIMIT 300',
ARRAY_A
);
}
$grouped = array();
foreach ( $rows as $row ) {
$raw_label = (string) ( $row['credential_label'] ?? '' );
$credential_hash = strtolower( trim( (string) ( $row['credential_hash'] ?? '' ) ) );
$raw_provider = '';
$raw_aaguid = '';
if ( '' !== $credential_hash ) {
$raw_provider = $this->lookup_credential_provider_hint_for_hash( $credential_hash );
$raw_aaguid = $this->lookup_credential_aaguid_for_hash( $credential_hash );
}
$provider_meta = $this->resolve_authenticator_metadata_for_reporting( $raw_provider, $raw_label, $raw_aaguid );
$provider = (string) ( $provider_meta['label'] ?? 'Unknown Authenticator' );
if ( ! isset( $grouped[ $provider ] ) ) {
$grouped[ $provider ] = 0;
}
$grouped[ $provider ] += (int) ( $row['total'] ?? 0 );
}
arsort( $grouped );
foreach ( $grouped as $provider => $total ) {
$authenticator_rows[] = array(
'provider' => (string) $provider,
'total' => (int) $total,
);
}
}
$audit_per_page = 50;
$audit_page = isset( $_GET['advapafo_audit_page'] ) ? max( 1, absint( wp_unslash( $_GET['advapafo_audit_page'] ) ) ) : 1; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only pagination cursor on an admin-only screen; tab access itself is nonce-verified by resolve_active_tab().
$audit_total_rows = $this->count_combined_audit_login_rows();
$audit_total_pages = max( 1, (int) ceil( $audit_total_rows / $audit_per_page ) );
if ( $audit_page > $audit_total_pages ) {
$audit_page = $audit_total_pages;
}
$audit_offset = ( $audit_page - 1 ) * $audit_per_page;
$login_rows = $this->get_combined_audit_login_rows( $audit_per_page, $audit_offset );
$audit_start = 0;
$audit_end = 0;
if ( $audit_total_rows > 0 ) {
$audit_start = $audit_offset + 1;
$audit_end = min( $audit_offset + count( $login_rows ), $audit_total_rows );
}
$audit_pagination_links = array();
if ( $audit_total_pages > 1 ) {
$audit_page_base = wp_nonce_url(
add_query_arg(
array(
'page' => $this->page_slug,
'tab' => 'audit',
'advapafo_audit_page' => '%#%',
),
admin_url( 'options-general.php' )
),
'advapafo_tab_audit',
'advapafo_tab_nonce'
);
$audit_pagination_links = paginate_links(
array(
'base' => $audit_page_base,
'format' => '',
'current' => $audit_page,
'total' => $audit_total_pages,
'type' => 'array',
'prev_text' => __( 'Previous', 'advanced-passkey-login' ),
'next_text' => __( 'Next', 'advanced-passkey-login' ),
)
);
}
?>
|
|
|
| render_authenticator_provider_badge( (string) $row['provider'], array( 'provider_key' => $this->normalize_authenticator_provider_key( (string) $row['provider'] ) ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- render_authenticator_provider_badge() returns sanitized badge HTML. ?> |
|
Advanced Passkeys > Advanced. Re-enable it to restore login activity charts and detailed audit rows.', 'advanced-passkey-login' ); ?>
0 ) {
printf(
/* translators: 1: first visible row number, 2: last visible row number, 3: total rows */
esc_html__( 'Showing %1$s-%2$s of %3$s events.', 'advanced-passkey-login' ),
esc_html( number_format_i18n( $audit_start ) ),
esc_html( number_format_i18n( $audit_end ) ),
esc_html( number_format_i18n( $audit_total_rows ) )
);
} else {
esc_html_e( 'No events found.', 'advanced-passkey-login' );
}
?>
|
|
|
|
|
|
|
|
|
extract_credential_hash_from_log_payload( $data );
$raw_authenticator = isset( $data['authenticator'] ) ? (string) $data['authenticator'] : '';
$raw_label = isset( $data['authenticator_label'] ) ? (string) $data['authenticator_label'] : '';
$raw_aaguid = isset( $data['aaguid'] ) ? (string) $data['aaguid'] : '';
if ( '' === $raw_authenticator && '' !== $credential_hash ) {
$raw_authenticator = $this->lookup_credential_provider_hint_for_hash( $credential_hash );
}
if ( '' === $raw_label && '' !== $credential_hash ) {
$raw_label = $this->lookup_credential_label_for_hash( $credential_hash );
}
if ( '' === $raw_aaguid && '' !== $credential_hash ) {
$raw_aaguid = $this->lookup_credential_aaguid_for_hash( $credential_hash );
}
$meta = $this->resolve_authenticator_metadata_for_reporting( $raw_authenticator, $raw_label, $raw_aaguid );
$authenticator = (string) ( $meta['label'] ?? '' );
$authenticator_key = (string) ( $meta['key'] ?? 'unknown' );
if ( '' === $authenticator ) {
$authenticator = __( 'Unknown Authenticator', 'advanced-passkey-login' );
}
}
$user_ref = isset( $data['user_ref'] ) ? (string) $data['user_ref'] : '';
$user_ref_ui = '—';
if ( '' !== $user_ref ) {
$user_ref_ui = strlen( $user_ref ) > 12 ? substr( $user_ref, 0, 12 ) . '...' : $user_ref;
}
$ip_ui = $this->resolve_audit_ip_for_display( $data );
?>
|
|
|
format_utc_datetime_for_display( (string) ( $row['log_timestamp'] ?? '' ) ) ); ?> |
—
render_authenticator_provider_badge( $authenticator, array( 'provider_key' => $authenticator_key ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- render_authenticator_provider_badge() returns sanitized badge HTML. ?>
|
|
|
|
get_credentials_table_for_audit();
$table_sql = $this->quote_table_name( $table_name );
if ( '' === $table_sql ) {
$cache[ $credential_hash ] = '';
return '';
}
$label = (string) $wpdb->get_var( // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- one-off label lookup on plugin-owned custom table.
$wpdb->prepare(
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- table identifier is strict-validated by quote_table_name().
'SELECT COALESCE(NULLIF(TRIM(credential_label), ""), "") FROM ' . $table_sql . ' WHERE credential_id_hash = %s LIMIT 1',
$credential_hash
)
); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- table identifier is strict-validated by quote_table_name().
$cache[ $credential_hash ] = $label;
return $label;
}
/**
* Resolve credential AAGUID from registration audit logs by credential hash.
*
* @param string $credential_hash Credential SHA-256 hash.
* @return string
*/
private function lookup_credential_aaguid_for_hash( string $credential_hash ): string {
static $cache = array();
$credential_hash = strtolower( trim( $credential_hash ) );
if ( '' === $credential_hash ) {
return '';
}
if ( isset( $cache[ $credential_hash ] ) ) {
return (string) $cache[ $credential_hash ];
}
global $wpdb;
$resolved = '';
$log_tables = array_values(
array_unique(
array(
$wpdb->prefix . 'advapafo_logs',
$wpdb->prefix . 'wpk_logs',
)
)
);
$event_types = array(
'registered',
'registration_success',
'passkey_registered',
);
foreach ( $log_tables as $logs_table ) {
if ( ! $this->table_exists( $logs_table ) ) {
continue;
}
$table_sql = $this->quote_table_name( $logs_table );
if ( '' === $table_sql ) {
continue;
}
$rows = $wpdb->get_col( // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- bounded lookup from plugin-owned logs tables.
$wpdb->prepare(
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- table identifier is strict-validated by quote_table_name().
'SELECT log_data FROM ' . $table_sql . ' WHERE event_type IN (%s, %s, %s) ORDER BY id DESC LIMIT 400',
$event_types[0],
$event_types[1],
$event_types[2]
)
);
foreach ( $rows as $row_json ) {
$payload = json_decode( (string) $row_json, true );
if ( ! is_array( $payload ) ) {
continue;
}
$row_hash = $this->extract_credential_hash_from_log_payload( $payload );
if ( '' === $row_hash || ! hash_equals( $credential_hash, $row_hash ) ) {
continue;
}
$raw_aaguid = '';
if ( isset( $payload['aaguid'] ) ) {
$raw_aaguid = (string) $payload['aaguid'];
} elseif ( isset( $payload['AAGUID'] ) ) {
$raw_aaguid = (string) $payload['AAGUID'];
}
$resolved = $this->normalize_authenticator_aaguid( $raw_aaguid );
if ( '' !== $resolved ) {
break 2;
}
}
}
$cache[ $credential_hash ] = $resolved;
return $resolved;
}
/**
* Resolve credential provider hint from recent audit logs by credential hash.
*
* @param string $credential_hash Credential SHA-256 hash.
* @return string
*/
private function lookup_credential_provider_hint_for_hash( string $credential_hash ): string {
static $cache = array();
$credential_hash = strtolower( trim( $credential_hash ) );
if ( '' === $credential_hash ) {
return '';
}
if ( isset( $cache[ $credential_hash ] ) ) {
return (string) $cache[ $credential_hash ];
}
global $wpdb;
$resolved = '';
$log_tables = array_values(
array_unique(
array(
$wpdb->prefix . 'advapafo_logs',
$wpdb->prefix . 'wpk_logs',
)
)
);
$event_types = array(
'login_success',
'registered',
'registration_success',
'passkey_registered',
'login_credential_mismatch',
);
$provider_fields = array(
'authenticator',
'authenticator_label',
'provider',
'provider_label',
'device_name',
'device',
);
foreach ( $log_tables as $logs_table ) {
if ( ! $this->table_exists( $logs_table ) ) {
continue;
}
$table_sql = $this->quote_table_name( $logs_table );
if ( '' === $table_sql ) {
continue;
}
$rows = $wpdb->get_col( // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- bounded lookup from plugin-owned logs tables.
$wpdb->prepare(
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- table identifier is strict-validated by quote_table_name().
'SELECT log_data FROM ' . $table_sql . ' WHERE event_type IN (%s, %s, %s, %s, %s) ORDER BY id DESC LIMIT 600',
$event_types[0],
$event_types[1],
$event_types[2],
$event_types[3],
$event_types[4]
)
);
foreach ( $rows as $row_json ) {
$payload = json_decode( (string) $row_json, true );
if ( ! is_array( $payload ) ) {
continue;
}
$row_hash = $this->extract_credential_hash_from_log_payload( $payload );
if ( '' === $row_hash || ! hash_equals( $credential_hash, $row_hash ) ) {
continue;
}
$raw_provider = '';
foreach ( $provider_fields as $field ) {
if ( ! isset( $payload[ $field ] ) ) {
continue;
}
$candidate = trim( sanitize_text_field( (string) $payload[ $field ] ) );
if ( '' !== $candidate ) {
$raw_provider = $candidate;
break;
}
}
$raw_aaguid = '';
if ( isset( $payload['aaguid'] ) ) {
$raw_aaguid = (string) $payload['aaguid'];
} elseif ( isset( $payload['AAGUID'] ) ) {
$raw_aaguid = (string) $payload['AAGUID'];
}
$resolved = $this->resolve_authenticator_provider_label_from_signals( $raw_provider, $raw_provider, $raw_aaguid );
if ( '' === $resolved && '' !== $raw_provider && ! $this->is_generic_authenticator_placeholder( $raw_provider ) ) {
$resolved = $raw_provider;
}
if ( '' !== $resolved ) {
break 2;
}
}
}
$cache[ $credential_hash ] = $resolved;
return $resolved;
}
/**
* Extract a credential hash from a log payload using known key variants.
*
* @param array $payload Parsed log payload.
* @return string
*/
private function extract_credential_hash_from_log_payload( array $payload ): string {
foreach ( array( 'credential_hash', 'credential_id_hash', 'credentialIdHash', 'hash' ) as $key ) {
if ( ! isset( $payload[ $key ] ) ) {
continue;
}
$value = strtolower( trim( (string) $payload[ $key ] ) );
if ( '' !== $value ) {
return $value;
}
}
return '';
}
/**
* Normalize AAGUID input into canonical lowercase UUID format.
*
* @param string $aaguid Raw AAGUID value.
* @return string
*/
private function normalize_authenticator_aaguid( string $aaguid ): string {
$aaguid = strtolower( trim( $aaguid ) );
if ( '' === $aaguid ) {
return '';
}
$hex = str_replace( '-', '', $aaguid );
if ( ! preg_match( '/^[a-f0-9]{32}$/', $hex ) ) {
return '';
}
if ( str_repeat( '0', 32 ) === $hex ) {
return '';
}
return substr( $hex, 0, 8 ) . '-' . substr( $hex, 8, 4 ) . '-' . substr( $hex, 12, 4 ) . '-' . substr( $hex, 16, 4 ) . '-' . substr( $hex, 20, 12 );
}
/**
* Locate the active credentials table used for reporting.
*
* @return string
*/
private function get_credentials_table_for_audit(): string {
global $wpdb;
$candidates = array(
$wpdb->prefix . 'advapafo_credentials',
$wpdb->prefix . 'wpk_credentials',
);
$fallback = '';
foreach ( $candidates as $candidate ) {
if ( ! $this->table_exists( $candidate ) ) {
continue;
}
if ( '' === $fallback ) {
$fallback = $candidate;
}
$table_sql = $this->quote_table_name( $candidate );
if ( '' === $table_sql ) {
continue;
}
$active_count = (int) $wpdb->get_var( 'SELECT COUNT(*) FROM ' . $table_sql . ' WHERE revoked_at IS NULL' ); // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared -- table identifier is strict-validated by quote_table_name().
if ( $active_count > 0 ) {
return $candidate;
}
}
return $fallback;
}
/**
* Check whether a database table exists.
*
* @param string $table_name Table name.
* @return bool
*/
private function table_exists( string $table_name ): bool {
global $wpdb;
$table_name = trim( $table_name );
if ( '' === $table_name ) {
return false;
}
static $exists_cache = array();
if ( array_key_exists( $table_name, $exists_cache ) ) {
return (bool) $exists_cache[ $table_name ];
}
$found = (string) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $table_name ) ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
$exists_cache[ $table_name ] = ( $found === $table_name );
return (bool) $exists_cache[ $table_name ];
}
/**
* Quote and validate a table name for SQL usage.
*
* @param string $table_name Table name.
* @return string
*/
private function quote_table_name( string $table_name ): string {
$table_name = trim( $table_name );
if ( '' === $table_name ) {
return '';
}
if ( 1 !== preg_match( '/^[A-Za-z0-9_]+$/', $table_name ) ) {
return '';
}
return '`' . $table_name . '`';
}
/**
* Convert a UTC datetime string to a Unix timestamp.
*
* @param string $raw_datetime Datetime value.
* @return int
*/
private function utc_datetime_to_timestamp( string $raw_datetime ): int {
$raw_datetime = trim( $raw_datetime );
if ( '' === $raw_datetime ) {
return 0;
}
$date_utc = DateTimeImmutable::createFromFormat( 'Y-m-d H:i:s', $raw_datetime, new DateTimeZone( 'UTC' ) );
if ( $date_utc instanceof DateTimeImmutable ) {
return $date_utc->getTimestamp();
}
$fallback_ts = strtotime( $raw_datetime . ' UTC' );
return $fallback_ts ? (int) $fallback_ts : 0;
}
/**
* Format UTC datetime for admin display in site timezone.
*
* @param string $raw_datetime Datetime value.
* @return string
*/
private function format_utc_datetime_for_display( string $raw_datetime ): string {
$timestamp = $this->utc_datetime_to_timestamp( $raw_datetime );
if ( $timestamp <= 0 ) {
return $raw_datetime;
}
return wp_date(
get_option( 'date_format' ) . ' ' . get_option( 'time_format' ),
$timestamp,
wp_timezone()
);
}
/**
* Render relative time label from UTC datetime input.
*
* @param string $raw_datetime Datetime value.
* @return string
*/
private function format_utc_relative_for_display( string $raw_datetime ): string {
$timestamp = $this->utc_datetime_to_timestamp( $raw_datetime );
if ( $timestamp <= 0 ) {
return '';
}
$now = time();
$delta = $now - $timestamp;
if ( $delta < 0 ) {
return '';
}
if ( $delta < MINUTE_IN_SECONDS ) {
return __( 'just now', 'advanced-passkey-login' );
}
return sprintf(
/* translators: %s human time diff */
__( '%s ago', 'advanced-passkey-login' ),
human_time_diff( $timestamp, $now )
);
}
/**
* Get combined audit/login rows across new and legacy log tables.
*
* @param int $limit Row limit.
* @param int $offset Row offset.
* @return array>
*/
private function get_combined_audit_login_rows( int $limit, int $offset = 0 ): array {
global $wpdb;
$limit = max( 1, min( 2000, absint( $limit ) ) );
$offset = max( 0, min( 100000, absint( $offset ) ) );
$tables = array( $wpdb->prefix . 'advapafo_logs' );
$where_sql = '';
$table_sql = '';
foreach ( $tables as $table_name ) {
if ( ! $this->table_exists( $table_name ) ) {
continue;
}
$table_sql = $this->quote_table_name( $table_name );
if ( '' === $table_sql ) {
continue;
}
$where_sql = $this->build_audit_event_where_sql( $this->get_audit_event_types(), true );
break;
}
if ( '' === $table_sql || '' === $where_sql ) {
return array();
}
return $wpdb->get_results( // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- table identifier is strict-validated by quote_table_name(); query parameters are prepared below.
$wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- table identifier is strict-validated by quote_table_name(); WHERE SQL is pre-prepared by build_audit_event_where_sql().
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table and WHERE fragments are strict-validated/prepared before interpolation.
'SELECT id, event_type, log_timestamp, log_data FROM ' . $table_sql . ' WHERE ' . $where_sql . ' ORDER BY log_timestamp DESC, id DESC LIMIT %d OFFSET %d', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table and WHERE fragments are strict-validated/prepared before interpolation.
$limit,
$offset
),
ARRAY_A
);
}
/**
* Get combined audit/login rows for a trailing day range.
*
* @param int $days Number of days.
* @param int $limit Row limit.
* @return array>
*/
private function get_combined_audit_login_rows_for_last_days( int $days, int $limit = 300 ): array {
global $wpdb;
$days = max( 1, min( 365, absint( $days ) ) );
$limit = max( 1, min( 2000, absint( $limit ) ) );
$cutoff_utc = gmdate( 'Y-m-d H:i:s', time() - ( $days * DAY_IN_SECONDS ) );
$tables = array( $wpdb->prefix . 'advapafo_logs' );
$where_sql = '';
$table_sql = '';
foreach ( $tables as $table_name ) {
if ( ! $this->table_exists( $table_name ) ) {
continue;
}
$table_sql = $this->quote_table_name( $table_name );
if ( '' === $table_sql ) {
continue;
}
$where_sql = $this->build_audit_event_where_sql( $this->get_audit_event_types(), true );
break;
}
if ( '' === $table_sql || '' === $where_sql ) {
return array();
}
return $wpdb->get_results( // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- table identifier is strict-validated by quote_table_name(); query parameters are prepared below.
$wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- table identifier is strict-validated by quote_table_name(); WHERE SQL is pre-prepared by build_audit_event_where_sql().
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table and WHERE fragments are strict-validated/prepared before interpolation.
'SELECT id, event_type, log_timestamp, log_data FROM ' . $table_sql . ' WHERE ' . $where_sql . ' AND log_timestamp >= %s ORDER BY log_timestamp DESC, id DESC LIMIT %d', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table and WHERE fragments are strict-validated/prepared before interpolation.
$cutoff_utc,
$limit
),
ARRAY_A
);
}
/**
* Count combined audit/login rows across available log tables.
*
* @return int
*/
private function count_combined_audit_login_rows(): int {
global $wpdb;
$total = 0;
$tables = array( $wpdb->prefix . 'advapafo_logs' );
foreach ( $tables as $table_name ) {
if ( ! $this->table_exists( $table_name ) ) {
continue;
}
$table_sql = $this->quote_table_name( $table_name );
if ( '' === $table_sql ) {
continue;
}
$where_sql = $this->build_audit_event_where_sql( $this->get_audit_event_types(), true );
$total += (int) $wpdb->get_var( // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- aggregate count over plugin-owned log tables.
'SELECT COUNT(*) FROM ' . $table_sql . ' WHERE ' . $where_sql // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- table identifier is strict-validated by quote_table_name(); WHERE SQL is pre-prepared by build_audit_event_where_sql().
);
}
return $total;
}
/**
* Count a specific event type across combined log tables.
*
* @param string $event_type Event type key.
* @return int
*/
private function count_combined_login_event( string $event_type ): int {
global $wpdb;
$event_type = sanitize_key( $event_type );
if ( '' === $event_type ) {
return 0;
}
$total = 0;
$tables = array( $wpdb->prefix . 'advapafo_logs' );
foreach ( $tables as $table_name ) {
if ( ! $this->table_exists( $table_name ) ) {
continue;
}
$table_sql = $this->quote_table_name( $table_name );
if ( '' === $table_sql ) {
continue;
}
$total += (int) $wpdb->get_var( // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- aggregate count over plugin-owned log table.
$wpdb->prepare( 'SELECT COUNT(*) FROM ' . $table_sql . ' WHERE event_type = %s', $event_type ) // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- table identifier is strict-validated by quote_table_name().
);
}
return $total;
}
/**
* Whether audit/activity logging is currently enabled.
*
* @return bool
*/
private function is_activity_logging_enabled(): bool {
return (bool) advapafo_get_setting( 'activity_logging_enabled', true );
}
/**
* Resolve a displayable IP address from a decoded log payload, if present.
*
* @param array $data Decoded log payload.
* @return string
*/
private function resolve_audit_ip_for_display( array $data ): string {
$candidate_keys = array( 'ip_masked', 'ip_address', 'client_ip', 'remote_addr', 'ip' );
foreach ( $candidate_keys as $candidate_key ) {
if ( ! isset( $data[ $candidate_key ] ) ) {
continue;
}
$value = sanitize_text_field( (string) $data[ $candidate_key ] );
if ( '' === $value ) {
continue;
}
if ( filter_var( $value, FILTER_VALIDATE_IP ) ) {
return $value;
}
}
return '—';
}
/**
* List all event types included in audit reporting.
*
* @return array
*/
private function get_audit_event_types(): array {
return array(
'login_success',
'login_password_success',
'login_password_blocked_passkey_only',
'login_rate_limited',
'login_failed',
'login_credential_mismatch',
'login_begin_failed',
'login_bypass_cookie_used',
'attestation_policy_pass',
'attestation_policy_dry_run_block',
'attestation_policy_blocked',
'attestation_policy_aaguid_dry_run_block',
'attestation_policy_aaguid_blocked',
'trusted_device_first_seen',
'trusted_device_match',
'trusted_device_mismatch',
'trusted_device_marked_trusted',
'trusted_device_revoked',
'session_hardening_other_sessions_terminated',
);
}
/**
* Build a prepared WHERE SQL fragment for selected audit event filters.
*
* @param array $events Event keys.
* @param bool $include_magic_recovery_prefixes Include magic/recovery prefixed events.
* @return string
*/
private function build_audit_event_where_sql( array $events, bool $include_magic_recovery_prefixes ): string {
global $wpdb;
if ( empty( $events ) ) {
return '1=0';
}
$placeholders = implode( ',', array_fill( 0, count( $events ), '%s' ) );
$where_sql = 'event_type IN (' . $placeholders . ')';
$params = array_values( $events );
if ( $include_magic_recovery_prefixes ) {
$where_sql = '(' . $where_sql . ' OR event_type LIKE %s OR event_type LIKE %s)';
$params[] = 'magic_link_%';
$params[] = 'recovery_code_%';
}
return (string) $wpdb->prepare( $where_sql, ...$params ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- dynamic placeholder template is built from fixed literals and placeholder lists.
}
/**
* Map an event type to success/blocked/failed trend bucket.
*
* @param string $event_type Event type key.
* @return string
*/
private function classify_audit_event_for_activity_trend( string $event_type ): string {
if ( '' === $event_type ) {
return '';
}
if ( in_array( $event_type, array( 'login_success', 'login_password_success', 'login_bypass_cookie_used', 'magic_link_success', 'recovery_code_success' ), true ) ) {
return 'success';
}
if ( in_array( $event_type, array( 'login_password_blocked_passkey_only', 'login_rate_limited', 'recovery_code_rate_limited', 'magic_link_rate_limited' ), true ) ) {
return 'blocked';
}
if ( in_array( $event_type, array( 'login_failed', 'login_credential_mismatch', 'login_begin_failed', 'magic_link_invalid_or_expired', 'magic_link_replay_denied', 'recovery_code_invalid_or_used' ), true ) ) {
return 'failed';
}
return '';
}
/**
* Map an event type to feed badge metadata.
*
* @param string $event_type Event type key.
* @return array{key:string,label:string}
*/
private function classify_audit_event_for_activity_feed( string $event_type ): array {
if ( '' === $event_type ) {
return array(
'key' => 'info',
'label' => __( 'Info', 'advanced-passkey-login' ),
);
}
if ( in_array( $event_type, array( 'login_success', 'login_password_success', 'login_bypass_cookie_used', 'magic_link_success', 'recovery_code_success', 'attestation_policy_pass', 'trusted_device_match', 'trusted_device_first_seen', 'trusted_device_marked_trusted', 'session_hardening_other_sessions_terminated' ), true ) ) {
return array(
'key' => 'success',
'label' => __( 'Successful', 'advanced-passkey-login' ),
);
}
if ( in_array( $event_type, array( 'login_password_blocked_passkey_only', 'login_rate_limited', 'magic_link_rate_limited', 'recovery_code_rate_limited', 'attestation_policy_dry_run_block', 'attestation_policy_aaguid_dry_run_block', 'attestation_policy_blocked', 'attestation_policy_aaguid_blocked', 'trusted_device_mismatch', 'trusted_device_revoked' ), true ) ) {
return array(
'key' => 'blocked',
'label' => __( 'Blocked', 'advanced-passkey-login' ),
);
}
if ( in_array( $event_type, array( 'login_failed', 'login_credential_mismatch', 'login_begin_failed', 'magic_link_invalid_or_expired', 'magic_link_replay_denied', 'magic_link_send_failed', 'magic_link_recaptcha_failed', 'magic_link_user_not_found', 'magic_link_signing_error', 'magic_link_bot_challenge_failed', 'recovery_code_invalid_or_used', 'recovery_code_user_not_found', 'recovery_code_alert_failed', 'recovery_stepup_failed', 'recovery_stepup_magic_link_send_failed' ), true ) ) {
return array(
'key' => 'failed',
'label' => __( 'Failed', 'advanced-passkey-login' ),
);
}
if ( 0 === strpos( $event_type, 'magic_link_' ) || 0 === strpos( $event_type, 'recovery_code_' ) ) {
return array(
'key' => 'failed',
'label' => __( 'Failed', 'advanced-passkey-login' ),
);
}
return array(
'key' => 'info',
'label' => __( 'Info', 'advanced-passkey-login' ),
);
}
/**
* Normalize authenticator/provider labels for reporting.
*
* @param string $label Raw label.
* @return string
*/
private function resolve_authenticator_provider_for_reporting( string $label = '' ): string {
$meta = $this->resolve_authenticator_metadata_for_reporting( '', $label );
return (string) ( $meta['label'] ?? 'Unknown Authenticator' );
}
/**
* Build normalized authenticator metadata for consistent labels and icons.
*
* @param string $provider Raw provider value from logs.
* @param string $label Raw credential label.
* @param string $aaguid Raw AAGUID value.
* @return array{label:string,key:string}
*/
private function resolve_authenticator_metadata_for_reporting( string $provider = '', string $label = '', string $aaguid = '' ): array {
$provider = trim( $provider );
$label = trim( $label );
$aaguid = $this->normalize_authenticator_aaguid( $aaguid );
$resolved = $this->resolve_authenticator_provider_label_from_signals( $provider, $label, $aaguid );
if ( '' === $resolved ) {
$fallback = '' !== $provider ? $provider : $label;
$resolved = $this->is_generic_authenticator_placeholder( $fallback ) ? '' : $fallback;
}
if ( '' === $resolved || stripos( $resolved, 'unknown' ) !== false || stripos( $resolved, 'platform authenticator' ) !== false ) {
$resolved = $this->infer_unknown_authenticator_label( $provider, $label );
}
$resolved = (string) advapafo_get_setting(
'authenticator_provider_label',
$resolved,
array(
'provider' => '',
'label' => '' !== $label ? $label : $provider,
)
);
if ( '' === trim( $resolved ) ) {
$resolved = 'Unknown Authenticator';
}
return array(
'label' => $resolved,
'key' => $this->normalize_authenticator_provider_key( $resolved ),
);
}
/**
* Determine whether a provider/label is only a generic placeholder.
*
* @param string $label Provider or label candidate.
* @return bool
*/
private function is_generic_authenticator_placeholder( string $label ): bool {
$normalized = $this->normalize_provider_string_for_matching( $label );
if ( '' === $normalized ) {
return true;
}
return in_array(
$normalized,
array(
'passkey',
'passkeys',
'pass key',
'authenticator',
'unknown authenticator',
'platform authenticator',
'security key',
'this device',
'device',
),
true
);
}
/**
* Get default AAGUID-to-provider mappings from generated data file.
*
* @return array
*/
private function get_default_authenticator_aaguid_map(): array {
static $cached_map = null;
if ( is_array( $cached_map ) ) {
return $cached_map;
}
$defaults = array(
'08987058-cadc-4b81-b6e1-30de50dcbe96' => 'Windows Hello',
'2fc0579f-8113-47ea-b116-bb5a8db9202a' => 'YubiKey',
);
$map_file = ADVAPAFO_PLUGIN_DIR . 'includes/data/aaguid-provider-map.php';
if ( ! is_string( $map_file ) || '' === $map_file || ! file_exists( $map_file ) ) {
$cached_map = $defaults;
return $cached_map;
}
$loaded = require $map_file; // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable -- local plugin path and static filename.
if ( ! is_array( $loaded ) ) {
$cached_map = $defaults;
return $cached_map;
}
$normalized = array();
foreach ( $loaded as $aaguid => $provider ) {
$key = $this->normalize_authenticator_aaguid( (string) $aaguid );
$name = trim( (string) $provider );
if ( '' === $key || '' === $name ) {
continue;
}
$normalized[ $key ] = sanitize_text_field( $name );
}
if ( empty( $normalized ) ) {
$cached_map = $defaults;
return $cached_map;
}
$cached_map = array_merge( $defaults, $normalized );
return $cached_map;
}
/**
* Get provider-key icon assets generated from upstream AAGUID JSON.
*
* @return array
*/
private function get_authenticator_provider_icon_asset_map(): array {
static $cached_map = null;
if ( is_array( $cached_map ) ) {
return $cached_map;
}
$map_file = ADVAPAFO_PLUGIN_DIR . 'includes/data/provider-icon-map.php';
if ( ! is_string( $map_file ) || '' === $map_file || ! file_exists( $map_file ) ) {
$cached_map = array();
return $cached_map;
}
$loaded = require $map_file; // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable -- local plugin path and static filename.
if ( ! is_array( $loaded ) ) {
$cached_map = array();
return $cached_map;
}
$normalized = array();
foreach ( $loaded as $provider_key => $icons ) {
$key = sanitize_key( (string) $provider_key );
if ( '' === $key || ! is_array( $icons ) ) {
continue;
}
$icon_light = isset( $icons['icon_light'] ) ? trim( (string) $icons['icon_light'] ) : '';
$icon_dark = isset( $icons['icon_dark'] ) ? trim( (string) $icons['icon_dark'] ) : '';
if ( '' !== $icon_light && ! str_starts_with( $icon_light, 'data:image/' ) ) {
$icon_light = '';
}
if ( '' !== $icon_dark && ! str_starts_with( $icon_dark, 'data:image/' ) ) {
$icon_dark = '';
}
if ( '' === $icon_light && '' === $icon_dark ) {
continue;
}
$normalized[ $key ] = array(
'icon_light' => $icon_light,
'icon_dark' => $icon_dark,
);
}
$cached_map = $normalized;
return $cached_map;
}
/**
* Resolve a canonical provider label from raw provider/label signals.
*
* @param string $provider Raw provider value from logs.
* @param string $label Raw credential label.
* @param string $aaguid Canonical AAGUID value.
* @return string
*/
private function resolve_authenticator_provider_label_from_signals( string $provider = '', string $label = '', string $aaguid = '' ): string {
if ( '' !== $aaguid ) {
static $filtered_aaguid_map = null;
if ( ! is_array( $filtered_aaguid_map ) ) {
$aaguid_map = $this->get_default_authenticator_aaguid_map();
/**
* Filter known AAGUID to provider label mappings.
*
* @param array $aaguid_map Existing AAGUID map.
*/
$filtered_aaguid_map = (array) advapafo_get_setting( 'authenticator_aaguid_map', $aaguid_map );
}
$aaguid_map = $filtered_aaguid_map;
if ( isset( $aaguid_map[ $aaguid ] ) ) {
$mapped = trim( (string) $aaguid_map[ $aaguid ] );
if ( '' !== $mapped ) {
return $mapped;
}
}
}
$signal = $this->normalize_provider_string_for_matching( trim( $provider . ' ' . $label ) );
if ( '' === $signal ) {
return '';
}
$known_providers = array(
'iCloud Keychain' => array( 'icloud', 'i cloud', 'apple password', 'apple passwords', 'apple passkey', 'apple keychain' ),
'Bitwarden' => array( 'bitwarden', 'bitwarded' ),
'Google Password Manager' => array( 'google password manager', 'google passkey', 'google', 'chrome password', 'chrome passkey', 'chrome', 'android credential manager' ),
'Samsung Pass' => array( 'samsung pass', 'samsung passkey', 'samsung' ),
'LastPass' => array( 'lastpass' ),
'1Password' => array( '1password', 'onepassword' ),
'Dashlane' => array( 'dashlane' ),
'NordPass' => array( 'nordpass' ),
'Proton Pass' => array( 'proton pass', 'protonpass' ),
'Windows Hello' => array( 'windows hello', 'microsoft authenticator', 'microsoft password manager' ),
'YubiKey' => array( 'yubikey', 'yubico' ),
'Keeper' => array( 'keeper' ),
'Enpass' => array( 'enpass' ),
'KeePassXC' => array( 'keepassxc', 'keepass dx', 'keepassdx', 'keepass passkey' ),
'RoboForm' => array( 'roboform' ),
);
foreach ( $known_providers as $canonical => $needles ) {
foreach ( $needles as $needle ) {
if ( strpos( $signal, (string) $needle ) !== false ) {
return $canonical;
}
}
}
return '';
}
/**
* Infer unknown authenticator category from weaker hints.
*
* @param string $provider Raw provider value from logs.
* @param string $label Raw credential label.
* @return string
*/
private function infer_unknown_authenticator_label( string $provider = '', string $label = '' ): string {
$signal = $this->normalize_provider_string_for_matching( trim( $provider . ' ' . $label ) );
if ( strpos( $signal, 'security key' ) !== false || strpos( $signal, 'fido' ) !== false || strpos( $signal, 'u2f' ) !== false || strpos( $signal, 'hardware key' ) !== false || strpos( $signal, 'usb key' ) !== false ) {
return 'Unknown Security Key';
}
if ( strpos( $signal, 'cross device' ) !== false || strpos( $signal, 'cross-device' ) !== false || strpos( $signal, 'hybrid' ) !== false || strpos( $signal, 'cable' ) !== false || strpos( $signal, 'phone passkey' ) !== false || strpos( $signal, 'qr passkey' ) !== false ) {
return 'Unknown Cross-Device Authenticator';
}
if ( strpos( $signal, 'platform authenticator' ) !== false || strpos( $signal, 'platform passkey' ) !== false || strpos( $signal, 'this device' ) !== false || strpos( $signal, 'touch id' ) !== false || strpos( $signal, 'face id' ) !== false || strpos( $signal, 'biometric' ) !== false ) {
return 'Unknown Platform Authenticator';
}
return 'Unknown Authenticator';
}
/**
* Render a styled authenticator provider badge.
*
* @param string $provider Provider label.
* @param array $options Optional render options.
* @return string
*/
private function render_authenticator_provider_badge( string $provider, array $options = array() ): string {
$provider = trim( $provider );
if ( '' === $provider ) {
$provider = 'Unknown Authenticator';
}
$provider_key = isset( $options['provider_key'] ) ? sanitize_key( (string) $options['provider_key'] ) : '';
if ( '' === $provider_key ) {
$provider_key = $this->normalize_authenticator_provider_key( $provider );
}
$icon_markup = $this->get_authenticator_provider_icon_svg( $provider_key );
$html = '';
if ( '' !== $icon_markup ) {
$html .= '' . $icon_markup . '';
}
$html .= '' . esc_html( $provider ) . '';
$html .= '';
// Markup is assembled from sanitized text/key plus static internal SVG strings.
return $html;
}
/**
* Allowed HTML tags/attributes for authenticator badge markup.
*
* @return array>
*/
private function get_authenticator_provider_badge_allowed_html(): array {
return array(
'span' => array(
'class' => true,
'aria-hidden' => true,
),
'picture' => array(
'class' => true,
),
'source' => array(
'srcset' => true,
'media' => true,
),
'img' => array(
'class' => true,
'src' => true,
'alt' => true,
'width' => true,
'height' => true,
'loading' => true,
'decoding' => true,
),
'svg' => array(
'viewBox' => true,
'width' => true,
'height' => true,
'fill' => true,
'aria-hidden' => true,
'focusable' => true,
'role' => true,
'stroke' => true,
'stroke-width' => true,
'stroke-linecap' => true,
'stroke-linejoin' => true,
'fill-opacity' => true,
'xmlns' => true,
),
'path' => array(
'd' => true,
'fill' => true,
'stroke' => true,
'stroke-width' => true,
'stroke-linecap' => true,
'stroke-linejoin' => true,
'fill-opacity' => true,
),
'circle' => array(
'cx' => true,
'cy' => true,
'r' => true,
'fill' => true,
'stroke' => true,
'stroke-width' => true,
),
'rect' => array(
'x' => true,
'y' => true,
'width' => true,
'height' => true,
'rx' => true,
'fill' => true,
'stroke' => true,
'stroke-width' => true,
),
);
}
/**
* Normalize provider label to a stable key.
*
* @param string $provider Provider label.
* @return string
*/
private function normalize_authenticator_provider_key( string $provider ): string {
$provider_lc = $this->normalize_provider_string_for_matching( $provider );
if ( strpos( $provider_lc, 'bitwarden' ) !== false || strpos( $provider_lc, 'bitwarded' ) !== false ) {
return 'bitwarden';
}
if ( strpos( $provider_lc, 'icloud' ) !== false || strpos( $provider_lc, 'apple' ) !== false ) {
return 'icloud';
}
if ( strpos( $provider_lc, '1password' ) !== false || strpos( $provider_lc, 'onepassword' ) !== false ) {
return 'onepassword';
}
if ( strpos( $provider_lc, 'lastpass' ) !== false ) {
return 'lastpass';
}
if ( strpos( $provider_lc, 'google' ) !== false || strpos( $provider_lc, 'chrome' ) !== false ) {
return 'google';
}
if ( strpos( $provider_lc, 'keepassxc' ) !== false || strpos( $provider_lc, 'keepassdx' ) !== false || strpos( $provider_lc, 'keepass' ) !== false ) {
return 'unknown-security-key';
}
if ( strpos( $provider_lc, 'samsung' ) !== false ) {
return 'samsung';
}
if ( strpos( $provider_lc, 'windows hello' ) !== false || strpos( $provider_lc, 'microsoft authenticator' ) !== false || strpos( $provider_lc, 'microsoft password manager' ) !== false ) {
return 'windows-hello';
}
if ( strpos( $provider_lc, 'yubikey' ) !== false || strpos( $provider_lc, 'yubico' ) !== false ) {
return 'yubikey';
}
if ( strpos( $provider_lc, 'keeper' ) !== false ) {
return 'keeper';
}
if ( strpos( $provider_lc, 'enpass' ) !== false ) {
return 'enpass';
}
if ( strpos( $provider_lc, 'roboform' ) !== false ) {
return 'roboform';
}
if ( strpos( $provider_lc, 'dashlane' ) !== false ) {
return 'dashlane';
}
if ( strpos( $provider_lc, 'nordpass' ) !== false ) {
return 'nordpass';
}
if ( strpos( $provider_lc, 'proton' ) !== false ) {
return 'proton-pass';
}
if ( strpos( $provider_lc, 'unknown platform' ) !== false || strpos( $provider_lc, 'platform authenticator' ) !== false ) {
return 'unknown-platform';
}
if ( strpos( $provider_lc, 'unknown security key' ) !== false || strpos( $provider_lc, 'security key' ) !== false || strpos( $provider_lc, 'u2f' ) !== false || strpos( $provider_lc, 'fido' ) !== false ) {
return 'unknown-security-key';
}
if ( strpos( $provider_lc, 'unknown cross device' ) !== false || strpos( $provider_lc, 'cross device' ) !== false || strpos( $provider_lc, 'cross-device' ) !== false || strpos( $provider_lc, 'hybrid' ) !== false || strpos( $provider_lc, 'cable' ) !== false ) {
return 'unknown-cross-device';
}
return 'unknown';
}
/**
* Normalize provider text for robust matching across log payload variants.
*
* @param string $provider Raw provider label.
* @return string
*/
private function normalize_provider_string_for_matching( string $provider ): string {
$normalized = strtolower( wp_strip_all_tags( trim( $provider ) ) );
$normalized = str_replace( array( '-', '_', '/', '\\', '.', ',', ':', ';', '|', '(', ')', '[', ']' ), ' ', $normalized );
$normalized = preg_replace( '/[^a-z0-9\s]+/u', ' ', $normalized );
$normalized = preg_replace( '/\s+/u', ' ', $normalized );
return is_string( $normalized ) ? trim( $normalized ) : '';
}
/**
* Return SVG icon markup for known provider keys.
*
* @param string $provider_key Provider key.
* @return string
*/
private function get_authenticator_provider_icon_svg( string $provider_key ): string {
$assets = $this->get_authenticator_provider_icon_asset_map();
if ( isset( $assets[ $provider_key ] ) && is_array( $assets[ $provider_key ] ) ) {
$icon_light = isset( $assets[ $provider_key ]['icon_light'] ) ? trim( (string) $assets[ $provider_key ]['icon_light'] ) : '';
$icon_dark = isset( $assets[ $provider_key ]['icon_dark'] ) ? trim( (string) $assets[ $provider_key ]['icon_dark'] ) : '';
if ( '' === $icon_light && '' === $icon_dark ) {
return '';
}
if ( '' !== $icon_dark && '' !== $icon_light && $icon_dark !== $icon_light ) {
return '
';
}
$single = '' !== $icon_light ? $icon_light : $icon_dark;
if ( '' !== $single ) {
return '
';
}
}
return '';
}
/**
* Render advanced settings tab.
*/
private function render_advanced_tab() {
$show_separator = (bool) advapafo_get_setting( 'show_separator', true );
$conditional_ui_enabled = (bool) advapafo_get_setting( 'conditional_ui_enabled', false );
$activity_logging_enabled = (bool) advapafo_get_setting( 'activity_logging_enabled', true );
$activity_logging_overridden = advapafo_is_setting_overridden( 'activity_logging_enabled' );
if ( $conditional_ui_enabled ) {
$show_separator = false;
}
$button_style = advapafo_get_setting( 'button_style', 'black' );
$rp_name = advapafo_get_setting( 'rp_name', '' );
$rp_id = advapafo_get_setting( 'rp_id', '' );
$login_challenge_ttl = absint( advapafo_get_setting( 'login_challenge_ttl', 300 ) );
$registration_challenge_ttl = absint( advapafo_get_setting( 'registration_challenge_ttl', 300 ) );
$window = absint( advapafo_get_setting( 'rate_limit_window', 300 ) );
$max_failures = absint( advapafo_get_setting( 'rate_limit_max_failures', 5 ) );
$lockout = absint( advapafo_get_setting( 'rate_limit_lockout', 900 ) );
$show_separator_overridden = advapafo_is_setting_overridden( 'show_separator' );
$conditional_ui_enabled_overridden = advapafo_is_setting_overridden( 'conditional_ui_enabled' );
$button_style_overridden = advapafo_is_setting_overridden( 'button_style' );
$rp_name_overridden = advapafo_is_setting_overridden( 'rp_name' );
$rp_id_overridden = advapafo_is_setting_overridden( 'rp_id' );
$login_challenge_ttl_overridden = advapafo_is_setting_overridden( 'login_challenge_ttl' ) || advapafo_is_setting_overridden( 'challenge_ttl' );
$registration_challenge_ttl_overridden = advapafo_is_setting_overridden( 'registration_challenge_ttl' ) || advapafo_is_setting_overridden( 'challenge_ttl' );
$window_overridden = advapafo_is_setting_overridden( 'rate_limit_window' );
$max_failures_overridden = advapafo_is_setting_overridden( 'rate_limit_max_failures' );
$lockout_overridden = advapafo_is_setting_overridden( 'rate_limit_lockout' );
?>
__( 'Login Form', 'advanced-passkey-login' ),
'code' => '[advapafo_login_button]',
'description' => __( 'Display a passkey login form on any page.', 'advanced-passkey-login' ),
'placement' => __( 'Best for custom login pages.', 'advanced-passkey-login' ),
),
array(
'title' => __( 'Register Button', 'advanced-passkey-login' ),
'code' => '[advapafo_register_button]',
'description' => __( 'Let signed-in users register a new passkey.', 'advanced-passkey-login' ),
'placement' => __( 'Best for account and onboarding pages.', 'advanced-passkey-login' ),
),
array(
'title' => __( 'Account Passkeys', 'advanced-passkey-login' ),
'code' => '[advapafo_passkey_profile]',
'description' => __( 'Show a user-facing passkey management area.', 'advanced-passkey-login' ),
'placement' => __( 'Best for profile or dashboard pages.', 'advanced-passkey-login' ),
),
array(
'title' => __( 'Conditional Prompt', 'advanced-passkey-login' ),
'code' => '[advapafo_passkey_prompt]',
'description' => __( 'Prompt eligible users to set up passwordless login.', 'advanced-passkey-login' ),
'placement' => __( 'Best after login or checkout.', 'advanced-passkey-login' ),
),
);
if ( class_exists( 'ADVAPAFO_Integration_Manager' ) && method_exists( 'ADVAPAFO_Integration_Manager', 'get_integration_shortcodes' ) ) {
$integration_shortcodes = ADVAPAFO_Integration_Manager::get_integration_shortcodes();
foreach ( $integration_shortcodes as $integration_shortcode ) {
if ( empty( $integration_shortcode['title'] ) || empty( $integration_shortcode['code'] ) ) {
continue;
}
$shortcodes[] = array(
'title' => sanitize_text_field( (string) $integration_shortcode['title'] ),
'code' => sanitize_text_field( (string) $integration_shortcode['code'] ),
'description' => __( 'Integration-specific passkey entry point.', 'advanced-passkey-login' ),
'placement' => __( 'Shown only when the related plugin is active.', 'advanced-passkey-login' ),
);
}
}
?>
label
redirect_to
class
allow_multiple
button_label
[advapafo_login_button label="Sign in securely" redirect_to="/my-account/"]
[advapafo_login_button allow_multiple="1" class="my-passkey-login"]
[advapafo_register_button label="Add this device"]
[advapafo_passkey_prompt title="Secure your account" button_label="Set up passkey"]
page_slug )
),
'advapafo_tab_' . sanitize_key( $active_tab ),
'advapafo_tab_nonce'
);
?>
option_group . '-options' );
}
$conditional_input = filter_input( INPUT_POST, 'advapafo_conditional_ui_enabled', FILTER_DEFAULT );
if ( $has_valid_nonce && is_string( $conditional_input ) ) {
$raw_conditional = sanitize_text_field( $conditional_input );
$posted_conditional = '' !== $raw_conditional && '0' !== $raw_conditional;
}
$conditional_enabled = null !== $posted_conditional
? (bool) $posted_conditional
: (bool) get_option( 'advapafo_conditional_ui_enabled', false );
if ( $conditional_enabled ) {
return 0;
}
return ! empty( $value ) ? 1 : 0;
}
/**
* Sanitize eligible roles list.
*
* @param mixed $roles Submitted roles.
* @return array
*/
public function sanitize_roles( $roles ) {
if ( ! is_array( $roles ) ) {
return array( 'administrator' );
}
$valid_roles = array_keys( wp_roles()->roles );
$sanitized = array_values( array_intersect( array_map( 'sanitize_key', $roles ), $valid_roles ) );
if ( empty( $sanitized ) ) {
return array( 'administrator' );
}
return $sanitized;
}
/**
* Sanitize max passkeys setting.
*
* @param mixed $value Raw value.
* @return int
*/
public function sanitize_max_passkeys( $value ) {
return min( 999999, max( 0, absint( $value ) ) );
}
/**
* Sanitize user verification mode.
*
* @param mixed $value Raw value.
* @return string
*/
public function sanitize_user_verification( $value ) {
$allowed = array( 'required', 'preferred', 'discouraged' );
return in_array( $value, $allowed, true ) ? $value : 'required';
}
/**
* Sanitize button style setting.
*
* @param mixed $value Raw value.
* @return string
*/
public function sanitize_button_style( $value ) {
$allowed = array( 'black', 'light_grey' );
$value = sanitize_key( (string) $value );
return in_array( $value, $allowed, true ) ? $value : 'black';
}
/**
* Sanitize relying party ID.
*
* @param mixed $value Raw value.
* @return string
*/
public function sanitize_rp_id( $value ) {
$value = strtolower( sanitize_text_field( $value ) );
return preg_replace( '/[^a-z0-9.-]/', '', $value );
}
/**
* Sanitize rate-limit window setting.
*
* @param mixed $value Raw value.
* @return int
*/
public function sanitize_rate_limit_window( $value ) {
return min( 3600, max( 60, absint( $value ) ) );
}
/**
* Sanitize max failures setting.
*
* @param mixed $value Raw value.
* @return int
*/
public function sanitize_rate_limit_max_failures( $value ) {
return min( 50, max( 1, absint( $value ) ) );
}
/**
* Sanitize lockout duration setting.
*
* @param mixed $value Raw value.
* @return int
*/
public function sanitize_rate_limit_lockout( $value ) {
return min( 86400, max( 60, absint( $value ) ) );
}
/**
* Sanitize challenge timeout setting.
*
* @param mixed $value Raw value.
* @return int
*/
public function sanitize_challenge_ttl( $value ) {
return min( 1200, max( 30, absint( $value ) ) );
}
}