Forked from eteubert/wordpress-passwort-reset-unmultisite.php
Last active
November 12, 2025 14:48
-
-
Save WillPresley/6c4d736158f9988fe5298b7fbf03d9cf to your computer and use it in GitHub Desktop.
WordPress Multisite: Password Reset on Local Blog
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| /** | |
| * Plugin Name: Multisite — Local Password Reset | |
| * Plugin URI: https://gist.github.com/WillPresley/6c4d736158f9988fe5298b7fbf03d9cf | |
| * Description: Keep users on their subsite for the entire password reset flow in WP Multisite (lost password, reset links in emails, titles, etc). | |
| * Version: 1.2.0 | |
| * Author: Will Presley | |
| * Author URI: https://willpresley.com | |
| * License: MIT | |
| * | |
| * This file modernizes and secures the 2016 gist: | |
| * - prevents activation outside Multisite | |
| * - replaces hard-coded site ID (1) with get_network()->site_id | |
| * - improves email message/title and makes strings translatable | |
| * - uses named callbacks so they can be removed or tested | |
| */ | |
| defined( 'ABSPATH' ) || exit; | |
| /** | |
| * Activation hook: only allow network activation on a Multisite installation. | |
| * | |
| * When a non-multisite admin attempts to activate, deactivate immediately and show a message. | |
| * | |
| * Note: register_activation_hook runs in the context of a single-site activation OR network | |
| * activation attempt; we explicitly refuse single-site activation. | |
| * | |
| * @return void | |
| */ | |
| function ms_local_pass_reset_activation() { | |
| // If WordPress is not a Multisite instance, deactivate and stop. | |
| if ( ! is_multisite() ) { | |
| // Make sure the plugin is deactivated. | |
| if ( function_exists( 'deactivate_plugins' ) ) { | |
| deactivate_plugins( plugin_basename( __FILE__ ) ); | |
| } | |
| /* translators: %s: plugin name */ | |
| wp_die( sprintf( __( '%s can only be activated on a WordPress Multisite network.', 'ms-local-pass-reset' ), '<strong>Multisite — Local Password Reset</strong>' ), __( 'Plugin activation error', 'ms-local-pass-reset' ), array( 'back_link' => true ) ); | |
| } | |
| } | |
| register_activation_hook( __FILE__, 'ms_local_pass_reset_activation' ); | |
| /** | |
| * Fix "Lost Password?" URL on the login page so it uses the local site (subsite) login endpoint. | |
| * | |
| * @param string $lostpassword_url The lost password URL generated by WP. | |
| * @param string $redirect Optional redirect_to query param. | |
| * @return string Modified lost password URL for the current site. | |
| */ | |
| function ms_local_pass_reset_lostpassword_url( $lostpassword_url, $redirect ) { | |
| // Build args for the local site's wp-login.php lostpassword action. | |
| $args = array( 'action' => 'lostpassword' ); | |
| if ( ! empty( $redirect ) ) { | |
| $args['redirect_to'] = $redirect; | |
| } | |
| // site_url() respects the current site's URL; use it so users remain on their subsite. | |
| return add_query_arg( $args, site_url( 'wp-login.php' ) ); | |
| } | |
| add_filter( 'lostpassword_url', 'ms_local_pass_reset_lostpassword_url', 10, 2 ); | |
| /** | |
| * Filter network_site_url to keep password-related URL generation pointing to the current site. | |
| * | |
| * Some core and plugins call network_site_url() for password/reset links — intercept the ones | |
| * that contain lostpassword/resetpass actions and return the current site's wp-login.php URL. | |
| * | |
| * @param string $url The complete URL. | |
| * @param string $path Path component passed to network_site_url(). | |
| * @param string $scheme Scheme (e.g. 'http', 'https', 'login'). | |
| * @return string Possibly-modified URL. | |
| */ | |
| function ms_local_pass_reset_network_site_url( $url, $path, $scheme ) { | |
| // We check the $path and $url to be safe (some calls pass full URL in $url). | |
| $needle_lost = 'action=lostpassword'; | |
| $needle_reset = 'action=resetpass'; | |
| // If either action is present, map to the current site's login endpoint. | |
| if ( false !== stripos( $url, $needle_lost ) || false !== stripos( $path, $needle_lost ) ) { | |
| return site_url( 'wp-login.php?action=lostpassword', $scheme ); | |
| } | |
| if ( false !== stripos( $url, $needle_reset ) || false !== stripos( $path, $needle_reset ) ) { | |
| return site_url( 'wp-login.php?action=resetpass', $scheme ); | |
| } | |
| // Otherwise return the original. | |
| return $url; | |
| } | |
| add_filter( 'network_site_url', 'ms_local_pass_reset_network_site_url', 10, 3 ); | |
| /** | |
| * Replace main network site URL with current site URL inside the password reset message. | |
| * | |
| * This addresses cases where the message built by core contains network_site_url() values. | |
| * We use the network's site_id rather than assuming '1'. | |
| * | |
| * @param string $message Complete email message (plain text). | |
| * @param string $key Password reset key. | |
| * @param string $user_login User's login name. | |
| * @param WP_User|WP_Error $user_data User data object (or error). | |
| * @return string Modified message. | |
| */ | |
| function ms_local_pass_reset_retrieve_password_message( $message, $key, $user_login, $user_data ) { | |
| // Safety: determine network site id if available. | |
| $network_site_id = null; | |
| if ( function_exists( 'get_network' ) && get_network() ) { | |
| $network_site_id = get_network()->site_id; | |
| } | |
| // Only attempt replacement if we have a network site id. | |
| if ( $network_site_id ) { | |
| $message = str_replace( get_site_url( $network_site_id ), get_site_url(), $message ); | |
| } | |
| return $message; | |
| } | |
| add_filter( 'retrieve_password_message', 'ms_local_pass_reset_retrieve_password_message', 10, 4 ); | |
| /** | |
| * Provide a localized, clearer password reset email title. | |
| * | |
| * Using sprintf + translation ensures languages other than English get a chance to translate. | |
| * | |
| * @param string $title Original email subject. | |
| * @return string Modified subject line. | |
| */ | |
| function ms_local_pass_reset_retrieve_password_title( $title ) { | |
| /* translators: %s: site name */ | |
| $site_name = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ); | |
| // Wrap the site name into a translated template. Use the site's locale for translation. | |
| return sprintf( __( '[%s] Password Reset', 'ms-local-pass-reset' ), $site_name ); | |
| } | |
| add_filter( 'retrieve_password_title', 'ms_local_pass_reset_retrieve_password_title', 10, 1 ); | |
| /** | |
| * OPTIONAL: If you also want to fully override the outgoing retrieve_password_message (so it | |
| * doesn't reference the main site at all and is completely localized), you can enable the | |
| * function below. It builds a message similar to core but forces local-site URLs and a clear body. | |
| * | |
| * Uncomment the add_filter() line below to enable. | |
| * | |
| * NOTE: this function uses $_SERVER['REMOTE_ADDR'] when available to include requester IP. | |
| * We sanitize that value before inserting it into the plain-text email body. | |
| * | |
| * @param string $message Default message supplied by WP (ignored). | |
| * @param string $key Reset key. | |
| * @param string $user_login Username. | |
| * @param WP_User|WP_Error $user_data User object. | |
| * @return string | |
| */ | |
| function ms_local_pass_reset_full_custom_message( $message, $key, $user_login, $user_data ) { | |
| /* translators: note — plain text email body pieces */ | |
| $locale = get_locale(); | |
| $text = __( 'Someone has requested a password reset for the following account:' ) . "\r\n\r\n"; | |
| $text .= wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ) . "\r\n\r\n"; | |
| /* translators: %s: username */ | |
| $text .= sprintf( __( 'Username: %s' ), $user_login ) . "\r\n\r\n"; | |
| $text .= __( 'If this was a mistake, ignore this email and nothing will happen.' ) . "\r\n\r\n"; | |
| // Build local-site reset link. Use rawurlencode for the login. | |
| $reset_url = site_url( "wp-login.php?action=rp&key=$key&login=" . rawurlencode( $user_login ), 'login' ); | |
| $text .= __( 'To reset your password, visit the following address:' ) . "\r\n\r\n"; | |
| $text .= $reset_url . '&wp_lang=' . $locale . "\r\n\r\n"; | |
| // Add requester IP if available (sanitized). | |
| if ( ! is_user_logged_in() ) { | |
| $requester_ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : ''; | |
| if ( $requester_ip ) { | |
| /* translators: %s: IP address */ | |
| $text .= sprintf( __( 'This password reset request originated from the IP address %s.' ), $requester_ip ) . "\r\n"; | |
| } | |
| } | |
| // Ensure any network site URL references are replaced with the current site URL (defensive). | |
| if ( function_exists( 'get_network' ) && get_network() ) { | |
| $network_site_id = get_network()->site_id; | |
| $text = str_replace( get_site_url( $network_site_id ), get_site_url(), $text ); | |
| } | |
| return $text; | |
| } | |
| // To enable the full custom message above, uncomment the line below (and remove duplicate filters if needed). | |
| // add_filter( 'retrieve_password_message', 'ms_local_pass_reset_full_custom_message', 10, 4 ); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment