<?PHP
#
#   FILE:  CalendarEvents.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 calendar events.
*/
class CalendarEvents extends Plugin
{
    /**
    * Register information about this plugin.
    */
    function Register()
    {
        $this->Name = "Calendar Events";
        $this->Version = "1.0.6";
        $this->Description = "Functionality for adding an events calendar 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",
            "GoogleMaps" => "1.0.5",
            "MetricsRecorder" => "1.2.4",
            "MetricsReporter" => "0.9.2",
            "SocialMedia" => "1.1.0");
        $this->EnabledByDefault = FALSE;
        $this->Instructions = '
            <p>
              <b>Note:</b> The calendar event metadata fields can be configured
              on the <a href="index.php?P=DBEditor">Metadata Field Editor</i></a>
              page once the plugin has been installed.
            </p>';

        $this->CfgSetup["TodayIncludesOngoingEvents"] = array(
            "Type" => "Flag",
            "Label" => "Use Ongoing Events for <i>Today</i> Button",
            "Help" => "Consider ongoing events, i.e., those that started before "
                ."today's date, when determing where to jump to when clicking "
                ."the <i>Today</i> button.",
            "OnLabel" => "Yes",
            "OffLabel" => "No",
            "Default" => FALSE);

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

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

    /**
    * 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 event
        $GLOBALS["AF"]->AddCleanUrl(
            "%^".$RegexCleanUrlPrefix."/([0-9]+)(|/.+)%",
            "P_CalendarEvents_Event",
            array("EventId" => "\$1"),
            array($this, "CleanUrlTemplate"));

        # clean URL for viewing a single event in iCalendar format
        $GLOBALS["AF"]->AddCleanUrl(
            "%^".$RegexCleanUrlPrefix."/ical/([0-9]+)(|/.+)%",
            "P_CalendarEvents_iCal",
            array("EventId" => "\$1"),
            array($this, "CleanUrlTemplate"));

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

        # construct the expression to match the month
        $MonthString = "jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec";
        $MonthString .= "|january|february|march|april|may|june|july|august|september|october|november|december";
        $MonthString .= "|0?[1-9]|1[12]";

        # clean URL for viewing all a specific month of events
        $GLOBALS["AF"]->AddCleanUrl(
            "%^".$RegexCleanUrlPrefix."/month/([0-9]{4})/(".$MonthString.")/?$%i",
            "P_CalendarEvents_Events",
            array("Year" => "\$1", "Month" => "\$2 \$1"),
            $CleanUrlPrefix."/month/\$Year/\$Month");

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

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

        # create a new metadata schema and save its ID
        $Schema = MetadataSchema::Create(
            "Calendar Events",
            $DefaultPrivs,
            $DefaultPrivs,
            $DefaultPrivs,
            "index.php?P=P_CalendarEvents_Event&EventId=\$ID",
            "Calendar Event");
        $this->ConfigSetting("MetadataSchemaId", $Schema->Id());

        # the names of the fields to create
        $FieldsToCreate = array(
            "Title",
            "Description",
            "Url",
            "Categories",
            "Attachments",
            "Release Flag",
            "Start Date",
            "End Date",
            "All Day",
            "Contact Email",
            "Contact URL",
            "Venue",
            "Street Address",
            "Locality",
            "Region",
            "Postal Code",
            "Country",
            "Coordinates",
            "Authored By",
            "Last Modified By",
            "Date of Creation",
            "Date of Modification",
            "Date of Release");

        # 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.";
            }
        }

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

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

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

        # get the ordering objects for the schema
        $DisplayOrder = $Schema->GetDisplayOrder();
        $EditOrder = $Schema->GetEditOrder();

        # the names of the groups to create and which fields they should hold
        # and in which order
        $GroupsToCreate = array(
            "Date and Time" => array(
                "Start Date",
                "End Date",
                "All Day"),
            "Contact" => array(
                "Contact Email",
                "Contact URL"),
            "Location" => array(
                "Venue",
                "Street Address",
                "Locality",
                "Region",
                "Postal Code",
                "Country",
                "Coordinates"),
            "Administration" => array(
                "Authored By",
                "Last Modified By",
                "Date of Creation",
                "Date of Modification",
                "Date of Release"));

        # create the groups
        foreach ($GroupsToCreate as $GroupToCreate => $FieldsToContain)
        {
            # create the group in each ordering
            $DisplayGroup = $DisplayOrder->CreateGroup($GroupToCreate);
            $EditGroup = $EditOrder->CreateGroup($GroupToCreate);

            # reverse the fields to contain because all of the fields are added
            # in reverse order
            $FieldsToContain = array_reverse($FieldsToContain);

            # add each field to the new groups
            foreach ($FieldsToContain as $FieldToContain)
            {
                # get the field object
                $Field = $Schema->GetFieldByName($FieldToContain);

                try
                {
                    # add the field to each group for each ordering
                    $DisplayOrder->MoveFieldToTopOfGroup($DisplayGroup, $Field);
                    $EditOrder->MoveFieldToTopOfGroup($EditGroup, $Field);
                }

                catch (Exception $Exception)
                {
                    return "Could not move the ".$FieldToContain." field to the "
                           .$GroupToCreate." group.";
                }
            }
        }

        # update the editing and viewing privileges now that the fields have
        # been created
        $EditingPrivs = clone $DefaultPrivs;
        $EditingPrivs->AddCondition($Schema->GetFieldByName("Authored By"));
        $Schema->EditingPrivileges($EditingPrivs);
        $ViewingPrivs = clone $DefaultPrivs;
        $ViewingPrivs->AddCondition($Schema->GetFieldByName("Release Flag"), 1);
        $Subgroup = new PrivilegeSet();
        $Subgroup->AllRequired(TRUE);
        $Subgroup->AddPrivilege(PRIV_MYRESOURCEADMIN);
        $Subgroup->AddCondition($Schema->GetFieldByName("Authored By"));
        $ViewingPrivs->AddSet($Subgroup);
        $Schema->ViewingPrivileges($ViewingPrivs);
    }

    /**
    * 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", "<"))
        {
            # fix the viewing privileges
            $Schema = new MetadataSchema($this->GetSchemaId());
            $ViewingPrivs = new PrivilegeSet();
            $ViewingPrivs->AddPrivilege(PRIV_NEWSADMIN);
            $ViewingPrivs->AddPrivilege(PRIV_SYSADMIN);
            $ViewingPrivs->AddCondition($Schema->GetFieldByName("Release Flag"), 1);
            $Subgroup = new PrivilegeSet();
            $Subgroup->AllRequired(TRUE);
            $Subgroup->AddPrivilege(PRIV_MYRESOURCEADMIN);
            $Subgroup->AddCondition($Schema->GetFieldByName("Authored By"));
            $ViewingPrivs->AddSet($Subgroup);
            $Schema->ViewingPrivileges($ViewingPrivs);
        }

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

            # convert the categories field into an option
            $CategoriesField = $Schema->GetFieldByName("Categories");
            $CategoriesField->Type(MetadataSchema::MDFTYPE_OPTION);
            $CategoriesField->AllowMultiple(TRUE);
        }

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

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

    /**
    * 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_DAILY" => "FillInMissingCoordinates",
            "EVENT_IN_HTML_HEADER" => "InHtmlHeader",
            "EVENT_MODIFY_PRIMARY_NAV" => "AddPrimaryNavLinks",
            "EVENT_MODIFY_SECONDARY_NAV" => "AddSecondaryNavLinks",
            "EVENT_RESOURCE_CREATE" => "ResourceCreated",
            "EVENT_RESOURCE_ADD" => "ResourceUpdated",
            "EVENT_RESOURCE_DUPLICATE" => "ResourceDuplicated",
            "EVENT_RESOURCE_MODIFY" => "ResourceUpdated",
            "EVENT_POST_FIELD_EDIT_FILTER" => "FieldPossiblyEdited");
    }

    /**
    * 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_CalendarEvents_Event")
        {
            # if no resource ID found
            if (count($Matches) <= 2)
            {
                # return match unchanged
                return $Matches[0];
            }

            # get the event from the matched ID
            $Event = new CalendarEvents_Event($Matches[2]);

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

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

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

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

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

    /**
    * Add 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 events page for the current month
        $NavItems = $this->InsertNavItemBefore(
            $NavItems,
            "Events",
            "index.php?P=P_CalendarEvents_Events",
            "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 event if the user can
        # author them
        if ($this->UserCanAuthorEvents($GLOBALS["G_User"]))
        {
            $SafeSchemaId = urlencode($this->GetSchemaId());
            $NavItems = $this->InsertNavItemBefore(
                $NavItems,
                "Add New Event",
                "index.php?P=EditResource&ID=NEW&SC=".$SafeSchemaId,
                "Administration");
        }

        # add a link to the events list if the user can edit them
        if ($this->UserCanEditEvents($GLOBALS["G_User"]))
        {
            $NavItems = $this->InsertNavItemBefore(
                $NavItems,
                "List Events",
                "index.php?P=P_CalendarEvents_ListEvents",
                "Administration");
        }

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

    /**
    * Callback executed whenever a resource is created.
    * @param Resource $Resource Newly-created resource.
    */
    public function ResourceCreated(Resource $Resource)
    {
        # only handle resources that use the events metadata schema
        if ($this->IsEvent($Resource))
        {
            $Resource->Set("Authored By", $GLOBALS["G_User"]->Id());

            # set the release date if events are released by default
            if ($Resource->Get("Release Flag"))
            {
                $Resource->Set("Date of Release", date("Y-m-d H:i:s"));
            }

            # set the location for the event
            $this->SetCoordinates($Resource->Id());
        }
    }

