<?PHP

#
#   Axis--UserFactory.php
#   An Meta-Object for Handling User Information
#
#   Copyright 2003 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/
#

class UserFactory {

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

    # object constructor
    function UserFactory($SessionOrDb)
    {
        # if a session was passed in
        if (is_object($SessionOrDb) && method_exists($SessionOrDb, "Session"))
        {
            # swipe database handle from session
            $this->DB = $SessionOrDb->DB;

            # save session
            $this->Session = $SessionOrDb;
        }
        # else if database handle was passed in
        elseif (is_object($SessionOrDb) && method_exists($SessionOrDb, "Database"))
        {
            # save database handle
            $this->DB = $SessionOrDb;

            # create session
            $this->Session = new Session($this->DB);
        }
        else
        {
            # error out
            $this->Result = U_ERROR;
            exit(1);
        }
    }

    /**
    * Create new user.  The second password and e-mail address parameters are
    * intended for second copies of each entered by the user.
    * @param UserName Login name for new user.
    * @param Password Password for new user.
    * @param PasswordAgain Second copy of password entered by user.
    * @param EMail E-mail address for new user.
    * @param EMailAgain Second copy of e-mail address entered by user.
    * @param IgnoreErrorCodes Array containing any error codes that should
    *       be ignored.  (OPTIONAL)
    * @return User object or array of error codes.
    */
    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("APUsers");
        $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 creation values (returns array of error codes)
    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 Condition SQL condition (without "WHERE") to limit user count.  (OPTIONAL)
    * @return Count of users.
    */
    function GetUserCount($Condition = NULL)
    {
        return $this->DB->Query("SELECT COUNT(*) AS UserCount FROM APUsers"
                .($Condition ? " WHERE ".$Condition : ""), "UserCount");
    }

    # return total number of user that matched last GetMatchingUsers call
    # before the return size was limited
    function GetMatchingUserCount()
    {
        return $this->MatchingUserCount;
    }

    # return array of users currently logged in
    function GetLoggedInUsers()
    {
        # start with empty array (to prevent array errors)
        $ReturnValue = array();

        # load array of logged in user
        $UserIds = $this->Session->GetFromAllSessions("APUserId");

        # for each logged in user
        foreach ($UserIds as $UserId)
        {
            # load all data values for user
            $this->DB->Query("SELECT * FROM APUsers WHERE UserId = '".$UserId."'");
            $ReturnValue[$UserId] = $this->DB->FetchRow();
        }

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

    # return array of users recently logged in. returns 10 users by default
    function GetRecentlyLoggedInUsers($Since = NULL, $Limit = 10)
    {
        # start with empty array (to prevent array errors)
        $ReturnValue = array();

        # get users recently logged in during the last 24 hours if no date given
        if (is_null($Since))
        {
            $Date = date("Y-m-d H:i:s", time()-86400);
        }

        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 U.*
            FROM APUsers U
            LEFT JOIN
            -- the dummy table is an optimization. see the comment by Vimal
            -- Gupta in the MySQL docs:
            -- http://dev.mysql.com/doc/refman/5.0/en/in-subquery-optimization.html
            (SELECT DataValue
             FROM APSessionData
             WHERE DataName = 'APUserId'
             -- allows fetching distinct DataValue values but with GROUP BY
             -- optimizations
             GROUP BY DataValue) as Dummy
            -- using this convoluted method because DataValue is an integer
            -- (UserId) serialized by PHP to a string
            ON CONCAT('s:', CHAR_LENGTH(CAST(U.UserId AS CHAR)),
            ':\"', U.UserId,'\";') = Dummy.DataValue
            WHERE U.LastActiveDate >= '".$Date."'
            AND Dummy.DataValue IS NULL
            ORDER BY U.LastActiveDate DESC
            LIMIT ".intval($Limit));

        while (FALSE !== ($Row = $this->DB->FetchRow()))
        {
            $ReturnValue[$Row["UserId"]] = $Row;
        }

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

    # return array of user names who have the specified privileges
    # (array index is user IDs)
    function GetUsersWithPrivileges()
    {
        # start with query string that will return all users
        $Args = func_get_args();
        $QueryString = "SELECT DISTINCT APUsers.UserId, UserName FROM APUsers"
                .(count($Args) ? ", APUserPrivileges" : "");

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

        # close privilege condition in query string and add user ID condition
        $QueryString .= count($Args) 
                ? ") 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;
    }

    # return array of user objects who have values matching search string
    # (array indexes are user IDs)
    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;
    }

    # return array of user names/IDs who have values matching search string
    # (array indexes are user IDs, array values are user names)
    function FindUserNames($SearchString, $FieldName = "UserName",
            $SortFieldName = "UserName", $Offset = 0, $Count = 9999999)
    {
        # 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)";
        }
        $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
    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
    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);
    }

    # check whether e-mail address currently has account associated with it
    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);
    }

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

    var $DB;
    var $Session;
    var $SortFieldName;
    var $MatchingUserCount;

    # callback function for sorting users
    function CompareUsersForSort($UserA, $UserB)
    {
        return strcasecmp($UserA[$this->SortFieldName], $UserB[$this->SortFieldName]);
    }

}
