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

/**
* Plugin that provides support for blog entries.
*/
class Blog extends Plugin
{

    /**
     * CSV file with the default categories.
     */
    const DEFAULT_CATEGORIES_CSV = "DefaultCategories.csv";

    /**
    * The path to the directory holding the XML representations of the blog
    * fields relative to this file.
    */
    const FIELD_XML_PATH = "install/";

    /**
    * The name of the blog entry title field.
    */
    const TITLE_FIELD_NAME = "Title";

    /**
    * The name of the blog entry body field.
    */
    const BODY_FIELD_NAME = "Body";

    /**
    * The name of the blog entry creation date field.
    */
    const CREATION_DATE_FIELD_NAME = "Date of Creation";

    /**
    * The name of the blog entry modification date field.
    */
    const MODIFICATION_DATE_FIELD_NAME = "Date of Modification";

    /**
    * The name of the blog entry publication date field.
    */
    const PUBLICATION_DATE_FIELD_NAME = "Date of Publication";

    /**
    * The name of the blog entry author field.
    */
    const AUTHOR_FIELD_NAME = "Author";

    /**
    * The name of the blog entry editor field.
    */
    const EDITOR_FIELD_NAME = "Editor";

    /**
    * The name of the blog entry categories field.
    */
    const CATEGORIES_FIELD_NAME = "Categories";

    /**
    * The name of the blog entry image field.
    */
    const IMAGE_FIELD_NAME = "Image";

    /**
    * The name of the blog entry notifications field.
    */
    const NOTIFICATIONS_FIELD_NAME = "Notifications Sent";

    /**
    * Register information about this plugin.
    */
    function Register()
    {
        $this->Name = "Blog";
        $this->Version = "1.0.12";
        $this->Description = "Plugin that adds blog functionality to CWIS.";
        $this->Author = "Internet Scout";
        $this->Url = "http://scout.wisc.edu/cwis/";
        $this->Email = "scout@scout.wisc.edu";
        $this->Requires = array(
            "CWISCore" => "3.1.0",
            "MetricsRecorder" => "1.2.4",
            "MetricsReporter" => "0.9.2",
            "SocialMedia" => "1.1.0",
            "Mailer" => "1.0.6");
        $this->EnabledByDefault = FALSE;
        $this->Instructions = '
            <p>
              <b>Note:</b> The blog entry metadata fields can be configured on
              the <a href="index.php?P=DBEditor">Metadata Field Editor</i></a>
              page once the plugin is installed.
            </p>
            <p>
              Users may subscribe to blog entry notifications and notifications
              may be sent once a notification e-mail template has been chosen
              below. E-mail templates are created on the
              <a href="index.php?P=P_Mailer_EditMessageTemplates"><i>Email
              Message Templates</i></a> page. A few blog-specific keywords can
              be used in the template in addition to the default mail keywords
              available. They are:
            </p>
            <dl class="cw-list cw-list-dictionary cw-content-specialstrings">
              <dt>X-BLOG:UNSUBSCRIBE-X</dt>
              <dd>URL to the page to unsubscribe from blog entry notifications</dd>
              <dt>X-BLOG:BLOGNAME-X</dt>
              <dd>name of the blog</dd>
              <dt>X-BLOG:BLOGDESCRIPTION-X</dt>
              <dd>description of the blog</dd>
              <dt>X-BLOG:BLOGURL-X</dt>
              <dd>URL to the blog landing page</dd>
              <dt>X-BLOG:ENTRYURL-X</dt>
              <dd>URL to the blog entry</dd>
              <dt>X-BLOG:ENTRYAUTHOR-X</dt>
              <dd>the author\'s real name if available and username otherwise</dd>
              <dt>X-BLOG:TEASER-X</dt>
              <dd>blog entry teaser, possibly containing HTML</dd>
              <dt>X-BLOG:TEASERTEXT-X</dt>
              <dd>blog entry teaser in plain text</dd>
            </dl>';
    }

    /**
    * Set up plugin configuration options.
    * @return NULL if configuration setup succeeded, otherwise a string with
    *       an error message indicating why config setup failed.
    */
    public function SetUpConfigOptions()
    {
        $this->CfgSetup["BlogName"] = array(
                "Type" => "Text",
                "Label" => "Blog Name",
                "Help" => "The name of the blog.");

        $this->CfgSetup["BlogDescription"] = array(
                "Type" => "Paragraph",
                "Label" => "Blog Description",
                "Help" => "A description of the blog.");

        $this->CfgSetup["EnableComments"] = array(
                "Type" => "Flag",
                "Label" => "Enable Comments",
                "Help" => "Enable user comments for blog entries.",
                "OnLabel" => "Yes",
                "OffLabel" => "No");

        $this->CfgSetup["ShowAuthor"] = array(
                "Type" => "Flag",
                "Label" => "Show Author",
                "Help" => "Show the author of blog entries when displaying them.",
                "OnLabel" => "Yes",
                "OffLabel" => "No");

        $this->CfgSetup["MaxTeaserLength"] = array(
                "Type" => "Number",
                "Label" => "Maximum Teaser Length",
                "Help" => "The maximum length of the teaser in number of characters.");

        $this->CfgSetup["EntriesPerPage"] = array(
                "Type" => "Option",
                "Label" => "Entries Per Page",
                "Help" => "The number of blog entries to display in the blog at once.",
                "Options" => array(
                        5 => 5,
                        10 => 10,
                        25 => 25));

        $this->CfgSetup["CleanUrlPrefix"] = array(
                "Type" => "Text",
                "Label" => "Clean URL Prefix",
                "Help" => "The prefix for the clean URLs for the blog.");

        $this->CfgSetup["ViewMetricsPrivs"] = array(
                "Type" => "Privileges",
                "Label" => "Privileges Required to View Metrics",
                "Help" => "The user privileges required to view blog entry metrics.",
                "AllowMultiple" => TRUE);

        # get the mailer template options
        $Mailer = $GLOBALS["G_PluginManager"]->GetPlugin("Mailer");
        $MailerTemplates = $Mailer->GetTemplateList();
        $TemplateOptions = array(-1 => "--") + $Mailer->GetTemplateList();
        $this->CfgSetup["NotificationTemplate"] = array(
                "Type" => "Option",
                "Label" => "Notification E-mail Template",
                "Help" => "The Mailer template to use for notifications.",
                "Default" => -1,
                "Options" => $TemplateOptions);

        $this->CfgSetup["NotificationLoginPrompt"] = array(
                "Type" => "Text",
                "Label" => "Notification Login Prompt",
                "Help" => "Text to display when asking users to log in so"
                        ." they can subscribe to notifications.",
                "Default" => "Please log in to subscribe to notifications"
                        ." of new blog posts.");

        return NULL;
    }

