<?PHP

#
#   Axis--UserFactory.php
#   An Meta-Object for Handling User Information
#
#   Copyright 2003-2012 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 (ealmasy@axisdata.com)
#
#   Part of the AxisPHP library v1.2.4
#   For more information see http://www.axisdata.com/AxisPHP/
#

class UserFactory
{

    # ---- PUBLIC INTERFACE --------------------------------------------------

    /**
    * Object constructor.
    */
    public function __construct()
    {
        # create database connection
        $this->DB = new Database();
    }

    /**
    * Create new user.  The second password and e-mail address parameters are
    * intended for second copies of each entered by the user.
    * @param string $UserName Login name for new user.
    * @param string $Password Password for new user.
    * @param string $PasswordAgain Second copy of password entered by user.
    * @param string $EMail E-mail address for new user.
    * @param string $EMailAgain Second copy of e-mail address entered by user.
    * @param bool $IgnoreErrorCodes Array containing any error codes that should
    *       be ignored.  (OPTIONAL)
    * @return User object or array of error codes.
    */
    public function CreateNewUser(
            $UserName, $Password, $PasswordAgain, $EMail, $EMailAgain,
            $IgnoreErrorCodes = NULL)
    {
        # check incoming values
        $ErrorCodes = $this->TestNewUserValues(
            $UserName, $Password, $PasswordAgain, $EMail, $EMailAgain);

        # discard any errors we are supposed to ignore
        if ($IgnoreErrorCodes)
        {
            $ErrorCodes = array_diff($ErrorCodes, $IgnoreErrorCodes);
        }

        # if error found in incoming values return error codes to caller
        if (count($ErrorCodes)) {  return $ErrorCodes;  }

        # add user to database
        $UserName = User::NormalizeUserName($UserName);
        $this->DB->Query("INSERT INTO APUsers"
                ." (UserName, CreationDate)"
                ." VALUES ('".addslashes($UserName)."', NOW())");

        # create new user object
        $UserId = $this->DB->LastInsertId();
        $User = new User($this->DB, (int)$UserId);

        # if new user object creation failed return error code to caller
        if ($User->Status() != U_OKAY) {  return array($User->Status());  }

        # set password and e-mail address
        $User->SetPassword($Password);
        $User->Set("EMail", $EMail);

        # return new user object to caller
        return $User;
    }

