get_user(); if ( $user instanceof WP_User ) { /** * Filter content of the message sent in email * * Password reset is mandatory in this case, hence removing the * "nothing will happen" paragraph. * * @param string $message Original message. * * @return string Filtered message. */ $filter = function ( string $message ): string { return str_replace( __( 'If this was a mistake, ignore this email and nothing will happen.' ) . "\r\n\r\n", '', $message ); // phpcs:ignore WordPress.WP.I18n.MissingArgDomain -- intentionally, because in this case we want to use the same translation as in WordPress core. }; /** * Scope the filter to this single send; on bulk operations the closure * would otherwise accumulate on the filter chain, once per processed user. */ add_filter( 'retrieve_password_message', $filter ); retrieve_password( $user->user_login ); remove_filter( 'retrieve_password_message', $filter ); } } /** * Check if this user is authorized to force a password reset for the target user * * The plugin's managing capability alone must not authorize acting on an * arbitrary target: it is granted to every site administrator, so on * multisite it would otherwise let a subsite admin act on super admins * and on users of other sites. WordPress core's `edit_user` meta * capability already encodes the correct per-target rules — on * multisite it requires `manage_network_users` and denies non * super-admins editing super admins — so deferring to it mirrors the * guards core applies to destructive user actions in * "wp-admin/users.php" without second-guessing them. * * @param int $target_user_id ID of the user the action would affect. * * @return bool Boolean "true" if this user is authorized to force a password reset for the target user, "false" otherwise. */ public function can_force_password_reset_for( int $target_user_id ): bool { $user_id = $this->get_user_id(); if ( null === $user_id || $target_user_id === $user_id ) { return false; } return user_can( $user_id, 'edit_user', $target_user_id ); } /** * Add user meta to controll the password reset request for this user * * @param int|string $requestor ID of user who requested the password reset, string "WP-CLI" if requested via WP-CLI, or other string identifier. * @param bool $with_current_password_allowed Whether the current password is allowed to initiate the password reset process or not. * * @return void */ public function force_password_reset( $requestor, bool $with_current_password_allowed ): void { $this->update_meta( self::USER_META_KEY__REQUEST, [ 'requested_at' => Utils\Date_Time::get_utc_timestamp(), 'requested_by' => $requestor, 'with_current_password_allowed' => $with_current_password_allowed, ], ); } /** * Remove the password reset request data from user meta * * This is triggered only after user successfully reset their password * * @return void */ public function remove_password_reset_enforcement(): void { $this->delete_meta( self::USER_META_KEY__REQUEST ); } /** * Check if a password reset was requested for this user * * @return bool Whether the password reset is required or not. */ public function is_password_reset_required(): bool { if ( null === $this->get_user_id() ) { return false; } return is_array( $this->get_password_reset_request_data() ); } /** * Get the password reset event data * * @return ?array{requested_at:int,requested_by:int|string,with_current_password_allowed:bool} Password reset event data, or null if no password reset was requested. */ public function get_password_reset_request_data(): ?array { if ( false === $this->is_request_config_loaded ) { // Update the flag to avoid multiple loading attempts. $this->is_request_config_loaded = true; /** @var array $meta_value */ $meta_value = $this->get_meta_as_array( self::USER_META_KEY__REQUEST ); if ( ! empty( $meta_value ) ) { /** * Normalize the stored shape here so every consumer receives a * fully-populated, type-coerced array. A missing * "with_current_password_allowed" defaults to the strict branch. */ $requested_by = $meta_value['requested_by'] ?? 0; $this->request_config = [ 'requested_at' => Utils\Type::ensure_int( $meta_value['requested_at'] ?? 0 ), 'requested_by' => is_string( $requested_by ) ? $requested_by : Utils\Type::ensure_int( $requested_by ), 'with_current_password_allowed' => Utils\Type::ensure_bool( $meta_value['with_current_password_allowed'] ?? false ), ]; } } return $this->request_config; } /** * Resolve the display name of the user (or actor) who requested the reset * * @param int|string $requestor Numeric user ID, or a string identifier (e.g. "WP-CLI"). * * @return string Human-readable requestor label. */ public function get_requestor_display_name( $requestor ): string { return is_string( $requestor ) ? $requestor : ( get_the_author_meta( 'display_name', Utils\Type::ensure_int( $requestor ) ) ?: __( 'an unknown user', 'password-reset-enforcement' ) ); } /** * Get the link to the password reset form * * @return null|string|WP_Error Link to the password reset form; instance of WP_Error in case of "spammy", non-existed, or non-logged-in users. */ public function get_password_reset_form_link() { $login_url = wp_login_url(); $request_config = $this->get_password_reset_request_data(); if ( null === $request_config || false === $request_config['with_current_password_allowed'] ) { $link = add_query_arg( [ 'action' => 'lostpassword' ], $login_url, ); } else { $user = $this->get_user(); $key = ''; if ( $user instanceof WP_User ) { $key = get_password_reset_key( $user ); } // This can only happen to users who are marked as "spammy" or don't exist. if ( null === $user || empty( $key ) || $key instanceof WP_Error ) { return new WP_Error( 'password_invalidated', __( 'Error: Your current password has been invalidated. Please contact the administrator to get the new password.', 'password-reset-enforcement' ), ); } $link = add_query_arg( [ 'action' => 'rp', 'key' => $key, 'login' => rawurlencode( $user->user_login ), 'wp_lang' => get_user_locale( $user ), ], $login_url, ); } return $link; } }