<?PHP
#
#   User.php
#   An Object for Handling User Information
#
#   Copyright 1999-2001 Axis Data
#   This code is free software that can be used or redistributed under the
#   terms of Version 2 of the GNU General Public License, as published by the
#   Free Software Foundation (http://www.fsf.org).
#
#   Author:  Edward Almasy (almasy@axisdata.com)
#
#   Part of the AxisPHP library v1.2.4
#   For more information see http://www.axisdata.com/AxisPHP/
#

# status values (error codes)
define("U_OKAY", 0);
define("U_ERROR", 1);
define("U_BADPASSWORD", 2);
define("U_NOSUCHUSER", 3);
define("U_PASSWORDSDONTMATCH", 4);
define("U_EMAILSDONTMATCH", 5);
define("U_DUPLICATEUSERNAME", 6);
define("U_ILLEGALUSERNAME", 7);
define("U_EMPTYUSERNAME", 8);
define("U_ILLEGALPASSWORD", 9);
define("U_ILLEGALPASSWORDAGAIN", 10);
define("U_EMPTYPASSWORD", 11);
define("U_EMPTYPASSWORDAGAIN", 12);
define("U_ILLEGALEMAIL", 13);
define("U_ILLEGALEMAILAGAIN", 14);
define("U_EMPTYEMAIL", 15);
define("U_EMPTYEMAILAGAIN", 16);
define("U_NOTLOGGEDIN", 17);
define("U_MAILINGERROR", 18);
define("U_TEMPLATENOTFOUND", 19);
define("U_DUPLICATEEMAIL", 20);
define("U_NOTACTIVATED", 21);

class User
{
    # ---- PUBLIC INTERFACE --------------------------------------------------

    public function __construct($UserInfoOne = NULL, $UserInfoTwo = NULL)
    {
        # assume constructor will succeed and user is not logged in
        $this->Result = U_OKAY;
        $this->LoggedIn = FALSE;

        # create database connection
        $this->DB = new Database();

        # if user info passed in
        if (is_int($UserInfoOne) || is_string($UserInfoOne)
                || is_int($UserInfoTwo) || is_string($UserInfoTwo))
        {
            # if user ID was passed in
            if (is_int($UserInfoOne) || is_int($UserInfoTwo))
            {
                # save user ID
                $this->UserId = is_int($UserInfoOne) ? $UserInfoOne : $UserInfoTwo;

                # get whether the user is logged in
                $this->LoggedIn = (bool)$this->DB->Query("
                    SELECT LoggedIn FROM APUsers
                    WHERE UserId='".addslashes($this->UserId)."'",
                    "LoggedIn");
            }
            else
            {
                # look up user ID in database
                $UserInfoTwo = is_string($UserInfoOne) ? $UserInfoOne : $UserInfoTwo;
                $this->DB->Query("SELECT UserId, LoggedIn FROM APUsers"
                        ." WHERE UserName='".addslashes($UserInfoTwo)."'");
                $Record = $this->DB->FetchRow();
                if ($Record)
                {
                    $this->UserId = $Record["UserId"];
                    $this->LoggedIn = $Record["LoggedIn"];
                }

                # if user ID was not found
                if ($Record === FALSE)
                {
                    # if name looks like it could actually be a user ID
                    if (preg_match("/^[-]*[0-9]+$/", $UserInfoTwo))
                    {
                        # assume name was user ID
                        $this->UserId = intval($UserInfoTwo);
                    }
                    else
                    {
                        # set code indicating no user found
                        $this->Result = U_NOSUCHUSER;
                        unset($this->UserId);
                    }
                }
            }
        }
        else
        {
            # if user ID is available from session
            if (isset($_SESSION["APUserId"]))
            {
                # save user ID
                $this->UserId = $_SESSION["APUserId"];

                # set flag indicating user is currently logged in
                $this->LoggedIn = TRUE;
            }
        }
    }

    public function Status()
    {
        return $this->Result;
    }

