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

/**
* Plugin that offers additional integration with social media sites, like
* Facebook and Twitter. Adds HTML markup to a resource's view page so that
* social media websites can extract relevant metadata more easily when somebody
* shares a resource.
*/
class SocialMedia extends Plugin
{

    /**
    * Base URL used for sharing a URL to Facebook.
    */
    const BASE_FACEBOOK_SHARE_URL = "http://www.facebook.com/sharer.php";

    /**
    * Base URL used for sharing a URL to Twitter.
    */
    const BASE_TWITTER_SHARE_URL = "https://twitter.com/intent/tweet";

    /**
    * Base URL used for sharing a URL to LinkedIn.
    */
    const BASE_LINKEDIN_SHARE_URL = "http://www.linkedin.com/shareArticle";

    /**
    * Base URL used for sharing a URL to Google Plus.
    */
    const BASE_GOOGLEPLUS_SHARE_URL = "https://plus.google.com/share";

    /**
    * Value used to signify that e-mail should be used.
    */
    const SITE_EMAIL = "em";

    /**
    * Value used to signify that Facebook should be used.
    */
    const SITE_FACEBOOK = "fb";

    /**
    * Value used to signify that Twitter should be used.
    */
    const SITE_TWITTER = "tw";

    /**
    * Value used to signify that LinkedIn should be used.
    */
    const SITE_LINKEDIN = "li";

    /**
    * Value used to signify that Google Plus should be used.
    */
    const SITE_GOOGLEPLUS = "gp";

