Changeset View
Changeset View
Standalone View
Standalone View
plugins/libcalendaring/libvcalendar.php
Show First 20 Lines • Show All 143 Lines • ▼ Show 20 Line(s) | |||||
$count = substr_count($vcal, 'BEGIN:VEVENT') + substr_count($vcal, 'BEGIN:VTODO'); | $count = substr_count($vcal, 'BEGIN:VEVENT') + substr_count($vcal, 'BEGIN:VTODO'); | ||||
$expected_memory = $count * 70*1024; // assume ~ 70K per event (empirically determined) | $expected_memory = $count * 70*1024; // assume ~ 70K per event (empirically determined) | ||||
if (!rcube_utils::mem_check($expected_memory)) { | if (!rcube_utils::mem_check($expected_memory)) { | ||||
throw new Exception("iCal file too big"); | throw new Exception("iCal file too big"); | ||||
} | } | ||||
} | } | ||||
$vobject = VObject\Reader::read($vcal, VObject\Reader::OPTION_FORGIVING | VObject\Reader::OPTION_IGNORE_INVALID_LINES); | $vobject = Sabre\VObject\Reader::read($vcal, Sabre\VObject\Reader::OPTION_FORGIVING | Sabre\VObject\Reader::OPTION_IGNORE_INVALID_LINES); | ||||
machniak: We have `use \Sabre\VObject` in the file, so it should work without this change. Maybe the… | |||||
if ($vobject) | if ($vobject) | ||||
return $this->import_from_vobject($vobject); | return $this->import_from_vobject($vobject); | ||||
} | } | ||||
catch (Exception $e) { | catch (Exception $e) { | ||||
if ($forward_exceptions) { | if ($forward_exceptions) { | ||||
throw $e; | throw $e; | ||||
} | } | ||||
else { | else { | ||||
▲ Show 20 Lines • Show All 81 Lines • ▼ Show 20 Line(s) | |||||
// parse the vevent block surrounded with the vcalendar heading | // parse the vevent block surrounded with the vcalendar heading | ||||
if (strlen($buffer) && preg_match('/BEGIN:(VEVENT|VTODO|VFREEBUSY)/i', $buffer)) { | if (strlen($buffer) && preg_match('/BEGIN:(VEVENT|VTODO|VFREEBUSY)/i', $buffer)) { | ||||
try { | try { | ||||
$this->import($this->vhead . $buffer . "END:VCALENDAR", $this->charset, true, false); | $this->import($this->vhead . $buffer . "END:VCALENDAR", $this->charset, true, false); | ||||
} | } | ||||
catch (Exception $e) { | catch (Exception $e) { | ||||
if ($this->forward_exceptions) { | if ($this->forward_exceptions) { | ||||
throw new VObject\ParseException($e->getMessage() . " in\n" . $buffer); | throw new Sabre\VObject\ParseException($e->getMessage() . " in\n" . $buffer); | ||||
} | } | ||||
else { | else { | ||||
// write the failing section to error log | // write the failing section to error log | ||||
rcube::raise_error(array( | rcube::raise_error(array( | ||||
'code' => 600, 'type' => 'php', | 'code' => 600, 'type' => 'php', | ||||
'file' => __FILE__, 'line' => __LINE__, | 'file' => __FILE__, 'line' => __LINE__, | ||||
'message' => $e->getMessage() . " in\n" . $buffer), | 'message' => $e->getMessage() . " in\n" . $buffer), | ||||
true, false); | true, false); | ||||
▲ Show 20 Lines • Show All 157 Lines • ▼ Show 20 Line(s) | |||||
try { | try { | ||||
if (empty($event[$field]) && !empty($ve->{$attr})) { | if (empty($event[$field]) && !empty($ve->{$attr})) { | ||||
$event[$field] = $ve->{$attr}->getDateTime(); | $event[$field] = $ve->{$attr}->getDateTime(); | ||||
} | } | ||||
} catch (Exception $e) {} | } catch (Exception $e) {} | ||||
} | } | ||||
// map other attributes to internal fields | // map other attributes to internal fields | ||||
foreach ($ve->children as $prop) { | foreach ($ve->children() as $prop) { | ||||
if (!($prop instanceof VObject\Property)) | if (!($prop instanceof Sabre\VObject\Property)) | ||||
continue; | continue; | ||||
$value = strval($prop); | $value = strval($prop); | ||||
switch ($prop->name) { | switch ($prop->name) { | ||||
case 'DTSTART': | case 'DTSTART': | ||||
case 'DTEND': | case 'DTEND': | ||||
case 'DUE': | case 'DUE': | ||||
▲ Show 20 Lines • Show All 206 Lines • ▼ Show 20 Line(s) | |||||
} | } | ||||
// find alarms | // find alarms | ||||
foreach ($ve->select('VALARM') as $valarm) { | foreach ($ve->select('VALARM') as $valarm) { | ||||
$action = 'DISPLAY'; | $action = 'DISPLAY'; | ||||
$trigger = null; | $trigger = null; | ||||
$alarm = array(); | $alarm = array(); | ||||
foreach ($valarm->children as $prop) { | foreach ($valarm->children() as $prop) { | ||||
$value = strval($prop); | $value = strval($prop); | ||||
switch ($prop->name) { | switch ($prop->name) { | ||||
case 'TRIGGER': | case 'TRIGGER': | ||||
foreach ($prop->parameters as $param) { | foreach ($prop->parameters as $param) { | ||||
if ($param->name == 'VALUE' && $param->getValue() == 'DATE-TIME') { | if ($param->name == 'VALUE' && $param->getValue() == 'DATE-TIME') { | ||||
$trigger = '@' . $prop->getDateTime()->format('U'); | $trigger = '@' . $prop->getDateTime()->format('U'); | ||||
$alarm['trigger'] = $prop->getDateTime(); | $alarm['trigger'] = $prop->getDateTime(); | ||||
▲ Show 20 Lines • Show All 51 Lines • ▼ Show 20 Line(s) | |||||
if (!empty($alarm['trigger'])) { | if (!empty($alarm['trigger'])) { | ||||
$event['valarms'][] = $alarm; | $event['valarms'][] = $alarm; | ||||
} | } | ||||
} | } | ||||
} | } | ||||
// assign current timezone to event start/end | // assign current timezone to event start/end | ||||
if (!empty($event['start']) && $event['start'] instanceof DateTime) { | if (!empty($event['start']) && ($event['start'] instanceof DateTime || $event['start'] instanceof DateTimeImmutable)) { | ||||
$this->_apply_timezone($event['start']); | $this->_apply_timezone($event['start']); | ||||
} | } | ||||
else { | else { | ||||
unset($event['start']); | unset($event['start']); | ||||
} | } | ||||
if (!empty($event['end']) && $event['end'] instanceof DateTime) { | if (!empty($event['end']) && ($event['end'] instanceof DateTimeImmutable || $event['end'] instanceof DateTimeImmutable)) { | ||||
$this->_apply_timezone($event['end']); | $this->_apply_timezone($event['end']); | ||||
} | } | ||||
else { | else { | ||||
unset($event['end']); | unset($event['end']); | ||||
} | } | ||||
// some iTip CANCEL messages only contain the start date | // some iTip CANCEL messages only contain the start date | ||||
if (empty($event['end']) && !empty($event['start']) && $this->method == 'CANCEL') { | if (empty($event['end']) && !empty($event['start']) && $this->method == 'CANCEL') { | ||||
$event['end'] = clone $event['start']; | $event['end'] = clone $event['start']; | ||||
} | } | ||||
// T2531: Remember SCHEDULE-AGENT in custom property to properly | // T2531: Remember SCHEDULE-AGENT in custom property to properly | ||||
// support event updates via CalDAV when SCHEDULE-AGENT=CLIENT is used | // support event updates via CalDAV when SCHEDULE-AGENT=CLIENT is used | ||||
if (isset($schedule_agent)) { | if (isset($schedule_agent)) { | ||||
$event['x-custom'][] = array('SCHEDULE-AGENT', $schedule_agent); | $event['x-custom'][] = array('SCHEDULE-AGENT', $schedule_agent); | ||||
} | } | ||||
// minimal validation | // minimal validation | ||||
if (empty($event['uid']) || ($event['_type'] == 'event' && empty($event['start']) != empty($event['end']))) { | if (empty($event['uid']) || ($event['_type'] == 'event' && empty($event['start']) != empty($event['end']))) { | ||||
throw new VObject\ParseException('Object validation failed: missing mandatory object properties'); | throw new Sabre\VObject\ParseException('Object validation failed: missing mandatory object properties'); | ||||
} | } | ||||
return $event; | return $event; | ||||
} | } | ||||
/** | /** | ||||
* Apply user timezone to DateTime object | * Apply user timezone to DateTime object | ||||
*/ | */ | ||||
Show All 18 Lines | |||||
/** | /** | ||||
* Parse the given vfreebusy component into an array representation | * Parse the given vfreebusy component into an array representation | ||||
*/ | */ | ||||
private function _parse_freebusy($ve) | private function _parse_freebusy($ve) | ||||
{ | { | ||||
$this->freebusy = array('_type' => 'freebusy', 'periods' => array()); | $this->freebusy = array('_type' => 'freebusy', 'periods' => array()); | ||||
$seen = array(); | $seen = array(); | ||||
foreach ($ve->children as $prop) { | foreach ($ve->children() as $prop) { | ||||
if (!($prop instanceof VObject\Property)) | if (!($prop instanceof Sabre\VObject\Property)) | ||||
continue; | continue; | ||||
$value = strval($prop); | $value = strval($prop); | ||||
switch ($prop->name) { | switch ($prop->name) { | ||||
case 'CREATED': | case 'CREATED': | ||||
case 'LAST-MODIFIED': | case 'LAST-MODIFIED': | ||||
case 'DTSTAMP': | case 'DTSTAMP': | ||||
▲ Show 20 Lines • Show All 71 Lines • ▼ Show 20 Line(s) | |||||
/** | /** | ||||
* Helper method to correctly interpret an all-day date value | * Helper method to correctly interpret an all-day date value | ||||
*/ | */ | ||||
public static function convert_datetime($prop, $as_array = false) | public static function convert_datetime($prop, $as_array = false) | ||||
{ | { | ||||
if (empty($prop)) { | if (empty($prop)) { | ||||
return $as_array ? array() : null; | return $as_array ? array() : null; | ||||
} | } | ||||
else if ($prop instanceof VObject\Property\iCalendar\DateTime) { | else if ($prop instanceof Sabre\VObject\Property\iCalendar\DateTime) { | ||||
if (count($prop->getDateTimes()) > 1) { | if (count($prop->getDateTimes()) > 1) { | ||||
$dt = array(); | $dt = array(); | ||||
$dateonly = !$prop->hasTime(); | $dateonly = !$prop->hasTime(); | ||||
foreach ($prop->getDateTimes() as $item) { | foreach ($prop->getDateTimes() as $item) { | ||||
$item->_dateonly = $dateonly; | $item->_dateonly = $dateonly; | ||||
$dt[] = $item; | $dt[] = $item; | ||||
} | } | ||||
} | } | ||||
else { | else { | ||||
$dt = $prop->getDateTime(); | $dt = $prop->getDateTime(); | ||||
if (!$prop->hasTime()) { | if (!$prop->hasTime()) { | ||||
$dt->_dateonly = true; | $dt->_dateonly = true; | ||||
} | } | ||||
} | } | ||||
} | } | ||||
else if ($prop instanceof VObject\Property\iCalendar\Period) { | else if ($prop instanceof Sabre\VObject\Property\iCalendar\Period) { | ||||
$dt = array(); | $dt = array(); | ||||
foreach ($prop->getParts() as $val) { | foreach ($prop->getParts() as $val) { | ||||
try { | try { | ||||
list($start, $end) = explode('/', $val); | list($start, $end) = explode('/', $val); | ||||
$start = DateTimeParser::parseDateTime($start); | $start = DateTimeParser::parseDateTime($start); | ||||
// This is a duration value. | // This is a duration value. | ||||
if ($end[0] === 'P') { | if ($end[0] === 'P') { | ||||
$dur = DateTimeParser::parseDuration($end); | $dur = DateTimeParser::parseDuration($end); | ||||
$end = clone $start; | $end = clone $start; | ||||
$end->add($dur); | $end->add($dur); | ||||
} | } | ||||
else { | else { | ||||
$end = DateTimeParser::parseDateTime($end); | $end = DateTimeParser::parseDateTime($end); | ||||
} | } | ||||
$dt[] = array($start, $end); | $dt[] = array($start, $end); | ||||
} | } | ||||
catch (Exception $e) { | catch (Exception $e) { | ||||
// ignore single date parse errors | // ignore single date parse errors | ||||
} | } | ||||
} | } | ||||
} | } | ||||
else if ($prop instanceof \DateTime) { | else if ($prop instanceof \DateTime || $prop instanceof \DateTimeImmutable) { | ||||
$dt = $prop; | $dt = $prop; | ||||
} | } | ||||
// force return value to array if requested | // force return value to array if requested | ||||
if ($as_array && !is_array($dt)) { | if ($as_array && !is_array($dt)) { | ||||
$dt = empty($dt) ? array() : array($dt); | $dt = empty($dt) ? array() : array($dt); | ||||
} | } | ||||
▲ Show 20 Lines • Show All 80 Lines • ▼ Show 20 Line(s) | |||||
* @param boolean Add VTIMEZONE block with timezone definitions for the included events | * @param boolean Add VTIMEZONE block with timezone definitions for the included events | ||||
* @return string Events in iCalendar format (http://tools.ietf.org/html/rfc5545) | * @return string Events in iCalendar format (http://tools.ietf.org/html/rfc5545) | ||||
*/ | */ | ||||
public function export($objects, $method = null, $write = false, $get_attachment = false, $with_timezones = true) | public function export($objects, $method = null, $write = false, $get_attachment = false, $with_timezones = true) | ||||
{ | { | ||||
$this->method = $method; | $this->method = $method; | ||||
// encapsulate in VCALENDAR container | // encapsulate in VCALENDAR container | ||||
$vcal = new VObject\Component\VCalendar(); | $vcal = new Sabre\VObject\Component\VCalendar(); | ||||
$vcal->VERSION = '2.0'; | $vcal->VERSION = '2.0'; | ||||
$vcal->PRODID = $this->prodid; | $vcal->PRODID = $this->prodid; | ||||
$vcal->CALSCALE = 'GREGORIAN'; | $vcal->CALSCALE = 'GREGORIAN'; | ||||
if (!empty($method)) { | if (!empty($method)) { | ||||
$vcal->METHOD = $method; | $vcal->METHOD = $method; | ||||
} | } | ||||
Show All 39 Lines | |||||
* @param object VCalendar object to append event to or false for directly sending data to stdout | * @param object VCalendar object to append event to or false for directly sending data to stdout | ||||
* @param callable Callback function to fetch attachment contents, false if no attachment export | * @param callable Callback function to fetch attachment contents, false if no attachment export | ||||
* @param object RECURRENCE-ID property when serializing a recurrence exception | * @param object RECURRENCE-ID property when serializing a recurrence exception | ||||
*/ | */ | ||||
private function _to_ical($event, $vcal, $get_attachment, $recurrence_id = null) | private function _to_ical($event, $vcal, $get_attachment, $recurrence_id = null) | ||||
{ | { | ||||
$type = !empty($event['_type']) ? $event['_type'] : 'event'; | $type = !empty($event['_type']) ? $event['_type'] : 'event'; | ||||
$cal = $vcal ?: new VObject\Component\VCalendar(); | $cal = $vcal ?: new Sabre\VObject\Component\VCalendar(); | ||||
$ve = $cal->create($this->type_component_map[$type]); | $ve = $cal->create($this->type_component_map[$type]); | ||||
$ve->UID = $event['uid']; | $ve->UID = $event['uid']; | ||||
// set DTSTAMP according to RFC 5545, 3.8.7.2. | // set DTSTAMP according to RFC 5545, 3.8.7.2. | ||||
$dtstamp = !empty($event['changed']) && empty($this->method) ? $event['changed'] : new DateTime('now', new \DateTimeZone('UTC')); | $dtstamp = !empty($event['changed']) && empty($this->method) ? $event['changed'] : new DateTime('now', new \DateTimeZone('UTC')); | ||||
$ve->DTSTAMP = $this->datetime_prop($cal, 'DTSTAMP', $dtstamp, true); | $ve->DTSTAMP = $this->datetime_prop($cal, 'DTSTAMP', $dtstamp, true); | ||||
// all-day events end the next day | // all-day events end the next day | ||||
Show All 14 Lines | |||||
if (!empty($event['end'])) { | if (!empty($event['end'])) { | ||||
$ve->add($this->datetime_prop($cal, 'DTEND', $event['end'], false, !empty($event['allday']))); | $ve->add($this->datetime_prop($cal, 'DTEND', $event['end'], false, !empty($event['allday']))); | ||||
} | } | ||||
if (!empty($event['due'])) { | if (!empty($event['due'])) { | ||||
$ve->add($this->datetime_prop($cal, 'DUE', $event['due'], false)); | $ve->add($this->datetime_prop($cal, 'DUE', $event['due'], false)); | ||||
} | } | ||||
// we're exporting a recurrence instance only | // we're exporting a recurrence instance only | ||||
if (!$recurrence_id && !empty($event['recurrence_date']) && $event['recurrence_date'] instanceof DateTime) { | if (!$recurrence_id && !empty($event['recurrence_date']) && ($event['recurrence_date'] instanceof DateTime || $event['recurrence_date'] instanceof DateTimeImmutable)) { | ||||
$recurrence_id = $this->datetime_prop($cal, 'RECURRENCE-ID', $event['recurrence_date'], false, !empty($event['allday'])); | $recurrence_id = $this->datetime_prop($cal, 'RECURRENCE-ID', $event['recurrence_date'], false, !empty($event['allday'])); | ||||
if (!empty($event['thisandfuture'])) { | if (!empty($event['thisandfuture'])) { | ||||
$recurrence_id->add('RANGE', 'THISANDFUTURE'); | $recurrence_id->add('RANGE', 'THISANDFUTURE'); | ||||
} | } | ||||
} | } | ||||
if ($recurrence_id) { | if ($recurrence_id) { | ||||
$ve->add($recurrence_id); | $ve->add($recurrence_id); | ||||
Show All 25 Lines | |||||
if (!empty($event['recurrence']['FREQ'])) { | if (!empty($event['recurrence']['FREQ'])) { | ||||
$ve->add('RRULE', libcalendaring::to_rrule($event['recurrence'], !empty($event['allday']))); | $ve->add('RRULE', libcalendaring::to_rrule($event['recurrence'], !empty($event['allday']))); | ||||
} | } | ||||
// add EXDATEs each one per line (for Thunderbird Lightning) | // add EXDATEs each one per line (for Thunderbird Lightning) | ||||
if (is_array($exdates)) { | if (is_array($exdates)) { | ||||
foreach ($exdates as $exdate) { | foreach ($exdates as $exdate) { | ||||
if ($exdate instanceof DateTime) { | if ($exdate instanceof DateTime || $exdate instanceof DateTimeImmutable) { | ||||
$ve->add($this->datetime_prop($cal, 'EXDATE', $exdate)); | $ve->add($this->datetime_prop($cal, 'EXDATE', $exdate)); | ||||
} | } | ||||
} | } | ||||
} | } | ||||
// add RDATEs | // add RDATEs | ||||
if (is_array($rdates)) { | if (is_array($rdates)) { | ||||
foreach ($rdates as $rdate) { | foreach ($rdates as $rdate) { | ||||
$ve->add($this->datetime_prop($cal, 'RDATE', $rdate)); | $ve->add($this->datetime_prop($cal, 'RDATE', $rdate)); | ||||
▲ Show 20 Lines • Show All 48 Lines • ▼ Show 20 Line(s) | |||||
$completed = !empty($event['changed']) ? $event['changed'] : new DateTime('now - 1 hour'); | $completed = !empty($event['changed']) ? $event['changed'] : new DateTime('now - 1 hour'); | ||||
$ve->add($this->datetime_prop($cal, 'COMPLETED', $completed, true)); | $ve->add($this->datetime_prop($cal, 'COMPLETED', $completed, true)); | ||||
} | } | ||||
if (!empty($event['valarms'])) { | if (!empty($event['valarms'])) { | ||||
foreach ($event['valarms'] as $alarm) { | foreach ($event['valarms'] as $alarm) { | ||||
$va = $cal->createComponent('VALARM'); | $va = $cal->createComponent('VALARM'); | ||||
$va->action = $alarm['action']; | $va->action = $alarm['action']; | ||||
if ($alarm['trigger'] instanceof DateTime) { | if ($alarm['trigger'] instanceof DateTime || $alarm['trigger'] instanceof DateTimeImmutable) { | ||||
$va->add($this->datetime_prop($cal, 'TRIGGER', $alarm['trigger'], true, null, true)); | $va->add($this->datetime_prop($cal, 'TRIGGER', $alarm['trigger'], true, null, true)); | ||||
} | } | ||||
else { | else { | ||||
$alarm_props = array(); | $alarm_props = array(); | ||||
if (!empty($alarm['related']) && strtoupper($alarm['related']) == 'END') { | if (!empty($alarm['related']) && strtoupper($alarm['related']) == 'END') { | ||||
$alarm_props['RELATED'] = 'END'; | $alarm_props['RELATED'] = 'END'; | ||||
} | } | ||||
$va->add('TRIGGER', $alarm['trigger'], $alarm_props); | $va->add('TRIGGER', $alarm['trigger'], $alarm_props); | ||||
Show All 25 Lines | |||||
// legacy support | // legacy support | ||||
else if (!empty($event['alarms'])) { | else if (!empty($event['alarms'])) { | ||||
$va = $cal->createComponent('VALARM'); | $va = $cal->createComponent('VALARM'); | ||||
list($trigger, $va->action) = explode(':', $event['alarms']); | list($trigger, $va->action) = explode(':', $event['alarms']); | ||||
$val = libcalendaring::parse_alarm_value($trigger); | $val = libcalendaring::parse_alarm_value($trigger); | ||||
if (!empty($val[3])) { | if (!empty($val[3])) { | ||||
$va->add('TRIGGER', $val[3]); | $va->add('TRIGGER', $val[3]); | ||||
} | } | ||||
else if ($val[0] instanceof DateTime) { | else if ($val[0] instanceof DateTime || $val[0] instanceof DateTimeImmutable) { | ||||
$va->add($this->datetime_prop($cal, 'TRIGGER', $val[0], true, null, true)); | $va->add($this->datetime_prop($cal, 'TRIGGER', $val[0], true, null, true)); | ||||
} | } | ||||
$ve->add($va); | $ve->add($va); | ||||
} | } | ||||
// Find SCHEDULE-AGENT | // Find SCHEDULE-AGENT | ||||
if (!empty($event['x-custom'])) { | if (!empty($event['x-custom'])) { | ||||
foreach ((array) $event['x-custom'] as $prop) { | foreach ((array) $event['x-custom'] as $prop) { | ||||
▲ Show 20 Lines • Show All 131 Lines • ▼ Show 20 Line(s) | |||||
* or false if no timezone information is available | * or false if no timezone information is available | ||||
*/ | */ | ||||
public static function get_vtimezone($tzid, $from = 0, $to = 0, $cal = null) | public static function get_vtimezone($tzid, $from = 0, $to = 0, $cal = null) | ||||
{ | { | ||||
// TODO: Consider using tzurl.org database for better interoperability e.g. with Outlook | // TODO: Consider using tzurl.org database for better interoperability e.g. with Outlook | ||||
if (!$from) $from = time(); | if (!$from) $from = time(); | ||||
if (!$to) $to = $from; | if (!$to) $to = $from; | ||||
if (!$cal) $cal = new VObject\Component\VCalendar(); | if (!$cal) $cal = new Sabre\VObject\Component\VCalendar(); | ||||
if (is_string($tzid)) { | if (is_string($tzid)) { | ||||
try { | try { | ||||
$tz = new \DateTimeZone($tzid); | $tz = new \DateTimeZone($tzid); | ||||
} | } | ||||
catch (\Exception $e) { | catch (\Exception $e) { | ||||
return false; | return false; | ||||
} | } | ||||
▲ Show 20 Lines • Show All 111 Lines • ▼ Show 20 Line(s) | |||||
} | } | ||||
/** | /** | ||||
* Override Sabre\VObject\Property\Text that quotes commas in the location property | * Override Sabre\VObject\Property\Text that quotes commas in the location property | ||||
* because Apple clients treat that property as list. | * because Apple clients treat that property as list. | ||||
*/ | */ | ||||
class vobject_location_property extends VObject\Property\Text | class vobject_location_property extends Sabre\VObject\Property\Text | ||||
{ | { | ||||
/** | /** | ||||
* List of properties that are considered 'structured'. | * List of properties that are considered 'structured'. | ||||
* | * | ||||
* @var array | * @var array | ||||
*/ | */ | ||||
protected $structuredValues = array( | protected $structuredValues = array( | ||||
// vCard | // vCard | ||||
Show All 9 Lines |
We have use \Sabre\VObject in the file, so it should work without this change. Maybe the leading backslash is a problem?