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

/**
* Helps print and validate form fields, and their errors, associated with a
* form.
*/
class FormTool
{
    # ---- PUBLIC INTERFACE --------------------------------------------------

    /**
    * Object constructor.
    * @param mixed $FormFields Array of form field objects or the path to a file
    *      that loads and returns form field objects via @p $FormFields.
    * @param array $AdditionalErrorMessages Additional error messages to add, if
    *      any. This parameter is optional.
    */
    public function __construct($FormFields, $AdditionalErrorMessages = NULL)
    {
        # if form field value is not an array
        if (!is_array($FormFields))
        {
            # look for form field file to include
            $PossibleFileNames = array(
                    "local/include/%NAME%",
                    "local/include/%NAME%.php",
                    "local/include/Form--%NAME%.php",
                    "local/include/SPT--Form--%NAME%.php",
                    "include/%NAME%",
                    "include/%NAME%.php",
                    "include/Form--%NAME%.php",
                    "include/SPT--Form--%NAME%.php",
                    );
            foreach ($PossibleFileNames as $FileName)
            {
                $FileName = str_replace("%NAME%", $FormFields, $FileName);
                if (file_exists($FileName))
                {
                    $FormFieldFile = $FileName;
                    break;
                }
            }

            # if form field file was found
            if (isset($FormFieldFile))
            {
                # load form field file (should set $FormFields)
                include_once($FormFieldFile);
            }
        }

        # save field info with field name as index
        foreach ($FormFields as $Field)
        {
            $this->Fields[$Field->Name()] = $Field;
        }

        # save additional error messages (if any)
        $this->AdditionalErrorMessages = $AdditionalErrorMessages;

        # set default error color
        $this->ErrorColor = "red";

        # save any additional fields indicated to be marked for error
        if (isset($_GET["FTAddErrFields"]) && count($_GET["FTAddErrFields"]))
        {
            $this->AdditionalErrorFields = explode("!", $_GET["FTAddErrFields"]);
        }
        else
        {
            $this->AdditionalErrorFields = array();
        }
    }

    /**
    * Get and set the value for a form field.
    * @param string $FieldName Form field name.
    * @param mixed $NewValue New value to set for the form field. This parameter
    *      is optional.
    * @return mixed Returns The current value for the form field.
    */
    public function ValueForField($FieldName, $NewValue = NULL)
    {
        return $this->Fields[$FieldName]->Value($NewValue);
    }

    /**
    * Determine whether all of the required form field values are set in the
    * POST variables.
    * @return Returns TRUE if all the required form field values are set in the
    *      POST variables.
    */
    public function AllRequiredVarsAreSet()
    {
        # assume that all required variables will be found
        $AllFound = TRUE;

        # for each required form variable
        foreach ($this->Fields as $FieldName => $Field)
        {
            # if variable is not set in $_POST
            if (!isset($_POST[$FieldName]) || !strlen($_POST[$FieldName]))
            {
                # set flag indicating we found a missing var and exit loop
                $AllFound = FALSE;
                break;
            }
        }

        # report back to caller whether we found on required vars
        return $AllFound;
    }

    /**
    * Return the URL parameter string with form field values that are set in
    * the POST variables. The URL parameter string does not include a leading
    * separator (? or &).
    * @param bool $IncludePasswords TRUE to include password fields in the URL
    *      parameters.
    */
    public function GetValsAsUrlParams($IncludePasswords = FALSE)
    {
        # assume no values will be found
        $UrlParams = "";

        # for each form field
        foreach ($this->Fields as $FieldName => $Field)
        {
            # if form value is set and contains something and is not excluded
            if (isset($_POST[$FieldName])
                    && strlen(trim($_POST[$FieldName]))
                    && (!$Field->IsPassword() || $IncludePasswords))
            {
                # add value to URL param string
                $UrlParams = strlen($UrlParams)
                        ? $UrlParams."&".$FieldName."=".urlencode($_POST[$FieldName])
                        : $FieldName."=".urlencode($_POST[$FieldName]);
            }
        }

        # return parameter string to caller
        return $UrlParams;
    }

    /**
    * Set the values for the form fields from the URL parameters when available.
    */
    public function SetFieldValuesFromUrlParams()
    {
        # for each field
        foreach ($this->Fields as $FieldName => $Field)
        {
            # if value is available for field in incoming GET parameters
            if (isset($_GET[$FieldName]))
            {
                # set field value
                $Field->Value($_GET[$FieldName]);
            }
        }
    }

    /**
    * Check the incoming form values for each field and report whether errors
    * were found.
    * @return Returns TRUE if errors were found.
    */
    public function IncomingFieldValuesHaveErrors()
    {
        return (count($_GET) || count($_POST)) ?
            (strlen($this->GetErrorCodesAsUrlParams()) ? TRUE : FALSE) : FALSE;
    }