    /**
    * Register information about this plugin.
    */
    public function Register()
    {
        $this->Name = "Social Media";
        $this->Version = "1.1.5";
        $this->Description = "Plugin that offers additional integration with
            social media websites, like Facebook and Twitter, adding HTML markup
            to a resource's view page so that relevant metadata can be extracted
            more easily when somebody shares it.";
        $this->Author = "Internet Scout";
        $this->Url = "http://scout.wisc.edu/cwis/";
        $this->Email = "scout@scout.wisc.edu";
        $this->Requires = array(
            "CWISCore" => "2.4.1",
            "MetricsRecorder" => "1.2.4");
        $this->EnabledByDefault = TRUE;

        $this->Instructions = <<<EOT
            <p><strong>Note</strong>: The user interface in use must signal the
            <code>EVENT_IN_HTML_HEADER</code> event in its template file for
            this plugin to add additional metadata to a resource's view page.
            The user interfaces that come with CWIS do so.</p>
            <p>You must <a href="https://dev.twitter.com/docs/cards">request
            approval</a> from Twitter for your site&#39;s information to display
            in tweets.</p>
EOT;

        $this->CfgSetup["GeneralSection"] = array(
            "Type" => "Heading",
            "Label" => "General");

        $this->CfgSetup["SiteName"] = array(
            "Type" => "Text",
            "Label" => "Site Name",
            "Default" => $GLOBALS["SysConfig"]->PortalName(),
            "Help" => "The name of this website. Used in the additional metadata
                added to a resource's view page.");

        $this->CfgSetup["MaxDescriptionLength"] = array(
            "Type" => "Number",
            "Label" => "Maximum Description Length",
            "Default" => 1200,
            "Help" => "The maximum length of the description in number of
                characters. Used in the additional metadata added to a
                resource's view page.",
            "Size" => 6);

        $this->CfgSetup["TwitterSection"] = array(
            "Type" => "Heading",
            "Label" => "Twitter");

        $this->CfgSetup["TwitterUsername"] = array(
            "Type" => "Text",
            "Label" => "Twitter User Name",
            "Help" => "The Twitter user name associated with this website, e.g.,
                <i>@example</i>. Used in the additional metadata added to a
                resource's view page.");

        # add options for each schema
        foreach (MetadataSchema::GetAllSchemas() as $Schema)
        {
            $Id = $Schema->Id();
            $Name = $Schema->Name();

            # skip the user schema
            if ($Id == MetadataSchema::SCHEMAID_USER)
            {
                continue;
            }

            $this->CfgSetup["Schema/".$Id] = array(
                "Type" => "Heading",
                "Label" => $Name." Resources");

            $this->CfgSetup["Enabled/".$Id] = array(
                "Type" => "Flag",
                "Label" => "Enabled",
                "Default" => FALSE,
                "Help" => "Whether or not to add social media metadata for
                    resources using this metadata schema.",
                "OnLabel" => "Yes",
                "OffLabel" => "No");

            $this->CfgSetup["TitleField/".$Id] = array(
                "Type" => "MetadataField",
                "Label" => "Resource Title Field",
                "Help" => "The field to use as the title in the additional metadata
                    added to a resource's view page.",
                "FieldTypes" => MetadataSchema::MDFTYPE_TEXT,
                "SchemaId" => $Id);

            $this->CfgSetup["DescriptionField/".$Id] = array(
                "Type" => "MetadataField",
                "Label" => "Resource Description Field",
                "Help" => "The field to use as the description in the additional
                    metadata added to a resource's view page.",
                "FieldTypes" => MetadataSchema::MDFTYPE_PARAGRAPH,
                "SchemaId" => $Id);

            $this->CfgSetup["ScreenshotField/".$Id] = array(
                "Type" => "MetadataField",
                "Label" => "Resource Screenshot/Poster Field",
                "Help" => "The field to use as the screenshot/poster image in
                    the additional metadata added to a resource's view page.",
                "FieldTypes" => MetadataSchema::MDFTYPE_IMAGE,
                "SchemaId" => $Id);
        }
    }

    /**
    * Startup initialization for plugin.
    * @return NULL if initialization was successful, otherwise a string
    *       containing an error message indicating why initialization failed.
    */
    public function Initialize()
    {
        # sharing URL format friendly to robots.txt

        # clean URL for the share redirect with the user ID
        $GLOBALS["AF"]->AddCleanUrl(
            "%^sh/([0-9]+)/(fb|tw|li|gp)/([0-9]+)%",
            "P_SocialMedia_ShareResource",
            array("ResourceId" => "$1", "Site" => "$2", "UserId" => "$3"),
            "sh/\$ResourceId/\$Site");

        # clean URL for the share redirect without the user ID. this is less
        # specific so it should come after the one above
        $GLOBALS["AF"]->AddCleanUrl(
            "%^sh/([0-9]+)/(fb|tw|li|gp)%",
            "P_SocialMedia_ShareResource",
            array("ResourceId" => "$1", "Site" => "$2"),
            "sh/\$ResourceId/\$Site");

        # backward compatibility with older sharing URL format

        # clean URL for the share redirect with the user ID
        $GLOBALS["AF"]->AddCleanUrl(
            "%^sh([0-9]+)/(fb|tw|li|gp)/([0-9]+)%",
            "P_SocialMedia_ShareResource",
            array("ResourceId" => "$1", "Site" => "$2", "UserId" => "$3"),
            "sh\$ResourceId/\$Site");

        # clean URL for the share redirect without the user ID. this is less
        # specific so it should come after the one above
        $GLOBALS["AF"]->AddCleanUrl(
            "%^sh([0-9]+)/(fb|tw|li|gp)%",
            "P_SocialMedia_ShareResource",
            array("ResourceId" => "$1", "Site" => "$2"),
            "sh\$ResourceId/\$Site");

        # register our events with metrics recorder
        $GLOBALS["G_PluginManager"]->GetPlugin("MetricsRecorder")->RegisterEventType(
                "SocialMedia", "ShareResource");

        # report success
        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.1.0 to 1.1.0
        if (version_compare($PreviousVersion, "1.1.0", "<"))
        {
            $SchemaId = MetadataSchema::SCHEMAID_DEFAULT;

            # the default schema was always enabled in prior versions
            $this->ConfigSetting("Enabled/".$SchemaId, TRUE);

            # migrate old field settings
            $this->ConfigSetting(
                "TitleField/".$SchemaId,
                $this->ConfigSetting("TitleField"));
            $this->ConfigSetting(
                "DescriptionField/".$SchemaId,
                $this->ConfigSetting("DescriptionField"));
            $this->ConfigSetting(
                "ScreenshotField/".$SchemaId,
                $this->ConfigSetting("ScreenshotField"));
        }

        # upgrade from versions < 1.1.1 to 1.1.1
        if (version_compare($PreviousVersion, "1.1.1", "<"))
        {
            # set the maximum description length to 1200 by default
            $this->ConfigSetting("MaxDescriptionLength", 1200);
        }

        return NULL;
    }

    /**
    * Declare the events this plugin provides to the application framework.
    * @return An array of the events this plugin provides.
    */
    public function DeclareEvents()
    {
        return array(
            "SocialMedia_MODIFY_IMAGES"
              => ApplicationFramework::EVENTTYPE_CHAIN);
    }

    /**
    * Hook event callbacks into the application framework.
    * @return Returns an array of events to be hooked into the application
    *      framework.
    */
    public function HookEvents()
    {
        return array("EVENT_IN_HTML_HEADER" => "PrintMetaTags");
    }

    /**
    * Print the meta tags in the header. Additional information can be found at
    * the following URLs:
    * @li http://developers.facebook.com/docs/opengraph/property-types/
    * @li http://developers.facebook.com/docs/opengraph/creating-object-types/
    * @li https://dev.twitter.com/docs/cards
    */
    public function PrintMetaTags()
    {
        # variables used to determine if the current page is a view page
        $Path = $GLOBALS["AF"]->GetUncleanUrl();
        $ResourceId = NULL;

        # check if on a view page for one of the schemas
        foreach (MetadataSchema::GetAllSchemas() as $Schema)
        {
            # skip the user metadata schema
            if ($Schema->Id() == MetadataSchema::SCHEMAID_USER)
            {
                continue;
            }

            # if on the view page for the schema
            if ($Schema->PathMatchesViewPage($Path))
            {
                $IdParameter = $Schema->GetViewPageIdParameter();
                $ResourceId = GetFormValue($IdParameter);
                break;
            }
        }

        # if not on a view page (i.e. resource ID is not valid)
        if (!Resource::ItemExists($ResourceId))
        {
            return;
        }

        $Resource = new Resource($ResourceId);
        $Schema = $Resource->Schema();

        # only add metadata for enabled schemas
        if (!$this->IsEnabledForSchema($Schema))
        {
            return;
        }

        # extract the metadata from the resource
        $SiteName = $this->ConfigSetting("SiteName");
        $Url = $this->GetViewPageUrl($Resource);
        $Title = $this->GetSimpleFieldValue($Resource, "TitleField");
        $Description = $this->GetSimpleFieldValue($Resource, "DescriptionField");
        $Images = $this->GetImagesForResource($Resource);
        $TwitterUsername = $this->FormatTwitterUsername(
            $this->ConfigSetting("TwitterUsername"));

        # limit the description length
        $MaxDescriptionLength = $this->ConfigSetting("MaxDescriptionLength");
        $Description = NeatlyTruncateString($Description, $MaxDescriptionLength);

        # add marker indicating the beginning of our additions
        print "\n<!-- Social Media (start) -->\n";

        # print meta tags for Open Graph (Facebook, but other sites also use it)
        $this->PrintOpenGraphTag("og:site_name", $SiteName);
        $this->PrintOpenGraphTag("og:url", $Url);
        $this->PrintOpenGraphTag("og:title", $Title);
        $this->PrintOpenGraphTag("og:description", $Description);
        $this->PrintOpenGraphImages($Images);

        # Twitter needs shorter text
        $MaxTwitterDescLength = min(200, $MaxDescriptionLength);
        $TwitterTitle = NeatlyTruncateString($Title, 70);
        $TwitterDescription = NeatlyTruncateString($Description, $MaxTwitterDescLength);

        # print meta tags for Twitter
        $this->PrintTwitterTag("twitter:card", "summary");
        $this->PrintTwitterTag("twitter:site:id", $TwitterUsername);
        $this->PrintTwitterTag("twitter:title", $TwitterTitle);
        $this->PrintTwitterTag("twitter:description", $TwitterDescription);
        $this->PrintTwitterImages($Images);

        # add marker indicating the beginning of our additions
        print "<!-- Social Media (end) -->\n";
    }

    /**
    * Share a resource to a social media website. This will cause a redirect.
    * @param Resource $Resource Resource to construct a sharing URL for.
    * @param string $Site Website to share to.
    * @param int $UserId Optional user ID to associate with the share action.
    * @see SITE_EMAIL
    * @see SITE_FACEBOOK
    * @see SITE_TWITTER
    * @see SITE_LINKEDIN
    * @see SITE_GOOGLEPLUS
    */
    public function ShareResource($Resource, $Site, $UserId)
    {
        # go to the home page if the user can't view this resource
        if (!$Resource->UserCanView($GLOBALS["G_User"]))
        {
            $GLOBALS["AF"]->SetJumpToPage("Home");
            return;
        }

        # if BotDetector is available, use it to bounce robots back to the home page
        if ($GLOBALS["G_PluginManager"]->PluginEnabled("BotDetector") &&
            $GLOBALS["G_PluginManager"]->GetPlugin("BotDetector")->CheckForSpamBot())
        {
            $GLOBALS["AF"]->SetJumpToPage("Home");
            return;
        }

        # e-mail sharing should be handled separately
        if ($Site == self::SITE_EMAIL)
        {
            # record an event
            $GLOBALS["G_PluginManager"]->GetPlugin("MetricsRecorder")->RecordEvent(
                "SocialMedia",
                "ShareResource",
                $Resource->Id(),
                $Site,
                $UserId);

            # suppress HTML output because an AJAX request would have been used
            $GLOBALS["AF"]->SuppressHTMLOutput();

            return;
        }

        $SharingUrl = $this->GetSharingUrl($Resource, $Site);

        # redirect to the sharing URL if it could be retrieved
        if (!is_null($SharingUrl))
        {
            # record an event
            $GLOBALS["G_PluginManager"]->GetPlugin("MetricsRecorder")->RecordEvent(
                "SocialMedia",
                "ShareResource",
                $Resource->Id(),
                $Site,
                $UserId);

            $GLOBALS["AF"]->SetJumpToPage($SharingUrl);
        }

        # go to the home page otherwise
        else
        {
            $GLOBALS["AF"]->SetJumpToPage("Home");
        }
    }

    /**
    * 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.
    */
    public function GetShareUrl(Resource $Resource, $Site)
    {
        # map share sites to the URL tokens
        $SiteTokens = array(
            "FACEBOOK" => "fb",
            "TWITTER" => "tw",
            "LINKEDIN" => "li",
            "GOOGLEPLUS" => "gp");

        # get the base URL for sharing links
        $BaseShareUrl = "index.php?P=P_SocialMedia_ShareResource";
        $BaseShareUrl .= "&ResourceId=" . urlencode($Resource->Id());

        # get the share URL for the resource
        $ShareToken = GetArrayValue($SiteTokens, strtoupper($Site));
        $ShareUrl = $BaseShareUrl . "&Site=" . $ShareToken;

        # try to make the URL clean
        $ShareUrl = $GLOBALS["AF"]->GetCleanUrlForPath($ShareUrl);

        # make the URL absolute
        $ShareUrl = OurBaseUrl() . $ShareUrl;

        return $ShareUrl;
    }

    /**
    * Determine if meta tag printing is enabled for a metadata schema.
    * @param MetadataSchema $Schema Schema to test with.
    * @return Returns TRUE if meta tag printing is enabled for the schema.
    */
    protected function IsEnabledForSchema(MetadataSchema $Schema)
    {
        return $this->ConfigSetting("Enabled/".$Schema->Id());
    }

    /**
    * Get the URL to the resource's view page. This will use the clean URL if
    * .htaccess support is available.
    * @param Resource $Resource Resource for which to get the view page URL.
    * @return Returns the view page URL for the resource.
    */
    protected function GetViewPageUrl(Resource $Resource)
    {
        $Schema = new MetadataSchema($Resource->SchemaId());
        $SafeResourceId = urlencode($Resource->Id());

        # get the view page
        $ViewPageUrl = $Schema->ViewPage();

        # replace the ID parameter with the actual resource ID
        $ViewPageUrl = preg_replace("%\\\$ID%", $SafeResourceId, $ViewPageUrl);

        # make the URL clean, if possible
        if ($GLOBALS["AF"]->HtaccessSupport())
        {
            $ViewPageUrl = $GLOBALS["AF"]->GetCleanUrlForPath($ViewPageUrl);
        }

        # tack on the rest of the URL to make it absolute
        $ViewPageUrl = $GLOBALS["AF"]->BaseUrl() . $ViewPageUrl;

        # and, finally, return the URL
        return $ViewPageUrl;
    }

    /**
    * Get the value of a field that has one value that is text.
    * @param Resource $Resource Resource from which to get the image.
    * @param string $Setting Plugin setting from which to get the field.
    * @return Returns the field value or NULL if there isn't one.
    * @see GetImageFieldValue()
    */
    protected function GetSimpleFieldValue(Resource $Resource, $Setting)
    {
        # make sure the function to get the value is loaded
        $GLOBALS["AF"]->LoadFunction("GetResourceFieldValue");

        # load the resource's metadata schema
        $Schema = new MetadataSchema($Resource->SchemaId());

        # get the value, possibly having it filtered by plugins
        return trim(strip_tags(GetResourceFieldValue(
            $Resource,
            $this->GetFieldForSetting($Schema, $Setting))));
    }

    /**
    * Get the URL, width, height, and MIME type of an image field.
    * @param Resource $Resource Resource from which to get the image.
    * @param string $Setting Plugin setting from which to get the field.
    * @return Returns an array with the following structure:
    *      array(screenshot URL, width, height, MIME type)
    * @see GetSimpleFieldValue()
    */
    protected function GetImageFieldValue(Resource $Resource, $Setting)
    {
        # screenshot URL, width, height, MIME type
        $Values = array(NULL, NULL, NULL, NULL);

        # load the resource's metadata schema
        $Schema = new MetadataSchema($Resource->SchemaId());

        # return empty values if the setting isn't set
        if (is_null($this->ConfigSetting($Setting."/".$Schema->Id())))
        {
            return $Values;
        }

        # make sure the function is loaded
        $GLOBALS["AF"]->LoadFunction("GetResourceFieldValue");

        # get the image, possibly having it filtered by plugins
        $Images = GetResourceFieldValue(
            $Resource,
            $this->GetFieldForSetting($Schema, $Setting));

        # return empty values if there is no image
        if (!count($Images))
        {
            return $Values;
        }
        $Image = array_shift($Images);
        if (!($Image instanceof SPTImage))
        {
            return $Values;
        }

        # make sure image URL is absolute
        $ImageUrl = $Image->Url();
        if ((stripos($ImageUrl, "http://") !== 0)
                && (stripos($ImageUrl, "https://") !== 0))
        {
            $ImageUrl = ApplicationFramework::BaseUrl().$ImageUrl;
        }

        # screenshot URL, width, height, MIME type
        $Values[0] = $ImageUrl;
        $Values[1] = $Image->Width();
        $Values[2] = $Image->Height();
        $Values[3] = $Image->Mimetype();

        return $Values;
    }

    /**
    * Get all images to associate with a resource in the additional metadata
    * added to a resource's view page.
    * @param Resource $Resource Resource to get images for.
    * @return An array of final image data, with a URL, width, height, and
    *      MIME type for each image. The order of the images is the order of
    *      importance, i.e., the first image is the most important, etc.
    */
    protected function GetImagesForResource(Resource $Resource)
    {
        $Images = array();

        # first, get the initial image from the field, which might be nothing
        list($ScreenshotUrl, $ScreenshotWidth, $ScreenshotHeight, $Mimetype) =
            $this->GetImageFieldValue($Resource, "ScreenshotField");

        # add the initial image if it's set
        if (!is_null($ScreenshotUrl))
        {
            $Images[] = array(
                "Url" => $ScreenshotUrl,
                "Width" => $ScreenshotWidth,
                "Height" => $ScreenshotHeight,
                "Mimetype" => $Mimetype);
        }

        # signal the event to modify the images
        $ReturnValue = $GLOBALS["AF"]->SignalEvent(
            "SocialMedia_MODIFY_IMAGES",
            array("Resource" => $Resource, "Images" => $Images));

        # extract the final list of images from the return value
        $FinalImages = $ReturnValue["Images"];

        # validate the images
        foreach ($FinalImages as $Key => $Image)
        {
            # remove the image if it doesn't at least have a URL
            if (!isset($Image["Url"]) || strlen(trim($Image["Url"])) < 1)
            {
                unset($FinalImages[$Key]);
                continue;
            }

            # make sure all the necessary fields exist
            $FinalImages[$Key] = $FinalImages[$Key] + array(
                "Url" => NULL,
                "Width" => NULL,
                "Height" => NULL,
                "Mimetype" => NULL);
        }

        return $FinalImages;
    }

    /**
    * Get the metadata field associated with the given plugin setting.
    * @param MetadataSchema $Schema Metadata schema to use for fetching the
    *     field.
    * @param string $Setting Plugin setting from which to get the field.
    * @return Returns the field for the setting or NULL if it isn't set.
    */
    protected function GetFieldForSetting(MetadataSchema $Schema, $Setting)
    {
        $FieldId = $this->ConfigSetting($Setting."/".$Schema->Id());

        # return NULL if the field ID is not set
        if (is_null($FieldId) || !strlen($FieldId))
        {
            return NULL;
        }

        try
        {
            $Field = $Schema->GetField($FieldId);
        }

        # return NULL if the field does not exist
        catch (InvalidArgumentException $Exception)
        {
            return NULL;
        }

        # return NULL if the field isn't valid
        if ($Field->Status() !== MetadataSchema::MDFSTAT_OK)
        {
            return NULL;
        }

        return $Field;
    }

    /**
    * Determine if a property for a meta tag is set, i.e., not blank.
    * @param string $Property Property value to check.
    * @return Returns TRUE if the property is set and FALSE otherwise.
    */
    protected function IsPropertySet($Property)
    {
        return strlen(trim($Property)) > 0;
    }

    /**
    * Format a Twitter user name for use in a Twitter meta tag.
    * @param string $Username Twitter user name to format.
    * @return Returns the formatted Twitter user name.
    */
    protected function FormatTwitterUsername($Username)
    {
        # don't format a blank user name
        if (!$this->IsPropertySet($Username))
        {
            return NULL;
        }

        # add the @ to the user name, if necessary
        if ($Username{0} != "@")
        {
            $Username = "@" . $Username;
        }

        return $Username;
    }

    /**
    * Print a meta tag tailored to the Open Graph specification. See:
    * @li http://developers.facebook.com/docs/opengraph/property-types/
    * @li http://developers.facebook.com/docs/opengraph/creating-object-types/
    * @param string $Property The value identifier.
    * @param string $Content The metadata content.
    * @see PrintTwitterTag()
    */
    protected function PrintOpenGraphTag($Property, $Content)
    {
        # ignore blank properties
        if (!$this->IsPropertySet($Content))
        {
            return;
        }

        print $this->GetMetaTag("property", $Property, "content", $Content)."\n";
    }

    /**
    * Print a meta tag tailored to Twitter's specifications. See:
    * https://dev.twitter.com/docs/cards
    * @param string $Name The value identifier.
    * @param string $Content The metadata content.
    * @see PrintOpenGraphTag()
    */
    protected function PrintTwitterTag($Name, $Content)
    {
        # ignore blank properties
        if (!$this->IsPropertySet($Content))
        {
            return;
        }

        print $this->GetMetaTag("name", $Name, "content", $Content)."\n";
    }

    /**
    * Print the Open Graph meta tags for the given image data.
    * @param array $Images Image data to put in Open Graph meta tags.
    */
    protected function PrintOpenGraphImages(array $Images)
    {
        # print the tags for each image
        foreach ($Images as $Image)
        {
            $this->PrintOpenGraphTag("og:image", $Image["Url"]);
            $this->PrintOpenGraphTag("og:image:width", $Image["Width"]);
            $this->PrintOpenGraphTag("og:image:height", $Image["Height"]);
            $this->PrintOpenGraphTag("og:image:type", $Image["Mimetype"]);
        }
    }

    /**
    * Print the Twitter meta tags for the given image data.
    * @param array $Images Image data to put in Twitter meta tags.
    */
    protected function PrintTwitterImages(array $Images)
    {
        # print the tags for each image
        foreach ($Images as $Image)
        {
            $this->PrintOpenGraphTag("twitter:image", $Image["Url"]);
            $this->PrintOpenGraphTag("twitter:image:width", $Image["Width"]);
            $this->PrintOpenGraphTag("twitter:image:height", $Image["Height"]);

            # Twitter currently only supports one image in summary cards
            break;
        }
    }

    /**
    * Generate a meta tag given a parameter list of attribute names and values.
    * Odd parameters should be attribute names and even parameters should be
    * values.
    * @return Returns the generated meta tag.
    */
    protected function GetMetaTag()
    {
        $Arguments = func_get_args();
        $Tag = "<meta";

        # loop through each argument, ensuring that they come in pairs
        $N_Args = count($Arguments);
        for ($i = 0; $N_Args - $i > 1; $i += 2)
        {
            $Attribute = $Arguments[$i];
            $Value = $Arguments[$i+1];
            $SafeValue = defaulthtmlentities(strip_tags(trim($Value)));

            # add the attribute name/value pair to the tag
            $Tag .= " " . $Attribute . '="' . $SafeValue  . '"';
        }

        $Tag .= " />";

        return $Tag;
    }

    /**
    * Get the sharing URL for a resource and a social media website.
    * @param Resource $Resource Resource to construct a sharing URL for.
    * @param string $Site Website to share to.
    * @return Returns the sharing URL or NULL if there's an error.
    * @see SITE_FACEBOOK
    * @see SITE_TWITTER
    * @see SITE_LINKEDIN
    * @see SITE_GOOGLEPLUS
    */
    public function GetSharingUrl(Resource $Resource, $Site)
    {
        switch ($Site)
        {
            case self::SITE_FACEBOOK:
                return $this->GetSharingUrlForFacebook($Resource);
            case self::SITE_TWITTER:
                return $this->GetSharingUrlForTwitter($Resource);
            case self::SITE_LINKEDIN:
                return $this->GetSharingUrlForLinkedIn($Resource);
            case self::SITE_GOOGLEPLUS:
                return $this->GetSharingUrlForGooglePlus($Resource);
            default:
                return NULL;
        }
    }

    /**
    * Construct a sharing URL for Facebook for a resource.
    * @param Resource $Resource Resource to construct a sharing URL for.
    * @return Returns a sharing URL for Facebook for the resource.
    */
    protected function GetSharingUrlForFacebook(Resource $Resource)
    {
        # add the basic parameters and get the title, which may not be available
        $Parameters = array("u" => $this->GetViewPageUrl($Resource));
        $Title = $this->GetSimpleFieldValue($Resource, "TitleField");

        # add the title, if available
        if ($this->IsPropertySet($Title))
        {
            $Parameters["t"] = $Title;
        }

        # encode them for a URL query
        $QueryParameters = http_build_query($Parameters);

        # construct the URL
        $Url = self::BASE_FACEBOOK_SHARE_URL . "?" . $QueryParameters;

        return $Url;
    }

    /**
    * Construct a sharing URL for Twitter for a resource.
    * @param Resource $Resource Resource to construct a sharing URL for.
    * @return Returns a sharing URL for Twitter for the resource.
    */
    protected function GetSharingUrlForTwitter(Resource $Resource)
    {
        # add the basic parameters and get the twitter user name, which may not
        # be available
        $Parameters = array("url" => $this->GetViewPageUrl($Resource));
        $TwitterUsername = $this->FormatTwitterUsername(
            $this->ConfigSetting("TwitterUsername"));

        # add the twitter user name, if available
        if ($this->IsPropertySet($TwitterUsername))
        {
            # don't include the @
            $Parameters["via"] = substr($TwitterUsername, 1);
        }

        # encode them for a URL query
        $QueryParameters = http_build_query($Parameters);

        # construct the URL
        $Url = self::BASE_TWITTER_SHARE_URL . "?" . $QueryParameters;

        return $Url;
    }

    /**
    * Construct a sharing URL for LinkedIn for a resource.
    * @param Resource $Resource Resource to construct a sharing URL for.
    * @return Returns a sharing URL for LinkedIn for the resource.
    */
    protected function GetSharingUrlForLinkedIn(Resource $Resource)
    {
        # construct the query parameters. LinkedIn supports some other useful
        # parameters, but they sometimes cause issues. since LinkedIn also
        # supports the Open Graph protocol, which is embedded in a resource's
        # view page already, ignore the extra parameters here
        $QueryParameters = http_build_query(array(
            "mini" => "true",
            "url" => $this->GetViewPageUrl($Resource)));

        # construct the URL
        $Url = self::BASE_LINKEDIN_SHARE_URL . "?" . $QueryParameters;

        return $Url;
    }

    /**
    * Construct a sharing URL for Google+ for a resource.
    * @param Resource $Resource Resource to construct a sharing URL for.
    * @return Returns a sharing URL for Google+ for the resource.
    */
    protected function GetSharingUrlForGooglePlus(Resource $Resource)
    {
        # construct the query parameters
        $QueryParameters = http_build_query(array(
            "url" => $this->GetViewPageUrl($Resource)));

        # construct the URL
        $Url = self::BASE_GOOGLEPLUS_SHARE_URL . "?" . $QueryParameters;

        return $Url;
    }
}
