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

/**
* Plugin for providing a common mechanism for sending emails to users
* using templates.
*/
class Mailer 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 = "Mailer";
        $this->Version = "1.1.0";
        $this->Description = "Generates and emails messages to users based"
                ." on templates.";
        $this->Author = "Internet Scout";
        $this->Url = "http://scout.wisc.edu/cwis/";
        $this->Email = "scout@scout.wisc.edu";
        $this->Requires = array(
                "CWISCore" => "2.2.4");
        $this->EnabledByDefault = TRUE;

        $this->CfgSetup["BaseUrl"] = array(
                "Type" => "Text",
                "Label" => "Base URL",
                "Help" => "This value overrides any automatically-determined"
                        ." value for the X-BASEURL-X template keyword.",
                );

        $this->CfgSetup["EmailTaskPriority"] = array(
            "Type" => "Option",
            "Label" => "E-mail Task Priority",
            "Help" => "Priority of the e-mail sending tasks when using the task queue.",
            "AllowMultiple" => FALSE,
            "Default" => ApplicationFramework::PRIORITY_BACKGROUND,
            "Options" => array(
                ApplicationFramework::PRIORITY_BACKGROUND => "Background",
                ApplicationFramework::PRIORITY_LOW => "Low",
                ApplicationFramework::PRIORITY_MEDIUM => "Medium",
                ApplicationFramework::PRIORITY_HIGH => "High"));
    }

    /**
    * 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()
    {
        $this->ConfigSetting("Templates", array());
        return NULL;
    }

    /**
    * Upgrade from a previous version.
    * @param string $PreviousVersion Previous version number.
    */
    public function Upgrade($PreviousVersion)
    {
        # upgrade to 1.0.1
        if (version_compare($PreviousVersion, "1.0.1", "<"))
        {
            # get the current list of templates
            $Templates = $this->ConfigSetting("Templates");

            # add the "CollapseBodyMargins" setting
            foreach ($Templates as $Id => $Template)
            {
                $Templates[$Id]["CollapseBodyMargins"] = FALSE;
            }

            # set the updated templates
            $this->ConfigSetting("Templates", $Templates);
        }

        # upgrade to 1.0.2
        if (version_compare($PreviousVersion, "1.0.2", "<"))
        {
            # get the current list of templates
            $Templates = $this->ConfigSetting("Templates");

            # add the plain text fields
            foreach ($Templates as $Id => $Template)
            {
                $Templates[$Id]["PlainTextBody"] = "";
                $Templates[$Id]["PlainTextItemBody"] = "";
            }

            # set the updated templates
            $this->ConfigSetting("Templates", $Templates);
        }

        # upgrade to 1.0.6
        if (version_compare($PreviousVersion, "1.0.6", "<"))
        {
            $this->ConfigSetting(
                "EmailTaskPriority",
                ApplicationFramework::PRIORITY_BACKGROUND);
        }
    }

    /**
    * 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",
                );
    }

    /**
    * Declare events.
    * @return an array of the events this plugin provides.
    */
    public function DeclareEvents()
    {
        return array(
            "Mailer_EVENT_IS_TEMPLATE_IN_USE" =>
            ApplicationFramework::EVENTTYPE_CHAIN);
    }


    # ---- 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(
                "EditMessageTemplates" => "Edit Email Templates",
                );
    }

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

    /**
    * Retrieve list of currently available templates, with template IDs for
    * the indexes and template names for the values.
    * @return Array containing template names.
    */
    public function GetTemplateList()
    {
        $Templates = $this->ConfigSetting("Templates");
        $TemplateList = array();
        if(count($Templates) > 0)
        {
            foreach ($Templates as $Id => $Template)
            {
                $TemplateList[$Id] = $Template["Name"];
            }
        }
        return $TemplateList;
    }

    /**
    * Add or update a template according to passed parameters.
    * the indexes and template names for the values.
    * @param String $Name The name of the template.
    * @param String $From The from parameter for the template
    * @param String $Subject The subject of the email to send.
    * @param String $Body The body of the email to send.
    * @param String $ItemBody The body of each individual iterable to send.
    * @param String $PlainTextBody Non-HTML version of the body
    * @param String $PlainTextItemBody Non-HTML version of the item body
    * @param String $Headers Any additional headers to send.
    * @param Boolean $CollapseBodyMargins Whether or not the margins should be collapsed.
    * @return Integer of template id.
    */
    public function AddTemplate($Name, $From, $Subject,
                          $Body, $ItemBody, $PlainTextBody,
                          $PlainTextItemBody,
                          $Headers, $CollapseBodyMargins = FALSE
                         )
    {
        $G_Templates = $this->ConfigSetting("Templates");
        # get next template ID
        $TemplateId = (($G_Templates === NULL) || !count($G_Templates)) ? 0
                : (max(array_keys($G_Templates)) + 1);
        $G_Templates[$TemplateId] = array();
        $G_Templates[$TemplateId]["Name"] = $Name;
        $G_Templates[$TemplateId]["From"] = $From;
        $G_Templates[$TemplateId]["Subject"] = $Subject;
        $G_Templates[$TemplateId]["Body"] = $Body;
        $G_Templates[$TemplateId]["ItemBody"] = $ItemBody;
        $G_Templates[$TemplateId]["PlainTextBody"] = $PlainTextBody;
        $G_Templates[$TemplateId]["PlainTextItemBody"] = $PlainTextItemBody;
        $G_Templates[$TemplateId]["Headers"] = $Headers;
        $G_Templates[$TemplateId]["CollapseBodyMargins"] = $CollapseBodyMargins;

        $this->ConfigSetting("Templates", $G_Templates);
        return $TemplateId;
    }

    /**
    * Send email to specified recipients using specified template.
    * @param int $TemplateId ID of template to use in generating emails.
    * @param array $Recipients User object or ID or email address, or an array
    *       of any of these to use as email recipients.
    * @param array $Resources Resource or Resource ID or array of Resources or
    *       array of Resource IDs to be referred to within email messages.
    *       (OPTIONAL, defaults to NULL)
    * @param mixed $ExtraValues Array of additional values to swap into template,
    *       with value keywords (without the "X-" and "-X") for the index
    *       and values for the values.  This parameter can be used to
    *       override the builtin keywords.  (OPTIONAL)
    * @return Number of email messages sent.
    */
    public function SendEmail(
            $TemplateId, $Recipients, $Resources = NULL, $ExtraValues = NULL)
    {
        # initialize count of emails sent
        $MessagesSent = 0;

        # convert incoming parameters to arrays if necessary
        if (!is_array($Recipients)) {  $Recipients = array($Recipients);  }
        if ($Resources && !is_array($Resources))
                {  $Resources = array($Resources);  }

        # load resource objects if necessary
        if (count($Resources) && !is_object(reset($Resources)))
        {
            foreach ($Resources as $Id)
            {
                $NewResources[$Id] = new Resource($Id);
            }
            $Resources = $NewResources;
        }

        # retrieve appropriate template
        $Templates = $this->ConfigSetting("Templates");
        $Template = $Templates[$TemplateId];

        # set up parameters for keyword replacement callback
        $this->KRItemBody = $Template["ItemBody"];
        $this->KRResources = $Resources;
        $this->KRExtraValues = $ExtraValues;

        # for each recipient
        foreach ($Recipients as $Recipient)
        {
            # convert recipient to User if User ID supplied
            if (is_numeric($Recipient))
            {
                $Recipient = new CWUser($Recipient);
                if ($Recipient->Status() != U_OKAY) {  continue;  }
                if ($Recipient->HasPriv(PRIV_USERDISABLED)) { continue; }
            }

            # if recipient is User
            if (is_object($Recipient))
            {
                # retrieve destination address from user
                $Address = trim($Recipient->Get("EMail"));

                # set up per-user parameters for keyword replacement callback
                $this->KRUser = $Recipient;
            }
            else
            {
                # assume recipient is just destination address
                $Address = $Recipient;

                # clear per-user parameters for keyword replacement callback
                unset($this->KRUser);
            }

            # get the message subject
            $Subject = trim($this->ReplaceKeywords($Template["Subject"]));

            # skip if there is no destination address or no subject
            if (!strlen($Address) || !strlen($Subject))
            {
                continue;
            }

            # create and set up the message to send
            $Msg = new Email();
            $Msg->CharSet($GLOBALS["AF"]->HtmlCharset());
            $Msg->From($this->ReplaceKeywords($Template["From"]));
            $Msg->To($Address);
            $Msg->Subject($Subject);

            # set up headers for message
            $Headers = array();
            $Headers[] = "Auto-Submitted: auto-generated";
            $Headers[] = "Precedence: list";
            $Headers[] = "X-SiteUrl: ". OurBaseUrl();
            if (strlen($Template["Headers"]))
            {
                $ExtraHeaders = $this->SplitByLineEnding($Template["Headers"]);
                foreach ($ExtraHeaders as $Line)
                {
                    if (strlen(trim($Line)))
                    {
                        $Headers[] = $this->ReplaceKeywords($Line);
                    }
                }
            }

            # add the headers to the message
            $Msg->AddHeaders($Headers);

            # check if we have an html body
            if (strlen(trim(strip_tags($Template["Body"])))==0)
            {
                # if there was no html body, then construct one based
                # on the plain text body, replacing keywords as we go
                $Body = "<pre>"
                      .$this->ReplaceKeywordsInBody(
                          $Template["PlainTextBody"], FALSE)
                      ."</pre>";
            }
            else
            {
                # otherwise, just do keyword replacement on the HTML body
                $Body = $this->ReplaceKeywordsInBody($Template["Body"]);
            }

            # wrap HTML where necessary to keep it below 998 characters
            $Body = Email::WrapHtmlAsNecessary($Body);

            # construct the style attribute necessary for collapsing the body
            # margins, if instructed to do so
            $MarginsStyle = $Template["CollapseBodyMargins"]
                ? ' style="margin:0;padding:0;"' : "";

            # wrap the message in boilerplate HTML
            $Body = '<!DOCTYPE html>
                     <html lang="en"'.$MarginsStyle.'>
                       <head><meta charset="'.$GLOBALS["AF"]->HtmlCharset().'" /></head>
                       <body'.$MarginsStyle.'>'.$Body.'</body>
                     </html>';

            # add the body to the message
            $Msg->Body($Body);

            # plain text body and item body are set
            if (strlen(trim($Template["PlainTextBody"])) > 0)
            {
                $this->KRItemBody = $Template["PlainTextItemBody"];

                # start with the body from the template and replace keywords
                $PlainTextBody = $this->ReplaceKeywordsInBody(
                    $Template["PlainTextBody"],
                    FALSE);

                # wrap body where necessary to keep it below 998 characters
                $PlainTextBody = wordwrap($PlainTextBody, 998, TRUE);

                # add the alternate body to the message
                $Msg->AlternateBody($PlainTextBody);
            }

            # an HTML e-mail only
            else
            {
                # additional headers are needed
                $Msg->AddHeaders(array(
                    "MIME-Version: 1.0",
                    "Content-Type: text/html; charset=".$GLOBALS["AF"]->HtmlCharset()
                ));
            }

            # send message to the user
            $Msg->Send();
            $MessagesSent++;
        }

        # report number of emails sent back to caller
        return $MessagesSent;
    }

    /**
    * Send email to specified recipients using specified template but use
    * background tasks instead of trying to send all to all of the
    * recipients at once.
    * @param int $TemplateId ID of template to use in generating emails.
    * @param mixed $Recipients User object or ID or email address, or an array
    *       of any of these to use as email recipients.
    * @param mixed $Resources Resource or Resource ID or array of Resources or
    *       array of Resource IDs to be referred to within email messages.
    *       (OPTIONAL, defaults to NULL)
    * @param array $ExtraValues Array of additional values to swap into template,
    *       with value keywords (without the "X-" and "-X") for the index
    *       and values for the values.  This parameter can be used to
    *       override the builtin keywords.  (OPTIONAL)
    * @return Number of email messages to be sent.
    */
    public function SendEmailUsingTasks(
        $TemplateId,
        $Recipients,
        $Resources = NULL,
        $ExtraValues = NULL)
    {
        # retrieve appropriate template and its name
        $Templates = $this->ConfigSetting("Templates");

        # don't send e-mail if the template is invalid
        if (!isset($Templates[$TemplateId]))
        {
            return 0;
        }

        # convert incoming parameters to arrays if necessary
        if (!is_array($Recipients)) {  $Recipients = array($Recipients);  }
        if ($Resources && !is_array($Resources))
                {  $Resources = array($Resources);  }

        # load resource IDs if necessary because they're better for serializing
        # for the unique task
        if (count($Resources) && is_object(reset($Resources)))
        {
            foreach ($Resources as $Resource)
            {
                $NewResources[] = intval($Resource->Id());
            }

            $Resources = $NewResources;
        }

        # get the name of the template for the task description
        $TemplateName = $Templates[$TemplateId]["Name"];

        # variable to track how many e-mails are to be sent
        $NumToBeSent = 0;

        # for each user
        foreach ($Recipients as $Recipient)
        {
            # skip invalid users
            if (is_numeric($Recipient))
            {
                $Recipient = new CWUser($Recipient);
            }
            if (is_object($Recipient))
            {
                if ($Recipient->Status() != U_OKAY) {  continue;  }
            }

            # build task description
            $RecipientEmail = is_object($Recipient)
                    ? $Recipient->Get("EMail") : $Recipient;
            $TaskDescription = "Send e-mail to \""
                    .$RecipientEmail."\" using the \""
                    .$TemplateName."\" template.";

            # use user IDs rather than user objects because they are
            #       smaller to serialize when queueing tasks
            if (is_object($Recipient))
            {
                $Recipient = $Recipient->Id();
            }

            # queue the unique task
            $GLOBALS["AF"]->QueueUniqueTask(
                array($this, "SendEmail"),
                array($TemplateId, $Recipient, $Resources, $ExtraValues),
                $this->ConfigSetting("EmailTaskPriority"),
                $TaskDescription);

            # increment the number of e-mail messages to be sent
            $NumToBeSent += 1;
        }

        # return the number to be sent
        return $NumToBeSent;
    }

    /**
    * Get a list of template users.
    * @param int $TemplateId Template to check.
    * @return array of strings identifying template users.
    */
    public function FindTemplateUsers($TemplateId)
    {
        $Result = $GLOBALS["AF"]->SignalEvent(
            "Mailer_EVENT_IS_TEMPLATE_IN_USE", array(
                "TemplateId" => $TemplateId,
                "TemplateUsers" => array()) );

        return $Result["TemplateUsers"];
    }

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

    # values for use by KeywordReplacmentCallback()
    private $KRUser;
    private $KRItemBody;
    private $KRResources;
    private $KRExtraValues;
    private $KRIsHtml;

    /**
    * Replace keywords in the given body text.
    * @param string $Body Body text.
    * @param bool $IsHtml Set to TRUE to escape necessary characters in keywords.
    * @return Returns the body text with keywords replaced.
    */
    protected function ReplaceKeywordsInBody($Body, $IsHtml=TRUE)
    {
        $NewBody = "";

        # flag whether the output is HTML
        $this->KRIsHtml = $IsHtml;

        # for each line of template
        foreach ($this->SplitByLineEnding($Body) as $Line)
        {
            # replace any keywords in line and add line to message body
            # along with a newline to avoid line truncation by mail transfer
            # agents
            $NewBody .= $this->ReplaceKeywords($Line) . "\n";
        }

        return $NewBody;
    }

    /**
    * Split a string by its line endings, e.g., CR, LF, or CRLF.
    * @param string $Value String to split.
    * @return Returns the string split by its line endings.
    */
    protected function SplitByLineEnding($Value)
    {
        return preg_split('/\r\n|\r|\n/', $Value);
    }

    /**
    * Process keyword replacements.
    * @param string $Line Text to process.
    * @return string modified text.
    */
    private function ReplaceKeywords($Line)
    {
        return preg_replace_callback("/X-([A-Z0-9:]+)-X/",
                array($this, "KeywordReplacementCallback"), $Line);
    }

    /**
    * Callback to handle keyword repacements within a message.
    * @param array $Matches Array of matching keywords.
    * @return string modified text.
    */
    private function KeywordReplacementCallback($Matches)
    {
        static $CurrentResource;
        static $FieldNameMappings;
        static $InResourceList = FALSE;
        static $ResourceNumber;

        # if extra value was supplied with keyword that matches the match string
        if (count($this->KRExtraValues)
                && isset($this->KRExtraValues[$Matches[1]]))
        {
            # return extra value to caller
            return $this->KRExtraValues[$Matches[1]];
        }

        # if current resource is not yet set then set default value if available
        if (!isset($CurrentResource) && count($this->KRResources))
        {
            $CurrentResource = reset($this->KRResources);
        }

        # start out with assumption that no replacement text will be found
        $Replacement = $Matches[0];

        # switch on match string
        switch ($Matches[1])
        {
            case "PORTALNAME":
                $Replacement = $GLOBALS["G_SysConfig"]->PortalName();
                break;

            case "BASEURL":
                $Replacement = strlen(trim($this->ConfigSetting("BaseUrl")))
                        ? trim($this->ConfigSetting("BaseUrl"))
                        : OurBaseUrl()."index.php";
                break;

            case "ADMINEMAIL":
                $Replacement = $GLOBALS["G_SysConfig"]->AdminEmail();
                break;

            case "LEGALNOTICE":
                $Replacement = $GLOBALS["G_SysConfig"]->LegalNotice();
                break;

            case "USERLOGIN":
            case "USERNAME":
                if (isset($this->KRUser))
                {
                    $Value = $this->KRUser->Get("UserName");
                    $Replacement = $this->KRIsHtml ? StripXSSThreats($Value) : $Value;
                }
                break;

            case "USERREALNAME":
                if (isset($this->KRUser))
                {
                    $Value = $this->KRUser->Get("RealName");

                    # if the user hasn't specified a full name
                    if (!strlen(trim($Value)))
                    {
                        $Value = $this->KRUser->Get("UserName");
                    }

                    $Replacement = $this->KRIsHtml ? StripXSSThreats($Value) : $Value;
                }
                break;

            case "USEREMAIL":
                if (isset($this->KRUser))
                {
                    $Value = $this->KRUser->Get("EMail");
                    $Replacement = $this->KRIsHtml ? StripXSSThreats($Value) : $Value;
                }
                break;

            case "RESOURCELIST":
                $Replacement = "";
                if ($InResourceList == FALSE)
                {
                    $InResourceList = TRUE;
                    $ResourceNumber = 1;
                    foreach ($this->KRResources as $Resource)
                    {
                        $CurrentResource = $Resource;
                        $TemplateLines = $this->SplitByLineEnding($this->KRItemBody);
                        foreach ($TemplateLines as $Line)
                        {
                            $Replacement .= preg_replace_callback(
                                    "/X-([A-Z0-9:]+)-X/",
                                    array($this, "KeywordReplacementCallback"),
                                    $Line) . "\n";
                        }
                        $ResourceNumber++;
                    }
                    $InResourceList = FALSE;
                }
                break;

            case "RESOURCENUMBER":
                $Replacement = $ResourceNumber;
                break;

            case "RESOURCECOUNT":
                $Replacement = count($this->KRResources);
                break;

            case "RESOURCEID":
                $Replacement = $CurrentResource->Id();
                break;

            case "RESOURCEVIEWURL":
                static $Schemas = array();
                static $BaseUrl;

                # retrieve view page for schema used for resource
                $SchemaId = $CurrentResource->SchemaId();
                if (!array_key_exists($SchemaId, $Schemas))
                {
                    $Schemas[$SchemaId] = new MetadataSchema($SchemaId);
                }
                $ViewPage = $Schemas[$SchemaId]->ViewPage();

                # make sure view page will work for clean URL substitution
                if (strpos($ViewPage, "index.php") !== 0)
                {
                    $ViewPage = "index.php".$ViewPage;
                }

                # insert resource ID into view page
                $ViewPage = preg_replace("%\\\$ID%",
                        $CurrentResource->Id(), $ViewPage);

                # get any clean URL for view page
                $ViewPage = $GLOBALS["AF"]->GetCleanUrlForPath($ViewPage);

                # add base URL to view page
                if (!isset($BaseUrl))
                {
                    $BaseUrl = strlen(trim($this->ConfigSetting("BaseUrl")))
                            ? trim($this->ConfigSetting("BaseUrl"))
                            : OurBaseUrl();
                }
                $Replacement = $BaseUrl.$ViewPage;
                break;

            default:
                # map to date/time values if appropriate
                $DateFormats = array(
                        "DATE"            => "M j Y",
                        "TIME"            => "g:ia T",
                        "YEAR"            => "Y",
                        "YEARABBREV"      => "y",
                        "MONTH"           => "n",
                        "MONTHNAME"       => "F",
                        "MONTHABBREV"     => "M",
                        "MONTHZERO"       => "m",
                        "DAY"             => "j",
                        "DAYZERO"         => "d",
                        "DAYWITHSUFFIX"   => "jS",
                        "WEEKDAYNAME"     => "l",
                        "WEEKDAYABBREV"   => "D",
                        "HOUR"            => "g",
                        "HOURZERO"        => "h",
                        "MINUTE"          => "i",
                        "TIMEZONE"        => "T",
                        "AMPMLOWER"       => "a",
                        "AMPMUPPER"       => "A",
                        );

                # map the share format values if appropriate
                if ($GLOBALS["G_PluginManager"]->PluginEnabled("SocialMedia"))
                {
                    $ShareFormats = array(
                        "FACEBOOK", "TWITTER", "LINKEDIN", "GOOGLEPLUS");
                }
                else
                {
                    $ShareFormats = array();
                }

                if (isset($DateFormats[$Matches[1]]))
                {
                    $Replacement = date($DateFormats[$Matches[1]]);
                }
                else if (isset($CurrentResource))
                {
                    # get the schema ID for the resource
                    $SchemaId = $CurrentResource->SchemaId();

                    # load field name mappings (if not already loaded)
                    if (!isset($FieldNameMappings[$SchemaId]))
                    {
                        $Schema = new MetadataSchema($SchemaId);
                        foreach ($Schema->GetFields() as $Field)
                        {
                            $NormalizedName = strtoupper(
                                preg_replace("/[^A-Za-z0-9]/",
                                             "", $Field->Name()));
                            $FieldNameMappings[$SchemaId][$NormalizedName]
                                = $Field;

                            if ($Field->Type() == MetadataSchema::MDFTYPE_USER)
                            {
                                $Key = $NormalizedName.":SMARTNAME";
                                $FieldNameMappings[$SchemaId][$Key]
                                    = $Field;
                            }
                        }
                    }

                    # if keyword refers to known field
                    $KeywordIsField = preg_match(
                        "/FIELD:([A-Z0-9:]+)/", $Matches[1], $SubMatches);
                    $KeywordIsShare = preg_match(
                        "/SHARE:([A-Z]+)/", $Matches[1], $ShareSubMatches);

                    # if there's a match for a field keyword
                    if ($KeywordIsField
                        && isset($FieldNameMappings[$SchemaId][$SubMatches[1]]))
                    {
                        # replacement is value from current resource
                        $Field = $FieldNameMappings[$SchemaId][$SubMatches[1]];

                        # if this is a User field and the SMARTNAME was requested
                        if ($Field->Type() == MetadataSchema::MDFTYPE_USER &&
                            preg_match("/:SMARTNAME$/", $SubMatches[1]))
                        {
                            # iterate over the users in this field
                            $Users = $CurrentResource->Get($Field, TRUE);
                            $Replacement = array();
                            foreach ($Users as $User)
                            {
                                # if they have a real name set, use that
                                # otherwise, fall back to UserName
                                $RealName = trim($User->Get("RealName"));
                                $Replacement[]= (strlen($RealName)) ? $RealName :
                                         $User->Get("UserName") ;
                            }
                        }
                        else
                        {
                            # otherwise just get the value
                            $Replacement = $CurrentResource->Get($Field);
                        }

                        # combine array values with commas
                        if (is_array($Replacement))
                        {
                            $Replacement = implode(", ", $Replacement);
                        }

                        if (!$Field->AllowHTML())
                        {
                            $Replacement = $this->KRIsHtml
                                ? htmlspecialchars($Replacement) : $Replacement;
                            $Replacement = wordwrap($Replacement, 78);
                        }

                        # HTML is allowed but there isn't any HTML in the value,
                        # so wrapping is okay
                        else if (strpos($Replacement, ">") === FALSE
                                 && strpos($Replacement, "<") === FALSE)
                        {
                            $Replacement = wordwrap($Replacement, 78);
                        }

                        # HTML is allowed and in the value but it shouldn't be
                        # used in the plain text version
                        else if (!$this->KRIsHtml)
                        {
                            # try as hard as possible to convert the HTML to plain text
                            $Replacement = Email::ConvertHtmlToPlainText($Replacement);

                            # wrap the body
                            $Replacement = wordwrap($Replacement, 78);
                        }

                        $Replacement = $this->KRIsHtml
                            ? StripXSSThreats($Replacement) : $Replacement;
                    }

                    # if there's a match for a share keyword
                    else if ($KeywordIsShare
                             && in_array($ShareSubMatches[1], $ShareFormats))
                    {
                        $Replacement = $this->GetShareUrl(
                            $CurrentResource,
                            $ShareSubMatches[1]);
                    }
                }
                break;
        }

        # return replacement string to caller
        return $Replacement;
    }

    /**
    * Get the share URL for the given resource to the given site, where site is
    * one of "Facebook", "Twitter", "LinkedIn", or "GooglePlus".
    * @param Resource $Resource The resource for which to get the share URL.
    * @param string $Site The site for which to get the share URL.
    * @return Returns the share URL for the resource and site or NULL if the
    *      SocialMedia plugin isn't available.
    */
    private function GetShareUrl(Resource $Resource, $Site)
    {
        # the social media plugin needs to be available
        if (!$GLOBALS["G_PluginManager"]->PluginEnabled("SocialMedia"))
        {
            return NULL;
        }

        # return the share URL from the social media plugin
        $Plugin = $GLOBALS["G_PluginManager"]->GetPlugin("SocialMedia");
        return $Plugin->GetShareUrl($Resource, $Site);
    }
}
