<?PHP
#
#   FILE:  Tags.php
#
#   Part of the Collection Workflow Integration System (CWIS)
#   Copyright 2011 Internet Scout Project
#   http://scout.wisc.edu/
#

class Tags extends Plugin
{

    /**
     * @const TAG_FIELD_NAME the name of the tag metadata field
     */
    const TAG_FIELD_NAME = "Tags (Plugin)";

    /**
     * @const TAG_FIELD_XML file name of the XML representation of the tag field
     */
    const TAG_FIELD_XML = "Tags_Tag.xml";

    /**
     * @const DEFAULT_TAGS_CSV CSV file with the default tags
     */
    const DEFAULT_TAGS_CSV = "DefaultTags.csv";

    /**
     * @const CRITERIA_OWNS_RESOURCE criterion used when a user owns a resource
     */
    const CRITERIA_OWNS_RESOURCE = -1;

    /**
     * @const CRITERIA_LOGGED_IN criterion used when a user is logged in
     */
    const CRITERIA_LOGGED_IN = -2;

    /**
     * Register information about this plugin.
     */
    public function Register()
    {
        $this->Name = "Tags";
        $this->Version = "1.0.4";
        $this->Description = "Adds tag metadata support to resources.";
        $this->Author = "Internet Scout";
        $this->Url = "http://scout.wisc.edu/cwis/";
        $this->Email = "scout@scout.wisc.edu";
        $this->Requires = array("CWISCore" => "2.4.1");
        $this->EnabledByDefault = FALSE;

        $PrivilegeFactory = new PrivilegeFactory();
        $Criteria = $PrivilegeFactory->GetPrivileges(TRUE, FALSE);

        # remove undesirable privileges
        unset($Criteria[PRIV_USERDISABLED]);

        foreach ($Criteria as $Value => $Privilege)
        {
            $Criteria[$Value] = "has the \"".$Privilege."\" privilege";
        }

        $Criteria[self::CRITERIA_LOGGED_IN] = "is logged in";
        $Criteria[self::CRITERIA_OWNS_RESOURCE] = "is the resource creator";
        ksort($Criteria);

        $this->CfgSetup["AddTags"] = array(
            "Type" => "Option",
            "Label" => "Restrict tag creation to any user that",
            "Help" => $this->FixHelp("
                An user may add tags to a resource if he or she meets one or
                more of the selected criteria. If nothing is selected, then any
                user, even those not logged in, will be able to add tags."),
            "AllowMultiple" => TRUE,
            "Options" => $Criteria);

        $this->CfgSetup["Privileged"] = array(
            "Type" => "Option",
            "Label" => "Mark tags as privileged for any user that",
            "Help" => $this->FixHelp("
                Mark the tags as privileged for any user that meets the selected
                criteria. If nothing is selected, then every tag will be marked
                as privileged."),
            "AllowMultiple" => TRUE,
            "Options" => $Criteria);

        $this->CfgSetup["RemoveOthers"] = array(
            "Type" => "Option",
            "Label" => "
                Restrict removal of another user's unprivileged tags to
                any user that",
            "Help" => $this->FixHelp("
                An user may remove another user's unprivileged tags from a
                resource if he or she meets one or more of the selected
                criteria. If nothing is selected, then any user, even those not
                logged in, will be able to remove another user's unprivileged
                tags."),
            "AllowMultiple" => TRUE,
            "Options" => $Criteria);

        $this->CfgSetup["RemoveOthersPrivileged"] = array(
            "Type" => "Option",
            "Label" => "
                Restrict removal of another user's privileged tags to any user
                that",
            "Help" => $this->FixHelp("
                An user may remove another user's privileged tags from a
                resource if he or she meets one or more of the selected
                criteria. If nothing is selected, then any user, even those not
                logged in, will be able to remove another user's privileged
                tags."),
            "AllowMultiple" => TRUE,
            "Options" => $Criteria);

        $this->CfgSetup["Blacklist"] = array(
            "Type" => "Paragraph",
            "Label" => "Blacklist",
            "Help" => $this->FixHelp("
                Enter tags, one per line, that should not be allowed. Regular
                expressions are allowed and are automatically enclosed between
                ^ and $."));

        $this->CfgSetup["Reset"] = array(
            "Type" => "Flag",
            "Label" => "Reset Configuration",
            "Help" => $this->FixHelp("
                Reset the configuration to the state it was in when the plugin
                was installed."),
            "OnLabel" => "Yes",
            "OffLabel" => "No");
    }

    /**
     * Initialize the plugin, i.e., reset the plugin configuration settings
     * if requested.
     */
    public function Initialize()
    {
        # reset settings if requested
        if ($this->ConfigSetting("Reset"))
        {
            $this->ResetPluginConfiguration();
        }
    }

    /**
     * Install necessary SQL tables.
     * @return NULL on success or error message on error
     */
    public function Install()
    {
        require_once dirname(__FILE__) . "/objects/Tags_ImmutableStruct.php";
        require_once dirname(__FILE__) . "/objects/Tags_TagReference.php";

        # table for tag references
        if (FALSE === Tags_TagReference::CreateStorageTable())
        { return "Could not create the tag references table."; }

        # try to get the field
        $Field = $this->GetTagField();

        # try to create it if it doesn't exist
        if (!($Field instanceof MetadataField))
        {
            # create the tag field from its XML representation
            $Schema = new MetadataSchema();
            $Xml = $this->GetTagXml();
            $Field = $Schema->AddFieldFromXml($Xml);

            if (!($Field instanceof MetadataField))
            {
                return "Could not create the tag metadata field.";
            }
        }

        # get the file that holds the default tags
        $DefaultTagsFile = @fopen($this->GetDefaultTagsFile(), "r");

        if ($DefaultTagsFile === FALSE)
        {
            return "Could not prepopulate the tag metadata field.";
        }

        # get the tags
        $Tags = @fgetcsv($DefaultTagsFile);

        if ($Tags === FALSE)
        {
            return "Could not parse the default tags";
        }

        # add each tag
        foreach ($Tags as $Tag)
        {
            $ControlledName = new ControlledName(NULL, $Tag, $Field->Id());
        }

        # close the default tag file
        @fclose($DefaultTagsFile);

        $this->ResetPluginConfiguration();

        return NULL;
    }

    /**
     * Uninstall the plugin.
     * @return NULL|string NULL if successful or an error message otherwise
     */
    public function Uninstall()
    {
        $Database = new Database();

        # tag references table
        if (FALSE === $Database->Query("DROP TABLE Tags_TagReferences;"))
        { return "Could not remove the tag references table"; }

        # remove the tag field
        $Field = $this->GetTagField();
        $Schema = new MetadataSchema($Field->SchemaId());
        $Schema->DropField($Field->Id());
    }

    /**
     * Declare the events this plugin provides to the application framework.
     * @return an array of the events this plugin provides
     */
    public function DeclareEvents()
    {
        return array(
            "TAGS_EDIT_TAGS"
                => ApplicationFramework::EVENTTYPE_DEFAULT,
            "TAGS_ADD_TAG"
                => ApplicationFramework::EVENTTYPE_FIRST,
            "TAGS_REMOVE_TAG"
                => ApplicationFramework::EVENTTYPE_FIRST,
            "TAGS_TAG_IS_BLACKLISTED"
                => ApplicationFramework::EVENTTYPE_FIRST);
    }

    /**
    * Hook the events into the application framework.
    * @return an array of events to be hooked into the application framework
    */
    public function HookEvents()
    {
        return array(
            "EVENT_PAGE_LOAD" => "PageLoaded",
            "EVENT_RESOURCE_DELETE" => "ResourceDeleted",
            "EVENT_APPEND_HTML_TO_FIELD_DISPLAY" => "AppendToFieldDisplay",
            "EVENT_IN_HTML_HEADER" => "InHtmlHeader",
            "EVENT_CNAME_REMAPPED" => "ControlledNameRemapped",
            "TAGS_EDIT_TAGS" => "EditTags",
            "TAGS_ADD_TAG" => "AddTagExternal",
            "TAGS_REMOVE_TAG" => "RemoveTagExternal",
            "TAGS_TAG_IS_BLACKLISTED" => "TagIsBlacklisted");
    }

    /**
    * Make sure there is no stale or invalid data prior to fetching any later
    * on.
    * @param string $PageName Name of the page being loaded.
    */
    public function PageLoaded($PageName)
    {
        if ($PageName == "FullRecord")
        {
            $ResourceId = GetArrayValue($_GET, "ID");
            $ResourceId = $ResourceId
                ? $ResourceId : GetArrayValue($_GET, "ResourceId");
            $this->UpdateTagsAndReferences($ResourceId);
        }
    }

    /**
    * Delete tag references when the resource they refer to is deleted.
    * @param Resource $Resource Resource object
    */
    public function ResourceDeleted($Resource)
    {
        Tags_TagReference::DeleteTagReferences(array(
            "ResourceId" => $Resource->Id()));
    }

    /**
    * Append HTML to field display to allow adding tags on-the-fly.
    * @param MetadataField $Field MetadataField object
    * @param Resource $Resource Resource object
    * @param string $Context Context in which the field is displayed
    * @param string $Html Current HTML to display with the field
     */
    public function AppendToFieldDisplay($Field, $Resource, $Context, $Html)
    {
        global $User, $AF;

        $TagField = $this->GetTagField();
        $IsTag = $Field->Name() == $TagField->Name();

        if ($IsTag && $Context == "DISPLAY")
        {
            if ($this->CanAddTags($User, $Resource))
            {
                $Tags = $this->GetEditableTags($User, $Resource);
                $TagsHtml = defaulthtmlentities(implode("\n", $Tags));
                $ReturnToHtml = "index.php?P=FullRecord&amp;ID=".$Resource->Id();
                $Blacklist = defaulthtmlentities($this->ConfigSetting("Blacklist"));

                $Variables = array(
                    "AF" => $AF,
                    "HelpImage" => "plugins/Tags/interface/default/images/help.png",
                    "Tags" => $TagsHtml,
                    "ReturnTo" => $ReturnToHtml,
                    "ResourceId" => $Resource->Id(),
                    "FieldId" =>$TagField->Id(),
                    "Blacklist" => $Blacklist);

                $Html .= $this->IncludeAndBuffer("EditTagsForm", $Variables);
            }
        }

        return array(
            "Field" => $Field,
            "Resource" => $Resource,
            "Context" => $Context,
            "Html" => $Html);
    }

    /**
     * Print stylesheet and Javascript elements in the page header
     */
    public function InHtmlHeader()
    {
        $CssUrl = "plugins/Tags/interface/default/include/style.css";
        $JsUrl = "plugins/Tags/interface/default/include/Tags.js";
?>
<link rel="stylesheet" type="text/css" href="<?PHP print $CssUrl; ?>" />
<script type="text/javascript" src="<?PHP print $JsUrl; ?>"></script>
<?PHP
    }

    /**
    * Update tag references when a controlled name is remapped.
    * @param string $OldControlledNameId The old controlled name ID
    * @param string $NewControlledNameId The new controlled name ID
    */
    public function ControlledNameRemapped(
        $OldControlledNameId,
        $NewControlledNameId)
    {
        $TagReferences = Tags_TagReference::GetTagReferences(array(
            "ControlledNameId" => $OldControlledNameId));

        foreach ($TagReferences as $TagReference)
        {
            $TagReference = Tags_TagReference::UpdateControlledNameId(
                $TagReference, $NewControlledNameId);
        }
    }

    /**
    * Verify any changes to the tags and restore tags or remove them as
    * necessary based on the config values.
    * @param User $User User object.
    * @param Resource $Resource Resource object.
    * @param array $Tags Array of tags to verify.
    */
    public function EditTags(User $User, Resource $Resource, array $Tags)
    {
        # current tags and tag references
        $CurrentTags = $Resource->Get($this->GetTagField());
        $TagReferences = Tags_TagReference::GetTagReferences(array(
            "ResourceId" => $Resource->Id()));

        $ChangeMade = FALSE;
        $CanAddTags = $this->CanAddTags($User, $Resource);
        $TagField = $this->GetTagField();
        $ControlledNameFactory = new ControlledNameFactory($TagField->Id());

        # create tags that don't exist yet, if permitted
        foreach ($Tags as $Key => $Tag)
        {
            # don't consider blacklisted tags and disregard it from here on out
            if ($this->TagIsBlacklisted($Tag))
            {
                unset($Tags[$Key]);
                continue;
            }

            $ControlledName = $ControlledNameFactory->GetItemByName($Tag);

            # normalize the tag
            $Tag = $Tags[$Key] = $this->NormalizeTag($Tag);

            # if the user is permitted to add tags
            if ($CanAddTags)
            {
                # create tag if it doesn't exist
                if (!($ControlledName instanceof ControlledName))
                {
                    $ControlledName = $this->CreateTag($Tag);
                }

                # tag isn't set for resource, so add it and create reference
                if (!in_array($ControlledName->Id(), $CurrentTags))
                {
                    $ChangeMade = TRUE;
                    $this->AddTag($Resource, $ControlledName);
                    $this->AddTagReference($User, $Resource, $ControlledName);
                }
            }

            # otherwise disregard it from here on out
            else
            {
                unset($Tag[$Key]);
            }
        }

        # remove tags, if permitted
        foreach ($TagReferences as $TagReferenceId => $TagReference)
        {
            $ControlledNameId = $TagReference->ControlledNameId;
            $ControlledName = $ControlledNameFactory->GetItem($ControlledNameId);
            $Tag = $ControlledName->Name();

            # tag deleted
            if (!in_array($Tag, $Tags))
            {
                # can remove the tag, so remove the tag and reference
                if ($this->CanRemoveTag($User, $Resource, $TagReference))
                {
                    $ChangeMade = TRUE;
                    $this->RemoveTag($Resource, $ControlledName);
                    $this->RemoveTagReference($TagReference);
                }

                # add the tag back into the list
                else
                {
                    $Tags[] = $Tag;
                }
            }
        }

        # update search and recommender databases if a change was made
        if ($ChangeMade)
        {
            $SearchEngine = new SPTSearchEngine();
            $SearchEngine->QueueUpdateForItem($Resource->Id());
            $Recommender = new SPTRecommender();
            $Recommender->QueueUpdateForItem($Resource->Id());
        }
    }

    /**
    * Add a tag as necessary based on the config values.
    * @param User $User User object
    * @param Resource $Resource Resource object
    * @param string $Tag Tag to add
     */
    public function AddTagExternal(User $User, Resource $Resource, $Tag)
    {
        $Tag = $this->NormalizeTag($Tag);
        $CurrentTags = $Resource->Get($this->GetTagField());
        $ControlledNameId = array_search($Tag, $CurrentTags);

        # don't add blacklisted tags
        if ($this->TagIsBlacklisted($Tag))
        {
            return;
        }

        # if the tag isn't already set for the resource and the user can add
        # tags
        if (FALSE === $ControlledNameId && $this->CanAddTags($User, $Resource))
        {
            $TagField = $this->GetTagField();
            $ControlledNameFactory = new ControlledNameFactory($TagField->Id());
            $ControlledName = $ControlledNameFactory->GetItemByName($Tag);

            # create tag if it doesn't exist
            if (!($ControlledName instanceof ControlledName))
            {
                $ControlledName = $this->CreateTag($Tag);
            }

            $this->AddTag($Resource, $ControlledName);
            $this->AddTagReference($User, $Resource, $ControlledName);

            # update search and recommender databases
            $SearchEngine = new SPTSearchEngine();
            $SearchEngine->QueueUpdateForItem($Resource->Id());
            $Recommender = new SPTRecommender();
            $Recommender->QueueUpdateForItem($Resource->Id());
        }
    }

    /**
    * Remove a tag as necessary based on the config values and t
    * @param User $User User object
    * @param Resource $Resource Resource object
    * @param string $Tag Tag as a string
    */
    public function RemoveTagExternal(User $User, Resource $Resource, $Tag)
    {
        $Tag = $this->NormalizeTag($Tag);
        $TagField = $this->GetTagField();
        $CurrentTags = $Resource->Get($TagField);
        $ControlledNameId = array_search($Tag, $CurrentTags);

        # if the tag is set for the resource
        if (FALSE !== $ControlledNameId)
        {
            # get the tag reference(s)
            $TagReferences = Tags_TagReference::GetTagReferences(array(
                "ResourceId" => $Resource->Id(),
                "ControlledNameId" => $ControlledNameId));

            # remove the tag reference if possible
            if (count($TagReferences))
            {
                $TagReference = array_shift($TagReferences);

                # if the user is permitted to remove the tag
                if ($this->CanRemoveTag($User, $Resource, $TagReference))
                {
                    $ControlledNameFactory =
                        new ControlledNameFactory($TagField->Id());
                    $ControlledName =
                        $ControlledNameFactory->GetItem($ControlledNameId);

                    # remove the tag and reference
                    $this->RemoveTag($Resource, $ControlledName);
                    $this->RemoveTagReference($TagReference);

                    # update search and recommender databases
                    $SearchEngine = new SPTSearchEngine();
                    $SearchEngine->QueueUpdateForItem($Resource->Id());
                    $Recommender = new SPTRecommender();
                    $Recommender->QueueUpdateForItem($Resource->Id());
                }
            }
        }
    }

    /**
    * Determine whether the given tag is in the list of blacklisted tags.
    * @param string $Tag Tag as a string
    * @return TRUE if the tag is blacklisted or FALSE otherwise
    */
    public function TagIsBlacklisted($Tag)
    {
        $BadTags = explode("\n", $this->ConfigSetting("Blacklist"));

        foreach ($BadTags as $BadTag)
        {
            # remove trailing newline
            $BadTag = trim($BadTag);

            if (@preg_match('/^'.$BadTag.'$/i', $Tag))
            {
                return TRUE;
            }
        }

        return FALSE;
    }

    /**
    * Update tags and references for the given resource so that there is no
    * stale or invalid data.
    * @param int $ResourceId Resource ID.
    */
    protected function UpdateTagsAndReferences($ResourceId)
    {
        global $DB;

        $TagField = $this->GetTagField();

        # remove tags and references for bad resources
        if (!Resource::ItemExists($ResourceId))
        {
            # clear tag references
            Tags_TagReference::DeleteTagReferences(
                array("ResourceId" => $ResourceId));

            return;
        }

        $Resource = new Resource($ResourceId);
        $ControlledNameFactory = new ControlledNameFactory($TagField->Id());
        $UserFactory = new CWUserFactory();
        $TagReferences = Tags_TagReference::GetTagReferences(array(
            "ResourceId" => $Resource->Id()));
        $CurrentTags = $Resource->Get($TagField);

        foreach ($TagReferences as $TagReference)
        {
            $ControlledNameId = $TagReference->ControlledNameId;

            # remove references to tags that are no longer set for the resource
            if (!isset($CurrentTags[$ControlledNameId]))
            {
                $this->RemoveTagReference($TagReference);
                continue;
            }

            $ControlledName = new ControlledName($ControlledNameId);

            # remove references to tags that no longer exist, i.e., the
            # the controlled name has been deleted
            if ($ControlledName->Status() == ControlledName::STATUS_INVALID_ID)
            {
                $this->RemoveTagReference($TagReference);
                continue;
            }

            $Tag = $ControlledName->Name();

            # remove blacklisted tags and their references
            if ($this->TagIsBlacklisted($Tag))
            {
                $this->RemoveTagReference($TagReference);
                $ControlledName->Delete(TRUE);
                continue;
            }

            $UserId = $TagReference->UserId;
            $Count = $UserFactory->GetUserCount(
                "UserId = '".addslashes($UserId)."'");

            # update tag references added by users that no longer exist. set
            # the user ID to the owner of the resource
            if ($Count < 1)
            {
                $AddedByName = $Resource->Get("Added By Id");
                $Owner = new CWUser($AddedByName);

                $TagReference = Tags_TagReference::UpdateUserId(
                    $TagReference, $Owner);
            }

            $UserId = $TagReference->UserId;
            $User = new CWUser($UserId);
            $Privileged = $this->UserMeetsCriteria(
                $User, $Resource, "Privileged");

            # update the IsPrivileged value if it has changed
            if ($TagReference->IsPrivileged != $Privileged)
            {
                $TagReference = Tags_TagReference::UpdateIsPrivileged(
                    $TagReference, $Privileged);
            }
        }
    }

    /**
    * Get the tags that the given user can remove for the given resource.
    * @param User $User User object
    * @param Resource $Resource Resource object
    * @return an array of tags (controlled name ID => controlled name value)
    */
    protected function GetEditableTags(User $User, Resource $Resource)
    {
        $TagReferences = Tags_TagReference::GetTagReferences(array(
            "ResourceId" => $Resource->Id()));
        $Tags = array();

        foreach ($TagReferences as $TagReference)
        {
            if ($this->CanRemoveTag($User, $Resource, $TagReference))
            {
                $ControlledNameId = $TagReference->ControlledNameId;
                $ControlledName = new ControlledName($ControlledNameId);
                $Tags[$ControlledNameId] = $ControlledName->Name();
            }
        }

        sort($Tags);
        return $Tags;
    }

    /**
    * Determine if the given user can add a tag to the given resource.
    * @param User $User User object
    * @param Resource $Resource Resource object
    * @return TRUE if the user can add a tag or FALSE otherwise
    */
    protected function CanAddTags(User $User, Resource $Resource)
    {
        return $this->UserMeetsCriteria($User, $Resource, "AddTags");
    }

    /**
    * Determine if the given user can remove the tag of the given resource and
    * referenced by the given tag reference.
    * @param User $User User object
    * @param Resource $Resource Resource object
    * @param Tags_TagReference $TagReference Tags_TagReference
    * @return TRUE if the user can remove the tag or FALSE otherwise
    */
    protected function CanRemoveTag(
        User $User,
        Resource $Resource,
        Tags_TagReference $TagReference)
    {
        # if the user created the tag reference
        if ($TagReference->UserId == $User->Id())
        {
            return TRUE;
        }

        $IsPrivileged = $TagReference->IsPrivileged;
        $CanRemovePrivileged = $this->UserMeetsCriteria(
            $User,
            $Resource,
            "RemoveOthersPrivileged");

        # if the tag is privileged and the user can remove privileged tags
        if ($IsPrivileged && $CanRemovePrivileged)
        {
            return TRUE;
        }

        $CanRemove = $this->UserMeetsCriteria($User, $Resource, "RemoveOthers");

        # if the tag is not privileged and the user can remove unprivileged tags
        if (!$IsPrivileged && $CanRemove)
        {
            return TRUE;
        }

        return FALSE;
    }

    /**
    * Determine if the given user meets the criteria for the given setting for
    * the given resource.
    * @param User $User User object
    * @param Resource $Resource Resource object
    * @param string $Setting Name of configuration setting
    * @return TRUE if the user meets the criteria or FALSE otherwise
    */
    protected function UserMeetsCriteria(
        User $User,
        Resource $Resource,
        $Setting)
    {
        $Value = $this->ConfigSetting($Setting);

        # if nothing is set, assume anyone meets the criteria
        if (empty($Value))
        {
            return TRUE;
        }

        # if being logged in is part of the criteria and the user is logged in
        if (in_array(self::CRITERIA_LOGGED_IN, $Value))
        {
            if ($User->IsLoggedIn())
            {
                return TRUE;
            }
        }

        # if owning the resource is part of the criteria and the user owns it
        if (in_array(self::CRITERIA_OWNS_RESOURCE, $Value))
        {
            if ($User->Name() == $Resource->Get("Added By Id"))
            {
                return TRUE;
            }
        }

        # if the user has any of the privileges. the criteria values from the
        # plugin (self::CRITERIA_*) have negative values and can be safely
        # included here
        if (count(array_intersect($User->GetPrivList(), $Value)))
        {
            return TRUE;
        }

        return FALSE;
    }

    /**
    * Create a tag reference for the given values.
    * @param User $User User object
    * @param Resource $Resource Resource object
    * @param ControlledName $ControlledName ControlledName object
    * @return the new Tags_TagReference object
    */
    protected function AddTagReference(
        User $User,
        Resource $Resource,
        ControlledName $ControlledName)
    {
        # get the value for the IsPrivileged reference value
        $Privileged = $this->UserMeetsCriteria($User, $Resource, "Privileged");

        # add the tag reference
        $TagReference = Tags_TagReference::AddTagReference(
            $User,
            $Resource,
            $ControlledName,
            $Privileged);

        return $TagReference;
    }

    /**
    * Remove the given tag reference.
    * @param Tags_TagReference $TagReference Tags_TagReference object
    */
    protected function RemoveTagReference(Tags_TagReference $TagReference)
    {
        Tags_TagReference::DeleteTagReferences(
            array("TagReferenceId" => $TagReference->TagReferenceId));
    }

    /**
    * Create the given tag, i.e., a new controlled name.
    * @param string $Tag Tag value
    * @return the new tag as a ControlledName object
    */
    protected function CreateTag($Tag)
    {
        $TagField = $this->GetTagField();
        $ControlledNameFactory = new ControlledNameFactory($TagField->Id());
        $ControlledName = $ControlledNameFactory->GetItemByName($Tag);

        # if the controlled name doesn't already exist, create it
        if (!($ControlledName instanceof ControlledName))
        {
            $TagField = $this->GetTagField();
            $ControlledName = new ControlledName(NULL, $Tag, $TagField->Id());
        }

        return $ControlledName;
    }

    /**
    * Add the given tag to the resource.
    * @param Resource $Resource Target Resource.
    * @param ControlledName $ControlledName ControlledName for the tag to add.
    */
    protected function AddTag(
        Resource $Resource,
        ControlledName $ControlledName)
    {
        $TagField = $this->GetTagField();

        $Tags = $Resource->Get($TagField);
        $Tags[$ControlledName->Id()] = $ControlledName->Name();

        $Resource->ClearByField($TagField);
        $Resource->Set($TagField, $Tags);
    }

    /**
    * Remove the given tag from the resource.
    * @param Resource $Resource Resource object
    * @param ControlledName $ControlledName ControlledName object
     */
    protected function RemoveTag(
        Resource $Resource,
        ControlledName $ControlledName)
    {
        $TagField = $this->GetTagField();

        $Tags = $Resource->Get($TagField);
        unset($Tags[$ControlledName->Id()]);

        $Resource->ClearByField($TagField);
        $Resource->Set($TagField, $Tags);
    }

    /**
    * Normalize a tag so that it doesn't begin or end with whitespace and so
    * that it is in lower case.
    * @param string $Tag Tag value as a string
    * @return normalized tag value as a string
    */
    protected function NormalizeTag($Tag)
    {
        return strtolower(trim($Tag));
    }

    /**
    * Get the metadata field object for the tag field.
    * @return MetadataField object for the tag field
    */
    protected function GetTagField()
    {
        $Schema = new MetadataSchema();
        $Field = $Schema->GetFieldByName(self::TAG_FIELD_NAME);

        return $Field;
    }

    /**
    * Get the XML representation of the tag metadata field as a string.
    * @return XML representation of the tag metadata field as a string
    */
    protected function GetTagXml()
    {
        $Path = dirname(__FILE__) . "/install/" . self::TAG_FIELD_XML;
        $Xml = file_get_contents($Path);

        return $Xml;
    }

    /**
    * Get the path to the default tags file.
    * @return string path to the default tags file
    */
    protected function GetDefaultTagsFile()
    {
        $Path = dirname(__FILE__) . "/install/" . self::DEFAULT_TAGS_CSV;

        return $Path;
    }

    /**
    * Remove beginning and ending whitespace, as well as redundant whitespace,
    * so that the help string of an configuration option displays correctly.
    * @param string $String Help string
    * @return the help string without the whitespace defined in the description
    */
    protected function FixHelp($String)
    {
        return preg_replace('/\s{2,}/', " ", trim($String));
    }

    /**
    * Reset the plugin configuration to the defaults.
    */
    protected function ResetPluginConfiguration()
    {
        $this->ConfigSetting("AddTags", array(
            self::CRITERIA_LOGGED_IN));

        $this->ConfigSetting("RemoveOthers", array(
            self::CRITERIA_OWNS_RESOURCE,
            PRIV_NAMEADMIN,
            PRIV_RESOURCEADMIN));

        $this->ConfigSetting("Privileged", array(
            self::CRITERIA_OWNS_RESOURCE,
            PRIV_NAMEADMIN,
            PRIV_RESOURCEADMIN));

        $this->ConfigSetting("RemoveOthersPrivileged", array(
            PRIV_NAMEADMIN,
            PRIV_RESOURCEADMIN));

        $this->ConfigSetting("Blacklist", NULL);
        $this->ConfigSetting("Reset", FALSE);
    }

    /**
    * Include the given file in the same directory, buffering its output,
    * and then return that output.
    * @param string $FileName Name of the file to include (in the same directory)
    * @param array $Variables Array of variables to put in the scope of the include
    * @return the buffered output
    */
    protected function IncludeAndBuffer($FileName, array $Variables=array())
    {
        return $this->CallAndBuffer(
            array($this, "IncludeAndBufferAux"),
            dirname(__FILE__)."/interface/default/include/".$FileName.".php",
            $Variables);
    }

    /**
    * Call the given function and buffer its output, if any. Any additional
    * parameters passed to this method will be passed as parameters to the
    * given function.
    * @param callable $Function Function or method
    * @return the buffered output
    */
    protected function CallAndBuffer($Function)
    {
        ob_start();

        $Parameters = func_get_args();
        array_shift($Parameters);

        call_user_func_array($Function, $Parameters);
        $Buffer = ob_get_contents();

        ob_end_clean();

        return $Buffer;
    }

    /**
    * Auxiliary method for self::IncludeAndBuffer that is used for creating an
    * unpolluted scope.
    */
    private function IncludeAndBufferAux()
    {
        extract(func_get_arg(1));
        include(func_get_arg(0));
    }
}
