Location: PHPKode > projects > NetOffice > netoffice/includes/htpasswd.class.php
<?php // $Revision: 1.4 $
/* vim: set expandtab ts=4 sw=4 sts=4: */

/**
 * $Id: htpasswd.class.php,v 1.4 2003/10/28 22:40:46 madbear Exp $
 * 
 * Copyright (c) 2003 by the NetOffice developers
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 */

/*	**************************************************************

	Revision 0.9 1999/01/27 16:41:00 hide@address.com

	Public Methods:

		initialize	version
		sane		do_not_blame_cdi	cryptPass
		isUser		getPass				verifyUser
		changePass	addUser				genSalt
		deleteUser	getUserNum			assignPass
		renameUser

	Internal Methods:

		utime	htReadFile	htWriteFile
		error	genPass

*/

class Htpasswd {
    // Globally accessable variables
    var $VERSION = 'Revision 0.8 1999/01/17 15:20:00 hide@address.com'; 
    // var $UID		= getmyuid();
    // Set this to the user ID of the process
    // this program runs as. Used for sanity
    // checking. Defaults to same ID as
    // the file calling it.
    // UID deprecated 0.9 - it was used for some of the more anal
    // sane() routines - forgot to take it out in 0.7 when I should have
    var $WIN32 = false; // Set to true for M$ BloatWare servers
    