    # return text message corresponding to current (or specified) status code
    public function StatusMessage($StatusCode = NULL)
    {
        $APUserStatusMessages = array(
                U_OKAY                => "The operation was successful.",
                U_ERROR               => "There has been an error.",
                U_BADPASSWORD         => "The password you entered was"
                                            ." incorrect.",
                U_NOSUCHUSER          => "No such user name was found.",
                U_PASSWORDSDONTMATCH  => "The new passwords you entered do"
                                            ." not match.",
                U_EMAILSDONTMATCH     => "The e-mail addresses you entered"
                                            ." do not match.",
                U_DUPLICATEUSERNAME   => "The user name you requested is"
                                            ." already in use.",
                U_ILLEGALUSERNAME     => "The user name you requested is too"
                                            ." short, too long, or contains"
                                            ." illegal characters.",
                U_ILLEGALPASSWORD     => "The new password you requested is"
                                            ." too short, too long, or"
                                            ." contains illegal characters.",
                U_ILLEGALEMAIL        => "The e-mail address you entered"
                                            ." appears to be invalid.",
                U_NOTLOGGEDIN         => "The user is not logged in.",
                U_MAILINGERROR        => "An error occurred while attempting"
                                            ." to send e-mail.  Please notify"
                                            ." the system administrator.",
                U_TEMPLATENOTFOUND    => "An error occurred while attempting"
                                            ." to generate e-mail.  Please"
                                            ." notify the system administrator.",
                U_DUPLICATEEMAIL      => "The e-mail address you supplied already"
                                            ." has an account associated with it.",
                );

        return ($StatusCode === NULL) ? $APUserStatusMessages[$this->Result]
                : $APUserStatusMessages[$StatusCode];
    }

    public function Delete()
    {
        # clear priv list values
        $this->DB->Query("DELETE FROM APUserPrivileges WHERE UserId = '"
                .$this->UserId."'");

        # delete user record from database
        $this->DB->Query("DELETE FROM APUsers WHERE UserId = '".$this->UserId."'");

        # report to caller that everything succeeded
        $this->Result = U_OKAY;
        return $this->Result;
    }

    /**
     * Set email function to use instead of mail().
     * @param callback $NewValue Callable email function or method that accepts the
     *       same arguments as mail().
     */
    public static function SetEmailFunction($NewValue)
    {
        if (is_callable($NewValue))
        {
            self::$EmailFunc = $NewValue;
        }
    }


    # ---- Getting/Setting Values --------------------------------------------

    public function Id()
    {
        return $this->UserId;
    }
    public function Name()
    {
        return $this->Get("UserName");
    }

    /**
    * Get the best available name associated with a user, i.e., the real name
    * or, if it isn't available, the user name.
    * @return Returns the best available name for the user.
    */
    public function GetBestName()
    {
        $RealName = $this->Get("RealName");

        # the real name is available, so use it
        if (strlen(trim($RealName)))
        {
            return $RealName;
        }

        # the real name isn't available, so use the user name
        return $this->Get("UserName");
    }

    public function LastLocation($NewLocation = NULL)
    {
        # return NULL if not associated with a particular user
        if ($this->UserId === NULL) {  return NULL;  }

        if ($NewLocation)
        {
            $this->DB->Query("UPDATE APUsers SET"
                    ." LastLocation = '".addslashes($NewLocation)."',"
                    ." LastActiveDate = NOW(),"
                    ." LastIPAddress = '".$_SERVER["REMOTE_ADDR"]."'"
                    ." WHERE UserId = '".addslashes($this->UserId)."'");
            if (isset($this->DBFields))
            {
                $this->DBFields["LastLocation"] = $NewLocation;
                $this->DBFields["LastActiveDate"] = date("Y-m-d H:i:s");
            }
        }
        return $this->Get("LastLocation");
    }
    public function LastActiveDate()
    {
        return $this->Get("LastActiveDate");
    }
    public function LastIPAddress()
    {
        return $this->Get("LastIPAddress");
    }

    # get value from specified field
    public function Get($FieldName)
    {
        # return NULL if not associated with a particular user
        if ($this->UserId === NULL) {  return NULL;  }

        return $this->UpdateValue($FieldName);
    }

