Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save WillPresley/6c4d736158f9988fe5298b7fbf03d9cf to your computer and use it in GitHub Desktop.

Select an option

Save WillPresley/6c4d736158f9988fe5298b7fbf03d9cf to your computer and use it in GitHub Desktop.
WordPress Multisite: Password Reset on Local Blog
<?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