    var $FILE = ""; // Filename Holder
    var $ERROR = ""; // Last error message
    var $EMPTY = false; // Is the FILE empty?
    var $CONTENTS = ""; // Raw htpasswd contents
    var $EXISTS = false; // Boolean. True if $FILE exists
    var $SANE = false; // Boolean. True if $FILE passes all tests
    var $IDIOT = false; // Boolean. True if user is an idiot.
    var $DEBUG = false; // Boolean. Logs errors to error_log if set
    var $USERS = array(); // Array of [index#][(user|pass)]=value
    var $USERCOUNT = 0; // Counter - total number of users in $FILE 
    // Zero based indexing on $USERS
    // **************************************************************
    // An auto-constructor, can initilize the filename when
    // called from new()
    function Htpasswd ($passwdFile = "")
    {
        if (!empty($passwdFile)) {
            $this->initialize($passwdFile);
        } 
        return;
    } 
    // **************************************************************
    // The Initialize function sets up the FILE, checks it
    // for sanity, then loads it into the processes memory
    // htReadFile() should only be called using this method.
    function initialize ($passwdFile)
    {
        $this->FILE = $passwdFile;

        srand((double)microtime() * 1000000); // Seed the random number gen
        
        if (empty($passwdFile)) {
            // PHP is going to bitch about this, this is here just because
            $this->error("Invalid initialize() or new() method: No file specified!", 1);
            exit; // Just in case
        } 

        if (file_exists($this->FILE)) {
            $this->EXISTS = true;
            if ($this->sane($this->FILE)) {
                $this->SANE = true;
                $this->htReadFile();
            } else {
                // Preserve the error generated by sane()
                return;
            } 
        } else {
            $this->SANE = true; // Non-existant files are safe
        } 
        return;
    } 
    // **************************************************************
    // Turns off sanity checking. Needless to say if you do this
    // you're an idiot, but I'll give you the rope...
    function do_not_blame_cdi ()
    {
        $this->IDIOT = true;
        $this->error("No sanity checking on files", 0);
        return;
    } 
    // **************************************************************
    // Checks file sanity. Can be called publicly, giving the
    // full path to the file to be checked.
    // Can be disabled if you're an idiot by
    // calling $Htpasswd->do_not_blame_cdi()
    // Tons of junk removed Rev 0.7
    function sane ($filename)
    {
        if ($this->IDIOT) {
            return true;
        } 
        // If it's a Win32 box, there's no sense in doing all this
        if ($this->WIN32) {
            // You're on your own
            return true;
        } 
        // Some kind of *nix machine - let's do some
        // rudimentary checks
        if (!(is_readable($filename))) {
            $this->error("File [$filename] not readable", 0);
            return false;
        } 
        if (!(is_writeable($filename))) {
            $this->error("File [$filename] not writeable", 0);
            return false;
        } 
        if (is_dir($filename)) {
            $this->error("File [$filename] is a directory", 0);
            return false;
        } 
        if (is_link($filename)) {
            $this->error("File [$filename] is a symlink", 0);
            return false;
        } 
        // I had a lot of routines in here to do a lot of checking
        // on the file permissions and you know what? That
        // ain't my job. It's yours.
        // File is assumed to be sane - too bad I'm not.
        return true;
    } 
    // **************************************************************
    // Not really needed but it's a legacy thing...
    function version ()
    {
        return $this->VERSION;
    } 
    // **************************************************************
    // Error handling. Fatals immediately exit the program (very
    // few errors generate a fatal exit. Most just carp a warning
    // and continue. Logged via error_log method.
    function error ($errMsg, $die)
    {
        $this->ERROR = $errMsg; 
        // croak or carp?
        // If logging is turned off AND this is not
        // a Fatal error, just return
        if ((!($this->DEBUG)) && ($die != 1)) {
            return;
        } 

        if ($this->DEBUG) {
            error_log($this->ERROR, 0);
        } 

        if ($die == 1) {
            echo "<B> ERROR $this->ERROR </B> <BR> \n";
            exit;
        } 

        return;
    } 
    // **************************************************************
    // Internal function to read the FILE and process it's contents
    // Can be called publicly to re-read the file, but why would
    // you want to introduce another series of system calls like that?
    // This does the lions share of the work. This should only be
    // called once per process, and it should be called internally
    // by the initialize method. Have I mentioned that enough yet?
    function htReadFile ()
    {
        global $php_errormsg;

        $Mytemp = array();
        $Myjunk = array();
        $Junk = array();
        $count = 0;
        $user = "";
        $pass = "";
        $temp = "";
        $key = "";
        $val = "";
        $filesize = 0;
        $errno = 0;
        $empty = false;
        $contents = "";

        $filename = $this->FILE;
        $filesize = filesize($filename);

        if ($filesize < 3) {
            $empty = true;
        } 
        // Why did I pick 3? I dunno - seemed like the number
        // to use at the time.
        // (Actually, think [char]:[\n], the absolute smallest
        // size a "legitimate" password file can ever be.)
        if (!($empty)) {
            $this->EMPTY = false;

            $fd = fopen($filename, "r");

            if (empty($fd)) {
                $this->error("FATAL File access error [$php_errormsg]", 1);
                exit; // Just in case
            } 

            $contents = fread($fd, filesize($filename));
            fclose($fd);

            $this->CONTENTS = $contents;
            $Mytemp = split("\n", $contents);
            for($count = 0;$count < count($Mytemp);$count++) {
                $user = "";
                $pass = "";

                if (empty($Mytemp[$count])) {
                    break;
                } 
                if (ereg("^(\n|\W)(.?)", $Mytemp[$count])) {
                    break;
                } 

                if (!(ereg(":", $Mytemp[$count]))) {
                    $user = $Mytemp[$count];
                    $errno = ($count + 1);
                    $this->error("FATAL invalid user [$user] on line [$errno] in [$filename]", 1);
                } 

                list ($user, $pass) = split(":", $Mytemp[$count]);

                if (($user != "") and ($pass != "")) {
                    $Myjunk[$count]["user"] = $user;
                    $Myjunk[$count]["pass"] = $pass;
                } 
            } 

            $this->USERS = $Myjunk;
            $this->USERCOUNT = $count;
        } else {
            // Empty file. Label it as such
            $this->USERS = $Myjunk;
            $this->USERCOUNT = -1;
            $this->EMPTY = true;
        } 

        return;
    } // end htReadFile()
    // **************************************************************
    // Given a plain text password and salt, returns crypt() encrypted
    // version. If salt is not passed or referenced, it will generate
    // a random salt automatically.
    function cryptPass ($passwd, $salt = "")
    {
        return $passwd;
    } // end cryptPass
    // **************************************************************
    // Returns true if UserID is found in the password file. False
    // otherwise.
    function isUser ($UserID)
    {
        $key = "";
        $val = "";
        $user = "";
        $pass = "";
        $found = false;

        if (empty($UserID)) {
            return false;
        } 
        if ($this->EMPTY) {
            return false;
        } 

        for($count = 0; $count <= $this->USERCOUNT; $count++) {
            if ($UserID == $this->USERS[$count]["user"]) {
                $found = true;
            } 
        } 

        return $found;
    } // end isUser
    // **************************************************************
    // Fetches the encrypted password from the password file and
    // returns it. Returns null on failure.
    function getPass ($UserID)
    {
        $key = "";
        $val = "";
        $user = "";
        $pass = "";
        $usernum = -1;

        if ($this->EMPTY) {
            return $pass;
        } 
        if (empty($UserID)) {
            return $pass;
        } 
        if (!($this->isUser($UserID))) {
            return $pass;
        } 

        $usernum = $this->getUserNum($UserID);
        if ($usernum == -1) {
            return false;
        } 

        $pass = $this->USERS[$usernum]["pass"];

        return $pass;
    } // end getPass
    // **************************************************************
    // Returns true if Users password matches the password in
    // the password file.
    