    # get value (formatted as a date) from specified field
    public function GetDate($FieldName, $Format = "")
    {
        # return NULL if not associated with a particular user
        if ($this->UserId === NULL) {  return NULL;  }

        # retrieve specified value from database
        if (strlen($Format) > 0)
        {
            $this->DB->Query("SELECT DATE_FORMAT(`".addslashes($FieldName)
                    ."`, '".addslashes($Format)."') AS `".addslashes($FieldName)
                    ."` FROM APUsers WHERE UserId='".$this->UserId."'");
        }
        else
        {
            $this->DB->Query("SELECT `".addslashes($FieldName)."` FROM APUsers WHERE UserId='".$this->UserId."'");
        }
        $Record = $this->DB->FetchRow();

        # return value to caller
        return $Record[$FieldName];
    }

    # set value in specified field
    public function Set($FieldName, $NewValue)
    {
        # return error if not associated with a particular user
        if ($this->UserId === NULL) {  return U_NOTLOGGEDIN;  }

        $this->UpdateValue($FieldName, $NewValue);
        $this->Result = U_OKAY;
        return $this->Result;
    }


    # ---- Login Functions ---------------------------------------------------

    public function Login($UserName, $Password, $IgnorePassword = FALSE)
    {
        # if user not found in DB
        $this->DB->Query("SELECT * FROM APUsers"
                ." WHERE UserName = '"
                        .addslashes(self::NormalizeUserName($UserName))."'");
        if ($this->DB->NumRowsSelected() < 1)
        {
            # result is no user by that name
            $this->Result = U_NOSUCHUSER;
        }
        else
        {
            # if user account not yet activated
            $Record = $this->DB->FetchRow();
            if (!$Record["RegistrationConfirmed"])
            {
                # result is user registration not confirmed
                $this->Result = U_NOTACTIVATED;
            }
            else
            {
                # grab password from DB
                $StoredPassword = $Record["UserPassword"];

                if (isset($Password[0]) && $Password[0] == " ")
                {
                    $Challenge = md5(date("Ymd").$_SERVER["REMOTE_ADDR"]);
                    $StoredPassword = md5( $Challenge . $StoredPassword );

                    $EncryptedPassword = trim($Password);
                }
                else
                {
                    # if supplied password matches encrypted password
                    $EncryptedPassword = crypt($Password, $StoredPassword);
                }

                if (($EncryptedPassword == $StoredPassword) || $IgnorePassword)
                {
                    # result is success
                    $this->Result = U_OKAY;

                    # store user ID for session
                    $this->UserId = $Record["UserId"];
                    $_SESSION["APUserId"] = $this->UserId;

                    # update last login date
                    $this->DB->Query("UPDATE APUsers SET LastLoginDate = NOW(),"
                            ." LoggedIn = '1'"
                            ." WHERE UserId = '".$this->UserId."'");

                    # Check for old format hashes, and rehash if possible
                    if ($EncryptedPassword === $StoredPassword &&
                        substr($StoredPassword, 0, 3) !== "$1$" &&
                        $Password[0] !== " " &&
                        CRYPT_MD5 )
                    {
                        $NewPassword = crypt($Password, self::GetSaltForCrypt() );
                        $this->DB->Query(
                            "UPDATE APUsers SET UserPassword='"
                                    .addslashes($NewPassword)."' "
                            ."WHERE UserId='".$this->UserId."'");
                    }

                    # since self::DBFields might already have been set to false if
                    # the  user wasn't logged in when this is called, populate it
                    # with user data so that a call to self::UpdateValue will be
                    # able to properly fetch the data associated with the user
                    $this->DBFields = $Record;

                    # set flag to indicate we are logged in
                    $this->LoggedIn = TRUE;
                }
                else
                {
                    # result is bad password
                    $this->Result = U_BADPASSWORD;
                }
            }
        }

        # return result to caller
        return $this->Result;
    }

    # log this user out
    public function Logout()
    {
        # clear user ID (if any) for session
        unset($_SESSION["APUserId"]);

        # if user is marked as logged in
        if ($this->LoggedIn)
        {
            # set flag to indicate user is no longer logged in
            $this->LoggedIn = FALSE;

            # clear login flag in database
            $this->DB->Query(
                    "UPDATE APUsers SET LoggedIn = '0' "
                    ."WHERE UserId='".$this->UserId."'");
        }
    }

