'WPS Hide Login', 'rename-wp-login/rename-wp-login.php' => 'Rename wp-login.php', 'wp-hide-security-enhancer/wp-hide.php' => 'WP Hide & Security Enhancer', 'hide-my-wp/index.php' => 'Hide My WP Ghost', 'easy-hide-login/wp-hide-login.php' => 'Easy Hide Login', 'custom-login-url/custom-login-url.php' => 'Custom Login URL', ); /** * Whether the current request targets the custom login slug. * * @var bool */ private static $is_login_request = false; /** * Whether the current request must be blocked. * * @var bool */ private static $block = false; /** * Is the feature switched on and usable? * * @return bool */ public static function active() { if ( defined( 'SITEAUDITHUB_DISABLE_LOGIN_GUARD' ) && SITEAUDITHUB_DISABLE_LOGIN_GUARD ) { return false; } if ( is_multisite() ) { return false; // Not supported on multisite in this version. } return (int) Settings::get( 'login_enabled' ) && self::valid_slug( (string) Settings::get( 'login_slug' ) ) && ! self::conflicts(); } /** * Active conflicting plugins. * * @return array Names. */ public static function conflicts() { $active = (array) get_option( 'active_plugins', array() ); $found = array(); foreach ( self::CONFLICTS as $file => $name ) { if ( in_array( $file, $active, true ) ) { $found[] = $name; } } return $found; } /** * Syntax-only slug validation. * * @param string $slug Slug. * @return bool */ public static function valid_slug( $slug ) { return (bool) preg_match( '/^[a-z0-9][a-z0-9\-]{2,48}[a-z0-9]$/', $slug ) && ! in_array( $slug, self::RESERVED, true ); } /** * Full validation used when saving settings. * * @param string $slug Slug (already sanitized). * @return true|string Error message. */ public static function check_slug( $slug ) { if ( ! preg_match( '/^[a-z0-9][a-z0-9\-]{2,48}[a-z0-9]$/', $slug ) ) { return __( 'Use 4 to 50 characters: lowercase letters, numbers and hyphens (not at the start or end).', 'siteaudithub' ); } if ( in_array( $slug, self::RESERVED, true ) ) { return __( 'This address is reserved by WordPress or is too easy to guess. Choose another one.', 'siteaudithub' ); } $types = array_values( get_post_types( array( 'public' => true ) ) ); if ( get_page_by_path( $slug, OBJECT, $types ) ) { return __( 'A page or post already uses this address. Choose another one.', 'siteaudithub' ); } if ( term_exists( $slug ) ) { return __( 'A category or tag already uses this address. Choose another one.', 'siteaudithub' ); } return true; } /** * The custom login URL. * * @param string|null $scheme URL scheme. * @return string */ public static function login_url( $scheme = null ) { $slug = (string) Settings::get( 'login_slug' ); if ( get_option( 'permalink_structure' ) ) { return user_trailingslashit( home_url( '/' . $slug, $scheme ) ); } return home_url( '/', $scheme ) . '?' . $slug; } /** * Register hooks (called during plugins_loaded). */ public static function init() { add_action( 'update_option_' . Settings::OPTION, array( __CLASS__, 'on_settings_change' ), 20, 2 ); add_action( 'add_option_' . Settings::OPTION, array( __CLASS__, 'on_settings_add' ), 20, 2 ); if ( ! self::active() ) { return; } self::classify_request(); add_action( 'wp_loaded', array( __CLASS__, 'handle_request' ), 1 ); foreach ( array( 'site_url', 'network_site_url', 'wp_redirect', 'login_url', 'logout_url', 'lostpassword_url', 'register_url' ) as $filter ) { add_filter( $filter, array( __CLASS__, 'rewrite_url' ), 100 ); } // WordPress' convenience redirects (/login, /admin, /dashboard) would reveal the address. remove_action( 'template_redirect', 'wp_redirect_admin_locations', 1000 ); } /** * Request path relative to the home path, without slashes. * * @return string */ private static function request_path() { $uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $path = (string) wp_parse_url( $uri, PHP_URL_PATH ); $path = rawurldecode( $path ); $home = (string) wp_parse_url( home_url( '/' ), PHP_URL_PATH ); if ( '' !== $home && '/' !== $home && 0 === strpos( $path, $home ) ) { $path = substr( $path, strlen( untrailingslashit( $home ) ) ); } return strtolower( trim( $path, '/' ) ); } /** * Decide what this request is (runs early, before WordPress routes it). */ private static function classify_request() { $slug = (string) Settings::get( 'login_slug' ); $path = self::request_path(); // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( $path === $slug || ( '' === $path && isset( $_GET[ $slug ] ) && ! get_option( 'permalink_structure' ) ) ) { self::$is_login_request = true; return; } $is_login_file = ( 'wp-login.php' === basename( $path ) ) || ( 'wp-login' === $path ); if ( $is_login_file ) { $action = isset( $_REQUEST['action'] ) ? sanitize_key( wp_unslash( $_REQUEST['action'] ) ) : ''; // Password-protected posts submit to wp-login.php?action=postpass; keep that working. if ( 'postpass' !== $action ) { self::$block = true; } } // phpcs:enable WordPress.Security.NonceVerification.Recommended } /** * Serve the login form at the custom slug or block hidden entry points. */ public static function handle_request() { global $pagenow; if ( self::$is_login_request ) { self::serve_login(); return; } // Logged-out visitors probing /wp-admin/ (except AJAX and admin-post handlers). if ( is_admin() && ! is_user_logged_in() && ! wp_doing_ajax() && ! wp_doing_cron() && 'admin-post.php' !== $pagenow && ! ( defined( 'WP_CLI' ) && WP_CLI ) && 'cli' !== PHP_SAPI ) { self::$block = true; } if ( self::$block ) { self::deny(); } } /** * Load wp-login.php for the custom address. */ private static function serve_login() { // Variables wp-login.php expects in the global scope. global $error, $interim_login, $action, $user_login, $user, $redirect_to, $pagenow; $pagenow = 'wp-login.php'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited require_once ABSPATH . 'wp-login.php'; exit; } /** * Respond to a blocked request: 404 page or redirect home. */ private static function deny() { nocache_headers(); if ( 'home' === Settings::get( 'login_block_mode' ) ) { wp_safe_redirect( home_url( '/' ), 302, 'SiteAuditHub' ); exit; } status_header( 404 ); global $wp_query; $template = get_query_template( '404' ); if ( $wp_query instanceof \WP_Query && $template && ! wp_is_block_theme() ) { $wp_query->set_404(); include $template; exit; } if ( $wp_query instanceof \WP_Query && wp_is_block_theme() && function_exists( 'locate_block_template' ) ) { $wp_query->set_404(); $block = locate_block_template( '', '404', array( '404' ) ); if ( $block ) { include $block; exit; } } wp_die( esc_html__( 'Page not found.', 'siteaudithub' ), esc_html__( 'Not Found', 'siteaudithub' ), array( 'response' => 404 ) ); } /** * Rewrite any WordPress-generated wp-login.php URL to the custom address. * * @param string $url URL. * @return string */ public static function rewrite_url( $url ) { if ( ! is_string( $url ) || false === strpos( $url, 'wp-login.php' ) ) { return $url; } $query = (string) wp_parse_url( $url, PHP_URL_QUERY ); $args = array(); if ( '' !== $query ) { wp_parse_str( $query, $args ); } if ( isset( $args['action'] ) && 'postpass' === $args['action'] ) { return $url; } $scheme = 0 === strpos( $url, 'https://' ) ? 'https' : null; $target = self::login_url( $scheme ); if ( ! $args ) { return $target; } $encoded = array(); foreach ( $args as $key => $value ) { if ( is_scalar( $value ) ) { $encoded[ $key ] = rawurlencode( (string) $value ); } } return add_query_arg( $encoded, $target ); } /** * Notify the administrator when the login address is enabled or changed. * * @param mixed $old Old value. * @param mixed $new New value. */ public static function on_settings_change( $old, $new ) { $old = is_array( $old ) ? $old : array(); $new = is_array( $new ) ? $new : array(); $was_on = ! empty( $old['login_enabled'] ) ? (string) ( isset( $old['login_slug'] ) ? $old['login_slug'] : '' ) : ''; $is_on = ! empty( $new['login_enabled'] ) ? (string) ( isset( $new['login_slug'] ) ? $new['login_slug'] : '' ) : ''; if ( $was_on === $is_on ) { return; } Settings::flush(); if ( '' === $is_on ) { Logger::log( 'info', 'Custom login URL disabled; wp-login.php restored.' ); return; } $url = self::login_url(); Logger::log( 'info', 'Custom login URL enabled or changed.' ); $user = wp_get_current_user(); $to = $user && $user->exists() && is_email( $user->user_email ) ? $user->user_email : get_option( 'admin_email' ); wp_mail( $to, /* translators: %s: site name */ sprintf( __( '[%s] Your login address has changed', 'siteaudithub' ), wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ) ), sprintf( /* translators: 1: login URL, 2: constant */ __( "SiteAuditHub now serves your WordPress login page at:\n\n%1\$s\n\nBookmark this address. The old wp-login.php address no longer works.\n\nIf you ever get locked out, add this line to wp-config.php to restore the default login:\n%2\$s\n\nor rename the siteaudithub folder in wp-content/plugins.", 'siteaudithub' ), $url, "define( 'SITEAUDITHUB_DISABLE_LOGIN_GUARD', true );" ) ); } /** * First save creates the option. * * @param string $option Option. * @param mixed $value Value. */ public static function on_settings_add( $option, $value ) { self::on_settings_change( array(), $value ); } /** * Health overview line. * * @return array */ public static function health_row() { $forced_off = defined( 'SITEAUDITHUB_DISABLE_LOGIN_GUARD' ) && SITEAUDITHUB_DISABLE_LOGIN_GUARD; return array( 'label' => __( 'Login address', 'siteaudithub' ), 'value' => self::active() ? __( 'Custom', 'siteaudithub' ) : __( 'Default (wp-login.php)', 'siteaudithub' ), 'status' => self::active() ? 'good' : 'notice', 'note' => $forced_off ? __( 'Disabled by SITEAUDITHUB_DISABLE_LOGIN_GUARD in wp-config.php.', 'siteaudithub' ) : ( self::active() ? '' : __( 'Optional: set a custom login address in Settings → Login.', 'siteaudithub' ) ), ); } }