<?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";

    /**
    * The name of the blog entry type field.
    */
    const BLOG_NAME_FIELD_NAME = "Blog Name";

    /**
    * The name of the blog subscription flag field.
    */
    const SUBSCRIPTION_FIELD_NAME = "Subscribe to Blog Entries";

    /**
    * Register information about this plugin.
    */
    public function Register()
    {
        $this->Name = "Blog";
        $this->Version = "1.0.17";
        $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["ViewMetricsPrivs"] = array(
                "Type" => "Privileges",
                "Label" => "Privileges Required to View Metrics",
                "Help" => "The user privileges required to view blog entry metrics.",
                "AllowMultiple" => TRUE);
        $this->CfgSetup["EmailNotificationBlog"] = array(
                "Type" => "Option",
                "Label" => "Blog to Send Notification Emails",
                "Help" => "The blog which subscribed users should get "
                        ."notification emails from.",
                "Options" => $this->GetAvailableBlogs() +
                        array(-1 => "(do not send email)"),
                "Default" => -1,
                "AllowMultiple" => FALSE);

        # get the mailer template options
        $Mailer = $GLOBALS["G_PluginManager"]->GetPlugin("Mailer");
        $MailerTemplates = $Mailer->GetTemplateList();
        $TemplateOptions = array(-1 => "(do not send email)")
                + $Mailer->GetTemplateList();

        $this->CfgSetup["NotificationTemplate"] = array(
                "Type" => "Option",
                "Label" => "Notification E-mail Template",
                "Help" => "The Mailer template to use for notifications of new posts.",
                "Default" => -1,
                "Options" => $TemplateOptions);

        $this->CfgSetup["SubscriptionConfirmationTemplate"] = array(
                "Type" => "Option",
                "Label" => "Subscription E-mail Template",
                "Help" => "The Mailer template for subscription notification emails.",
                "Default" => -1,
                "Options" => $TemplateOptions);

        $this->CfgSetup["UnsubscriptionConfirmationTemplate"] = array(
               "Type" => "Option",
               "Label" => "Unsubscription E-mail Template",
               "Help" => "The Mailer template for unsubscription notification emails.",
               "Default" => -1,
               "Options" => $TemplateOptions);

        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()
    {
        foreach ($this->ConfigSetting("BlogSettings") as $BlogId => $BlogSettings)
        {
            if (isset($BlogSettings["CleanUrlPrefix"]) &&
                strlen($BlogSettings["CleanUrlPrefix"])>0 )
            {
                $RegexCleanUrlPrefix = preg_quote($BlogSettings["CleanUrlPrefix"]);

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

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

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

        # set up standard field mappings
        $Schema = new MetadataSchema($this->GetSchemaId());
        $Schema->StdNameToFieldMapping("Title",
                $Schema->GetFieldIdByName(self::TITLE_FIELD_NAME));

        # add extra function dirs that we need
        # (these are listed in reverse order because each will be added to the
        #       beginning of the search list)
        $GLOBALS["AF"]->AddFunctionDirectories(array(
            "plugins/".__CLASS__."/interface/default/include/",
            "plugins/".__CLASS__."/interface/%ACTIVEUI%/include/",
            "local/plugins/".__CLASS__."/interface/default/include/",
            "local/plugins/".__CLASS__."/interface/%ACTIVEUI%/include/"));

        # add extra image dirs that we need
        # (must be added explicitly because our code gets loaded
        #       on pages other than ours)
        $GLOBALS["AF"]->AddImageDirectories(array(
            "local/plugins/".__CLASS__."/interface/%ACTIVEUI%/images/",
            "local/plugins/".__CLASS__."/interface/default/images/",
            "plugins/".__CLASS__."/interface/%ACTIVEUI%/images/",
            "plugins/".__CLASS__."/interface/default/images/"));

        # if MetricsRecorder is enabled, register our subscriber count event
        if ($GLOBALS["G_PluginManager"]->PluginEnabled("MetricsRecorder"))
        {
            $GLOBALS["G_PluginManager"]->GetPlugin("MetricsRecorder")->RegisterEventType(
                $this->Name, "NumberOfSubscribers");
        }

        # 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);

        # 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,
                $DefaultPrivs,
                "index.php?P=P_Blog_Entry&ID=\$ID",
                "Blog Entry");
        $this->ConfigSetting("MetadataSchemaId", $Schema->Id());

        # populate our new schema with fields from XML file
        if ($Schema->AddFieldsFromXmlFile(
                "plugins/".__CLASS__."/install/MetadataSchema--"
                        .__CLASS__.".xml") === FALSE)
        {
            return "Error loading Blog metadata fields from XML: ".implode(
                    " ", $Schema->ErrorMessages("AddFieldsFromXmlFile"));
        }

        # populate user schema with new fields from XML file (if any)
        $UserSchema = new MetadataSchema(MetadataSchema::SCHEMAID_USER);
        $UserFieldsFile = "plugins/".__CLASS__
                ."/install/MetadataSchema--User.xml";
        if ($UserSchema->AddFieldsFromXmlFile($UserFieldsFile) === FALSE)
        {
            return "Error loading User metadata fields from XML: ".implode(
                    " ", $UserSchema->ErrorMessages("AddFieldsFromXmlFile"));
        }

        # disable the subscribe field until an notification e-mail template is
        # selected
        $SubscribeField = $UserSchema->GetField(self::SUBSCRIPTION_FIELD_NAME);
        $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->GetField(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);

        # create ConfigSetting "BlogSettings"
        $this->ConfigSetting("BlogSettings", array());

        # create the default blog
        $DefaultBlog = $this->CreateBlog($GLOBALS["SysConfig"]->PortalName() . " Blog");
        $this->BlogSettings($DefaultBlog, $this->GetBlogConfigTemplate());
        $this->BlogSetting($DefaultBlog, "BlogName",
                $GLOBALS["SysConfig"]->PortalName() . " Blog");

        # createa a default News blog as well
        $NewsBlog = $this->CreateBlog("News");
        $this->BlogSettings($NewsBlog, $this->GetBlogConfigTemplate());
        $this->BlogSetting($NewsBlog, "BlogName", "News");

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

        $ViewingPrivs = $EditingPrivs;
        $ViewingPrivs->AddCondition(
            $Schema->GetField(self::PUBLICATION_DATE_FIELD_NAME), "now", "<");
        $Schema->ViewingPrivileges($ViewingPrivs);
    }

    /**
    * Upgrade from a previous version.
    * @param string $PreviousVersion Previous version of the plugin.
    * @return Returns NULL on success and an error message otherwise.
    */
    public function Upgrade($PreviousVersion)
    {
        # upgrade from versions < 1.0.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&ID=\$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->GetField(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->GetField(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))
            {
                if ($Schema->AddFieldsFromXmlFile(
                        "plugins/".__CLASS__."/install/MetadataSchema--"
                                .__CLASS__.".xml") === FALSE)
                {
                    return "Error loading Blog metadata fields from XML: ".implode(
                            " ", $Schema->ErrorMessages("AddFieldsFromXmlFile"));
                }
            }

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

            # try to create the subscription field if necessary
            if (!$UserSchema->FieldExists(self::SUBSCRIPTION_FIELD_NAME))
            {
                if ($Schema->AddFieldsFromXmlFile(
                        "plugins/".__CLASS__."/install/MetadataSchema--"
                                ."User.xml") === FALSE)
                {
                    return "Error loading User metadata fields from XML: ".implode(
                            " ", $Schema->ErrorMessages("AddFieldsFromXmlFile"));
                }

                # 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->GetField(self::SUBSCRIPTION_FIELD_NAME);
            $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.");
        }

        # upgrade to 1.0.14
        if (version_compare($PreviousVersion, "1.0.14", "<"))
        {
            # add new blog name field
            if ($Schema->AddFieldsFromXmlFile(
                    "plugins/".__CLASS__."/install/MetadataSchema--"
                            .__CLASS__.".xml") === FALSE)
            {
                return "Error loading Blog metadata fields from XML: ".implode(
                        " ", $Schema->ErrorMessages("AddFieldsFromXmlFile"));
            }

            # create ConfigSetting "BlogSettings"
            $this->ConfigSetting("BlogSettings", array());

            # convert current blog to a newly created blog
            $DefaultBlogId = $this->CreateBlog($this->ConfigSetting("BlogName"));
            $this->BlogSettings($DefaultBlogId, $this->GetBlogConfigTemplate());

            # copy the plugin ConfigSettings that should apply to the default blog
            # into blog settings instead
            foreach (array("BlogName", "BlogDescription", "EnableComments",
                           "ShowAuthor", "MaxTeaserLength", "EntriesPerPage",
                           "CleanUrlPrefix", "NotificationLoginPrompt") as $ToMigrate)
            {
                $this->BlogSetting($DefaultBlogId, $ToMigrate,
                    $this->ConfigSetting($ToMigrate));
            }

            # upgrade current blog entries to include Blog Name field
            $BlogNameToSet = array();
            $BlogNameToSet[$DefaultBlogId] = 1;

            $Factory = new ResourceFactory($this->GetSchemaId());
            foreach ($Factory->GetItemIds() as $Id)
            {
                $BlogEntry = new Blog_Entry($Id);
                $BlogEntry->Set(self::BLOG_NAME_FIELD_NAME, $BlogNameToSet);
            }

            # Create a new Blog for news
            $NewsBlogId = $this->CreateBlog("News");
            $this->BlogSettings($NewsBlogId, $this->GetBlogConfigTemplate());
            $this->BlogSetting($NewsBlogId, "BlogName", "News");

            # Migrate Announcements into Blog
            $BlogNameToSet = array();
            $BlogNameToSet[$NewsBlogId] = 1;

            $DB = new Database();

            # Find an admin user to be the author/editor of these announcements
            $UserId = $DB->Query(
                "SELECT MIN(UserId) AS UserId FROM APUserPrivileges "
                ."WHERE Privilege=".PRIV_SYSADMIN, "UserId");

            $DB->Query("SELECT * FROM Announcements");
            while (FALSE !== ($Record = $DB->FetchRow()))
            {
                $ToSet = array(
                    self::TITLE_FIELD_NAME => $Record["AnnouncementHeading"],
                    self::BODY_FIELD_NAME => $Record["AnnouncementText"],
                    self::PUBLICATION_DATE_FIELD_NAME => $Record["DatePosted"],
                    self::MODIFICATION_DATE_FIELD_NAME => $Record["DatePosted"],
                    self::CREATION_DATE_FIELD_NAME => $Record["DatePosted"],
                    self::BLOG_NAME_FIELD_NAME => $BlogNameToSet,
                    self::AUTHOR_FIELD_NAME => $UserId);

                $BlogNews = Resource::Create($this->GetSchemaId());
                foreach ($ToSet as $FieldName => $Value)
                {
                    $BlogNews->Set($FieldName, $Value);
                }
                $BlogNews->IsTempResource(FALSE);

                # needs to be updated after resource addition so that
                #  event hooks won't modify it
                $BlogNews->Set(self::EDITOR_FIELD_NAME, $UserId);
            }

            # drop the Announcements table
            if (FALSE === $DB->Query(
                "DROP TABLE IF EXISTS Announcements"))
            {
                return "Could not drop the old news table.";
            }
        }

        # upgrade to 1.0.15
        if (version_compare($PreviousVersion, "1.0.15", "<"))
        {
            $Schema = new MetadataSchema($this->GetSchemaId());
            $Schema->ViewPage("index.php?P=P_Blog_Entry&ID=\$ID");
        }

        # upgrade to 1.0.17
        if (version_compare($PreviousVersion, "1.0.17", "<"))
        {
            # make sure all metadata fields in our schema are permanent
            $Schema = new MetadataSchema($this->GetSchemaId());
            $Fields = $Schema->GetFields();
            foreach ($Fields as $Field)
            {
                $Field->IsTempItem(FALSE);
            }
        }
    }

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

        $Schema->Delete();

        # remove the subscription field from the user schema
        $UserSchema = new MetadataSchema(MetadataSchema::SCHEMAID_USER);
        $SubscribeField = $UserSchema->GetField(self::SUBSCRIPTION_FIELD_NAME);
        $UserSchema->DropField($SubscribeField->Id());

        return NULL;
    }

    /**
    * Hook event callbacks into the application framework.
    * @return Returns an array of events to be hooked into the application
    *      framework.
    */
    public function HookEvents()
    {
        $Hooks = array(
            "EVENT_IN_HTML_HEADER" => "InHtmlHeader",
            "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",
            "EVENT_PLUGIN_EXTEND_EDIT_RESOURCE_COMPLETE_ACCESS_LIST"
                => "ExtendEditResourceCompleteAccessList",
            "EVENT_SYSTEM_ADMINISTRATION_MENU" => "AdminMenu",
            "EVENT_COLLECTION_ADMINISTRATION_MENU" => "AddCollectionAdminMenuItems",
            "EVENT_HTML_INSERTION_POINT" => "InsertBlogSummary",
            "EVENT_USER_ADMINISTRATION_MENU" => "DeclareUserPages",
            "EVENT_DAILY" => "RunDaily",
            );
        if ($this->ConfigSetting("AddNavItem"))
        {
            $Hooks["EVENT_MODIFY_PRIMARY_NAV"] = "AddPrimaryNavLinks";
        }
        return $Hooks;
    }

    /**
    * 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;
        }
        // @codingStandardsIgnoreStart
?>
  <link rel="stylesheet" type="text/css" href="<?PHP $GLOBALS["AF"]->PUIFile("style.css"); ?>" />
<?PHP
       // @codingStandardsIgnoreEnd
    }

    /**
    * Add a link to the Add/Edit Blogs page to the administration menu.
    * @return array of links
    */
    public function AdminMenu()
    {
        return array("ListBlogs" => "Add/Edit Blogs");
    }

    /**
    * Declare the user administration pages this plugin provides.
    * @return array page URLs and labels
    */
    public function DeclareUserPages()
    {
        return array("SubscriberStatistics" => "Blog Subscription Metrics");
    }

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

        if ($GLOBALS["G_PluginManager"]->PluginEnabled("MetricsRecorder"))
        {
            $Pages["BlogReports"] = "Blog Usage Metrics";
        }

        return $Pages;
    }

    /**
    * 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)
    {
        $BlogSchema = new MetadataSchema( $this->GetSchemaId() );

        # add a link to the page to create a new blog entry if the user can
        # author blog entries
        if ($BlogSchema->UserCanAuthor($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 ($BlogSchema->UserCanEdit($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->GetField(self::SUBSCRIPTION_FIELD_NAME);
            $SubscribeField->Enabled(is_numeric($NewValue) && $NewValue != -1);
        }

        if ($ConfigSetting == "BlogName")
        {
            $SubscribeField = $UserSchema->GetField(self::SUBSCRIPTION_FIELD_NAME);

            # 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());
    }

    /**
    * Select a blog on which to operate.
    * @param int $BlogId BlogId to operate on.
    */
    public function SetCurrentBlog($BlogId)
    {
        if (!array_key_exists($BlogId, $this->GetAvailableBlogs()))
        {
            throw new Exception(
                "Attept to seelct a blog that does not exist");
        }

        $this->SelectedBlog = $BlogId;
    }

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

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

    /**
    * Get whether comments are enabled.
    * @return Returns TRUE if comments are enabled and FALSE otherwise.
    */
    public function EnableComments()
    {
        return $this->GetSettingForSelectedBlog("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->GetSettingForSelectedBlog("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->GetSettingForSelectedBlog("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->GetSettingForSelectedBlog("EntriesPerPage");
    }

    /**
    * Get the clean URL prefix.
    * @return Returns the clean URL prefix.
    */
    public function CleanUrlPrefix()
    {
        return $this->GetSettingForSelectedBlog("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)
    {
        # 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->GetField(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->GetField(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() &&
            strlen($this->CleanUrlPrefix()) )
        {
            # 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";
            $Get["BlogId"] = $this->SelectedBlog;
        }

        # 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 Blog_Entry $Entry Blog entry to print.
    * @param int $Size The size of the share buttons.
    * @param string $Color The color of the share buttons. (NULL, "grey", or
    *      "maroon").
    */
    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"));

        # add HTML for share buttons
        // @codingStandardsIgnoreStart
?>
  <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
       // @codingStandardsIgnoreEnd
    }

    /**
    * 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 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(self::SUBSCRIPTION_FIELD_NAME);
    }

    /**
    * 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(self::SUBSCRIPTION_FIELD_NAME, (bool)$Status);

            # pull out the configured Mailer template for notifications
            $MailerTemplate = $this->ConfigSetting(
                $Status ? "SubscriptionConfirmationTemplate" :
                        "UnsubscriptionConfirmationTemplate");

            # if a template was selected
            if ($MailerTemplate != -1)
            {
                # send notifications
                $Mailer = $GLOBALS["G_PluginManager"]->GetPlugin("Mailer");
                $Mailer->SendEmail($MailerTemplate, $User);
            }
        }
    }

    /**
    * 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->GetField(self::SUBSCRIPTION_FIELD_NAME);

        # 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)
        {
            $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 && $Entry !== NULL )
        {
            # the user should only be allowed to send out notifications if
            # they can edit blog entries
            if (!$Entry->UserCanEdit($User))
            {
                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;
        }

        $NotificationBlog = $this->ConfigSetting("EmailNotificationBlog");
        if ($Entry->GetBlogId() != $NotificationBlog)
        {
            return;
        }

        $this->SetCurrentBlog( $NotificationBlog );
        $Mailer = $GLOBALS["G_PluginManager"]->GetPlugin("Mailer");
        $UserIds = $this->GetSubscribers();
        $Schema = new MetadataSchema($this->GetSchemaId());
        $BodyField = $Schema->GetField(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->BlogSetting($Entry->GetBlogId(), "BlogName"),
            "BLOG:BLOGDESCRIPTION" => $this->BlogSetting(
                    $Entry->GetBlogId(), "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);
    }

    /**
    * Record daily subscriber statistics.
    * @param int $LastRunAt Timestamp of last time this event ran.
    */
    public function RunDaily($LastRunAt)
    {
        if (!$GLOBALS["G_PluginManager"]->PluginEnabled("MetricsRecorder"))
        {
            return;
        }

        $UserIds = $this->GetSubscribers();
        $SubscriberCount = count($UserIds);

        $GLOBALS["G_PluginManager"]->GetPlugin("MetricsRecorder")->RecordEvent(
            $this->Name,
            "NumberOfSubscribers",
            $this->ConfigSetting("EmailNotificationBlog"),
            $SubscriberCount,
            NULL,
            0,
            FALSE);
    }

    /**
    * Get UserIds of blog subscribers.
    * @return array of UserIds.
    */
    public function GetSubscribers()
    {
        $UserSchema = new MetadataSchema(MetadataSchema::SCHEMAID_USER);
        $SubscribeField = $UserSchema->GetField(self::SUBSCRIPTION_FIELD_NAME);
        $UserField = $UserSchema->GetField("UserId");

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

        return $UserIds;
    }

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

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

    /**
    * Add pages that this plugin use to EditResourceComplete's
    * "AllowedAccessList", so EditResourceComplete can be redirected from these
    * pages.
    *
    * @param array $AccessList Previous Access List to add regexes to
    * @return Returns an array of regexes to match refers that are allowed
    */
    public function ExtendEditResourceCompleteAccessList($AccessList)
    {
        array_push($AccessList, "/P=P_Blog_ListEntries/i");
        return array("AllowList" => $AccessList);
    }

    /**
    * Get the list of available blogs.
    * @return array of blog names keyed by BlogId.
    */
    public function GetAvailableBlogs()
    {
        $Schema = new MetadataSchema($this->GetSchemaId());
        $BlogNameField = $Schema->GetField(self::BLOG_NAME_FIELD_NAME);

        return $BlogNameField->GetPossibleValues();
    }

    /**
    * Create a new blog.
    * @param string $BlogName Name of the blog to create
    * @return BlogId for the newly created blog
    */
    public function CreateBlog($BlogName)
    {
        $Schema = new MetadataSchema($this->GetSchemaId());
        $TypeField = $Schema->GetField(self::BLOG_NAME_FIELD_NAME );

        $NewBlogCName = new ControlledName(NULL, $BlogName, $TypeField->Id());

        return $NewBlogCName->Id();
    }

    /**
    * Find the blog ID corresponding to a given BlogName, if one exists.
    * @param string $BlogName BlogName to search for
    * @return BlogId for the given name, if one exists. FALSE if it did not.
    */
    public function GetBlogIdByName($BlogName)
    {
        $Schema = new MetadataSchema($this->GetSchemaId());
        $TypeField = $Schema->GetField(self::BLOG_NAME_FIELD_NAME);
        $CNFactory = new ControlledNameFactory( $TypeField->Id() );

        return $CNFactory->GetItemIdByName($BlogName);
    }

    /**
    * Delete a blog and all its entries.
    * @param int $BlogId BlogId to delete.
    */
    public function DeleteBlog($BlogId)
    {
        # do not attept to delete blogs that do not exist
        if (!array_key_exists($BlogId, $this->GetAvailableBlogs()))
        {
            throw new Exception(
                "Attept to get/set setting for a blog that does not exist");
        }

        # fetch all the entries for this blog
        $EntryFactory = new Blog_EntryFactory($BlogId);

        # delete all of them
        foreach ($EntryFactory->GetItemIds() as $EntryId)
        {
            $TgtEntry = new Blog_Entry($EntryId);
            $TgtEntry->Delete();
        }

        # delete the CName we have for this blog
        $BlogCName = new ControlledName($BlogId);
        $BlogCName->Delete();

        # delete our copy of the blog settings
        $AllSettings = $this->ConfigSetting("BlogSettings");
        unset($AllSettings[$BlogId]);
        $this->ConfigSetting("BlogSettings", $AllSettings);
    }

    /**
    * Get/set all the settings for a particular blog.
    * @param int $BlogId BlogId on which to operate.
    * @param array $NewSettings Updated settings for the specified blog (OPTIONAL).
    * @return All settings for the specified blog
    */
    public function BlogSettings($BlogId, $NewSettings=NULL)
    {
        # do not attept to manipulate settings for blogs that do not exist
        if (!array_key_exists($BlogId, $this->GetAvailableBlogs()))
        {
            throw new Exception(
                "Attept to get/set setting for a blog that does not exist");
        }

        # pull out current settings
        $AllSettings = $this->ConfigSetting("BlogSettings");

        # use the defaults if we have no settings for this blog
        if (!array_key_exists($BlogId, $AllSettings))
         {
            $AllSettings[$BlogId] = $this->GetBlogConfigTemplate();
        }

        # when we're updating settings
        if ($NewSettings !== NULL)
        {
            # detect an update to the blog name, update the backing CName
            if ($AllSettings[$BlogId]["BlogName"] != $NewSettings["BlogName"] )
            {
                $CName = new ControlledName($BlogId);
                $CName->Name($NewSettings["BlogName"]);
            }

            # store the new setting
            $AllSettings[$BlogId] = $NewSettings;
            $this->ConfigSetting("BlogSettings", $AllSettings);
        }

        # return settings for this blog
        return $AllSettings[$BlogId];
    }

    /**
    * Get/set an individual setting for a specified blog.
    * @param int $BlogId BlogId to operate on.
    * @param string $SettingName Setting to retrieive/set.
    * @param mixed $NewValue New value (OPTIONAL)
    * @return mixed value of the requested setting
    */
    public function BlogSetting($BlogId, $SettingName, $NewValue=NULL)
    {
        $MySettings = $this->BlogSettings($BlogId);

        # look for changes to any of the values associated with this blog
        if ($NewValue !== NULL)
        {
            $MySettings[$SettingName] = $NewValue;
            $this->BlogSettings($BlogId, $MySettings);
        }

        # return the requested setting
        return $MySettings[$SettingName] ;
    }

    /**
    * Fetch the ConfigSettingsUI format description of settings each blog can have.
    */
    public function GetBlogConfigOptions()
    {
        return $this->BlogConfigurationOptions;
    }

    /**
    * Get an array of expected settings for blogs
    * @return array of settings
    */
    public function GetBlogConfigTemplate()
    {
        static $Result;

        if (!isset($Result))
        {
            $Result = array();
            foreach ($this->BlogConfigurationOptions as $Name => $UISettings)
            {
                $Result[$Name] = isset($UISettings["Default"]) ?
                    $UISettings["Default"] : NULL ;
            }
        }

        return $Result;
    }

    /**
     * Insert a summary of couple blog entries from a specified blog.
     * @param string $PageName The name of the page that signaled the event.
     * @param string $Location Describes the location on the page where the insertion
     *        insertion point occurs.
     * @param int $Context Specify which blog and how many blogs to print
     */
    public function InsertBlogSummary($PageName, $Location, $Context=NULL)
    {
        # if no context provided or no blog name provided, bail
        if (!is_array($Context) || !isset($Context["Blog Name"]))
        {
            return;
        }

        $DisplayBlog = $this->GetBlogIdByName($Context["Blog Name"]);
        $DisplayCount = isset($Context["Max Entries"]) ? $Context["Max Entries"] : 2 ;

        # if the Blog Name is not valid, just print out no blog to display
        if ($DisplayBlog === FALSE)
        {
            ?>
            <h2>There are no blog posts at this time.</h2>
            <?PHP
        }
        else
        {
            $GLOBALS["AF"]->LoadFunction("Blog_PrintSummaryBlock");
            Blog_PrintSummaryBlock($DisplayBlog, $DisplayCount);
        }
    }

    /**
    * Get a configuration parameter for the currently selected blog.
    * @param string $SettingName Name of the setting to get
    * @return mixed setting value
    */
    private function GetSettingForSelectedBlog($SettingName)
    {
        if (!isset($this->SelectedBlog))
        {
            throw new Exception("Attempt to get a blog setting when no blog is selected");
        }

        return $this->BlogSetting($this->SelectedBlog, $SettingName);
    }

    # parameters that we expect each blog to have
    private $BlogConfigurationOptions = array(
        "BlogName" => array(
            "Type" => "Text",
            "Label" => "Blog Name",
            "Help" => "The name of the blog."),
        "BlogDescription" => array(
            "Type" => "Paragraph",
            "Label" => "Blog Description",
            "Help" => "A description of the blog."),
        "EnableComments" => array(
            "Type" => "Flag",
            "Label" => "Enable Comments",
            "Help" => "Enable user comments for blog entries.",
            "Default" => FALSE),
        "ShowAuthor" => array(
            "Type" => "Flag",
            "Label" => "Show Author",
            "Help" => "Show the author of blog entries when displaying them.",
            "Default" => TRUE),
        "MaxTeaserLength" => array(
            "Type" => "Number",
            "Label" => "Maximum Teaser Length",
            "Help" => "The maximum length of the teaser in number of characters.",
            "Default" => 1200),
        "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),
                "Default" => 10),
        "CleanUrlPrefix" => array(
            "Type" => "Text",
            "Label" => "Clean URL Prefix",
            "Help" => "The prefix for the clean URLs for the blog."),
            // @codingStandardsIgnoreStart
        "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."),
            // @codingStandardsIgnoreEnd
        );

    private $SelectedBlog;
}
