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

class PrivilegeEditingUI
{

    /**
    * Constructor for privilege editing UI.
    * @param int $SchemaId ID for metadata schema that will be used for any
    *       resource references in privileges.
    */
    public function __construct($SchemaId = MetadataSchema::SCHEMAID_DEFAULT)
    {
        # save metadata schema
        $this->SchemaId = $SchemaId;
    }

    /**
    * Display interface for editing specified privilege set.
    * @param string $Identifier Alphanumeric identifier for this privilege set.
    * @param object $PrivilegeSet Current values for privilege set.
    * @param bool $IsNested (for recursion - DO NOT USE)
    */
    public function DisplaySet($Identifier, PrivilegeSet $PrivilegeSet,
            $IsNested = FALSE)
    {
        # include needed JavaScript
        $GLOBALS["AF"]->RequireUIFile(__CLASS__.".js");

        # build form field prefix
        $FFPrefix = "F_Priv_".$Identifier."_";

        # retrieve privilege info as an array
        $PrivilegeInfo = $PrivilegeSet->GetPrivilegeInfo();

        # add "Top-Level Logic" option list if we are at the top of the hierarchy
        if (!$IsNested)
        {
            $Logic = $PrivilegeInfo["Logic"];
            ?>
              <div class="priv-set">
                <fieldset class="priv-fieldset priv-logic">
                  <label for="<?PHP print $FFPrefix; ?>Logic">Top-Level Logic:</label>
                  <select id="<?PHP print $FFPrefix; ?>Logic"
                          name="<?PHP print $FFPrefix; ?>Logic">
                    <option <?PHP if ($Logic == "AND") print "selected"; ?>>AND</option>
                    <option <?PHP if ($Logic == "OR") print "selected"; ?>>OR</option>
                  </select>
                </fieldset>
            <?PHP
        }

        # discard logic so that the rest of the array can be iterated over
        unset($PrivilegeInfo["Logic"]);

        # if there are no conditions set
        if (count($PrivilegeInfo) == 0)
        {
            # print out message indicating no conditions yet set
            print("<i>(no requirements)</i><br/>");
        }
        else
        {
            # print out each condition
            foreach ($PrivilegeInfo as $Condition)
            {
                ?><fieldset class="priv-fieldset"><?PHP

                # if condition tests against a user privilege
                if (is_numeric($Condition))
                {
                    $this->DisplaySubjectField($FFPrefix, "current_user");
                    $this->DisplayOperatorField($FFPrefix);
                    $this->DisplayValueField($FFPrefix, $Condition);
                }
                # else if condition tests against whether we have a resource
                else if (is_array($Condition)
                        && $Condition["FieldId"] == PrivilegeSet::HAVE_RESOURCE)
                {
                    $this->DisplaySubjectField($FFPrefix, "have_resource");
                    $this->DisplayOperatorField($FFPrefix,
                            (bool)$Condition["Value"] ? "==" : "!=" );
                    $this->DisplayValueField($FFPrefix);
                }
                # else if condition tests against a metadata field value
                else if (is_array($Condition))
                {
                    $this->DisplaySubjectField($FFPrefix, $Condition["FieldId"]);
                    $this->DisplayOperatorField($FFPrefix, $Condition["Operator"]);

                    try
                    {
                        $Field = new MetadataField($Condition["FieldId"]);
                    }
                    catch (Exception $e)
                    {
                        # do nothing here, but we'd like to continue
                    }

                    if (isset($Field) && $Field->Type() == MetadataSchema::MDFTYPE_OPTION)
                    {
                        # Option fields use the selector menu, rather than a form field.
                        # Values are ControlledName Ids, prefixed with a "C" to distinguish
                        # them from privilge flag numbers.
                        $this->DisplayValueField($FFPrefix, "C".$Condition["Value"], "NULL");
                    }
                    else
                    {
                        $this->DisplayValueField($FFPrefix, NULL, $Condition["Value"]);
                    }
                }
                # else if condition is a privilege subset
                else if ($Condition instanceof PrivilegeSet)
                {
                    $this->DisplaySubjectField($FFPrefix, "set_entry");
                    $this->DisplayOperatorField($FFPrefix);
                    $this->DisplayValueField($FFPrefix, $Condition->AllRequired() ? "AND" : "OR");

                    # end the fieldset for the set entry row
                    ?></fieldset><?PHP

                    # print the nested fields
                    $this->DisplaySet($Identifier, $Condition, TRUE);

                    # begin a new fieldset for the set exit row
                    ?><fieldset class="priv-fieldset"><?PHP

                    $this->DisplaySubjectField($FFPrefix, "set_exit");
                    $this->DisplayOperatorField($FFPrefix);
                    $this->DisplayValueField($FFPrefix);
                }

                ?></fieldset><?PHP
            }
        }

        # if we are at the top level
        if (!$IsNested)
        {
            $NumBlankRows = 6;

            # print a number of blank rows to be used if JavaScript is disabled
            for ($Index = 0;  $Index < $NumBlankRows;  $Index++)
            {
                ?><fieldset class="priv-fieldset priv-extra"><?PHP
                $this->DisplaySubjectField($FFPrefix);
                $this->DisplayOperatorField($FFPrefix);
                $this->DisplayValueField($FFPrefix);
                ?></fieldset><?PHP
            }

            # print a blank row for cloning within JavaScript
            ?><fieldset class="priv-fieldset priv-js-clone_target"><?PHP
            $this->DisplaySubjectField($FFPrefix);
            $this->DisplayOperatorField($FFPrefix);
            $this->DisplayValueField($FFPrefix);
            ?></fieldset><?PHP

            # print the button to add a new row when using JavaScript
            ?><button class="priv-js-add">Add Condition</button><?PHP

            # print the closing div for the set
            ?></div><?PHP
        }
    }

