<?php
#Application name: PhpCollab
#Status page: 2
#Path by root: ../includes/htpasswd.class.php

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

	Revision 0.9 1999/01/27 16:41:00 cdi@thewebmasters.net

	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 cdi@thewebmasters.net';

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



?>
