From f7a1dee626f4bcdcdde7c5edab759622243e6efd Mon Sep 17 00:00:00 2001 From: cairocoder01 Date: Fri, 26 Jun 2026 14:47:10 +0300 Subject: [PATCH] Add login site redirect functionality for multisite users --- disciple-tools-multisite.php | 3 + includes/login-site-redirect.php | 333 +++++++++++++++++++++++++++++++ 2 files changed, 336 insertions(+) create mode 100644 includes/login-site-redirect.php diff --git a/disciple-tools-multisite.php b/disciple-tools-multisite.php index ed47e93..76d6458 100755 --- a/disciple-tools-multisite.php +++ b/disciple-tools-multisite.php @@ -220,6 +220,9 @@ public function __call( $method = '', $args = array() ) { register_activation_hook( __FILE__, [ 'DT_Multisite', 'activation' ] ); register_deactivation_hook( __FILE__, [ 'DT_Multisite', 'deactivation' ] ); +// Loaded on every request — not gated on is_network_admin(). +require_once __DIR__ . '/includes/login-site-redirect.php'; + /** * Make the update checker available on multisites when the default theme is not Disciple.Tools */ diff --git a/includes/login-site-redirect.php b/includes/login-site-redirect.php new file mode 100644 index 0000000..ebc95fd --- /dev/null +++ b/includes/login-site-redirect.php @@ -0,0 +1,333 @@ +ID, $main_site_id ) ) { + return; + } + + $subsites = $this->get_user_subsites( $user->ID, $main_site_id ); + + if ( empty( $subsites ) ) { + return; + } + + if ( count( $subsites ) === 1 ) { + // wp_redirect (not safe) bypasses the cross-domain block in + // wp_validate_redirect() for subdomain multisite URLs. + wp_redirect( get_blog_option( $subsites[0]->userblog_id, 'siteurl' ) ); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect + exit; + } + + // Site selector lives on wp-login.php. Use site_url() directly — + // wp_login_url() is filtered by DT to return the custom /login page, + // which doesn't fire login_init and ignores unknown actions. + wp_safe_redirect( add_query_arg( 'action', 'dt-select-site', site_url( 'wp-login.php', 'login' ) ) ); + exit; + } + + // ------------------------------------------------------------------------- + // Already-logged-in hook (and fallback for fresh logins) + // ------------------------------------------------------------------------- + + /** + * Fires for every path through wp-login.php, including the already-logged-in + * shortcut where wp_login never fires. + * + * @param string $redirect_to + * @param string $requested_redirect_to + * @param WP_User|WP_Error $user + * @return string + */ + public function maybe_redirect_to_subsite( $redirect_to, $requested_redirect_to, $user ) { + if ( is_wp_error( $user ) || ! ( $user instanceof WP_User ) ) { + return $redirect_to; + } + + if ( ! is_main_site() ) { + return $redirect_to; + } + + $main_site_id = get_main_site_id(); + + if ( is_user_member_of_blog( $user->ID, $main_site_id ) ) { + return $redirect_to; + } + + $subsites = $this->get_user_subsites( $user->ID, $main_site_id ); + + if ( empty( $subsites ) ) { + return $redirect_to; + } + + if ( count( $subsites ) === 1 ) { + return get_blog_option( $subsites[0]->userblog_id, 'siteurl' ); + } + + // Multiple subsites: show a picker page. + return add_query_arg( 'action', 'dt-select-site', site_url( 'wp-login.php', 'login' ) ); + } + + // ------------------------------------------------------------------------- + // Site-picker page + // ------------------------------------------------------------------------- + + /** + * Intercept the dt-select-site login action and display the site picker. + * Hooks into login_init so it runs inside wp-login.php where + * login_header() and login_footer() are already defined. + */ + public function handle_site_selector() { + $action = isset( $_GET['action'] ) ? sanitize_key( wp_unslash( $_GET['action'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + if ( $action !== 'dt-select-site' ) { + return; + } + + if ( ! is_user_logged_in() ) { + wp_safe_redirect( wp_login_url() ); + exit; + } + + $main_site_id = get_main_site_id(); + $subsites = $this->get_user_subsites( get_current_user_id(), $main_site_id ); + + // Edge case: user somehow ended up here with only one site. + if ( count( $subsites ) === 1 ) { + wp_redirect( get_blog_option( $subsites[0]->userblog_id, 'siteurl' ) ); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect + exit; + } + + $this->render_site_selector( $subsites ); + exit; + } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + /** + * Return the sites a user belongs to, excluding the given site ID. + * + * @param int $user_id + * @param int $exclude_site_id + * @return array + */ + private function get_user_subsites( $user_id, $exclude_site_id ) { + $all_sites = get_blogs_of_user( $user_id ); + return array_values( array_filter( $all_sites, function ( $site ) use ( $exclude_site_id ) { + return (int) $site->userblog_id !== (int) $exclude_site_id; + } ) ); + } + + /** + * Allow wp_safe_redirect() to send users to subsite domains. + * Required for subdomain multisite where each subsite is on its own hostname. + * + * @param array $allowed_hosts + * @return array + */ + public function allow_subsite_redirect_hosts( $allowed_hosts ) { + foreach ( get_sites() as $site ) { + $allowed_hosts[] = $site->domain; + } + return array_unique( $allowed_hosts ); + } + + /** + * Return the logo URL for a given blog: custom if set, otherwise the DT default. + * + * @param int $blog_id + * @return string + */ + private function get_site_logo_url( $blog_id ) { + $custom = get_blog_option( $blog_id, 'custom_logo_url' ); + if ( ! empty( $custom ) ) { + return $custom; + } + return apply_filters( 'dt_default_logo', get_template_directory_uri() . '/dt-assets/images/disciple-tools-logo-white.png' ); + } + + /** + * Render a site-picker page using the standard WordPress login chrome. + * login_header() and login_footer() are defined inside wp-login.php, + * which is executing when login_init fires. + * + * @param array $sites + */ + private function render_site_selector( array $sites ) { + login_header( __( 'Select Your Site', 'disciple_tools' ) ); + ?> + +

+ +

+
+ userblog_id, 'siteurl' ); + $description = get_blog_option( $site->userblog_id, 'blogdescription' ); + $logo_url = $this->get_site_logo_url( $site->userblog_id ); + $display_url = preg_replace( '#^https?://#', '', rtrim( $site_url, '/' ) ); + ?> + +
+ +
+
+
blogname ); ?>
+
+ +
+ +
+
+ +
+