    /**
    * Construct a new privilege set from the given array of form data.
    * @param array $FormData An array of form data coming from elements
    *   generated by PrintPrivilegeFields().  For example, if
    *   PrintPrivilegeFields() was called on the previous page with
    *   PrivilegeType = "ViewingPrivileges", you should you should pass
    *   $_POST["F_ViewingPrivileges"] in here.  This variable will be
    *   modified by reference.
    * @return Returns a PrivilegeSet object.
    * @throws Exception if invalid data is given.
    */
    function GetPrivilegeSetsFromForm()
    {
        # for each form field
        $Sets = array();
        $Logics = array();
        foreach ($_POST as $FieldName => $FieldValue)
        {
            # if field looks like privilege set data
            if (preg_match("/^F_Priv_/", $FieldName))
            {
                # extract identifier from field name
                $Pieces = explode("_", $FieldName);
                $Identifier = $Pieces[2];

                # if field looks like privilege set top-level logic
                if (preg_match("/_Logic\$/", $FieldName))
                {
                    # save logic for later use
                    $Logics[$Identifier] = $FieldValue;
                }
                else
                {
                    # retrieve privilege set from field
                    $Sets[$Identifier] =
                            $this->ExtractPrivilegeSetFromFormData($FieldValue);
                }
            }
        }

        # for each top-level logic found
        foreach ($Logics as $Identifier => $Logic)
        {
            # if no corresponding privilege set was found
            if (!isset($Sets[$Identifier]))
            {
                # load empty set for this identifier
                $Sets[$Identifier] = new PrivilegeSet();
            }

            # set logic in corresponding privilege set
            $Sets[$Identifier]->AllRequired(($Logic == "AND") ? TRUE : FALSE);
        }

        # return any privilege sets found to caller
        return $Sets;
    }


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

    private $SchemaId;