    /**
    * Startup initialization for plugin.
    * @return NULL if initialization was successful, otherwise a string
    *       containing an error message indicating why initialization failed.
    */
    public function Initialize()
    {
        $CleanUrlPrefix = $this->CleanUrlPrefix();
        $RegexCleanUrlPrefix = preg_quote($CleanUrlPrefix);

        # clean URL for viewing a single blog entry
        $GLOBALS["AF"]->AddCleanUrl(
            "%^".$RegexCleanUrlPrefix."/([0-9]+)(|/.+)%",
            "P_Blog_Entry",
            array("EntryId" => "$1"),
            array($this, "CleanUrlTemplate"));

        # clean URL for viewing all blog entries
        $GLOBALS["AF"]->AddCleanUrl(
            "%^".$RegexCleanUrlPrefix."/?$%",
            "P_Blog_Entries",
            array(),
            $CleanUrlPrefix."/");

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

        # report success
        return NULL;
    }

    /**
    * Install this plugin.
    * @return Returns NULL if everything went OK or an error message otherwise.
    */
    public function Install()
    {
        # setup the default privileges for authoring and editing
        $DefaultPrivs = new PrivilegeSet();
        $DefaultPrivs->AddPrivilege(PRIV_NEWSADMIN);
        $DefaultPrivs->AddPrivilege(PRIV_SYSADMIN);

        # set the default blog name
        $this->ConfigSetting("BlogName", $GLOBALS["SysConfig"]->PortalName() . " Blog");

        # use a blank blog description by default
        $this->ConfigSetting("BlogDescription", "");

        # disable comments by default
        $this->ConfigSetting("EnableComments", FALSE);

        # show the author of a blog entry by default
        $this->ConfigSetting("ShowAuthor", TRUE);

        # use 1200 as the default max teaser length
        $this->ConfigSetting("MaxTeaserLength", 1200);

        # show ten blog entries per page by default
        $this->ConfigSetting("EntriesPerPage", 10);

        # use "blog" as the default clean URL prefix
        $this->ConfigSetting("CleanUrlPrefix", "blog");

        # use the default privs as the default privs to view metrics
        $this->ConfigSetting("ViewMetricsPrivs", array(PRIV_NEWSADMIN, PRIV_SYSADMIN));

        # create a new metadata schema and save its ID
        $Schema = MetadataSchema::Create(
                "Blog",
                $DefaultPrivs,
                $DefaultPrivs,
                new PrivilegeSet(),
                "index.php?P=P_Blog_Entry&EntryId=\$ID",
                "Blog Entry");
        $this->ConfigSetting("MetadataSchemaId", $Schema->Id());

        # the names of the fields to create
        $FieldsToCreate = array(
            self::TITLE_FIELD_NAME,
            self::BODY_FIELD_NAME,
            self::CREATION_DATE_FIELD_NAME,
            self::MODIFICATION_DATE_FIELD_NAME,
            self::PUBLICATION_DATE_FIELD_NAME,
            self::AUTHOR_FIELD_NAME,
            self::EDITOR_FIELD_NAME,
            self::CATEGORIES_FIELD_NAME,
            self::IMAGE_FIELD_NAME,
            self::NOTIFICATIONS_FIELD_NAME);

        # create each field from its XML representation
        foreach ($FieldsToCreate as $FieldToCreate)
        {
            $Xml = $this->GetFieldXml($FieldToCreate);

            # if fetching the XML failed
            if (is_null($Xml))
            {
                return "Could not get the XML representation of the "
                    . $FieldToCreate . " field.";
            }

            # create the field from its XML representation
            $Field = $Schema->AddFieldFromXml($Xml);

            # if the field couldn't be created
            if (!($Field instanceof MetadataField))
            {
                return "Could not create the " . $FieldToCreate . " field.";
            }
        }

        $UserSchema = new MetadataSchema(MetadataSchema::SCHEMAID_USER);

        # the names of the user schema fields to create
        $UserSchemaFieldsToCreate = array(
            "Subscribe to Blog Entries");

        # create each field from its XML representation
        foreach ($UserSchemaFieldsToCreate as $FieldToCreate)
        {
            $Xml = $this->GetFieldXml($FieldToCreate);

            # if fetching the XML failed
            if (is_null($Xml))
            {
                return "Could not get the XML representation of the "
                    . $FieldToCreate . " field.";
            }

            # create the field from its XML representation
            $Field = $UserSchema->AddFieldFromXml($Xml);

            # if the field couldn't be created
            if (!($Field instanceof MetadataField))
            {
                return "Could not create the " . $FieldToCreate . " field.";
            }
        }

        # disable the subscribe field until an notification e-mail template is
        # selected
        $SubscribeField = $UserSchema->GetFieldByName("Subscribe to Blog Entries");
        $SubscribeField->Enabled(FALSE);

        # change the subscribe field label to match the blog name
        $SubscribeField->Label("Subscribe to ".$this->ConfigSetting("BlogName"));

        # get the file that holds the default categories
        $DefaultCategoriesFile = @fopen($this->GetDefaultCategoriesFile(), "r");

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

        # get the categories
        $Categories = @fgetcsv($DefaultCategoriesFile);

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

        $CategoriesField = $Schema->GetFieldByName(self::CATEGORIES_FIELD_NAME);

        # add each category
        foreach ($Categories as $Category)
        {
            $ControlledName = new ControlledName(
                NULL,
                $Category,
                $CategoriesField->Id());
        }

        # close the default category file
        @fclose($DefaultCategoriesFile);

        # update the editing privileges now that the fields have been created
        $EditingPrivs = $DefaultPrivs;
        $EditingPrivs->AddCondition($Schema->GetFieldByName(self::AUTHOR_FIELD_NAME));
        $Schema->EditingPrivileges($EditingPrivs);
    }