    /**
    * Test new user values (usually used before creating new user).
    * @param string $UserName User name entered.
    * @param string $Password Password entered.
    * @param string $PasswordAgain Second copy of password entered.
    * @param string $EMail Email entered.
    * @param string $EMailAgain Second copy of email entered.
    * @return array Codes (U_* constants) for errors found (if any).
    */
    public function TestNewUserValues(
            $UserName, $Password, $PasswordAgain, $EMail, $EMailAgain)
    {
        $ErrorCodes = array();
        if (strlen(User::NormalizeUserName($UserName)) == 0)
            {  $ErrorCodes[] = U_EMPTYUSERNAME;  }
        elseif (!User::IsValidUserName($UserName))
            {  $ErrorCodes[] = U_ILLEGALUSERNAME;  }
        elseif ($this->UserNameExists($UserName))
            {  $ErrorCodes[] = U_DUPLICATEUSERNAME;  }

        if ($this->EMailAddressIsInUse($EMail))
            {  $ErrorCodes[] = U_DUPLICATEEMAIL;  }

        $FoundOtherPasswordError = FALSE;
        if (strlen(User::NormalizePassword($Password)) == 0)
        {
            $ErrorCodes[] = U_EMPTYPASSWORD;
            $FoundOtherPasswordError = TRUE;
        }
        elseif (!User::IsValidPassword($Password))
        {
            $ErrorCodes[] = U_ILLEGALPASSWORD;
            $FoundOtherPasswordError = TRUE;
        }

        if (strlen(User::NormalizePassword($PasswordAgain)) == 0)
        {
            $ErrorCodes[] = U_EMPTYPASSWORDAGAIN;
            $FoundOtherPasswordError = TRUE;
        }
        elseif (!User::IsValidPassword($PasswordAgain))
        {
            $ErrorCodes[] = U_ILLEGALPASSWORDAGAIN;
            $FoundOtherPasswordError = TRUE;
        }

        if ($FoundOtherPasswordError == FALSE)
        {
            if (User::NormalizePassword($Password)
                    != User::NormalizePassword($PasswordAgain))
            {
                $ErrorCodes[] = U_PASSWORDSDONTMATCH;
            }
        }

        $FoundOtherEMailError = FALSE;
        if (strlen(User::NormalizeEMailAddress($EMail)) == 0)
        {
            $ErrorCodes[] = U_EMPTYEMAIL;
            $FoundOtherEMailError = TRUE;
        }
        elseif (!User::IsValidLookingEMailAddress($EMail))
        {
            $ErrorCodes[] = U_ILLEGALEMAIL;
            $FoundOtherEMailError = TRUE;
        }

        if (strlen(User::NormalizeEMailAddress($EMailAgain)) == 0)
        {
            $ErrorCodes[] = U_EMPTYEMAILAGAIN;
            $FoundOtherEMailError = TRUE;
        }
        elseif (!User::IsValidLookingEMailAddress($EMailAgain))
        {
            $ErrorCodes[] = U_ILLEGALEMAILAGAIN;
            $FoundOtherEMailError = TRUE;
        }

        if ($FoundOtherEMailError == FALSE)
        {
            if (User::NormalizeEMailAddress($EMail)
                    != User::NormalizeEMailAddress($EMailAgain))
            {
                $ErrorCodes[] = U_EMAILSDONTMATCH;
            }
        }

        return $ErrorCodes;
    }

    /**
     * Return number of users in the system.
     * @param string $Condition SQL condition (without "WHERE") to limit
     *      user count.  (OPTIONAL)
     * @return int Count of users.
     */
    public function GetUserCount($Condition = NULL)
    {
        return $this->DB->Query("SELECT COUNT(*) AS UserCount FROM APUsers"
                .($Condition ? " WHERE ".$Condition : ""), "UserCount");
    }

    /**
    * Get total number of user that matched last GetMatchingUsers() call.
    * @return int Number of users.
    * @see UserFactory::GetMatchingUser()
    */
    public function GetMatchingUserCount()
    {
        return $this->MatchingUserCount;
    }

    /**
     * Get users who are currently logged in (i.e. recently active and not logged out).
     * @param int $InactivityTimeout Number of minutes after which an inactive user
     *       is considered to be no longer logged in.  (OPTIONAL, defaults to 60)
     * @return Array of User objects.
     */
    public function GetLoggedInUsers($InactivityTimeout = 60)
    {
        # query IDs of logged-in users from database
        $LoggedInCutoffTime = date("Y-m-d H:i:s",
                time() - ($InactivityTimeout * 60));
        $this->DB->Query("SELECT UserId FROM APUsers"
                ." WHERE LastActiveDate > '".$LoggedInCutoffTime."'"
                ." AND LoggedIn != '0'");
        $UserIds = $this->DB->FetchColumn("UserId");

        # load array of logged in users
        $ReturnValue = array();
        foreach ($UserIds as $Id)
        {
            $ReturnValue[$Id] = new User(intval($Id));
        }

        # return array of user data to caller
        return $ReturnValue;
    }