    /**
    * Print HTML select element used to decide the subject of a condition
    * (where conditions are comprised of 'subject', 'verb', and 'object',
    * and subject can be a metadata field, Current User, etc).
    * @param string $FFPrefix Prefix to use for form fields.
    * @param mixed $Selected The value to select. (OPTIONAL)
    */
    private function DisplaySubjectField($FFPrefix, $Selected=NULL)
    {
        $SupportedFields = $this->GetSupportedFieldsForPrivileges();

        # construct the list of options to present for this field
        # present all the metadata fields using their FieldIds
        $ValidSelections = array_keys($SupportedFields);
        # represent other elements as strings, which need to match the
        #   values in GetPrivilegeSetFromFormData()
        $ValidSelections []= "current_user";
        $ValidSelections []= "set_entry";
        $ValidSelections []= "set_exit";
        $ValidSelections []= "have_resource";
    ?>
      <select name="<?PHP print $FFPrefix; ?>[]"
              class="priv priv-field priv-select priv-field-subject priv-type-user_field priv-type-flag_field priv-type-option_field priv-type-timestamp_field priv-type-date_field priv-type-number_field priv-type-set_entry priv-type-set_exit">
        <option class="priv priv-option priv-field-subject priv-type-user_field priv-type-flag_field priv-type-option_field priv-type-timestamp_field priv-type-date_field priv-type-number_field priv-type-privilege priv-type-set_entry priv-type-set_exit">--</option>
        <option class="priv priv-option priv-field-subject priv-type-have_resource"
                <?PHP if ($Selected === "have_resource") print "selected"; ?>
                value="have_resource">Checking resource</option>
        <?PHP foreach ($SupportedFields as $Id => $Field) {
                  $SafeValue = defaulthtmlentities($Id);
                  $SafeLabel = defaulthtmlentities($Field->GetDisplayName());
                  $SafeClassType = defaulthtmlentities(strtolower($Field->TypeAsName())); ?>
          <option class="priv priv-option priv-field-subject priv-type-<?PHP print $SafeClassType; ?>_field"
                  <?PHP if ($Selected == $Id) print "selected"; ?>
                  value="<?PHP print $SafeValue; ?>"><?PHP print $SafeLabel; ?></option>
        <?PHP } ?>
        <option class="priv priv-option priv-field-subject priv-type-privilege"
                <?PHP if ($Selected === "current_user") print "selected"; ?>
                value="current_user">Current User</option>
        <option class="priv priv-option priv-field-subject priv-type-set_entry"
                <?PHP if ($Selected === "set_entry") print "selected"; ?>
                value="set_entry">(</option>
        <option class="priv priv-option priv-field-subject priv-type-set_exit"
                <?PHP if ($Selected === "set_exit") print "selected"; ?>
                value="set_exit">)</option>
        <?PHP if (!is_null($Selected) && !in_array($Selected, $ValidSelections, TRUE)){ ?>
            <option class="priv priv-option priv-field-subject priv-type-user_field" selected value="">INVALID FIELD</option>
        <?PHP } ?>
      </select>
    <?PHP
    }

    /**
    * Print the form field for the operator field.
    * @param string $FFPrefix Prefix to use for form fields.
    * @param mixed $Selected The value to select. (OPTIONAL)
    */
    private function DisplayOperatorField($FFPrefix, $Selected=NULL)
    {
        $SupportedOperators = $this->GetSupportedOperatorsForPrivileges();
    ?>
      <select name="<?PHP print $FFPrefix; ?>[]"
              class="priv priv-field priv-select priv-field-operator priv-type-user_field priv-type-flag_field priv-type-option_field priv-type-timestamp_field priv-type-date_field priv-type-number_field">
        <option class="priv priv-option priv-field-operator priv-type-user_field priv-type-flag_field priv-type-option_field priv-type-timestamp_field priv-type-date_field priv-type-number_field priv-type-privilege priv-type-set_entry priv-type-set_exit">--</option>
        <?PHP if (in_array("==", $SupportedOperators)) { ?>
          <option class="priv priv-option priv-field-operator priv-type-user_field priv-type-flag_field priv-type-option_field priv-type-number_field priv-type-have_resource"
                  <?PHP if ($Selected == "==") print "selected"; ?>
                  value="==">==</option>
        <?PHP } ?>
        <?PHP if (in_array("==", $SupportedOperators)) { ?>
          <option class="priv priv-option priv-field-operator priv-type-user_field priv-type-flag_field priv-type-option_field priv-type-number_field priv-type-have_resource"
                  <?PHP if ($Selected == "!=") print "selected"; ?>
                  value="!=">!=</option>
        <?PHP } ?>
        <?PHP if (in_array("==", $SupportedOperators)) { ?>
          <option class="priv priv-option priv-field-operator priv-type-timestamp_field priv-type-date_field priv-type-number_field"
                  <?PHP if ($Selected == "<") print "selected"; ?>
                  value="&lt;">&lt;</option>
        <?PHP } ?>
        <?PHP if (in_array("==", $SupportedOperators)) { ?>
          <option class="priv priv-option priv-field-operator priv-type-timestamp_field priv-type-date_field priv-type-number_field"
                  <?PHP if ($Selected == ">") print "selected"; ?>
                  value="&gt;">&gt;</option>
        <?PHP } ?>
      </select>
    <?PHP
    }

    /**
    * Print the form fields for the value field.
    * @param string $FFPrefix Prefix to use for form fields.
    * @param mixed $Selected The value to select for the select box. (OPTIONAL)
    * @param mixed $Value The existing value for the input box. (OPTIONAL)
    */
    private function DisplayValueField($FFPrefix, $Selected=NULL, $Value=NULL)
    {
        $this->PrintPrivilegeValueSelectorField($FFPrefix, $Selected);
        $this->PrintPrivilegeValueInputField($FFPrefix, $Value);
    }

