/** * Exports this task in iCalendar format. * * @param Horde_Icalendar $calendar A Horde_Icalendar object that acts as * the container. * * @return Horde_Icalendar_Vtodo A vtodo component of this task. */ public function toiCalendar(Horde_Icalendar $calendar) { $vTodo = Horde_Icalendar::newComponent('vtodo', $calendar); $v1 = $calendar->getAttribute('VERSION') == '1.0'; $vTodo->setAttribute('UID', $this->uid); if (!empty($this->assignee)) { $vTodo->setAttribute('ATTENDEE', Nag::getUserEmail($this->assignee), array('ROLE' => 'REQ-PARTICIPANT')); } $vTodo->setAttribute('ORGANIZER', !empty($this->organizer) ? Nag::getUserEmail($this->organizer) : Nag::getUserEmail($this->owner)); if (!empty($this->name)) { $vTodo->setAttribute('SUMMARY', $this->name); } if (!empty($this->desc)) { $vTodo->setAttribute('DESCRIPTION', $this->desc); } if (isset($this->priority)) { $vTodo->setAttribute('PRIORITY', $this->priority); } if (!empty($this->parent_id) && !empty($this->parent)) { $vTodo->setAttribute('RELATED-TO', $this->parent->uid); } if ($this->private) { $vTodo->setAttribute('CLASS', 'PRIVATE'); } if (!empty($this->start)) { $vTodo->setAttribute('DTSTART', $this->start); } if ($this->due) { $vTodo->setAttribute('DUE', $this->due); if ($this->alarm) { if ($v1) { $vTodo->setAttribute('AALARM', $this->due - $this->alarm * 60); } else { $vAlarm = Horde_Icalendar::newComponent('valarm', $vTodo); $vAlarm->setAttribute('ACTION', 'DISPLAY'); $vAlarm->setAttribute('TRIGGER;VALUE=DURATION', '-PT' . $this->alarm . 'M'); $vTodo->addComponent($vAlarm); } } } if ($this->completed) { $vTodo->setAttribute('STATUS', 'COMPLETED'); $vTodo->setAttribute('COMPLETED', $this->completed_date ? $this->completed_date : $_SERVER['REQUEST_TIME']); $vTodo->setAttribute('PERCENT-COMPLETE', '100'); } else { if (!empty($this->estimate)) { $vTodo->setAttribute('PERCENT-COMPLETE', $this->actual / $this->estimate * 100); } if ($v1) { $vTodo->setAttribute('STATUS', 'NEEDS ACTION'); } else { $vTodo->setAttribute('STATUS', 'NEEDS-ACTION'); } } // Recurrence. // We may have to implicitely set DTSTART if not set explicitely, may // some clients choke on missing DTSTART attributes while RRULE exists. if ($this->recurs()) { if ($v1) { $rrule = $this->recurrence->toRRule10($calendar); } else { $rrule = $this->recurrence->toRRule20($calendar); } if (!empty($rrule)) { $vTodo->setAttribute('RRULE', $rrule); } /* The completions represent deleted recurrences */ foreach ($this->recurrence->getCompletions() as $exception) { if (!empty($exception)) { // Use multiple EXDATE attributes instead of EXDATE // attributes with multiple values to make Apple iCal // happy. list($year, $month, $mday) = sscanf($exception, '%04d%02d%02d'); $vTodo->setAttribute('EXDATE', array(new Horde_Date($year, $month, $mday)), array('VALUE' => 'DATE')); } } } if ($this->tags) { $vTodo->setAttribute('CATEGORIES', implode(', ', $this->tags)); } /* Get the task's history. */ $created = $modified = null; try { $log = $GLOBALS['injector']->getInstance('Horde_History')->getHistory('nag:' . $this->tasklist . ':' . $this->uid); foreach ($log as $entry) { switch ($entry['action']) { case 'add': $created = $entry['ts']; break; case 'modify': $modified = $entry['ts']; break; } } } catch (Exception $e) { } if (!empty($created)) { $vTodo->setAttribute($v1 ? 'DCREATED' : 'CREATED', $created); if (empty($modified)) { $modified = $created; } } if (!empty($modified)) { $vTodo->setAttribute('LAST-MODIFIED', $modified); } return $vTodo; }
/** * Exports this event in iCalendar format. * * @param Horde_Icalendar $calendar A Horde_Icalendar object that acts as * a container. * * @return array An array of Horde_Icalendar_Vevent objects for this event. */ public function toiCalendar($calendar) { $vEvent = Horde_Icalendar::newComponent('vevent', $calendar); $v1 = $calendar->getAttribute('VERSION') == '1.0'; $vEvents = array(); // For certain recur types, we must output in the event's timezone // so that the BYDAY values do not get out of sync with the UTC // date-time. See Bug: 11339 if ($this->recurs()) { switch ($this->recurrence->getRecurType()) { case Horde_Date_Recurrence::RECUR_WEEKLY: case Horde_Date_Recurrence::RECUR_YEARLY_WEEKDAY: case Horde_Date_Recurrence::RECUR_MONTHLY_WEEKDAY: if (!$this->timezone) { $this->timezone = date_default_timezone_get(); } } } if ($this->isAllDay()) { $vEvent->setAttribute('DTSTART', $this->start, array('VALUE' => 'DATE')); $vEvent->setAttribute('DTEND', $this->end, array('VALUE' => 'DATE')); $vEvent->setAttribute('X-FUNAMBOL-ALLDAY', 1); } else { $this->setTimezone(true); $params = array(); if ($this->timezone) { try { if (!$this->baseid) { $tz = $GLOBALS['injector']->getInstance('Horde_Timezone'); $vEvents[] = $tz->getZone($this->timezone)->toVtimezone(); } $params['TZID'] = $this->timezone; } catch (Horde_Exception $e) { Horde::log('Unable to locate the tz database.', 'WARN'); } } $vEvent->setAttribute('DTSTART', clone $this->start, $params); $vEvent->setAttribute('DTEND', clone $this->end, $params); } $vEvent->setAttribute('DTSTAMP', $_SERVER['REQUEST_TIME']); $vEvent->setAttribute('UID', $this->uid); /* Get the event's create and last modify date. */ $created = $modified = null; try { $history = $GLOBALS['injector']->getInstance('Horde_History'); $created = $history->getActionTimestamp('kronolith:' . $this->calendar . ':' . $this->uid, 'add'); $modified = $history->getActionTimestamp('kronolith:' . $this->calendar . ':' . $this->uid, 'modify'); /* The history driver returns 0 for not found. If 0 or null does * not matter, strip this. */ if ($created == 0) { $created = null; } if ($modified == 0) { $modified = null; } } catch (Exception $e) { } if (!empty($created)) { $vEvent->setAttribute($v1 ? 'DCREATED' : 'CREATED', $created); if (empty($modified)) { $modified = $created; } } if (!empty($modified)) { $vEvent->setAttribute('LAST-MODIFIED', $modified); } $vEvent->setAttribute('SUMMARY', $this->getTitle()); // Organizer if (count($this->attendees)) { $name = Kronolith::getUserName($this->creator); $email = Kronolith::getUserEmail($this->creator); $params = array(); if ($v1) { $tmp = new Horde_Mail_Rfc822_Address($email); if (!empty($name)) { $tmp->personal = $name; } $email = strval($tmp); } else { if (!empty($name)) { $params['CN'] = $name; } if (!empty($email)) { $email = 'mailto:' . $email; } } $vEvent->setAttribute('ORGANIZER', $email, $params); } if (!$this->isPrivate()) { if (!empty($this->description)) { $vEvent->setAttribute('DESCRIPTION', $this->description); } // Tags if ($this->tags) { $tags = implode(', ', $this->tags); $vEvent->setAttribute('CATEGORIES', $tags); } // Location if (!empty($this->location)) { $vEvent->setAttribute('LOCATION', $this->location); } if ($this->geoLocation) { $vEvent->setAttribute('GEO', array('latitude' => $this->geoLocation['lat'], 'longitude' => $this->geoLocation['lon'])); } // URL if (!empty($this->url)) { $vEvent->setAttribute('URL', $this->url); } } $vEvent->setAttribute('CLASS', $this->private ? 'PRIVATE' : 'PUBLIC'); // Status. switch ($this->status) { case Kronolith::STATUS_FREE: // This is not an official iCalendar value, but we need it for // synchronization. $vEvent->setAttribute('STATUS', 'FREE'); $vEvent->setAttribute('TRANSP', $v1 ? 1 : 'TRANSPARENT'); break; case Kronolith::STATUS_TENTATIVE: $vEvent->setAttribute('STATUS', 'TENTATIVE'); $vEvent->setAttribute('TRANSP', $v1 ? 0 : 'OPAQUE'); break; case Kronolith::STATUS_CONFIRMED: $vEvent->setAttribute('STATUS', 'CONFIRMED'); $vEvent->setAttribute('TRANSP', $v1 ? 0 : 'OPAQUE'); break; case Kronolith::STATUS_CANCELLED: if ($v1) { $vEvent->setAttribute('STATUS', 'DECLINED'); $vEvent->setAttribute('TRANSP', 1); } else { $vEvent->setAttribute('STATUS', 'CANCELLED'); $vEvent->setAttribute('TRANSP', 'TRANSPARENT'); } break; } // Attendees. foreach ($this->attendees as $email => $status) { $params = array(); switch ($status['attendance']) { case Kronolith::PART_REQUIRED: if ($v1) { $params['EXPECT'] = 'REQUIRE'; } else { $params['ROLE'] = 'REQ-PARTICIPANT'; } break; case Kronolith::PART_OPTIONAL: if ($v1) { $params['EXPECT'] = 'REQUEST'; } else { $params['ROLE'] = 'OPT-PARTICIPANT'; } break; case Kronolith::PART_NONE: if ($v1) { $params['EXPECT'] = 'FYI'; } else { $params['ROLE'] = 'NON-PARTICIPANT'; } break; } switch ($status['response']) { case Kronolith::RESPONSE_NONE: if ($v1) { $params['STATUS'] = 'NEEDS ACTION'; $params['RSVP'] = 'YES'; } else { $params['PARTSTAT'] = 'NEEDS-ACTION'; $params['RSVP'] = 'TRUE'; } break; case Kronolith::RESPONSE_ACCEPTED: if ($v1) { $params['STATUS'] = 'ACCEPTED'; } else { $params['PARTSTAT'] = 'ACCEPTED'; } break; case Kronolith::RESPONSE_DECLINED: if ($v1) { $params['STATUS'] = 'DECLINED'; } else { $params['PARTSTAT'] = 'DECLINED'; } break; case Kronolith::RESPONSE_TENTATIVE: if ($v1) { $params['STATUS'] = 'TENTATIVE'; } else { $params['PARTSTAT'] = 'TENTATIVE'; } break; } if (strpos($email, '@') === false) { $email = ''; } if ($v1) { if (empty($email)) { if (!empty($status['name'])) { $email = $status['name']; } } else { $tmp = new Horde_Mail_Rfc822_Address($email); if (!empty($status['name'])) { $tmp->personal = $status['name']; } $email = strval($tmp); } } else { if (!empty($status['name'])) { $params['CN'] = $status['name']; } if (!empty($email)) { $email = 'mailto:' . $email; } } $vEvent->setAttribute('ATTENDEE', $email, $params); } // Alarms. if (!empty($this->alarm)) { if ($v1) { $alarm = new Horde_Date($this->start); $alarm->min -= $this->alarm; $vEvent->setAttribute('AALARM', $alarm); } else { $vAlarm = Horde_Icalendar::newComponent('valarm', $vEvent); $vAlarm->setAttribute('ACTION', 'DISPLAY'); $vAlarm->setAttribute('DESCRIPTION', $this->getTitle()); $vAlarm->setAttribute('TRIGGER;VALUE=DURATION', ($this->alarm > 0 ? '-' : '') . 'PT' . abs($this->alarm) . 'M'); $vEvent->addComponent($vAlarm); } $hordeAlarm = $GLOBALS['injector']->getInstance('Horde_Alarm'); if ($hordeAlarm->exists($this->uid, $GLOBALS['registry']->getAuth()) && $hordeAlarm->isSnoozed($this->uid, $GLOBALS['registry']->getAuth())) { $vEvent->setAttribute('X-MOZ-LASTACK', new Horde_Date($_SERVER['REQUEST_TIME'])); $alarm = $hordeAlarm->get($this->uid, $GLOBALS['registry']->getAuth()); if (!empty($alarm['snooze'])) { $alarm['snooze']->setTimezone(date_default_timezone_get()); $vEvent->setAttribute('X-MOZ-SNOOZE-TIME', $alarm['snooze']); } } } // Recurrence. if ($this->recurs()) { if ($v1) { $rrule = $this->recurrence->toRRule10($calendar); } else { $rrule = $this->recurrence->toRRule20($calendar); } if (!empty($rrule)) { $vEvent->setAttribute('RRULE', $rrule); } // Exceptions. An exception with no replacement event is represented // by EXDATE, and those with replacement events are represented by // a new vEvent element. We get all known replacement events first, // then remove the exceptionoriginaldate from the list of the event // exceptions. Any exceptions left should represent exceptions with // no replacement. $exceptions = $this->recurrence->getExceptions(); $kronolith_driver = Kronolith::getDriver(null, $this->calendar); $search = new stdClass(); $search->baseid = $this->uid; $results = $kronolith_driver->search($search); foreach ($results as $days) { foreach ($days as $exceptionEvent) { // Need to change the UID so it links to the original // recurring event, but only if not using $v1. If using $v1, // we add the date to EXDATE and do NOT change the UID. if (!$v1) { $exceptionEvent->uid = $this->uid; } $vEventException = $exceptionEvent->toiCalendar($calendar); // This should never happen, but protect against it anyway. if (count($vEventException) > 2 || count($vEventException) > 1 && !$vEventException[0] instanceof Horde_Icalendar_Vtimezone && !$vEventException[1] instanceof Horde_Icalendar_Vtimezone) { throw new Kronolith_Exception(_("Unable to parse event.")); } $vEventException = array_pop($vEventException); // If $v1, need to add to EXDATE if (!$this->isAllDay()) { $exceptionEvent->setTimezone(true); } if (!$v1) { $vEventException->setAttribute('RECURRENCE-ID', $exceptionEvent->exceptionoriginaldate); } else { $vEvent->setAttribute('EXDATE', array($exceptionEvent->exceptionoriginaldate), array('VALUE' => 'DATE')); } $originaldate = $exceptionEvent->exceptionoriginaldate->format('Ymd'); $key = array_search($originaldate, $exceptions); if ($key !== false) { unset($exceptions[$key]); } $vEvents[] = $vEventException; } } /* The remaining exceptions represent deleted recurrences */ foreach ($exceptions as $exception) { if (!empty($exception)) { // Use multiple EXDATE attributes instead of EXDATE // attributes with multiple values to make Apple iCal // happy. list($year, $month, $mday) = sscanf($exception, '%04d%02d%02d'); if ($this->isAllDay()) { $vEvent->setAttribute('EXDATE', array(new Horde_Date($year, $month, $mday)), array('VALUE' => 'DATE')); } else { // Another Apple iCal/Calendar fix. EXDATE is only // recognized if the full datetime is present and matches // the time part given in DTSTART. $params = array(); if ($this->timezone) { $params['TZID'] = $this->timezone; } $exdate = clone $this->start; $exdate->year = $year; $exdate->month = $month; $exdate->mday = $mday; $vEvent->setAttribute('EXDATE', array($exdate), $params); } } } } array_unshift($vEvents, $vEvent); $this->setTimezone(false); return $vEvents; }
public function testBug4626MonthlyByDayRRule() { $rrule = new Horde_Date_Recurrence('2008-04-05 00:00:00'); $rrule->setRecurType(Horde_Date_Recurrence::RECUR_MONTHLY_WEEKDAY); $rrule->setRecurOnDay(Horde_Date::MASK_SATURDAY); $this->assertEquals('MP1 1+ SA #0', $rrule->toRRule10(new Horde_Icalendar())); $this->assertEquals('FREQ=MONTHLY;INTERVAL=1;BYDAY=1SA', $rrule->toRRule20(new Horde_Icalendar())); }