Menu

Snippets

Yet another programmer blogging about code

Give your Events Manager events an Add to Calendar link

The Events Manager plugin for WordPress lets you record and display your upcoming events, and is highly extensible through hooks and templates. Here’s how to give your single event page an Add to Calendar link that lets your visitors copy your events into Microsoft Outlook, Thunderbird, and any other iCalendar compliant calendar.

Edit: apologies to anyone who tried this code and got errors. I had copied the code from a class and forgotten to remove “public” from the function declarations; now fixed.

Edit: sometimes I need a smack upside the head; Events Manager has a placeholder for that: #_EVENTICALLINK; but at least mine handles recurring events and lets you customise the link text :) … anyway:

Add to Calendar links are basically links to very simple files in iCalendar format, generally with a .ics extension. To get something like that out of WordPress, the simplest way is to use the AJAX interface, which lets you bypass all the usual page/post stuff and get straight to your custom code. The page at that link describes how to do AJAX in WordPress, so lets focus on just the iCalendar stuff here.

To add a link to your event page, you need to either customise the event-single template or add a custom placeholder. We’ll use the custom placeholder, because it’s less work since we’re already writing PHP hooks for iCalendar. We’ll call our placeholder #_MYCALENDARLINK. Adding that to the Single Events Page format will give us our iCalendar link once we’re done.

/**
* add some output placeholders for Events Manager
* @param string $result
* @param EM_Event $EM_Event
* @param string $placeholder
* @return string
*/
function filterEventOutputPlaceholder($result, $EM_Event, $placeholder) {
    if ($placeholder == '#_MYCALENDARLINK') {
        // link for downloading the calendar entry into a local calendar
        // the nc parameter is to prevent caching of the result
        // so that the most up-to-date event data is always sent
        $url = admin_url('admin-ajax.php')
            . "?action=my-event-ics&id={$EM_Event->event_id}&nc=" . time();
        $result = "<p><a href='$url'>Click to add to your calendar</a></p>n";
    }

    return $result;
}

add_filter('em_event_output_placeholder', 'filterEventOutputPlaceholder', 10, 3);

Now we have to make the link do something. We need to add two AJAX hooks so that the AJAX request is handled whether or not the visitor is a logged in user. We then need to build up the iCalendar data, and send it in the required format and with the required headers. To help debug things, I often add an extra parameter so that I can get the data displayed as plain text on my browser, but you can omit that if you want.

One thing of note is that this code is written for version 5.1+ of Events Manager; prior to this, the EM_Event object had different names for its members.

/**
* generate an iCalendar .ics file for event
*/
function ajaxEventICS() {
    $event_id = preg_replace('/D/', '', $_REQUEST['id']);
    if (!$event_id)
        exit(0);

    $EM_Event = new EM_Event($event_id);
    if (!is_object($EM_Event))
        exit(0);

    // if plaintext is requested (for debugging), set content type to text
    // otherwise set to calendar and tell browser to download the file
    if ($_REQUEST['plaintext'] == 1) {
        header('Content-type: text/plain; charset=utf-8');
    }
    else {
        header('Content-type: text/calendar; charset=utf-8');
        header('Content-Disposition: inline; filename="event.ics"');
    }

    // character conversion arrays
    $charFrom = array('', ';', ',', "n", "t");
    $charTo = array('\', ';', ',', 'n', 't');

    // build array of iCalendar lines
    $ics = array();
    $ics[] = 'BEGIN:VCALENDAR';
    $ics[] = 'VERSION:2.0';
    $ics[] = 'METHOD:PUBLISH';
    $ics[] = 'CALSCALE:GREGORIAN';
    $ics[] = 'PRODID:-//Events Manager//1.0//EN';
    $ics[] = 'BEGIN:VEVENT';

    // manufacture a unique ID using the domain name of the website from options,
    // NOT from bloginfo (which may be filtered!)
    $ics[] = "UID:event-$event_id@" . parse_url(get_option('home'), PHP_URL_HOST);

    // use the event name as the summary
    $ics[] = 'SUMMARY:' . str_replace($charFrom, $charTo, $EM_Event->event_name);

    // add link to event post
    $ics[] = "URL:{$EM_Event->output('#_EVENTURL')}";

    // if event has a location, get the location name
    if (is_object($EM_Event->location))
        $ics[] = 'LOCATION:' . str_replace($charFrom, $charTo, $EM_Event->location->location_name);

    // get the start, end and last modified dates/times
    $gmtOffset = 60 * 60 * get_option('gmt_offset');
    $ics[] = 'DTSTART:' . date('YmdTHisZ', $EM_Event->start - $gmtOffset);
    $ics[] = 'DTEND:' . date('YmdTHisZ', $EM_Event->end - $gmtOffset);
    $ics[] = 'DTSTAMP:' . date('YmdTHisZ', strtotime($EM_Event->event_modified) - $gmtOffset);

    // get categories as comma-separated list
    $categories = array();
    $cats = $EM_Event->get_categories()->categories;
    foreach ($cats as $cat) {
        $categories[] = str_replace($charFrom, $charTo, $cat->output('#_CATEGORYNAME'));
    }
    if (count($categories) > 0)
        $ics[] = 'CATEGORIES:' . implode(',', $categories);

    // get recurring events in iCalendar format
    $recurrence = '';
    if (!$EM_Event->is_individual()) {
        $recurrence = $EM_Event->get_event_recurrence();
        if (!empty($recurrence)) {
            $days = array('SU','MO','TU','WE','TH','FR','SA');
            $until = date('YmdTHisZ', $recurrence->end - $gmtOffset);

            switch ($recurrence->freq) {
                case 'daily':
                    $ics[] = "RRULE:FREQ=DAILY;INTERVAL={$recurrence->interval};UNTIL=$until";
                    break;

                case 'weekly':
                    $bydays = explode(',', $recurrence->byday);
                    $BYDAY = array();
                    foreach ($bydays as $day) {
                        $BYDAY[] = $days[$day];
                    }
                    $BYDAY = implode(',', $BYDAY);
                    $ics[] = "RRULE:FREQ=WEEKLY;BYDAY=$BYDAY;INTERVAL={$recurrence->interval};UNTIL=$until";
                    break;

                case 'monthly':
                    $ics[] = "RRULE:FREQ=MONTHLY;BYDAY={$recurrence->byweekno}{$days[$recurrence->byday]};INTERVAL={$recurrence->interval};UNTIL=$until";
                    break;
            }
        }
    }

    // lines to close iCalendar
    $ics[] = 'END:VEVENT';
    $ics[] = 'END:VCALENDAR';

    // output lines wrapped to 75 characters per RFC-5545
    foreach ($ics as $line) {
        if (strpos($line, ' ') === FALSE) {
            // cut unspaced string
            echo wordwrap("$linen", 75, "nt", TRUE);
        }
        else {
            // preserve space where word was wrapped
            echo wordwrap("$linen", 75, "nt ", TRUE);
        }
    }

    exit;
}

add_action('wp_ajax_my-event-ics', 'ajaxEventICS');
add_action('wp_ajax_nopriv_my-event-ics', 'ajaxEventICS');

There, job is done and recorded in my Google Calendar.