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

/**
* Set of privileges used to access resource information or other parts of
* the system.  A privilege set is a combination of privileges (integers),
* MetadataFields (to check against a specified value), and
* privilege/MetadataField combinations.
*/
class PrivilegeSet {

    /**
    * Class constructor, used to create a new set or reload an existing
    * set from previously-constructed data.
    * @param string $Data Existing privilege set data, previously
    *       retrieved with PrivilegeSet::Data().  (OPTIONAL)
    * @see PrivilegeSet::Data()
    */
    function __construct($Data = NULL)
    {
        # if privilege data supplied
        if ($Data !== NULL)
        {
            # if data is in legacy form (an array of privileges)
            if (is_array($Data))
            {
                # set internal privilege set from array
                $this->Privileges = $Data;
            }
            else
            {
                # set internal values from data
                $this->Data($Data);
            }
        }
    }

    /**
    * Get/set privilege set data, in the form of an opaque string.  This
    * method can be used to retrieve an opaque string containing privilege
    * set data, which can then be saved (e.g. to a database) and later used
    * to reload a privilege set.  (Use instead of serialize() to avoid
    * future issues with internal class changes.)
    * @param string $NewValue New privilege set data.  (OPTIONAL)
    * @return string Current privilege set data (opaque value).
    */
    function Data($NewValue = NULL)
    {
        # if new data supplied
        if ($NewValue !== NULL)
        {
            # unpack new data
            $Data = unserialize($NewValue);

            # unpack privilege data (if available) and load
            if (array_key_exists("Privileges", $Data))
            {
                $this->Privileges = array();
                foreach ($Data["Privileges"] as $Priv)
                {
                    if (is_array($Priv) && array_key_exists("SUBSET", $Priv))
                    {
                        $Subset = new PrivilegeSet();
                        $Subset->Data($Priv["SUBSET"]);
                        $this->Privileges[] = $Subset;
                    }
                    else
                    {
                        $this->Privileges[] = $Priv;
                    }
                }
            }

            # load associated user ID if available
            if (array_key_exists("UserId", $Data))
            {
                $this->UserId = $Data["UserId"];
            }

            # load logic if available
            if (array_key_exists("Logic", $Data))
            {
                $this->Logic = $Data["Logic"];
            }
        }

        # serialize current data and return to caller
        $Data = array();
        if (count($this->Privileges))
        {
            foreach ($this->Privileges as $Priv)
            {
                $Data["Privileges"][] = is_object($Priv)
                        ? array("SUBSET" => $Priv->Data())
                        : $Priv;
            }
        }
        if ($this->UserId !== NULL) {  $Data["UserId"] = $this->UserId;  }
        $Data["Logic"] = $this->Logic;
        return serialize($Data);
    }

    /**
    * Check whether a privilege set is greater than or equal to another
    * privilege set.  Usually used to test whether a privilege set associated
    * with a user is sufficient to meet a privilege set required for an
    * operation or to access some particular piece of data.
    * @param object $Set Privilege set to compare against.
    * @param object $Resource Resource object to used for comparison, for
    *       sets that include user conditions.  (OPTIONAL)
    * @return bool TRUE if privileges in set are greater than or equal to
    *       privileges in specified set, otherwise FALSE.
    */
    function IsGreaterThan(PrivilegeSet $Set, Resource $Resource = NULL)
    {
        # if target set has no requirements then we must be greater
        if (!count($Set->Privileges)) {  return TRUE;  }

        # for each privilege in target set
        foreach ($Set->Privileges as $Priv)
        {
            # if privilege is actually a privilege subgroup
            if (is_object($Priv))
            {
                # check if our privileges are greater than subgroup
                $OursGreater = $this->IsGreaterThan($Priv, $Resource);
            }
            # else if privilege is actually a condition
            elseif (is_array($Priv))
            {
                # check if privilege set meets that condition
                $OursGreater = $this->MeetsCondition($Priv, $Resource);
            }
            # else privilege is actually a privilege
            else
            {
                # check we have specified privilege
                $OursGreater = $this->IncludesPrivilege($Priv);
            }

            # if only one privilege must be greater
            if ($this->Logic == "OR")
            {
                # if our privileges were greater
                if ($OursGreater)
                {
                    # bail out and report to caller that our privileges are greater
                    break;
                }
            }
            # else if all privileges must be greater
            else
            {
                # if our privileges were not greater
                if (!$OursGreater)
                {
                    # bail out and report to caller that our privileges are not greater
                    break;
                }
            }
        }

        # all privileges must have been greater (if all required) or none of
        #       the privileges were greater (if only one required)
        #       so report accordingly to caller
        return $OursGreater;
    }

