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

/**
* Plugin for defining and acting upon rules that describe a change in one or
* more metadata field states and actions to take when those changes are detected.
**/
class Rules extends Plugin
{

    # ---- STANDARD PLUGIN INTERFACE -----------------------------------------

    /**
    * Set the plugin attributes.  At minimum this method MUST set $this->Name
    * and $this->Version.  This is called when the plugin is initially loaded.
    */
    public function Register()
    {
        $this->Name = "Rules";
        $this->Version = "1.0.2";
        $this->Description = "Allows specifying rules that describe changes"
                ." in metadata that will trigger email to be sent or other"
                ." actions.";
        $this->Author = "Internet Scout";
        $this->Url = "http://scout.wisc.edu/cwis/";
        $this->Email = "scout@scout.wisc.edu";
        $this->Requires = array(
                "CWISCore" => "2.2.4",
                "Mailer" => "1.0.0");
        $this->EnabledByDefault = TRUE;

        $this->CfgSetup["MinutesBetweenChecks"] = array(
                "Type" => "Number",
                "Label" => "Rule Check Interval",
                "Units" => "minutes",
                "MaxVal" => 999999,
                "Default" => 5,
                "Help" => "The number of minutes between checks of the rules.",
                );
    }

    /**
    * Perform any work needed when the plugin is first installed (for example,
    * creating database tables).
    * @return NULL if installation succeeded, otherwise a string containing
    *       an error message indicating why installation failed.
    */
    public function Install()
    {
        # initialize empty rule set
        $this->ConfigSetting("Rules", array());

        # set a value for last run
        $this->ConfigSetting("LastRun", date("Y-m-d H:i:s"));

        return NULL;
    }

    /**
    * Initialize the plugin.  This is called after all plugins have been loaded
    * but before any methods for this plugin (other than Register() or Initialize())
    * have been called.
    * @return NULL if initialization was successful, otherwise a string containing
    *       an error message indicating why initialization failed.
    */
    public function Initialize()
    {
        $this->Rules = $this->ConfigSetting("Rules");
        /* SAMPLE RULES FOR TESTING
        if (!count($this->Rules))
        {
            $Schema = new MetadataSchema();
            $this->Rules[0] = array(
                    "RuleId" => 0,
                    "Enabled" => TRUE,
                    "Conditions" => array(
                            "FieldId" => $Schema->GetFieldIdByName("Title"),
                            "Operator" => "",
                            "Value" => "",
                            ),
                    "Actions" => array(
                            "Type" => "EMAIL",
                            "TemplateId" => 1,
                            "UserPrivilegeRestrictions" => array(PRIV_SYSADMIN),
                            ),
                    );
            $this->Rules[1] = array(
                    "RuleId" => 1,
                    "Enabled" => TRUE,
                    "Conditions" => array(
                            "FieldId" => $Schema->GetFieldIdByName("Description"),
                            "Operator" => "",
                            "Value" => "",
                            ),
                    "Actions" => array(
                            "Type" => "EMAIL",
                            "TemplateId" => 0,
                            "UserIsField" => $Schema->GetFieldIdByName(
                                    "Last Modified By Id"),
                            ),
                    );
            $this->ConfigSetting("Rules", $this->Rules);
        }
        */
        return NULL;
    }

    /**
    * Upgrade from a previous version.
    * @param string $PreviousVersion Previous version of the plugin.
    * @return Returns NULL on success and an error message otherwise.
    */
    public function Upgrade($PreviousVersion)
    {
        # upgrade from versions < 1.0.2 to 1.0.2
        if (version_compare($PreviousVersion, "1.0.2", "<"))
        {
            # make sure there is a value for last run
            $this->ConfigSetting("LastRun", date("Y-m-d H:i:s"));
        }
        return NULL;
    }

    /**
    * Hook the events into the application framework.
    * @return Returns an array of events to be hooked into the application
    *      framework.
    */
    public function HookEvents()
    {
        return array(
                "EVENT_COLLECTION_ADMINISTRATION_MENU" => "AddCollectionAdminMenuItems",
                "EVENT_PERIODIC" => "PeriodicRuleCheck",
                "EVENT_RESOURCE_MODIFY" => "ResourceModifiedRuleCheck",
                );
    }


    # ---- CALLABLE METHODS --------------------------------------------------

    /**
    * Get all existing rules.
    * @return Array of existing Rules with rule IDs for index.
    */
    public function GetRules()
    {
        return $this->Rules;
    }

    /**
    * Add new rule.
    * @return Returns new rule.
    */
    public function AddNewRule()
    {
        $NextId = count($this->Rules) ? max(array_keys($this->Rules)) + 1 : 1;
        $this->Rules[$NextId]["RuleId"] = $NextId;
        $this->ConfigSetting("Rules", $this->Rules);
        return $this->Rules[$NextId];
    }

    /**
    * Update existing rule.
    * Rule data is in the following format:
    *   $Rule["RuleId"]
    *   $Rule["Enabled"]
    *   $Rule["Conditions"]["FieldId"]
    *   $Rule["Conditions"]["Operator"]
    *   $Rule["Conditions"]["Value"]
    *   $Rule["Actions"]["Type"]
    *   $Rule["Actions"]["TemplateId"]
    *   $Rule["Actions"]["UserPrivilegeRestrictions"]
    * Either "Conditions" or "Actions" may also be arrays:
    *   $Rule["Conditions"][0]["FieldId"]
    *   $Rule["Conditions"][0]["Operator"]
    *   $Rule["Conditions"][0]["Value"]
    *   $Rule["Conditions"][1]["FieldId"]
    *   $Rule["Conditions"][1]["Operator"]
    *   $Rule["Conditions"][1]["Value"]
    *               :
    *             (etc)
    * @param mixed $Rule Updated rule info (multi-dimensional array).
    */
    public function UpdateRule($Rule)
    {
        $this->Rules[$Rule["RuleId"]] = $Rule;
        $this->ConfigSetting("Rules", $this->Rules);
    }

    /**
    * Delete existing rule.
    * @param int $Id ID of Rule to delete.
    */
    public function DeleteRule($Id)
    {
        if (isset($this->Rules[$Id]))
        {
            unset($this->Rules[$Id]);
            $this->ConfigSetting("Rules", $this->Rules);
        }
    }

    /**
    * Check rules and take any corresponding actions.
    * @param mixed $LastRunAt Date/time when rule check was last run.
    */
    public function CheckRules($LastRunAt)
    {
        # we need a last run
        if (!isset($LastRunAt))
        {
            return;
        }

        # for each rule
        foreach ($this->Rules as $Id => $Rule)
        {
            # if rule is enabled
            if ($Rule["Enabled"])
            {
                # make condition into an array if needed
                $Conditions = $Rule["Conditions"];
                if (isset($Conditions["FieldId"])) {  $Conditions = array($Conditions);  }

                # add field modification checks to conditions
                $ExpandedConditions = array();
                foreach ($Conditions as $Cond)
                {
                    if (isset($Cond["Operator"]) && strlen(trim($Cond["Operator"])))
                    {
                        $ExpandedConditions[] = $Cond;
                    }
                    $ExpandedConditions[] = array(
                            "FieldId" => $Cond["FieldId"],
                            "FieldAttribute" => "Modification",
                            "Operator" => ">",
                            "Value" => date("Y-m-d H:i:s", strtotime($LastRunAt)),
                            );
                }

                # look for resources that satisfy rule condition
                $Resources = $this->TestConditions($ExpandedConditions);

                # if resources were found
                if (count($Resources))
                {
                    # execute actions
                    $this->TakeAction($Rule["Actions"], $Resources);
                }
            }
        }
    }



    # ---- HOOKED METHODS ----------------------------------------------------