    public function GetPasswordSalt($UserName)
    {
        $this->DB->Query(
            "SELECT * FROM APUsers WHERE UserName = '"
            .addslashes(self::NormalizeUserName($UserName))."'");

        if ($this->DB->NumRowsSelected() < 1)
        {
            # result is no user by that name, generate a fake salt
            # to discourage user enumeration. Make it be an old-format
            # crypt() salt so that it's harder.
            $SaltString = $_SERVER["SERVER_ADDR"].$UserName;
            $Result = substr(base64_encode(md5($SaltString)), 0, 2);
        }
        else
        {
            # grab password from DB
            # Assumes that we used php's crypt() for the passowrd
            # management stuff, and will need to be changed if we
            # go to something else.
            $Record = $this->DB->FetchRow();
            $StoredPassword = $Record["UserPassword"];

            if (substr($StoredPassword, 0, 3) === "$1$")
            {
                $Result = substr($StoredPassword, 0, 12);
            }
            else
            {
                $Result = substr($StoredPassword, 0, 2);
            }
        }

        return $Result;
    }

    # report whether this user is or is not currently logged in
    public function IsLoggedIn()
    {
        return $this->LoggedIn;
    }
    public function IsNotLoggedIn()
    {
        return !$this->LoggedIn;
    }


    # ---- Password Functions ------------------------------------------------

    # set new password (with checks against old password)
    public function ChangePassword($OldPassword, $NewPassword, $NewPasswordAgain)
    {
        # return error if not associated with a particular user
        if ($this->UserId === NULL) {  return U_NOTLOGGEDIN;  }

        # if old password is not correct
        $StoredPassword = $this->DB->Query("SELECT UserPassword FROM APUsers"
                ." WHERE UserId='".$this->UserId."'", "UserPassword");
        $EncryptedPassword = crypt($OldPassword, $StoredPassword);
        if ($EncryptedPassword != $StoredPassword)
        {
            # set status to indicate error
            $this->Result = U_BADPASSWORD;
        }
        # else if new password is not legal
        elseif (!$this->IsValidPassword($NewPassword))
        {
            # set status to indicate error
            $this->Result = U_ILLEGALPASSWORD;
        }
        # else if both instances of new password do not match
        elseif (self::NormalizePassword($NewPassword)
                != self::NormalizePassword($NewPasswordAgain))
        {
            # set status to indicate error
            $this->Result = U_PASSWORDSDONTMATCH;
        }
        else
        {
            # set new password
            $this->SetPassword($NewPassword);

            # set status to indicate password successfully changed
            $this->Result = U_OKAY;
        }

        # report to caller that everything succeeded
        return $this->Result;
    }

    # set new password
    public function SetPassword($NewPassword)
    {
        # generate encrypted password
        $EncryptedPassword = crypt(self::NormalizePassword($NewPassword),
            self::GetSaltForCrypt() );

        # save encrypted password
        $this->UpdateValue("UserPassword", $EncryptedPassword);
    }

    public function SetEncryptedPassword($NewEncryptedPassword)
    {
        # save encrypted password
        $this->UpdateValue("UserPassword", $NewEncryptedPassword);
    }

    public function CreateNewUserWithEMailedPassword(
            $UserName, $EMail, $EMailAgain,
            $TemplateFile = "Axis--User--EMailTemplate.txt")
    {
        return CreateNewUserAndMailPasswordFromFile(
                $UserName, $EMail, $EMailAgain, $TemplateFile);
    }

    public function CreateNewUserAndMailPasswordFromFile(
            $UserName, $EMail, $EMailAgain,
            $TemplateFile = "Axis--User--EMailTemplate.txt")
    {
        # load e-mail template from file (first line is subject)
        $Template = file($TemplateFile, 1);
        $EMailSubject = array_shift($Template);
        $EMailBody = join("", $Template);

        return CreateNewUserAndMailPassword(
                $UserName, $EMail, $EMailAgain, $EMailSubject, $EMailBody);
    }