    /**
    * Check whether a privilege set is less than another privilege set.
    * @param object $Set Privilege set to compare against.
    * @param object $Resource Resource object to used for comparison, for
    *       sets that include user conditions.  (OPTIONAL)
    * @return bool TRUE if privileges in set are less than privileges in
    *       specified set, otherwise FALSE.
    */
    function IsLessThan(PrivilegeSet $Set, Resource $Resource = NULL)
    {
        # just return inverse of IsGreaterThan()
        return $this->IsGreaterThan($Set, $Resource) ? FALSE : TRUE;
    }

    /**
    * Add specified privilege to set.  If specified privilege is already
    * part of the set, no action is taken.
    * @param mixed $Privilege Privilege ID or object to add.
    * @see PrivilegeSet::RemovePrivilege()
    */
    function AddPrivilege($Privilege)
    {
        # add privilege if not currently in set
        if (!$this->IncludesPrivilege($Privilege))
        {
            if (is_object($Privilege)) {  $Privilege = $Privilege->Id();  }
            $this->Privileges[] = $Privilege;
        }
    }

    /**
    * Remove specified privilege from set.  If specified privilege is not
    * currently in the set, no action is taken.
    * @param mixed $Privilege Privilege ID or object to remove from set.
    * @see PrivilegeSet::AddPrivilege()
    */
    function RemovePrivilege($Privilege)
    {
        # remove privilege if currently in set
        if ($this->IncludesPrivilege($Privilege))
        {
            if (is_object($Privilege)) {  $Privilege = $Privilege->Id();  }
            $Index = array_search($Privilege, $this->Privileges);
            unset($this->Privileges[$Index]);
        }
    }

    /**
    * Check whether this privilege set includes the specified privilege.
    * @param mixed $Privilege Privilege ID or object to check.
    * @return bool TRUE if privilege is included, otherwise FALSE.
    */
    function IncludesPrivilege($Privilege)
    {
        # check whether privilege is in our list and report to caller
        if (is_object($Privilege)) {  $Privilege = $Privilege->Id();  }
        return $this->IsInPrivilegeData($Privilege) ? TRUE : FALSE;
    }

    /**
    * Get privilege information as an array, with numerical indexes
    * except for the logic, which is contained in a element with the
    * index "Logic".  Values are either an associative array with
    * three elements, "FieldId", "Operator", and "Value", or a
    * PrivilegeSet object (for subsets).
    * @return array Array with privilege information.
    */
    function GetPrivilegeInfo()
    {
        # grab privilege information and add logic
        $Info = $this->Privileges;
        $Info["Logic"] = $this->Logic;

        # return privilege info array to caller
        return $Info;
    }

    /**
    * Get list of privileges.  (Intended primarily for supporting legacy
    * privilege operations -- list contains privilege IDs only, and does
    * not include conditions.)
    * @return array Array of privilege IDs.
    */
    function GetPrivilegeList()
    {
        # create list of privileges with conditions stripped out
        $List = array();
        foreach ($this->Privileges as $Priv)
        {
            if (!is_array($Priv)) {  $List[] = $Priv;  }
        }

        # return list of privileges to caller
        return $List;
    }