    // method deprecated 0.5 <cdi>
    // use verifyUser() instead
    
    function checkPass ($UserID, $Pass)
    {
        $retval = $this->verifyUser($UserID, $Pass);
        return $retval;
    } // end checkPass
    // **************************************************************
    // Returns true if Users password is authenticated, false otherwise
    
    // $Pass should be passed in un-encrypted
    function verifyUser ($UserID, $Pass)
    {
        $pass = "";
        $match = false;
        $usernum = -1;
        $salt = "";

        if ($this->EMPTY) {
            return false;
        } 
        if (empty($UserID)) {
            return false;
        } 
        if (empty($Pass)) {
            return false;
        } 
        if (!($this->isUser($UserID))) {
            return false;
        } 

        $usernum = $this->getUserNum($UserID);
        if ($usernum == -1) {
            return false;
        } 

        $pass = $this->USERS[$usernum]["pass"];
        $salt = substr($pass, 0, 2);
        $Pass = $this->cryptPass($Pass, $salt);

        if ($pass == $Pass) {
            $match = true;
        } 

        return $match;
    } // end verifyUser
    // **************************************************************
    // Changes an existing users password. If "oldPass" is null, or
    // if oldPass is not passed to this method, there is no checking
    // to be sure it matches their old password.
    
    // Needless to say, you shouldn't do dat, but I'll give you
    // the rope...
    
    // NewPass should be passed to this method un-encrypted.
    
