'array', 'sanitize_callback' => array( $this, 'sanitize_options' ), 'default' => Adminvoro::get_default_options(), ) ); } /** * Enqueue admin assets. * * @param string $hook Page hook. * @return void */ public function enqueue_assets( $hook ) { if ( 'toplevel_page_' . self::MENU_SLUG !== $hook ) { return; } wp_enqueue_media(); wp_enqueue_style( 'adminvoro-admin', ADMINVORO_URL . 'assets/css/admin.css', array(), ADMINVORO_VERSION ); wp_enqueue_script( 'adminvoro-admin', ADMINVORO_URL . 'assets/js/admin.js', array( 'jquery' ), ADMINVORO_VERSION, true ); wp_localize_script( 'adminvoro-admin', 'adminvoroSettingsAdmin', array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'adminvoro_ajax_save' ), 'chooseLogo' => esc_html__( 'Choose login logo', 'adminvoro' ), 'useLogo' => esc_html__( 'Use this logo', 'adminvoro' ), 'noLogo' => esc_html__( 'No logo selected', 'adminvoro' ), 'saving' => esc_html__( 'Saving...', 'adminvoro' ), 'saveFailed' => esc_html__( 'Settings could not be saved. Please refresh and try again.', 'adminvoro' ), 'ajaxError' => esc_html__( 'A network error prevented saving. Please try again.', 'adminvoro' ), ) ); } /** * Add settings link on Plugins screen. * * @param array $links Plugin action links. * @return array */ public function add_settings_link( $links ) { $settings_link = sprintf( '%2$s', esc_url( admin_url( 'admin.php?page=' . self::MENU_SLUG ) ), esc_html__( 'Settings', 'adminvoro' ) ); array_unshift( $links, $settings_link ); return $links; } /** * Sanitize primary options. * * @param array $input Submitted settings. * @return array */ public function sanitize_options( $input ) { $input = is_array( $input ) ? wp_unslash( $input ) : array(); $existing = Adminvoro::get_options(); $output = $existing; $tab = isset( $input['active_tab'] ) ? sanitize_key( $input['active_tab'] ) : ''; switch ( $tab ) { case 'login-security': $output = $this->sanitize_login_security_options( $input, $existing, $output ); break; case 'login-branding': $output = $this->sanitize_login_branding_options( $input, $output ); break; case 'security': $output['disable_xmlrpc'] = empty( $input['disable_xmlrpc'] ) ? 0 : 1; $output['disable_user_enumeration'] = empty( $input['disable_user_enumeration'] ) ? 0 : 1; $output['hide_wp_version'] = empty( $input['hide_wp_version'] ) ? 0 : 1; $output['block_public_files'] = empty( $input['block_public_files'] ) ? 0 : 1; if ( ! empty( $existing['block_public_files'] ) !== ! empty( $output['block_public_files'] ) ) { add_action( 'shutdown', array( $this, 'flush_rewrite_rules_hard' ) ); } break; case 'performance': $output['disable_emojis'] = empty( $input['disable_emojis'] ) ? 0 : 1; $output['disable_embeds'] = empty( $input['disable_embeds'] ) ? 0 : 1; break; case 'admin-branding': $output['enable_admin_footer_text'] = empty( $input['enable_admin_footer_text'] ) ? 0 : 1; $output['custom_admin_footer_text'] = isset( $input['custom_admin_footer_text'] ) ? wp_kses_post( $input['custom_admin_footer_text'] ) : ''; break; case 'data-management': $output['delete_data_on_uninstall'] = empty( $input['delete_data_on_uninstall'] ) ? 0 : 1; break; } return wp_parse_args( $output, Adminvoro::get_default_options() ); } /** * Save primary plugin options through AJAX. * * @return void */ public function ajax_save_options() { if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'notices' => $this->render_notice_html( esc_html__( 'You do not have permission to manage Adminvoro Toolkit.', 'adminvoro' ), 'error' ), ), 403 ); } check_ajax_referer( 'adminvoro_ajax_save', 'nonce' ); $input = isset( $_POST[ ADMINVORO_OPTION ] ) && is_array( $_POST[ ADMINVORO_OPTION ] ) ? wp_unslash( $_POST[ ADMINVORO_OPTION ] ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $tab = isset( $input['active_tab'] ) ? sanitize_key( wp_unslash( $input['active_tab'] ) ) : ''; if ( '' === $tab ) { wp_send_json_error( array( 'notices' => $this->render_notice_html( esc_html__( 'Settings could not be saved because the active tab was missing.', 'adminvoro' ), 'error' ), ), 400 ); } $this->clear_settings_errors(); $options = $this->sanitize_options( $input ); $this->update_options_without_resanitizing( $options ); $errors = get_settings_errors( ADMINVORO_OPTION ); $has_errors = $this->settings_errors_have_errors( $errors ); $notices = $this->render_settings_errors_html( $errors ); if ( ! $has_errors ) { $notices .= $this->render_notice_html( $this->get_success_message_for_tab( $tab, $options ), 'success' ); } wp_send_json_success( array( 'notices' => $notices, 'options' => $options, 'currentLoginHtml' => $this->get_current_login_notice_html_improved( $options ), 'logoUrl' => $this->get_logo_preview_url( $options ), ) ); } /** * Persist already-sanitized AJAX options without running the Settings API sanitizer again. * * The registered sanitize callback expects an active_tab marker. AJAX saves sanitize first, * then store the final option array, so a second sanitize pass would otherwise restore * the previous database value. * * @param array $options Sanitized plugin options. * @return void */ private function update_options_without_resanitizing( $options ) { remove_filter( 'sanitize_option_' . ADMINVORO_OPTION, array( $this, 'sanitize_options' ), 10 ); update_option( ADMINVORO_OPTION, $options ); add_filter( 'sanitize_option_' . ADMINVORO_OPTION, array( $this, 'sanitize_options' ), 10, 1 ); } /** * Save redirects from the custom redirects form. * * @return void */ public function save_redirects() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to manage Adminvoro Toolkit.', 'adminvoro' ) ); } check_admin_referer( 'adminvoro_save_redirects' ); $result = $this->process_redirect_save(); $this->set_admin_notice( $result['message'], $result['type'] ); wp_safe_redirect( add_query_arg( array( 'page' => self::MENU_SLUG, 'tab' => 'redirects' ), admin_url( 'admin.php' ) ) ); exit; } /** * Save redirects through AJAX. * * @return void */ public function ajax_save_redirects() { if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'notices' => $this->render_notice_html( esc_html__( 'You do not have permission to manage Adminvoro Toolkit.', 'adminvoro' ), 'error' ), ), 403 ); } check_ajax_referer( 'adminvoro_ajax_save', 'nonce' ); $result = $this->process_redirect_save(); wp_send_json_success( array( 'notices' => $this->render_notice_html( $result['message'], $result['type'] ), ) ); } /** * Process submitted redirect rows. * * @return array */ private function process_redirect_save() { $rows = isset( $_POST['adminvoro_redirects'] ) && is_array( $_POST['adminvoro_redirects'] ) ? wp_unslash( $_POST['adminvoro_redirects'] ) : array(); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $redirects = Adminvoro_Redirects::sanitize_redirect_rows( $rows ); $submitted = $this->count_submitted_redirect_rows( $rows ); $skipped = max( 0, $submitted - count( $redirects ) ); update_option( ADMINVORO_REDIRECTS_OPTION, $redirects ); if ( $skipped > 0 ) { return array( 'message' => sprintf( /* translators: 1: Number of redirects saved. 2: Number of redirects skipped. */ __( '%1$d redirect(s) saved. %2$d invalid row(s) skipped.', 'adminvoro' ), count( $redirects ), $skipped ), 'type' => 'warning', ); } return array( 'message' => sprintf( /* translators: %d: Number of redirects saved. */ _n( '%d redirect saved.', '%d redirects saved.', count( $redirects ), 'adminvoro' ), count( $redirects ) ), 'type' => 'success', ); } /** * Render settings page. * * @return void */ public function render_page() { if ( ! current_user_can( 'manage_options' ) ) { return; } $options = Adminvoro::get_options(); $redirects = Adminvoro_Redirects::get_redirects(); $active_tab = $this->get_active_tab(); ?>