    public function CreateNewUserAndMailPassword(
            $UserName, $EMail, $EMailAgain, $EMailSubject, $EMailBody)
    {
        # make sure e-mail addresses match
        if ($EMail != $EMailAgain)
        {
            $this->Result = U_EMAILSDONTMATCH;
            return $this->Result;
        }

        # make sure e-mail address looks valid
        if ($this->IsValidLookingEMailAddress($EMail) == FALSE)
        {
            $this->Result = U_ILLEGALEMAIL;
            return $this->Result;
        }

        # generate random password
        $Password = $this->GetRandomPassword();

        # attempt to create new user with password
        $Result = $this->CreateNewUser($UserName, $Password, $Password);

        # if user creation failed
        if ($Result != U_OKAY)
        {
            # report error result to caller
            return $Result;
        }
        # else
        else
        {
            # set e-mail address in user record
            $this->Set("EMail", $EMail);

            # plug appropriate values into subject and body of e-mail message
            $EMailSubject = str_replace("X-USERNAME-X", $UserName, $EMailSubject);
            $EMailBody = str_replace("X-USERNAME-X", $UserName, $EMailBody);
            $EMailBody = str_replace("X-PASSWORD-X", $Password, $EMailBody);

            # send out e-mail message with new account info
            if (is_Callable(self::$EmailFunc))
            {
                $Result = call_user_func(self::$EmailFunc,
                        $EMail, $EMailSubject, $EMailBody,
                        "Auto-Submitted: auto-generated");
            }
            else
            {
                $Result = mail($EMail, $EMailSubject, $EMailBody,
                        "Auto-Submitted: auto-generated");
            }

            # if mailing attempt failed
            if ($Result != TRUE)
            {
                # report error to caller
                $this->Result = U_MAILINGERROR;
                return $this->Result;
            }
            # else
            else
            {
                # report success to caller
                $this->Result = U_OKAY;
                return $this->Result;
            }
        }
    }

    # get code for user to submit to confirm registration
    public function GetActivationCode()
    {
        # code is MD5 sum based on user name and encrypted password
        $ActivationCodeLength = 6;
        return $this->GetUniqueCode("Activation", $ActivationCodeLength);
    }

    # check whether confirmation code is valid
    public function IsActivationCodeGood($Code)
    {
        return (strtoupper(trim($Code)) == $this->GetActivationCode())
                ? TRUE : FALSE;
    }

    # get/set whether user registration has been confirmed
    public function IsActivated($NewValue = DB_NOVALUE)
    {
        return $this->UpdateValue("RegistrationConfirmed", $NewValue);
    }

    # get code for user to submit to confirm password reset
    public function GetResetCode()
    {
        # code is MD5 sum based on user name and encrypted password
        $ResetCodeLength = 10;
        return $this->GetUniqueCode("Reset", $ResetCodeLength);
    }

    # check whether password reset code is valid
    public function IsResetCodeGood($Code)
    {
        return (strtoupper(trim($Code)) == $this->GetResetCode())
                ? TRUE : FALSE;
    }

    # get code for user to submit to confirm mail change request
    public function GetMailChangeCode()
    {
        $ResetCodeLength = 10;
        return $this->GetUniqueCode("MailChange".$this->Get("EMail")
                    .$this->Get("NewEMail"),
              $ResetCodeLength);
    }

    public function IsMailChangeCodeGood($Code)
    {
        return (strtoupper(trim($Code)) == $this->GetMailChangeCode())
                ? TRUE : FALSE;
    }

