/** * Parses a string containing vCalendar data. * * @todo This method doesn't work well at all, if $base is VCARD. * * @param string $text The data to parse. * @param string $base The type of the base object. * @param boolean $clear If true clears this object before parsing. * * @return boolean True on successful import, false otherwise. * @throws Horde_Icalendar_Exception */ public function parsevCalendar($text, $base = 'VCALENDAR', $clear = true) { if ($clear) { $this->clear(); } $text = Horde_String::trimUtf8Bom($text); if (preg_match('/^BEGIN:' . $base . '(.*)^END:' . $base . '/ism', $text, $matches)) { $container = true; $vCal = $matches[1]; } else { // Text isn't enclosed in BEGIN:VCALENDAR // .. END:VCALENDAR. We'll try to parse it anyway. $container = false; $vCal = $text; } $vCal = trim($vCal); // Extract all subcomponents. $matches = $components = null; if (preg_match_all('/^BEGIN:(.*)\\s*?(\\r\\n|\\r|\\n)(.*)^END:\\1\\s*?/Uims', $vCal, $components)) { foreach ($components[0] as $key => $data) { // Remove from the vCalendar data. $vCal = str_replace($data, '', $vCal); } } elseif (!$container) { return false; } // Unfold "quoted printable" folded lines like: // BODY;ENCODING=QUOTED-PRINTABLE:= // another=20line= // last=20line while (preg_match_all('/^([^:]+;\\s*(ENCODING=)?QUOTED-PRINTABLE(.*=\\r?\\n)+(.*[^=])?(\\r?\\n|$))/mU', $vCal, $matches)) { foreach ($matches[1] as $s) { $r = preg_replace('/=\\r?\\n/', '', $s); $vCal = str_replace($s, $r, $vCal); } } // Unfold any folded lines. $vCal = preg_replace('/[\\r\\n]+[ \\t]/', '', $vCal); // Parse the remaining attributes. if (preg_match_all('/^((?:[^":]+|(?:"[^"]*")+)*):([^\\r\\n]*)\\r?$/m', $vCal, $matches)) { foreach ($matches[0] as $attribute) { preg_match('/([^;^:]*)((;(?:[^":]+|(?:"[^"]*")+)*)?):([^\\r\\n]*)[\\r\\n]*/', $attribute, $parts); $tag = trim(preg_replace('/^.*\\./', '', Horde_String::upper($parts[1]))); $value = $parts[4]; $params = array(); // Parse parameters. if (!empty($parts[2])) { preg_match_all('/;(([^;=]*)(=("[^"]*"|[^;]*))?)/', $parts[2], $param_parts); foreach ($param_parts[2] as $key => $paramName) { $paramName = Horde_String::upper($paramName); $paramValue = $param_parts[4][$key]; if ($paramName == 'TYPE') { $paramValue = preg_split('/(?<!\\\\),/', $paramValue); if (count($paramValue) == 1) { $paramValue = $paramValue[0]; } } if (is_string($paramValue)) { if (preg_match('/"([^"]*)"/', $paramValue, $parts)) { $paramValue = $parts[1]; } } else { foreach ($paramValue as $k => $tmp) { if (preg_match('/"([^"]*)"/', $tmp, $parts)) { $paramValue[$k] = $parts[1]; } } } if (isset($params[$paramName])) { if (is_array($params[$paramName])) { $params[$paramName][] = $paramValue; } else { $params[$paramName] = array($params[$paramName], $paramValue); } } else { $params[$paramName] = $paramValue; } } } // Charset and encoding handling. if (isset($params['ENCODING']) && Horde_String::upper($params['ENCODING']) == 'QUOTED-PRINTABLE' || isset($params['QUOTED-PRINTABLE'])) { $value = quoted_printable_decode($value); if (isset($params['CHARSET'])) { $value = Horde_String::convertCharset($value, $params['CHARSET'], 'UTF-8'); } } elseif (isset($params['CHARSET'])) { $value = Horde_String::convertCharset($value, $params['CHARSET'], 'UTF-8'); } // Get timezone info for date fields from $params. $tzid = isset($params['TZID']) ? trim($params['TZID'], '\\"') : false; switch ($tag) { // Date fields. case 'COMPLETED': case 'CREATED': case 'LAST-MODIFIED': case 'X-MOZ-LASTACK': case 'X-MOZ-SNOOZE-TIME': $this->setAttribute($tag, $this->_parseDateTime($value, $tzid), $params); break; case 'BDAY': case 'X-ANNIVERSARY': $this->setAttribute($tag, $this->_parseDate($value), $params); break; case 'DTEND': case 'DTSTART': case 'DTSTAMP': case 'DUE': case 'AALARM': case 'RECURRENCE-ID': // types like AALARM may contain additional data after a ; // ignore these. $ts = explode(';', $value); if (isset($params['VALUE']) && $params['VALUE'] == 'DATE') { $this->setAttribute($tag, $this->_parseDate($ts[0]), $params); } else { $this->setAttribute($tag, $this->_parseDateTime($ts[0], $tzid), $params); } break; case 'TRIGGER': if (isset($params['VALUE']) && $params['VALUE'] == 'DATE-TIME') { $this->setAttribute($tag, $this->_parseDateTime($value, $tzid), $params); } else { $this->setAttribute($tag, $this->_parseDuration($value), $params); } break; // Comma seperated dates. // Comma seperated dates. case 'EXDATE': case 'RDATE': if (!strlen($value)) { break; } $dates = array(); $separator = $this->_oldFormat ? ';' : ','; preg_match_all('/' . $separator . '([^' . $separator . ']*)/', $separator . $value, $values); foreach ($values[1] as $value) { $stamp = $this->_parseDateTime($value); if (!is_int($stamp)) { continue; } $dates[] = array('year' => date('Y', $stamp), 'month' => date('m', $stamp), 'mday' => date('d', $stamp)); } $this->setAttribute($tag, isset($dates[0]) ? $dates[0] : null, $params, true, $dates); break; // Duration fields. // Duration fields. case 'DURATION': $this->setAttribute($tag, $this->_parseDuration($value), $params); break; // Period of time fields. // Period of time fields. case 'FREEBUSY': $periods = array(); preg_match_all('/,([^,]*)/', ',' . $value, $values); foreach ($values[1] as $value) { $periods[] = $this->_parsePeriod($value); } $this->setAttribute($tag, isset($periods[0]) ? $periods[0] : null, $params, true, $periods); break; // UTC offset fields. // UTC offset fields. case 'TZOFFSETFROM': case 'TZOFFSETTO': $this->setAttribute($tag, $this->_parseUtcOffset($value), $params); break; // Integer fields. // Integer fields. case 'PERCENT-COMPLETE': case 'PRIORITY': case 'REPEAT': case 'SEQUENCE': $this->setAttribute($tag, intval($value), $params); break; // Geo fields. // Geo fields. case 'GEO': if ($value) { if ($this->_oldFormat) { $floats = explode(',', $value); $value = array('latitude' => floatval($floats[1]), 'longitude' => floatval($floats[0])); } else { $floats = explode(';', $value); $value = array('latitude' => floatval($floats[0]), 'longitude' => floatval($floats[1])); } } $this->setAttribute($tag, $value, $params); break; // Recursion fields. // Recursion fields. case 'EXRULE': case 'RRULE': $this->setAttribute($tag, trim($value), $params); break; // ADR, ORG and N are lists seperated by unescaped semicolons // with a specific number of slots. // ADR, ORG and N are lists seperated by unescaped semicolons // with a specific number of slots. case 'ADR': case 'N': case 'ORG': $value = trim($value); // As of rfc 2426 2.4.2 semicolon, comma, and colon must // be escaped (comma is unescaped after splitting below). $value = str_replace(array('\\n', '\\N', '\\;', '\\:'), array($this->_newline, $this->_newline, ';', ':'), $value); // Split by unescaped semicolons: $values = preg_split('/(?<!\\\\);/', $value); $value = str_replace('\\;', ';', $value); $values = str_replace('\\;', ';', $values); $this->setAttribute($tag, trim($value), $params, true, $values); break; // String fields. // String fields. default: if ($this->_oldFormat) { // vCalendar 1.0 and vCard 2.1 only escape semicolons // and use unescaped semicolons to create lists. $value = trim($value); // Split by unescaped semicolons: $values = preg_split('/(?<!\\\\);/', $value); $value = str_replace('\\;', ';', $value); $values = str_replace('\\;', ';', $values); $this->setAttribute($tag, trim($value), $params, true, $values); } else { $value = trim($value); // As of rfc 2426 2.4.2 semicolon, comma, and colon // must be escaped (comma is unescaped after splitting // below). $value = str_replace(array('\\n', '\\N', '\\;', '\\:', '\\\\'), array($this->_newline, $this->_newline, ';', ':', '\\'), $value); // Split by unescaped commas. $values = preg_split('/(?<!\\\\),/', $value); $value = str_replace('\\,', ',', $value); $values = str_replace('\\,', ',', $values); $this->setAttribute($tag, trim($value), $params, true, $values); } break; } } } // Process all components. if ($components) { // vTimezone components are processed first. They are // needed to process vEvents that may use a TZID. foreach ($components[0] as $key => $data) { $type = trim($components[1][$key]); if ($type != 'VTIMEZONE') { continue; } $component = $this->newComponent($type, $this); if ($component === false) { throw new Horde_Icalendar_Exception('Unable to create object for type ' . $type); } $component->parsevCalendar($data, $type); $this->addComponent($component); // Remove from the vCalendar data. $vCal = str_replace($data, '', $vCal); } // Now process the non-vTimezone components. foreach ($components[0] as $key => $data) { $type = trim($components[1][$key]); if ($type == 'VTIMEZONE') { continue; } $component = $this->newComponent($type, $this); if ($component === false) { throw new Horde_Icalendar_Exception('Unable to create object for type ' . $type); } $component->parsevCalendar($data, $type); $this->addComponent($component); } } return true; }