display_admin_notice(); ?>
render_login_branding_tab( $options ); break; case 'redirects': $this->render_redirects_tab( $redirects ); break; case 'security': $this->render_security_tab( $options ); break; case 'performance': $this->render_performance_tab( $options ); break; case 'admin-branding': $this->render_admin_branding_tab( $options ); break; case 'data-management': $this->render_data_management_tab( $options ); break; case 'login-security': default: $this->render_login_security_tab( $options ); break; } ?>
sanitize_local_page_url( $input['login_block_custom_url'] ) : ''; if ( 'custom_url' === $output['login_block_action'] && '' === $output['login_block_custom_url'] ) { $output['login_block_action'] = '404'; add_settings_error( ADMINVORO_OPTION, 'adminvoro-invalid-block-url', esc_html__( 'Enter a valid same-site custom page URL before selecting the custom page redirect option.', 'adminvoro' ), 'error' ); } $raw_slug = isset( $input['custom_login_slug'] ) ? trim( sanitize_text_field( $input['custom_login_slug'] ) ) : ''; $raw_slug = trim( $raw_slug, '/' ); $slug = sanitize_title( $raw_slug ); if ( '' !== $raw_slug && preg_match( '#[\\/\\\\]#', $raw_slug ) ) { $slug = ''; add_settings_error( ADMINVORO_OPTION, 'adminvoro-invalid-login-slug', esc_html__( 'The custom login slug cannot contain slashes.', 'adminvoro' ), 'error' ); } if ( '' !== $slug && in_array( $slug, Adminvoro::get_reserved_login_slugs(), true ) ) { $slug = ''; add_settings_error( ADMINVORO_OPTION, 'adminvoro-reserved-login-slug', esc_html__( 'That custom login slug is reserved by WordPress. Choose a different slug.', 'adminvoro' ), 'error' ); } if ( ! empty( $output['enable_custom_login'] ) && '' === $slug ) { $output['enable_custom_login'] = 0; $output['custom_login_slug'] = $old_slug; add_settings_error( ADMINVORO_OPTION, 'adminvoro-login-disabled', esc_html__( 'Custom login protection was not enabled because the login slug is invalid or empty.', 'adminvoro' ), 'error' ); } elseif ( '' !== $slug ) { $output['custom_login_slug'] = $slug; } else { $output['custom_login_slug'] = ''; } if ( $was_enabled !== (bool) $output['enable_custom_login'] || $old_slug !== $output['custom_login_slug'] ) { add_action( 'shutdown', array( $this, 'flush_rewrite_rules' ) ); } return $output; } /** * Sanitize login branding fields. * * @param array $input Submitted settings. * @param array $output Output settings. * @return array */ private function sanitize_login_branding_options( $input, $output ) { if ( ! empty( $input['reset_login_branding'] ) ) { $output['login_logo_id'] = 0; $output['login_logo_url'] = ''; $output['login_logo_text'] = ''; $output['login_background_color'] = ''; $output['login_text_color'] = ''; $output['login_link_color'] = ''; $output['login_logo_text_size'] = 18; return $output; } $output['login_logo_id'] = isset( $input['login_logo_id'] ) ? absint( $input['login_logo_id'] ) : 0; $output['login_logo_url'] = isset( $input['login_logo_url'] ) ? esc_url_raw( $input['login_logo_url'] ) : ''; $output['login_logo_text'] = isset( $input['login_logo_text'] ) ? wp_kses_post( $input['login_logo_text'] ) : ''; $output['login_background_color'] = isset( $input['login_background_color'] ) ? $this->sanitize_hex_color_field( $input['login_background_color'] ) : ''; $output['login_text_color'] = isset( $input['login_text_color'] ) ? $this->sanitize_hex_color_field( $input['login_text_color'] ) : ''; $output['login_link_color'] = isset( $input['login_link_color'] ) ? $this->sanitize_hex_color_field( $input['login_link_color'] ) : ''; $output['login_logo_text_size'] = isset( $input['login_logo_text_size'] ) ? $this->sanitize_login_text_size( $input['login_logo_text_size'] ) : 18; return $output; } /** * Flush rewrite rules after custom login changes. * * @return void */ public function flush_rewrite_rules() { flush_rewrite_rules( false ); } /** * Flush rewrite rules to the web server configuration. * * @return void */ public function flush_rewrite_rules_hard() { flush_rewrite_rules( true ); } /** * Count non-empty submitted redirect rows. * * @param array $rows Submitted redirect rows. * @return int */ private function count_submitted_redirect_rows( $rows ) { $count = 0; foreach ( $rows as $row ) { if ( ! is_array( $row ) || ! empty( $row['delete'] ) ) { continue; } $source = isset( $row['source'] ) ? trim( (string) $row['source'] ) : ''; $destination = isset( $row['destination'] ) ? trim( (string) $row['destination'] ) : ''; if ( '' === $source && '' === $destination ) { continue; } $count++; } return $count; } /** * Sanitize a same-site page URL for wp-login.php block redirects. * * @param mixed $url Submitted URL. * @return string */ private function sanitize_local_page_url( $url ) { if ( ! is_scalar( $url ) ) { return ''; } $url = trim( sanitize_text_field( wp_unslash( $url ) ) ); if ( '' === $url || 0 === strpos( $url, '//' ) ) { return ''; } if ( 0 === strpos( $url, '/' ) ) { return esc_url_raw( $url ); } $parts = wp_parse_url( $url ); $home_host = wp_parse_url( home_url( '/' ), PHP_URL_HOST ); if ( ! is_array( $parts ) || empty( $parts['scheme'] ) || empty( $parts['host'] ) || empty( $home_host ) ) { return ''; } if ( ! in_array( strtolower( $parts['scheme'] ), array( 'http', 'https' ), true ) ) { return ''; } if ( strtolower( $parts['host'] ) !== strtolower( $home_host ) ) { return ''; } return esc_url_raw( $url ); } /** * Sanitize a hex color field. * * @param mixed $color Submitted color. * @return string */ private function sanitize_hex_color_field( $color ) { if ( ! is_scalar( $color ) ) { return ''; } $color = sanitize_hex_color( wp_unslash( $color ) ); return is_string( $color ) ? $color : ''; } /** * Sanitize custom login text size. * * @param mixed $size Submitted size. * @return int */ private function sanitize_login_text_size( $size ) { $size = absint( $size ); if ( $size < 12 ) { return 12; } if ( $size > 48 ) { return 48; } return $size; } /** * Clear collected Settings API errors before an AJAX save. * * @return void */ private function clear_settings_errors() { global $wp_settings_errors; $wp_settings_errors = array(); } /** * Determine whether Settings API messages contain errors. * * @param array $errors Settings API messages. * @return bool */ private function settings_errors_have_errors( $errors ) { foreach ( $errors as $error ) { if ( isset( $error['type'] ) && 'error' === $error['type'] ) { return true; } } return false; } /** * Render Settings API messages as visible notices. * * @param array $errors Settings API messages. * @return string */ private function render_settings_errors_html( $errors ) { $html = ''; foreach ( $errors as $error ) { $type = isset( $error['type'] ) ? sanitize_key( $error['type'] ) : 'info'; $message = isset( $error['message'] ) ? $error['message'] : ''; if ( '' === $message ) { continue; } $html .= $this->render_notice_html( $message, $type ); } return $html; } /** * Render a high-contrast admin notice. * * @param string $message Notice message. * @param string $type Notice type. * @return string */ private function render_notice_html( $message, $type = 'success' ) { if ( 'updated' === $type ) { $type = 'success'; } $type = in_array( $type, array( 'success', 'error', 'warning', 'info' ), true ) ? $type : 'info'; ob_start(); ?>

get_current_login_url( $options ) ) ); } $messages = array( 'login-security' => __( 'Login security settings saved.', 'adminvoro' ), 'login-branding' => __( 'Login branding settings saved.', 'adminvoro' ), 'security' => __( 'Security settings saved.', 'adminvoro' ), 'performance' => __( 'Performance settings saved.', 'adminvoro' ), 'admin-branding' => __( 'Admin branding settings saved.', 'adminvoro' ), 'data-management' => __( 'Data settings saved.', 'adminvoro' ), ); return isset( $messages[ $tab ] ) ? $messages[ $tab ] : __( 'Settings saved.', 'adminvoro' ); } /** * Get current login notice HTML. * * @param array $options Plugin options. * @return string */ private function get_current_login_notice_html( $options ) { ob_start(); if ( Adminvoro::is_custom_login_disabled() ) : ?>
get_current_login_url( $options ); ?>

render_toggle( ADMINVORO_OPTION . '[enable_custom_login]', 'enable_custom_login', ! empty( $options['enable_custom_login'] ), esc_html__( 'Enable custom login URL', 'adminvoro' ), esc_html__( 'When enabled, non-logged-in direct visits to wp-login.php are blocked according to the action below.', 'adminvoro' ) ); ?>
get_current_login_url( $options ); ?>

<?php esc_attr_e( 'Selected login logo', 'adminvoro' ); ?>

'', 'source' => '', 'destination' => '', 'type' => 301, 'enabled' => 1, ), ); } foreach ( array_values( $redirects ) as $index => $redirect ) : $this->render_redirect_row( $index, $redirect ); endforeach; ?>

