<?PHP
#
#   FILE:  sync_common.php
#
#   Part of the Collection Workflow Integration System (CWIS)
#   Copyright 2002-2016 Edward Almasy and Internet Scout Research Group
#   http://scout.wisc.edu/cwis/
#

/**
* Common functions used by both the DrupalSync CWIS plugin and the
* cwis_user Drupal plugin.
*/
final class DrupalSync_Helper
{
    private $DBLink;
    private $InCwis;
    private $ErrorStatus;

    const STATUS_OK = 0;
    const STATUS_NODB = 1;

    /**
    * DB abstraction which uses Axis--Database inside CWIS and calls to mysqli_*
    * from inside Drupal to perform queries.
    * @param string $String SQL to run
    * @return Array of results.
    */
    private function Query($String)
    {
        $rc = array();

        $is_select = preg_match("/^select/i", $String) === 1;
        if ($this->InCwis)
        {
            $this->DBLink->Query($String);
            if ($is_select)
            {
                $rc = $this->DBLink->FetchRows();
            }
        }
        else
        {
            $rs = mysqli_query($this->DBLink, $String);

            if ($is_select && $rs instanceof mysqli_result)
            {
                while ($Row = mysqli_fetch_array($rs))
                {
                    $rc[]= $Row;
                }
            }
        }
        return $rc;
    }

    /**
    * Fetch a list of users who have queued modifications to synchronize.
    * @param string $Command The type of modification to look for.
    * @return array giving queued commands.
    */
    private function UsersTo($Command)
    {
        $TableName =
            "DrupalSync_".($this->InCwis?"DtoC":"CtoD");
        return $this->Query("SELECT * FROM ".$TableName
                            ." WHERE Command='".$Command."'");
    }

    /**
    * Mark a queued modification as complete.
    * @param string $UserName User to look for.
    * @param string $Command A command to mark complete.
    */
    private function CmdComplete($UserName,$Command)
    {
        $TableName =
            "DrupalSync_".($this->InCwis?"DtoC":"CtoD");
        $this->Query("DELETE FROM ".$TableName." WHERE "
                     ."Command='".$Command."' "
                     ."AND UserName='".addslashes($UserName)."'");
    }

    /**
    * Convert a Drupal username into a format that CWIS will accept.
    * Drupal is somewhat liberal in what characters area allowed, and
    * not all legal Drupal names are accepted by CWIS.
    * @param string $UserName Drupal format username.
    * @return a CWIS-friendly mapping of UserName.
    */
    private function NormalizeUserName($UserName)
    {
        # if the given username contains illegal characters
        if (!preg_match("/^[a-zA-Z0-9]{2,24}$/", $UserName))
        {
            # filter out all the illegal chars
            $rc = preg_replace("/[^a-zA-Z0-9]/", "", $UserName);
            # and if the name is too long, truncate it
            if (strlen($rc) > 24)
            {
                $rc = substr($rc, 0, 24);
            }
        }
        else
        {
            $rc = $UserName;
        }
        return $rc;
    }

    /**
    * Look up the current user's username on the other site.  These
    * will be identical except when the Drupal username contains
    * characters not allowed by CWIS.
    * @param string $LocalName Local name (on this site) of the user.
    * @return the remote name (on the other site) of the user.
    */
    private function RemoteName($LocalName)
    {
        $rc = "";

        $Tgt = ($this->InCwis ? "Drupal" : "Cwis"  )."Name";
        $Key = ($this->InCwis ? "Cwis"   : "Drupal")."Name";

        $Rows = $this->Query("SELECT ".$Tgt." FROM DrupalSync_UserNameMap "
                         ."WHERE ".$Key."='".addslashes($LocalName)."'");
        $Row = array_pop($Rows);

        if (isset($Row[$Tgt]) )
        {
            $rc = $Row[$Tgt];
        }

        return $rc;
    }

    /**
    * Create a user name mapping from this site to the remote.  For
    * example, "rob bobson" on Drupal would be "robbobson" on CWIS.
    * Note, this also provides a list of which user accounts are being
    * synchronized.
    * @param string $DrupalName Username in drupal.
    * @param string $CwisName Username in cwis.
    */
    private function MapUserName($DrupalName, $CwisName)
    {
        $ExistingMaps = $this->Query(
            "SELECT * FROM DrupalSync_UserNameMap WHERE "
            ."DrupalName='".addslashes($DrupalName)."' OR "
            ."CwisName='".addslashes($CwisName)."'");

        if ( count($ExistingMaps) == 0 )
        {
            $this->Query("INSERT INTO DrupalSync_UserNameMap "
                         ."(DrupalName, CwisName) VALUES "
                         ."('".addslashes($DrupalName)."',"
                         ."'".addslashes($CwisName)."')");
        }
    }

    /**
    * Initialize a DrupalSync_Helper.
    * @param mixed $DBInfo Database info: NULL in CWIS, array
    * containing "server", "user", "pass", and "database" in Drupal.
    */
    public function __construct($DBInfo=NULL)
    {
        $this->Status = self::STATUS_OK;

        if ($DBInfo !== NULL)
        {
            $this->InCwis = FALSE;
            if (strlen($DBInfo["server"])>0)
            {
                $this->DBLink = mysqli_connect(
                    $DBInfo["server"],
                    $DBInfo["user"],
                    $DBInfo["pass"],
                    $DBInfo["database"]);
                if ($this->DBLink === FALSE)
                {
                    $this->Status = self::STATUS_NODB;
                }
            }
            else
            {
                $this->Status = self::STATUS_NODB;
            }
        }
        else
        {
            $this->InCwis = TRUE;
            $this->DBLink = new Database();
        }
    }

    /**
    * Check for errors in setting up the helper.
    */
    public function GetStatus()
    {
        return $this->Status;
    }

    /**
    * In Drupal, close our connection to the DB when we're deleted.
    */
    public function __destruct()
    {
        if ( $this->InCwis==FALSE &&
             $this->Status==self::STATUS_OK)
        {
            mysqli_close($this->DBLink);
        }
    }

    /**
    * Queue a pending update for the other site to synchronize.  For
    * login events, a browser cookie is set.  For all events, a
    * database entry is created.
    * @param string $Command One of "Create", "Delete", "Update", "Login", or "Logout"
    * @param string $UserName User to work on.
    * @param string $Password Plaintext password for login events, NULL otherwise.
    * @param string $Email New email for user updates, NULL otherwise.
    */
    public function SignalCommand($Command, $UserName, $Password=NULL, $Email=NULL)
    {
        $TableName =
            "DrupalSync_".($this->InCwis?"CtoD":"DtoC");

        if ($Command=="Create")
        {
            # Check to see if we've already made a mapping for this
            # user
            $RemName = $this->RemoteName($UserName);

            if (strlen($RemName)==0)
            {
                if ($this->InCwis)
                {
                    $DrupalName = $UserName;
                    $CwisName   = $UserName;
                }
                else
                {
                    $DrupalName = $UserName;
                    $CwisName   = $this->NormalizeUserName($UserName);
                }

                $this->MapUserName($DrupalName, $CwisName);
                $RemName = $this->RemoteName($UserName);

                $this->Query(
                    "INSERT INTO ".$TableName." "
                    ."(Command, UserName, Password, Email) VALUES "
                    ."('Create', "
                    ."'".addslashes($RemName)."', "
                    ."'".addslashes($Password)."', "
                    ."'".addslashes($Email)."')");
            }
        }
        elseif ($Command == "Delete")
        {
            $RemName = $this->RemoteName($UserName);
            if (strlen($RemName)>0 )
            {
                $this->Query(
                    "INSERT INTO ".$TableName." "
                    ."(Command, UserName) VALUES "
                    ."('Delete','".addslashes($RemName)."')");
            }
        }
        elseif ($Command == "Update")
        {
            $RemName = $this->RemoteName($UserName);
            if (strlen($RemName)>0)
            {
                $this->Query(
                    "DELETE FROM ".$TableName." WHERE "
                    ."Command='Update' AND "
                    ."UserName='".addslashes($RemName)."'");
                $this->Query(
                    "INSERT INTO ".$TableName." "
                    ."(Command, UserName, Password, Email) VALUES "
                    ."('Update', "
                    ."'".addslashes($RemName)."', "
                    ."'".addslashes($Password)."', "
                    ."'".addslashes($Email)."')");
            }
            else
            {
                $this->SignalCommand(
                    "Create", $UserName, $Password, $Email);
            }
        }
        elseif ($Command == "Login" || $Command == "Logout" )
        {

            if ($Command=="Login")
            {
                $this->SignalCommand(
                    "Create", $UserName, $Password, $Email);
            }

            $RemName = $this->RemoteName($UserName);

            if ( strlen($RemName)>0 )
            {
                $this->Query(
                    "DELETE FROM ".$TableName." WHERE "
                    ."UserName='".addslashes($RemName)."' "
                    ."AND ( Command='Logout' OR Command='Login' )");

                $CookieName =
                    ($this->InCwis?"Drupal":"Cwis")."LoginToken";

                mt_srand((double)microtime() * 1000000);
                $Token = mt_rand(0, 2147483647);

                $CookieData = array(
                    "Version" => 1,
                    "UserName" => $RemName,
                    "Token" => $Token
                    );

                $EncData = urlencode(
                    base64_encode(gzcompress(serialize($CookieData))));
                setcookie($CookieName, $EncData, time()+3600*24, "/");

                $this->Query(
                    "INSERT INTO ".$TableName." "
                    ."(Command, UserName, Password, Token) VALUES "
                    ."('".$Command."',"
                    ."'".addslashes($RemName)."',"
                    ."'".addslashes($Password)."',"
                    .$Token.")");
            }
        }
    }

    /**
    * Determine if the other site has queued a login or a logout.
    * @param string $CurrentUserName Currently logged in user or NULL.
    */
    public function CheckForCommand($CurrentUserName)
    {
        $rc = array("Command" => NULL, "User"=>NULL);

        $CookieName = ($this->InCwis ?"Cwis":"Drupal")."LoginToken";
        $TableName =
            "DrupalSync_".($this->InCwis?"DtoC":"CtoD");

        # Look for a login cookie
        if (isset($_COOKIE[$CookieName]))
        {
            # Extract the cookie data
            $CookieData = unserialize(
                gzuncompress(base64_decode(urldecode($_COOKIE[$CookieName]))));

            # Versioned cookies to allow adding features later
            if ($CookieData["Version"] == 1)
            {
                $UserName = $CookieData["UserName"];
                $Token    = $CookieData["Token"];
                $QueryString =
                        "SELECT * FROM ".$TableName." WHERE "
                        ."UserName='".addslashes($UserName)."' AND "
                        ."Token=".$Token;

                $Data = $this->Query($QueryString);
                $Data = array_pop($Data);

                if ( $Data !== NULL ) {
                    # Process the requested command:
                    if ($Data["Command"] == "Login" &&
                        $CurrentUserName === NULL )
                    {
                        $rc = array("Command"  => "Login",
                                    "UserName" => $UserName,
                                    "Password" => $Data["Password"]);
                    }
                    elseif ($Data["Command"] == "Logout" &&
                            $CurrentUserName == $UserName )
                    {
                        $rc = array("Command"  => "Logout",
                                    "UserName" => $UserName);
                    }
                } # if Data was in DB
            } # if Cookie was version 1
        } # if Cookie was set

        return $rc;
    }

    /**
    * Fetch the queue of users to create.
    */
    public function UsersToCreate()
    {
        return $this->UsersTo("Create");
    }

    /**
    * Fetch the queue of users to delete.
    */
    public function UsersToDelete()
    {
        return $this->UsersTo("Delete");
    }

    /**
    * Fetch the queue of users to update.
    */
    public function UsersToUpdate()
    {
        return $this->UsersTo("Update");
    }

    /**
    * Mark a user creation as complete.
    * @param string $Name User that was created.
    */
    public function UserCreated($Name)
    {
        $this->CmdComplete($Name, "Create");
    }

    /**
    * Mark a user update as complete.
    * @param string $Name User that was updated.
    */
    public function UserUpdated($Name)
    {
        $this->CmdComplete($Name, "Update");
    }

    /**
    * Mark a user deletion as complete.
    * @param string $Name User that was deleted.
    */
    public function UserDeleted($Name)
    {
        $this->CmdComplete($Name, "Delete");
        $this->Query("DELETE FROM DrupalSync_UserNameMap WHERE "
                     .($this->InCwis?"Cwis":"Drupal")
                     ."Name='".addslashes($Name)."'");
    }

    /**
    * Mark a user login request as complete.
    * @param string $Name User that was logged in.
    */
    public function UserLoggedIn($Name)
    {
        setcookie(
            ($this->InCwis?"Cwis":"Drupal")."LoginToken", "",
            time()-3600);
        $this->CmdComplete($Name, "Login");
    }

    /**
    * Mark a user logout request as complete.
    * @param string $Name User that was logged out.
    */
    public function UserLoggedOut($Name)
    {
        setcookie(
            ($this->InCwis?"Cwis":"Drupal")."LoginToken", "",
            time()-3600);
        $this->CmdComplete($Name, "Logout");
    }

    /**
    * Check if a user exists in CWIS.
    * @param string $UserName User to look for
    * @param string $Password Plaintext password
    * @return TRUE when the user exists with the supplied pass, FALSE otherwise.
    */
    public function CwisUserExists($UserName, $Password)
    {
        $Row = $this->Query(
            "SELECT UserName, UserPassword, EMail FROM APUsers WHERE "
            ."UserName='"
            .addslashes($this->NormalizeUserName($UserName))."'");

        if (count($Row))
        {
            $Row = array_pop($Row);
            if (crypt($Password, $Row["UserPassword"]) == $Row["UserPassword"])
            {
                return $Row["EMail"];
            }
        }
        return FALSE;
    }

    /**
    * Check if an email address has a corresponding CWIS accont.
    * @param string $Email Email address to check
    * @return TRUE when there is a CWIS account, FALSE otherwise.
    */
    public function EmailRegisteredInCwis($Email)
    {
        $Row = $this->Query(
            "SELECT UserName, UserPassword, EMail FROM APUsers WHERE "
            ."EMail='".addslashes($Email)."'");

        return (count($Row) > 0);
    }
}