<?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.
    */
    public function Register()
    {
        $this->Name = "Calendar Events";
        $this->Version = "1.0.9";
        $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");

        $Schema = new MetadataSchema($this->ConfigSetting("MetadataSchemaId"));

        foreach (array("Title", "Description", "Url") as $FieldName)
        {
            $Schema->StdNameToFieldMapping($FieldName,
                    $Schema->GetFieldIdByName($FieldName));
        }

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

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

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

        # create index so that searching by date can be fast
        $DB = new Database();
        $SCID = $this->GetSchemaId();
        $DB->Query(
            "CREATE INDEX CalendarEvents_StartDateAllDay "
                ."ON Resources (AllDay".$SCID.", StartDate".$SCID.");");
    }

    /**
    * Upgrade from a previous version.
    * @param string $PreviousVersion Previous version of the plugin.
    * @return 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");
        }

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

            $DB->Query(
                "CREATE INDEX CalendarEvents_StartDateAllDay "
                    ."ON Resources (AllDay".$SCID.", StartDate".$SCID.");");
        }

        # upgrade to 1.0.8
        if (version_compare($PreviousVersion, "1.0.8", "<"))
        {
            # change schema name
            $Schema = new MetadataSchema($this->GetSchemaId());
            $Schema->Name("Events");
            $Schema->ResourceName("Event");
        }

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

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

        return NULL;
    }

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

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

        # remove index
        $DB = new Database();
        $DB->Query(
            "CREATE INDEX CalendarEvents_StartDateAllDay "
                ."ON Resources (AllDay".$SCID.", StartDate".$SCID.");");

        # delete schema
        $Schema->Delete();

        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()
    {
        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",
            "EVENT_PLUGIN_EXTEND_EDIT_RESOURCE_COMPLETE_ACCESS_LIST"
                    => "ExtendEditResourceCompleteAccessList");
    }

    /**
    * 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;
        }
        // @codingStandardsIgnoreStart
?>
<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
        // @codingStandardsIgnoreEnd
    }

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

            # 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)
    {
        # 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)
    {
        # 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 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)
    {
        # bail if the event no longer exists
        if (!CalendarEvents_Event::ItemExists($EventId))
        {
            return;
        }

        $Event = new CalendarEvents_Event($EventId);

        # don't deal with temp events
        if ($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.
    */
    public 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);
        // @codingStandardsIgnoreStart
    ?>
      <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
        // @codingStandardsIgnoreEnd
    }

    /**
    * 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();
        // @codingStandardsIgnoreStart
    ?>
      <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>
                  <?PHP  $GLOBALS["AF"]->SignalEvent("EVENT_HTML_INSERTION_POINT",
                      array($GLOBALS["AF"]->GetPageName(), "Resource Display Buttons", array(
                        "Resource" => $Event,
                        ))); ?>
                </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
        // @codingStandardsIgnoreEnd
    }

    /**
    * 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+"]));
        // @codingStandardsIgnoreStart
    ?>
      <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
        // @codingStandardsIgnoreEnd
    }

    /**
    * 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;
        // @codingStandardsIgnoreStart
?>
  <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
        // @codingStandardsIgnoreEnd
    }

    /**
    * 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);
        // @codingStandardsIgnoreStart
?>
  <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
        // @codingStandardsIgnoreEnd
    }

    /**
    * 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;
        // @codingStandardsIgnoreStart
    ?>
      <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
        // @codingStandardsIgnoreEnd
    }

    /**
    * 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));
        // @codingStandardsIgnoreStart
    ?>
      <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
        // @codingStandardsIgnoreEnd
    }

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

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

    /**
    * 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_CalendarEvents_ListEvents/i");
        return array("AllowList" => $AccessList);
    }
}
