# Adding, removing, and changing user names and passwords in .htpasswd with PHP

Here is some code I wrote to add, remove, and change user names and passwords in .htpasswd with PHP:

function adduser($user,$pass) {

try {

$htpasswd = '.htpasswd'; //$hash = '{SHA}'.crypt($pass, base64_encode($pass));

$hash = crypt_apr1_md5($pass); //APR1-MD5

$contents =$user . ':' . $hash;$lines = explode(PHP_EOL, file_get_contents($htpasswd)); // get .htpasswd print('<h4>input:</h4><pre>'.print_r(implode(PHP_EOL,$lines),true).'</pre>');

$exists = false; foreach($lines as $line){$existing_user = explode( ':', $line ); if ($existing_user[0] == $user) { //checks if user exists$contents = str_replace($line,$contents, $lines); //changes password for user$contents = implode(PHP_EOL, $contents);$exists = true;

if ($pass == '') { // removes user if password is empty$contents = str_replace($line, '',$lines); //removes user

$contents = array_filter($contents); // cleans empty space in array

$contents = implode(PHP_EOL,$contents);

$exists = true; } } } if ($exists == false) {

$contents = implode(PHP_EOL,$lines) . PHP_EOL . $contents; } file_put_contents($htpasswd, $contents); print('<h4>output:</h4><pre>'.print_r($contents,true).'</pre>');

}catch(Exception $e) { echo '<h3>fail: </h3>' .$e->getMessage();

}

}

if(isset($_GET['user'])){ adduser($_GET['user'], $_GET['pass']); echo '<h3>success</h3>'; }else{$htpasswd = '.htpasswd';

$lines = explode(PHP_EOL, file_get_contents($htpasswd)); // get .htpasswd

print('<h4>.htpasswd:</h4><pre>'.print_r(implode(PHP_EOL, $lines),true).'</pre>'); echo '<h3>no user set</h3>'; }  I would love to streamline it and clean it up. ## 2 Answers Here is my version based on two personal preferences: I hate scroll bars and I hate repititions. Hence I prefer everything typed once and also I like my code being fully visible in the default code area on Stack Overflow. • for this reason I removed that double spacing which just hurts my eyes • also I removed the try catch which is a cargo cult code that makes no sense. I was never able to understand what's the point in writing a try catch that's the only job is to echo the error message when without a try catch PHP will do exactly the same - echo the error message • also I removed the code repetitions such as mentioning the filename in a dozen places • also I changed the algorithm, to make it add a user line only once • I also removed that uncertainty when we can't make our mind whether we are working with an array or with a text • I also changed the function name as it doesn't only add a user • and some other improvements such as following the HTTP guidelines and the ability to choose the hashing algorithm without using inline comments here it goes <?php$filename = ".htpasswd";
if (isset($_POST['user'])) { manage_htpasswd($_POST['user'], $_POST['pass'],$filename);
header("Location: ".$_SERVER['REQUEST_URI']); exit; } function manage_htpasswd($user, $pass,$filename, $algo = 'crypt_apr1_md5') {$lines = file($filename); foreach ($lines as $i =>$line) {
$existing_user = explode(':',$line);
if ($existing_user[0] ===$user) {
unset($lines[$i]);
break;
}
}
if ($pass) {$lines[] = "$user:" .$algo($pass) . PHP_EOL; } file_put_contents($filename, $lines); } ?> <form method="post"> User: <input type="text" name="user"><br> Pass:<input type="text" name="pass"><br> <input type="submit"> </form> <h4><?=$filename ?>:</h4>
<pre>
<?= file_get_contents($filename) ?> </pre>  • So instead of updating a row, you delete it then append it to the end of the file. Fair enough. – mickmackusa Nov 30 '20 at 4:08 • @mickmackusa yeah I was thinking how to get rid of that found/not found business :) – Your Common Sense Nov 30 '20 at 4:10 • I think I like your non-regex versus my regex technique. Perhaps use strstr() with a true param instead of exploding to create $existing_user from an array (this way you don't even need to declare the variable). Your snippet is still making iterated function calls, but it does return early -- looks pretty clean to me and built with flexibility in mind. – mickmackusa Nov 30 '20 at 4:15
• @tony wait. indeed it does that. let me check – Your Common Sense Dec 3 '20 at 6:31
• @tony I beg my pardon. yes, my bad. there must be a PHP_EOL. So, when we are moving from a text to an array, every line in this array holds a new line symbol. when file_put_contents converts an array back to text, it just glues all the lines together. if a line holds a new line symbol, then the next one occurs on the new line. But once the line doesn't have a new line at the end, then the next line is glued to it. So every line should have a new line at the end. Means we have to add it to the new line. – Your Common Sense Dec 3 '20 at 6:37

For the sake of your code and mine, I hope that the usernames cannot contain any colons since that is the delimiting character between usernames and passwords!

I have a bias toward regex because I have a fair handle on it and I enjoy the utility and brevity that it affords my scripts. I also don't (personally) enjoy all of the imploding and exploding going on in your script.

The search pattern is the same for cases of deleting and updating -- only the replacement text is changed. My search pattern will look for an optional leading newline character/sequence with \R, then search for an identical match of the username followed by a colon, then match the remainder of the line of text. This line-consuming pattern means that if replacing with an empty string, then there will be no blank line in the file; alternatively, if updating, then a leading EOL character/sequence will be prepended (don't worry, I ltrim() later).

The single preg_replace() call will record the number of replacements that it makes. The number will be either 0 or 1 since the fourth parameter limits the replacements to 1 anyhow. If there were no replacements made, then logically we know that a new line is to be appended to the end of the file.

At the end of the custom function, I am going the extra step of returning the action that was successfully undertaken. The will give better information in the output.

isset() can receive multiple arguments, so I added the pass element as well since it is expected with the submission.

I am using printf() to output the mix of literal and dynamic text -- I find that it helps to make the code more readable.

Untested Code:

function editHtpasswordRow(string $user, string$pass): string
{
$file = '.htpasswd'; if ($pass === '') {
$newRow = '';$action = 'Delete';
} else {
$newRow = PHP_EOL .$user . ':' . crypt_apr1_md5($pass);$action = 'Update';
}

$content = preg_replace( '/\R?^' . preg_quote($user, '/') . ':.*/mu',
$newRow, file_get_contents($file),
1,
$count ); if (!$count && $newRow) {$content .= PHP_EOL . $newRow;$action = 'Insert';
}
file_put_contents($file, ltrim($content));
return $action; } if (isset($_POST['user'], $_POST['pass'])) { printf( '<h3>%s of %s was successful</h3>', editHtpasswordRow($_POST['user'], $_POST['pass']), htmlspecialchars($_POST['user'])
);
} else {
echo '<h4>Fetched .htpasswd content:</h4><pre>' . file_get_contents('.htpasswd') . '</pre>';
}


Edit: I missed something that YourCommonSense spotted... You should be using $_POST when writing to the filesystem. $_GET is for reading and \$_POST is for writing. I'll update my snippet now, +1 his post, and advise you to use his html form.