    /**
    * Callback executed whenever a resource is updated, i.e., added or modified.
    * @param Resource $Resource Just-updated resource.
    */
    public function ResourceUpdated(Resource $Resource)
    {
        # only handle resources that use the events metadata schema
        if ($this->IsEvent($Resource))
        {
            $Resource->Set("Last Modified By", $GLOBALS["G_User"]->Id());

            # set the release date if the event was released
            if ($Resource->Get("Release Flag"))
            {
                $Resource->Set("Date of Release", date("Y-m-d H:i:s"));
            }

            $StartDateTimestamp = strtotime($Resource->Get("Start Date"));
            $EndDateTimestamp = strtotime($Resource->Get("End Date"));

            # ensure that sure the end date is at least the start date
            if ($EndDateTimestamp < $StartDateTimestamp)
            {
                $Resource->Set(
                    "End Date",
                    date("Y-m-d H:i:s", $StartDateTimestamp));
            }
        }
    }

    /**
    * Callback executed whenever a resource field is possibly updated.
    * @param MetadataField $Field The field that was possbily updated.
    * @param Resource $Resource Resource that was possibly updated.
    * @param mixed $Value The possibly new value.
    */
    public function FieldPossiblyEdited(MetadataField $Field, Resource $Resource, $Value)
    {
        # location fields
        $LocationFields = array(
            "Venue", "Street Address", "Locality", "Region", "Postal Code",
            "Country");

        # only worry about events and location fields
        if ($this->IsEvent($Resource) && in_array($Field->Name(), $LocationFields))
        {
            if ($Resource->Get($Field->Name()) != $Value)
            {
                # clear the existing coordinates
                $Resource->Set("Coordinates", array("X" => NULL, "Y" => NULL));

                # set the location for the event. repeated calls to this will be
                # deduplicated
                $GLOBALS["AF"]->QueueUniqueTask(
                    array($this, "SetCoordinates"),
                    array($Resource->Id()),
                    ApplicationFramework::PRIORITY_MEDIUM,
                    "Set the location coordinates for a calendar event.");
            }
        }

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

    /**
    * Callback executed whenever a resource is duplicated.
    * @param Resource $DuplicateResource Duplicate resource.
    * @param Resource $SourceResource Source resource.
    */
    public function ResourceDuplicated(Resource $DuplicateResource, Resource $SourceResource)
    {
        # only handle resources that use the events metadata schema
        if ($this->IsEvent($DuplicateResource))
        {
            # get the resource as an event object
            $Event = new CalendarEvents_Event($DuplicateResource->Id());

            # add "[DUPLICATE]" to the title
            $Event->Set("Title", $Event->Get("Title") . " [DUPLICATE]");

            # get the date for now
            $Now = date("Y-m-d H:i:s");

            # update the timestamp fields
            $Event->Set("Date of Creation", $Now);
            $Event->Set("Date of Modification", $Now);

            # update the release date field if the event is released
            if ($Event->Get("Release Flag"))
            {
                $Event->Set("Date of Release", $Now);
            }

            # otherwise, clear the release date field
            else
            {
                $Event->Set("Date of Release", NULL);
            }

            # update the user fields
            $Event->Set("Authored By", $GLOBALS["G_User"]->Id());
            $Event->Set("Last Modified By", $GLOBALS["G_User"]->Id());
        }
    }

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

    /**
    * Get the flag that determines whether clicking the "Today" button should
    * consider ongoing events.
    * @return Returns the flag value.
    */
    public function TodayIncludesOngoingEvents()
    {
        return $this->ConfigSetting("TodayIncludesOngoingEvents");
    }

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

    /**
    * Record an event view with the Metrics Recorder plugin.
    * @param Resource $Event Event.
    */
    public function RecordEventView(CalendarEvents_Event $Event)
    {
        # make sure the event is valid before recording the view event
        if ($Event->Status() !== 1)
        {
            return;
        }

        # record the view event
        $GLOBALS["G_PluginManager"]->GetPlugin("MetricsRecorder")->RecordEvent(
            "CalendarEvents",
            "ViewEvent",
            $Event->Id());
    }

    /**
    * Record an event iCalendar file download with the Metrics Recorder plugin.
    * @param Resource $Event Event.
    */
    public function RecordEventiCalDownload(CalendarEvents_Event $Event)
    {
        # make sure the event is valid before recording the view event
        if ($Event->Status() !== 1)
        {
            return;
        }

        # record the view event
        $GLOBALS["G_PluginManager"]->GetPlugin("MetricsRecorder")->RecordEvent(
            "CalendarEvents",
            "iCalDownload",
            $Event->Id());
    }

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

    /**
    * Get the first month that contains an event.
    * @param bool $ReleasedOnly Set to FALSE to consider all events.
    * @return Returns the month and year of the first month that contains an
    *     event.
    */
    public function GetFirstMonth($ReleasedOnly=TRUE)
    {
        $Database = new Database();
        $Schema = new MetadataSchema($this->GetSchemaId());
        $StartDateName = $Schema->GetFieldByName("Start Date")->DBFieldName();
        $ReleaseFlagName = $Schema->GetFieldByName("Release Flag")->DBFieldName();

        # construct the base query for the first month that contains an event
        $Query = "
            SELECT MIN(".$StartDateName.") AS FirstMonth
            FROM Resources
            WHERE ResourceId >= 0
            AND SchemaId = ".addslashes($this->GetSchemaId());

        # add the release flag constraint if necessary
        if ($ReleasedOnly)
        {
            $Query .= " AND `".$ReleaseFlagName."` = '1' ";
        }

        # execute the query
        $FirstMonth = $Database->Query($Query, "FirstMonth");

        # convert the first month to its timestamp
        $Timestamp = strtotime($FirstMonth);

        # return NULL if there is no first month
        if ($Timestamp === FALSE)
        {
            return NULL;
        }

        # normalize the month and return
        return date("F Y", $Timestamp);
    }

    /**
    * Get the last month that contains an event.
    * @param bool $ReleasedOnly Set to FALSE to consider all events.
    * @return Returns the month and year of the last month that contains an
    *     event.
    */
    public function GetLastMonth($ReleasedOnly=TRUE)
    {
        $Database = new Database();
        $Schema = new MetadataSchema($this->GetSchemaId());
        $EndDateName = $Schema->GetFieldByName("End Date")->DBFieldName();
        $ReleaseFlagName = $Schema->GetFieldByName("Release Flag")->DBFieldName();

        # constrcut the base query for the last month that contains an event
        $Query = "
            SELECT MAX(".$EndDateName.") AS LastMonth
            FROM Resources
            WHERE ResourceId >= 0
            AND SchemaId = ".addslashes($this->GetSchemaId());

        # add the release flag constraint if necessary
        if ($ReleasedOnly)
        {
            $Query .= " AND `".$ReleaseFlagName."` = '1' ";
        }

        # execute the query
        $LastMonth = $Database->Query($Query, "LastMonth");

        # convert the last month to its timestamp
        $Timestamp = strtotime($LastMonth);

        # return NULL if there is no last month
        if ($Timestamp === FALSE)
        {
            return NULL;
        }

        # normalize the month and return
        return date("F Y", $Timestamp);
    }

    /**
    * Get the event IDs for all events in the given month.
    * @param string $Month Month and year of the events to get.
    * @param bool $ReleasedOnly Set to TRUE to only get released events. This
    *      parameter is optional and defaults to TRUE.
    * @return Returns an array of event IDs.
    */
    public function GetEventIdsForMonth($Month, $ReleasedOnly=TRUE)
    {
        $Schema = new MetadataSchema($this->GetSchemaId());

        # get the database field names for the date fields
        $StartDateName = $Schema->GetFieldByName("Start Date")->DBFieldName();
        $EndDateName = $Schema->GetFieldByName("End Date")->DBFieldName();

        # get the month range
        $Date = strtotime($Month);
        $SafeMonthStart = addslashes(date("Y-m-01 00:00:00", $Date));
        $SafeMonthEnd = addslashes(date("Y-m-t 23:59:59", $Date));

        # leaving this here until query testing with more data can be done
        $Condition = " ((".$StartDateName." >= '".$SafeMonthStart."' ";
        $Condition .= " AND ".$StartDateName." <= '".$SafeMonthEnd."') ";
        $Condition .= " OR (".$EndDateName." >= '".$SafeMonthStart."' ";
        $Condition .= " AND ".$EndDateName." <= '".$SafeMonthEnd."') ";
        $Condition .= " OR (".$StartDateName." < '".$SafeMonthStart."' ";
        $Condition .= " AND ".$EndDateName." >= '".$SafeMonthStart."')) ";

        # retrieve event IDs and return
        return $this->FetchEventIds($Condition, $ReleasedOnly);
    }

    /**
    * Get the event IDs for all upcoming events.
    * @param bool $ReleasedOnly Set to TRUE to only get released events. This
    *      parameter is optional and defaults to TRUE.
    * @param int $Limit Set to a positive integer to limit the amount of event
    *      IDs returned.
    * @return Returns an array of event IDs.
    */
    public function GetEventIdsForUpcomingEvents($ReleasedOnly=TRUE, $Limit=NULL)
    {
        $Schema = new MetadataSchema($this->GetSchemaId());

        # get the database field names for the date fields
        $StartDateName = $Schema->GetFieldByName("Start Date")->DBFieldName();
        $EndDateName = $Schema->GetFieldByName("End Date")->DBFieldName();
        $AllDayName = $Schema->GetFieldByName("All Day")->DBFieldName();

        # get the month range
        $SafeMonthStart = addslashes(date("Y-m-d 00:00:00"));
        $SafeMonthStartWithTime = addslashes(date("Y-m-d H:i:s"));
        $SafeMonthEnd = addslashes(date("Y-m-t 23:59:59"));

        # all day and is from today until the end of the month or not all day
        # and is from right now (including time) until the end of the month
        $Condition = " (".$AllDayName." = 1
                         AND ".$StartDateName." >= '".$SafeMonthStart."')
                       OR ((`".$AllDayName."` = 0 OR `".$AllDayName."` IS NULL)
                         AND ".$StartDateName." >= '".$SafeMonthStartWithTime."')";

        # retrieve event IDs and return
        return $this->FetchEventIds($Condition, $ReleasedOnly, $Limit);
    }