    /**
    * Add condition to privilege set.  If the condition is already present
    * in the set, no action is taken.
    * @param mixed $Field Metadata field object or ID to test against.
    * @param mixed $Value Value to test against.  (Specify NULL for User
    *       fields to test against current user and for Date/Timestamp fields
    *       to test against the current date and time.)
    * @param string $Operator String containing operator to used for
    *       condition.  (Standard PHP operators are used.)  (OPTIONAL,
    *       defaults to "==")
    * @return bool TRUE if condition was added, otherwise FALSE.
    */
    function AddCondition($Field, $Value = NULL, $Operator = "==")
    {
        # get field ID
        $FieldId = is_object($Field) ? $Field->Id() : $Field;

        # set up condition array
        $Condition = array(
                "FieldId" => intval($FieldId),
                "Operator" => trim($Operator),
                "Value" => $Value);

        # if condition is not already in set
        if (!$this->IsInPrivilegeData($Condition))
        {
            # add condition to privilege set
            $this->Privileges[] = $Condition;
        }
    }

    /**
    * Remove condition from privilege set.  If condition was not present
    * in privilege set, no action is taken.
    * @param mixed $Field Metadata field object or ID to test against.
    * @param mixed $Value Value to test against.  (Specify NULL for User
    *       fields to test against current user.)
    * @param string $Operator String containing operator to used for
    *       condition.  (Standard PHP operators are used.)  (OPTIONAL,
    *       defaults to "==")
    * @return bool TRUE if condition was removed, otherwise FALSE.
    */
    function RemoveCondition(MetadataField $Field, $Value = NULL, $Operator = "==")
    {
        # get field ID
        $FieldId = is_object($Field) ? $Field->Id() : $Field;

        # set up condition array
        $Condition = array(
                "FieldId" => intval($FieldId),
                "Operator" => trim($Operator),
                "Value" => $Value);

        # if condition is in set
        if ($this->IsInPrivilegeData($Condition))
        {
            # remove condition from privilege set
            $Index = array_search($Condition, $this->Privileges);
            unset($this->Privileges[$Index]);
        }
    }

    /**
    * Add subgroup of privileges/conditions to set.
    * @param PrivilegeSet $Set Subgroup to add.
    */
    function AddSet(PrivilegeSet $Set)
    {
        # if subgroup is not already in set
        if (!$this->IsInPrivilegeData($Set))
        {
            # add subgroup to privilege set
            $this->Privileges[] = $Set;
        }
    }

    /**
    * Get/set whether all privileges/conditions in set are required (i.e.
    * "AND" logic), or only one privilege/condition needs to be met ("OR").
    * By default only one of the specified privilegs/conditions in a set
    * is required.
    * @param bool $NewValue Specify TRUE if all privileges are required,
    *       otherwise FALSE if only one privilege required.  (OPTIONAL)
    * @return bool TRUE if all privileges required, otherwise FALSE.
    */
    function AllRequired($NewValue = NULL)
    {
        if ($NewValue !== NULL)
        {
            $this->Logic = $NewValue ? "AND" : "OR";
        }
        return ($this->Logic == "AND") ? TRUE : FALSE;
    }

    /**
    * Get/set ID of user associated with privilege set.
    * @param int $NewValue ID of user to associate with set.  (OPTIONAL)
    * @return int ID of user currently associated with set, or NULL if
    *       no user currently associated.
    */
    function AssociatedUserId($NewValue = NULL)
    {
        # if new associated user specified
        if ($NewValue !== NULL)
        {
            # save ID of new associated user
            $this->UserId = $NewValue;
        }

        # return ID of currently associated user to caller
        return $this->UserId;
    }


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

    private $Privileges = array();
    private $Logic = "OR";
    private $UserId = NULL;