    # send e-mail to user (returns TRUE on success)
    public function SendEMail(
                  $TemplateTextOrFileName, $FromAddress = NULL, $MoreSubstitutions = NULL,
                  $ToAddress = NULL)
    {
        # if template is file name
        if (@is_file($TemplateTextOrFileName))
        {
            # load in template from file
            $Template = file($TemplateTextOrFileName, 1);

            # report error to caller if template load failed
            if ($Template == FALSE)
            {
                $this->Status = U_TEMPLATENOTFOUND;
                return $this->Status;
            }

            # join into one text block
            $TemplateTextOrFileName = join("", $Template);
        }

        # split template into lines
        $Template = explode("\n", $TemplateTextOrFileName);

        # strip any comments out of template
        $FilteredTemplate = array();
        foreach ($Template as $Line)
        {
            if (!preg_match("/^[\\s]*#/", $Line))
            {
                $FilteredTemplate[] = $Line;
            }
        }

        # split subject line out of template (first non-comment line in file)
        $EMailSubject = array_shift($FilteredTemplate);
        $EMailBody = join("\n", $FilteredTemplate);

        # set up our substitutions
        $Substitutions = array(
                "X-USERNAME-X" => $this->Get("UserName"),
                "X-EMAILADDRESS-X" =>  $this->Get("EMail"),
                "X-ACTIVATIONCODE-X" => $this->GetActivationCode(),
                "X-RESETCODE-X" => $this->GetResetCode(),
                "X-CHANGECODE-X" => $this->GetMailChangeCode(),
                "X-IPADDRESS-X" => @$_SERVER["REMOTE_ADDR"],
                );

        # if caller provided additional substitutions
        if (is_array($MoreSubstitutions))
        {
            # add in entries from caller to substitution list
            $Substitutions = array_merge(
                    $Substitutions, $MoreSubstitutions);
        }

        # perform substitutions on subject and body of message
        $EMailSubject = str_replace(array_keys($Substitutions),
                array_values($Substitutions), $EMailSubject);
        $EMailBody = str_replace(array_keys($Substitutions),
                array_values($Substitutions), $EMailBody);

        $AdditionalHeaders = "Auto-Submitted: auto-generated";

        # if caller provided "From" address
        if ($FromAddress)
        {
            # prepend "From" address onto message
            $AdditionalHeaders .= "\r\nFrom: ".$FromAddress;
        }

        # send out mail message
        if (is_Callable(self::$EmailFunc))
        {
            $Result = call_user_func(self::$EmailFunc,
                    is_null($ToAddress)?$this->Get("EMail"):$ToAddress,
                    $EMailSubject, $EMailBody, $AdditionalHeaders);
        }
        else
        {
            $Result = mail(is_null($ToAddress)?$this->Get("EMail"):$ToAddress,
                    $EMailSubject,
                    $EMailBody, $AdditionalHeaders);
        }

        # report result of mailing attempt to caller
        $this->Status = ($Result == TRUE) ? U_OKAY : U_MAILINGERROR;
        return ($this->Status == U_OKAY);
    }


    # ---- Privilege Functions -----------------------------------------------

    /**
     * Check whether user has specified privilege(s).
     * @param mixed $Privilege Privilege or array of privileges.
     * @param mixed $Privileges One or more additional privileges.  (variable length
     *       argument list) (OPTIONAL)
     * @return TRUE if user has one or more of specified privilege(s),
     *       otherwise FALSE.
     */
    public function HasPriv($Privilege, $Privileges = NULL)
    {
        # return FALSE if not associated with a particular user
        if ($this->UserId === NULL) {  return FALSE;  }

        # bail out if empty array of privileges passed in
        if (is_array($Privilege) && !count($Privilege) && (func_num_args() < 2))
                {  return FALSE;  }

        # set up beginning of database query
        $Query = "SELECT COUNT(*) AS PrivCount FROM APUserPrivileges "
                        ."WHERE UserId='".$this->UserId."' AND (";

        # add first privilege(s) to query (first arg may be single value or array)
        if (is_array($Privilege))
        {
            $Sep = "";
            foreach ($Privilege as $Priv)
            {
                $Query .= $Sep."Privilege='".addslashes($Priv)."'";
                $Sep = " OR ";
            }
        }
        else
        {
            $Query .= "Privilege='".$Privilege."'";
            $Sep = " OR ";
        }

        # add any privileges from additional args to query
        $Args = func_get_args();
        array_shift($Args);
        foreach ($Args as $Arg)
        {
            $Query .= $Sep."Privilege='".$Arg."'";
            $Sep = " OR ";
        }

        # close out query
        $Query .= ")";

        # look for privilege in database
        $PrivCount = $this->DB->Query($Query, "PrivCount");

        # return value to caller
        return ($PrivCount > 0) ? TRUE : FALSE;
    }

    /**
    * Get an SQL query that will return IDs of all users that have the specified
    * privilege flags.  This method is useful primarily for subqueries.
    * @param mixed $Privilege Privilege or array of privileges.
    * @param mixed $Privileges One or more additional privileges.  (variable length
    *       argument list) (OPTIONAL)
    * @return SQL query to retrieve user IDs.
    */
    public static function GetSqlQueryForUsersWithPriv($Privilege, $Privileges = NULL)
    {
        # set up beginning of database query
        $Query = "SELECT DISTINCT UserId FROM APUserPrivileges "
                        ."WHERE ";

        # add first privilege(s) to query (first arg may be single value or array)
        if (is_array($Privilege))
        {
            $Sep = "";
            foreach ($Privilege as $Priv)
            {
                $Query .= $Sep."Privilege='".addslashes($Priv)."'";
                $Sep = " OR ";
            }
        }
        else
        {
            $Query .= "Privilege='".$Privilege."'";
            $Sep = " OR ";
        }

        # add any privileges from additional args to query
        $Args = func_get_args();
        array_shift($Args);
        foreach ($Args as $Arg)
        {
            $Query .= $Sep."Privilege='".$Arg."'";
            $Sep = " OR ";
        }

        # return query to caller
        return $Query;
    }