    /**
    * Get users recently logged in.
    * @param string $Since Used to define "recently".  (OPTIONAL, defaults
    *       to 24 hours)
    * @param int $Limit Maximum number of users to return.
    * @return array Array of User objects, with IDs for the index.
    */
    public function GetRecentlyLoggedInUsers($Since = NULL, $Limit = 10)
    {
        # get users recently logged in during the last 24 hours if no date given
        if ($Since === NULL)
        {
            $Date = date("Y-m-d H:i:s", time() - (24 * 60 * 60));
        }

        else
        {
            $Date = date("Y-m-d H:i:s", strtotime($Since));
        }

        # query for the users who were logged in since the given date
        $this->DB->Query("SELECT UserId FROM APUsers"
                ." WHERE LastActiveDate > '".$Date."'"
                        ." AND LoggedIn != '1'"
                ." ORDER BY LastActiveDate DESC"
                ." LIMIT ".intval($Limit));
        $UserIds = $this->DB->FetchColumn("UserId");

        $ReturnValue = array();
        foreach ($UserIds as $Id)
        {
            $ReturnValue[$Id] = new User(intval($Id));
        }

        # return array of user data to caller
        return $ReturnValue;
    }

    /**
    * Return array of user names who have the specified privileges.  Multiple
    * privileges can be passed in as parameters (rather than in an array), if
    * desired.
    * @return array User names with user IDs for the array index.
    */
    public function GetUsersWithPrivileges()
    {
        # retrieve privileges
        $Args = func_get_args();
        if (is_array(reset($Args))) {  $Args = reset($Args);  }
        $Privs = array();
        foreach ($Args as $Arg)
        {
            if (is_array($Args))
            {
                $Privs = array_merge($Privs, $Args);
            }
            else
            {
                $Privs[] = $Arg;
            }
        }

        # start with query string that will return all users
        $QueryString = "SELECT DISTINCT APUsers.UserId, UserName FROM APUsers"
                .(count($Privs) ? ", APUserPrivileges" : "");

        # for each specified privilege
        foreach ($Privs as $Index => $Priv)
        {
            # add condition to query string
            $QueryString .= ($Index == 0) ? " WHERE (" : " OR";
            $QueryString .= " APUserPrivileges.Privilege = ".$Priv;
        }

        # close privilege condition in query string and add user ID condition
        $QueryString.= count($Privs)
                ? ") AND APUsers.UserId = APUserPrivileges.UserId" : "";

        # add sort by user name to query string
        $QueryString .= " ORDER BY UserName ASC";

        # perform query
        $this->DB->Query($QueryString);

        # copy query result into user info array
        $Users = $this->DB->FetchColumn("UserName", "UserId");

        # return array of users to caller
        return $Users;
    }

    /**
    * Get users who have values matching specified string in specified field.
    * @param string $SearchString String to match.
    * @param string $FieldName Database column name to search.  (OPTIONAL,
    *       defaults to "UserName")
    * @param string $SortFieldName Database column name to sort results by.
    *       (OPTIONAL, defaults to "UserName")
    * @param int $Offset Starting index for results.  (OPTIONAL)
    * @param int $Count Maximum number of results to return.  (OPTIONAL)
    * @return array Array with user IDs for the index and User objects for
    *       the values.
    */
    public function FindUsers($SearchString, $FieldName = "UserName",
            $SortFieldName = "UserName", $Offset = 0, $Count = 9999999)
    {
        # retrieve matching user IDs
        $UserNames = $this->FindUserNames(
                $SearchString, $FieldName, $SortFieldName, $Offset, $Count);

        # create user objects
        $Users = array();
        foreach ($UserNames as $UserId => $UserName)
        {
            $Users[$UserId] = new User($this->DB, intval($UserId));
        }

        # return array of user objects to caller
        return $Users;
    }