    /**
    * Add entries to the Collection Administration menu.
    * @return array List of entries to add, with the label as the value and
    *       the page to link to as the index.
    */
    public function AddCollectionAdminMenuItems()
    {
        return array(
                "List" => "Edit Automation Rules",
                );
    }

    /**
    * Check rules and take any corresponding actions (hooked to EVENT_PERIODIC).
    * @param string $LastRunAt Date and time the event was last run, in SQL
    *       date format. (Passed in by the event signaler, but not used)
    * @return int Number of minutes before the even should be run again.
    */
    public function PeriodicRuleCheck($LastRunAt)
    {
        # check the rules and take any necessary actions
        $this->CheckRules($this->ConfigSetting("LastRun"));

        # save the time last run (now)
        $this->ConfigSetting("LastRun", date("Y-m-d H:i:s"));

        # return to caller the number of minutes before we should check again
        return $this->ConfigSetting("MinutesBetweenChecks");
    }

    /**
    * Check rules and take any corresponding actions (hooked to
    * EVENT_RESOURCE_MODIFY).
    * @param Resource $Resource The Resource that has been modified.
    *       (passed in from the Event signaler, but not used)
    */
    public function ResourceModifiedRuleCheck($Resource)
    {
        # check the rules and take any necessary actions
        $this->CheckRules($this->ConfigSetting("LastRun"));

        # save the time last run (now)
        $this->ConfigSetting("LastRun", date("Y-m-d H:i:s"));
    }


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

    private $Rules;