    /**
    * Check whether this privilege set meets the specified condition.
    * @param array $Condition Condition to check.
    * @param object $Resource Resource to use when checking.  (OPTIONAL)
    * @return bool TRUE if condition is met, otherwise FALSE.
    */
    private function MeetsCondition($Condition, Resource $Resource = NULL)
    {
        # if no resource was available to check against
        if ($Resource === NULL)
        {
            # report to caller that we do not meet condition
            return FALSE;
        }
        else
        {
            # pre-process condition parameters based on type of field
            $Field = new MetadataField($Condition["FieldId"]);
            $Operator = $Condition["Operator"];
            $Value = $Condition["Value"];
            $FieldValue  = $Resource->Get($Field, TRUE);
            switch ($Field->Type())
            {
                case MetadataSchema::MDFTYPE_USER:
                    # if supplied value is NULL
                    if ($Value === NULL)
                    {
                        # if local associated user ID is available
                        if ($this->UserId !== NULL)
                        {
                            # use ID of associated user
                            $Value = $this->UserId;
                        }
                        # else if global user ID available
                        elseif ($GLOBALS["G_User"]->IsLoggedIn())
                        {
                            # use global user ID
                            $Value = $GLOBALS["G_User"]->Id();
                        }
                        else
                        {
                            # report to caller that condition was not met
                            return FALSE;
                        }
                    }

                    # convert field value to user ID
                    $FieldValue = $FieldValue->Id();
                    break;

                case MetadataSchema::MDFTYPE_DATE:
                case MetadataSchema::MDFTYPE_TIMESTAMP:
                    # date field values are Date objects, so handle those
                    if ($FieldValue instanceof Date)
                    {
                        $FieldValue = strtotime($FieldValue->Formatted());
                    }

                    # timestamp field values are just the date/time string
                    else
                    {
                        $FieldValue = strtotime($FieldValue);
                    }

                    # use the current time for the value if it's NULL
                    if ($Value === NULL)
                    {
                        $Value = time();
                    }

                    # otherwise, parse the value to get a numeric timestamp
                    else
                    {
                        $Value = strtotime($Value);
                    }
                    break;

                case MetadataSchema::MDFTYPE_NUMBER:
                case MetadataSchema::MDFTYPE_FLAG:
                    break;

                default:
                    throw new Exception("Unsupported metadata field type ("
                            .print_r($Field->Type(), TRUE)
                            .") for condition in privilege set.");
                    break;
            }

            # compare field value and supplied value using specified operator
            switch ($Operator)
            {
                case "==":
                    $Result = ($FieldValue == $Value);
                    break;

                case "!=":
                    $Result = ($FieldValue != $Value);
                    break;

                case "<":
                    $Result = ($FieldValue < $Value);
                    break;

                case ">":
                    $Result = ($FieldValue > $Value);
                    break;

                case "<=":
                    $Result = ($FieldValue <= $Value);
                    break;

                case ">=":
                    $Result = ($FieldValue >= $Value);
                    break;

                default:
                    throw new Exception("Unsupported condition operator ("
                            .print_r($Operator, TRUE).") in privilege set.");
                    break;
            }

            # report to caller whether condition was met
            return $Result ? TRUE : FALSE;
        }
    }

    /**
    * Check whether specified item (privilege, condition, or subgroup) is
    * currently in the list of privileges.  This is necessary instead of just
    * using in_array() because in_array() generates NOTICE messages if the
    * array contains mixed types.
    * @param mixed $Item Item to look for.
    * @return bool TRUE if item found in privilege list, otherwise FALSE.
    */
    private function IsInPrivilegeData($Item)
    {
        # step through privilege data
        foreach ($this->Privileges as $Priv)
        {
            # report to caller if item is found
            if (is_object($Item))
            {
                if (is_object($Priv) && ($Item == $Priv)) {  return TRUE;  }
            }
            elseif (is_array($Item))
            {
                if (is_array($Priv) && ($Item == $Priv)) {  return TRUE;  }
            }
            elseif ($Item == $Priv) {  return TRUE;  }
        }

        # report to caller that item is not in privilege data
        return FALSE;
    }
}
