/** * Actually write the resource to the database. All checking of whether this is reasonable * should be done before this is called. * * @param DAVResource $resource The resource being written * @param string $caldav_data The actual data to be written * @param DAVResource $collection The collection containing the resource being written * @param int $author The user_no who wants to put this resource on the server * @param string $etag An etag unique for this event * @param string $put_action_type INSERT or UPDATE depending on what we are to do * @param boolean $caldav_context True, if we are responding via CalDAV, false for other ways of calling this * @param string Either 'INSERT' or 'UPDATE': the type of action we are doing * @param boolean $log_action Whether to log the fact that we are writing this into an action log (if configured) * @param string $weak_etag An etag that is NOT modified on ATTENDEE changes for this event * * @return boolean True for success, false for failure. */ function write_resource(DAVResource $resource, $caldav_data, DAVResource $collection, $author, &$etag, $put_action_type, $caldav_context, $log_action = true, $weak_etag = null) { global $tz_regex, $session; $path = $resource->bound_from(); $user_no = $collection->user_no(); $vcal = new vCalendar($caldav_data); $resources = $vcal->GetComponents('VTIMEZONE', false); // Not matching VTIMEZONE if (!isset($resources[0])) { $resource_type = 'Unknown'; /** @todo Handle writing non-calendar resources, like address book entries or random file data */ rollback_on_error($caldav_context, $user_no, $path, translate('No calendar content'), 412); return false; } else { $first = $resources[0]; if (!$first instanceof vComponent) { print $vcal->Render(); fatal('This is not a vComponent!'); } $resource_type = $first->GetType(); } $collection_id = $collection->collection_id(); $qry = new AwlQuery(); $qry->Begin(); $dav_params = array(':etag' => $etag, ':dav_data' => $caldav_data, ':caldav_type' => $resource_type, ':session_user' => $author, ':weak_etag' => $weak_etag); $calitem_params = array(':etag' => $etag); if ($put_action_type == 'INSERT') { $qry->QDo('SELECT nextval(\'dav_id_seq\') AS dav_id, null AS caldav_data'); } else { $qry->QDo('SELECT dav_id, caldav_data FROM caldav_data WHERE dav_name = :dav_name ', array(':dav_name' => $path)); } if ($qry->rows() != 1 || !($row = $qry->Fetch())) { // No dav_id? => We're toast! trace_bug('No dav_id for "%s" on %s!!!', $path, $create_resource ? 'create' : 'update'); rollback_on_error($caldav_context, $user_no, $path); return false; } $dav_id = $row->dav_id; $old_dav_data = $row->caldav_data; $dav_params[':dav_id'] = $dav_id; $calitem_params[':dav_id'] = $dav_id; $due = null; if ($first->GetType() == 'VTODO') { $due = $first->GetPValue('DUE'); } $calitem_params[':due'] = $due; $dtstart = $first->GetPValue('DTSTART'); if (empty($dtstart)) { $dtstart = $due; } $calitem_params[':dtstart'] = $dtstart; $dtend = $first->GetPValue('DTEND'); if (isset($dtend) && $dtend != '') { dbg_error_log('PUT', ' DTEND: "%s", DTSTART: "%s", DURATION: "%s"', $dtend, $dtstart, $first->GetPValue('DURATION')); $calitem_params[':dtend'] = $dtend; $dtend = ':dtend'; } else { // In this case we'll construct the SQL directly as a calculation relative to :dtstart $dtend = 'NULL'; if ($first->GetPValue('DURATION') != '' and $dtstart != '') { $duration = trim(preg_replace('#[PT]#', ' ', $first->GetPValue('DURATION'))); if ($duration == '') { $duration = '0 seconds'; } $dtend = '(:dtstart::timestamp with time zone + :duration::interval)'; $calitem_params[':duration'] = $duration; } elseif ($first->GetType() == 'VEVENT') { /** * From RFC2445 4.6.1: * For cases where a "VEVENT" calendar component specifies a "DTSTART" * property with a DATE data type but no "DTEND" property, the events * non-inclusive end is the end of the calendar date specified by the * "DTSTART" property. For cases where a "VEVENT" calendar component specifies * a "DTSTART" property with a DATE-TIME data type but no "DTEND" property, * the event ends on the same calendar date and time of day specified by the * "DTSTART" property. * * So we're looking for 'VALUE=DATE', to identify the duration, effectively. * */ $dtstart_prop = $first->GetProperty('DTSTART'); $value_type = $dtstart_prop->GetParameterValue('VALUE'); dbg_error_log('PUT', 'DTSTART without DTEND. DTSTART value type is %s', $value_type); if (isset($value_type) && $value_type == 'DATE') { $dtend = '(:dtstart::timestamp with time zone::date + \'1 day\'::interval)'; } else { $dtend = ':dtstart'; } } } $dtstamp = $first->GetPValue('DTSTAMP'); if (!isset($dtstamp) || $dtstamp == '') { // Strictly, we're dealing with an out of spec component here, but we'll try and survive $dtstamp = gmdate('Ymd\\THis\\Z'); } $calitem_params[':dtstamp'] = $dtstamp; $last_modified = $first->GetPValue('LAST-MODIFIED'); if (!isset($last_modified) || $last_modified == '') { $last_modified = $dtstamp; } $dav_params[':modified'] = $last_modified; $calitem_params[':modified'] = $last_modified; $created = $first->GetPValue('CREATED'); if ($created == '00001231T000000Z') { $created = '20001231T000000Z'; } $class = $first->GetPValue('CLASS'); /* Check and see if we should over ride the class. */ /** @todo is there some way we can move this out of this function? Or at least get rid of the need for the SQL query here. */ if (public_events_only($user_no, $path)) { $class = 'PUBLIC'; } /* * It seems that some calendar clients don't set a class... * RFC2445, 4.8.1.3: * Default is PUBLIC */ if (!isset($class) || $class == '') { $class = 'PUBLIC'; } $calitem_params[':class'] = $class; /** Calculate what timezone to set, first, if possible */ $last_olson = 'Turkmenikikamukau'; // I really hope this location doesn't exist! $tzid = GetTZID($first); if (!empty($tzid)) { $timezones = $vcal->GetComponents('VTIMEZONE'); foreach ($timezones as $k => $tz) { if ($tz->GetPValue('TZID') != $tzid) { /** * We'll skip any tz definitions that are for a TZID other than the DTSTART/DUE on the first VEVENT/VTODO */ dbg_error_log('ERROR', ' Event uses TZID[%s], skipping included TZID[%s]!', $tz->GetPValue('TZID'), $tzid); continue; } $olson = olson_from_tzstring($tzid); if (empty($olson)) { $olson = $tz->GetPValue('X-LIC-LOCATION'); if (!empty($olson)) { $olson = olson_from_tzstring($olson); } } } dbg_error_log('PUT', ' Using TZID[%s] and location of [%s]', $tzid, isset($olson) ? $olson : ''); if (!empty($olson) && $olson != $last_olson && preg_match($tz_regex, $olson)) { dbg_error_log('PUT', ' Setting timezone to %s', $olson); if ($olson != '') { $qry->QDo('SET TIMEZONE TO \'' . $olson . "'"); } $last_olson = $olson; } $params = array(':tzid' => $tzid); $qry = new AwlQuery('SELECT 1 FROM timezones WHERE tzid = :tzid', $params); if ($qry->Exec('PUT', __LINE__, __FILE__) && $qry->rows() == 0) { $params[':olson_name'] = $olson; $params[':vtimezone'] = isset($tz) ? $tz->Render() : null; $qry->QDo('INSERT INTO timezones (tzid, olson_name, active, vtimezone) VALUES(:tzid,:olson_name,false,:vtimezone)', $params); } if (!isset($olson) || $olson == '') { $olson = $tzid; } } $qry->QDo('SELECT new_sync_token(0,' . $collection_id . ')'); $calitem_params[':tzid'] = $tzid; $calitem_params[':uid'] = $first->GetPValue('UID'); $calitem_params[':summary'] = $first->GetPValue('SUMMARY'); $calitem_params[':location'] = $first->GetPValue('LOCATION'); $calitem_params[':transp'] = $first->GetPValue('TRANSP'); $calitem_params[':description'] = $first->GetPValue('DESCRIPTION'); $calitem_params[':rrule'] = $first->GetPValue('RRULE'); $calitem_params[':url'] = $first->GetPValue('URL'); $calitem_params[':priority'] = $first->GetPValue('PRIORITY'); $calitem_params[':percent_complete'] = $first->GetPValue('PERCENT-COMPLETE'); $calitem_params[':status'] = $first->GetPValue('STATUS'); if (!$collection->IsSchedulingCollection()) { if (do_scheduling_requests($vcal, $put_action_type == 'INSERT', $old_dav_data, true)) { $dav_params[':dav_data'] = $vcal->Render(null, true); $etag = null; } } if (!isset($dav_params[':modified'])) { $dav_params[':modified'] = 'now'; } if ($put_action_type == 'INSERT') { $sql = 'INSERT INTO caldav_data ( dav_id, user_no, dav_name, dav_etag, caldav_data, caldav_type, logged_user, created, modified, collection_id, weak_etag ) VALUES( :dav_id, :user_no, :dav_name, :etag, :dav_data, :caldav_type, :session_user, :created, :modified, :collection_id, :weak_etag )'; $dav_params[':collection_id'] = $collection_id; $dav_params[':user_no'] = $user_no; $dav_params[':dav_name'] = $path; $dav_params[':created'] = isset($created) && $created != '' ? $created : $dtstamp; } else { $sql = 'UPDATE caldav_data SET caldav_data=:dav_data, dav_etag=:etag, caldav_type=:caldav_type, logged_user=:session_user, modified=:modified, weak_etag=:weak_etag WHERE dav_id=:dav_id'; } $qry = new AwlQuery($sql, $dav_params); if (!$qry->Exec('PUT', __LINE__, __FILE__)) { fatal('Insert into calendar_item failed...'); rollback_on_error($caldav_context, $user_no, $path); return false; } if ($put_action_type == 'INSERT') { $sql = <<<EOSQL INSERT INTO calendar_item (user_no, dav_name, dav_id, dav_etag, uid, dtstamp, dtstart, dtend, summary, location, class, transp, description, rrule, tz_id, last_modified, url, priority, created, due, percent_complete, status, collection_id ) VALUES ( :user_no, :dav_name, :dav_id, :etag, :uid, :dtstamp, :dtstart, {$dtend}, :summary, :location, :class, :transp, :description, :rrule, :tzid, :modified, :url, :priority, :created, :due, :percent_complete, :status, :collection_id ) EOSQL; $sync_change = 201; $calitem_params[':collection_id'] = $collection_id; $calitem_params[':user_no'] = $user_no; $calitem_params[':dav_name'] = $path; $calitem_params[':created'] = $dav_params[':created']; } else { $sql = <<<EOSQL UPDATE calendar_item SET dav_etag=:etag, uid=:uid, dtstamp=:dtstamp, dtstart=:dtstart, dtend={$dtend}, summary=:summary, location=:location, class=:class, transp=:transp, description=:description, rrule=:rrule, tz_id=:tzid, last_modified=:modified, url=:url, priority=:priority, due=:due, percent_complete=:percent_complete, status=:status WHERE dav_id=:dav_id EOSQL; $sync_change = 200; } write_alarms($dav_id, $first); write_attendees($dav_id, $vcal); if ($log_action && function_exists('log_caldav_action')) { log_caldav_action($put_action_type, $first->GetPValue('UID'), $user_no, $collection_id, $path); } else { if ($log_action) { dbg_error_log('PUT', 'No log_caldav_action( %s, %s, %s, %s, %s) can be called.', $put_action_type, $first->GetPValue('UID'), $user_no, $collection_id, $path); } } $qry = new AwlQuery($sql, $calitem_params); if (!$qry->Exec('PUT', __LINE__, __FILE__)) { rollback_on_error($caldav_context, $user_no, $path); return false; } $qry->QDo("SELECT write_sync_change( {$collection_id}, {$sync_change}, :dav_name)", array(':dav_name' => $path)); $qry->Commit(); if (function_exists('post_commit_action')) { post_commit_action($put_action_type, $first->GetPValue('UID'), $user_no, $collection_id, $path); } // Uncache anything to do with the collection $cache = getCacheInstance(); $cache_ns = 'collection-' . preg_replace('{/[^/]*$}', '/', $path); $cache->delete($cache_ns, null); dbg_error_log('PUT', 'User: %d, ETag: %s, Path: %s', $author, $etag, $path); return true; // Success! }
$request->XMLResponse(200, $response); } function handle_cancel_request($ic) { global $c, $session, $request; $request->NeedPrivilege('CALDAV:schedule-send-reply'); $reply = new XMLDocument(array("DAV:" => "", "urn:ietf:params:xml:ns:caldav" => "C")); $response = $reply->NewXMLElement("response", false, false, 'urn:ietf:params:xml:ns:caldav'); $reply->CalDAVElement($response, "request-status", "2.0;Success"); // Cargo-cult setting $response = $reply->NewXMLElement("schedule-response", $response, $reply->GetXmlNsArray()); $request->XMLResponse(200, $response); } $ical = new vCalendar($request->raw_post); $method = $ical->GetPValue('METHOD'); $resources = $ical->GetComponents('VTIMEZONE', false); $first = $resources[0]; switch ($method) { case 'REQUEST': dbg_error_log('POST', 'Handling iTIP "REQUEST" method with "%s" component.', $method, $first->GetType()); if ($first->GetType() == 'VFREEBUSY') { handle_freebusy_request($first); } elseif ($first->GetType() == 'VEVENT') { $request->NeedPrivilege('CALDAV:schedule-send-invite'); handle_schedule_request($ical); } else { dbg_error_log('POST', 'Ignoring iTIP "REQUEST" with "%s" component.', $first->GetType()); } break; case 'REPLY': dbg_error_log('POST', 'Handling iTIP "REPLY" with "%s" component.', $first->GetType());
$transparency = preg_replace('{^.*:}', '', $transparency[0]->GetNSTag()); $qry->QDo('UPDATE collection SET schedule_transp = :transparency WHERE dav_name = :dav_name', array(':dav_name' => $dav_resource->dav_name(), ':transparency' => $transparency)); $success[$tag] = 1; } else { add_failure('set', $tag, 'HTTP/1.1 409 Conflict', translate("The CalDAV:schedule-calendar-transp property may only be set on calendars.")); } break; case 'urn:ietf:params:xml:ns:caldav:calendar-free-busy-set': add_failure('set', $tag, 'HTTP/1.1 409 Conflict', translate("The calendar-free-busy-set is superseded by the schedule-calendar-transp property of a calendar collection.")); break; case 'urn:ietf:params:xml:ns:caldav:calendar-timezone': if ($dav_resource->IsCollection() && $dav_resource->IsCalendar() && !$dav_resource->IsBinding()) { $tzcomponent = $setting->GetPath('urn:ietf:params:xml:ns:caldav:calendar-timezone'); $tzstring = $tzcomponent[0]->GetContent(); $calendar = new vCalendar($tzstring); $timezones = $calendar->GetComponents('VTIMEZONE'); if (count($timezones) == 0) { break; } $tz = $timezones[0]; // Backward compatibility $tzid = $tz->GetPValue('TZID'); $params = array(':tzid' => $tzid); $qry = new AwlQuery('SELECT 1 FROM timezones WHERE tzid = :tzid', $params); if ($qry->Exec('PUT', __LINE__, __FILE__) && $qry->rows() == 0) { $params[':olson_name'] = $calendar->GetOlsonName($tz); $params[':vtimezone'] = isset($tz) ? $tz->Render() : null; $qry->QDo('INSERT INTO timezones (tzid, olson_name, active, vtimezone) VALUES(:tzid,:olson_name,false,:vtimezone)', $params); } $qry->QDo('UPDATE collection SET timezone = :tzid WHERE dav_name = :dav_name', array(':tzid' => $tzid, ':dav_name' => $dav_resource->dav_name())); } else {
/** * Expand the instances for a STANDARD or DAYLIGHT component of a VTIMEZONE * * @param object $vResource is a VCALENDAR with a VTIMEZONE containing components needing expansion * @param object $range_start A RepeatRuleDateTime which is the beginning of the range for events. * @param object $range_end A RepeatRuleDateTime which is the end of the range for events. * @param int $offset_from The offset from UTC in seconds at the onset time. * * @return array of onset datetimes with UTC from/to offsets */ function expand_timezone_onsets(vCalendar $vResource, RepeatRuleDateTime $range_start, RepeatRuleDateTime $range_end) { global $c; $vtimezones = $vResource->GetComponents(); $vtz = $vtimezones[0]; $components = $vtz->GetComponents(); $instances = array(); $dtstart = null; $is_date = false; $has_repeats = false; $zone_tz = $vtz->GetPValue('TZID'); foreach ($components as $k => $comp) { if (DEBUG_EXPAND) { printf("Starting TZ expansion for component '%s' in timezone '%s'\n", $comp->GetType(), $zone_tz); foreach ($instances as $k => $v) { print ' : ' . $k; } print "\n"; } $dtstart_prop = $comp->GetProperty('DTSTART'); if (!isset($dtstart_prop)) { continue; } $dtstart = new RepeatRuleDateTime($dtstart_prop); $dtstart->setTimeZone('UTC'); $offset_from = $comp->GetPValue('TZOFFSETFROM'); $offset_from = $offset_from / 100 * 3600 + abs($offset_from) % 100 * 60 * ($offset_from < 0 ? -1 : 0); $offset_from *= -1; $offset_from = "{$offset_from} seconds"; dbg_error_log('tz/update', "%s of offset\n", $offset_from); $dtstart->modify($offset_from); $is_date = $dtstart->isDate(); $instances[$dtstart->UTC('Y-m-d\\TH:i:s\\Z')] = $comp; $rrule = $comp->GetProperty('RRULE'); $has_repeats = isset($rrule); if (!$has_repeats) { continue; } $recur = $comp->GetProperty('RRULE'); if (isset($recur)) { $recur = $recur->Value(); $this_start = clone $dtstart; $rule = new RepeatRule($this_start, $recur, $is_date); $i = 0; $result_limit = 1000; while ($date = $rule->next()) { $instances[$date->UTC('Y-m-d\\TH:i:s\\Z')] = $comp; if ($i++ >= $result_limit || $date > $range_end) { break; } } if (DEBUG_EXPAND) { print "After rrule_expand"; foreach ($instances as $k => $v) { print ' : ' . $k; } print "\n"; } } $properties = $comp->GetProperties('RDATE'); if (count($properties)) { foreach ($properties as $p) { $timezone = $p->GetParameterValue('TZID'); $rdate = $p->Value(); $rdates = explode(',', $rdate); foreach ($rdates as $k => $v) { $rdate = new RepeatRuleDateTime($v, $timezone, $is_date); if ($return_floating_times) { $rdate->setAsFloat(); } $instances[$rdate->UTC('Y-m-d\\TH:i:s\\Z')] = $comp; if ($rdate > $range_end) { break; } } } if (DEBUG_EXPAND) { print "After rdate_expand"; foreach ($instances as $k => $v) { print ' : ' . $k; } print "\n"; } } } ksort($instances); $onsets = array(); $start_utc = $range_start->UTC('Y-m-d\\TH:i:s\\Z'); $end_utc = $range_end->UTC('Y-m-d\\TH:i:s\\Z'); foreach ($instances as $utc => $comp) { if ($utc > $end_utc) { if (DEBUG_EXPAND) { printf("We're done: {$utc} is out of the range.\n"); } break; } if ($utc < $start_utc) { continue; } $onsets[$utc] = array('from' => $comp->GetPValue('TZOFFSETFROM'), 'to' => $comp->GetPValue('TZOFFSETTO'), 'name' => $comp->GetPValue('TZNAME'), 'type' => $comp->GetType()); } return $onsets; }
/** * Given a dav_id and an original vCalendar, pull out each of the VALARMs * and write the values into the calendar_alarm table. * * @return null */ function WriteCalendarAlarms($dav_id, vCalendar $vcal) { $qry = new AwlQuery('DELETE FROM calendar_alarm WHERE dav_id = ' . $dav_id); $qry->Exec('PUT', __LINE__, __FILE__); $components = $vcal->GetComponents(); $qry->SetSql('INSERT INTO calendar_alarm ( dav_id, action, trigger, summary, description, component, next_trigger ) VALUES( ' . $dav_id . ', :action, :trigger, :summary, :description, :component, :related::timestamp with time zone + :related_trigger::interval )'); $qry->Prepare(); foreach ($components as $component) { if ($component->GetType() == 'VTIMEZONE') { continue; } $alarms = $component->GetComponents('VALARM'); if (count($alarms) < 1) { return; } foreach ($alarms as $v) { $trigger = array_merge($v->GetProperties('TRIGGER')); if ($trigger == null) { continue; } // Bogus data. $trigger = $trigger[0]; $related = null; $related_trigger = '0M'; $trigger_type = $trigger->GetParameterValue('VALUE'); if (!isset($trigger_type) || $trigger_type == 'DURATION') { switch ($trigger->GetParameterValue('RELATED')) { case 'DTEND': $related = $component->GetPValue('DTEND'); break; case 'DUE': $related = $component->GetPValue('DUE'); break; default: $related = $component->GetPValue('DTSTART'); } $duration = $trigger->Value(); if (!preg_match('{^-?P(:?\\d+W)?(:?\\d+D)?(:?T(:?\\d+H)?(:?\\d+M)?(:?\\d+S)?)?$}', $duration)) { continue; } $minus = substr($duration, 0, 1) == '-'; $related_trigger = trim(preg_replace('#[PT-]#', ' ', $duration)); if ($minus) { $related_trigger = preg_replace('{(\\d+[WDHMS])}', '-$1 ', $related_trigger); } else { $related_trigger = preg_replace('{(\\d+[WDHMS])}', '$1 ', $related_trigger); } } else { if (false === strtotime($trigger->Value())) { continue; } // Invalid date. } $qry->Bind(':action', $v->GetPValue('ACTION')); $qry->Bind(':trigger', $trigger->Render()); $qry->Bind(':summary', $v->GetPValue('SUMMARY')); $qry->Bind(':description', $v->GetPValue('DESCRIPTION')); $qry->Bind(':component', $v->Render()); $qry->Bind(':related', $related); $qry->Bind(':related_trigger', $related_trigger); $qry->Exec('PUT', __LINE__, __FILE__); } } }
/** * Send an iMIP message since they look like a non-local user. * * @param string $method The METHOD parameter from the iTIP * @param string $to_email The e-mail address we're going to send to * @param vCalendar $vcal The iTIP part of the message. */ function doImipMessage($method, $to_email, vCalendar $itip) { global $c, $request; header('Debug: Sending iMIP ' . $method . ' message to ' . $to_email); $mime = new MultiPart(); $mime->addPart($itip->Render(), 'text/calendar; charset=UTF-8; method=' . $method); $friendly_part = isset($c->iMIP->template[$method]) ? $c->iMIP->template[$method] : <<<EOTEMPLATE This is a meeting ##METHOD## which your e-mail program should be able to import into your calendar. Alternatively you could save the attachment and load that into your calendar instead. EOTEMPLATE; $components = $itip->GetComponents('VTIMEZONE', false); $replaceable = array('METHOD', 'DTSTART', 'DTEND', 'SUMMARY', 'DESCRIPTION', 'URL'); foreach ($replaceable as $pname) { $search = '##' . $pname . '##'; if (strstr($friendly_part, $search) !== false) { $property = $itip->GetProperty($pname); if (empty($property)) { $property = $components[0]->GetProperty($pname); } if (empty($property)) { $replace = ''; } else { switch ($pname) { case 'DTSTART': case 'DTEND': $when = new RepeatRuleDateTime($property); $replace = $when->format('c'); break; default: $replace = $property->GetValue(); } } $friendly_part = str_replace($search, $replace, $friendly_part); } } $mime->addPart($friendly_part, 'text/plain'); $email = new EMail(); $email->SetFrom($request->principal->email()); $email->AddTo($to_email); $email->SetSubject($components[0]->GetPValue('SUMMARY')); $email->SetBody($mime->getMimeParts()); if (isset($c->iMIP->pretend_email)) { $email->Pretend($mime->getMimeHeaders()); } else { if (!isset($c->iMIP->send_email) || !$c->iMIP->send_email) { $email->PretendLog($mime->getMimeHeaders()); } else { $email->Send($mime->getMimeHeaders()); } } }