    /**
    * Look for resources that meet a set of rule conditions.
    * @param array $Conditions Array of conditions.
    * @return array List of resource IDs.
    */
    private function TestConditions($Conditions)
    {
        # use SQL keyword appropriate to desired logic for operations
        $CombineWord = " AND ";

        # for each condition
        foreach ($Conditions as $Cond)
        {
            $FieldId = $Cond["FieldId"];
            $Operator = $Cond["Operator"];
            $Value = $Cond["Value"];

            # if condition is about field modification time
            if (isset($Cond["FieldAttribute"])
                    && $Cond["FieldAttribute"] == "Modification")
            {
                # build query independent of field type
                if (isset($Queries["ResourceFieldTimestamps"]))
                {
                    $Queries["ResourceFieldTimestamps"] .= $CombineWord;
                }
                else
                {
                    $Queries["ResourceFieldTimestamps"] = "SELECT DISTINCT ResourceId"
                            ." FROM ResourceFieldTimestamps WHERE ";
                }
                $Queries["ResourceFieldTimestamps"] .=
                        "(FieldId = ".intval($FieldId)
                        ." AND Timestamp ".$Operator." '".addslashes($Value)."') ";
            }
            else
            {
                # if field is valid
                if (MetadataSchema::FieldExistsInAnySchema($FieldId))
                {
                    # determine query based on field type
                    $Field = new MetadataField($FieldId);
                    switch ($Field->Type())
                    {
                        case MetadataSchema::MDFTYPE_TEXT:
                        case MetadataSchema::MDFTYPE_PARAGRAPH:
                        case MetadataSchema::MDFTYPE_NUMBER:
                        case MetadataSchema::MDFTYPE_FLAG:
                        case MetadataSchema::MDFTYPE_USER:
                        case MetadataSchema::MDFTYPE_URL:
                            if (isset($Queries["Resources"]))
                            {
                                $Queries["Resources"] .= $CombineWord;
                            }
                            else
                            {
                                $Queries["Resources"] = "SELECT DISTINCT ResourceId"
                                        ." FROM Resources WHERE ";
                            }
                            if ($Field->Type() == MetadataSchema::MDFTYPE_USER)
                            {
                                $User = new SPTUser($Value);
                                $Value = $User->Id();
                            }
                            $Queries["Resources"] .= "`".$Field->DBFieldName()
                                    ."` ".$Operator." '".addslashes($Value)."' ";
                            break;

                        case MetadataSchema::MDFTYPE_CONTROLLEDNAME:
                            $QueryIndex = "ResourceNameInts".$Field->Id();
                            if (!isset($Queries[$QueryIndex]["A"]))
                            {
                                $Queries[$QueryIndex]["A"] =
                                        "SELECT DISTINCT ResourceId"
                                        ." FROM ResourceNameInts, ControlledNames "
                                        ." WHERE ControlledNames.FieldId = ".$Field->Id()
                                        ." AND ( ";
                                $CloseQuery[$QueryIndex]["A"] = TRUE;
                            }
                            else
                            {
                                $Queries[$QueryIndex]["A"] .= $CombineWord;
                            }
                            $Queries[$QueryIndex]["A"] .=
                                    "((ResourceNameInts.ControlledNameId"
                                            ." = ControlledNames.ControlledNameId"
                                    ." AND ControlledNames.ControlledNameId "
                                            .$Operator." '".addslashes($Value)."'))";
                            if (!isset($Queries[$QueryIndex]["B"]))
                            {
                                $Queries[$QueryIndex]["B"] =
                                        "SELECT DISTINCT ResourceId"
                                        . " FROM ResourceNameInts, ControlledNames,"
                                                ." VariantNames "
                                        ." WHERE ControlledNames.FieldId = ".$Field->Id()
                                        ." AND ( ";
                                $CloseQuery[$QueryIndex]["B"] = TRUE;
                            }
                            else
                            {
                                $Queries[$QueryIndex]["B"] .= $CombineWord;
                            }
                            $Queries[$QueryIndex]["B"] .=
                                    "((ResourceNameInts.ControlledNameId"
                                            ." = ControlledNames.ControlledNameId"
                                    ." AND ResourceNameInts.ControlledNameId"
                                            ." = VariantNames.ControlledNameId"
                                    ." AND VariantNames.ControlledNameId "
                                            .$Operator." '".addslashes($Value)."'))";
                            break;

                        case MetadataSchema::MDFTYPE_OPTION:
                            $QueryIndex = "ResourceNameInts".$Field->Id();
                            if (!isset($Queries[$QueryIndex]))
                            {
                                $Queries[$QueryIndex] =
                                        "SELECT DISTINCT ResourceId"
                                        ." FROM ResourceNameInts, ControlledNames "
                                        ." WHERE ControlledNames.FieldId = ".$Field->Id()
                                        ." AND ( ";
                                $CloseQuery[$QueryIndex] = TRUE;
                            }
                            else
                            {
                                $Queries[$QueryIndex] .= $CombineWord;
                            }
                            $Queries[$QueryIndex] .= "(ResourceNameInts.ControlledNameId"
                                        ." = ControlledNames.ControlledNameId"
                                    ." AND ControlledNames.ControlledNameId ".$Operator
                                    ." '".addslashes($Value)."')";
                            break;

                        case MetadataSchema::MDFTYPE_TREE:
                            $QueryIndex = "ResourceClassInts".$Field->Id();
                            if (!isset($Queries[$QueryIndex]))
                            {
                                $Queries[$QueryIndex] = "SELECT DISTINCT ResourceId"
                                        ." FROM ResourceClassInts, Classifications "
                                        ." WHERE ResourceClassInts.ClassificationId"
                                            ." = Classifications.ClassificationId"
                                        ." AND Classifications.FieldId = ".$Field->Id()
                                        ." AND ( ";
                                $CloseQuery[$QueryIndex] = TRUE;
                            }
                            else
                            {
                                $Queries[$QueryIndex] .= $CombineWord;
                            }
                            $Queries[$QueryIndex] .= " Classifications.ClassificationId "
                                    .$Operator." '".addslashes($Value)."'";
                            break;

                        case MetadataSchema::MDFTYPE_TIMESTAMP:
                            # if value appears to have time component or text description
                            if (strpos($Value, ":")
                                    || strstr($Value, "day")
                                    || strstr($Value, "week")
                                    || strstr($Value, "month")
                                    || strstr($Value, "year")
                                    || strstr($Value, "hour")
                                    || strstr($Value, "minute"))
                            {
                                if (isset($Queries["Resources"]))
                                {
                                    $Queries["Resources"] .= $CombineWord;
                                }
                                else
                                {
                                    $Queries["Resources"] = "SELECT DISTINCT ResourceId"
                                            ." FROM Resources WHERE ";
                                }

                                # flip operator if necessary
                                if (strstr($Value, "ago"))
                                {
                                    $OperatorFlipMap = array(
                                            "<" => ">=",
                                            ">" => "<=",
                                            "<=" => ">",
                                            ">=" => "<",
                                            );
                                    $Operator = isset($OperatorFlipMap[$Operator])
                                            ? $OperatorFlipMap[$Operator] : $Operator;
                                }

                                # use strtotime method to build condition
                                $TimestampValue = strtotime($Value);
                                if (($TimestampValue !== FALSE) &&
                                    ($TimestampValue != -1))
                                {
                                    if ((date("H:i:s", $TimestampValue) == "00:00:00")
                                            && (strpos($Value, "00:00") === FALSE)
                                            && ($Operator == "<="))
                                    {
                                        $NormalizedValue =
                                                date("Y-m-d", $TimestampValue)
                                                ." 23:59:59";
                                    }
                                    else
                                    {
                                        $NormalizedValue = date("Y-m-d H:i:s",
                                                $TimestampValue);
                                    }
                                }
                                else
                                {
                                    $NormalizedValue = addslashes($Value);
                                }
                                $Queries["Resources"] .=
                                        " ( `".$Field->DBFieldName()."` "
                                        .$Operator
                                        ." '".$NormalizedValue."' ) ";
                            }
                            else
                            {
                                # use Date object method to build condition
                                $Date = new Date($Value);
                                if ($Date->Precision())
                                {
                                    if (isset($Queries["Resources"]))
                                    {
                                        $Queries["Resources"] .= $CombineWord;
                                    }
                                    else
                                    {
                                        $Queries["Resources"] =
                                                "SELECT DISTINCT ResourceId"
                                                ." FROM Resources WHERE ";
                                    }
                                    $Queries["Resources"] .= " ( ".$Date->SqlCondition(
                                            $Field->DBFieldName(), NULL, $Operator)." ) ";
                                }
                            }
                            break;

                        case MetadataSchema::MDFTYPE_DATE:
                            $Date = new Date($Value);
                            if ($Date->Precision())
                            {
                                if (isset($Queries["Resources"]))
                                {
                                    $Queries["Resources"] .= $CombineWord;
                                }
                                else
                                {
                                    $Queries["Resources"] = "SELECT DISTINCT ResourceId"
                                            ." FROM Resources WHERE ";
                                }
                                $Queries["Resources"] .= " ( ".$Date->SqlCondition(
                                        $Field->DBFieldName()."Begin",
                                        $Field->DBFieldName()."End", $Operator)." ) ";
                            }
                            break;

                        case MetadataSchema::MDFTYPE_IMAGE:
                        case MetadataSchema::MDFTYPE_FILE:
                            # (these types not yet handled for comparisons)
                            break;
                    }
                }
            }
        }

        # if queries found
        if (isset($Queries))
        {
            # for each assembled query
            $DB = new Database();
            foreach ($Queries as $QueryIndex => $Query)
            {
                # if query has multiple parts (e.g. controlled names + variant names)
                if (is_array($Query))
                {
                    # for each part of query
                    $ResourceIds = array();
                    foreach ($Query as $PartIndex => $PartQuery)
                    {
                        # add closing paren if query was flagged to be closed
                        if (isset($CloseQuery[$QueryIndex])) {  $PartQuery .= " ) ";  }

                        # perform query and retrieve IDs
                        $DB->Query($PartQuery);
                        $ResourceIds = array_merge($ResourceIds,
                                $DB->FetchColumn("ResourceId"));
                    }
                }
                else
                {
                    # add closing paren if query was flagged to be closed
                    if (isset($CloseQuery[$QueryIndex])) {  $Query .= " ) ";  }

                    # perform query and retrieve IDs
                    $DB->Query($Query);
                    $ResourceIds = $DB->FetchColumn("ResourceId");
                }

                # if we already have some results
                if (isset($Results))
                {
                    # if search logic is set to AND
                    if ($CombineWord == " AND ")
                    {
                        # remove anything from results that was not returned from query
                        $Results = array_intersect($Results, $ResourceIds);
                    }
                    else
                    {
                        # add values returned from query to results
                        $Results = array_unique(array_merge($Results, $ResourceIds));
                    }
                }
                else
                {
                    # set results to values returned from query
                    $Results = $ResourceIds;
                }
            }
        }
        else
        {
            # initialize results to empty list
            $Results = array();
        }

        # return results to caller
        return $Results;
    }

