Commit 11f49c89 authored by Robert Rollins's avatar Robert Rollins

Initial commit of Date iCal 3.0!

This new version is still in a state of partial completeness. You may need to do
some manual fixing to get it working, due to a change I made in the classes
offered by this module. Try 'drush cc registry' if 'drush cc all' fails.

Please note that ALL of the hooks exposed by Date iCal have changed. They do
essentially the same things they used to do, but they're named differently.

The big change is that the Feeds plugin has been completely re-written to
conform to the Feeds APIs. It's now much more resilient and powerful. Feeds
Tamper will now work, and is in fact the prescribed way to alter data, since I
removed several alter hooks.
parent 3073af84
This diff is collapsed.
name = Date iCal
description = Allows creation of iCal feeds using Views, and provides a Feeds plugin for parsing iCal feeds.
description = Enables users to export iCal feeds using Views, and import iCal feeds using Feeds.
package = Date/Time
php = 5.3
core = 7.x
......@@ -10,21 +10,15 @@ dependencies[] = libraries (>=7.x-2.0)
dependencies[] = date
dependencies[] = date_api
; Date iCal doesn't actually depend on Feeds, but it's iCal import functionality won't work without it.
; Date iCal doesn't actually depend on Feeds, but it's iCal import functionality won't be usable without it.
;dependencies[] = feeds
; Includes
; Includes for iCal feed export using Views
files[] = includes/date_ical_plugin_row_ical_entity.inc
files[] = includes/date_ical_plugin_row_ical_fields.inc
files[] = includes/date_ical_plugin_style_ical_feed.inc
; Includes for iCal feed import using Feeds
files[] = includes/DateIcalFeedsParser.inc
files[] = includes/DateIcalFeedsParserOld.inc
files[] = includes/DateIcalIcalcreatorParser.inc
files[] = includes/DateIcalDateModuleParser.inc
; Tests
files[] = tests/date_ical_parser.test
files[] = tests/date_ical_parser_text.test
files[] = tests/date_ical_parser_link.test
files[] = tests/date_ical_parser_date.test
files[] = tests/date_ical_parser_location.test
files[] = tests/date_ical_parser_categories.test
......@@ -3,21 +3,37 @@
/**
* @file
* Adds ical functionality to Views, and an iCal parser to Feeds.
*
* TODO Figure out how to incorporate VVENUE information into the parser.
*/
/**
* The version number of the current release. This is inserted into the PRODID
* value of the iCal feeds created by Date iCal.
*/
define('DATE_ICAL_VERSION', '2.13-dev');
define('DATE_ICAL_VERSION', '3.0-dev');
/**
* Exception for when the date field for a row in the ical_fields row plugin is blank.
*/
class BlankDateFieldException extends Exception { }
/**
* Generic DateIcalFeedsParser exceptions.
*/
class DateIcalException extends Exception {}
/**
* DateIcalFeedsParser failed to parse some part of iCal.
*/
class DateIcalParseException extends DateIcalException {}
/**
* Implements hook_hook_info().
*/
function date_ical_hook_info() {
// TODO: Finish this.
// Use two "groups": date_ical_parse and date_ical_output.
}
/**
* Implements hook_views_api().
*/
......@@ -134,7 +150,7 @@ function date_ical_libraries_info() {
}
/**
* Implementation of hook_ctools_plugin_api().
* Implements hook_ctools_plugin_api().
*/
function date_ical_ctools_plugin_api($owner, $api) {
if ($owner == 'feeds' && $api == 'plugins') {
......@@ -143,32 +159,41 @@ function date_ical_ctools_plugin_api($owner, $api) {
}
/**
* Implementation of ctools plugin for feeds hook_feeds_plugins().
* Implements hook_feeds_plugins().
*/
function date_ical_feeds_plugins() {
$path = drupal_get_path('module', 'date_ical') . '/includes';
$info = array();
$info['DateIcalFeedsParser'] = array(
$info['DateIcalFeedsParserOld'] = array(
'hidden' => TRUE,
'handler' => array(
'parent' => 'FeedsParser',
'class' => 'DateIcalFeedsParser',
'file' => 'DateIcalFeedsParser.inc',
'class' => 'DateIcalFeedsParserOld',
'file' => 'DateIcalFeedsParserOld.inc',
'path' => $path,
),
);
$info['DateIcalIcalcreatorParser'] = array(
'name' => 'iCal parser',
'name' => 'iCal parser (old)',
'description' => t('Use the iCalcreator library to parse iCal feeds.'),
'help' => 'Parse iCal feeds.',
'handler' => array(
'parent' => 'DateIcalFeedsParser',
'parent' => 'DateIcalFeedsParserOld',
'class' => 'DateIcalIcalcreatorParser',
'file' => 'DateIcalIcalcreatorParser.inc',
'path' => $path,
),
);
$info['DateiCalFeedsParser'] = array(
'name' => 'iCal parser',
'description' => t('Parse iCal feeds.'),
'handler' => array(
'parent' => 'FeedsParser',
'class' => 'DateiCalFeedsParser',
'file' => 'DateiCalFeedsParser.inc',
'path' => $path,
),
);
return $info;
}
......
This diff is collapsed.
<?php
/**
* @file
* Basic classes.
*/
/**
* Parent class for Feeds integration.
*/
abstract class DateIcalFeedsParserOld extends FeedsParser {
/**
* The output sources the parser offers.
*
* array(
* 'feeds_output_key' => array(
* 'name' => 'Human readable name of output source.',
* 'description' => 'Longer description of source.',
* 'date_ical_parse_handler' => 'Method callback for parsing source before handing to feeds.',
* ),
* );
*/
static protected $sources;
/**
* Implementation of FeedsParser::getMappingSources().
*/
public function getMappingSources() {
// Quirky work around.
// Want to have the sources as a property of the class,
// but can't declare them with t().
$sources = $this::$sources;
foreach ($sources as &$source) {
foreach ($source as $key => &$value) {
if ($key == 'name' || $key == 'description') {
$value = t($value);
}
}
}
// Call parent::getMappingSources() to trigger additional target creation.
return $sources + parent::getMappingSources();
}
/**
* Override FeedsParser::getSourceElement().
*/
public function getSourceElement(FeedsSource $source, FeedsParserResult $result, $property_key) {
// Allow parent method to handle any mappings based on the parent node.
if (substr($property_key, 0, 7) == 'parent:') {
return parent::getSourceElement($source, $result, $property_key);
}
// Otherwise retrieve the current DateIcalComponent from the DateIcalParserResult.
if ($item = $result->currentItem()) {
if ($position = strpos($property_key, ':')) {
$key = substr($property_key, 0, $position);
$attribute = substr($property_key, ++$position);
}
else {
$key = $property_key;
}
// and use listed handler to get source output
$handler = $this::$sources[$property_key]['date_ical_parse_handler'];
$property = $item->getProperty($key);
if (empty($property)) {
// $property will be empty if the mapping is set up to parse optional source
// components (e.g. RRULE), and this particular VEVENT doesn't have one.
return '';
}
else {
return $this->$handler($property_key, $property, $item, $result, $source);
}
}
}
}
interface DateIcalComponentInterface {
public function getComponentType();
public function setProperty($name, $args);
public function getProperty($name);
}
class DateIcalParserResult extends FeedsParserResult {
// Feed extension timezone (X-WR-TIMEZONE)
public $timezone;
// Feed RFC 5545 timezones; we can't use these at
// present, only PHP timezonedb tz will be actually recognized.
// So this is more here as reminder/explanation.
public $timezones;
}
......@@ -3,9 +3,11 @@
/**
* @file
* Classes implementing Date iCal's iCalcreator-based parser functionality.
*
* @TODO: Figure out how to incorporate VVENUE information into the parser.
*/
class DateIcalIcalcreatorParser extends DateIcalFeedsParser {
class DateIcalIcalcreatorParser extends DateIcalFeedsParserOld {
/**
* Output sources this parser offers.
......@@ -123,22 +125,18 @@ class DateIcalIcalcreatorParser extends DateIcalFeedsParser {
}
}
// DEV NOTES:
// Due to the way that the loop after this one manipulates the $components array, all the work that gets done in here
// gets overridden. However, we probably *should* be using this, somehow, since I think it was in the old version
// in order to handle non-standard VTIMEZONES. Maybe?
$components = array();
while ($component = $calendar->getComponent('VTIMEZONE')) {
$components[$component->getProperty('tzid')] = new DateIcalIcalcreatorComponent($component);
$timezones = array();
while ($raw_component = $calendar->getComponent('VTIMEZONE')) {
$timezones[$raw_component->getProperty('tzid')] = new DateIcalIcalcreatorComponent($raw_component);
}
$result->timezones = $components;
$result->timezones = $timezones;
// Separate the individual feed items into DateIcalIcalcreatorComponents.
$components = array();
$component_types = array('vevent', 'vtodo', 'vjournal', 'vfreebusy', 'valarm');
foreach ($component_types as $component_type) {
while ($component = $calendar->getComponent($component_type)) {
$component = new DateIcalIcalcreatorComponent($component);
while ($raw_component = $calendar->getComponent($component_type)) {
$component = new DateIcalIcalcreatorComponent($raw_component);
// Allow modules to alter the DateIcalIcalcreatorComponent before we
// parse it into Feeds-readable data.
......
......@@ -207,8 +207,8 @@ class date_ical_plugin_row_ical_entity extends views_plugin_row {
}
// Set the item date to the proper display timezone.
$start->setTimezone(new dateTimezone($date_field['timezone']));
$end->setTimezone(new dateTimezone($date_field['timezone']));
$start->setTimezone(new DateTimeZone($date_field['timezone']));
$end->setTimezone(new DateTimeZone($date_field['timezone']));
// Check if the start and end dates indicate that this is an All Day event.
$all_day = date_is_all_day(
......@@ -304,7 +304,7 @@ class date_ical_plugin_row_ical_entity extends views_plugin_row {
'entity_type' => $this->entity_type,
'language' => $this->language,
);
drupal_alter('date_ical_html', $data, $this->view, $context);
drupal_alter('date_ical_export_html', $data, $this->view, $context);
$event = array();
$event['summary'] = date_ical_sanitize_text($data['summary']);
......@@ -333,23 +333,23 @@ class date_ical_plugin_row_ical_entity extends views_plugin_row {
// According to the iCal standard, CREATED and LAST-MODIFIED must be UTC.
// Fortunately, Drupal stores timestamps in the DB as UTC, so we just need
// to specify that UTC be used rather than the server's local timezone.
// to tell Date to treat the timestamp as UTC from the start.
if (isset($entity->created)) {
$event['created'] = new DateObject($entity->created, new DateTimeZone('UTC'));
$event['created'] = new DateObject($entity->created, 'UTC');
}
// Pull the 'changed' date from the entity (if available), so that
// subscription clients can tell if the event has been updated.
if (isset($entity->changed)) {
$event['last-modified'] = new DateObject($entity->changed, new DateTimeZone('UTC'));
$event['last-modified'] = new DateObject($entity->changed, 'UTC');
}
else if (isset($entity->created)) {
// If changed is unset, but created is, use that for last-modified.
$event['last-modified'] = new DateObject($entity->created, new DateTimeZone('UTC'));
$event['last-modified'] = new DateObject($entity->created, 'UTC');
}
// Allow other modules to alter the structured event object, before it gets
// passed to the style plugin to be converted into an iCal VEVENT.
drupal_alter('date_ical_feed_event_render', $event, $this->view, $context);
// passed to the style plugin to be converted into an iCalcreator vevent.
drupal_alter('date_ical_export_raw_event', $event, $this->view, $context);
return $event;
}
......
......@@ -131,14 +131,14 @@ class date_ical_plugin_row_ical_fields extends views_plugin_row {
$entity = $row->_field_data[$this->view->base_field]['entity'];
$entity_type = $row->_field_data[$this->view->base_field]['entity_type'];
if (isset($entity->created)) {
$event['created'] = new DateObject($entity->created, new DateTimeZone('UTC'));
$event['created'] = new DateObject($entity->created, 'UTC');
}
if (isset($entity->changed)) {
$event['last-modified'] = new DateObject($entity->changed, new DateTimeZone('UTC'));
$event['last-modified'] = new DateObject($entity->changed, 'UTC');
}
else if (isset($entity->created)) {
// If changed is unset, but created is, use that for last-modified.
$event['last-modified'] = new DateObject($entity->created, new DateTimeZone('UTC'));
$event['last-modified'] = new DateObject($entity->created, 'UTC');
}
$uri = entity_uri($entity_type, $entity);
$uri['options']['absolute'] = TRUE;
......@@ -193,8 +193,9 @@ class date_ical_plugin_row_ical_fields extends views_plugin_row {
'row' => $row,
'row_index' => $row_index,
'language' => $this->language,
'options' => $this->options,
);
drupal_alter('date_ical_fields_html', $text_fields, $this->view, $context);
drupal_alter('date_ical_export_html', $text_fields, $this->view, $context);
// Sanitize the text fields for iCal compliance, and add them to the event.
// Also strip all HTML from the summary and location fields, since they
......@@ -205,12 +206,7 @@ class date_ical_plugin_row_ical_fields extends views_plugin_row {
// Allow other modules to alter the event object, before it gets passed to
// the style plugin to be converted into an iCal VEVENT.
$context = array(
'entity' => $entity,
'entity_type' => $entity_type,
'language' => $this->language,
);
drupal_alter('date_ical_feed_event_render', $event, $this->view, $context);
drupal_alter('date_ical_export_raw_event', $event, $this->view, $context);
return $event;
}
......@@ -242,7 +238,7 @@ class date_ical_plugin_row_ical_fields extends views_plugin_row {
$date_field = $date_field_value[$delta];
$start = new DateObject($date_field['value'], $date_field['timezone_db']);
if (array_key_exists('value2', $date_field)) {
if (!empty($date_field['value2'])) {
$end = new DateObject($date_field['value2'], $date_field['timezone_db']);
}
else {
......@@ -254,7 +250,7 @@ class date_ical_plugin_row_ical_fields extends views_plugin_row {
}
}
elseif (is_numeric($date_field_value)) {
// Handle timestamps, which are always saved in UTC.
// Handle timestamps, which are always in UTC.
$start = new DateObject($date_field_value, 'UTC');
$end = new DateObject($date_field_value, 'UTC');
}
......@@ -330,9 +326,11 @@ class date_ical_plugin_row_ical_fields extends views_plugin_row {
* Retrieves a field value from the style plugin.
*
* @param int $index
* The index count of the row @see views_plugin_style::get_field().
* The index count of the row
* @param string $field_id
* The ID assigned to the required field in the display.
*
* @see views_plugin_style::get_field().
*/
function get_field($index, $field_id) {
if (empty($this->view->style_plugin) || !is_object($this->view->style_plugin) || empty($field_id)) {
......
......@@ -54,6 +54,7 @@ class date_ical_plugin_style_ical_feed extends views_plugin_style {
$options['no_calname'] = array('default' => FALSE, 'bool' => TRUE);
$options['disable_webcal'] = array('default' => FALSE, 'bool' => TRUE);
$options['exclude_dtstamp'] = array('default' => FALSE, 'bool' => TRUE);
$options['unescape_punctuation'] = array('default' => FALSE, 'bool' => TRUE);
return $options;
}
......@@ -92,6 +93,14 @@ class date_ical_plugin_style_ical_feed extends views_plugin_style {
conclcude that the event has been updated (even though it hasn't actually changed). Enable this option to exclude the DTSTAMP
field from your feeds, so that these buggy feed readers won't mark every event as updated every time they check."),
);
$form['unescape_punctuation'] = array(
'#type' => 'checkbox',
'#title' => t('Unescape Commas and Semicolons'),
'#default_value' => $this->_get_option('unescape_punctuation'),
'#description' => t('In order to comply with the iCal spec, Date iCal will "escape" commas and semicolons (prepend them with backslashes).
However, many calendar clients are bugged to not unescape these characters, leaving the backslashes littered throughout your events\' text fields.
Enable this option to have Date iCal unescape these characters before it exports the iCal feed.'),
);
}
function render() {
......@@ -160,11 +169,11 @@ class date_ical_plugin_style_ical_feed extends views_plugin_style {
$vevent = $vcalendar->newComponent('vevent');
// Get the start date as an array.
$start = $event['start']->toArray();
$timezone = $event['start']->getTimezone()->getName();
$timezones[$timezone] = $timezone;
$start_timezone = $event['start']->getTimezone()->getName();
$timezones[$start_timezone] = $start_timezone;
if ($event['all_day']) {
// All day events need to be specified as DATE, rather than DATE-TIME, or they get interpretted wrong.
// All Day events need to be specified as DATE, rather than DATE-TIME, or they get interpretted wrong.
$vevent->setDtstart($start['year'], $start['month'], $start['day'],
FALSE, FALSE, FALSE, FALSE, array('VALUE' => 'DATE'));
}
......@@ -176,7 +185,7 @@ class date_ical_plugin_style_ical_feed extends views_plugin_style {
$start['hour'],
$start['minute'],
$start['second'],
$timezone
$start_timezone
);
}
......@@ -186,8 +195,8 @@ class date_ical_plugin_style_ical_feed extends views_plugin_style {
// Only add the end date if there is one.
if (!empty($event['end'])) {
$end = $event['end']->toArray();
$timezone = $event['end']->getTimezone()->getName();
$timezones[$timezone] = $timezone;
$end_timezone = $event['end']->getTimezone()->getName();
$timezones[$end_timezone] = $end_timezone;
if ($event['all_day']) {
$vevent->setDtend($end['year'], $end['month'], $end['day'],
......@@ -201,7 +210,7 @@ class date_ical_plugin_style_ical_feed extends views_plugin_style {
$end['hour'],
$end['minute'],
$end['second'],
$timezone
$end_timezone
);
}
$end['tz'] = $event['end']->getTimezone();
......@@ -332,8 +341,8 @@ class date_ical_plugin_style_ical_feed extends views_plugin_style {
);
}
// Allow other modules to alter the VEVENT object.
drupal_alter('date_ical_feed_ical_vevent_render', $vevent, $this->view, $event);
// Allow other modules to alter the vevent before it's exported.
drupal_alter('date_ical_export_vevent', $vevent, $this->view, $event);
}
// Now add to the calendar all the timezones used by the events.
......@@ -343,16 +352,20 @@ class date_ical_plugin_style_ical_feed extends views_plugin_style {
}
}
// Allow other modules to alter the calendar as a whole.
drupal_alter('date_ical_feed_ical_vcalendar_render', $vcalendar, $this->view);
// Allow other modules to alter the vcalendar before it's exported.
drupal_alter('date_ical_export_vcalendar', $vcalendar, $this->view);
$output = $vcalendar->createCalendar();
// For some unknown reason (overzealous spec compliance?), iCalcreator
// escapes all commas and semicolons in the strings that it outputs.
// This unescapes them, though it does it with a pretty big hammer. This
// might be going too far.
// iCalcreator escapes all commas and semicolons in the strings that it,
// as the spec demands. However, some calendar clients are buggy and
// don't unescape these characters. Users may choose to unescape them
// here to sidestep those clients' bugs.
// NOTE: This results in a non-compliant iCal feed, but it seems like a
// LOT of major clients are bugged this way.
if ($this->_get_option('unescape_punctuation')) {
$output = str_replace('\,', ',', $output);
$output = str_replace('\;', ';', $output);
}
// In order to respect the Exclude DTSTAMP option, we unfortunately have
// to parse out the DTSTAMP properties after they get rendered. Simply
......@@ -388,8 +401,8 @@ class date_ical_plugin_style_ical_feed extends views_plugin_style {
}
}
// Allow other modules to alter the rendered calendar, just before it gets sent out.
drupal_alter('date_ical_post_render', $output, $this->view);
// Allow other modules to alter the rendered calendar.
drupal_alter('date_ical_export_post_render', $output, $this->view);
return $output;
}
......
BEGIN:VCALENDAR
PRODID:-//Google Inc//Google Calendar 70.9054//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-CALNAME:Events Feed
X-WR-TIMEZONE:America/New_York
X-WR-CALDESC:A calendar for the events feed
BEGIN:VTIMEZONE
TZID:America/New_York
X-LIC-LOCATION:America/New_York
BEGIN:DAYLIGHT
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
TZNAME:EDT
DTSTART:19700308T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:-0400
TZOFFSETTO:-0500
TZNAME:EST
DTSTART:19701101T020000
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
SUMMARY:UTC Event
DTSTART:20131020T000000Z
DTEND:20131020T020000Z
UID:date_ical_basic_test01
DESCRIPTION:This is a standard 2-hour event using UTC.
END:VEVENT
BEGIN:VEVENT
SUMMARY:America/New_York Event
DTSTART;TZID=America/New_York:20131009T190000
DTEND;TZID=America/New_York:20131009T210000
UID:date_ical_basic_test02
DESCRIPTION:This is a standard 2-hour event in America/New_York.
END:VEVENT
BEGIN:VEVENT
SUMMARY:Broken Event (Central Standard Time)
DTSTART;TZID=Central Standard Time:20131009T190000
DTEND;TZID=Central Standard Time:20131009T210000
UID:date_ical_basic_test03
DESCRIPTION:This event uses a deprecated TZID, Central Standard Time, and should be treated as UTC.
END:VEVENT
BEGIN:VEVENT
SUMMARY:Event with no DTEND or DURATION
DTSTART;TZID=America/New_York:20131009T190000
UID:date_ical_basic_test04
DESCRIPTION:This event doesn't have an end time.
END:VEVENT
BEGIN:VEVENT
SUMMARY:Event with no DTEND, but with 45-min DURATION
DTSTART;TZID=America/New_York:20131009T190000
DURATION:PT45M
UID:date_ical_basic_test05
DESCRIPTION:This event uses DURATION for end time, instead of DTEND.
END:VEVENT
BEGIN:VEVENT
SUMMARY:Event with no TZID (should use X-WR-TIMEZONE)
DTSTART:20131009T190000
DURATION:PT45M
UID:date_ical_basic_test06
DESCRIPTION:This event defines no TZID, so the parser should fall back onto the X-WR-TIMEZONE.
END:VEVENT
BEGIN:VEVENT
SUMMARY:Event with no DESCRIPTION
DTSTART;TZID=America/New_York:20131009T190000
DTEND;TZID=America/New_York:20131009T210000
UID:date_ical_basic_test07
END:VEVENT
BEGIN:VEVENT
SUMMARY:Event with LOCATION + ALTREP
DTSTART;TZID=America/New_York:20131009T190000
DTEND;TZID=America/New_York:20131009T210000
UID:date_ical_basic_test08
DESCRIPTION:This event has a LOCATION with an ALTREP.
LOCATION;ALTREP="http://www.example.com":Example's Place
END:VEVENT
BEGIN:VEVENT
SUMMARY:Event with 1 CATEGORIES
DTSTART;TZID=America/New_York:20131009T190000
DTEND;TZID=America/New_York:20131009T210000
UID:date_ical_basic_test09
DESCRIPTION:This event has a single CATEGORIES value.
CATEGORIES:Category 1
END:VEVENT
BEGIN:VEVENT
SUMMARY:Event with multiple CATEGORIES
DTSTART;TZID=America/New_York:20131009T190000
DTEND;TZID=America/New_York:20131009T210000
UID:date_ical_basic_test0A
DESCRIPTION:This event has several CATEGORIES values.
CATEGORIES:Category 1,Category 2,Category 3
END:VEVENT
BEGIN:VEVENT
SUMMARY:Event with a URL
DTSTART;TZID=America/New_York:20131009T190000
DTEND;TZID=America/New_York:20131009T210000
UID:date_ical_basic_test0B
DESCRIPTION:This event has a URL.
URL:http://www.example.com
END:VEVENT
BEGIN:VEVENT
SUMMARY:Event with escaped characters in DESCRIPTION
DTSTART;TZID=America/New_York:20131009T190000
DTEND;TZID=America/New_York:20131009T210000
UID:date_ical_basic_test0B
DESCRIPTION:This event has several escaped characters right here:\,\;\\\n\NThis text should be 2 lines down from the rest.
END:VEVENT
END:VALENDAR
BEGIN:VCALENDAR
PRODID:-//Google Inc//Google Calendar 70.9054//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
BEGIN:VEVENT
SUMMARY:One-day Date-type Event
DTSTART;VALUE=DATE:20131020
DTEND;VALUE=DATE:20131021
UID:date_ical_date_type_test01
DESCRIPTION:This is an all day event that takes place on Oct 20, 2013.
END:VEVENT
BEGIN:VEVENT
SUMMARY:Two-day Date-type Event
DTSTART;VALUE=DATE:20131020
DTEND;VALUE=DATE:20131022
UID:date_ical_date_type_test02
DESCRIPTION:This all day event runs from Oct 20-21, 2013.
END:VEVENT
BEGIN:VEVENT
SUMMARY:Date-type Event w/ 1-day DURATION
DTSTART;VALUE=DATE:20131020
DURATION:P1D
UID:date_ical_date_type_test03
DESCRIPTION:This all day event starts on Oct 20, 2013 and lasts 1 day.
END:VEVENT
BEGIN:VEVENT
SUMMARY:Date-type Event w/ 2-day DURATION
DTSTART;VALUE=DATE:20131020
DURATION:P2D
UID:date_ical_date_type_test04
DESCRIPTION:This all day event starts on Oct 20, 2013 and lasts 2 days.
END:VEVENT
BEGIN:VEVENT
SUMMARY:Date-type Event w/ no DTEND or DURATION
DTSTART;VALUE=DATE:20131020
UID:date_ical_date_type_test05
DESCRIPTION:This all day event defines no ending, and should thus be 1 day.
END:VEVENT
END:VALENDAR
BEGIN:VCALENDAR
PRODID:-//Google Inc//Google Calendar 70.9054//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
BEGIN:VEVENT
SUMMARY:Invalid Event: no DTSTART
UID:date_ical_test_no_dtstart
END:VEVENT
END:VALENDAR