render_toggle( ADMINVORO_OPTION . '[disable_xmlrpc]', 'disable_xmlrpc', ! empty( $options['disable_xmlrpc'] ), esc_html__( 'Disable XML-RPC', 'adminvoro' ), esc_html__( 'Turns off XML-RPC authentication and remote publishing endpoints through WordPress filters.', 'adminvoro' ) ); $this->render_toggle( ADMINVORO_OPTION . '[disable_user_enumeration]', 'disable_user_enumeration', ! empty( $options['disable_user_enumeration'] ), esc_html__( 'Disable user enumeration', 'adminvoro' ), esc_html__( 'Blocks common ?author=1 discovery requests for logged-out visitors.', 'adminvoro' ) ); $this->render_toggle( ADMINVORO_OPTION . '[hide_wp_version]', 'hide_wp_version', ! empty( $options['hide_wp_version'] ), esc_html__( 'Hide WordPress version', 'adminvoro' ), esc_html__( 'Removes the generator meta tag and generator output.', 'adminvoro' ) ); $this->render_toggle( ADMINVORO_OPTION . '[block_public_files]', 'block_public_files', ! empty( $options['block_public_files'] ), esc_html__( 'Block public readme and license files', 'adminvoro' ), esc_html__( 'Blocks public access to /readme.html and /license.txt.', 'adminvoro' ) ); ?>