    /**
    * Upgrade from a previous version.
    * @param $PreviousVersion Previous version of the plugin.
    * @return Returns NULL on success and an error message otherwise.
    */
    public function Upgrade($PreviousVersion)
    {
        # upgrade from versions < 1.0.1 to 1.0.1
        if (version_compare($PreviousVersion, "1.0.1", "<"))
        {
            $Database = new Database;
            $SchemaId = $this->GetSchemaId();

            # first, see if the schema attributes already exist
            $Database->Query("
                SELECT * FROM MetadataSchemas
                WHERE SchemaId = '".intval($SchemaId)."'");

            # if the schema attributes don't already exist
            if (!$Database->NumRowsSelected())
            {
                $AuthorPriv = array(PRIV_NEWSADMIN, PRIV_SYSADMIN);
                $Result = $Database->Query("
                    INSERT INTO MetadataSchemas
                    (SchemaId, Name, AuthoringPrivileges, ViewPage) VALUES (
                    '".intval($SchemaId)."',
                    'Blog',
                    '".mysql_escape_string(serialize($AuthorPriv))."',
                    'index.php?P=P_Blog_Entry&EntryId=\$ID')");

                # if the upgrade failed
                if ($Result === FALSE)
                {
                    return "Could not add the metadata schema attributes";
                }
            }
        }

        # upgrade from versions < 1.0.2 to 1.0.2
        if (version_compare($PreviousVersion, "1.0.2", "<"))
        {
            # be absolutely sure the privileges are correct
            $AuthoringPrivs = new PrivilegeSet();
            $AuthoringPrivs->AddPrivilege(PRIV_NEWSADMIN);
            $AuthoringPrivs->AddPrivilege(PRIV_SYSADMIN);
            $MetricsPrivs = array(PRIV_NEWSADMIN, PRIV_SYSADMIN);
            $Schema = new MetadataSchema($this->GetSchemaId());
            $Schema->AuthoringPrivileges($AuthoringPrivs);
            $this->ConfigSetting("ViewMetricsPrivs", $MetricsPrivs);
        }

        # upgrade from versions < 1.0.3 to 1.0.3
        if (version_compare($PreviousVersion, "1.0.3", "<"))
        {
            $Schema = new MetadataSchema($this->GetSchemaId());

            # setup the default privileges for authoring and editing
            $DefaultPrivs = new PrivilegeSet();
            $DefaultPrivs->AddPrivilege(PRIV_NEWSADMIN);
            $DefaultPrivs->AddPrivilege(PRIV_SYSADMIN);

            # the authoring and viewing privileges are the defaults
            $Schema->AuthoringPrivileges($DefaultPrivs);
            $Schema->ViewingPrivileges($DefaultPrivs);

            # the editing privileges are a bit different
            $EditingPrivs = $DefaultPrivs;
            $EditingPrivs->AddCondition($Schema->GetFieldByName(self::AUTHOR_FIELD_NAME));
            $Schema->EditingPrivileges($EditingPrivs);

            # set defaults for the view metrics privileges
            $this->ConfigSetting("ViewMetricsPrivs", array(PRIV_NEWSADMIN, PRIV_SYSADMIN));
        }

        # upgrade to 1.0.4
        if (version_compare($PreviousVersion, "1.0.4", "<"))
        {
            $Schema = new MetadataSchema($this->GetSchemaId());

            # setup the privileges for viewing
            $ViewingPrivs = new PrivilegeSet();
            $ViewingPrivs->AddPrivilege(PRIV_NEWSADMIN);
            $ViewingPrivs->AddPrivilege(PRIV_SYSADMIN);

            # set the viewing privileges
            $Schema->ViewingPrivileges($ViewingPrivs);
        }

        # upgrade to 1.0.6
        if (version_compare($PreviousVersion, "1.0.6", "<"))
        {
            $Schema = new MetadataSchema($this->GetSchemaId());

            # get the publication date field
            $PublicationDate = $Schema->GetFieldByName(self::PUBLICATION_DATE_FIELD_NAME);

            # setup the privileges for viewing
            $ViewingPrivs = new PrivilegeSet();
            $ViewingPrivs->AddPrivilege(PRIV_NEWSADMIN);
            $ViewingPrivs->AddPrivilege(PRIV_SYSADMIN);
            $ViewingPrivs->AddCondition($PublicationDate, "now", "<");

            # set the viewing privileges
            $Schema->ViewingPrivileges($ViewingPrivs);
        }

        # upgrade to 1.0.7
        if (version_compare($PreviousVersion, "1.0.7", "<"))
        {
            $Schema = new MetadataSchema($this->GetSchemaId());

            # try to create the notifications field if necessary
            if (!$Schema->FieldExists(self::NOTIFICATIONS_FIELD_NAME))
            {
                $Xml = $this->GetFieldXml(self::NOTIFICATIONS_FIELD_NAME);

                # if fetching the XML failed
                if (is_null($Xml))
                {
                    return "Could not get the XML representation of the "
                        . self::NOTIFICATIONS_FIELD_NAME . " field.";
                }

                # create the field from its XML representation
                $Field = $Schema->AddFieldFromXml($Xml);

                # if the field couldn't be created
                if (!($Field instanceof MetadataField))
                {
                    return "Could not create the " . self::NOTIFICATIONS_FIELD_NAME
                        . " field.";

                }
            }

            $UserSchema = new MetadataSchema(MetadataSchema::SCHEMAID_USER);

            # try to create the subscription field if necessary
            if (!$UserSchema->FieldExists("Subscribe to Blog Entries"))
            {
                $Xml = $this->GetFieldXml("Subscribe to Blog Entries");

                # if fetching the XML failed
                if (is_null($Xml))
                {
                    return "Could not get the XML representation of the "
                        ." Subscribe to Blog Entries field.";
                }

                # create the field from its XML representation
                $Field = $UserSchema->AddFieldFromXml($Xml);

                # if the field couldn't be created
                if (!($Field instanceof MetadataField))
                {
                    return "Could not create the Subscribe to Blog Entries"
                        ." field.";
                }

                # disable the subscribe field until an notification e-mail template is
                # selected
                $Field->Enabled(FALSE);
            }
        }

        # upgrade to 1.0.8
        if (version_compare($PreviousVersion, "1.0.8", "<"))
        {
            $UserSchema = new MetadataSchema(MetadataSchema::SCHEMAID_USER);
            $SubscribeField = $UserSchema->GetFieldByName("Subscribe to Blog Entries");
            $BlogName = $this->ConfigSetting("BlogName");

            # if a non-blank blog name is available
            if (strlen(trim($BlogName)))
            {
                # change the subscribe field's label to reflect the blog name
                $SubscribeField->Label("Subscribe to ".$BlogName);
            }

            # otherwise clear the label
            else
            {
                $SubscribeField->Label(NULL);
            }
        }

        # upgrade to 1.0.9
        if (version_compare($PreviousVersion, "1.0.9", "<"))
        {
            # set the resource name for the blog schema
            $Schema = new MetadataSchema($this->GetSchemaId());
            $Schema->ResourceName("Blog Entry");
        }

        # upgrade to 1.0.10
        if (version_compare($PreviousVersion, "1.0.10", "<"))
        {
            # set the notification login prompt
            $this->ConfigSetting("Please log in to subscribe to notifications"
                    ." of new blog posts.");
        }
    }

    /**
    * Uninstall this plugin.
    * @return Returns NULL if everything went OK or an error message otherwise.
    */
    public function Uninstall()
    {
        $Schema = new MetadataSchema($this->GetSchemaId());
        $ResourceFactory = new ResourceFactory($this->GetSchemaId());

        # delete each resource, including temp ones
        foreach ($ResourceFactory->GetItemIds(NULL, TRUE) as $ResourceId)
        {
            $Resource = new Resource($ResourceId);
            $Resource->Delete();
        }

        # delete each field, including disabled and temp fields
        foreach ($Schema->GetFields(NULL, NULL, TRUE, TRUE) as $Field)
        {
            $Schema->DropField($Field->Id());
        }

        # delete each metadata field order associated with the schema
        foreach (MetadataFieldOrder::GetOrdersForSchema($Schema) as $Order)
        {
            $Order->Delete();
        }

        # remove the subscription field from the user schema
        $UserSchema = new MetadataSchema(MetadataSchema::SCHEMAID_USER);
        $SubscribeField = $UserSchema->GetFieldByName("Subscribe to Blog Entries");
        $UserSchema->DropField($SubscribeField->Id());
    }

    /**
    * 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" => "InHtmlHeader",
            "EVENT_MODIFY_PRIMARY_NAV" => "AddPrimaryNavLinks",
            "EVENT_MODIFY_SECONDARY_NAV" => "AddSecondaryNavLinks",
            "EVENT_PLUGIN_CONFIG_CHANGE" => "PluginConfigChange",
            "EVENT_RESOURCE_CREATE" => "ResourceCreated",
            "EVENT_RESOURCE_DUPLICATE" => "ResourceDuplicated",
            "EVENT_RESOURCE_ADD" => "ResourceEdited",
            "EVENT_RESOURCE_MODIFY" => "ResourceEdited");
    }

    /**
    * Callback for constructing clean URLs to be inserted by the application
    * framework when more than regular expression replacement is required.
    * This method is passed to ApplicationFramework::AddCleanURL().
    * @param array $Matches Array of matches from preg_replace().
    * @param string $Pattern Original pattern for clean URL mapping.
    * @param string $Page Page name for clean URL mapping.
    * @param string $SearchPattern Full pattern passed to preg_replace().
    * @return string Replacement to be inserted in place of match.
    */
    public function CleanUrlTemplate($Matches, $Pattern, $Page, $SearchPattern)
    {
        if ($Page == "P_Blog_Entry")
        {
            # if no resource/classification ID found
            if (count($Matches) <= 2)
            {
                # return match unchanged
                return $Matches[0];
            }

            # get the blog entry from the matched ID
            $Entry = new Blog_Entry($Matches[2]);

            # return the replacement
            return "href=\"".defaulthtmlentities($Entry->EntryUrl())."\"";
        }

        # return match unchanged
        return $Matches[0];
    }

    /**
    * Print stylesheet and Javascript elements in the page header.
    */
    public function InHtmlHeader()
    {
        $PageName = $GLOBALS["AF"]->GetPageName();
        $BaseUrl = $GLOBALS["AF"]->BaseUrl();

        # only add the stylesheet for Blog pages
        if (!preg_match('/^P_Blog_/', $PageName))
        {
            return;
        }

?>
  <link rel="stylesheet" type="text/css" href="<?PHP $GLOBALS["AF"]->PUIFile("style.css"); ?>" />
<?PHP
    }

    /**
    * Add administration links for the plugin to the primary navigation links.
    * @param array $NavItems Existing nav items.
    * @return Returns the nav items, possibly edited.
    */
    public function AddPrimaryNavLinks($NavItems)
    {
        # add a link to the blog
        $NavItems = $this->InsertNavItemBefore(
            $NavItems,
            "Blog",
            "index.php?P=P_Blog_Entries",
            "About");

        return array("NavItems" => $NavItems);
    }

    /**
    * Add administration links for the plugin to the sidebar.
    * @param array $NavItems Existing nav items.
    * @return Returns the nav items, possibly edited.
    */
    public function AddSecondaryNavLinks($NavItems)
    {
        # add a link to the page to create a new blog entry if the user can
        # author blog entries
        if ($this->UserCanAuthorBlogEntries($GLOBALS["G_User"]))
        {
            $SafeSchemaId = urlencode($this->GetSchemaId());
            $NavItems = $this->InsertNavItemBefore(
                $NavItems,
                "Add New Blog Entry",
                "index.php?P=EditResource&ID=NEW&SC=".$SafeSchemaId,
                "Administration");
        }

        # add a link to the blog entry list if the user can edit blog entries
        if ($this->UserCanEditBlogEntries($GLOBALS["G_User"]))
        {
            $NavItems = $this->InsertNavItemBefore(
                $NavItems,
                "List Blog Entries",
                "index.php?P=P_Blog_ListEntries",
                "Administration");
        }

        return array("NavItems" => $NavItems);
    }

    /**
    * Handle plugin configuration changes.
    * @param string $PluginName Name of the plugin that has changed.
    * @param string $ConfigSetting Name of the setting that has change.
    * @param mixed $OldValue The old value of the setting.
    * @param mixed $NewValue The new value of the setting.
    */
    public function PluginConfigChange($PluginName, $ConfigSetting, $OldValue, $NewValue)
    {
        # only worried about changes to the blog plugin
        if ($PluginName != "Blog")
        {
            return;
        }

        $UserSchema = new MetadataSchema(MetadataSchema::SCHEMAID_USER);

        if ($ConfigSetting == "NotificationTemplate")
        {
            # enable/disable the subscribe field if only if a valid mail
            # template has been chosen
            $SubscribeField = $UserSchema->GetFieldByName("Subscribe to Blog Entries");
            $SubscribeField->Enabled(is_numeric($NewValue) && $NewValue != -1);
        }

        if ($ConfigSetting == "BlogName")
        {
            $SubscribeField = $UserSchema->GetFieldByName("Subscribe to Blog Entries");

            # if a non-blank name is given
            if (strlen(trim($NewValue)))
            {
                # change the subscribe field's label to reflect the blog name
                $SubscribeField->Label("Subscribe to ".$NewValue);
            }

            # otherwise clear the label
            else
            {
                $SubscribeField->Label(NULL);
            }
        }
   }

    /**
    * Callback executed whenever a resource is created.
    * @param Resource $Resource Newly-created resource.
    */
    public function ResourceCreated(Resource $Resource)
    {
        # only concerned with blog entry resources
        if (!$this->IsBlogEntry($Resource))
        {
            return;
        }

        # set the author field
        $Resource->Set(self::AUTHOR_FIELD_NAME, $GLOBALS["G_User"]->Id());
    }

    /**
    * Callback executed whenever a resource is duplicated.
    * @param Resource $Resource Just-duplicated resource.
    */
    public function ResourceDuplicated(Resource $Resource)
    {
        # only concerned with blog entry resources
        if (!$this->IsBlogEntry($Resource))
        {
            return;
        }

        # set the author, editor, and notications sent fields
        $Resource->Set(self::AUTHOR_FIELD_NAME, $GLOBALS["G_User"]->Id());
        $Resource->Set(self::EDITOR_FIELD_NAME, $GLOBALS["G_User"]->Id());
        $Resource->Set(self::NOTIFICATIONS_FIELD_NAME, FALSE);
    }

    /**
    * Callback executed whenever a resource is edited (or added).
    * @param Resource $Resource Just-modified resource.
    */
    public function ResourceEdited(Resource $Resource)
    {
        # only concerned with blog entry resources
        if (!$this->IsBlogEntry($Resource))
        {
            return;
        }

        # set the editor field
        $Resource->Set(self::EDITOR_FIELD_NAME, $GLOBALS["G_User"]->Id());
    }

    /**
    * Get the blog name.
    * @return Returns the blog name.
    */
    public function BlogName()
    {
        return $this->ConfigSetting("BlogName");
    }

    /**
    * Get the blog description.
    * @return Returns the blog description.
    */
    public function BlogDescription()
    {
        return $this->ConfigSetting("BlogDescription");
    }

    /**
    * Get whether comments are enabled.
    * @return Returns TRUE if comments are enabled and FALSE otherwise.
    */
    public function EnableComments()
    {
        return $this->ConfigSetting("EnableComments");
    }

    /**
    * Get whether the author should be displayed with blog entries.
    * @return Returns TRUE if the author should be displayed with blog entries
    *       and FALSE otherwise.
    */
    public function ShowAuthor()
    {
        return $this->ConfigSetting("ShowAuthor");
    }

    /**
    * Get the maximum length of the teaser in number of characters.
    * @return Returns the maximum length of the teaser.
    */
    public function MaxTeaserLength()
    {
        return $this->ConfigSetting("MaxTeaserLength");
    }

    /**
    * Get the number of blog entries to display at once in the blog.
    * @return Returns the number of blog entries to display at once in the blog.
    */
    public function EntriesPerPage()
    {
        return $this->ConfigSetting("EntriesPerPage");
    }

    /**
    * Get the clean URL prefix.
    * @return Returns the clean URL prefix.
    */
    public function CleanUrlPrefix()
    {
        return $this->ConfigSetting("CleanUrlPrefix");
    }

    /**
    * Get the schema ID associated with the blog entry metadata schema.
    * @return Returns the schema ID of the blog entry metadata schema.
    */
    public function GetSchemaId()
    {
        return $this->ConfigSetting("MetadataSchemaId");
    }

    /**
    * Record a blog entry view with the Metrics Recorder plugin.
    * @param Resource $Entry Blog entry.
    */
    public function RecordBlogEntryView(Resource $Entry)
    {
        # make sure the blog entry is valid before recording the event
        if ($Entry->Status() !== 1)
        {
            return;
        }

        # record the event
        $GLOBALS["G_PluginManager"]->GetPlugin("MetricsRecorder")->RecordEvent(
            "Blog",
            "ViewEntry",
            $Entry->Id());
    }

    /**
    * Determine if a resource is also a blog entry.
    * @param Resource $Resource Resource to check.
    * @return Returns TRUE if the resources is also a blog entry.
    */
    public function IsBlogEntry(Resource $Resource)
    {
        return $Resource->SchemaId() == $this->GetSchemaId();
    }

    /**
    * Get all blog entries.
    * @return Returns an array of all blog entries.
    */
    public function GetBlogEntries()
    {
        $Factory = new ResourceFactory($this->GetSchemaId());
        $Entries = array();

        # transform IDs to blog entry objects
        foreach ($Factory->GetItemIds() as $ItemId)
        {
            $Entries[$ItemId] = new Blog_Entry($ItemId);
        }

        return $Entries;
    }

    /**
    * Get all published blog entries.
    * @return Returns an array of all published blog entries.
    */
    public function GetPublishedBlogEntries()
    {
        $Schema = new MetadataSchema($this->GetSchemaId());
        $Factory = new ResourceFactory($Schema->Id());
        $PublicationField = $Schema->GetFieldByName(self::PUBLICATION_DATE_FIELD_NAME);
        $DBFieldName = $PublicationField->DBFieldName();
        $Entries = array();
        $Ids = $Factory->GetItemIds(
            $DBFieldName." < '".date("Y-m-d H:i:s")."'",
            FALSE,
            $DBFieldName,
            FALSE);

        # transform IDs to blog entry objects
        foreach ($Ids as $ItemId)
        {
            $Entries[$ItemId] = new Blog_Entry($ItemId);
        }

        return $Entries;
    }

    /**
    * Get the latest blog entry publication date.
    * @return Returns the latest blog entry publication date or NULL if there
    *      isn't one.
    */
    public function GetLatestPublicationDate()
    {
        $Schema = new MetadataSchema($this->GetSchemaId());
        $PublicationField = $Schema->GetFieldByName(self::PUBLICATION_DATE_FIELD_NAME);
        $DBFieldName = $PublicationField->DBFieldName();
        $Database = new Database();

        $Date = $Database->Query("
            SELECT MAX(".$DBFieldName.") AS LastChangeDate FROM Resources
            WHERE SchemaId = '".addslashes($this->GetSchemaId())."'",
            "LastChangeDate");

        return $Date ? $Date : NULL;
    }

    /**
    * Get the URL to the blog relative to the CWIS root.
    * @param array $Get Optional GET parameters to add.
    * @param string $Fragment Optional fragment ID to add.
    * @return Returns the URL to the blog relative to the CWIS root.
    */
    public function BlogUrl(array $Get=array(), $Fragment=NULL)
    {
        # if clean URLs are available
        if ($GLOBALS["AF"]->HtaccessSupport())
        {
            # base part of the URL
            $Url = $this->CleanUrlPrefix() . "/";
        }

        # clean URLs aren't available
        else
        {
            # base part of the URL
            $Url = "index.php";

            # add the page to the GET parameters
            $Get["P"] = "P_Blog_Entries";
        }

        # tack on the GET parameters, if necessary
        if (count($Get))
        {
            $Url .= "?" . http_build_query($Get);
        }

        # tack on the fragment identifier, if necessary
        if (!is_null($Fragment))
        {
            $Url .= "#" . urlencode($Fragment);
        }

        return $Url;
    }

    /**
    * Get the metrics for a blog entry.
    * @param Blog_Entry $Entry Blog entry for which to get metrics.
    * @return Returns an array of metrics. Keys are "Views", "Shares/Email",
    *     "Shares/Facebook", "Shares/Twitter", "Shares/LinkedIn", and
    *     "Shares/Google+".
    */
    public function GetBlogEntryMetrics(Blog_Entry $Entry)
    {
        # get the metrics plugins
        $Recorder = $GLOBALS["G_PluginManager"]->GetPlugin("MetricsRecorder");
        $Reporter = $GLOBALS["G_PluginManager"]->GetPlugin("MetricsReporter");

        # get the privileges to exclude
        $Exclude = $Reporter->ConfigSetting("PrivsToExcludeFromCounts");

        # get the view metrics
        $Metrics["Views"] = $Recorder->GetEventData(
            "Blog",
            "ViewEntry",
            NULL, NULL, NULL,
            $Entry->Id(),
            NULL,
            $Exclude);

        # get metrics for shares via e-mail
        $Metrics["Shares/Email"] = $Recorder->GetEventData(
            "SocialMedia",
            "ShareResource",
            NULL, NULL, NULL,
            $Entry->Id(),
            SocialMedia::SITE_EMAIL,
            $Exclude);

        # get metrics for shares via Facebook
        $Metrics["Shares/Facebook"] = $Recorder->GetEventData(
            "SocialMedia",
            "ShareResource",
            NULL, NULL, NULL,
            $Entry->Id(),
            SocialMedia::SITE_FACEBOOK,
            $Exclude);

        # get metrics for shares via Twitter
        $Metrics["Shares/Twitter"] = $Recorder->GetEventData(
            "SocialMedia",
            "ShareResource",
            NULL, NULL, NULL,
            $Entry->Id(),
            SocialMedia::SITE_TWITTER,
            $Exclude);

        # get metrics for shares via LinkedIn
        $Metrics["Shares/LinkedIn"] = $Recorder->GetEventData(
            "SocialMedia",
            "ShareResource",
            NULL, NULL, NULL,
            $Entry->Id(),
            SocialMedia::SITE_LINKEDIN,
            $Exclude);

        # get metrics for shares via Google+
        $Metrics["Shares/Google+"] = $Recorder->GetEventData(
            "SocialMedia",
            "ShareResource",
            NULL, NULL, NULL,
            $Entry->Id(),
            SocialMedia::SITE_GOOGLEPLUS,
            $Exclude);

        return $Metrics;
    }

    /**
    * Print share buttons for a blog entry.
    * @param int $Size The size of the share buttons.
    * @param string $Color The color of the share buttons. (NULL, "grey", or
    *      "maroon").
    * @param Blog_Entry $Entry Blog entry.
    */
    public function PrintShareButtonsForEntry(Blog_Entry $Entry, $Size=24, $Color=NULL)
    {
        $SafeId = defaulthtmlentities(urlencode($Entry->Id()));
        $SafeTitle = defaulthtmlentities(rawurlencode(strip_tags($Entry->Title())));
        $SafeUrl = defaulthtmlentities(rawurlencode($GLOBALS["AF"]->BaseUrl().$Entry->EntryUrl()));
        $FileSuffix = $Size;

        # add the color if given
        if (!is_null($Color))
        {
            $FileSuffix .= "-" . strtolower($Color);
        }

        # construct the base URL for share URLS
        $SafeBaseUrl = "index.php?P=P_SocialMedia_ShareResource";
        $SafeBaseUrl .= "&amp;ResourceId=" . $SafeId;

        # get the share metrics URL for e-mail
        $EmailShareUrl = OurBaseUrl()."index.php?";
        $EmailShareUrl .= http_build_query(array(
            "P" => "P_SocialMedia_ShareResource",
            "ResourceId" => $Entry->Id(),
            "Site" => "em"));
?>
  <ul class="cw-list cw-list-noindent cw-list-nobullets cw-list-horizontal blog-share">
    <li><a title="Share this blog entry via e-mail" onclick="setTimeout(function(){jQuery.get('<?PHP print $EmailShareUrl; ?>');},250);" href="mailto:?to=&amp;subject=<?PHP print $SafeTitle; ?>&amp;body=<?PHP print $SafeTitle; ?>:%0D%0A<?PHP print $SafeUrl; ?>"><img src="<?PHP $GLOBALS["AF"]->PUIFile("email_".$FileSuffix.".png"); ?>" alt="E-mail" /></a></li>
    <li><a title="Share this blog entry via Facebook" href="<?PHP print $SafeBaseUrl; ?>&amp;Site=fb"><img src="<?PHP $GLOBALS["AF"]->PUIFile("facebook_".$FileSuffix.".png"); ?>" alt="Facebook" /></a></li>
    <li><a title="Share this blog entry via Twitter" href="<?PHP print $SafeBaseUrl; ?>&amp;Site=tw"><img src="<?PHP $GLOBALS["AF"]->PUIFile("twitter_".$FileSuffix.".png"); ?>" alt="Twitter" /></a></li>
    <li><a title="Share this blog entry via LinkedIn" href="<?PHP print $SafeBaseUrl; ?>&amp;Site=li"><img src="<?PHP $GLOBALS["AF"]->PUIFile("linkedin_".$FileSuffix.".png"); ?>" alt="LinkedIn" /></a></li>
    <li><a title="Share this blog entry via Google+" href="<?PHP print $SafeBaseUrl; ?>&amp;Site=gp"><img src="<?PHP $GLOBALS["AF"]->PUIFile("googleplus_".$FileSuffix.".png"); ?>" alt="Google+" /></a></li>
  </ul>
<?PHP
    }

    /**
    * Determine if a user can author blog entries.
    * @param CWUser $User User to check.
    * @return Returns TRUE if the user can author blog entries.
    */
    public function UserCanAuthorBlogEntries(CWUser $User)
    {
        $Schema = new MetadataSchema($this->GetSchemaId());
        return $User->Privileges()->IsGreaterThan($Schema->AuthoringPrivileges());
    }

    /**
    * Determine if a user can edit blog entries.
    * @param CWUser $User User to check.
    * @param Blog_Entry $Entry Optional blog entry to check for authorship.
    * @return Returns TRUE if the user can edit blog entries.
    */
    public function UserCanEditBlogEntries(CWUser $User, Blog_Entry $Entry=NULL)
    {
        if ($Entry == NULL) $Entry = PrivilegeSet::NO_RESOURCE;
        $Schema = new MetadataSchema($this->GetSchemaId());
        return $User->Privileges()->IsGreaterThan(
            $Schema->EditingPrivileges(),
            $Entry);
    }

    /**
    * Determine if a user can post comments.
    * @param CWUser $User User to check.
    * @return Returns TRUE if the user can post comments.
    */
    public function UserCanPostComment(CWUser $User)
    {
        return $User->HasPriv(PRIV_SYSADMIN, PRIV_POSTCOMMENTS);
    }

    /**
    * Determine if a user can edit a comment.
    * @param CWUser $User User to check.
    * @param Message $Comment Comment to check.
    * @return Returns TRUE if the user can edit the comment.
    */
    public function UserCanEditComment(CWUser $User, Message $Comment)
    {
        # the user must be valid
        if ($User->Status() !== U_OKAY)
        {
            return FALSE;
        }

        # anonymous users can't edit comments
        if (!$User->IsLoggedIn())
        {
            return FALSE;
        }

        # users have to be system administrators or have the privilege to add comments
        if (!$User->HasPriv(PRIV_SYSADMIN, PRIV_POSTCOMMENTS))
        {
            return FALSE;
        }

        # the user must be the owner or a system administrator
        if ($User->Id() != $Comment->PosterId() && !$User->HasPriv(PRIV_SYSADMIN))
        {
            return FALSE;
        }

        # the user can edit the comment
        return TRUE;
    }

    /**
    * Determine if a user can delete a comment.
    * @param CWUser $User User to check.
    * @param Message $Comment Comment to check.
    * @return Returns TRUE if the user can delete the comment.
    */
    public function UserCanDeleteComment(CWUser $User, Message $Comment)
    {
        # the user must be valid
        if ($User->Status() !== U_OKAY)
        {
            return FALSE;
        }

        # anonymous users can't mark delete comments
        if (!$User->IsLoggedIn())
        {
            return FALSE;
        }

        # users have to be system administrators or have the privilege to add
        # comments
        if (!$User->HasPriv(PRIV_SYSADMIN, PRIV_POSTCOMMENTS))
        {
            return FALSE;
        }

        # users have to own the comment or be system or user administrators to
        # delete comments
        if ($User->Id() != $Comment->PosterId()
            && !$User->HasPriv(PRIV_SYSADMIN, PRIV_USERADMIN))
        {
            return FALSE;
        }

        # the user can edit the comment
        return TRUE;
    }

    /**
    * Determine if a user can mark a comment as spam.
    * @param CWUser $User User to check.
    * @param Message $Comment Comment to check.
    * @return Returns TRUE if the user can mark the comment as spam.
    */
    public function UserCanMarkCommentAsSpam(CWUser $User, Message $Comment)
    {
        return $this->UserCanEditUsers($User);
    }

    /**
    * Determine if a user can edit other users.
    * @param CWUser $User User to check.
    * @return Returns TRUE if the user can edit other users.
    */
    public function UserCanEditUsers(CWUser $User)
    {
        # the user must be valid
        if ($User->Status() !== U_OKAY)
        {
            return FALSE;
        }

        # anonymous users can't mark comments as spam
        if (!$User->IsLoggedIn())
        {
            return FALSE;
        }

        # users have to be system or user administrators
        if (!$User->HasPriv(PRIV_SYSADMIN, PRIV_USERADMIN))
        {
            return FALSE;
        }

        # the user can edit other users
        return TRUE;
    }

    /**
    * Determine if a user can view unpublished blog entries.
    * @param CWUser $User User to check.
    * @param Blog_Entry $Entry Optional blog entry to check for authorship.
    * @return Returns TRUE if the user can view unpublished blog entries.
    */
    public function UserCanViewUnpublishedEntries(CWUser $User, Blog_Entry $Entry=NULL)
    {
        # the user can view unpublished entries if they can edit entries
        if ($this->UserCanEditBlogEntries($User))
        {
            return TRUE;
        }

        # if a blog entry was supplied and the user authored the entry
        if ($Entry instanceof Blog_Entry
            && $User->Id() && $Entry->Get(self::AUTHOR_FIELD_NAME) == $User->Name())
        {
            return TRUE;
        }

        # the user can't view unpublished entries
        return FALSE;
    }

    /**
    * Determine if a user can view blog entry metrics.
    * @param CWUser $User User to check.
    * @return Returns TRUE if the user can view blog entry metrics.
    */
    public function UserCanViewMetrics(CWUser $User)
    {
        return $User->HasPriv($this->ConfigSetting("ViewMetricsPrivs"));
    }

    /**
    * Check a user's notification subscription status.
    * @param CWUser $User The user for which check the subscription status.
    * @return Returns TRUE if the user is subscribed and FALSE otherwise.
    */
    public function UserIsSubscribedToNotifications(CWUser $User)
    {
        # the user must be logged in and notifications must be able to be sent
        if (!$User->IsLoggedIn() || !$this->NotificationsCouldBeSent())
        {
            return FALSE;
        }

        # get the status of the subscription field for the user
        $Resource = $User->GetResource();
        return (bool) $Resource->Get("Subscribe to Blog Entries");
    }

    /**
    * Change the notification subscription status for the given user.
    * @param CWUser $User The user for which to change the subscription.
    * @param bool $Status TRUE to subscribe and FALSE to unsubscribe.
    */
    public function ChangeNotificationSubscription(CWUser $User, $Status)
    {
        # only set the status if notifications could be sent
        if ($User->IsLoggedIn() && $this->NotificationsCouldBeSent())
        {
            $Resource = $User->GetResource();
            $Resource->Set("Subscribe to Blog Entries", (bool) $Status);
        }
    }

    /**
    * Determine if notifications could be sent out.
    * @param Blog_Entry $Entry Optional blog entry to use as context.
    * @param CWUser $User Optional user to use as context.
    * @return Returns TRUE if notifications could be sent out and FALSE
    *      otherwise.
    */
    public function NotificationsCouldBeSent(Blog_Entry $Entry=NULL, CWUser $User=NULL)
    {
        # the template has to be set
        if (!is_numeric($this->ConfigSetting("NotificationTemplate"))
            || $this->ConfigSetting("NotificationTemplate") == -1)
        {
            return FALSE;
        }

        $UserSchema = new MetadataSchema(MetadataSchema::SCHEMAID_USER);
        $SubscribeField = $UserSchema->GetFieldByName("Subscribe to Blog Entries");

        # the subscription user field must exist
        if (!($SubscribeField instanceof MetadataField))
        {
            return FALSE;
        }

        # the subscription field must be enabled
        if (!$SubscribeField->Enabled())
        {
            return FALSE;
        }

        # perform blog entry checks if given one
        if ($Entry instanceof Blog_Entry)
        {
            # the entry has to be valid
            if ($Entry->Status() !== 1)
            {
                return FALSE;
            }

            $PublicationDate = $Entry->Get(self::PUBLICATION_DATE_FIELD_NAME);

            # the blog has to be published
            if (time() < strtotime($PublicationDate))
            {
                return FALSE;
            }

            # don't allow notifications to be sent multiple times
            if ($Entry->Get(self::NOTIFICATIONS_FIELD_NAME))
            {
                return FALSE;
            }
        }

        # perform user checks if given one
        if ($User instanceof CWUser)
        {
            # the user should only be allowed to send out notifications if
            # they can edit blog entries
            if (!$this->UserCanEditBlogEntries($User, $Entry))
            {
                return FALSE;
            }
        }

        # notifications could be sent
        return TRUE;
    }

    /**
    * Send out notification e-mails to subscribers about the given blog entry.
    * This will only send out e-mails if it's appropriate to do so, i.e., the
    * user's allowed to, an e-mail template is set, etc.
    * @param Blog_Entry $Entry The entry about which to notify subscribers.
    */
    public function NotifySubscribers(Blog_Entry $Entry)
    {
        # don't send notifications if not allowed
        if (!$this->NotificationsCouldBeSent($Entry, $GLOBALS["G_User"]))
        {
            return;
        }

        $Mailer = $GLOBALS["G_PluginManager"]->GetPlugin("Mailer");
        $UserSchema = new MetadataSchema(MetadataSchema::SCHEMAID_USER);
        $SubscribeField = $UserSchema->GetFieldByName("Subscribe to Blog Entries");
        $UserField = $UserSchema->GetFieldByName("UserId");

        # query user resources directly since the search engine isn't capable
        # of it yet
        $Database = new Database();
        $Database->Query("
            SELECT `".$UserField->DBFieldName()."` AS Id FROM Resources
            WHERE `ResourceId` > 0
            AND `".$SubscribeField->DBFieldName()."` = '1'
            AND `SchemaId` = '".intval(MetadataSchema::SCHEMAID_USER)."'");
        $UserIds = $Database->FetchColumn("Id");

        $BodyField = $this->GetFieldByName(self::BODY_FIELD_NAME);

        # remove HTML from the teaser text if HTML is allowed in the body field
        if ($BodyField->AllowHTML())
        {
            $TeaserText = Email::ConvertHtmlToPlainText($Entry->Teaser());
            $TeaserText = wordwrap($TeaserText, 78);
        }

        # HTML isn't allowed so just use the teaser verbatim
        else
        {
            $TeaserText = $Entry->Teaser();
        }

        # the extra replacement tokens
        $ExtraTokens = array(
            "BLOG:UNSUBSCRIBE" => OurBaseUrl()."index.php?P=Preferences",
            "BLOG:BLOGNAME" => $this->ConfigSetting("BlogName"),
            "BLOG:BLOGDESCRIPTION" => $this->ConfigSetting("BlogDescription"),
            "BLOG:BLOGURL" => OurBaseUrl().$this->BlogUrl(),
            "BLOG:ENTRYURL" => OurBaseUrl().$Entry->EntryUrl(),
            "BLOG:ENTRYAUTHOR" => $Entry->AuthorForDisplay(),
            "BLOG:TEASER" => $Entry->Teaser(),
            "BLOG:TEASERTEXT" => $TeaserText);

        # send the notification e-mails using tasks
        $Sent = $Mailer->SendEmailUsingTasks(
            $this->ConfigSetting("NotificationTemplate"),
            $UserIds,
            $Entry,
            $ExtraTokens);

        # flag that notifications have been sent
        $Entry->Set(self::NOTIFICATIONS_FIELD_NAME, TRUE);
    }

    /**
    * Get the XML representation for a field from a file with a given path.
    * @param string $Name Name of the field of which to fetch the XML
    *      representation.
    * @return Returns the XML representation string or NULL if an error occurs.
    */
    protected function GetFieldXml($Name)
    {
        $Path = dirname(__FILE__) . "/" . self::FIELD_XML_PATH . "/" . $Name . ".xml";
        $Xml = @file_get_contents($Path);

        return $Xml !== FALSE ? $Xml : NULL;
    }

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

        return $Path;
    }

    /**
    * Get a blog field by its name.
    * @param string $Name Field name.
    * @return Returns a MetadataField object or NULL if an error occurs.
    */
    public function GetFieldByName($Name)
    {
        $Schema = new MetadataSchema($this->GetSchemaId());
        return $Schema->GetFieldByName($Name);
    }

    /**
    * Insert a new nav item before another existing nav item. The new nav item
    * will be placed at the end of the list if the nav item it should be placed
    * before doesn't exist.
    * @param array $NavItems Existing nav items.
    * @param string $ItemLabel New nav item label.
    * @param string $ItemPage New nav item page.
    * @param string $Before Label of the existing item in front of which the new
    *      nav item should be placed.
    * @return Returns the nav items with the new nav item in place.
    */
    protected function InsertNavItemBefore(array $NavItems, $ItemLabel, $ItemPage, $Before)
    {
        $Result = array();

        foreach ($NavItems as $Label => $Page)
        {
            # if the new item should be inserted before this one
            if ($Label == $Before)
            {
                break;
            }

            # move the nav item to the results
            $Result[$Label] = $Page;
            unset($NavItems[$Label]);
        }

        # add the new item
        $Result[$ItemLabel] = $ItemPage;

        # add the remaining nav items, if any
        $Result = $Result + $NavItems;

        return $Result;
    }

}
