$__args Variables to expose to template. */ function advapafo_template_render_scope( string $__template_path, array $__args = array() ): void { if ( ! empty( $__args ) ) { extract( $__args, EXTR_SKIP ); // phpcs:ignore WordPress.PHP.DontExtract.extract_extract -- template views intentionally receive arg keys as local variables. } include $__template_path; } } if ( ! function_exists( 'advapafo_is_template_name_safe' ) ) { /** * Validate relative template name and guard against traversal. * * @param string $template_name Relative template path. * @return bool */ function advapafo_is_template_name_safe( string $template_name ): bool { if ( '' === $template_name ) { return false; } $normalized = wp_normalize_path( trim( $template_name ) ); if ( '' === $normalized ) { return false; } if ( str_starts_with( $normalized, '/' ) || str_starts_with( $normalized, '\\' ) ) { return false; } if ( preg_match( '#(^|/|\\\\)\.\.($|/|\\\\)#', $normalized ) ) { return false; } if ( str_contains( $normalized, "\0" ) ) { return false; } return true; } } if ( ! function_exists( 'advapafo_is_template_path_allowed' ) ) { /** * Ensure a resolved template path stays within known allowed roots. * * @param string $resolved_path Absolute template path. * @param array $allowed_roots Allowed absolute root directories. * @return bool */ function advapafo_is_template_path_allowed( string $resolved_path, array $allowed_roots ): bool { $real_file = realpath( $resolved_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_realpath -- realpath is required to validate canonical include paths. if ( false === $real_file ) { return false; } $real_file = wp_normalize_path( $real_file ); foreach ( $allowed_roots as $root ) { $real_root = realpath( $root ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_realpath -- canonical path comparison for traversal protection. if ( false === $real_root ) { continue; } $real_root = wp_normalize_path( $real_root ); if ( str_starts_with( $real_file, trailingslashit( $real_root ) ) || $real_file === $real_root ) { return true; } } return false; } } if ( ! function_exists( 'advapafo_get_template' ) ) { /** * Locate and render a plugin template with child/parent theme overrides. * * Search order: * 1) Child theme: /advanced-passkeys/{template} * 2) Parent theme: /advanced-passkeys/{template} * 3) Plugin fallback: /templates/{template} * * @param string $template_name Relative template path (e.g. login/button.php). * @param array $args Optional variables available in the template scope. * @return string Absolute resolved template path, or empty string when not found/invalid. */ function advapafo_get_template( $template_name, $args = array() ) { $template_name = is_string( $template_name ) ? wp_normalize_path( trim( $template_name ) ) : ''; $args = is_array( $args ) ? $args : array(); /** * Filter template request context before template resolution. * * @param array{template_name:string,args:array,theme_root:string,plugin_root:string} $context Template context. */ $context = apply_filters( 'advapafo_get_template_part', array( 'template_name' => $template_name, 'args' => $args, 'theme_root' => 'advanced-passkeys', 'plugin_root' => trailingslashit( ADVAPAFO_PLUGIN_DIR ) . 'templates', ) ); if ( ! is_array( $context ) ) { return ''; } $template_name = isset( $context['template_name'] ) && is_string( $context['template_name'] ) ? wp_normalize_path( trim( $context['template_name'] ) ) : ''; $args = isset( $context['args'] ) && is_array( $context['args'] ) ? $context['args'] : array(); $theme_root = isset( $context['theme_root'] ) && is_string( $context['theme_root'] ) && '' !== trim( $context['theme_root'] ) ? trim( $context['theme_root'], '/\\' ) : 'advanced-passkeys'; $plugin_root = isset( $context['plugin_root'] ) && is_string( $context['plugin_root'] ) && '' !== trim( $context['plugin_root'] ) ? wp_normalize_path( untrailingslashit( trim( $context['plugin_root'] ) ) ) : wp_normalize_path( untrailingslashit( trailingslashit( ADVAPAFO_PLUGIN_DIR ) . 'templates' ) ); if ( ! advapafo_is_template_name_safe( $template_name ) ) { return ''; } $relative_theme_path = trailingslashit( $theme_root ) . ltrim( $template_name, '/\\' ); $located_template = locate_template( array( $relative_theme_path ), false, false ); if ( ! is_string( $located_template ) || '' === $located_template ) { $located_template = wp_normalize_path( trailingslashit( $plugin_root ) . ltrim( $template_name, '/\\' ) ); } if ( ! is_string( $located_template ) || '' === $located_template || ! file_exists( $located_template ) ) { return ''; } $allowed_roots = array( get_stylesheet_directory() . '/' . $theme_root, get_template_directory() . '/' . $theme_root, $plugin_root, ); if ( ! advapafo_is_template_path_allowed( $located_template, $allowed_roots ) ) { return ''; } advapafo_template_render_scope( $located_template, $args ); return $located_template; } } // Allow env-based constant injection (same pattern used in planning-center-sso). // phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.VariableConstantNameFound -- dynamic constant names are constrained to ADVAPAFO_* entries in this allowlist. foreach ( array( 'ADVAPAFO_ALLOW_HTTP', 'ADVAPAFO_RP_ID', 'ADVAPAFO_RP_NAME', 'ADVAPAFO_CHALLENGE_TTL', 'ADVAPAFO_USER_VERIFICATION', 'ADVAPAFO_RATE_WINDOW', 'ADVAPAFO_RATE_MAX_ATTEMPTS', 'ADVAPAFO_RATE_LOCKOUT', 'ADVAPAFO_ENABLE_LOGGING', 'ADVAPAFO_SETTINGS', ) as $advapafo_env_const ) { if ( ! defined( $advapafo_env_const ) ) { $advapafo_env_value = getenv( $advapafo_env_const ); if ( false !== $advapafo_env_value && '' !== $advapafo_env_value ) { define( $advapafo_env_const, $advapafo_env_value ); } } } // phpcs:enable WordPress.NamingConventions.PrefixAllGlobals.VariableConstantNameFound unset( $advapafo_env_const, $advapafo_env_value ); /** * Map logical setting keys to stored option names and defaults. * * @return array */ function advapafo_get_setting_registry(): array { return array( 'enabled' => array( 'option' => 'advapafo_enabled', 'default' => true, ), 'show_separator' => array( 'option' => 'advapafo_show_separator', 'default' => true, ), 'conditional_ui_enabled' => array( 'option' => 'advapafo_conditional_ui_enabled', 'default' => false, ), 'show_setup_notice' => array( 'option' => 'advapafo_show_setup_notice', 'default' => true, ), 'eligible_roles' => array( 'option' => 'advapafo_eligible_roles', 'default' => array( 'administrator' ), ), 'eligible_user' => array( 'option' => '', 'default' => null, ), 'max_passkeys_per_user' => array( 'option' => 'advapafo_max_passkeys_per_user', 'default' => 0, ), 'user_verification' => array( 'option' => 'advapafo_user_verification', 'default' => 'required', ), 'button_style' => array( 'option' => 'advapafo_button_style', 'default' => 'black', ), 'rp_name' => array( 'option' => 'advapafo_rp_name', 'default' => '', ), 'rp_id' => array( 'option' => 'advapafo_rp_id', 'default' => '', ), 'challenge_ttl' => array( 'option' => 'advapafo_challenge_ttl', 'default' => 300, ), 'login_challenge_ttl' => array( 'option' => 'advapafo_login_challenge_ttl', 'default' => 300, ), 'registration_challenge_ttl' => array( 'option' => 'advapafo_registration_challenge_ttl', 'default' => 300, ), 'rate_limit_window' => array( 'option' => 'advapafo_rate_limit_window', 'default' => 300, ), 'rate_window' => array( 'option' => 'advapafo_rate_window', 'default' => 300, ), 'rate_limit_max_failures' => array( 'option' => 'advapafo_rate_limit_max_failures', 'default' => 5, ), 'rate_max_attempts' => array( 'option' => 'advapafo_rate_max_attempts', 'default' => 5, ), 'rate_limit_lockout' => array( 'option' => 'advapafo_rate_limit_lockout', 'default' => 900, ), 'rate_lockout' => array( 'option' => 'advapafo_rate_lockout', 'default' => 900, ), 'activity_logging_enabled' => array( 'option' => 'advapafo_activity_logging_enabled', 'default' => true, ), 'log_retention_days' => array( 'option' => 'advapafo_log_retention_days', 'default' => 90, ), 'login_redirect' => array( 'option' => 'advapafo_login_redirect', 'default' => '', ), 'username_autocomplete_value' => array( 'option' => '', 'default' => 'username webauthn', ), 'passkey_allow_session_establishment' => array( 'option' => '', 'default' => true, ), 'passkey_remember_me' => array( 'option' => '', 'default' => false, ), 'passkey_emit_wp_login_failed' => array( 'option' => '', 'default' => false, ), 'last_used_pill_label' => array( 'option' => '', 'default' => '', ), 'last_used_pill_freshness_days' => array( 'option' => '', 'default' => 90, ), 'last_used_pill_visible' => array( 'option' => '', 'default' => null, ), 'authenticator_provider_label' => array( 'option' => '', 'default' => '', ), 'authenticator_aaguid_map' => array( 'option' => '', 'default' => array(), ), 'enforce_https' => array( 'option' => 'advapafo_enforce_https', 'default' => true, ), 'enable_woocommerce_support' => array( 'option' => 'advapafo_enable_woocommerce_support', 'default' => true, ), 'enable_edd_support' => array( 'option' => 'advapafo_enable_edd_support', 'default' => true, ), 'enable_memberpress_support' => array( 'option' => 'advapafo_enable_memberpress_support', 'default' => true, ), 'enable_ultimate_member_support' => array( 'option' => 'advapafo_enable_ultimate_member_support', 'default' => true, ), 'enable_learndash_support' => array( 'option' => 'advapafo_enable_learndash_support', 'default' => true, ), 'enable_buddyboss_support' => array( 'option' => 'advapafo_enable_buddyboss_support', 'default' => true, ), 'enable_gravityforms_support' => array( 'option' => 'advapafo_enable_gravityforms_support', 'default' => true, ), 'enable_pmp_support' => array( 'option' => 'advapafo_enable_pmp_support', 'default' => true, ), ); } /** * Normalize a setting key from plugin option names or local configuration keys. * * @param string $setting_key Setting key. * @return string */ function advapafo_normalize_setting_key( $setting_key ): string { $setting_key = sanitize_key( (string) $setting_key ); if ( str_starts_with( $setting_key, 'advapafo_' ) ) { $setting_key = substr( $setting_key, 9 ); } return $setting_key; } /** * Normalize local configuration arrays from filters or constants. * * @param mixed $configuration Raw configuration value. * @return array */ function advapafo_normalize_local_configuration( $configuration ): array { if ( is_string( $configuration ) ) { $maybe_unserialized = maybe_unserialize( $configuration ); if ( is_array( $maybe_unserialized ) ) { $configuration = $maybe_unserialized; } else { $maybe_json = json_decode( $configuration, true ); $configuration = is_array( $maybe_json ) ? $maybe_json : array(); } } if ( ! is_array( $configuration ) ) { return array(); } $normalized = array(); foreach ( $configuration as $key => $value ) { $normalized_key = advapafo_normalize_setting_key( (string) $key ); if ( '' !== $normalized_key ) { $normalized[ $normalized_key ] = $value; } } return $normalized; } /** * Get the effective local configuration supplied by PHP code. * * @return array */ function advapafo_get_local_configuration(): array { $filter_configuration = advapafo_normalize_local_configuration( apply_filters( 'advapafo_local_configuration', array() ) ); $constant_configuration = defined( 'ADVAPAFO_SETTINGS' ) ? advapafo_normalize_local_configuration( ADVAPAFO_SETTINGS ) : array(); return array( 'filter' => $filter_configuration, 'constant' => $constant_configuration, ); } /** * Check whether a specific setting is dictated by local PHP configuration. * * @param string $setting_key Setting key. * @return bool */ function advapafo_is_setting_overridden( $setting_key ): bool { $setting_key = advapafo_normalize_setting_key( $setting_key ); $configuration = advapafo_get_local_configuration(); return array_key_exists( $setting_key, $configuration['filter'] ) || array_key_exists( $setting_key, $configuration['constant'] ); } /** * Apply legacy configuration filters from a single compatibility layer. * * Local configuration and constants intentionally bypass these legacy filters. * This keeps code-level deployments deterministic while preserving existing * extension callbacks for sites that already use them. * * @param string $setting_key Setting key. * @param mixed $value Current resolved value. * @param array $context Optional contextual values. * @return mixed */ function advapafo_apply_legacy_setting_filter( string $setting_key, $value, array $context = array() ) { $user = $context['user'] ?? null; $credential_hash = isset( $context['credential_hash'] ) ? (string) $context['credential_hash'] : ''; $reason = isset( $context['reason'] ) ? (string) $context['reason'] : ''; switch ( $setting_key ) { case 'conditional_ui_enabled': return apply_filters( 'advapafo_enable_conditional_ui', (bool) $value ); case 'eligible_user': return $user instanceof WP_User ? apply_filters( 'advapafo_is_eligible_user', $value, $user ) : $value; case 'max_passkeys_per_user': $filtered = $user instanceof WP_User ? apply_filters( 'advapafo_max_passkeys_per_user', null, $user ) : null; return null !== $filtered ? $filtered : $value; case 'username_autocomplete_value': return apply_filters( 'advapafo_username_autocomplete_value', (string) $value ); case 'passkey_allow_session_establishment': return $user instanceof WP_User ? apply_filters( 'advapafo_passkey_allow_session_establishment', (bool) $value, $user, $credential_hash ) : $value; case 'passkey_remember_me': return $user instanceof WP_User ? apply_filters( 'advapafo_passkey_remember_me', (bool) $value, $user ) : $value; case 'login_redirect': return $user instanceof WP_User ? apply_filters( 'advapafo_login_redirect', (string) $value, $user ) : $value; case 'passkey_emit_wp_login_failed': return $user instanceof WP_User ? apply_filters( 'advapafo_passkey_emit_wp_login_failed', (bool) $value, $user, $credential_hash, $reason ) : $value; case 'last_used_pill_label': return apply_filters( 'advapafo_last_used_pill_label', (string) $value, $user instanceof WP_User ? $user : null ); case 'last_used_pill_freshness_days': return apply_filters( 'advapafo_last_used_pill_freshness_days', (int) $value, $user instanceof WP_User ? $user : null ); case 'last_used_pill_visible': return apply_filters( 'advapafo_last_used_pill_visible', (bool) $value, isset( $context['timestamp'] ) ? (int) $context['timestamp'] : 0, isset( $context['freshness_days'] ) ? (int) $context['freshness_days'] : 0, $user instanceof WP_User ? $user : null ); case 'authenticator_provider_label': return apply_filters( 'advapafo_authenticator_provider_label', (string) $value, isset( $context['provider'] ) ? (string) $context['provider'] : '', isset( $context['label'] ) ? (string) $context['label'] : '' ); case 'authenticator_aaguid_map': return apply_filters( 'advapafo_authenticator_aaguid_map', is_array( $value ) ? $value : array() ); } return $value; } // phpcs:disable Universal.NamingConventions.NoReservedKeywordParameterNames.defaultFound -- Public API intentionally uses the documented $default getter argument. /** * Get one setting using local code overrides before database fallback. * * Evaluation order: advapafo_local_configuration filter, ADVAPAFO_SETTINGS * constant, advapafo_settings option array, mapped legacy option, default. * * @param string $setting_key Setting key, with or without advapafo_ prefix. * @param mixed $default Default value. * @param array $context Optional contextual values and lookup flags. * @return mixed */ function advapafo_get_setting( $setting_key, $default = null, array $context = array() ) { $setting_key = advapafo_normalize_setting_key( $setting_key ); if ( '' === $setting_key ) { return $default; } $registry = advapafo_get_setting_registry(); $registered = $registry[ $setting_key ] ?? null; $resolved_default = null !== $default ? $default : ( $registered['default'] ?? null ); $configuration = advapafo_get_local_configuration(); $apply_legacy = ! array_key_exists( 'apply_legacy', $context ) || (bool) $context['apply_legacy']; $skip_database = ! empty( $context['skip_database'] ); if ( array_key_exists( $setting_key, $configuration['filter'] ) ) { return $configuration['filter'][ $setting_key ]; } if ( array_key_exists( $setting_key, $configuration['constant'] ) ) { return $configuration['constant'][ $setting_key ]; } $resolved_value = $resolved_default; if ( ! $skip_database ) { $settings_option = get_option( 'advapafo_settings', array() ); if ( is_array( $settings_option ) ) { $settings_option = advapafo_normalize_local_configuration( $settings_option ); if ( array_key_exists( $setting_key, $settings_option ) ) { $resolved_value = $settings_option[ $setting_key ]; return $apply_legacy ? advapafo_apply_legacy_setting_filter( $setting_key, $resolved_value, $context ) : $resolved_value; } } if ( ! empty( $registered['option'] ) ) { $resolved_value = get_option( $registered['option'], $resolved_default ); } } return $apply_legacy ? advapafo_apply_legacy_setting_filter( $setting_key, $resolved_value, $context ) : $resolved_value; } // phpcs:enable Universal.NamingConventions.NoReservedKeywordParameterNames.defaultFound /** * Render the admin badge used beside code-managed settings. * * @param string|array $setting_key Setting key or related keys. */ function advapafo_render_managed_setting_badge( $setting_key ): void { $setting_keys = (array) $setting_key; $is_managed = false; foreach ( $setting_keys as $key ) { if ( advapafo_is_setting_overridden( (string) $key ) ) { $is_managed = true; break; } } if ( ! $is_managed ) { return; } echo '' . esc_html__( 'Managed via code', 'advanced-passkey-login' ) . ''; } // ────────────────────────────────────────────────────────────── // Composer autoload (lbuchs/webauthn) // ────────────────────────────────────────────────────────────── $advapafo_autoload = ADVAPAFO_PLUGIN_DIR . 'vendor/autoload.php'; if ( PHP_VERSION_ID >= 80000 && file_exists( $advapafo_autoload ) ) { $advapafo_should_load_autoloader = true; $advapafo_autoload_real = ADVAPAFO_PLUGIN_DIR . 'vendor/composer/autoload_real.php'; if ( file_exists( $advapafo_autoload_real ) ) { $advapafo_autoload_real_src = file_get_contents( $advapafo_autoload_real ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- local filesystem read for composer class detection. if ( is_string( $advapafo_autoload_real_src ) && preg_match( '/class\s+(ComposerAutoloaderInit[0-9a-fA-F_]+)/', $advapafo_autoload_real_src, $m ) ) { if ( ! empty( $m[1] ) && class_exists( (string) $m[1], false ) ) { $advapafo_should_load_autoloader = false; } } } if ( $advapafo_should_load_autoloader ) { require_once $advapafo_autoload; } } unset( $advapafo_autoload, $advapafo_should_load_autoloader, $advapafo_autoload_real, $advapafo_autoload_real_src ); /** * Run one-time option normalization tasks for advapafo_* keys. */ function advapafo_migrate_legacy_options_once(): void { if ( (int) get_option( 'advapafo_legacy_options_migrated_v1', 0 ) === 1 ) { return; } // Legacy free builds commonly stored a default cap of 5; move to unlimited. $legacy_cap = (int) get_option( 'advapafo_max_passkeys_per_user', 0 ); if ( 5 === $legacy_cap ) { update_option( 'advapafo_max_passkeys_per_user', 0 ); } update_option( 'advapafo_legacy_options_migrated_v1', 1, false ); } /** * Ensure passkey cap defaults to unlimited for existing installs migrated earlier. */ function advapafo_remove_legacy_passkey_cap_once(): void { if ( (int) get_option( 'advapafo_cap_migrated_v1', 0 ) === 1 ) { return; } if ( (int) get_option( 'advapafo_max_passkeys_per_user', 0 ) === 5 ) { update_option( 'advapafo_max_passkeys_per_user', 0 ); } update_option( 'advapafo_cap_migrated_v1', 1, false ); } // ────────────────────────────────────────────────────────────── // Load classes // ────────────────────────────────────────────────────────────── require_once ADVAPAFO_PLUGIN_DIR . 'includes/class-advapafo-passkeys.php'; require_once ADVAPAFO_PLUGIN_DIR . 'includes/class-advapafo-settings.php'; require_once ADVAPAFO_PLUGIN_DIR . 'includes/class-advapafo-login-form.php'; require_once ADVAPAFO_PLUGIN_DIR . 'includes/class-advapafo-shortcodes.php'; require_once ADVAPAFO_PLUGIN_DIR . 'includes/class-advapafo-integration-manager.php'; // ────────────────────────────────────────────────────────────── // Bootstrap // ────────────────────────────────────────────────────────────── /** * Initialize plugin services after core/plugin load. */ function advapafo_init() { advapafo_migrate_legacy_options_once(); advapafo_remove_legacy_passkey_cap_once(); new ADVAPAFO_Passkeys(); new ADVAPAFO_Login_Form(); new ADVAPAFO_Shortcodes(); new ADVAPAFO_Integration_Manager(); if ( is_admin() ) { new ADVAPAFO_Settings(); } } add_action( 'plugins_loaded', 'advapafo_init' ); /** * Detect whether the plugin is network-activated. */ function advapafo_is_network_active(): bool { if ( ! is_multisite() ) { return false; } $active = (array) get_site_option( 'active_sitewide_plugins', array() ); return isset( $active[ plugin_basename( ADVAPAFO_PLUGIN_FILE ) ] ); } /** * Run a callback for current site or across the whole network. * * @param bool $network_wide Whether the plugin is network-activated. * @param callable $callback Callback to run for each relevant site. */ function advapafo_for_each_site( bool $network_wide, callable $callback ): void { if ( ! is_multisite() || ! $network_wide ) { $callback(); return; } $original_blog_id = get_current_blog_id(); $page = 1; $per_page = 200; do { $site_ids = get_sites( array( 'fields' => 'ids', 'number' => $per_page, 'paged' => $page, ) ); foreach ( $site_ids as $site_id ) { switch_to_blog( (int) $site_id ); $callback(); restore_current_blog(); } ++$page; } while ( ! empty( $site_ids ) ); if ( get_current_blog_id() !== (int) $original_blog_id ) { switch_to_blog( (int) $original_blog_id ); restore_current_blog(); } } // ────────────────────────────────────────────────────────────── // Activation / deactivation // ────────────────────────────────────────────────────────────── register_activation_hook( __FILE__, 'advapafo_activate' ); register_deactivation_hook( __FILE__, 'advapafo_deactivate' ); /** * Activation routine. * * @param bool $network_wide Whether plugin is being activated network-wide. */ function advapafo_activate( bool $network_wide = false ) { if ( version_compare( PHP_VERSION, '8.0', '<' ) ) { deactivate_plugins( plugin_basename( ADVAPAFO_PLUGIN_FILE ) ); wp_die( esc_html__( 'Advanced Passkeys for Secure Login requires PHP 8.0 or higher. Please upgrade PHP before activating this plugin.', 'advanced-passkey-login' ) ); } if ( version_compare( $GLOBALS['wp_version'], '6.0', '<' ) ) { deactivate_plugins( plugin_basename( ADVAPAFO_PLUGIN_FILE ) ); wp_die( esc_html__( 'Advanced Passkeys for Secure Login requires WordPress 6.0 or higher. Please update WordPress before activating this plugin.', 'advanced-passkey-login' ) ); } require_once ADVAPAFO_PLUGIN_DIR . 'includes/class-advapafo-passkeys.php'; advapafo_for_each_site( $network_wide, static function (): void { ADVAPAFO_Passkeys::create_tables(); ADVAPAFO_Passkeys::schedule_cron(); } ); flush_rewrite_rules(); } /** * Deactivation routine. * * @param bool $network_wide Whether plugin is being deactivated network-wide. */ function advapafo_deactivate( bool $network_wide = false ) { require_once ADVAPAFO_PLUGIN_DIR . 'includes/class-advapafo-passkeys.php'; advapafo_for_each_site( $network_wide, static function (): void { ADVAPAFO_Passkeys::unschedule_cron(); } ); // Nothing else to tear down on deactivation; tables are preserved until uninstall. } /** * Provision plugin tables/cron when a new site is created on multisite. * * @param WP_Site $new_site The newly provisioned site. */ function advapafo_multisite_initialize_site( WP_Site $new_site ): void { if ( ! advapafo_is_network_active() ) { return; } require_once ADVAPAFO_PLUGIN_DIR . 'includes/class-advapafo-passkeys.php'; switch_to_blog( (int) $new_site->blog_id ); ADVAPAFO_Passkeys::create_tables(); ADVAPAFO_Passkeys::schedule_cron(); restore_current_blog(); } add_action( 'wp_initialize_site', 'advapafo_multisite_initialize_site' ); // ────────────────────────────────────────────────────────────── // Settings link on Plugins page // ────────────────────────────────────────────────────────────── add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), function ( $links ) { $url = admin_url( 'options-general.php?page=advanced-passkey-login' ); array_unshift( $links, sprintf( '%s', esc_url( $url ), esc_html__( 'Settings', 'advanced-passkey-login' ) ) ); return $links; } ); add_filter( 'plugin_row_meta', function ( $links, $file ) { if ( plugin_basename( __FILE__ ) !== $file ) { return $links; } $links[] = sprintf( '%s', esc_url( 'https://wordpress.org/plugins/advanced-passkey-login/' ), esc_html__( 'Plugin Page', 'advanced-passkey-login' ) ); return $links; }, 10, 2 ); // ────────────────────────────────────────────────────────────── // Security notice for dev-only flags // ────────────────────────────────────────────────────────────── add_action( 'admin_notices', function () { if ( ! is_admin() || ! current_user_can( 'manage_options' ) ) { return; } $warnings = array(); if ( defined( 'ADVAPAFO_ALLOW_HTTP' ) && ADVAPAFO_ALLOW_HTTP ) { $warnings[] = 'ADVAPAFO_ALLOW_HTTP is enabled — insecure transport is allowed. Disable in production.'; } if ( empty( $warnings ) ) { return; } echo '

Advanced Passkeys for Secure Login security warning:

    '; foreach ( $warnings as $w ) { echo '
  • ' . wp_kses( $w, array( 'strong' => array() ) ) . '
  • '; } echo '
'; } );