    /**
    * Get users who have values matching specified string in specified field.
    * @param string $SearchString String to match.
    * @param string $FieldName Database column name to search.  (OPTIONAL,
    *       defaults to "UserName")
    * @param string $SortFieldName Database column name to sort results by.
    *       (OPTIONAL, defaults to "UserName")
    * @param int $Offset Starting index for results.  (OPTIONAL)
    * @param int $Count Maximum number of results to return.  (OPTIONAL)
    * @param array $IdExclusions User IDs to exclude.  (OPTIONAL)
    * @param array $ValueExclusions User names to exclude.  (OPTIONAL)
    * @return array Array with user IDs for the index and user names for
    *       the values.
    */
    public function FindUserNames($SearchString, $FieldName = "UserName",
            $SortFieldName = "UserName", $Offset = 0, $Count = 9999999,
            $IdExclusions = array(), $ValueExclusions = array())
    {
        # Construct a database query:
        $QueryString = "SELECT UserId, UserName FROM APUsers WHERE";

        # If the search string is a valid username which is shorter than the
        # minimum word length indexed by the FTS, just do a normal
        # equality test instead of using the index.
        # Otherwise, FTS away.
        $MinWordLen = $this->DB->Query(
            "SHOW VARIABLES WHERE variable_name='ft_min_word_len'", "Value");
        if (User::IsValidUserName($SearchString) &&
            strlen($SearchString) < $MinWordLen )
        {
            $QueryString .= " UserName='".addslashes($SearchString)."'";
        }
        else
        {
            # massage search string to use AND logic
            $Words = preg_split("/[\s]+/", trim($SearchString));
            $NewSearchString = "";
            $InQuotedString = FALSE;
            foreach ($Words as $Word)
            {
                if ($InQuotedString == FALSE) {  $NewSearchString .= "+";  }
                if (preg_match("/^\"/", $Word)) {  $InQuotedString = TRUE;  }
                if (preg_match("/\"$/", $Word)) {  $InQuotedString = FALSE;  }
                $NewSearchString .= $Word." ";
            }
            $QueryString .= " MATCH (".$FieldName.")"
                ." AGAINST ('".addslashes(trim($NewSearchString))."'"
                ." IN BOOLEAN MODE)";
        }

        # add each ID exclusion
        foreach ($IdExclusions as $IdExclusion)
        {
            $QueryString .= " AND ".$this->ItemIdFieldName." != '"
                .addslashes($IdExclusion)."' ";
        }

        # add each value exclusion
        foreach ($ValueExclusions as $ValueExclusion)
        {
            $QueryString .= " AND ".$this->ItemNameFieldName." != '"
                .addslashes($ValueExclusion)."' ";
        }

        $QueryString .= " ORDER BY ".$SortFieldName
            ." LIMIT ".$Offset.", ".$Count;

        # retrieve matching user IDs
        $this->DB->Query($QueryString);
        $UserNames = $this->DB->FetchColumn("UserName", "UserId");

        # return names/IDs to caller
        return $UserNames;
    }