    /**
    * Construct a new privilege set from the given array of form data.
    * @param array $FormData An array of form data coming from elements
    *   generated by PrintPrivilegeFields().  For example, if
    *   PrintPrivilegeFields() was called on the previous page with
    *   PrivilegeType = "ViewingPrivileges", you should you should pass
    *   $_POST["F_ViewingPrivileges"] in here.  This variable will be
    *   modified by reference.
    * @return Returns a PrivilegeSet object.
    * @throws Exception if invalid data is given.
    */
    function ExtractPrivilegeSetFromFormData(array &$FormData)
    {
        $NewPrivilegeSet = new PrivilegeSet();
        $Privileges = $this->GetPrivileges();
        $SupportedFields = $this->GetSupportedFieldsForPrivileges();
        $SupportedOperators = $this->GetSupportedOperatorsForPrivileges();

        while (count($FormData))
        {
            # extract the form fields
            $SubjectField = array_shift($FormData);
            $OperatorField = array_shift($FormData);
            $ValueSelectField = array_shift($FormData);
            $ValueInputField = array_shift($FormData);

            # privilege condition
            if ($SubjectField == "current_user")
            {
                # invalid privilege ID
                if (!isset($Privileges[$ValueSelectField])
                        || is_null($ValueSelectField))
                {
                    throw new Exception("Invalid privilege (".$ValueSelectField.")");
                }

                $NewPrivilegeSet->AddPrivilege($ValueSelectField);
            }

            # resource exists condition
            else if ($SubjectField == "have_resource")
            {
                 $NewPrivilegeSet->AddCondition(
                     PrivilegeSet::HAVE_RESOURCE,
                         ($OperatorField == "==") ? TRUE : FALSE, "==" );
            }
            # metadata field condition
            else if (is_numeric($SubjectField))
            {
                # invalid field ID
                if (!isset($SupportedFields[$SubjectField]) || is_null($SubjectField))
                {
                    throw new Exception("Invalid or unsupported field ("
                            .$SubjectField.")");
                }

                # invalid operator
                if (!in_array($OperatorField, $SupportedOperators))
                {
                    throw new Exception("Invalid or unsupported operator ("
                            .$OperatorField.")");
                }

                $MetadataField = $SupportedFields[$SubjectField];

                switch ($MetadataField->Type())
                {
                    case MetadataSchema::MDFTYPE_USER:
                        $Value = NULL;
                        break;

                    case MetadataSchema::MDFTYPE_FLAG:
                        $Value = 1;
                        break;

                    case MetadataSchema::MDFTYPE_TIMESTAMP:
                    case MetadataSchema::MDFTYPE_DATE:
                    case MetadataSchema::MDFTYPE_NUMBER:
                        $Value = $ValueInputField;
                        break;

                    case MetadataSchema::MDFTYPE_OPTION:
                        # strip the "C" prefix used to distinguish controlled
                        #       names from priv flags
                        $Value = intval( substr( $ValueSelectField, 1));
                        break;

                    default:
                        $Value = NULL;
                        break;
                }

                $NewPrivilegeSet->AddCondition($MetadataField, $Value, $OperatorField);
            }

            # entering a nested privilege set
            else if ($SubjectField == "set_entry")
            {
                # the logic is invalid
                if ($ValueSelectField != "AND" && $ValueSelectField != "OR")
                {
                    throw new Exception("Invalid privilege set logic ("
                            .$ValueSelectField.")");
                }

                $NestedPrivilegeSet = $this->ExtractPrivilegeSetFromFormData($FormData);

                # only add the nested privilege set if it's not empty. use 1
                # because the logic is in the privilege info array
                if (count($NestedPrivilegeSet->GetPrivilegeInfo()) > 1)
                {
                    $NestedPrivilegeSet->AllRequired($ValueSelectField == "AND");
                    $NewPrivilegeSet->AddSet($NestedPrivilegeSet);
                }
            }

            # exiting a privilege set
            else if ($SubjectField == "set_exit")
            {
                break;
            }

            # unknown condition type
            else
            {
                throw new Exception("Unknown condition type: ".$SubjectField);
            }
        }

        return $NewPrivilegeSet;
    }

