Loop through posts of a custom-post-type (event) and create .ics (iCal) file?

This is entirely based on Event Organiser (a plug-in I’ve developed). The code is lifted almost straight from the source but with alterations. As such I’ve not tested the code as given.

Step 1: Create a feed

This is simple:

add_action('init','wpse63611_add_events_feed');
function wpse63611_add_events_feed(){
     add_feed('my-events','wpse63611_events_feed_output');
}

This adds a feed to your site: www.site.com?feed=my-events or www.site.com/feed/my-events if you have pretty permalinks. The wpse63611_events_feed_output() callback will output the contents of the ICAL file. But first…

Step 2: Alter the query

WordPress doesn’t know what this feed is meant to contain. Here we use the pre_get_posts to tell WordPress, that for this feed we want posts of post type ‘wr_event’. We could get events from a particular category, venue or between certain dates too.

By separating out the query from the output, you can have multiple feeds which use the same output function – but query different events (based on time, location, category etc).

add_action( 'pre_get_posts', 'wpse63611_event_feed_query' );
function wpse63611_event_feed_query( $query ) {

     $yesterday = current_time('timestamp') - 24*60*60;
     $compare = $latest ? '>' : '<';

     if( $query->is_feed('eo-events') ){
         $query->set('post_type', 'wr_event');
         $query->set('posts_per_page', -1);
         $query->set('meta_key', 'event_date');
         $query->set('orderby', 'meta_value_num');
         $query->set('order', 'ASC');
         $query->set('meta_compare', $compare);
         $query->set('meta_value', $value);
     }
}

Step 3: The contents of the ICS file

As mentioned earlier, wpse63611_events_feed_output() is responsible for printing the output of our feed.

 function wpse63611_events_feed_output(){
      //Let's give it a name;
      $filename = urlencode( 'my_events' . date('Y-m-d') . '.ics' );

      //Collect output 
      ob_start();

      // File header
      header( 'Content-Description: File Transfer' );
      header( 'Content-Disposition: attachment; filename=" . $filename );
      header("Content-type: text/calendar');
      header("Pragma: 0");
      header("Expires: 0");
?>
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//<?php  get_bloginfo('name'); ?>//NONSGML Events //EN
CALSCALE:GREGORIAN
X-WR-CALNAME:<?php echo get_bloginfo('name');?> - Events

<?php

    // Loop through events
    if ( have_posts() ):

         $now = new DateTime();
         $datestamp =$now->format('Ymd\THis\Z');

         while( have_posts() ): the_post();
              global $post;

              $uid = md5(uniqid(mt_rand(), true))."@mysite.com";

              $start = unixToiCal(get_event_date($post), get_event_time($post));
              $end = unixToiCal(get_event_end_date($post), get_event_end_time($post));

              $summary = wpse63611_escape_icalText(get_the_title())
              $description = apply_filters('the_excerpt_rss',  get_the_content());
              $description = wpse63611_escape_icalText($description);

BEGIN:VEVENT
UID:<?php echo $uid;?>

DTSTAMP:<?php echo $datestamp;?>

DTSTART:<?php echo $start; ?>

DTEND:<?php echo $end; ?>

SUMMARY:<?php echo wpse63611_esc_ical_text($summary);?>

DESCRIPTION:<?php echo wpse63611_esc_ical_text($description);?>

END:VEVENT

         endwhile;

    endif;
?>
END:VCALENDAR
<?php

    //Collect output and echo 
    $eventsical = ob_get_contents();
    ob_end_clean();
    echo $eventsical;
    exit();
}   

I’ve used the unixToiCal function you’ve defined in your question. I’ve also used the following to remove anything that might upset an ICAL parser:

 function wpse63611_esc_ical_text( $text="" ){

    $text = str_replace("\\", "\\\\", $text);
    $text = str_replace(",", "\,", $text);
    $text = str_replace(";", "\;", $text);
    $text = str_replace("\n", "\n ", $text);

    return $text;
 }