    /**
    * Execute one or more actions.
    * @param array $Actions Actions to run.
    * @param array $Resources Array of Resource IDs or objects to supply
    *       to actions that may use resources when they're run.
    */
    private function TakeAction($Actions, $Resources)
    {
        # make action into an array if needed
        if (isset($Actions["Type"])) {  $Actions = array($Actions);  }

        # for each action
        foreach ($Actions as $Action)
        {
            switch (strtoupper($Action["Type"]))
            {
                case "EMAIL":
                default:
                    # if mail recipient(s) should be taken from metadata field
                    if (isset($Action["UserIsField"]) && strlen($Action["UserIsField"]))
                    {
                        # if specified field was valid
                        $Schema = new MetadataSchema();
                        $Field = $Schema->GetField($Action["UserIsField"]);
                        if ($Field->Status() == MetadataSchema::MDFSTAT_OK)
                        {
                            # for each resource
                            $Users = array();
                            foreach ($Resources as $Resource)
                            {
                                # pull out resource object
                                if (!is_object($Resource))
                                {
                                    $Resource = new Resource($Resource);
                                }

                                # get user from specified field for resource
                                foreach ($Resource->Get($Field) as $UserId=>$UserName)
                                {
                                    $Users[$UserId][$Resource->Id()] = $Resource;
                                }
                            }

                            # for each user
                            foreach ($Users as $UserId => $UserResources)
                            {
                                # send email for user
                                if (!isset($Mailer))
                                {
                                    $Mailer = $GLOBALS["G_PluginManager"]->GetPlugin(
                                        "Mailer");
                                }
                                $Mailer->SendEmail($Action["TemplateId"],
                                        $UserId, $UserResources);
                            }
                        }
                    }
                    else
                    {
                        # if there are user privileges specified
                        if (count($Action["UserPrivilegeRestrictions"]))
                        {
                            # retrieve list of users with specified privileges
                            if (!isset($UFactory))
                            {
                                $DB = new Database();
                                $UFactory = new CWUserFactory();
                            }
                            $UserNames = $UFactory->GetUsersWithPrivileges(
                                    $Action["UserPrivilegeRestrictions"]);

                            # if users found
                            if (count($UserNames))
                            {
                                # pull out mailer plugin
                                if (!isset($Mailer))
                                {
                                    $Mailer = $GLOBALS["G_PluginManager"]->GetPlugin(
                                        "Mailer");
                                }

                                # filter out any invalid resources
                                $NewResources = array();
                                foreach ($Resources as $Resource)
                                {
                                    # snag resource object if we don't already have it
                                    if (!is_object($Resource))
                                    {
                                        $Resource = new Resource($Resource);
                                    }

                                    $NewResources[]= $Resource;
                                }
                                $Resources = $NewResources;

                                # send email
                                $Mailer->SendEmail($Action["TemplateId"],
                                        array_keys($UserNames), $Resources);
                            }
                        }
                    }
                    break;
            }
        }
    }
}