    /**
    * Return the URL parameter string with error codes for any form value
    * errors. The URL parameter string does not include a leading separator (?
    * or &).
    * @return Returns the URL parameter string with error codes or an empty
    *      string if no errors were found.
    */
    public function GetErrorCodesAsUrlParams()
    {
        # start with empty error code string
        $ErrorCodeString = "";

        # for each field value
        foreach ($this->Fields as $FieldName => $Field)
        {
            # if validation function says that value is invalid
            $ErrorCode = $this->CheckFieldValue($FieldName);
            if ($ErrorCode)
            {
                # add error code for value to error code string
                $ErrorCodeString .= (strlen($ErrorCodeString) ? "!" : "")
                        .$FieldName."-".$ErrorCode;
            }
        }

        # if values were added to error code string
        if (strlen($ErrorCodeString))
        {
            # prepend name of GET variable to contain error codes
            $ErrorCodeString = "FTFieldErrs=".$ErrorCodeString;
        }

        # if additional error codes were supplied
        if (isset($this->AdditionalErrorCodes) && count($this->AdditionalErrorCodes))
        {
            # for each additional error code
            foreach ($this->AdditionalErrorCodes as $Code)
            {
                # append code to string
                $AddCodeString = isset($AddCodeString) ? $AddCodeString."!".$Code
                        : $Code;
            }

            # append additional error code string to error code string
            $ErrorCodeString .= (strlen($ErrorCodeString) ? "&" : "")
                    ."FTAddErrCodes=".$AddCodeString;
        }

        # if additional fields were supplied to be marked as erroneous
        if (count($this->AdditionalErrorFields))
        {
            # for each additional error code
            foreach ($this->AdditionalErrorFields as $FieldName)
            {
                # append code to string
                $AddFieldString = isset($AddFieldString) ? $AddFieldString."!".$FieldName
                        : $FieldName;
            }

            # append additional error code string to error code string
            $ErrorCodeString .= (strlen($ErrorCodeString) ? "&" : "")
                    ."FTAddErrFields=".$AddFieldString;
        }

        # return error code string to caller
        return $ErrorCodeString;
    }

    /**
    * Save additional fields marked as having errors.
    * @param array $FieldNames Field names to mark as having errors.
    */
    public function SetAdditionalErrorFields($FieldNames)
    {
        # convert to array if needed
        if (!is_array($FieldNames))
        {
            $FieldNames = array($FieldNames);
        }

        # save fields (if not already present)
        foreach ($FieldNames as $FieldName)
        {
            if (!in_array($FieldName, $this->AdditionalErrorFields))
            {
                $this->AdditionalErrorFields[] = $FieldName;
            }
        }
    }

    /**
    * Save additional error codes if not already present.
    * @param array $Codes Additional error codes to save.
    */
    public function SetAdditionalErrorCodes($Codes)
    {
        # convert to array if needed
        if (!is_array($Codes))
        {
            $Codes = array($Codes);
        }

        # save codes (if not already present)
        foreach ($Codes as $Code)
        {
            if (!isset($this->AdditionalErrorCodes)
                    || !in_array($Code, $this->AdditionalErrorCodes))
            {
                $this->AdditionalErrorCodes[] = $Code;
            }
        }
    }

    /**
    * Convenience method that adds value and error codes to the URL.
    * @param string $BaseUrl Base URL to which to add the value and error codes.
    * @param bool $IncludePasswords TRUE to include password fields in the URL
    *      parameters.
    * @return Returns the base URL with the values and error codes added to it.
    */
    public function GetUrlWithValuesAndErrorCodes($BaseUrl, $IncludePasswords = FALSE)
    {
        $ValParams = $this->GetValsAsUrlParams($IncludePasswords);
        $ErrParams = $this->GetErrorCodesAsUrlParams();
        $ParamStart = strpos($BaseUrl, "?") ? "&" : "?";
        return $BaseUrl
                .(strlen($ValParams) ? $ParamStart.$ValParams : "")
                .(strlen($ErrParams) ?
                        (strlen($ValParams) ? "&" : $ParamStart).$ErrParams : "");
    }

    /**
    * Get the list of error messages based on the error codes from the URL via
    * $_GET.
    * @param bool $EliminateDuplicateMessages TRUE to remove error messages that
    *      are exactly the same.
    * @return Returns an array of error messages.
    */
    public function GetErrorMessages($EliminateDuplicateMessages = TRUE)
    {
        # start with empty list
        $ErrorList = array();

        # if it looks like there are field-specific error messages to be had
        if (isset($_GET["FTFieldErrs"]))
        {
            # split error data into list of fields
            $FieldList = explode("!", $_GET["FTFieldErrs"]);

            # for each field found
            foreach ($FieldList as $FieldListEntry)
            {
                # split field entry into name and code
                list($FieldName, $ErrorCode) = explode("-", $FieldListEntry);

                # if we know about this field
                if (isset($this->Fields[$FieldName]))
                {
                    # translate error code into message and add to list
                    $Field = $this->Fields[$FieldName];
                    $Replacements = array(
                            "%N" => "<i>".$Field->Name()."</i>",
                            "%V" => "<i>".$Field->Value()."</i>",
                            "%L" => "<i>".preg_replace("/:$/", "",
                                    $Field->Label())."</i>",
                            "%C" => "<i>".$ErrorCode."</i>",
                            );
                    $Message = $Field->GetInvalidValueMessage($ErrorCode);
                    $ErrorList[$FieldName] = str_replace(
                            array_keys($Replacements), $Replacements, $Message);
                }
            }
        }

        # if it looks like there are additional general error messages to be had
        if (isset($_GET["FTAddErrCodes"]) && count($this->AdditionalErrorMessages))
        {
            # split error data into list of codes
            $CodeList = explode("!", $_GET["FTAddErrCodes"]);

            # for each code found
            foreach ($CodeList as $Code)
            {
                # if there is a message corresponding to this code
                if (isset($this->AdditionalErrorMessages[$Code]))
                {
                    # add message to list
                    $ErrorList[$Code] = $this->AdditionalErrorMessages[$Code];
                }
            }
        }

        # remove duplicate messages (if requested by caller)
        if ($EliminateDuplicateMessages)
        {
            $NewErrorList = array();
            foreach ($ErrorList as $Code => $Message)
            {
                if (!in_array($Message, $NewErrorList))
                {
                    $NewErrorList[$Code] = $Message;
                }
            }
            $ErrorList = $NewErrorList;
        }

        # return list of error messages to caller
        return $ErrorList;
    }

