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;
?>
|
/>
|