    /**
    * Return array of users who have values matching search string (in
    * specific field if requested).  (Search string respects POSIX-compatible
    * regular expressions.)  Optimization: $SearchString = ".*." and
    * $FieldName = NULL will return all users ordered by $SortFieldName.
    * @param string $SearchString Search pattern.
    * @param string $FieldName Database column name to search.  (OPTIONAL)
    * @param string $SortFieldName Database column name to sort results by.
    *       (OPTIONAL, defaults to "UserName")
    * @param int $ResultsStartAt Starting index for results.  (OPTIONAL)
    * @param int $ReturnNumber Maximum number of results to return.  (OPTIONAL)
    * @return array Array with user IDs for the index and associative
    *       arrays of database columns for the values.
    * @see UserFactory::GetMatchingUserCount()
    */
    public function GetMatchingUsers($SearchString, $FieldName = NULL,
                              $SortFieldName = "UserName",
                              $ResultsStartAt = 0, $ReturnNumber = NULL)
    {
        # start with empty array (to prevent array errors)
        $ReturnValue = array();

        # if empty search string supplied, return nothing
        $TrimmedSearchString = trim($SearchString);
        if (empty($TrimmedSearchString))
        {
            return $ReturnValue;
        }

        # make sure ordering is done by user name if not specified
        $SortFieldName = empty($SortFieldName) ? "UserName" : $SortFieldName;

        # begin constructing the query
        $Query = "SELECT * FROM APUsers";
        $QueryOrderBy = " ORDER BY $SortFieldName";
        $QueryLimit = empty($ReturnNumber) ? "" : " LIMIT $ResultsStartAt, $ReturnNumber";

        # the Criteria Query will be used to get the total number of results without the
        # limit clause
        $CriteriaQuery = $Query;

        # if specific field comparison requested
        if (!empty($FieldName))
        {
            # append queries with criteria
            $Query .= " WHERE ".$FieldName." REGEXP '".addslashes($SearchString)."'";
            $CriteriaQuery = $Query;
        }

        # optimize for returning all users
        else if ($SearchString == ".*.")
        {
            # set field name to username - this would be the first field
            # returned by a field to field search using the above RegExp
            $FieldName = "UserName";
        }

        # add order by and limit to query for optimizing
        $Query .= $QueryOrderBy.$QueryLimit;

        # execute query...
        $this->DB->Query($Query);

        # ...and process query return
        while ($Record = $this->DB->FetchRow())
        {
            # if specific field or all users requested
            if (!empty($FieldName))
            {
                # add user to return array
                $ReturnValue[$Record["UserId"]] = $Record;

                # add matching search field to return array
                $ReturnValue[$Record["UserId"]]["APMatchingField"] = $FieldName;
            }

            else
            {
                # for each user data field
                foreach ($Record as $FName => $FValue)
                {
                    # if search string appears in data field
                    if (strpos($Record[$FName], $SearchString) !== FALSE)
                    {
                        # add user to return array
                        $ReturnValue[$Record["UserId"]] = $Record;

                        # add matching search field to return array
                        $ReturnValue[$Record["UserId"]]["APMatchingField"] = $FName;
                    }
                }
            }
        }

        # add matching user count
        $this->DB->Query($CriteriaQuery);
        $this->MatchingUserCount = $this->DB->NumRowsSelected();

        # return array of matching users to caller
        return $ReturnValue;
    }

    /**
    * Check whether user name currently exists.
    * @param string $UserName Name to check.
    * @return bool TRUE if name is already in use, otherwise FALSE.
    */
    public function UserNameExists($UserName)
    {
        # normalize user name
        $UserName = User::NormalizeUserName($UserName);

        # check whether user name is already in use
        $NameCount = $this->DB->Query(
                "SELECT COUNT(*) AS NameCount FROM APUsers"
                    ." WHERE UserName = '".addslashes($UserName)."'",
                "NameCount");

        # report to caller whether name exists
        return ($NameCount > 0) ? TRUE : FALSE;
    }

    /**
    * Check whether e-mail address currently has account associated with it.
    * @param string $Address Address to check.
    * @return bool TRUE if address is in use, otherwise FALSE.
    */
    public function EMailAddressIsInUse($Address)
    {
        # normalize address
        $UserName = User::NormalizeEMailAddress($Address);

        # check whether address is already in use
        $AddressCount = $this->DB->Query(
                "SELECT COUNT(*) AS AddressCount FROM APUsers"
                    ." WHERE EMail = '".addslashes($Address)."'",
                "AddressCount");

        # report to caller whether address is in use
        return ($AddressCount > 0) ? TRUE : FALSE;
    }

    /**
    * Get the users sorted by when they signed up, starting with those who
    * signed up most recently. By default, the number of users returned is five.
    * @param int $Limit The maximum number of users to return.
    */
    public function GetNewestUsers($Limit = 5)
    {
        # assume no users will be found
        $Users = array();

        # fetch the newest users
        $this->DB->Query("SELECT *"
                ." FROM APUsers"
                ." ORDER BY CreationDate DESC"
                ." LIMIT ".intval($Limit));
        $UserIds = $this->DB->FetchColumn("UserId");

        # for each user id found
        foreach ($UserIds as $UserId)
        {
            $Users[$UserId] = new SPTUser($UserId);
        }

        # return the newest users
        return $Users;
    }

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

    protected $DB;
    protected $SortFieldName;
    protected $MatchingUserCount;
}
