diff --git a/inc/DAVResource.php b/inc/DAVResource.php index 03c4d9ee..7cb3f5b2 100644 --- a/inc/DAVResource.php +++ b/inc/DAVResource.php @@ -1059,8 +1059,8 @@ EOQRY; /** - * Checks whether this resource is a calendar - * @param string $type The type of scheduling collection, 'read', 'write' or 'any' + * Checks whether this resource is a scheduling inbox/outbox collection + * @param string $type The type of scheduling collection, 'inbox', 'outbox' or 'any' */ function IsSchedulingCollection( $type = 'any' ) { if ( $this->_is_collection && preg_match( '{schedule-(inbox|outbox)}', $this->collection->type, $matches ) ) { @@ -1070,6 +1070,18 @@ EOQRY; } + /** + * Checks whether this resource is IN a scheduling inbox/outbox collection + * @param string $type The type of scheduling collection, 'inbox', 'outbox' or 'any' + */ + function IsInSchedulingCollection( $type = 'any' ) { + if ( !$this->_is_collection && preg_match( '{schedule-(inbox|outbox)}', $this->collection->type, $matches ) ) { + return ($type == 'any' || $type == $matches[1]); + } + return false; + } + + /** * Checks whether this resource is an addressbook */ @@ -1256,6 +1268,14 @@ EOQRY; } + /** + * Checks whether the target collection is for public events only + */ + function IsPublicOnly() { + return ( isset($this->collection->publicly_events_only) && $this->collection->publicly_events_only == 't' ); + } + + /** * Return the type of whatever contains this resource, or would if it existed. */ @@ -1382,6 +1402,11 @@ EOQRY; return clone($this->resource); break; + case 'dav-data': + if ( !isset($this->resource) ) $this->FetchResource(); + return $this->resource->caldav_data; + break; + case 'principal': if ( !isset($this->principal) ) $this->FetchPrincipal(); return clone($this->principal); diff --git a/inc/WritableCollection.php b/inc/WritableCollection.php index 34e5b802..6d2bb91e 100644 --- a/inc/WritableCollection.php +++ b/inc/WritableCollection.php @@ -17,7 +17,7 @@ class WritableCollection extends DAVResource { return $p->GetParameterValue('TZID'); } - /** + /** * Writes the data to a member in the collection and returns the segment_name of the * resource in our internal namespace. * @@ -36,7 +36,7 @@ class WritableCollection extends DAVResource { return false; } - global $tz_regex, $session, $caldav_context; + global $session, $caldav_context; $resources = $vcal->GetComponents('VTIMEZONE',false); // Not matching VTIMEZONE $user_no = $this->user_no(); @@ -179,18 +179,11 @@ class WritableCollection extends DAVResource { $calitem_params[':dtstamp'] = $dtstamp; $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 + * RFC2445, 4.8.1.3: Default is PUBLIC */ - if ( !isset($class) || $class == '' ) { + if ( $this->IsPublicOnly() || !isset($class) || $class == '' ) { $class = 'PUBLIC'; } $calitem_params[':class'] = $class; @@ -202,19 +195,11 @@ class WritableCollection extends DAVResource { $tz = $vcal->GetTimeZone($tzid); $olson = $vcal->GetOlsonName($tz); - 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 ) ) { + if ( !empty($olson) && ($olson != $last_olson) ) { dbg_error_log( 'PUT', ' Setting timezone to %s', $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 ); - } } $created = $first->GetPValue('CREATED'); @@ -258,8 +243,8 @@ EOSQL; } if ( !$this->IsSchedulingCollection() ) { - write_alarms($dav_id, $first); - write_attendees($dav_id, $vcal); + $this->WriteCalendarAlarms($dav_id, $vcal); + $this->WriteCalendarAttendees($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 ); } @@ -317,4 +302,144 @@ EOSQL; return $segment_name; } + + /** + * 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__); + } + } + } + + + /** + * Parse out the attendee property and write a row to the + * calendar_attendee table for each one. + * @param int $dav_id The dav_id of the caldav_data we're processing + * @param vComponent The VEVENT or VTODO containing the ATTENDEEs + * @return null + */ + function WriteCalendarAttendees( $dav_id, vCalendar $vcal ) { + $qry = new AwlQuery('DELETE FROM calendar_attendee WHERE dav_id = '.$dav_id ); + $qry->Exec('PUT',__LINE__,__FILE__); + + $attendees = $vcal->GetAttendees(); + if ( count($attendees) < 1 ) return; + + $qry->SetSql('INSERT INTO calendar_attendee ( dav_id, status, partstat, cn, attendee, role, rsvp, property ) + VALUES( '.$dav_id.', :status, :partstat, :cn, :attendee, :role, :rsvp, :property )' ); + $qry->Prepare(); + $processed = array(); + foreach( $attendees AS $v ) { + $attendee = $v->Value(); + if ( isset($processed[$attendee]) ) { + dbg_error_log( 'LOG', 'Duplicate attendee "%s" in resource "%d"', $attendee, $dav_id ); + dbg_error_log( 'LOG', 'Original: "%s"', $processed[$attendee] ); + dbg_error_log( 'LOG', 'Duplicate: "%s"', $v->Render() ); + continue; /** @todo work out why we get duplicate ATTENDEE on one VEVENT */ + } + $qry->Bind(':attendee', $attendee ); + $qry->Bind(':status', $v->GetParameterValue('STATUS') ); + $qry->Bind(':partstat', $v->GetParameterValue('PARTSTAT') ); + $qry->Bind(':cn', $v->GetParameterValue('CN') ); + $qry->Bind(':role', $v->GetParameterValue('ROLE') ); + $qry->Bind(':rsvp', $v->GetParameterValue('RSVP') ); + $qry->Bind(':property', $v->Render() ); + $qry->Exec('PUT',__LINE__,__FILE__); + $processed[$attendee] = $v->Render(); + } + } + + /** + * Writes the data to a member in the collection and returns the segment_name of the + * resource in our internal namespace. + * + * @param vCalendar $member_dav_name The path to the resource to be deleted. + * @return boolean Success is true, or false on failure. + */ + function actualDeleteCalendarMember( $member_dav_name ) { + global $session, $caldav_context; + + // A quick sanity check... + $segment_name = str_replace( $this->dav_name(), '', $member_dav_name ); + if ( strstr($segment_name, '/') !== false ) { + @dbg_error_log( "DELETE", "DELETE: Refused to delete member '%s' from calendar '%s'!", $member_dav_name, $this->dav_name() ); + return false; + } + + // We need to serialise access to this process just for this collection + $cache = getCacheInstance(); + $myLock = $cache->acquireLock('collection-'.$this->dav_name()); + + $qry = new AwlQuery(); + $params = array( ':dav_name' => $member_dav_name ); + + if ( $qry->QDo("SELECT write_sync_change(collection_id, 404, caldav_data.dav_name) FROM caldav_data WHERE dav_name = :dav_name", $params ) + && $qry->QDo("DELETE FROM property WHERE dav_name = :dav_name", $params ) + && $qry->QDo("DELETE FROM locks WHERE dav_name = :dav_name", $params ) + && $qry->QDo("DELETE FROM caldav_data WHERE dav_name = :dav_name", $params ) ) { + @dbg_error_log( "DELETE", "DELETE: Calendar member %s deleted from calendar '%s'", $member_dav_name, $this->dav_name() ); + + $cache->releaseLock($myLock); + + return true; + } + + $cache->releaseLock($myLock); + return false; + + } + } diff --git a/inc/caldav-DELETE.php b/inc/caldav-DELETE.php index fddb3b24..1029816f 100644 --- a/inc/caldav-DELETE.php +++ b/inc/caldav-DELETE.php @@ -17,6 +17,7 @@ $container->NeedPrivilege('DAV::unbind'); $lock_opener = $request->FailIfLocked(); +require_once('schedule-functions.php'); function delete_collection( $id ) { $params = array( ':collection_id' => $id ); @@ -72,12 +73,14 @@ else { $request->DoResponse( 412, translate("Resource has changed on server - not deleted") ); } - $params = array( ':dav_id' => $dav_resource->resource_id() ); - + // Check to see if we need to do any scheduling transactions for this one. + do_scheduling_for_delete($dav_resource); + // We need to serialise access to this process just for this collection $cache = getCacheInstance(); $myLock = $cache->acquireLock('collection-'.$dav_resource->parent_path()); + $params = array( ':dav_id' => $dav_resource->resource_id() ); if ( $qry->QDo("SELECT write_sync_change(collection_id, 404, caldav_data.dav_name) FROM caldav_data WHERE dav_id = :dav_id", $params ) && $qry->QDo("DELETE FROM property WHERE dav_name = (SELECT dav_name FROM caldav_data WHERE dav_id = :dav_id)", $params ) && $qry->QDo("DELETE FROM locks WHERE dav_name = (SELECT dav_name FROM caldav_data WHERE dav_id = :dav_id)", $params ) @@ -92,7 +95,6 @@ else { $request->DoResponse( 204 ); } $cache->releaseLock($myLock); - } $request->DoResponse( 500 ); diff --git a/inc/schedule-functions.php b/inc/schedule-functions.php new file mode 100644 index 00000000..9553941c --- /dev/null +++ b/inc/schedule-functions.php @@ -0,0 +1,310 @@ + + * @copyright Morphoss Ltd - http://www.morphoss.com/ + * @license http://gnu.org/copyleft/gpl.html GNU GPL v2 or later version + */ + +require_once('vCalendar.php'); +require_once('WritableCollection.php'); +require_once('RRule-v2.php'); + +/** + * Entry point for scheduling on DELETE, for which there are thee outcomes: + * - We don't do scheduling (disabled, no organizer, ...) + * - We are an ATTENDEE declining the meeting. + * - We are the ORGANIZER canceling the meeting. + * + * @param DAVResource $deleted_resource The resource which has already been deleted + */ +function do_scheduling_for_delete(DAVResource $deleted_resource ) { + // By the time we arrive here the resource *has* actually been deleted from disk + // we can only fail to (de-)schedule the activity... + global $request, $c; + + if ( !isset($request) || (isset($c->enable_auto_schedule) && !$c->enable_auto_schedule) ) return true; + if ( $deleted_resource->IsInSchedulingCollection() ) return true; + + $caldav_data = $deleted_resource->GetProperty('dav-data'); + if ( empty($caldav_data) ) return true; + + $vcal = new vCalendar($caldav_data); + $organizer = $vcal->GetOrganizer(); + if ( $organizer === false || empty($organizer) ) { + dbg_error_log( 'schedule', 'Event has no organizer - no scheduling required.' ); + return true; + } + if ( $vcal->GetScheduleAgent() != 'SERVER' ) { + dbg_error_log( 'schedule', 'SCHEDULE-AGENT=%s - no scheduling required.', $vcal->GetScheduleAgent() ); + return true; + } + $organizer_email = preg_replace( '/^mailto:/i', '', $organizer->Value() ); + + if ( $request->principal->email() == $organizer_email ) { + return doItipOrganizerCancel( $vcal ); + } + else { + if ( isset($_SERVER['HTTP_SCHEDULE_REPLY']) && $_SERVER['HTTP_SCHEDULE_REPLY'] == 'F') { + dbg_error_log( 'schedule', 'Schedule-Request header set to "F" - no scheduling required.' ); + return true; + } + return doItipAttendeeReply( $vcal, 'DECLINED', $request->principal->email()); + } + +} + + +/** +* Do the scheduling adjustments for a REPLY when an ATTENDEE updates their status. +* @param vCalendar $vcal The resource that the ATTENDEE is writing to their calendar +* @param string $organizer The property which is the event ORGANIZER. +*/ +//function do_scheduling_reply( vCalendar $vcal, vProperty $organizer ) { +function doItipAttendeeReply( vCalendar $resource, $partstat ) { + global $request; + + $organizer = $resource->GetOrganizer(); + $organizer_email = preg_replace( '/^mailto:/i', '', $organizer->Value() ); + $organizer_principal = new Principal('email',$organizer_email ); + + $sql = 'SELECT caldav_data.dav_name, caldav_data.caldav_data FROM caldav_data JOIN calendar_item USING(dav_id) '; + $sql .= 'WHERE caldav_data.collection_id IN (SELECT collection_id FROM collection WHERE is_calendar AND user_no =?) '; + $sql .= 'AND uid=? LIMIT 1'; + $uids = $resource->GetPropertiesByPath('/VCALENDAR/*/UID'); + if ( count($uids) == 0 ) { + dbg_error_log( 'schedule', 'No UID in VCALENDAR - giving up on REPLY.' ); + return true; + } + $uid = $uids[0]->Value(); + $qry = new AwlQuery($sql,$organizer_principal->user_no(), $uid); + if ( !$qry->Exec('schedule',__LINE__,__FILE__) || $qry->rows() < 1 ) { + dbg_error_log( 'schedule', 'Could not find original event from organizer - giving up on REPLY.' ); + return true; + } + $row = $qry->Fetch(); + $collection_path = preg_replace('{/[^/]+$}', '/', $row->dav_name ); + $segment_name = str_replace($collection_path, '', $row->dav_name ); + $vcal = new vCalendar($row->caldav_data); + + $attendees = $vcal->GetAttendees(); + foreach( $attendees AS $v ) { + $email = preg_replace( '/^mailto:/i', '', $v->Value() ); + if ( $email == $request->principal->email() ) { + $attendee = $v; + break; + } + } + if ( empty($attendee) ) { + dbg_error_log( 'schedule', 'Could not find ATTENDEE in VEVENT - giving up on REPLY.' ); + return true; + } + + $attendee->SetParameterValue('PARTSTAT', $partstat); + $attendee->SetParameterValue('SCHEDULE-STATUS', '2.0'); + $vcal->UpdateAttendeeStatus($request->principal->email(), clone($attendee) ); + + $organizer_calendar = new WritableCollection(array('path' => $collection_path)); + $organizer_inbox = new WritableCollection(array('path' => $organizer_principal->internal_url('schedule-inbox'))); + + $schedule_reply = GetItip(new vCalendar($row->caldav_data),'REPLY',$attendee->Value()); + $schedule_request = GetItip(new vCalendar($row->caldav_data),'REQUEST',null); + + dbg_error_log( 'schedule', 'Writing ATTENDEE scheduling REPLY from %s to %s', $request->principal->email(), $organizer_principal->email() ); + + $response = '3.7'; // Organizer was not found on server. + if ( !$organizer_calendar->Exists() ) { + dbg_error_log('ERROR','Default calendar at "%s" does not exist for user "%s"', + $organizer_calendar->dav_name(), $schedule_target->username()); + $response = '5.2'; // No scheduling support for user + } + else { + if ( ! $organizer_inbox->HavePrivilegeTo('schedule-deliver-reply') ) { + $response = '3.8'; // No authority to deliver replies to organizer. + } + $response = '1.2'; // Scheduling reply delivered successfully + if ( $organizer_calendar->WriteCalendarMember($vcal, false, false, $segment_name) === false ) { + dbg_error_log('ERROR','Could not write updated calendar member to %s', $attendee_calendar->dav_name() ); + trace_bug('Failed to write scheduling resource.'); + } + $organizer_inbox->WriteCalendarMember($schedule_reply, false, false, $request->principal->username().$segment_name); + } + + + dbg_error_log( 'schedule', 'Status for organizer <%s> set to "%s"', $organizer->Value(), $response ); + $organizer->SetParameterValue( 'SCHEDULE-STATUS', $response ); + $resource->UpdateOrganizerStatus($organizer); // Which was passed in by reference, and we're updating it here. + + // Now we loop through the *other* ATTENDEEs, updating them on the status of the ATTENDEE DECLINE/ACCEPT + foreach( $attendees AS $attendee ) { + $email = preg_replace( '/^mailto:/i', '', $attendee->Value() ); + if ( $email == $request->principal->email() || $email == $organizer_principal->email() ) continue; + + $agent = $attendee->GetParameterValue('SCHEDULE-AGENT'); + if ( !empty($agent) && $agent != 'SERVER' ) continue; + + $schedule_target = new Principal('email',$email); + if ( $schedule_target->Exists() ) { + $attendee_calendar = new WritableCollection(array('path' => $schedule_target->internal_url('schedule-default-calendar'))); + if ( !$attendee_calendar->Exists() ) { + dbg_error_log('ERROR','Default calendar at "%s" does not exist for user "%s"', + $attendee_calendar->dav_name(), $schedule_target->username()); + continue; + } + else { + $attendee_inbox = new WritableCollection(array('path' => $schedule_target->internal_url('schedule-inbox'))); + if ( ! $attendee_inbox->HavePrivilegeTo('schedule-deliver-invite') ) continue; + + if ( $attendee_calendar->WriteCalendarMember($vcal, false) === false ) { + dbg_error_log('ERROR','Could not write updated calendar member to %s', $attendee_calendar->dav_name()); + trace_bug('Failed to write scheduling resource.'); + } + $attendee_inbox->WriteCalendarMember($schedule_request, false); + } + } + } + + return true; +} + +function GetItip( VCalendar $vcal, $method, $attendee_value ) { + + $iTIP = $vcal->GetItip($method, $attendee_value ); + $iTIP->AddProperty('REQUEST-STATUS','2.0'); + $components = $iTIP->GetComponents(); + foreach( $components AS $comp ) { + $properties = array(); + foreach( $comp->GetProperties() AS $k=> $property ) { + switch( $property->Name() ) { + case 'DTSTART': + case 'DTEND': + case 'DUE': + $when = new RepeatRuleDateTime($property); + $properties[] = new vProperty( $property->Name() . ":" . $when->UTC() ); + break; + default: + $properties[] = $property; + } + } + $comp->SetProperties($properties); + } + + return $iTIP; +} + +/** + * Handles sending the iTIP CANCEL messages to each ATTENDEE by the ORGANIZER. + * @param vCalendar $vcal What's being cancelled. + */ +function doItipOrganizerCancel( vCalendar $vcal ) { + global $request; + + $attendees = $vcal->GetAttendees(); + if ( count($attendees) == 0 && count($old_attendees) == 0 ) { + dbg_error_log( 'schedule', 'Event has no attendees - no scheduling required.', count($attendees) ); + return true; + } + + dbg_error_log( 'schedule', 'Writing scheduling resources for %d attendees', count($attendees) ); + $scheduling_actions = false; + + $iTIP = GetItip($vcal, 'CANCEL', null); + + foreach( $attendees AS $attendee ) { + $email = preg_replace( '/^mailto:/i', '', $attendee->Value() ); + if ( $email == $request->principal->email() ) { + dbg_error_log( 'schedule', "not delivering to owner '%s'", $request->principal->email() ); + continue; + } + + $agent = $attendee->GetParameterValue('SCHEDULE-AGENT'); + if ( $agent && $agent != 'SERVER' ) { + dbg_error_log( 'schedule', "not delivering to %s, schedule agent set to value other than server", $email ); + continue; + } + $schedule_target = new Principal('email',$email); + if ( !$schedule_target->Exists() ) { + $response = '3.7'; + } + else { + $attendee_inbox = new WritableCollection(array('path' => $schedule_target->internal_url('schedule-inbox'))); + if ( ! $attendee_inbox->HavePrivilegeTo('schedule-deliver-invite') ) { + dbg_error_log( 'schedule', "No authority to deliver invite to %s", $schedule_target->internal_url('schedule-inbox') ); + $response = '3.8'; + } + else { + $attendee_calendar = new WritableCollection(array('path' => $schedule_target->internal_url('schedule-default-calendar'))); + $response = processItipCancel( $vcal, $attendee, $attendee_calendar, $schedule_target ); + deliverItipCancel( $iTIP, $attendee, $attendee_inbox ); + } + } + dbg_error_log( 'schedule', 'Status for attendee <%s> set to "%s"', $attendee->Value(), $response ); + $attendee->SetParameterValue( 'SCHEDULE-STATUS', $response ); + $scheduling_actions = true; + } + + return true; +} + +/** + * Does the actual processing of the iTIP CANCEL message on behalf of an ATTENDEE, + * which generally means writing it into the ATTENDEE's default calendar. + * + * @param vCalendar $vcal The message. + * @param vProperty $attendee + * @param WritableCollection $attendee_calendar + */ +function processItipCancel( vCalendar $vcal, vProperty $attendee, WritableCollection $attendee_calendar, Principal $attendee_principal ) { + + dbg_error_log( 'schedule', 'Processing iTIP CANCEL to %s', $attendee->Value()); + if ( !$attendee_calendar->Exists() ) { + dbg_error_log('ERROR', 'Default calendar at "%s" does not exist for attendee "%s"', + $attendee_calendar->dav_name(), $attendee->Value()); + return '5.2'; // No scheduling support for user + } + + $sql = 'SELECT caldav_data.dav_name FROM caldav_data JOIN calendar_item USING(dav_id) '; + $sql .= 'WHERE caldav_data.collection_id IN (SELECT collection_id FROM collection WHERE is_calendar AND user_no =?) '; + $sql .= 'AND uid=? LIMIT 1'; + $uids = $vcal->GetPropertiesByPath('/VCALENDAR/*/UID'); + if ( count($uids) == 0 ) { + dbg_error_log( 'schedule', 'No UID in VCALENDAR - giving up on CANCEL processing.' ); + return '3.8'; + } + $uid = $uids[0]->Value(); + $qry = new AwlQuery($sql, $attendee_principal->user_no(), $uid); + if ( !$qry->Exec('schedule',__LINE__,__FILE__) || $qry->rows() < 1 ) { + dbg_error_log( 'schedule', 'Could not find ATTENDEE copy of original event - not trying to DELETE it!' ); + return '1.2'; + } + $row = $qry->Fetch(); + + if ( $attendee_calendar->actualDeleteCalendarMember($row->dav_name) === false ) { + dbg_error_log('ERROR', 'Could not delete calendar member %s for %s', + $row->dav_name(), $attendee->Value()); + trace_bug('Failed to write scheduling resource.'); + return '5.2'; + } + + return '1.2'; // Scheduling invitation delivered successfully + +} + + +/** + * Delivers the iTIP CANCEL message to an ATTENDEE's Scheduling Inbox Collection. + * + * This is pretty simple at present, but could be extended in the future to do the sending + * of e-mail to a remote attendee. + * + * @param vCalendar $iTIP + * @param vProperty $attendee + * @param WritableCollection $attendee_inbox + */ +function deliverItipCancel( vCalendar $iTIP, vProperty $attendee, WritableCollection $attendee_inbox ) { + $attendee_inbox->WriteCalendarMember($iTIP, false); +} + diff --git a/testing/tests/scheduling/3026-PUT-Accept.result b/testing/tests/scheduling/3026-PUT-Accept.result index 9985afc7..73ec3062 100644 --- a/testing/tests/scheduling/3026-PUT-Accept.result +++ b/testing/tests/scheduling/3026-PUT-Accept.result @@ -29,16 +29,16 @@ BEGIN:VEVENT CREATED:20111018T195845Z UID:E1A13F04-iCal-schedule DTEND;TZID=Pacific/Auckland:20111019T110000 -ATTENDEE;CN=Manager 1;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED: - mailto:manager1@example.net -ATTENDEE;CN=user1@example.net;CUTYPE=INDIVIDUAL;EMAIL=user1@example.net; - PARTSTAT=ACCEPTED;ROLE=REQ-PARTICIPANT:mailto:user1@example.net TRANSP:OPAQUE SUMMARY:Meeting with User1 DTSTART;TZID=Pacific/Auckland:20111019T100000 DTSTAMP:20111018T200107Z ORGANIZER;CN="Manager 1":mailto:manager1@example.net SEQUENCE:5 +ATTENDEE;CN=Manager 1;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED: + mailto:manager1@example.net +ATTENDEE;CN=user1@example.net;CUTYPE=INDIVIDUAL;EMAIL=user1@example.net; + PARTSTAT=ACCEPTED;ROLE=REQ-PARTICIPANT:mailto:user1@example.net END:VEVENT END:VCALENDAR < @@ -118,16 +118,16 @@ BEGIN:VEVENT CREATED:20111018T195845Z UID:E1A13F04-iCal-schedule DTEND;TZID=Pacific/Auckland:20111019T110000 -ATTENDEE;CN=Manager 1;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED: - mailto:manager1@example.net -ATTENDEE;CN=user1@example.net;CUTYPE=INDIVIDUAL;EMAIL=user1@example.net; - PARTSTAT=ACCEPTED;ROLE=REQ-PARTICIPANT:mailto:user1@example.net TRANSP:OPAQUE SUMMARY:Meeting with User1 DTSTART;TZID=Pacific/Auckland:20111019T100000 DTSTAMP:20111018T200107Z ORGANIZER;CN="Manager 1":mailto:manager1@example.net SEQUENCE:5 +ATTENDEE;CN=Manager 1;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED: + mailto:manager1@example.net +ATTENDEE;CN=user1@example.net;CUTYPE=INDIVIDUAL;EMAIL=user1@example.net; + PARTSTAT=ACCEPTED;ROLE=REQ-PARTICIPANT:mailto:user1@example.net END:VEVENT END:VCALENDAR < diff --git a/testing/tests/scheduling/3027-PUT-iCal-with-attendees.result b/testing/tests/scheduling/3027-PUT-iCal-with-attendees.result index 5c2adf11..707c4729 100644 --- a/testing/tests/scheduling/3027-PUT-iCal-with-attendees.result +++ b/testing/tests/scheduling/3027-PUT-iCal-with-attendees.result @@ -127,16 +127,16 @@ BEGIN:VEVENT CREATED:20111018T195845Z UID:E1A13F04-iCal-schedule DTEND;TZID=Pacific/Auckland:20111019T110000 -ATTENDEE;CN=Manager 1;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED: - mailto:manager1@example.net -ATTENDEE;CN=user1@example.net;CUTYPE=INDIVIDUAL;EMAIL=user1@example.net; - PARTSTAT=ACCEPTED;ROLE=REQ-PARTICIPANT:mailto:user1@example.net TRANSP:OPAQUE SUMMARY:Meeting with User1 DTSTART;TZID=Pacific/Auckland:20111019T100000 DTSTAMP:20111018T200107Z ORGANIZER;CN="Manager 1":mailto:manager1@example.net SEQUENCE:5 +ATTENDEE;CN=Manager 1;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED: + mailto:manager1@example.net +ATTENDEE;CN=user1@example.net;CUTYPE=INDIVIDUAL;EMAIL=user1@example.net; + PARTSTAT=ACCEPTED;ROLE=REQ-PARTICIPANT:mailto:user1@example.net END:VEVENT END:VCALENDAR < diff --git a/testing/tests/scheduling/3027-PUT-iCal-with-attendees.test b/testing/tests/scheduling/3027-PUT-iCal-with-attendees.test index c9fce207..d06a4b8d 100644 --- a/testing/tests/scheduling/3027-PUT-iCal-with-attendees.test +++ b/testing/tests/scheduling/3027-PUT-iCal-with-attendees.test @@ -5,7 +5,7 @@ TYPE=PUT URL=http://regression.host/caldav.php/manager1/home/E1A13F04-iCal-schedule.ics HEADER=Content-Type: text/calendar HEADER=DAVKit/4.0.3 (732.2); CalendarStore/4.0.4 (997.7); iCal/4.0.4 (1395.7); Mac OS X/10.6.8 (10K549) -HEADER=If-Match: "87421a4ff0b84a95a31db428ee6d11d9" +HEADER=If-Match: "d60f8959edc5eee6e949a2e5b81dd746" HEAD AUTH=manager1:manager1 diff --git a/testing/tests/scheduling/3028-DELETE-reply.result b/testing/tests/scheduling/3028-DELETE-reply.result new file mode 100644 index 00000000..b9b5f27b --- /dev/null +++ b/testing/tests/scheduling/3028-DELETE-reply.result @@ -0,0 +1,14 @@ +HTTP/1.1 204 No Content +Date: Dow, 01 Jan 2000 00:00:00 GMT +DAV: 1, 2, 3, access-control, calendar-access, calendar-schedule +DAV: extended-mkcol, calendar-proxy, bind, addressbook, calendar-auto-schedule +Content-Length: 0 +Content-Type: text/plain; charset="utf-8" + + + dav_name: >/manager1/home/E1A13F04-iCal-schedule.ics< + + dav_name: >/user1/home/E1A13F04-iCal-schedule.ics< + + dav_name: >/manager1/.in/user1E1A13F04-iCal-schedule.ics< + diff --git a/testing/tests/scheduling/3028-DELETE-reply.test b/testing/tests/scheduling/3028-DELETE-reply.test new file mode 100644 index 00000000..6dcf874d --- /dev/null +++ b/testing/tests/scheduling/3028-DELETE-reply.test @@ -0,0 +1,18 @@ +# +# iCal DELETE's the invitation in the .in +# +TYPE=DELETE +URL=http://regression.host/user1/.in/E1A13F04-iCal-schedule.ics + +HEADER=User-Agent: DAVKit/4.0.3 (732.2); CalendarStore/4.0.4 (997.7); iCal/4.0.4 (1395.7); Mac OS X/10.6.8 (10K549) +HEAD + +# +# Query to confirm we got rid of it. There should be two now. +QUERY +SELECT dav_name + FROM calendar_item + WHERE uid = 'E1A13F04-iCal-schedule' + ORDER BY dav_id +ENDQUERY + diff --git a/testing/tests/scheduling/3029-DELETE-reply.result b/testing/tests/scheduling/3029-DELETE-reply.result new file mode 100644 index 00000000..798ba092 --- /dev/null +++ b/testing/tests/scheduling/3029-DELETE-reply.result @@ -0,0 +1,12 @@ +HTTP/1.1 204 No Content +Date: Dow, 01 Jan 2000 00:00:00 GMT +DAV: 1, 2, 3, access-control, calendar-access, calendar-schedule +DAV: extended-mkcol, calendar-proxy, bind, addressbook, calendar-auto-schedule +Content-Length: 0 +Content-Type: text/plain; charset="utf-8" + + + dav_name: >/manager1/home/E1A13F04-iCal-schedule.ics< + + dav_name: >/user1/home/E1A13F04-iCal-schedule.ics< + diff --git a/testing/tests/scheduling/3029-DELETE-reply.test b/testing/tests/scheduling/3029-DELETE-reply.test new file mode 100644 index 00000000..cac5837a --- /dev/null +++ b/testing/tests/scheduling/3029-DELETE-reply.test @@ -0,0 +1,20 @@ +# +# iCal DELETE's the invitation in the .in +# +TYPE=DELETE +URL=http://regression.host/manager1/.in/user1E1A13F04-iCal-schedule.ics + +HEADER=User-Agent: DAVKit/4.0.3 (732.2); CalendarStore/4.0.4 (997.7); iCal/4.0.4 (1395.7); Mac OS X/10.6.8 (10K549) +HEAD + +AUTH=manager1:manager1 + +# +# Query to confirm we got rid of it. There should be two now. +QUERY +SELECT dav_name + FROM calendar_item + WHERE uid = 'E1A13F04-iCal-schedule' + ORDER BY dav_id +ENDQUERY + diff --git a/testing/tests/scheduling/3030-DELETE-attendee-event.result b/testing/tests/scheduling/3030-DELETE-attendee-event.result new file mode 100644 index 00000000..0e514a6b --- /dev/null +++ b/testing/tests/scheduling/3030-DELETE-attendee-event.result @@ -0,0 +1,70 @@ +HTTP/1.1 204 No Content +Date: Dow, 01 Jan 2000 00:00:00 GMT +DAV: 1, 2, 3, access-control, calendar-access, calendar-schedule +DAV: extended-mkcol, calendar-proxy, bind, addressbook, calendar-auto-schedule +Content-Length: 0 +Content-Type: text/plain; charset="utf-8" + + + caldav_data: >BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Apple Inc.//iCal 4.0.4//EN +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:Pacific/Auckland +BEGIN:DAYLIGHT +TZOFFSETFROM:+1200 +RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU +DTSTART:20070930T020000 +TZNAME:GMT+13:00 +TZOFFSETTO:+1300 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+1300 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU +DTSTART:20080406T030000 +TZNAME:GMT+12:00 +TZOFFSETTO:+1200 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20111018T195845Z +UID:E1A13F04-iCal-schedule +DTEND;TZID=Pacific/Auckland:20111019T140000 +TRANSP:OPAQUE +SUMMARY:Meeting with User1 +DTSTART;TZID=Pacific/Auckland:20111019T130000 +DTSTAMP:20111024T035702Z +ORGANIZER;CN="Manager 1":mailto:manager1@example.net +SEQUENCE:6 +ATTENDEE;CN=Manager 1;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED: + mailto:manager1@example.net +ATTENDEE;CN=user1@example.net;CUTYPE=INDIVIDUAL;EMAIL=user1@example.net; + PARTSTAT=DECLINED;ROLE=REQ-PARTICIPANT;RSVP=TRUE;SCHEDULE-STATUS=2.0:mai + lto:user1@example.net +END:VEVENT +END:VCALENDAR +< + dav_name: >/manager1/home/E1A13F04-iCal-schedule.ics< + + caldav_data: >BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Apple Inc.//iCal 4.0.4//EN +CALSCALE:GREGORIAN +METHOD:REPLY +REQUEST-STATUS:2.0 +BEGIN:VEVENT +UID:E1A13F04-iCal-schedule +DTEND:20111019T010000Z +ATTENDEE;CN=user1@example.net;CUTYPE=INDIVIDUAL;EMAIL=user1@example.net; + PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;RSVP=TRUE;SCHEDULE-STATUS=1.2 + :mailto:user1@example.net +DTSTART:20111019T000000Z +ORGANIZER;CN="Manager 1":mailto:manager1@example.net +SEQUENCE:7 +DTSTAMP:20111102T010804Z +END:VEVENT +END:VCALENDAR +< + dav_name: >/manager1/.in/user1E1A13F04-iCal-schedule.ics< + diff --git a/testing/tests/scheduling/3030-DELETE-attendee-event.test b/testing/tests/scheduling/3030-DELETE-attendee-event.test new file mode 100644 index 00000000..3f7ec167 --- /dev/null +++ b/testing/tests/scheduling/3030-DELETE-attendee-event.test @@ -0,0 +1,23 @@ +# +# We now DELETE the ATTENDEE's copy of the actual event. +# - This should send a CANCEL reply and update the manager's +# event copy with the PARTSTAT=DECLINED +# +TYPE=DELETE +URL=http://regression.host/user1/home/E1A13F04-iCal-schedule.ics + +HEADER=User-Agent: DAVKit/4.0.3 (732.2); CalendarStore/4.0.4 (997.7); iCal/4.0.4 (1395.7); Mac OS X/10.6.8 (10K549) +HEAD + +# +# Query to confirm we got rid of it. There should be two now: +# - An event in the manager's calendar, with a PARTSTART=DECLINED for user 1 +# - An iTIP message in the managers's inbox. +QUERY +SELECT calendar_item.dav_name, + caldav_data.caldav_data + FROM calendar_item JOIN caldav_data USING(dav_id, dav_name) + WHERE uid = 'E1A13F04-iCal-schedule' + ORDER BY dav_id +ENDQUERY + diff --git a/testing/tests/scheduling/3031-DELETE-reply.test b/testing/tests/scheduling/3031-DELETE-reply.test new file mode 100644 index 00000000..8e423e19 --- /dev/null +++ b/testing/tests/scheduling/3031-DELETE-reply.test @@ -0,0 +1,21 @@ +# +# Now DELETE's the cancelation reply in the Manager's .in +# +TYPE=DELETE +URL=http://regression.host/manager1/.in/user1E1A13F04-iCal-schedule.ics + +HEADER=User-Agent: DAVKit/4.0.3 (732.2); CalendarStore/4.0.4 (997.7); iCal/4.0.4 (1395.7); Mac OS X/10.6.8 (10K549) +HEAD + +AUTH=manager1:manager1 + +# +# Query to confirm we got rid of it. There should only be one +# lonely meeting in the manager's calendar (which has been declined). +QUERY +SELECT dav_name + FROM calendar_item + WHERE uid = 'E1A13F04-iCal-schedule' + ORDER BY dav_id +ENDQUERY + diff --git a/testing/tests/scheduling/3032-PUT-iCal-with-attendees.test b/testing/tests/scheduling/3032-PUT-iCal-with-attendees.test new file mode 100644 index 00000000..6d24381b --- /dev/null +++ b/testing/tests/scheduling/3032-PUT-iCal-with-attendees.test @@ -0,0 +1,73 @@ +# +# PUT an event with several attendees - so we can delete the organizer +# copy in the next request. +# +# After this we should see 7 events: 1 manager, 3 attendees, 3 attendee iTIP +# +TYPE=PUT +URL=http://regression.host/caldav.php/manager1/home/E1A13F04-iCal-schedule.ics +HEADER=Content-Type: text/calendar +HEADER=DAVKit/4.0.3 (732.2); CalendarStore/4.0.4 (997.7); iCal/4.0.4 (1395.7); Mac OS X/10.6.8 (10K549) +HEADER=If-Match: "651df94a71cc99384231637a5df101f4" +HEAD + +AUTH=manager1:manager1 + +BEGINDATA +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Apple Inc.//iCal 4.0.4//EN +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:Pacific/Auckland +BEGIN:DAYLIGHT +TZOFFSETFROM:+1200 +RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU +DTSTART:20070930T020000 +TZNAME:GMT+13:00 +TZOFFSETTO:+1300 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+1300 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU +DTSTART:20080406T030000 +TZNAME:GMT+12:00 +TZOFFSETTO:+1200 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20111018T195845Z +UID:E1A13F04-iCal-schedule +DTEND;TZID=Pacific/Auckland:20111019T140000 +ATTENDEE;CN="Manager 1";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailto:manag + er1@example.net +ATTENDEE;CN="user1@example.net";CUTYPE=INDIVIDUAL;EMAIL="user1@example.n + et";PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:user1@ex + ample.net +ATTENDEE;CN="user2@example.net";CUTYPE=INDIVIDUAL;EMAIL="user2@example.n + et";PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:user2@ex + ample.net +ATTENDEE;CN="user3@example.net";CUTYPE=INDIVIDUAL;EMAIL="user3@example.n + et";PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:user3@ex + ample.net +TRANSP:OPAQUE +SUMMARY:Meeting with User1 +DTSTART;TZID=Pacific/Auckland:20111019T130000 +DTSTAMP:20111024T035702Z +ORGANIZER;CN="Manager 1":mailto:manager1@example.net +SEQUENCE:7 +END:VEVENT +END:VCALENDAR +ENDDATA + + + +QUERY +SELECT caldav_data.user_no, caldav_data.dav_name, + caldav_type, logged_user, caldav_data.caldav_data AS "vcalendar", + summary +FROM caldav_data JOIN calendar_item USING(dav_name) LEFT JOIN timezones ON (tz_id=tzid) +WHERE calendar_item.uid = 'E1A13F04-iCal-schedule' +ORDER BY caldav_data.dav_id +ENDQUERY + diff --git a/testing/tests/scheduling/3033-DELETE-organizer-event.test b/testing/tests/scheduling/3033-DELETE-organizer-event.test new file mode 100644 index 00000000..12b13916 --- /dev/null +++ b/testing/tests/scheduling/3033-DELETE-organizer-event.test @@ -0,0 +1,31 @@ +# +# We now DELETE the ORGANIZER's copy of the actual event. +# - This should send a CANCEL reply and remove each attendee's +# copy of the event +# +TYPE=DELETE +URL=http://regression.host/manager1/home/E1A13F04-iCal-schedule.ics + +HEADER=User-Agent: DAVKit/4.0.3 (732.2); CalendarStore/4.0.4 (997.7); iCal/4.0.4 (1395.7); Mac OS X/10.6.8 (10K549) +HEAD + +AUTH=manager1:manager1 + +# Before we run, this time we'll assume everyone has read their inbox +# and all existing iTIP messages are deleted. +DOSQL +DELETE FROM caldav_data WHERE dav_name ~ E'/\\.in/.*E1A13F04-iCal-schedule\\.ics' +ENDDOSQL + +# +# Query to confirm we got rid of it. There should be two now: +# - An event in the manager's calendar, with a PARTSTART=DECLINED for user 1 +# - An iTIP message in the managers's inbox. +QUERY +SELECT calendar_item.dav_name, + caldav_data.caldav_data + FROM calendar_item JOIN caldav_data USING(dav_id, dav_name) + WHERE uid = 'E1A13F04-iCal-schedule' + ORDER BY dav_id +ENDQUERY +