    /**
    * Print a select box for the value field.
    * @param string $FFPrefix Prefix to use for form fields.
    * @param mixed $Selected The value to select. (OPTIONAL)
    */
    private function PrintPrivilegeValueSelectorField($FFPrefix, $Selected=NULL)
    {
        $Privileges = $this->GetPrivileges();

       $OptionValues = $this->GetOptionValuesForPrivset();
    ?>
      <select name="<?PHP print $FFPrefix; ?>[]"
              class="priv priv-field priv-select priv-field-value priv-type-option_field priv-type-privilege priv-type-set_entry priv-type-set_exit">
        <option class="priv priv-option priv-field-value priv-type-user_field priv-type-flag_field priv-type-option_field priv-type-timestamp_field priv-type-date_field priv-type-number_field priv-type-privilege priv-type-set_entry priv-type-set_exit">--</option>
        <?PHP foreach ($Privileges as $Id => $Privilege) {
                  $SafeValue = defaulthtmlentities($Id);
                  $SafeLabel = defaulthtmlentities($Privilege->Name()); ?>
          <option class="priv priv-option priv-field-value priv-type-privilege"
                  <?PHP if ($Selected == $Id) print "selected"; ?>
                  value="<?PHP print $SafeValue; ?>"><?PHP print $SafeLabel; ?> privilege</option>
        <?PHP }

        foreach ($OptionValues as $FieldName => $Values)
        {
            $SafeName = defaulthtmlentities($FieldName);
            foreach ($Values as $CNId => $CName)
            {
                # prefix CNIds with "C" to distinguish from privilege flag numbers
                $SafeValue = "C".defaulthtmlentities($CNId);
                $SafeLabel = defaulthtmlentities($CName);
                ?><option class="priv priv-option priv-field-value priv-type-option_field"
                          data-field-name="<?PHP print $SafeName; ?>"
                          <?PHP if ($Selected == "C".$CNId) print "selected"; ?>
                          value="<?PHP print $SafeValue; ?>"><?PHP print $SafeLabel; ?></option><?PHP
            }
        }
        ?>
        <option class="priv priv-option priv-field-value priv-type-set_entry"
                <?PHP if ($Selected == "AND") print "selected"; ?>
                value="AND">AND</option>
        <option class="priv priv-option priv-field-value priv-type-set_entry"
                <?PHP if ($Selected == "OR") print "selected"; ?>
                value="OR">OR</option>
      </select>
    <?PHP
    }

    /**
    * Print an input box for the value field.
    * @param string $FFPrefix Prefix to use for form fields.
    * @param mixed $Value The existing value. (OPTIONAL)
    */
    private function PrintPrivilegeValueInputField($FFPrefix, $Value=NULL)
    {
        $SafeValue = defaulthtmlentities($Value);
    ?>
      <input name="<?PHP print $FFPrefix; ?>[]"
             type="text"
             class="priv priv-field priv-input priv-field-value priv-type-timestamp_field priv-type-date_field priv-type-number_field"
             value="<?PHP print $SafeValue; ?>" />
    <?PHP
    }


    /**
    * Get the list of all metadata fields supported in the privileges associated
    * with metadata schemas.
    * @return Returns an array of supported metadata fields.
    */
    private function GetSupportedFieldsForPrivileges()
    {
        static $SupportedFields;

        if (!isset($SupportedFields))
        {
            $Schema = new MetadataSchema($this->SchemaId);
            $SupportedFieldTypesInOrder = array(
                MetadataSchema::MDFTYPE_USER,
                MetadataSchema::MDFTYPE_FLAG,
                MetadataSchema::MDFTYPE_OPTION,
                MetadataSchema::MDFTYPE_TIMESTAMP,
                MetadataSchema::MDFTYPE_NUMBER);

            $SupportedFields = array();

            foreach ($SupportedFieldTypesInOrder as $Type)
            {
                $SupportedFields += $Schema->GetFields($Type);
            }
        }

        return $SupportedFields;
    }

    /**
    * Get the list of supported operators that could be used in privilege
    * settings.
    * @return Returns an array of the supported operators.
    */
    private function GetSupportedOperatorsForPrivileges()
    {
        static $Operators = array("==", "!=", "<", ">");

        return $Operators;
    }

    /**
    * Get the list of option values allowed for privilege sets
    * @return array( Field Name => array(OptionId => OptionValue))
    */
    private function GetOptionValuesForPrivset()
    {
        static $OptionValues;

        if (!isset($OptionValues))
        {
            $OptionValues = array();
            $Schema = new MetadataSchema($this->SchemaId);

            foreach ($Schema->GetFields(MetadataSchema::MDFTYPE_OPTION) as $Field)
            {
                $OptionValues[$Field->Name()] = $Field->GetPossibleValues();
            }
        }

        return $OptionValues;
    }

    /**
    * Get the list of privileges.
    * @return Returns an array of all privileges.
    */
    private function GetPrivileges()
    {
        static $Privileges;

        if (!isset($Privileges))
        {
            $PrivilegeFactory = new PrivilegeFactory();
            $Privileges = $PrivilegeFactory->GetPrivileges();
        }

        return $Privileges;
    }
}


