'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 );
?>
get_current_login_url( $options );
?>
render_redirect_row(
'__index__',
array(
'id' => '',
'source' => '',
'destination' => '',
'type' => 301,
'enabled' => 1,
)
);
?>
|
|
|
|
|
|
__( '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';
?>