    /**
    * Get an SQL query that will return IDs of all users that do not have the
    * specified privilege flags.  This method is useful primarily for subqueries.
    * @param mixed $Privilege Privilege or array of privileges.
    * @param mixed $Privileges One or more additional privileges.  (variable length
    *       argument list) (OPTIONAL)
    * @return SQL query to retrieve user IDs.
    */
    public static function GetSqlQueryForUsersWithoutPriv($Privilege, $Privileges = NULL)
    {
        # set up beginning of database query
        $Query = "SELECT DISTINCT UserId FROM APUserPrivileges "
                        ."WHERE ";

        # add first privilege(s) to query (first arg may be single value or array)
        if (is_array($Privilege))
        {
            $Sep = "";
            foreach ($Privilege as $Priv)
            {
                $Query .= $Sep."Privilege != '".addslashes($Priv)."'";
                $Sep = " AND ";
            }
        }
        else
        {
            $Query .= "Privilege != '".$Privilege."'";
            $Sep = " AND ";
        }

        # add any privileges from additional args to query
        $Args = func_get_args();
        array_shift($Args);
        foreach ($Args as $Arg)
        {
            $Query .= $Sep."Privilege != '".$Arg."'";
            $Sep = " AND ";
        }

        # return query to caller
        return $Query;
    }

    public function GrantPriv($Privilege)
    {
        # return error if not associated with a particular user
        if ($this->UserId === NULL) {  return U_NOTLOGGEDIN;  }

        # if privilege value is invalid
        if (intval($Privilege) != trim($Privilege))
        {
            # set code to indicate error
            $this->Result = U_ERROR;
        }
        else
        {
            # if user does not already have privilege
            $PrivCount = $this->DB->Query("SELECT COUNT(*) AS PrivCount"
                    ." FROM APUserPrivileges"
                    ." WHERE UserId='".$this->UserId."'"
                    ." AND Privilege='".$Privilege."'",
                    "PrivCount");
            if ($PrivCount == 0)
            {
                # add privilege for this user to database
                $this->DB->Query("INSERT INTO APUserPrivileges"
                        ." (UserId, Privilege) VALUES"
                        ." ('".$this->UserId."', ".$Privilege.")");
            }

            # set code to indicate success
            $this->Result = U_OKAY;
        }

        # report result to caller
        return $this->Result;
    }

    public function RevokePriv($Privilege)
    {
        # return error if not associated with a particular user
        if ($this->UserId === NULL) {  return U_NOTLOGGEDIN;  }

        # remove privilege from database (if present)
        $this->DB->Query("DELETE FROM APUserPrivileges"
                         ." WHERE UserId = '".$this->UserId."'"
                         ." AND Privilege = '".$Privilege."'");

        # report success to caller
        $this->Result = U_OKAY;
        return $this->Result;
    }

    public function GetPrivList()
    {
        # return empty list if not associated with a particular user
        if ($this->UserId === NULL) {  return array();  }

        # read privileges from database and return array to caller
        $this->DB->Query("SELECT Privilege FROM APUserPrivileges"
                ." WHERE UserId='".$this->UserId."'");
        return $this->DB->FetchColumn("Privilege");
    }

    public function SetPrivList($NewPrivileges)
    {
        # return error if not associated with a particular user
        if ($this->UserId === NULL) {  return U_NOTLOGGEDIN;  }

        # clear old priv list values
        $this->DB->Query("DELETE FROM APUserPrivileges"
                ." WHERE UserId='".$this->UserId."'");

        # for each priv value passed in
        foreach ($NewPrivileges as $Privilege)
        {
            # set priv for user
            $this->GrantPriv($Privilege);
        }
    }