    /**
    * Get counts for events for the future, occurring, and past events.
    * @param bool $ReleasedOnly Set to FALSE to get the counts for all events.
    * @return Returns the counts for future, occurring, and past events.
    */
    public function GetEventCountsByTense($ReleasedOnly=TRUE)
    {
        $Database = new Database();
        $Schema = new MetadataSchema($this->GetSchemaId());

        # get the database field names for the date fields
        $StartDateName = $Schema->GetFieldByName("Start Date")->DBFieldName();
        $EndDateName = $Schema->GetFieldByName("End Date")->DBFieldName();
        $AllDayName = $Schema->GetFieldByName("All Day")->DBFieldName();
        $ReleaseFlagName = $Schema->GetFieldByName("Release Flag")->DBFieldName();

        # get the month range
        $SafeTodayStart = addslashes(date("Y-m-d 00:00:00"));
        $SafeTodayWithTime = addslashes(date("Y-m-d H:i:s"));
        $SafeTodayEnd = addslashes(date("Y-m-d 23:59:59"));

        # construct the first part of the query
        $PastEventsCount = $Database->Query("
            SELECT COUNT(*) as EventCount FROM Resources
            WHERE `ResourceId` >= 0
            AND `SchemaId` = '".addslashes($this->GetSchemaId())."'
            ".($ReleasedOnly ? "AND `".$ReleaseFlagName."` = 1" : "")."
            AND ((`".$AllDayName."` = 1
                   AND `".$EndDateName."` < '".$SafeTodayStart."')
                 OR ((`".$AllDayName."` = 0 OR `".$AllDayName."` IS NULL)
                   AND `".$EndDateName."` < '".$SafeTodayWithTime."'))",
            "EventCount");

        # rather than doing complex SQL query logic, just get the count of all
        # of the events and subtract the others below
        $AllEventsCount = $Database->Query("
            SELECT COUNT(*) as EventCount FROM Resources
            WHERE `ResourceId` >= 0
            AND `SchemaId` = '".addslashes($this->GetSchemaId())."'
            ".($ReleasedOnly ? "AND `".$ReleaseFlagName."` = 1" : ""),
            "EventCount");

        $FutureEventsCount = $Database->Query("
            SELECT COUNT(*) as EventCount FROM Resources
            WHERE `ResourceId` >= 0
            AND `SchemaId` = '".addslashes($this->GetSchemaId())."'
            ".($ReleasedOnly ? "AND `".$ReleaseFlagName."` = 1" : "")."
            AND ((`".$AllDayName."` = 1
                   AND `".$StartDateName."` > '".$SafeTodayEnd."')
                 OR ((`".$AllDayName."` = 0 OR `".$AllDayName."` IS NULL)
                   AND `".$StartDateName."` > '".$SafeTodayWithTime."'))",
            "EventCount");

        # return the counts
        return array(
            "Past" => $PastEventsCount,
            "Occurring" => $AllEventsCount - $PastEventsCount - $FutureEventsCount,
            "Future" => $FutureEventsCount);
    }

