-
-
Save philipnewcomer/59a695415f5f9a2dd851deda42d0552f to your computer and use it in GitHub Desktop.
<?php | |
/** | |
* Recursive function to generate a unique username. | |
* | |
* If the username already exists, will add a numerical suffix which will increase until a unique username is found. | |
* | |
* @param string $username | |
* | |
* @return string The unique username. | |
*/ | |
function generate_unique_username( $username ) { | |
static $i; | |
if ( null === $i ) { | |
$i = 1; | |
} else { | |
$i++; | |
} | |
if ( ! username_exists( $username ) ) { | |
return $username; | |
} | |
$new_username = sprintf( '%s-%s', $username, $i ); | |
if ( ! username_exists( $new_username ) ) { | |
return $new_username; | |
} else { | |
return call_user_func( __FUNCTION__, $username ); | |
} | |
} |
Where can I put this code?
I really need help, thankyou
@marounmelhem You may want to use sanitize_user( $username )
instead of sanitize_title( $username )
.
My final, minor improvement on making sprintf more explicit and including @KiwiKilian 's suggestion.
function generate_unique_username( $username ) {
$username = sanitize_user( $username );
static $i;
if ( null === $i ) {
$i = 1;
} else {
$i ++;
}
if ( ! username_exists( $username ) ) {
return $username;
}
$new_username = sprintf( '%s-%s', $username, $i );
if ( ! username_exists( $new_username ) ) {
return $new_username;
} else {
return call_user_func( __FUNCTION__, $username );
}
}
Nice approach @philipnewcomer.
Been looking at your final code from May 27. Not sure if it's the best approach as it could perhaps introduce a vulnerability in so far as if a malicious person were to register an account lets say using the username "Bob". The above code would check if username "Bob" already exists, lets say the username already exists. So the code loops though adding "+1" until it finds a unique username.
So it eventually finds a unique username of say "Bob-10". The malicious person then tries to register another account again with the username "Bob". This time the function loop gives gives the malicious person a username of "Bob-11".
Now the malicious person knows how the usernames are being formed (by incrementing +1 each time).
Now the malicious person knows other people have registered accounts with the following usernames on your website:
Bob-1
Bob-2
Bob-3
Bob-4
Bob-5
Bob-6
Bob-7
Bob-8
Bob-9
This would seem to make it easier for the malicious person to run targeted brute force attacks now that he/she knows the usernames for the other accounts, all they need do now is brute force the password for the given username.
A better method might be to take the username provided and automatically add some random characters to it. This prevents malicious people from obtaining usernames for accounts on your website as described above. PHP code something like this...
``
function generateRandomString($username){
//specify characters to be used in generating random string, do not specify any characters that wordpress does not allow in teh creation of usernames.
$characters = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNPQRSTUVWXYZ-_";
//get the total length of specified characters to be used in generating random string
$charactersLength = strlen($characters);
//declare a string that we will use to create the random string
$randomString = '';
//set random string length to 11 characters, can change if you don't like the number 11
for ($i = 0; $i < 11; $i++) {
//generate random characters
$randomCharacter = $characters[rand(0, $charactersLength - 1)];
//add the random characters to the random string
$randomString .= $randomCharacter;
};
//wordpress username max 60 characters in length, so take first 60 character of final username string,
//assuming some douche has a really long username.... Can change to a lower number if you don't like the number 60
$randomUsername = substr($username."_".$randomString , 0 , 60 );
//sanitize_user, just in case
$sanRandomUsername = sanitize_user($randomUsername);
//check if randomUsername already exsists....you never know..and also check that randomUsername contains Uppercase/Lowercase/Intergers
if ( (! username_exists( $sanRandomUsername )) && (preg_match('([a-zA-Z].*[0-9]|[0-9].*[a-zA-Z])', $sanRandomUsername)==1) ) {
//return the unique username if it does not exist
return $sanRandomUsername;
}else {
// if the username already exists or username does not contain Uppercase/Lowercase/Intergers ,call generateRandomString function again
return call_user_func("generateRandomString", $username );
}
}//end of generateRandomString function
//call generateRandomString function and pass username which we will use to generate part if the RandomUsername
$sanRandomUsername = generateRandomString($username);
//display UNIQUE & Randomly generated username
echo $sanRandomUsername;
``
Now if any person registers an account lets say using the username "Bob" they will receive a randomly generated username that is unique for example "Bob_1BXssxYuHhL".
@mullet456 The two points that make this not as much of a concern for me are:
1.) For me this was an internal php routine that users didn't call (I wrote a user loader to migrate eCommerce data over in batch.)
2.) This is the same vulnerability that exists within the password reset page within WordPress by default. That page itself will tell you if a username exists or not.
However, your suggestion is still valid. What I'd do here is to replace the $i = 1; and i++; assignments with an i = rand() call that pays attention to the sixty character limit on usernames and probably added four zero padded random integers. I probably am not going to do this on my production system running this. Ideally, this would be _random-characters too. It's a balance between security (using all sixty characters) and convenience (not making a user type in sixty characters.)
I might modify this gist to include your suggestion when I'm on a computer (and not on my phone) and add more error checking for the length. But that's how I'd approach this. If you beat me to the modification, go for it.
@mullet456 The two points that make this not as much of a concern for me are:
1.) For me this was an internal php routine that users didn't call (I wrote a user loader to migrate eCommerce data over in batch.)
2.) This is the same vulnerability that exists within the password reset page within WordPress by default. That page itself will tell you if a username exists or not.However, your suggestion is still valid. What I'd do here is to replace the $i = 1; and i++; assignments with an i = rand() call that pays attention to the sixty character limit on usernames and probably added four zero padded random integers. I probably am not going to do this on my production system running this. Ideally, this would be _random-characters too. It's a balance between security (using all sixty characters) and convenience (not making a user type in sixty characters.)
I might modify this gist to include your suggestion when I'm on a computer (and not on my phone) and add more error checking for the length. But that's how I'd approach this. If you beat me to the modification, go for it.
updated my comment above, with solution. In regards to a password reset page if someone enters details to reset a password the page should just display a message stating something like "if there is an account registered with those details and email will have been sent with a password reset link "
@mullet456 the white space on your above is all messed up and makes it hard to read. It looks like you accidentally used tabs instead of spaces, and GitHub is mangling it.
@mullet456 the white space on your above is all messed up and makes it hard to read. It looks like you accidentally used tabs instead of spaces, and GitHub is mangling it.
@brian-stinar i have checked it on Edge, Chrome and Firefox and it is displaying ok on them for me both logged in and logged out.
@mullet456 Weird, everything for me is almost indented at the same level of indent, and then there are extra spaces around everything... Maybe this is just me, but here's what I'm seeing on both Chrome and Safari on my Mac:
@mullet456 Weird, everything for me is almost indented at the same level of indent, and then there are extra spaces around everything... Maybe this is just me, but here's what I'm seeing on both Chrome and Safari on my Mac:
![]()
Indents removed
I like this solution (with updates) a lot. For me the vulnerability of a malicious actor discovering username patterns isn't so much an issue because the users will never actually see their usernames. I'm never showing them their usernames and explicitly asking them for their email address on login but I do need a good way to generate unique usernames when they register (users are only supplying their name and emails on registration).
I do agree about the native security oversight on the default WP login form - that is something that has bothered for me awhile about WP.
The final function you've put together is perfect. Thanks guys!
What WordPress folder should I upload this php file into?
@rongenius757 you put it wherever you need to use it - probably inside your plugin code. Send me a message if you need some help on this after Google'ing my name + city.
@everyone else - WordPress already exposes usernames in a bunch of ways. Here's my favorite: https://wordpress.org/support/topic/rest-api-exposed-user-data-for-all-users/
@brian-stinar can you help me with this please? I need it to autogenerate username with digits only. The user can only logins with the username name and not emails.
@poyo12 Here's the free help version:
` function generateRandomString($username = null){
//specify characters to be used in generating random string, do not specify any characters that WordPress does not allow in the creation of usernames.
$characters = "0123456789";
.
.
... everything else the same except for this...
for ($i = 0; $i < 60; $i++) {
`
and then call it with no parameters, like generateRandomString(). Might be better to cut out all reference to $username, if you never, ever want the user to pass in what they'd like ("007" or anything...) This depends on how flexible / reusable you want this code. I'm not going to rewrite this portion during the free help session.
Since that's the first part of the username generation routine. Then, you need to disable email-based logins, probably like this:
remove_filter( 'authenticate', 'wp_authenticate_email_password', 20 );
https://www.wpbeginner.com/plugins/how-to-disable-login-with-email-address-feature-in-wordpress/
Does this make sense? I'm sure the first part of this will work, since I heavily modified this, and used it in a production system. I'm not positive about the second part, but it looks good to me with the filter removal.
Let me know if you want to move onto the paid help session, but this should get you close to where you need to be. Focus on really understanding how those portions work which I said to modify, and you should be able to do it.
@brian-stinar, i'm looking for the same thing as @poyo12 .
I had someone help me but they simply added it on the register.php from a plugin (i'm afraid that this method will not work if I change that particular plugin to manage my buddypress website).
I was wondering if there was a way to make a standalone plugin that does the same (basically, whenever a person register, a username is generated, either by incrementation from a chosen number, or by random 12 digits generation)
I'm a bit stuck.
@mronikoyi yes, the cleanest way to do this is with it's OWN plugin. The next thing that would work is to put it in your theme. It's not a good idea to put this in someone else's plugin, since then you'll have the same problems you're describing.
Here's the free help version to get you started:
https://developer.wordpress.org/plugins/intro/
and I recommend using boilerplate plugin code to get started:
https://wppb.me/
https://github.com/DevinVinson/WordPress-Plugin-Boilerplate
Send us a contact request through my consulting company if you want the non-free version of help. These (and the above tested, working code) should be everything you need to get this going.
thanks @brian-stinar !
I thought so too!
I didn't knbow about the boilerplate plugin generator! Thanks for the info !
Sure, no problem. We make a good chunk of cash off open source software, and I try and contribute how I can.
This would actually be a good plugin to write... Maybe I'll have one of my junior level people take care of this.
Added comment block.
/**
* Generates a unique username.
*
* @param string $username Username to check.
* @return string username
*/
function generate_unique_username( $username ) {
$username = sanitize_user( $username );
static $i;
if ( null === $i ) {
$i = 1;
} else {
$i ++;
}
if ( ! username_exists( $username ) ) {
return $username;
}
$new_username = sprintf( '%s-%s', $username, $i );
if ( ! username_exists( $new_username ) ) {
return $new_username;
} else {
return call_user_func( __FUNCTION__, $username );
}
}
New here on GitHub but I thought I'd share my take on this code. My website does not allow user registration. Users are currently created manually. This will allow for use of a csv file (via plugin) to load new users and ensure unique usernames. I replaced the recursive function calls with a while loop. Because of where I hooked into sanitize_user is already performed by the .../includes/user.php edit_user function. Because of hook location removal of the registration error is required.
function generate_unique_username( $errors, $update, $user ) {
$username = $user->user_login;
//strip off any trailing digits on the username (Optional, delete if functionality not desired)
static $loop = 0;
do {
$loop++;
$last = substr($username, strlen($username)-$loop);
} while ( is_numeric( $last ) );
$username = substr($username, 0, strlen($username)-($loop-1));
//iterate to find the next unused username
$i = 0;
$new_username = $username;
while ( username_exists( $new_username ) ) {
$i ++;
$new_username = sprintf( '%s%d', $username, $i );
}
//Store the new username and clear the error log for username error only
$user->user_login = $new_username;
if ($errors->has_errors()) {
if (in_array('user_login', $errors->get_error_codes()) ) {
if(count($errors->get_error_messages( 'user_login' )) == 1) {
if(str_contains($errors->get_error_message( 'user_login' ), "This username is already registered")) {
$errors->remove('user_login');
}
}
}
}
}
add_action( 'user_profile_update_errors' , 'generate_unique_username', 10 , 3 );
Perfect thank you! I also added sanitize_title to remove spacing/extra characters: