<?PHP
#
#   FILE:  sync_common.php
#
#   Part of the Collection Workflow Integration System (CWIS)
#   Copyright 2002-2013 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 mysql_*
    * from inside Drupal to perform queries.
    * @param String the 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)
            {
                while ($Row = $this->DBLink->FetchRow()){ $rc []= $Row; }
            }
        }
        else
        {
            $rs = mysql_query($String, $this->DBLink);

            if ($is_select && is_resource($rs) )
            {
                while ($Row = mysql_fetch_array($rs)){ $rc []= $Row; }
            }
        }
        return $rc;
    }

    /**
    * Fetch a list of users who have queued modifications to synchronize.
    * @param Comamnd 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 UserName the username to look for.
    * @param Command the 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 UserName the Drupal format username.
    * @return a CWIS-friendly mapping of UserName.
    */
    private function NormalizeUserName($UserName)
    {
        $rc = "";
        if (! preg_match("/^[a-zA-Z0-9]{2,24}$/", $UserName))
        {
            for ($i=0;
                 $i<strlen($UserName) && strlen($rc) <= 24;
                 $i++)
            {
                if (preg_match("/^[a-zA-Z0-9]$/", $UserName[$i]))
                {
                    $rc .= $UserName[$i];
                }
            }
        }
        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 LocalName the 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.
    */
    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 DBInfo NULL in CWIS, array containing "server", "user",
    * "pass", and "database" in Drupal.
    */
    function __construct($DBInfo=NULL)
    {
        $this->Status = DrupalSync_Helper::STATUS_OK;

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

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

    /**
    * In Drupal, close our connection to the DB when we're deleted.
    */
    function __destruct()
    {
        if ( $this->InCwis==FALSE &&
             $this->Status==DrupalSync_Helper::STATUS_OK)
        {
            mysql_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 Command "Create", "Delete", "Update", "Login", or "Logout"
    * @param UserName for the affected account.
    * @param Password in cleartext for login events, NULL otherwise.
    * @param Email for user updates, NULL otherwise.
    */
    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 CurrentUserName Currently logged in user or NULL.
    */
    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.
    */
    function UsersToCreate() { return $this->UsersTo("Create"); }

    /**
    * Fetch the queue of users to delete.
    */

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

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

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

    /**
    * Mark a user deletion as complete.
    * @param Name the username that was deleted.
    */
    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.
    * @paran Name the username that was logged in.
    */
    function UserLoggedIn($Name){
        setcookie( ($this->InCwis?"Cwis":"Drupal")."LoginToken","",
                   time()-3600);
        $this->CmdComplete($Name, "Login");
    }

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

    /**
    * Check if a user exists in CWIS.
    * @param UserName User to look for
    * @param Password in plain text
    * @return TRUE when the user exists with the supplied pass, FALSE otherwise.
    */
    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 Email the address to check
    * @return TRUE when there is a CWIS account, FALSE otherwise.
    */
    function EmailRegisteredInCwis($Email){
        $Row = $this->Query(
            "SELECT UserName, UserPassword, EMail FROM APUsers WHERE "
            ."EMail='".addslashes($Email)."'");

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