    /**
    * Get counts for events for each month.
    * @param bool $ReleasedOnly Set to FALSE to get the counts for all events.
    * @return Returns the event counts for each month.
    */
    public function GetEventCounts($ReleasedOnly=TRUE)
    {
        $Schema = new MetadataSchema($this->GetSchemaId());

        # get the bounds of the months
        $FirstMonth = $this->GetFirstMonth();
        $LastMonth = $this->GetLastMonth();

        # convert the month strings to timestamps
        $FirstMonthTimestamp = strtotime($FirstMonth);
        $LastMonthTimestamp = strtotime($LastMonth);

        # print an emprty array if the timestamps aren't valid or there are no
        # events
        if ($FirstMonthTimestamp === FALSE || $LastMonthTimestamp === FALSE)
        {
            return array();
        }

        # get the boundaries as numbers
        $FirstYearNumber = intval(date("Y", $FirstMonthTimestamp));
        $FirstMonthNumber = intval(date("m", $FirstMonthTimestamp));
        $LastYearNumber = intval(date("Y", $LastMonthTimestamp));
        $LastMonthNumber = intval(date("m", $LastMonthTimestamp));

        # start off the query
        $Query = "SELECT ";

        # get the database field names for the date fields
        $StartDateName = $Schema->GetFieldByName("Start Date")->DBFieldName();
        $EndDateName = $Schema->GetFieldByName("End Date")->DBFieldName();
        $ReleaseFlagName = $Schema->GetFieldByName("Release Flag")->DBFieldName();

        # loop through the years
        for ($i = $FirstYearNumber; $i <= $LastYearNumber; $i++)
        {
            # loop through the months
            for ($j = ($i == $FirstYearNumber ? $FirstMonthNumber : 1);
                 ($i == $LastYearNumber ? $j <= $LastMonthNumber : $j < 13);
                 $j++)
            {
                $ColumnName = date("MY", mktime(0, 0, 0, $j, 1, $i));
                $LastDay = date("t", mktime(0, 0, 0, $j, 1, $i));

                $Start = $i."-".$j."-01 00:00:00";
                $End = $i."-".$j."-".$LastDay." 23:59:59";

                $Query .= " sum((".$StartDateName." >= '".$Start."' ";
                $Query .= " AND ".$StartDateName." <= '".$End."') ";
                $Query .= " OR (".$EndDateName." >= '".$Start."' ";
                $Query .= " AND ".$EndDateName." <= '".$End."') ";
                $Query .= " OR (".$StartDateName." < '".$Start."' ";
                $Query .= " AND ".$EndDateName." >= '".$Start."')) AS ".$ColumnName.", ";
            }
        }

        # remove the trailing comma
        $Query = rtrim($Query, ", ") . " ";

        # add the table name
        $Query .= "
            FROM Resources
            WHERE ResourceId >= 0
            AND SchemaId = '".$this->GetSchemaId()."'";

        # add the release flag constraint if necessary
        if ($ReleasedOnly)
        {
            $Query .= " AND `".$ReleaseFlagName."` = '1' ";
        }

        $Database = new Database();
        $Database->Query($Query);

        return $Database->FetchRow();
    }

    /**
    * Get the URL to the events list 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 events list relative to the CWIS root.
    */
    public function EventsListUrl(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_CalendarEvents_Events";
        }

        # 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 an event.
    * @param CalendarEvents_Event $Event Event for which to get metrics.
    * @return Returns an array of metrics. Keys are "Views", "iCalDownloads",
    *     "Shares/Email", "Shares/Facebook", "Shares/Twitter",
    *     "Shares/LinkedIn", and "Shares/Google+".
    */
    public function GetEventMetrics(CalendarEvents_Event $Event)
    {
        # 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(
            "CalendarEvents",
            "ViewEvent",
            NULL, NULL, NULL,
            $Event->Id(),
            NULL,
            $Exclude);

        # get the iCal download metrics
        $Metrics["iCalDownloads"] = $Recorder->GetEventData(
            "CalendarEvents",
            "iCalDownload",
            NULL, NULL, NULL,
            $Event->Id(),
            NULL,
            $Exclude);

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

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

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

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

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

