'404', 'slug' => '', ]; /** * Paths (relative to the site, lowercase) that stay reachable while logged * out: front-end AJAX and form handlers, assets the login screen loads, core * upgrade and install screens, and Application Passwords, which must reach * auth_redirect() so the visitor lands on the custom login. */ const EXEMPT_PATHS = [ '/wp-admin/admin-ajax.php', '/wp-admin/admin-post.php', '/wp-admin/load-styles.php', '/wp-admin/load-scripts.php', '/wp-admin/upgrade.php', '/wp-admin/install.php', '/wp-admin/setup-config.php', '/wp-admin/maint/repair.php', '/wp-admin/authorize-application.php', ]; /** * Result of ao_get_effective_login_slug() for this request. * * @var array{slug:string,source:string,enabled:bool,error:string} */ private $effective = [ 'slug' => '', 'source' => 'db', 'enabled' => false, 'error' => '', ]; /** * Check permalink if have trailing slash * * @var bool */ private $has_trailing_slash = false; /** * Placeholder for wp-login parameter * * @var bool */ private $wp_login_php; const MENU_SLUG = 'adminoptimizer-custom-login-url'; /** * Start the engine running * * @return void */ protected function init(): void { // A safe-save that ran out is reverted here, before the effective slug is computed, so this very request // already sees the restored slug. One array-key read on the option that is already loaded. if ( is_array( $this->options ) && isset( $this->options[ Custom_Login_Url_Safe_Save::PENDING_KEY ] ) ) { Custom_Login_Url_Safe_Save::maybe_expire(); $this->options = get_option( static::OPTION_NAME, [] ); add_action( Custom_Login_Url_Safe_Save::CRON_HOOK, [ Custom_Login_Url_Safe_Save::class, 'maybe_expire' ] ); } // The settings object keeps the stored values; the constants only change what the runtime uses. $this->settings = new Custom_Login_Url_Settings( $this->options ); $this->effective = ao_get_effective_login_slug( is_array( $this->options ) ? $this->options : [] ); $this->redirect = ao_get_login_redirect_target( is_array( $this->options ) ? $this->options : [] ); if ( $this->effective['enabled'] ) { $this->options['custom_slug'] = $this->effective['slug']; $this->has_trailing_slash = $this->check_permalink_trailing_slash(); $this->change_login_url(); add_action( 'wp_loaded', [ $this, 'maybe_redirect_load_url' ] ); add_filter( 'login_url', [ $this, 'login_url' ], 10, 3 ); add_filter( 'wp_redirect', [ $this, 'wp_redirect' ], 10, 2 ); add_filter( 'site_url', [ $this, 'site_url' ], 10, 4 ); remove_action( 'template_redirect', 'wp_redirect_admin_locations', 1000 ); } if ( is_admin() ) { add_filter( Security_Monitor::SITE_HEALTH_ITEMS_FILTER, [ $this, 'add_site_health_item' ] ); add_action( 'adminoptim_module_state_changed', [ Custom_Login_Url_Safe_Save::class, 'on_module_state_changed' ], 10, 2 ); add_action( 'update_option_' . self::OPTION_NAME, [ $this, 'on_option_updated' ], 10, 2 ); add_action( 'add_option_' . self::OPTION_NAME, [ $this, 'on_option_added' ], 10, 2 ); if ( '' !== $this->effective['error'] ) { add_action( 'admin_notices', [ $this, 'render_invalid_constant_notice' ] ); } } } /** * Option updated: email the new login URL when the slug changed. * * @param mixed $old_value Previous option value. * @param mixed $new_value New option value. * * @return void */ public function on_option_updated( $old_value, $new_value ): void { // The safe-save's own writes (confirm, revert) are not a user change: no cron change, no "changed" email. if ( Custom_Login_Url_Safe_Save::is_internal_write() ) { return; } Custom_Login_Url_Safe_Save::sync_schedule( $old_value, $new_value ); $this->maybe_email_login_url( $old_value, $new_value ); } /** * Option created (first save): treated as a change from "no custom slug". * * @param string $option Option name. * @param mixed $value New option value. * * @return void */ public function on_option_added( $option, $value ): void { if ( Custom_Login_Url_Safe_Save::is_internal_write() ) { return; } Custom_Login_Url_Safe_Save::sync_schedule( [], $value ); $this->maybe_email_login_url( [], $value ); } /** * Email the full login URL to the site admin when the stored slug changed. * * Skipped when the setting is off, the slug is unchanged or empty, or a * wp-config.php constant controls the value. Only the slug is sent: no * credentials or tokens. * * @param mixed $old_value Previous option value. * @param mixed $new_value New option value. * * @return void */ private function maybe_email_login_url( $old_value, $new_value ): void { $old_slug = is_array( $old_value ) && isset( $old_value['custom_slug'] ) ? (string) $old_value['custom_slug'] : ''; $new_slug = is_array( $new_value ) && isset( $new_value['custom_slug'] ) ? (string) $new_value['custom_slug'] : ''; if ( '' === $new_slug || $new_slug === $old_slug || 'db' !== $this->effective['source'] ) { return; } // A missing key means "on": installs that predate the setting get it by default. if ( isset( $new_value['email_on_change'] ) && empty( $new_value['email_on_change'] ) ) { return; } self::send_login_url_email( $new_slug ); } /** * The module's guide address. It is the `website` of the module's entry in * Module_Registry, so there is one definition for the whole plugin; the * settings help, the emails and Site Health all read it from here. * * @return string */ public static function get_docs_url(): string { $module = Module_Registry::get_module( self::MODULE_ID ); return is_array( $module ) && ! empty( $module['website'] ) ? (string) $module['website'] : ''; } /** * Email the login URL to the site admin. The one place the message is built, * shared by the settings-screen save, the safe-save revert and * `wp ao login-url --notify`. Only the slug in force and recovery help are * sent: no credentials, tokens, or the slug that was replaced. * * Variants: 'changed' (an empty slug is the "default login restored" * message sent by `reset`) and 'reverted' (an unconfirmed change was undone; * an empty slug means the default login is back). * * @param string $slug Slug now in force, or '' for the default login. * @param string $variant 'changed' (default) or 'reverted'. * @param int $minutes For 'reverted': minutes the change had to be confirmed. * * @return bool Whether wp_mail() accepted the message. */ public static function send_login_url_email( string $slug, string $variant = 'changed', int $minutes = 0 ): bool { $admin_email = get_option( 'admin_email' ); if ( ! is_email( $admin_email ) ) { return false; } $site_name = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ); // The default login URL, built without site_url( 'wp-login.php' ), which this module rewrites to the custom one. $default_url = trailingslashit( site_url() ) . 'wp-login.php'; $recovery = "define( 'AO_DISABLE_CUSTOM_LOGIN', true );"; if ( 'reverted' === $variant ) { /* translators: %s: site name. */ $subject = sprintf( __( '[%s] Your new login URL was not confirmed and was reverted', 'admin-optimizer' ), $site_name ); $message = sprintf( /* translators: 1: site name, 2: minutes, 3: the login URL now in force, 4: wp-config.php line, 5: documentation URL. */ __( "The change to a new login URL for %1\$s was not confirmed within the %2\$d-minute limit, so it was reverted.\n\nThe login URL in force now:\n\n%3\$s\n\nTo try again, save the new login URL in Admin Optimizer, open the check link it shows in a new tab, and keep that tab's page loading.\n\nIf you are locked out, add this line to your wp-config.php to restore the default login, then log in at wp-login.php again:\n\n%4\$s\n\nMore help: %5\$s\n\nThis message was sent by Admin Optimizer. It does not contain your password.", 'admin-optimizer' ), $site_name, $minutes, '' === $slug ? $default_url . ' ' . __( '(the default WordPress login)', 'admin-optimizer' ) : self::build_url( $slug ), $recovery, self::get_docs_url() ); } elseif ( '' === $slug ) { /* translators: %s: site name. */ $subject = sprintf( __( '[%s] Your login URL was reset', 'admin-optimizer' ), $site_name ); $message = sprintf( /* translators: 1: site name, 2: the default login URL, 3: wp-config.php line, 4: documentation URL. */ __( "The custom login URL for %1\$s was removed. The default login page works again:\n\n%2\$s\n\nIf you set a custom login URL again and get locked out, add this line to your wp-config.php to restore the default login:\n\n%3\$s\n\nMore help: %4\$s\n\nThis message was sent by Admin Optimizer. It does not contain your password.", 'admin-optimizer' ), $site_name, $default_url, $recovery, self::get_docs_url() ); } else { /* translators: %s: site name. */ $subject = sprintf( __( '[%s] Your login URL has changed', 'admin-optimizer' ), $site_name ); $message = sprintf( /* translators: 1: site name, 2: the new login URL, 3: wp-config.php line, 4: documentation URL. */ __( "The login URL for %1\$s has changed. Your new login URL is:\n\n%2\$s\n\nBookmark it now. The old wp-login.php address no longer shows the login page.\n\nIf you are locked out, add this line to your wp-config.php to restore the default login, then log in at wp-login.php again:\n\n%3\$s\n\nMore help: %4\$s\n\nThis message was sent by Admin Optimizer. It does not contain your password.", 'admin-optimizer' ), $site_name, self::build_url( $slug ), $recovery, self::get_docs_url() ); } return (bool) wp_mail( $admin_email, $subject, $message ); } /** * Admin notice: AO_CUSTOM_LOGIN_URL was blocked by the validator, so the module is off. * * @return void */ public function render_invalid_constant_notice(): void { if ( ! current_user_can( 'manage_options' ) ) { return; } ?>

AO_CUSTOM_LOGIN_URL', esc_html( Custom_Login_Url_Validator::get_messages()[ $this->effective['error'] ] ?? $this->effective['error'] ) ), [ 'code' => [] ] ); ?>

true ] ) ); $collides = $page instanceof \WP_Post && 'publish' === $page->post_status; } // Plugin detection runs only here, when Site Health rows are built, never on a normal request. $items[] = self::build_site_health_item( $options, $collides, self::get_module_admin_url(), '' === $slug ? [] : Custom_Login_Url_Compat::detect_conflicts() ); return $items; } /** * Build the Site Health row from the saved options. Pure, so every rule * can be tested. The tab cannot confirm from outside that wp-login.php is * really hidden (server rewrites and other plugins are not handled, and * loopback checks are out of scope), so a good result says "configured", * never "hidden". * * @param array $options Saved `adminoptim_custom_login` option. * @param bool $collides Whether a published page/post already uses the custom slug. * @param string $url Link to the module's own screen. * @param array $conflicts Names of active plugins that also change the login URL. * * @return array */ public static function build_site_health_item( array $options, bool $collides, string $url, array $conflicts = [] ): array { $slug = isset( $options['custom_slug'] ) ? (string) $options['custom_slug'] : ''; $target = ao_get_login_redirect_target( $options ); $current = ''; $recommended = ''; $reason = ''; $link_label = __( 'Open module', 'admin-optimizer' ); $status = 'warning'; // Same rules and lists as the settings screen; the content lookup is passed in as $collides. $result = '' === $slug ? null : Custom_Login_Url_Validator::validate( $slug, [ 'redirection_slug' => $target['slug'], 'check_content' => false, ] ); if ( '' === $slug ) { $message = __( 'The module is on, but no login URL is saved yet, so wp-login.php is unchanged.', 'admin-optimizer' ); $current = __( 'Nothing saved', 'admin-optimizer' ); $recommended = __( 'Save a login URL', 'admin-optimizer' ); $reason = __( 'The form shows an example value, but nothing changes until you save it once.', 'admin-optimizer' ); $link_label = __( 'Save the settings', 'admin-optimizer' ); } elseif ( $conflicts ) { /* translators: %s: plugin names, e.g. "WPS Hide Login, LoginPress". */ $message = sprintf( __( 'Another plugin also changes the login URL: %s. Two plugins doing this can lock you out.', 'admin-optimizer' ), implode( ', ', $conflicts ) ); $current = implode( ', ', $conflicts ); $recommended = __( 'Only one plugin changing the login URL', 'admin-optimizer' ); $reason = __( 'Deactivate one of them, or the login page may stop working.', 'admin-optimizer' ); $url = self::get_docs_url(); $link_label = __( 'Read the guide', 'admin-optimizer' ); } elseif ( 'collision_redirect' === $result['code'] ) { /* translators: %s: the saved slug used for both URLs. */ $message = sprintf( __( 'The login URL and the redirection URL are both "%s".', 'admin-optimizer' ), $slug ); $current = $slug; $recommended = __( 'Two different addresses', 'admin-optimizer' ); $reason = __( 'Blocked visitors are sent to the login page itself, which defeats the point of moving it.', 'admin-optimizer' ); $link_label = __( 'Change redirection URL', 'admin-optimizer' ); } elseif ( $collides || 'collision_base' === $result['code'] ) { /* translators: %s: the saved login URL slug. */ $message = sprintf( __( 'The login URL "%s" is already used by a page or a WordPress path.', 'admin-optimizer' ), $slug ); $current = $slug; $recommended = __( 'A slug that no page or WordPress path uses', 'admin-optimizer' ); $reason = __( 'That page or path would become unreachable, or the login page would not load there.', 'admin-optimizer' ); $link_label = __( 'Change login URL', 'admin-optimizer' ); } elseif ( Custom_Login_Url_Validator::STATUS_OK !== $result['status'] ) { /* translators: 1: the saved login URL slug, 2: why it is a problem. */ $message = sprintf( __( 'The login URL "%1$s" is a poor choice. %2$s', 'admin-optimizer' ), $slug, $result['message'] ); $current = $slug; $recommended = __( 'A unique word that is not a common login name', 'admin-optimizer' ); $reason = __( 'Bots probe /login, /admin and similar addresses first, so a guessable one adds little protection.', 'admin-optimizer' ); $link_label = __( 'Change login URL', 'admin-optimizer' ); } else { $status = 'ok'; $message = __( 'Login URL is configured.', 'admin-optimizer' ); } // A row has one link (the fix), so the guide goes in the explanation; the conflict row links to it directly. if ( 'ok' !== $status && ! $conflicts ) { /* translators: %s: documentation URL. */ $reason .= ' ' . sprintf( __( 'Guide: %s', 'admin-optimizer' ), self::get_docs_url() ); } return [ 'id' => 'custom-login-url', 'label' => __( 'Custom Login URL', 'admin-optimizer' ), 'message' => $message, 'status' => $status, 'current' => $current, 'recommended' => $recommended, 'reason' => $reason, 'url' => $url, 'link_label' => $link_label, 'priority' => 30, ]; } /** * The request path, normalized for matching: percent-decoded, lowercase, * query string dropped, repeated slashes collapsed ("//wp-login.php"), and * the home or site subdirectory removed. Always starts with "/", or is '' * when there is no REQUEST_URI (CLI). Requests are matched on this path, * never by searching the whole URL. * * @return string */ public static function get_request_path(): string { if ( ! isset( $_SERVER['REQUEST_URI'] ) ) { return ''; } $uri = rawurldecode( wp_unslash( $_SERVER['REQUEST_URI'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- only compared, never output. $path = strtolower( (string) preg_replace( '#/+#', '/', (string) strtok( $uri, '?#' ) ) ); foreach ( [ home_url(), site_url() ] as $base_url ) { $base = untrailingslashit( strtolower( (string) wp_parse_url( $base_url, PHP_URL_PATH ) ) ); if ( '' !== $base && ( $path === $base || 0 === strpos( $path, $base . '/' ) ) ) { $path = substr( $path, strlen( $base ) ); break; } } return '' === $path ? '/' : $path; } /** * Whether a logged-out request must be left alone: not hidden, not * redirected. The one place this is decided. * * Exempt by default: the paths in EXEMPT_PATHS, and the password-protected * post form (POST to wp-login.php?action=postpass, nothing else). * * @param string|null $path Normalized request path; null reads the current request. * @param string|null $method HTTP method; null reads the current request. * @param string|null $action The `action` request parameter; null reads the current request. * * @return bool */ public static function is_exempt_request( ?string $path = null, ?string $method = null, ?string $action = null ): bool { $path = $path ?? self::get_request_path(); $method = strtoupper( $method ?? ( isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : 'GET' ) ); $action = $action ?? ( isset( $_REQUEST['action'] ) && is_string( $_REQUEST['action'] ) ? sanitize_key( wp_unslash( $_REQUEST['action'] ) ) : '' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- compared to a fixed string only. $exempt = in_array( $path, self::EXEMPT_PATHS, true ) || ( '/wp-login.php' === $path && 'POST' === $method && 'postpass' === $action ); /** * Filters whether a request is exempt from the hidden wp-login.php and wp-admin handling. * * @param bool $exempt True to leave the request alone. * @param string $path Normalized request path, e.g. "/wp-admin/admin-post.php". * @param string $method Upper-case HTTP method. * @param string $action The `action` request parameter ('' if none). */ return (bool) apply_filters( 'ao_login_url_exempt_requests', $exempt, $path, $method, $action ); } /** * Send the headers that keep page caches and CDNs away from login-related * responses (the custom login page, redirects and the 404). wp-login.php * already calls nocache_headers(); the other two are what it lacks. * * @return void */ private function send_no_cache_headers(): void { nocache_headers(); if ( ! defined( 'DONOTCACHEPAGE' ) ) { define( 'DONOTCACHEPAGE', true ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- constant defined by page-cache plugins. } if ( ! headers_sent() ) { header( 'X-LiteSpeed-Cache-Control: no-cache' ); } } /** * What a hidden endpoint answers with, for a redirect mode. The same answer * for wp-login.php and wp-admin: 404 mode never redirects. * * @param string $mode '404', 'home' or 'custom'. * * @return string 'render-404', 'redirect-home' or 'redirect-custom'. */ public static function get_hidden_response_plan( string $mode ): string { if ( 'home' === $mode ) { return 'redirect-home'; } return 'custom' === $mode ? 'redirect-custom' : 'render-404'; } /** * Answer a logged-out request for a hidden endpoint, per the redirect mode. * 404 mode renders the theme's 404 inline for wp-login.php and wp-admin alike * (one code path, so the two cannot be told apart); home and custom modes * redirect (302, no-cache). Never returns. * * @return void */ private function respond_hidden_request(): void { $plan = self::get_hidden_response_plan( $this->redirect['mode'] ); if ( 'render-404' === $plan ) { $this->render_inline_404(); } $this->send_no_cache_headers(); if ( 'redirect-home' === $plan ) { $url = home_url( '/' ); } else { $url = self::build_url( $this->redirect['slug'], null, $this->has_trailing_slash ); } wp_safe_redirect( $url, 302 ); die(); } /** * Make is_admin() answer false for the rest of this request. * * Scripts under wp-admin define WP_ADMIN, which sends WordPress and themes down * admin-only paths (the admin bar calls get_current_screen(), which does not * exist here, and block styles are registered differently). is_admin() asks a * current screen first, so hand it one that says "front end". Done as early as * plugins_loaded for a wp-admin request that carries no login cookie, the 404 * is built from the same state as the one for wp-login.php. * * @return void */ private function use_front_screen(): void { if ( ! is_admin() ) { return; } $GLOBALS['current_screen'] = new class() { // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited /** * Stand-in for WP_Screen::in_admin(): never in an admin area. * * @param string|null $admin Area being asked about. * * @return bool */ public function in_admin( $admin = null ) { return false; } }; } /** * Render the theme's 404 for a hidden endpoint: status 404, no-cache * headers, no Location header, no dependency on any page slug. * * This runs on wp_loaded, so the main query has not been parsed yet: the * request is swapped for a junk path (as change_login_url() does for * wp-login.php), wp() builds the query for it, and the 404 state is then set * explicitly. template-loader.php picks the theme's 404 template (block * themes included) and ends the request, which also answers HEAD with the * headers only. A site with no usable theme gets a minimal wp_die() 404. * * @return void */ private function render_inline_404(): void { global $pagenow, $wp_query; $this->send_no_cache_headers(); $_SERVER['REQUEST_URI'] = $this->user_trailingslashit( '/' . str_repeat( '-/', 10 ) ); $pagenow = 'index.php'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited // WordPress builds its query from the entry script too; without this wp-login.php and wp-admin // would each produce a differently built 404 page. $_SERVER['PHP_SELF'] = '/index.php'; $_SERVER['SCRIPT_NAME'] = '/index.php'; if ( ! defined( 'WP_USE_THEMES' ) ) { define( 'WP_USE_THEMES', true ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound } $this->use_front_screen(); // The 404 must stay a 404: WordPress would otherwise "guess" a permalink for the made-up URL and // redirect (wp-admin under a WordPress folder ends up at the home page). add_filter( 'redirect_canonical', '__return_false' ); wp(); $wp_query->set_404(); status_header( 404 ); nocache_headers(); if ( '' === locate_template( [ '404.php', 'index.php' ] ) && ! wp_is_block_theme() ) { wp_die( esc_html__( 'Page not found.', 'admin-optimizer' ), '', [ 'response' => 404 ] ); } require_once ABSPATH . WPINC . '/template-loader.php'; die; } /** * Change login url * * @return void */ public function change_login_url() { global $pagenow; $path = self::get_request_path(); // --- 1. wp-signup / wp-activate are not available on a single site. --- if ( ! is_multisite() && preg_match( '#^/wp-(signup|activate)(\.php)?(/|$)#', $path ) ) { wp_die( esc_html__( 'This feature is not enabled.', 'admin-optimizer' ) ); } // --- 2. Exempt requests are left alone (see is_exempt_request()). --- if ( self::is_exempt_request( $path ) ) { return; } // --- 2b. A cookieless wp-admin request is logged out for certain: look front-end from the start. --- if ( preg_match( '#^/wp-admin(/|$)#', $path ) && 'home' !== $this->redirect['mode'] && 'custom' !== $this->redirect['mode'] && ! isset( $_COOKIE[ LOGGED_IN_COOKIE ] ) ) { $this->use_front_screen(); } if ( ! is_admin() && preg_match( '#^/wp-(login|register)(\.php)?(/|$)#', $path ) ) { // --- 3. wp-login.php, wp-register.php: pretend the page does not exist. --- $this->wp_login_php = true; add_filter( 'adminoptim_is_login_page', '__return_true' ); $_SERVER['REQUEST_URI'] = $this->user_trailingslashit( '/' . str_repeat( '-/', 10 ) ); $pagenow = 'index.php'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited } elseif ( untrailingslashit( $path ) === '/' . $this->options['custom_slug'] || ( ! get_option( 'permalink_structure' ) && isset( $_GET[ $this->options['custom_slug'] ] ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended && empty( $_GET[ $this->options['custom_slug'] ] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended // --- 4. The custom slug: serve the login screen, never cached. --- $pagenow = 'wp-login.php'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited add_filter( 'adminoptim_is_login_page', '__return_true' ); $this->send_no_cache_headers(); } } /** * Redirect to user defined url * * @return void */ public function maybe_redirect_load_url() { global $pagenow; $path = self::get_request_path(); if ( self::is_exempt_request( $path ) ) { return; } // --- 1. Logged-out wp-admin, by path as well as is_admin(), before auth_redirect() can leak the login URL. --- if ( ( is_admin() || preg_match( '#^/wp-admin(/|$)#', $path ) ) && ! is_user_logged_in() && ! defined( 'DOING_AJAX' ) ) { $this->respond_hidden_request(); } if ( 'wp-login.php' === $pagenow && ( '/' === substr( $path, -1 ) ) !== $this->has_trailing_slash && get_option( 'permalink_structure' ) ) { // --- 2. Custom slug without the permalink structure's trailing slash: canonical redirect. --- wp_safe_redirect( $this->user_trailingslashit( $this->get_url() ) . ( ! empty( $_SERVER['QUERY_STRING'] ) ? '?' . wp_unslash( $_SERVER['QUERY_STRING'] ) : '' ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized die; } elseif ( $this->wp_login_php ) { // --- 3. Someone asked for the real wp-login.php: 404 or redirect, per the redirect mode. --- $referer = (string) wp_get_referer(); if ( function_exists( 'wpmu_activate_signup' ) && false !== strpos( $referer, 'wp-activate.php' ) ) { $referred_query = wp_parse_url( $referer ); if ( ! empty( $referred_query['query'] ) ) { parse_str( $referred_query['query'], $referer ); if ( ! empty( $referer['key'] ) ) { $result = wpmu_activate_signup( $referer['key'] ); if ( is_wp_error( $result ) && ( 'already_active' === $result->get_error_code() || 'blog_taken' === $result->get_error_code() ) ) { wp_safe_redirect( $this->get_url() . ( ! empty( wp_unslash( $_SERVER['QUERY_STRING'] ) ) ? '?' . wp_unslash( $_SERVER['QUERY_STRING'] ) : '' ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized die; } } } } $this->respond_hidden_request(); } elseif ( 'wp-login.php' === $pagenow ) { // --- 4. The custom slug was requested: serve the real login screen. --- global $error, $interim_login, $action, $user_login; // A safe-save verification link proves the new URL loads. Confirm before wp-login.php can send an // already logged-in admin away. The token grants nothing else. if ( isset( $_GET['ao_verify'] ) && is_string( $_GET['ao_verify'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- an HMAC token, checked in constant time. Custom_Login_Url_Safe_Save::confirm_with_token( sanitize_text_field( wp_unslash( $_GET['ao_verify'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended // wp-login.php only recognises a session through the auth cookie, which the browser sends to // wp-admin alone, so on the custom slug it would show the form. The logged-in cookie is sent // site-wide: send an already logged-in admin on to the dashboard ourselves. if ( is_user_logged_in() ) { wp_safe_redirect( admin_url() ); die; } } @require_once ABSPATH . 'wp-login.php'; //phpcs:ignore die; } } /** * Retrieve user defined login url * * @param string $login_or_redirect Login or Redirect. * @param string|null $scheme http or https. * * @return string */ private function get_url( $login_or_redirect = 'login', $scheme = null ) { $slug = ( 'redirect' === $login_or_redirect ) ? $this->options['redirection_slug'] : $this->options['custom_slug']; return self::build_url( $slug, $scheme, $this->has_trailing_slash ); } /** * Build the public URL for a slug. Shared by the runtime, the settings * screen and the change email so they print the same address. * * @param string $slug Slug. * @param string|null $scheme http or https. * @param bool|null $trailing_slash Trailing slash; null reads the permalink structure. * * @return string */ public static function build_url( $slug, $scheme = null, $trailing_slash = null ) { $structure = get_option( 'permalink_structure' ); if ( null === $trailing_slash ) { $trailing_slash = '/' === substr( (string) $structure, -1, 1 ); } if ( $structure ) { $url = home_url( '/', $scheme ) . $slug; if ( $trailing_slash ) { $url = trailingslashit( $url ); } } else { $url = home_url( '/', $scheme ) . '?' . $slug; } return $url; } /** * Check permalink trailing slash * * @return bool */ private function check_permalink_trailing_slash() { return ( '/' === substr( get_option( 'permalink_structure' ), - 1, 1 ) ); } /** * Check user trailing slash * * @param string $text String to check. * * @return string */ private function user_trailingslashit( $text ) { return $this->has_trailing_slash ? trailingslashit( $text ) : untrailingslashit( $text ); } /** * Redirection * * @param string $location Location to redirect to. * @param string $status Status - not used. * * @return string */ public function wp_redirect( $location, $status ) { if ( strpos( $location, 'https://wordpress.com/wp-login.php' ) !== false ) { return $location; } return $this->filter_wp_login_php( $location ); } /** * Filter wp_login.php * * @param string $url Url to filter. * @param string $scheme http or https. * * @return string */ public function filter_wp_login_php( $url, $scheme = null ) { if ( strpos( $url, 'wp-login.php' ) !== false ) { // The password-protected post form posts to wp-login.php?action=postpass. Rewriting it would print the // custom login URL on public pages, so it stays as is and is exempted in is_exempt_request(). parse_str( (string) wp_parse_url( $url, PHP_URL_QUERY ), $url_args ); if ( isset( $url_args['action'] ) && 'postpass' === $url_args['action'] ) { return $url; } if ( is_ssl() ) { $scheme = 'https'; } $args = explode( '?', $url ); if ( isset( $args[1] ) ) { parse_str( $args[1], $args ); $url = add_query_arg( $args, $this->get_url( 'login', $scheme ) ); } else { $url = $this->get_url( 'login', $scheme ); } } return $url; } /** * Get the login url * * @param string $login_url Login url. * @param string $redirect Redirect url. * @param bool $force_reauth Force reauthentication. * * @return string|null */ public function login_url( $login_url, $redirect, $force_reauth ) { if ( is_404() ) { return '#'; } $login_url = $this->filter_wp_login_php( $login_url ); if ( false === $force_reauth ) { return $login_url; } if ( empty( $redirect ) ) { return $login_url; } $redirect = explode( '?', $redirect ); if ( admin_url( 'options.php' ) === $redirect[0] ) { $login_url = admin_url(); } return $login_url; } /** * Site Url * * @param string $url Site url. * @param string $path Site path. * @param string $scheme http or https. * @param int $blog_id Blog id. * * @return mixed|string */ public function site_url( $url, $path, $scheme, $blog_id ) { return $this->filter_wp_login_php( $url, $scheme ); } }