hidden="hidden">

render_toggle( ADMINVORO_OPTION . '[disable_emojis]', 'disable_emojis', ! empty( $options['disable_emojis'] ), esc_html__( 'Disable emojis', 'adminvoro' ), esc_html__( 'Removes WordPress emoji detection scripts, styles, filters, and DNS prefetch hints.', 'adminvoro' ) ); $this->render_toggle( ADMINVORO_OPTION . '[disable_embeds]', 'disable_embeds', ! empty( $options['disable_embeds'] ), esc_html__( 'Disable embeds', 'adminvoro' ), esc_html__( 'Disables oEmbed discovery links and dequeues the wp-embed script on the frontend.', 'adminvoro' ) ); ?>

render_toggle( ADMINVORO_OPTION . '[enable_admin_footer_text]', 'enable_admin_footer_text', ! empty( $options['enable_admin_footer_text'] ), esc_html__( 'Enable custom admin footer', 'adminvoro' ), esc_html__( 'When enabled, the footer text below replaces the default WordPress footer text.', 'adminvoro' ) ); ?>

render_toggle( ADMINVORO_OPTION . '[delete_data_on_uninstall]', 'delete_data_on_uninstall', ! empty( $options['delete_data_on_uninstall'] ), esc_html__( 'Delete plugin data on uninstall', 'adminvoro' ), esc_html__( 'When enabled, deleting Adminvoro removes all saved settings and redirect rules. Deactivation and normal updates never remove data.', 'adminvoro' ) ); ?>
__( 'Login Security', 'adminvoro' ), 'login-branding' => __( 'Login Branding', 'adminvoro' ), 'redirects' => __( 'Redirects', 'adminvoro' ), 'security' => __( 'Security', 'adminvoro' ), 'performance' => __( 'Performance', 'adminvoro' ), 'admin-branding' => __( 'Admin Branding', 'adminvoro' ), 'data-management' => __( 'Data', 'adminvoro' ), ); } /** * Get active tab. * * @return string */ private function get_active_tab() { $tabs = $this->get_tabs(); $tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'login-security'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended return array_key_exists( $tab, $tabs ) ? $tab : 'login-security'; } /** * Get current login URL for display. * * @param array $options Plugin options. * @return string */ private function get_current_login_url( $options ) { if ( empty( $options['custom_login_slug'] ) ) { return wp_login_url(); } return home_url( '/' . trim( sanitize_title( $options['custom_login_slug'] ), '/' ) . '/' ); } /** * Set a short-lived admin notice. * * @param string $message Notice text. * @param string $type Notice type. * @return void */ private function set_admin_notice( $message, $type = 'success' ) { set_transient( 'adminvoro_admin_notice_' . get_current_user_id(), array( 'message' => sanitize_text_field( $message ), 'type' => sanitize_key( $type ), ), 30 ); } /** * Display and clear a stored admin notice. * * @return void */ private function display_admin_notice() { $key = 'adminvoro_admin_notice_' . get_current_user_id(); $notice = get_transient( $key ); if ( ! is_array( $notice ) || empty( $notice['message'] ) ) { return; } delete_transient( $key ); $type = isset( $notice['type'] ) && in_array( $notice['type'], array( 'success', 'error', 'warning', 'info' ), true ) ? $notice['type'] : 'success'; ?>