    // Returns true on success, false on failure
    function changePass ($UserID, $newPass, $oldPass = "")
    { 
        // global $php_errormsg;
        $passwdFile = $this->FILE;
        $pass = "";
        $newname;
        $newpass; 
        // Can't very well change the password of a non-existant
        // user now can we?
        if ($this->EMPTY) {
            return false;
        } 
        if (empty($UserID)) {
            return false;
        } 

        if (!($this->isUser($UserID))) {
            // No sniffing for valid user IDs please
            $this->error("changePass failure for [$UserID]: Authentication Failure", 0);
            return false;
        } 

        if (empty($newPass)) {
            $this->error("changePass failure - no new password submitted", 0);
            return false;
        } 

        $newname = strtolower($UserID);
        $newpass = strtolower($newPass);

        if ($newname == $newpass) {
            $this->error("changePass failure: UserID and password cannot be the same", 0);
            return false;
        } 
        // If no old Password, don't force it to match
        // their existing password. NOT RECOMMENDED!
        // Be SURE to always send the oldPass!
        if (!(empty($oldPass))) {
            // Must validate the user now
            if (!($this->verifyUser($UserID, $oldPass))) {
                $this->error("changePass failure for [$UserID] : Authentication Failed", 0);
                return false;
            } 
            // OK - so the password is valid - are we planning
            // on actually changing it ?
            if ($newPass == $oldPass) {
                // Passwords are the same, no sense wasting time here
                return true;
            } 
        } 
        // Valid user with new password, OK to change.
        $usernum = $this->getUserNum($UserID);

        if ($usernum == -1) {
            return false;
        } 
        // No salt to cryptPass - generates a random one for us
        $this->USERS[$usernum]["pass"] = $this->cryptPass($newPass);

        if (!($this->htWriteFile())) {
            $this->error("FATAL could not save new password file! [$php_errormsg]", 1);
            exit; // just in case
        } 

        return true;
    } // end changePass
    // **************************************************************
    // A modified copy of changePass - changes the users name.
    // If $Pass is sent, it authenticates before allowing the change.
    // Returns true on success, false if;
    
    // The OldID is not found
    // The NewID already exists
    // The Password is sent and auth fails
    function renameUser ($OldID, $NewID, $Pass = "")
    {
        if ($this->EMPTY) {
            return false;
        } 
        if (empty($OldID)) {
            return false;
        } 
        if (empty($NewID)) {
            return false;
        } 

        if (!($this->isUser($OldID))) {
            // Send an auth failure - prevents people from fishing for
            // valid userIDs.
            // YOU will know its's because User is Unknown -
            // this error is slightly different than the real
            // authentication failure message. Compare the two.
            // Security through obscurity sucks but oh well..
            $this->error("renameUser failure for [$OldID]: Authentication Failure", 0);
            return false;
        } 
        if ($this->isUser($NewID)) {
            $this->error("Cannot change UserID, [$NewID] already exists", 0);
            return false;
        } 
        // If no Password, force a name change,
        // otherwise authenticate first.
        // Be SURE to always send the Pass!
        if (!(empty($Pass))) {
            // Must validate the user now
            if (!($this->verifyUser($OldID, $Pass))) {
                $this->error("renameUser failure for [$OldID] : Authentication Failed", 0);
                return false;
            } 
            // OK - so the password is valid - are we planning
            // on actually changing our name ?
            if ($NewID == $OldID) {
                // Nice new name ya got there Homer...
                return true;
            } 
        } 
        // Valid user, OK to change.
        $usernum = $this->getUserNum($OldID);

        if ($usernum == -1) {
            return false;
        } 

        $this->USERS[$usernum]["user"] = $NewID;

        if (!($this->htWriteFile())) {
            $this->error("FATAL could not save password file! [$php_errormsg]", 1);
            exit; // just in case
        } 

        return true;
    } // end renameUser
    // **************************************************************
    // Writes the new password file. Writes a temp file first,
    // then attempts to copy the temp file over the existing file
    // Original file not harmed if this fails.
    // Also kinda sorta gets around the lack of file locking in PHP
    // Hey, You there - PHP maintainer - FLOCK damn it! Not -everything-
    // in life is inside a friggen database.
    // On success, re-calls the initialize method to re-read
    // the new password file and returns true. False on failure
    function htWriteFile ()
    {
        global $php_errormsg;

        $filename = $this->FILE; 
        // On WIN32 box this should -still- work OK,
        // but it'll generate the tempfile in the system
        // temporary directory (usually c:\windows\temp)
        // YMMV
        $tempfile = tempnam("/tmp", "fort");

        $name = "";
        $pass = "";
        $count = 0;
        $fd;
        $myerror = "";

        if ($this->EMPTY) {
            $this->USERCOUNT = 0;
        } 

        if (!copy($filename, $tempfile)) {
            $this->error("FATAL cannot create backup file [$tempfile] [$php_errormsg]", 1);
            exit; // Just in case
        } 

        $fd = fopen($tempfile, "w");

        if (empty($fd)) {
            $myerror = $php_errormsg; // In case the unlink generates 
            // a new one - we don't care if
            // the unlink fails - we're
            // already screwed anyway
            unlink($tempfile);
            $this->error("FATAL File [$tempfile] access error [$myerror]", 1);
            exit; // Just in case
        } 

        for($count = 0; $count <= $this->USERCOUNT; $count++) {
            $name = $this->USERS[$count]["user"];
            $pass = $this->USERS[$count]["pass"];

            if (($name != "") && ($pass != "")) {
                fwrite($fd, "$name:$pass\n");
            } 
        } 

        fclose($fd);

        if (!copy($tempfile, $filename)) {
            $myerror = $php_errormsg; // Stash the error, see above
            unlink($tempfile);
            $this->error("FATAL cannot copy file [$filename] [$myerror]", 1);
            exit; // Just in case
        } 
        // Update successful
        unlink($tempfile);

        if (file_exists($tempfile)) {
            // Not fatal but it should be noted
            $this->error("Could not unlink [$tempfile] : [$php_errormsg]", 0);
        } 
        // Update the information in memory with the
        // new file contents.
        $this->initialize($filename);

        return true;
    } 
    // **************************************************************
    // Should be fairly obvious - adds a user to the htpasswd file
    // Returns true on success, false on failure
    function addUser ($UserID, $newPass)
    { 
        // global $php_errormsg;
        $count = $this->USERCOUNT;

        if (empty($UserID)) {
            $this->error("addUser fail. No UserID", 0);
            return false;
        } 
        if (empty($newPass)) {
            $this->error("addUser fail. No password", 0);
            return false;
        } 

        if ($this->isUser($UserID)) {
            $this->error("addUser fail. UserID already exists", 0);
            return false;
        } 

        if ($this->EMPTY) {
            $count = 0;
        } 

        $this->USERS[$count]["user"] = $UserID; 
        // No salt to cryptPass() - will generate a random one for us
        $this->USERS[$count]["pass"] = $this->cryptPass($newPass);

        if (!($this->htWriteFile())) {
            $this->error("FATAL could not add user due to file error! [$php_errormsg]", 1);
            exit; // Just in case
        } 
        // Successfully added user
        return true;
    } // end addUser
    // **************************************************************
    // Same as addUser, but adds the user to the password file
    // with a randomly generated password.
    
