user_email` keeps reading something instead of crashing. * * **Why every user still has an email string.** `user_email` is populated * with a unique, permanently unroutable address under a `.invalid` domain * (RFC 2606 reserves it so it can never resolve). This is not a fallback path * that might one day deliver mail — it is a placeholder that exists so the * ecosystem's thousands of `$user->user_email` reads return a string. Mail to * it cannot leave the building. See docs/plugin-compatibility.md. * * @package SiriusPress */ defined( 'ABSPATH' ) || exit; final class SP_Identity { const META_ADDRESS = 'sirius_wallet_address'; const META_ADDED = 'sirius_wallet_added'; /** * The wallet address for a user, or '' if they have none (an account * created before the plugin was active, or by a plugin that made its own). */ public static function address_of( $user_id ) { return (string) get_user_meta( (int) $user_id, self::META_ADDRESS, true ); } /** * Find the account that owns an address. * * @return WP_User|null */ public static function user_by_address( $address ) { $address = SP_CashAddr::normalize( $address ); if ( '' === $address ) { return null; } $found = get_users( array( 'meta_key' => self::META_ADDRESS, 'meta_value' => $address, 'number' => 2, 'fields' => 'all', 'count_total' => false, ) ); // Two accounts on one address should be impossible — set_address() // refuses it — but if a direct database edit ever produced one, // authenticating an ambiguous identity is the wrong move. if ( 1 !== count( $found ) ) { return null; } return $found[0]; } /** * Attach an address to an account. * * @return true|WP_Error */ public static function set_address( $user_id, $address ) { $user_id = (int) $user_id; $address = SP_CashAddr::normalize( $address ); if ( '' === $address ) { return new WP_Error( 'sirius_bad_address', __( 'That is not a valid Bitcoin Cash address.', 'sirius-press' ) ); } if ( SP_CashAddr::decode( $address )['prefix'] !== SP_Settings::prefix() ) { return new WP_Error( 'sirius_wrong_network', sprintf( /* translators: %s: the network prefix this site expects, e.g. bitcoincash */ __( 'This site uses %s addresses. That address is on a different network.', 'sirius-press' ), SP_Settings::prefix() ) ); } $existing = self::user_by_address( $address ); if ( $existing && (int) $existing->ID !== $user_id ) { return new WP_Error( 'sirius_address_taken', __( 'Another account already uses that address.', 'sirius-press' ) ); } update_user_meta( $user_id, self::META_ADDRESS, $address ); if ( ! get_user_meta( $user_id, self::META_ADDED, true ) ) { update_user_meta( $user_id, self::META_ADDED, gmdate( 'c' ) ); } return true; } /** * A stable, unroutable placeholder email for an address. * * Derived from the address so it is deterministic: re-running a migration * does not churn every user's `user_email`, and two accounts never collide * on one placeholder. */ public static function stub_email( $address ) { $address = SP_CashAddr::normalize( $address ); $tag = $address ? substr( hash( 'sha256', $address ), 0, 16 ) : wp_generate_password( 16, false ); return 'noreply+' . $tag . '@' . SP_Settings::stub_email_domain(); } /** * A login name derived from an address. * * Addresses are 42 characters of base32 and make miserable usernames, so * the default is the last eight characters of the payload — short enough * to type, long enough not to collide in practice, and disambiguated with * a counter if it does. */ public static function suggest_login( $address ) { $address = SP_CashAddr::normalize( $address ); $body = substr( (string) strstr( $address, ':' ), 1 ); $base = 'bch_' . substr( $body, -8 ); $login = $base; $n = 2; while ( username_exists( $login ) ) { $login = $base . '_' . $n; $n++; if ( $n > 50 ) { $login = $base . '_' . wp_generate_password( 4, false ); break; } } return $login; } /** * Create an account for an address that has proved itself. * * The password is random and never shown to anyone: there is no password * login path in this fork, and leaving it empty would let any plugin that * calls `wp_authenticate()` with an empty password walk straight in. * * @param string $address * @param string $login Optional preferred username. * @param string $role Defaults to the site's configured default role. * @return int|WP_Error User ID. */ public static function create_user( $address, $login = '', $role = '' ) { $address = SP_CashAddr::normalize( $address ); if ( '' === $address ) { return new WP_Error( 'sirius_bad_address', __( 'That is not a valid Bitcoin Cash address.', 'sirius-press' ) ); } if ( self::user_by_address( $address ) ) { return new WP_Error( 'sirius_address_taken', __( 'An account already exists for that address.', 'sirius-press' ) ); } $login = sanitize_user( $login, true ); if ( '' === $login || username_exists( $login ) ) { $login = self::suggest_login( $address ); } $user_id = wp_insert_user( array( 'user_login' => $login, 'user_pass' => wp_generate_password( 64, true, true ), 'user_email' => self::stub_email( $address ), 'display_name' => $login, 'role' => '' !== $role ? $role : get_option( 'default_role', 'subscriber' ), ) ); if ( is_wp_error( $user_id ) ) { return $user_id; } $set = self::set_address( $user_id, $address ); if ( is_wp_error( $set ) ) { // Do not leave a half-made account that can never be signed into. require_once ABSPATH . 'wp-admin/includes/user.php'; wp_delete_user( $user_id ); return $set; } /** * Fires after a wallet-backed account is created. * * @param int $user_id * @param string $address Normalised CashAddress. */ do_action( 'sirius_press_user_registered', $user_id, $address ); return $user_id; } // --------------------------------------------------------------- admin UI /** Show the address in the users list, where the email column used to be useful. */ public static function hooks() { add_filter( 'manage_users_columns', array( __CLASS__, 'add_column' ) ); add_filter( 'manage_users_custom_column', array( __CLASS__, 'render_column' ), 10, 3 ); add_action( 'show_user_profile', array( __CLASS__, 'profile_field' ) ); add_action( 'edit_user_profile', array( __CLASS__, 'profile_field' ) ); add_action( 'personal_options_update', array( __CLASS__, 'save_profile_field' ) ); add_action( 'edit_user_profile_update', array( __CLASS__, 'save_profile_field' ) ); } public static function add_column( $columns ) { $out = array(); foreach ( $columns as $key => $label ) { if ( 'email' === $key ) { // Replace rather than append: a column of identical .invalid // placeholders is worse than no column. $out['sirius_address'] = __( 'Wallet', 'sirius-press' ); continue; } $out[ $key ] = $label; } if ( ! isset( $out['sirius_address'] ) ) { $out['sirius_address'] = __( 'Wallet', 'sirius-press' ); } return $out; } public static function render_column( $value, $column, $user_id ) { if ( 'sirius_address' !== $column ) { return $value; } $address = self::address_of( $user_id ); if ( '' === $address ) { return '' . esc_html__( 'no wallet', 'sirius-press' ) . ''; } return '' . esc_html( SP_CashAddr::shorten( $address ) ) . ''; } public static function profile_field( $user ) { $address = self::address_of( $user->ID ); $can_edit = current_user_can( 'edit_users' ) || get_current_user_id() === (int) $user->ID; ?>

get_error_message() ); } } }