        return $Metrics;
    }

    /**
    * Set the coordinates for the event based on its address.
    * @param int $EventId Event ID.
    */
    public function SetCoordinates($EventId)
    {
        $Event = new CalendarEvents_Event($EventId);

        # don't deal with invalid or temp events
        if ($Event->Status() != 1 || $Event->Id() < 0)
        {
            return;
        }

        # get the address as a one-line string
        $Address = $Event->LocationString();

        # don't deal with events without addresses
        if (!strlen($Address))
        {
            return;
        }

        # try to get the location from Google
        $Location = $GLOBALS["AF"]->SignalEvent(
            "GoogleMaps_EVENT_GEOCODE", array($Address, TRUE));

        # if the geocode info was available, set the location
        if (!is_null($Location))
        {
            $Location["X"] = $Location["Lat"];
            $Location["Y"] = $Location["Lng"];
            $Event->Set("Coordinates", $Location);
        }
    }

    /**
    * Periodic job to look for events that are missing coordinates and
    * fill them in.
    */
    function FillInMissingCoordinates()
    {
        $ResourceFactory = new ResourceFactory($this->GetSchemaId());

        $EventIds = $ResourceFactory->GetItemIds(
            "Coordinates".$this->GetSchemaId()."X IS NULL AND "
            ."Coordinates".$this->GetSchemaId()."Y IS NULL");

        foreach ($EventIds as $EventId)
        {
            $GLOBALS["AF"]->QueueUniqueTask(array($this, "SetCoordinates"),
                array($EventId),
                ApplicationFramework::PRIORITY_MEDIUM,
                "Set the location coordinates for a calendar event when they become available");
        }
    }

    /**
    * Print an event summary.
    * @param CalendarEvents_Event $Event Event to print.
    */
    public function PrintEventSummary(CalendarEvents_Event $Event)
    {
        $SafeId = defaulthtmlentities($Event->Id());
        $SafeEventUrl = defaulthtmlentities($Event->EventUrl());
        $SafeTitle = defaulthtmlentities($Event->Get("Title"));
        $SafeUrl = defaulthtmlentities($Event->Get("Url"));
        $SafeUrlDisplay = defaulthtmlentities(NeatlyTruncateString($Event->Get("Url"), 90, TRUE));
        $SafeStartDate = defaulthtmlentities($Event->StartDateForDisplay());
        $SafeStartDateForParsing = defaulthtmlentities($Event->StartDateForParsing());
        $SafeEndDate = defaulthtmlentities($Event->EndDateForDisplay());
        $SafeEndDateForParsing = defaulthtmlentities($Event->EndDateForParsing());
        $SafeEndDateTimeForDisplay = defaulthtmlentities($Event->EndDateTimeForDisplay());
        $Description = $Event->Get("Description");
        $Location = $Event->LocationForHtml();
        $SpanInDays = $Event->SpanInDays();
        $SafeSpanInDays = defaulthtmlentities($SpanInDays);
        $IsAllDay = $Event->Get("All Day");
        $CanEditEvents = $this->UserCanEditEvents($GLOBALS["G_User"], $Event);
    ?>
      <article class="vevent calendar_events-event calendar_events-summary"
               itemscope="itemscope"
               itemtype="http://schema.org/Event">
        <link rel="profile" href="http://microformats.org/profile/hcalendar">
        <link itemprop="url" href="<?PHP print $SafeEventUrl; ?>" />
        <header class="calendar_events-header">
          <?PHP if ($CanEditEvents) { ?>
            <div class="cw-table cw-table-fullsize cw-table-fauxtable cw-content-complexheader">
              <div class="cw-table-fauxrow">
                <div class="cw-table-fauxcell">
                  <h1 class="calendar_events-title">
                    <a href="index.php?P=P_CalendarEvents_Event&amp;EventId=<?PHP print $SafeId; ?>">
                      <span class="summary"
                            itemprop="name"><?PHP print $SafeTitle; ?></span>
                    </a>
                  </h1>
                </div>
                <div class="cw-table-fauxcell">
                  <a class="cw-button cw-button-elegant"
                     href="index.php?P=EditResource&amp;ID=<?PHP print $SafeId; ?>">Edit</a>
                </div>
              </div>
            </div>
          <?PHP } else { ?>
            <h1 class="calendar_events-title">
              <a href="index.php?P=P_CalendarEvents_Event&amp;EventId=<?PHP print $SafeId; ?>">
                <span class="summary"
                      itemprop="name"><?PHP print $SafeTitle; ?></span>
              </a>
            </h1>
          <?PHP } ?>
          <p>
            <time class="dtstart calendar_events-start_date"
                  itemprop="startDate"
                  datetime="<?PHP print $SafeStartDateForParsing; ?>">
              <?PHP print $SafeStartDate; ?></time>
            <?PHP if ($SpanInDays > 1) { ?>
              <time class="dtend calendar_events-end_date"
                    itemprop="endDate"
                    datetime="<?PHP print $SafeEndDateForParsing; ?>">
                <?PHP print $SafeEndDate; ?></time>
              <?PHP if ($SpanInDays < 11) { ?>
                <span class="calendar_events-span">(<?PHP print $SafeSpanInDays; ?> days)</span>
              <?PHP } ?>
            <?PHP } else if (!$IsAllDay) { ?>
              <time class="dtend calendar_events-end_date"
                    itemprop="endDate"
                    datetime="<?PHP print $SafeEndDateForParsing; ?>">
                <?PHP print $SafeEndDateTimeForDisplay; ?></time>
            <?PHP } else { ?>
              <time class="dtend calendar_events-end_date"
                    itemprop="endDate"
                    datetime="<?PHP print $SafeEndDateForParsing; ?>"></time>
            <?PHP } ?>
            <?PHP if ($Location) { ?>
              <span class="location calendar_events-location"><?PHP print $Location; ?></span>
            <?PHP } ?>
          </p>
        </header>

        <div class="description calendar_events-description"
             itemprop="description"><?PHP print $Description; ?></div>

        <?PHP if ($SafeUrl) { ?>
          <p><a class="url calendar_events-url"
                href="<?PHP print $SafeUrl; ?>"><?PHP print $SafeUrlDisplay; ?></a></p>
        <?PHP } ?>

        <div class="cw-table-fauxtable cw-table-fullsize"><div class="cw-table-fauxrow">
          <p class="cw-table-fauxcell calendar_events-more">
            <a href="index.php?P=P_CalendarEvents_Event&amp;EventId=<?PHP print $SafeId; ?>">
              <span class="calendar_events-bullet">&raquo;</span> More Information</a>
          </p>
          <section class="cw-table-fauxcell calendar_events-actions">
            <?PHP $this->PrintExtraButtonsForEvent($Event, "grey"); ?>
            <?PHP $this->PrintShareButtonsForEvent($Event, 16, "grey"); ?>
          </section>
        </div></div>
      </article>
    <?PHP
    }

    /**
    * Print an event.
    * @param CalendarEvents_Event $Event Event to print.
    */
    public function PrintEvent(CalendarEvents_Event $Event)
    {
        $Schema = new MetadataSchema($this->GetSchemaId());

        $SafeId = defaulthtmlentities($Event->Id());
        $SafeUrl = defaulthtmlentities($Event->Get("Url"));
        $SafeUrlDisplay = defaulthtmlentities(NeatlyTruncateString($Event->Get("Url"), 100, TRUE));
        $SafeBestUrl = defaulthtmlentities($Event->GetBestUrl());
        $SafeTitle = defaulthtmlentities($Event->Get("Title"));
        $SafeStartDate = defaulthtmlentities($Event->StartDateForDisplay());
        $SafeStartDateForParsing = defaulthtmlentities($Event->StartDateForParsing());
        $SafeEndDate = defaulthtmlentities($Event->EndDateForDisplay());
        $SafeEndDateForParsing = defaulthtmlentities($Event->EndDateForParsing());
        $SafeEndDateTimeForDisplay = defaulthtmlentities($Event->EndDateTimeForDisplay());
        $SafeContactEmail = defaulthtmlentities($Event->Get("Contact Email"));
        $SafeContactUrl = defaulthtmlentities($Event->Get("Contact Url"));
        $SafeStaticMapUrl = defaulthtmlentities($Event->StaticMapUrl());
        $SafeMapUrl = defaulthtmlentities($Event->MapUrl());
        $SafeiCalUrl = defaulthtmlentities($Event->iCalUrl());
        $SafeiCalFileName = defaulthtmlentities(iCalendar::GenerateFileNameFromSummary($Event->Get("Title")));
        $Description = $Event->Get("Description");
        $Categories = $Event->CategoriesForDisplay();
        $Attachments = $Event->AttachmentsForDisplay();
        $Coordinates = $Event->Get("Coordinates");
        $Location = $Event->LocationForHtml();
        $SpanInDays = $Event->SpanInDays();
        $SafeSpanInDays = defaulthtmlentities($SpanInDays);
        $IsAllDay = $Event->Get("All Day");
        $HasContact = $SafeContactEmail || $SafeContactUrl;
        $HasCategories = count($Categories);
        $HasAttachments = count($Attachments);
        $CanEditEvents = $this->UserCanEditEvents($GLOBALS["G_User"], $Event);
        $CanViewMetrics = $this->UserCanViewMetrics($GLOBALS["G_User"]);
        $SafeCategoriesLabel = $Schema->GetFieldByName("Categories")->GetDisplayName();
        $SafeAttachmentsLabel = $Schema->GetFieldByName("Attachments")->GetDisplayName();
    ?>
      <article class="vevent calendar_events-event calendar_events-full"
               itemscope="itemscope"
               itemtype="http://schema.org/Event">
        <link rel="profile" href="http://microformats.org/profile/hcalendar">
        <link itemprop="url" href="<?PHP print $SafeBestUrl; ?>" />
        <header class="calendar_events-header">
          <?PHP if ($CanEditEvents) { ?>
            <div class="cw-table cw-table-fullsize cw-table-fauxtable cw-content-complexheader">
              <div class="cw-table-fauxrow">
                <div class="cw-table-fauxcell">
                  <h1 class="summary calendar_events-title"
                      itemprop="name"><?PHP print $SafeTitle; ?></h1>
                </div>
                <div class="cw-table-fauxcell">
                  <a class="cw-button cw-button-elegant"
                     href="index.php?P=EditResource&amp;ID=<?PHP print $SafeId; ?>">Edit</a>
                </div>
              </div>
            </div>
          <?PHP } else { ?>
            <h1 class="summary calendar_events-title"
                itemprop="name"><?PHP print $SafeTitle; ?></h1>
          <?PHP } ?>
          <p>
            <time class="dtstart calendar_events-start_date"
                  itemprop="startDate"
                  datetime="<?PHP print $SafeStartDateForParsing; ?>">
              <?PHP print $SafeStartDate; ?></time>
            <?PHP if ($SpanInDays > 1) { ?>
              <time class="dtend calendar_events-end_date"
                    itemprop="endDate"
                    datetime="<?PHP print $SafeEndDateForParsing; ?>">
                <?PHP print $SafeEndDate; ?></time>
              <?PHP if ($SpanInDays < 11) { ?>
                <span class="calendar_events-span">(<?PHP print $SafeSpanInDays; ?> days)</span>
              <?PHP } ?>
            <?PHP } else if (!$IsAllDay && $SafeStartDate != $SafeEndDateTimeForDisplay) { ?>
              <time class="dtend calendar_events-end_date"
                    itemprop="endDate"
                    datetime="<?PHP print $SafeEndDateForParsing; ?>">
                <?PHP print $SafeEndDateTimeForDisplay; ?></time>
            <?PHP } else { ?>
              <time class="dtend calendar_events-end_date"
                    itemprop="endDate"
                    datetime="<?PHP print $SafeEndDateForParsing; ?>"></time>
            <?PHP } ?>
          </p>
        </header>

        <div class="description"
             itemprop="description"><?PHP print $Description; ?></div>

        <?PHP if ($SafeUrl) { ?>
          <p><a class="url calendar_events-url"
                href="<?PHP print $SafeUrl; ?>"><?PHP print $SafeUrlDisplay; ?></a></p>
        <?PHP } ?>

        <div class="calendar_events-fancy_box">
          <?PHP if ($HasCategories) { ?>
            <section id="categories">
              <header><b><?PHP print $SafeCategoriesLabel; ?>:</b></header>
              <ul class="cw-list cw-list-noindent cw-list-nobullets cw-list-horizontal cw-list-separated calendar_events-categories"
                  itemprop="keywords">
                <?PHP foreach ($Categories as $Category) {
                          $SafeCategory = defaulthtmlentities($Category); ?>
                  <li><i class="category"><?PHP print $Category; ?></i></li>
                <?PHP } ?>
              </ul>
            </section>
          <?PHP } ?>

          <?PHP if ($HasContact) { ?>
            <section id="contact">
              <header><b>Contact:</b></header>
              <ul class="cw-list cw-list-noindent cw-list-nobullets cw-list-horizontal cw-list-separated cw-list-or cw-list-no_terminator calendar_events-contact">
                <?PHP if ($SafeContactEmail) { ?>
                  <li><a href="mailto:<?PHP print $SafeContactEmail; ?>"
                         title="E-mail the event organizer"><?PHP print $SafeContactEmail; ?></a></li>
                <?PHP } ?>
                <?PHP if ($SafeContactUrl) { ?>
                  <li><a href="<?PHP print $SafeContactUrl; ?>"
                         title="Go to the event organizer's contact page"><?PHP print $SafeContactUrl; ?></a></li>
                <?PHP } ?>
              </ul>
            </section>
          <?PHP } ?>

          <?PHP if ($HasAttachments) { ?>
            <section id="attachments">
              <header><b><?PHP print $SafeAttachmentsLabel; ?>:</b></header>
              <ul class="cw-list cw-list-noindent cw-list-nobullets cw-list-horizontal cw-list-separated cw-list-no_terminator calendar_events-attachments">
                <?PHP foreach ($Attachments as $Attachment) {
                          $SafeName = defaulthtmlentities($Attachment[0]);
                          $SafeLink = defaulthtmlentities($Attachment[1]); ?>
                  <li><a href="<?PHP print $SafeLink; ?>" title="Download this attachment"><?PHP print $SafeName; ?></a></li>
                <?PHP } ?>
              </ul>
            </section>
          <?PHP } ?>

          <section id="import">
            <header><b>Add to Calendar:</b></header>
            <a href="<?PHP print $SafeiCalUrl; ?>"
               title="Download this event in iCalendar format to import it into your personal calendar"><?PHP print $SafeiCalFileName; ?></a>
          </section>
        </div>

        <?PHP if ($SafeMapUrl && $SafeStaticMapUrl) { ?>
          <section id="location" class="calendar_events-location-section">
            <header><h1 class="calendar_events-location-header">Location</h1></header>
            <div class="calendar_events-location-wrapper">
              <a href="<?PHP print $SafeMapUrl; ?>"
                 title="View this location in Google Maps"
                 target="_blank"><?PHP print $Location; ?></a></div>
            <a href="<?PHP print $SafeMapUrl; ?>"
               title="View this location in Google Maps"
               target="_blank">
              <img src="<?PHP print $SafeStaticMapUrl; ?>" alt="Map" />
            </a>
          </section>
        <?PHP } ?>

        <section id="share">
          <?PHP $this->PrintShareButtonsForEvent($Event); ?>
        </section>

        <?PHP if ($CanViewMetrics) { ?>
          <section id="metrics">
            <h1 class="calendar_events-metrics-header">Metrics</h1>
            <?PHP $this->PrintEventMetrics($Event); ?>
          </section>
        <?PHP } ?>
      </article>
    <?PHP
    }

    /**
    * Print the metrics for an event.
    * @param CalendarEvents_Event $Event Event for which to print metrics.
    */
    public function PrintEventMetrics(CalendarEvents_Event $Event)
    {
        $Metrics = $this->GetEventMetrics($Event);

        $SafeNumViews = defaulthtmlentities(count($Metrics["Views"]));
        $SafeNumiCalDownloads = defaulthtmlentities(count($Metrics["iCalDownloads"]));
        $SafeNumEmail = defaulthtmlentities(count($Metrics["Shares/Email"]));
        $SafeNumFacebook = defaulthtmlentities(count($Metrics["Shares/Facebook"]));
        $SafeNumTwitter = defaulthtmlentities(count($Metrics["Shares/Twitter"]));
        $SafeNumLinkedIn = defaulthtmlentities(count($Metrics["Shares/LinkedIn"]));
        $SafeNumGooglePlus = defaulthtmlentities(count($Metrics["Shares/Google+"]));
    ?>
      <table class="cw-table cw-table-sideheaders cw-table-padded cw-table-striped calendar_events-metrics-table">
        <tbody>
          <tr>
            <th>Views</th>
            <td><?PHP print $SafeNumViews; ?></td>
          </tr>
          <tr>
            <th>iCalendar Downloads</th>
            <td><?PHP print $SafeNumiCalDownloads; ?></td>
          </tr>
          <tr>
            <th>Shared via E-mail</th>
            <td><?PHP print $SafeNumEmail; ?></td>
          </tr>
          <tr>
            <th>Shared to Facebook</th>
            <td><?PHP print $SafeNumFacebook; ?></td>
          </tr>
          <tr>
            <th>Shared to Twitter</th>
            <td><?PHP print $SafeNumTwitter; ?></td>
          </tr>
          <tr>
            <th>Shared to LinkedIn</th>
            <td><?PHP print $SafeNumLinkedIn; ?></td>
          </tr>
          <tr>
            <th>Shared to Google+</th>
            <td><?PHP print $SafeNumGooglePlus; ?></td>
          </tr>
        </tbody>
      </table>
    <?PHP
    }

    /**
    * Print share buttons for an event.
    * @param CalendarEvents_Event $Event Event.
    * @param int $Size The size of the share buttons.
    * @param string $Color The color of the share buttons. (NULL, "grey", or
    *      "maroon").
    */
    public function PrintShareButtonsForEvent(CalendarEvents_Event $Event, $Size=24, $Color=NULL)
    {
        $SafeId = defaulthtmlentities(urlencode($Event->Id()));
        $SafeTitle = defaulthtmlentities(rawurlencode(strip_tags($Event->Get("Title"))));
        $SafeUrl = defaulthtmlentities(rawurlencode($GLOBALS["AF"]->BaseUrl().$Event->EventUrl()));
        $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;
?>
  <ul class="cw-list cw-list-noindent cw-list-nobullets cw-list-horizontal calendar_events-share">
    <li><a title="Share this event via e-mail" onclick="jQuery.get(cw.getRouterUrl()+'?P=P_SocialMedia_ShareResource&ResourceId=<?PHP print $SafeId; ?>&Site=em');" 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 event 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 event 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 event 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 event via Google+" href="<?PHP print $SafeBaseUrl; ?>&amp;Site=gp"><img src="<?PHP $GLOBALS["AF"]->PUIFile("googleplus_".$FileSuffix.".png"); ?>" alt="Google+" /></a></li>
  </ul>
<?PHP
    }

    /**
    * Print extra buttons for an event.
    * @param CalendarEvents_Event $Event Event.
    * @param string $Color The color of the buttons. (NULL or "grey")
    */
    public function PrintExtraButtonsForEvent(CalendarEvents_Event $Event, $Color=NULL)
    {
        # get the file suffix
        $FileSuffix = is_null($Color) ? "" : "-" . strtolower($Color);

        # get the URL to the full event page
        $EventUrl = $Event->EventUrl();

        # assume these won't be set by default
        $ContactUrl = NULL;
        $MapUrl = NULL;
        $AttachmentsUrl = NULL;

        # go to the full event page with both contact options they're both set
        if ($Event->Get("Contact Email") && $Event->Get("Contact URL"))
        {
            $ContactUrl = $EventUrl . "#contact";
        }

        # use a mailto: if just the contact e-mail field is set
        else if ($Event->Get("Contact Email"))
        {
            $ContactUrl = "mailto:" . urlencode($Event->Get("Contact Email"));
        }

        # use the contact URL if it's set
        else if ($Event->Get("Contact URL"))
        {
            $ContactUrl = $Event->Get("Contact URL");
        }

        # if any of the location fields are set
        if ($Event->LocationString())
        {
            $MapUrl = $EventUrl . "#location";
        }

        # if there are any attachments
        if (count($Event->Get("Attachments")))
        {
            $AttachmentsUrl = $EventUrl . "#attachments";
        }

        # escape URLs for insertion into HTML
        $SafeiCalUrl = defaulthtmlentities($Event->iCalUrl());
        $SafeContactUrl = defaulthtmlentities($ContactUrl);
        $SafeMapUrl = defaulthtmlentities($MapUrl);
        $SafeAttachmentsUrl = defaulthtmlentities($AttachmentsUrl);
?>
  <ul class="cw-list cw-list-noindent cw-list-nobullets cw-list-horizontal calendar_events-extra">
    <li><a title="Download this event in iCalendar format to import it into your personal calendar" href="<?PHP print $SafeiCalUrl; ?>"><img src="<?PHP $GLOBALS["AF"]->PUIFile("calendar-import_16".$FileSuffix.".png"); ?>" alt="iCalendar" /></a></li>
    <?PHP if ($ContactUrl) { ?>
      <li><a title="Contact the event organizer" href="<?PHP print $SafeContactUrl; ?>"><img src="<?PHP $GLOBALS["AF"]->PUIFile("at-sign_16".$FileSuffix.".png"); ?>" alt="Contact" /></a></li>
    <?PHP } ?>
    <?PHP if ($MapUrl) { ?>
      <li><a title="View this event on a map" href="<?PHP print $SafeMapUrl; ?>"><img src="<?PHP $GLOBALS["AF"]->PUIFile("marker_16".$FileSuffix.".png"); ?>" alt="Map" /></a></li>
    <?PHP } ?>
    <?PHP if ($AttachmentsUrl) { ?>
      <li><a title="View files attached to this event" href="<?PHP print $SafeAttachmentsUrl; ?>"><img src="<?PHP $GLOBALS["AF"]->PUIFile("paper-clip_16".$FileSuffix.".png"); ?>" alt="Attachments" /></a></li>
    <?PHP } ?>
  </ul>
<?PHP
    }

    /**
    * Print the transport controls from browsing the events calendar.
    * @param array $EventCounts An array of months mapped to the count of events for
    *      that month.
    * @param string $FirstMonth The first month that contains an event.
    * @param string $CurrentMonth The current month being displayed.
    * @param string $LastMonth The last month that contains an event.
    * @param int $PreviousMonthTimestamp The timestamp for the previous month.
    * @param int $NextMonthTimestamp The timestamp for the next month.
    */
    public function PrintTransportControls(
        array $EventCounts,
        $FirstMonth,
        $CurrentMonth,
        $LastMonth,
        $PreviousMonthTimestamp,
        $NextMonthTimestamp)
    {
        $SafePreviousMonthName = date("F", $PreviousMonthTimestamp);
        $SafeNextMonthName = date("F", $NextMonthTimestamp);
        $CurrentMonthKey = date("MY");
        $HasEventsForCurrentMonth =
            isset($EventCounts[$CurrentMonthKey])
            && $EventCounts[$CurrentMonthKey] > 0;
    ?>
      <div class="cw-table cw-table-fullsize cw-table-fauxtable calendar_events-transport_controls">
        <div class="cw-table-fauxrow">
          <div class="cw-table-fauxcell calendar_events-back">
            <?PHP if ($this->ShowUrl($EventCounts, $PreviousMonthTimestamp)) { ?>
              <a class="cw-button cw-button-elegant"
                 href="<?PHP print defaulthtmlentities($this->GetUrl($PreviousMonthTimestamp)); ?>">&larr; <?PHP print $SafePreviousMonthName; ?></a>
            <?PHP } ?>
          </div>
          <div class="cw-table-fauxcell calendar_events-selector">
            <form method="get" action="index.php">
              <input type="hidden" name="P" value="P_CalendarEvents_Events" />
              <?PHP $this->PrintMonthSelector($EventCounts, $FirstMonth, $LastMonth, $CurrentMonth); ?>
              <noscript><input class="cw-button cw-button-constrained cw-button-elegant" type="submit" value="Update" /></noscript>
              <?PHP if ($HasEventsForCurrentMonth) { ?>
                <a class="cw-button cw-button-constrained cw-button-elegant calendar_events-go_to_today"
                   href="<?PHP print $this->EventsListUrl(array(), "today"); ?>">Today</a>
              <?PHP } ?>
            </form>
          </div>
          <div class="cw-table-fauxcell calendar_events-forward">
            <?PHP if ($this->ShowUrl($EventCounts, $NextMonthTimestamp)) { ?>
              <a class="cw-button cw-button-elegant"
                 href="<?PHP print defaulthtmlentities($this->GetUrl($NextMonthTimestamp)); ?>"><?PHP print $SafeNextMonthName; ?> &rarr;</a>
            <?PHP } ?>
          </div>
        </div>
      </div>
    <?PHP
    }

    /**
    * Print the month selector.
    * @param array $EventCounts An array of months mapped to the count of events
    *      for that month.
    * @param string $FirstMonth The first month that contains an event.
    * @param string $LastMonth The last month that contains an event.
    * @param string $SelectedMonth The month that should be selected.
    */
    public function PrintMonthSelector(
        array $EventCounts,
        $FirstMonth,
        $LastMonth,
        $SelectedMonth=NULL)
    {
        # print nothing if there are no events
        if (!count($EventCounts))
        {
            return;
        }

        $CleanUrlPrefix = $this->CleanUrlPrefix();

        # convert the month strings to timestamps
        $FirstMonthTimestamp = strtotime($FirstMonth);
        $LastMonthTimestamp = strtotime($LastMonth);
        $SelectedMonthTimestamp = strtotime($SelectedMonth);

        # print nothing if the timestamps aren't valid or not set
        if ($FirstMonthTimestamp === FALSE || $LastMonthTimestamp === FALSE)
        {
            return;
        }

        $SelectedHasEvents = isset($EventCounts[date("MY", $SelectedMonthTimestamp)]);
        $FirstYearNumber = intval(date("Y", $FirstMonthTimestamp));
        $FirstMonthNumber = intval(date("m", $FirstMonthTimestamp));
        $LastYearNumber = intval(date("Y", $LastMonthTimestamp));
        $LastMonthNumber = intval(date("m", $LastMonthTimestamp));
        $SelectedYearNumber = intval(date("Y", $SelectedMonthTimestamp));
        $SelectedMonthNumber = intval(date("m", $SelectedMonthTimestamp));
        $SelectedMonthAbbr = strtolower(date("M", $SelectedMonthTimestamp));
    ?>
      <select class="calendar_events-month_selector" name="Month">
        <?PHP if (!$SelectedHasEvents) { ?>
          <!-- dummy option for the current month -->
          <option value="<?PHP print $SelectedMonthAbbr; ?> <?PHP print $SelectedYearNumber; ?>"><?PHP print $SelectedMonth; ?></option>
        <?PHP } ?>
        <?PHP for ($i = $FirstYearNumber; $i <= $LastYearNumber; $i++) { ?>
          <optgroup label="<?PHP print defaulthtmlentities($i); ?>">
            <?PHP for ($j = ($i == $FirstYearNumber ? $FirstMonthNumber : 1);
                       ($i == $LastYearNumber ? $j <= $LastMonthNumber : $j < 13);
                       $j++)
                  {
                      $Selected = $SelectedYearNumber == $i && $SelectedMonthNumber == $j;
                      $MonthName = date("F", mktime(0, 0, 0, $j));
                      $MonthNameAbbr = date("M", mktime(0, 0, 0, $j));
                      $EventCount = $EventCounts[$MonthNameAbbr.$i];
                                                                              ?>
              <option <?PHP if ($Selected) print ' selected="selected" '; ?>
                      <?PHP if ($EventCount < 1) print ' disabled="disabled" '; ?>
                      value="<?PHP print strtolower($MonthNameAbbr)." ".$i; ?>">
                <?PHP print $MonthName; ?>
              </option>
            <?PHP } ?>
          </optgroup>
        <?PHP } ?>
      </select>
      <script type="text/javascript">
        (function(){
          var selector = jQuery(".calendar_events-month_selector");

          // explicitly set the width so that it doesn't change when the selected
          // option's text does
          selector.css("width", "135px");

          // submit the form when the selector changes
          selector.change(function(){
            <?PHP if ($GLOBALS["AF"]->HtaccessSupport()) { ?>
              // use clean URLs whenever possible
              var values = jQuery(this).val().split(" ");
              window.location.href = cw.getBaseUrl() + "<?PHP print $CleanUrlPrefix; ?>/month/" + values[1] + "/" + values[0];
            <?PHP } else { ?>
              this.form.submit();
            <?PHP } ?>
          });

          // remove the year from the selected option when the select box gets
          // activated, but keep the width the same
          selector.mousedown(function(){
            var selector = jQuery(this),
                selected = jQuery("option:selected", this);
            selected.html(jQuery.trim(selected.html()).split(/\s+/)[0]);
          });

          // add the year back in. this isn't perfect, but it's best way (so far) to
          // get this to work cross-browser. the only issue comes up when the user
          // selects an already-selected option, which shouldn't be often. also
          selector.bind("change blur", function(){
            var selected = jQuery("option:selected", this),
                label = jQuery.trim(selected.html());

            if (label.search(/[0-9]{4}$/) === -1) {
              selected.html(label + " " + jQuery.trim(selected.parent().attr("label")));
            }
          });

          // add the month for the initially selected value. don't use "change"
          // because it will submit the form
          selector.blur();
        }());
      </script>
    <?PHP
    }

    /**
    * Get the URL for the month that contains the given timestamp.
    * @param int $Timestamp Timestamp.
    * @return Returns the URL for the month that contains the timestamp.
    */
    protected function GetUrl($Timestamp)
    {
       $SafeMonth = urlencode(strtolower(date("M", $Timestamp)));
       $SafeYear = urlencode(date("Y", $Timestamp));
       return "index.php?P=P_CalendarEvents_Events&Year=".$SafeYear."&Month=".$SafeMonth;
    }

    /**
    * Determine whether a URL for the month that contains the given timestamp should
    * be displayed, based on whether the month has any events in it.
    * @param array $EventCounts An array of months mapped to the count of events for
    *      that month.
    * @param int $Timestamp Timestamp.
    * @return Returns TRUE if the URL should be displayed.
    */
    protected function ShowUrl(array $EventCounts, $Timestamp)
    {
        $Key = date("M", $Timestamp) . date("Y", $Timestamp);
        return array_key_exists($Key, $EventCounts) && $EventCounts[$Key] > 0;
    }

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

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

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

    /**
    * Fetch event IDs.
    * @param string $Condition Additional SQL condition string.
    * @param bool $ReleasedOnly Set to TRUE to only return released events.
    * @param int $Limit Set to a positive integer to limit the amount of event
    *      IDs returned.
    * @return Returns an array of matched event IDs.
    */
    protected function FetchEventIds($Condition=NULL, $ReleasedOnly=TRUE, $Limit=NULL)
    {
        $Database = new Database();
        $Schema = new MetadataSchema($this->GetSchemaId());
        $TitleName = $Schema->GetFieldByName("Title")->DBFieldName();
        $StartDateName = $Schema->GetFieldByName("Start Date")->DBFieldName();
        $EndDateName = $Schema->GetFieldByName("End Date")->DBFieldName();

        # construct the first part of the query
        $Query = "
            SELECT ResourceId FROM Resources
            WHERE ResourceId >= 0
            AND SchemaId = '".addslashes($this->GetSchemaId())."'";

        # if only released events should be returned
        if ($ReleasedOnly)
        {
            $ReleaseFlagName = $Schema->GetFieldByName("Release Flag")->DBFieldName();
            $Query .= " AND ".$ReleaseFlagName." = '1' ";
        }

        # add the condition string if given
        if (!is_null($Condition))
        {
            $Query .= " AND " . $Condition;
        }

        # add sorting parameters
        $Query .= " ORDER BY ".$StartDateName." ASC, ";
        $Query .= " ".$EndDateName." ASC, ";
        $Query .= " ".$TitleName." ASC ";

        # add the limit if given
        if (!is_null($Limit))
        {
            $Query .= " LIMIT " . intval($Limit). " ";
        }

        # execute the query
        $Database->Query($Query);

        # return the IDs
        return $Database->FetchColumn("ResourceId");
    }

    /**
    * 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__) . "/install/" . $Name . ".xml";
        $Xml = @file_get_contents($Path);

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

    /**
    * Get the path to the default categories file.
    * @return Returns the path to the default categories file.
    */
    protected function GetDefaultCategoriesFile()
    {
        return dirname(__FILE__) . "/install/DefaultCategories.csv";
    }

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

}