    /**
    * Print the label and input for given form field.
    * @param string $FieldName Form field name.
    */
    public function PrintField($FieldName)
    {
        $this->Fields[$FieldName]->PrintField(
                ($this->ErrorCodesAvailable() && $this->CheckFieldValue($FieldName))
                || in_array($FieldName, $this->AdditionalErrorFields));
    }

    /**
    * Print the label element for the given field.
    * @param string $FieldName Form field name.
    */
    public function PrintLabelForField($FieldName)
    {
        $this->Fields[$FieldName]->PrintLabel(
                ($this->ErrorCodesAvailable() && $this->CheckFieldValue($FieldName))
                || in_array($FieldName, $this->AdditionalErrorFields));
    }

    /**
    * Print the input element for the given field.
    * @param string $FieldName Form field name.
    */
    public function PrintInputForField($FieldName)
    {
        $this->Fields[$FieldName]->PrintInput(
                ($this->ErrorCodesAvailable() && $this->CheckFieldValue($FieldName))
                || in_array($FieldName, $this->AdditionalErrorFields));
    }

    /**
    * Determine if there are any error codes available from the form.
    * @return Return TRUE if there are any error codes available.
    */
    public function ErrorCodesAvailable()
    {
        return isset($_GET["FTFieldErrs"]) || isset($_GET["FTAddErrCodes"]);
    }

    /**
    * Get an array of US state names with their two-letter abbreviations as the
    * index.
    * @return Returns an array of US state names with their two-letter
    *      abbreviations as the index.
    */
    public static function GetArrayOfUsStates()
    {
        return array(
                "" => "--",
                "AL" => "Alabama",
                "AK" => "Alaska",
                "AZ" => "Arizona",
                "AR" => "Arkansas",
                "CA" => "California",
                "CO" => "Colorado",
                "CT" => "Connecticut",
                "DE" => "Delaware",
                "DC" => "District of Columbia",
                "FL" => "Florida",
                "GA" => "Georgia",
                "HI" => "Hawaii",
                "ID" => "Idaho",
                "IL" => "Illinois",
                "IN" => "Indiana",
                "IA" => "Iowa",
                "KS" => "Kansas",
                "KY" => "Kentucky",
                "LA" => "Louisiana",
                "ME" => "Maine",
                "MD" => "Maryland",
                "MA" => "Massachusetts",
                "MI" => "Michigan",
                "MN" => "Minnesota",
                "MS" => "Mississippi",
                "MO" => "Missouri",
                "MT" => "Montana",
                "NE" => "Nebraska",
                "NV" => "Nevada",
                "NH" => "New Hampshire",
                "NJ" => "New Jersey",
                "NM" => "New Mexico",
                "NY" => "New York",
                "NC" => "North Carolina",
                "ND" => "North Dakota",
                "OH" => "Ohio",
                "OK" => "Oklahoma",
                "OR" => "Oregon",
                "PA" => "Pennsylvania",
                "RI" => "Rhode Island",
                "SC" => "South Carolina",
                "SD" => "South Dakota",
                "TN" => "Tennessee",
                "TX" => "Texas",
                "UT" => "Utah",
                "VT" => "Vermont",
                "VA" => "Virginia",
                "WA" => "Washington",
                "WV" => "West Virginia",
                "WI" => "Wisconsin",
                "WY" => "Wyoming",
                );
    }


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

    private $Fields;
    private $ErrorColor;
    private $AdditionalErrorCodes;
    private $AdditionalErrorFields;
    private $AdditionalErrorMessages;

    /**
    * Extracts a form variable for the specified field and validates it via the
    * form objects.
    * @param string $FieldName Form field name.
    * @return Returns an error code.
    */
    private function CheckFieldValue($FieldName)
    {
        $Value = isset($_POST[$FieldName]) ? $_POST[$FieldName]
                : (isset($_GET[$FieldName]) ? $_GET[$FieldName] : NULL);
        $ErrorCode = $this->Fields[$FieldName]->IsInvalidValue($Value);
        return $ErrorCode;
    }
}