    # ---- Miscellaneous Functions -------------------------------------------

    # get unique alphanumeric code for user
    public function GetUniqueCode($SeedString, $CodeLength)
    {
        # return NULL if not associated with a particular user
        if ($this->UserId === NULL) {  return NULL;  }

        return substr(strtoupper(md5(
                $this->Get("UserName").$this->Get("UserPassword").$SeedString)),
                0, $CodeLength);
    }


    # ---- PRIVATE INTERFACE -------------------------------------------------

    protected $DB;      # handle to SQL database we use to store user information
    protected $UserId = NULL;    # user ID number for reference into database
    protected $Result;    # result of last operation
    protected $LoggedIn;  # flag indicating whether user is logged in
    private $DBFields;  # used for caching user values

    # optional mail function to use instead of mail()
    private static $EmailFunc = NULL;

    # check whether a user name is valid  (alphanumeric string of 2-24 chars)
    public static function IsValidUserName($UserName)
    {
        if (preg_match("/^[a-zA-Z0-9]{2,24}$/", $UserName))
        {
            return TRUE;
        }
        else
        {
            return FALSE;
        }
    }

    # check whether a password is valid  (at least 6 characters)
    public static function IsValidPassword($Password)
    {
        if (strlen(self::NormalizePassword($Password)) < 6)
        {
            return FALSE;
        }
        else
        {
            return TRUE;
        }
    }

    # check whether an e-mail address looks valid
    public static function IsValidLookingEMailAddress($EMail)
    {
        if (preg_match("/^[a-zA-Z0-9._\-]+@[a-zA-Z0-9._\-]+\.[a-zA-Z]{2,3}$/",
                $EMail))
        {
            return TRUE;
        }
        else
        {
            return FALSE;
        }
    }

    # get normalized version of e-mail address
    public static function NormalizeEMailAddress($EMailAddress)
    {
        return strtolower(trim($EMailAddress));
    }

    # get normalized version of user name
    public static function NormalizeUserName($UserName)
    {
        return trim($UserName);
    }

    # get normalized version of password
    public static function NormalizePassword($Password)
    {
        return trim($Password);
    }

    # generate random password
    public function GetRandomPassword($PasswordMinLength = 6, $PasswordMaxLength = 8)
    {
        # seed random number generator
        mt_srand((double)microtime() * 1000000);

        # generate password of requested length
        return sprintf("%06d", mt_rand(pow(10, ($PasswordMinLength - 1)),
                (pow(10, $PasswordMaxLength) - 1)));
    }

    # convenience function to supply parameters to Database->UpdateValue()
    public function UpdateValue($FieldName, $NewValue = DB_NOVALUE)
    {
        return $this->DB->UpdateValue("APUsers", $FieldName, $NewValue,
                "UserId = '".$this->UserId."'", $this->DBFields);
    }

    # methods for backward compatibility with earlier versions of User
    public function GivePriv($Privilege)
    {
        $this->GrantPriv($Privilege);
    }

    /**
    * Get a salt to use with crypt()
    */
    private static function GetSaltForCrypt()
    {
        # generate a password salt by grabbing CRYPT_SALT_LENGTH
        # random bytes, then base64 encoding while filtering out
        # non-alphanumeric characters to get a string all the hashes
        # accept as a salt
        $Salt = preg_replace("/[^A-Za-z0-9]/","",
                base64_encode(openssl_random_pseudo_bytes(
                    CRYPT_SALT_LENGTH) ));

        # select the best available hashing algorithm, provide a salt
        # in the correct format for that algorithm
        if (CRYPT_SHA512==1)
        {
            return '$6$'.substr($Salt, 0, 16);
        }
        elseif (CRYPT_SHA256==1)
        {
            return '$5$'.substr($Salt, 0, 16);
        }
        elseif (CRYPT_BLOWFISH==1)
        {
            return '$2y$'.substr($Salt, 0, 22);
        }
        elseif (CRYPT_MD5==1)
        {
            return '$1$'.substr($Salt, 0, 12);
        }
        elseif (CRYPT_EXT_DES==1)
        {
            return '_'.substr($Salt, 0, 8);
        }
        else
        {
            return substr($Salt, 0, 2);
        }
    }
}
