notice_transient_prefix . absint( $user_id ); } /** * Register settings-page hooks. */ public function __construct() { add_action( 'admin_menu', array( $this, 'add_admin_menu' ) ); add_action( 'admin_init', array( $this, 'register_settings' ) ); add_action( 'admin_action_update', array( $this, 'flag_settings_save' ), 1 ); add_action( 'admin_post_advapafo_dismiss_quick_setup', array( $this, 'dismiss_quick_setup' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) ); add_filter( 'admin_footer_text', array( $this, 'filter_admin_footer_text' ), 20 ); add_filter( 'update_footer', array( $this, 'filter_update_footer' ), 20 ); } /** * Determine whether the quick setup sidebar card has been dismissed. * * @return bool */ private function is_quick_setup_dismissed() { return '1' === (string) get_option( 'advapafo_quick_setup_dismissed', '0' ); } /** * Persist dismissal of the quick setup sidebar card. */ public function dismiss_quick_setup() { check_admin_referer( 'advapafo_dismiss_quick_setup' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to perform this action.', 'advanced-passkey-login' ) ); } update_option( 'advapafo_quick_setup_dismissed', '1', false ); $raw_redirect = filter_input( INPUT_POST, 'redirect_to', FILTER_SANITIZE_URL ); $redirect_to = is_string( $raw_redirect ) && '' !== $raw_redirect ? wp_validate_redirect( $raw_redirect, admin_url( 'options-general.php?page=' . $this->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 %3$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( '%2$s', esc_url( 'https://wordpress.org/plugins/advanced-passkey-login' ), esc_html__( 'WordPress.org', 'advanced-passkey-login' ) ); $github_link = sprintf( '%2$s', esc_url( 'https://github.com/mbuiux/advanced-passkey-login' ), esc_html__( 'GitHub', 'advanced-passkey-login' ) ); return $wordpress_link . ' | ' . $github_link; } /** * Detect when our settings form is submitted to options.php and store a * per-user flag BEFORE the redirect happens. Avoids relying on the * settings-updated URL param or the settings_errors transient, both of * which can be consumed or missing depending on environment. */ public function flag_settings_save() { if ( ! isset( $_SERVER['REQUEST_METHOD'] ) ) { return; } $request_method = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized immediately for method check. if ( 'POST' !== strtoupper( $request_method ) ) { return; } if ( ! current_user_can( 'manage_options' ) ) { return; } if ( empty( $_POST['option_page'] ) ) { return; } $option_page = sanitize_text_field( wp_unslash( $_POST['option_page'] ) ); if ( $this->option_group !== $option_page ) { return; } if ( empty( $_POST['_wpnonce'] ) ) { return; } $nonce = sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ); if ( ! wp_verify_nonce( $nonce, $option_page . '-options' ) ) { return; } $user_id = get_current_user_id(); if ( $user_id <= 0 ) { return; } $notice = array( 'type' => 'success', 'message' => __( 'Settings saved.', 'advanced-passkey-login' ), ); set_transient( $this->get_notice_transient_key( $user_id ), $notice, 180 ); } /** * Retrieve and consume any pending save notice for the user. * * @param int $user_id User ID. * @return array|null */ private function consume_save_notice( $user_id ) { if ( $user_id <= 0 ) { return null; } $key = $this->get_notice_transient_key( $user_id ); $notice = get_transient( $key ); if ( false === $notice ) { return null; } delete_transient( $key ); if ( ! is_array( $notice ) || empty( $notice['message'] ) ) { return null; } $type = ! empty( $notice['type'] ) ? sanitize_key( $notice['type'] ) : 'success'; if ( ! in_array( $type, array( 'success', 'error', 'warning', 'info' ), true ) ) { $type = 'success'; } return array( 'type' => $type, 'message' => wp_kses_post( $notice['message'] ), ); } /** * 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 ( 'dashboard' !== $active_tab ) { 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 ); } $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_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(); $user_id = get_current_user_id(); $queued_notices = array(); $notice_source = 'none'; $core_settings_errors = get_settings_errors(); foreach ( $core_settings_errors as $notice ) { if ( empty( $notice['message'] ) ) { continue; } $type = ! empty( $notice['type'] ) ? sanitize_key( $notice['type'] ) : 'info'; if ( 'updated' === $type ) { $type = 'success'; } if ( ! in_array( $type, array( 'success', 'error', 'warning', 'info' ), true ) ) { $type = 'info'; } $queued_notices[] = array( 'type' => $type, 'message' => wp_kses_post( $notice['message'] ), ); } if ( ! empty( $queued_notices ) ) { $notice_source = 'core_settings_errors'; } $transient_present = false; if ( $user_id > 0 ) { $transient_present = false !== get_transient( $this->get_notice_transient_key( $user_id ) ); } if ( empty( $queued_notices ) ) { $save_notice = $this->consume_save_notice( $user_id ); if ( ! empty( $save_notice ) ) { $queued_notices[] = $save_notice; $notice_source = 'transient'; } } $notice_debug = filter_input( INPUT_GET, 'advapafo_notice_debug', FILTER_SANITIZE_NUMBER_INT ); $raw_debug_nonce = filter_input( INPUT_GET, 'advapafo_notice_debug_nonce', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); $debug_nonce_valid = false; if ( is_string( $raw_debug_nonce ) && '' !== $raw_debug_nonce ) { $debug_nonce = sanitize_text_field( $raw_debug_nonce ); $debug_nonce_valid = wp_verify_nonce( $debug_nonce, 'advapafo_notice_debug' ); } $show_debug = current_user_can( 'manage_options' ) && is_string( $notice_debug ) && '1' === $notice_debug && $debug_nonce_valid; $debug_payload = array(); if ( $show_debug ) { $request_method_debug = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : ''; $debug_payload = array( 'method' => $request_method_debug, 'page' => $this->page_slug, 'tab' => $active_tab, 'settings_updated_get' => '(not read)', 'core_errors_count' => count( $core_settings_errors ), 'transient_present' => $transient_present ? 'yes' : 'no', 'queued_notices_count' => count( $queued_notices ), 'notice_source' => $notice_source, 'user_id' => $user_id, ); } ?>

render_dashboard_tab(); ?> render_shortcodes_tab(); ?>
option_group ); ?> render_preserved_hidden_fields( $active_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', '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; } /** * Output hidden fields to preserve values from non-active tabs. * * @param string $active_tab Active tab key. */ private function render_preserved_hidden_fields( $active_tab ) { if ( 'advanced' === $active_tab ) { $enabled = (bool) advapafo_get_setting( 'enabled', true ); $show_setup_notice = (bool) advapafo_get_setting( 'show_setup_notice', true ); $integration_settings = class_exists( 'ADVAPAFO_Integration_Manager' ) && method_exists( 'ADVAPAFO_Integration_Manager', 'get_settings_registry' ) ? ADVAPAFO_Integration_Manager::get_settings_registry() : array(); $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' ); echo ''; echo ''; foreach ( $integration_settings as $integration_setting ) { if ( empty( $integration_setting['master_option'] ) ) { continue; } $master_option = sanitize_key( (string) $integration_setting['master_option'] ); $dependency_active = ! empty( $integration_setting['dependency_active'] ); $master_value = $dependency_active ? (bool) advapafo_get_setting( $master_option, ! empty( $integration_setting['default_master'] ) ) : false; echo ''; } foreach ( $roles as $role ) { echo ''; } echo ''; echo ''; return; } if ( 'settings' === $active_tab ) { $show_separator = (bool) advapafo_get_setting( 'show_separator', true ); $conditional_ui_enabled = (bool) advapafo_get_setting( 'conditional_ui_enabled', 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 ) ); echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; } } /** * 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' ); ?>

$role ) : ?>
/>

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(); $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; } /** * 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 .= ''; } $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 ); 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 ) ) ); } }