    // Returns plain text password on success, null on failure
    function assignPass ($UserID)
    {
        $pass = "";
        $count = $this->USERCOUNT;

        if (empty($UserID)) {
            $this->error("assignPass fail. No UserID", 0);
            return "";
        } 
        if ($this->EMPTY) {
            $count = 0;
        } 

        if ($this->isUser($UserID)) {
            $this->error("assignPass fail. UserID already exists. Use genPass instead", 0);
            return "";
        } 

        $pass = $this->genPass();

        $this->USERS[$count]["user"] = $UserID; 
        // No salt to cryptPass() - will generate a random one for us
        $this->USERS[$count]["pass"] = $this->cryptPass($pass);

        if (!($this->htWriteFile())) {
            $this->error("FATAL could not add user due to file error! [$php_errormsg]", 1);
            exit; // Just in case
        } 
        // Successfully added user
        return($pass);
    } // end assignPass
    // **************************************************************
    // Again, fairly obvious - deletes a user from the htpasswd file
    // Returns true on success, false on failure
    function deleteUser ($UserID)
    { 
        // global $php_errormsg;
        $found = false; 
        // Can't delete non-existant UserIDs
        if ($this->EMPTY) {
            return false;
        } 
        if (empty($UserID)) {
            // PHP should complain about this, but just in case
            $this->error("deleteUser fail. No UserID to delete.", 0);
            return false;
        } 
        if (!($this->isUser($UserID))) {
            $this->error("Cannot delete : [$UserID] not found.", 0);
            return false;
        } 

        $usernum = $this->getUserNum($UserID);

        if ($usernum == -1) {
            return false;
        } 

        $this->USERS[$usernum]["user"] = "";
        $this->USERS[$usernum]["pass"] = "";

        if (!($this->htWriteFile())) {
            $this->error("FATAL could not remove user due to file error! [$php_errormsg]", 1);
            exit; // Just in case
        } 
        // Successfully deleted user
        return true;
    } // end deleteUser
    // **************************************************************
    // Returns the user's UserID in the password file.
    // (Glorified line number)
    // Returns -1 if not found or errors
    function getUserNum ($UserID)
    {
        $count = 0;
        $usernum = -1;
        $name = "";

        if ($this->EMPTY) {
            return $usernum;
        } 
        if (empty($UserID)) {
            return $usernum;
        } 

        if (!($this->isUser($UserID))) {
            return $usernum;
        } 

        for($count = 0; $count <= $this->USERCOUNT; $count++) {
            $name = $this->USERS[$count]["user"];

            if ($name != "") {
                if ($name == $UserID) {
                    $usernum = $count;
                    break;
                } 
            } 
        } 

        return $usernum;
    } 
    // **************************************************************
    // Calculates current microtime
    function utime()
    {
        $time = explode(" ", microtime());
        $usec = (double)$time[0];
        $sec = (double)$time[1];
        return $sec + $usec;
    } 
    // **************************************************************
    // Generates a pseudo random 2 digit salt. Method will
    // generate different salts when called multiple times by
    // the same process.
    function genSalt ()
    {
        $random = 0;
        $rand64 = "";
        $salt = "";

        $random = rand(); // Seeded via initialize()
         
        // Crypt(3) can only handle A-Z a-z ./
        $rand64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
        $salt = substr($rand64, $random % 64, 1) . substr($rand64, ($random / 64) % 64, 1);
        $salt = substr($salt, 0, 2); // Just in case
        
        return($salt);
    } 
    // **************************************************************
    // Generates a pseudo random 5 to 8 digit password. Method will even
    // generate different passwords when called multiple times by
    // the same process.
    function genPass ()
    {
        $random = 0;
        $rand78 = "";
        $randpass = "";
        $pass = "";

        $maxcount = rand(4, 9); 
        // The rand() limits (min 4, max 9) don't actually limit the number
        // returned by rand, so keep looping until we have a password that's
        // more than 4 characters and less than 9.
        if (($maxcount > 8) or ($maxcount < 5)) {
            do {
                $maxcount = rand(4, 9);
            } while (($maxcount > 8) or ($maxcount < 5));
        } 

        $rand78 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()-=_+abcdefghijklmnopqrstuvwxyz";
        for($count = 0; $count <= $maxcount; $count++) {
            $random = rand(0, 77);
            $randpass = substr($rand78, $random, 1);
            $pass = $pass . $randpass;
        } 

        $pass = substr($pass, 0, 8); // Just in case
        
        return($pass);
    } // end genPass
    // **************************************************************
    // Generates a pseudo random 5 to 8 digit User ID. Method will
    // generate different User IDs when called multiple times by
    // the same process.
    function genUser ()
    {
        $random = 0;
        $rand78 = "";
        $randuser = "";
        $userid = "";

        $maxcount = rand(4, 9);

        if (($maxcount > 8) or ($maxcount < 5)) {
            do {
                $maxcount = rand(4, 9);
            } while (($maxcount > 8) or ($maxcount < 5));
        } 

        $rand62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
        for($count = 0; $count <= $maxcount; $count++) {
            $random = rand(0, 61);
            $randuser = substr($rand62, $random, 1);
            $userid = $userid . $randuser;
        } 

        $userid = substr($userid, 0, 8); // Just in case
        
        return($userid);
    } // end genUser
    // **************************************************************
    // **************************************************************
} // END CLASS.HTPASSWD

?>
Return current item: NetOffice