DAViCal
schedule-functions.php
1 <?php
12 require_once('vCalendar.php');
13 require_once('WritableCollection.php');
14 require_once('RRule.php');
15 
24 function do_scheduling_for_delete(DAVResource $deleted_resource ) {
25  // By the time we arrive here the resource *has* actually been deleted from disk
26  // we can only fail to (de-)schedule the activity...
27  global $request, $c;
28 
29  if ( !isset($request) || (isset($c->enable_auto_schedule) && !$c->enable_auto_schedule) ) return true;
30  if ( $deleted_resource->IsInSchedulingCollection() ) return true;
31 
32  $caldav_data = $deleted_resource->GetProperty('dav-data');
33  if ( empty($caldav_data) ) return true;
34 
35  $vcal = new vCalendar($caldav_data);
36  $organizer = $vcal->GetOrganizer();
37  if ( $organizer === false || empty($organizer) ) {
38  dbg_error_log( 'schedule', 'Event has no organizer - no scheduling required.' );
39  return true;
40  }
41  if ( $vcal->GetScheduleAgent() != 'SERVER' ) {
42  dbg_error_log( 'schedule', 'SCHEDULE-AGENT=%s - no scheduling required.', $vcal->GetScheduleAgent() );
43  return true;
44  }
45  $organizer_email = preg_replace( '/^mailto:/i', '', $organizer->Value() );
46 
47  if ( $request->principal->email() == $organizer_email ) {
48  return doItipOrganizerCancel( $vcal );
49  }
50  else {
51  if ( isset($_SERVER['HTTP_SCHEDULE_REPLY']) && $_SERVER['HTTP_SCHEDULE_REPLY'] == 'F') {
52  dbg_error_log( 'schedule', 'Schedule-Reply header set to "F" - no scheduling required.' );
53  return true;
54  }
55  return doItipAttendeeReply( $vcal, 'DECLINED', $request->principal->email());
56  }
57 
58 }
59 
60 
66 //function do_scheduling_reply( vCalendar $vcal, vProperty $organizer ) {
67 function doItipAttendeeReply( vCalendar $resource, $partstat ) {
68  global $request;
69 
70  $organizer = $resource->GetOrganizer();
71  $organizer_email = preg_replace( '/^mailto:/i', '', $organizer->Value() );
72  $organizer_principal = new Principal('email',$organizer_email );
73 
74  if ( !$organizer_principal->Exists() ) {
75  dbg_error_log( 'schedule', 'Unknown ORGANIZER "%s" - unable to notify.', $organizer->Value() );
76  //TODO: header( "Debug: Could maybe do the iMIP message dance for organizer ". $organizer->Value() );
77  return true;
78  }
79 
80  $sql = 'SELECT caldav_data.dav_name, caldav_data.caldav_data FROM caldav_data JOIN calendar_item USING(dav_id) ';
81  $sql .= 'WHERE caldav_data.collection_id IN (SELECT collection_id FROM collection WHERE is_calendar AND user_no =?) ';
82  $sql .= 'AND uid=? LIMIT 1';
83  $uids = $resource->GetPropertiesByPath('/VCALENDAR/*/UID');
84  if ( count($uids) == 0 ) {
85  dbg_error_log( 'schedule', 'No UID in VCALENDAR - giving up on REPLY.' );
86  return true;
87  }
88  $uid = $uids[0]->Value();
89  $qry = new AwlQuery($sql, $organizer_principal->user_no(), $uid);
90  if ( !$qry->Exec('schedule',__LINE__,__FILE__) || $qry->rows() < 1 ) {
91  dbg_error_log( 'schedule', 'Could not find original event from organizer - giving up on REPLY.' );
92  return true;
93  }
94  $row = $qry->Fetch();
95  $collection_path = preg_replace('{/[^/]+$}', '/', $row->dav_name );
96  $segment_name = str_replace($collection_path, '', $row->dav_name );
97  $vcal = new vCalendar($row->caldav_data);
98 
99  $attendees = $vcal->GetAttendees();
100  foreach( $attendees AS $v ) {
101  $email = preg_replace( '/^mailto:/i', '', $v->Value() );
102  if ( $email == $request->principal->email() ) {
103  $attendee = $v;
104  break;
105  }
106  }
107  if ( empty($attendee) ) {
108  dbg_error_log( 'schedule', 'Could not find ATTENDEE in VEVENT - giving up on REPLY.' );
109  return true;
110  }
111 
112  $attendee->SetParameterValue('PARTSTAT', $partstat);
113  $attendee->SetParameterValue('SCHEDULE-STATUS', '2.0');
114  $vcal->UpdateAttendeeStatus($request->principal->email(), clone($attendee) );
115 
116  $organizer_calendar = new WritableCollection(array('path' => $collection_path));
117  $organizer_inbox = new WritableCollection(array('path' => $organizer_principal->internal_url('schedule-inbox')));
118 
119  $schedule_reply = GetItip(new vCalendar($vcal->Render(null, true)), 'REPLY', $attendee->Value(), array('CUTYPE'=>true, 'SCHEDULE-STATUS'=>true));
120  $schedule_request = GetItip(new vCalendar($row->caldav_data), 'REQUEST', null);
121 
122  dbg_error_log( 'schedule', 'Writing ATTENDEE scheduling REPLY from %s to %s', $request->principal->email(), $organizer_principal->email() );
123 
124  $response = '3.7'; // Organizer was not found on server.
125  if ( !$organizer_calendar->Exists() ) {
126  if ( doImipMessage('REPLY', $organizer_principal->email(), $vcal) ) {
127  $response = '1.1'; // Scheduling whoosit 'Sent'
128  }
129  else {
130  dbg_error_log('ERROR','Default calendar at "%s" does not exist for user "%s"',
131  $organizer_calendar->dav_name(), $schedule_target->username());
132  $response = '5.2'; // No scheduling support for user
133  }
134  }
135  else {
136  if ( ! $organizer_inbox->HavePrivilegeTo('schedule-deliver-reply') ) {
137  $response = '3.8'; // No authority to deliver replies to organizer.
138  }
139  $response = '1.2'; // Scheduling reply delivered successfully
140  if ( $organizer_calendar->WriteCalendarMember($vcal, false, false, $segment_name) === false ) {
141  dbg_error_log('ERROR','Could not write updated calendar member to %s', $attendee_calendar->dav_name() );
142  trace_bug('Failed to write scheduling resource.');
143  }
144  $organizer_inbox->WriteCalendarMember($schedule_reply, false, false, $request->principal->username().$segment_name);
145  }
146 
147 
148  dbg_error_log( 'schedule', 'Status for organizer <%s> set to "%s"', $organizer->Value(), $response );
149  $organizer->SetParameterValue( 'SCHEDULE-STATUS', $response );
150  $resource->UpdateOrganizerStatus($organizer); // Which was passed in by reference, and we're updating it here.
151 
152  // Now we loop through the *other* ATTENDEEs, updating them on the status of the ATTENDEE DECLINE/ACCEPT
153  foreach( $attendees AS $attendee ) {
154  $email = preg_replace( '/^mailto:/i', '', $attendee->Value() );
155  if ( $email == $request->principal->email() || $email == $organizer_principal->email() ) continue;
156 
157  $agent = $attendee->GetParameterValue('SCHEDULE-AGENT');
158  if ( !empty($agent) && $agent != 'SERVER' ) continue;
159 
160  $schedule_target = new Principal('email',$email);
161  if ( $schedule_target->Exists() ) {
162  $attendee_calendar = new WritableCollection(array('path' => $schedule_target->internal_url('schedule-default-calendar')));
163  if ( !$attendee_calendar->Exists() ) {
164  dbg_error_log('ERROR','Default calendar at "%s" does not exist for user "%s"',
165  $attendee_calendar->dav_name(), $schedule_target->username());
166  continue;
167  }
168  else {
169  $attendee_inbox = new WritableCollection(array('path' => $schedule_target->internal_url('schedule-inbox')));
170  if ( ! $attendee_inbox->HavePrivilegeTo('schedule-deliver-invite') ) continue;
171 
172  if ( $attendee_calendar->WriteCalendarMember($vcal, false) === false ) {
173  dbg_error_log('ERROR','Could not write updated calendar member to %s', $attendee_calendar->dav_name());
174  trace_bug('Failed to write scheduling resource.');
175  }
176  $attendee_inbox->WriteCalendarMember($schedule_request, false);
177  }
178  }
179  else {
180  //TODO: header( "Debug: Could maybe do the iMIP message dance for attendee ". $email );
181  }
182  }
183 
184  return true;
185 }
186 
187 function GetItip( VCalendar $vcal, $method, $attendee_value, $clear_attendee_parameters = null ) {
188  $iTIP = $vcal->GetItip($method, $attendee_value, $clear_attendee_parameters );
189  $components = $iTIP->GetComponents();
190  foreach( $components AS $comp ) {
191  $comp->AddProperty('REQUEST-STATUS','2.0');
192  $properties = array();
193  foreach( $comp->GetProperties() AS $k=> $property ) {
194  switch( $property->Name() ) {
195  case 'DTSTART':
196  case 'DTEND':
197  case 'DUE':
198  $when = new RepeatRuleDateTime($property);
199  $new_prop = new vProperty($property->Name());
200  $new_prop->Value($when->UTC());
201  $properties[] = $new_prop;
202  break;
203  default:
204  $properties[] = $property;
205  }
206  }
207  $comp->SetProperties($properties);
208  }
209  return $iTIP;
210 }
211 
212 
217 function doItipOrganizerCancel( vCalendar $vcal ) {
218  global $request;
219 
220  $attendees = $vcal->GetAttendees();
221  if ( count($attendees) == 0 && count($old_attendees) == 0 ) {
222  dbg_error_log( 'schedule', 'Event has no attendees - no scheduling required.', count($attendees) );
223  return true;
224  }
225 
226  dbg_error_log( 'schedule', 'Writing scheduling resources for %d attendees', count($attendees) );
227  $scheduling_actions = false;
228 
229  $iTIP = GetItip($vcal, 'CANCEL', null);
230 
231  foreach( $attendees AS $attendee ) {
232  $email = preg_replace( '/^mailto:/i', '', $attendee->Value() );
233  if ( $email == $request->principal->email() ) {
234  dbg_error_log( 'schedule', "not delivering to owner '%s'", $request->principal->email() );
235  continue;
236  }
237 
238  $agent = $attendee->GetParameterValue('SCHEDULE-AGENT');
239  if ( $agent && $agent != 'SERVER' ) {
240  dbg_error_log( 'schedule', "not delivering to %s, schedule agent set to value other than server", $email );
241  continue;
242  }
243  $schedule_target = new Principal('email',$email);
244  if ( !$schedule_target->Exists() ) {
245  if ( doImipMessage('CANCEL', $email, $vcal) ) {
246  $response = '1.1'; // Scheduling whoosit 'Sent'
247  }
248  else {
249  $response = '3.7';
250  }
251  }
252  else {
253  $attendee_inbox = new WritableCollection(array('path' => $schedule_target->internal_url('schedule-inbox')));
254  if ( ! $attendee_inbox->HavePrivilegeTo('schedule-deliver-invite') ) {
255  dbg_error_log( 'schedule', "No authority to deliver invite to %s", $schedule_target->internal_url('schedule-inbox') );
256  $response = '3.8';
257  }
258  else {
259  $attendee_calendar = new WritableCollection(array('path' => $schedule_target->internal_url('schedule-default-calendar')));
260  $response = processItipCancel( $vcal, $attendee, $attendee_calendar, $schedule_target );
261  deliverItipCancel( $iTIP, $attendee, $attendee_inbox );
262  }
263  }
264  dbg_error_log( 'schedule', 'Status for attendee <%s> set to "%s"', $attendee->Value(), $response );
265  $attendee->SetParameterValue( 'SCHEDULE-STATUS', $response );
266  $scheduling_actions = true;
267  }
268 
269  return true;
270 }
271 
272 
281 function processItipCancel( vCalendar $vcal, vProperty $attendee, WritableCollection $attendee_calendar, Principal $attendee_principal ) {
282  global $request;
283 
284  dbg_error_log( 'schedule', 'Processing iTIP CANCEL to %s', $attendee->Value());
285  //TODO: header( "Debug: Could maybe do the iMIP message dance for attendee ". $attendee->Value() );
286  if ( !$attendee_calendar->Exists() ) {
287  if ( doImipMessage('CANCEL', $attendee_principal->email(), $vcal) ) {
288  return '1.1'; // Scheduling whoosit 'Sent'
289  }
290  else {
291  dbg_error_log('ERROR', 'Default calendar at "%s" does not exist for attendee "%s"',
292  $attendee_calendar->dav_name(), $attendee->Value());
293  return '5.2'; // No scheduling support for user
294  }
295  }
296 
297  $sql = 'SELECT caldav_data.dav_name FROM caldav_data JOIN calendar_item USING(dav_id) ';
298  $sql .= 'WHERE caldav_data.collection_id IN (SELECT collection_id FROM collection WHERE is_calendar AND user_no =?) ';
299  $sql .= 'AND uid=? LIMIT 1';
300  $uids = $vcal->GetPropertiesByPath('/VCALENDAR/*/UID');
301  if ( count($uids) == 0 ) {
302  dbg_error_log( 'schedule', 'No UID in VCALENDAR - giving up on CANCEL processing.' );
303  return '3.8';
304  }
305  $uid = $uids[0]->Value();
306  $qry = new AwlQuery($sql, $attendee_principal->user_no(), $uid);
307  if ( !$qry->Exec('schedule',__LINE__,__FILE__) || $qry->rows() < 1 ) {
308  dbg_error_log( 'schedule', 'Could not find ATTENDEE copy of original event - not trying to DELETE it!' );
309  return '1.2';
310  }
311  $row = $qry->Fetch();
312 
313  if ( $attendee_calendar->actualDeleteCalendarMember($row->dav_name) === false ) {
314  dbg_error_log('ERROR', 'Could not delete calendar member %s for %s',
315  $row->dav_name(), $attendee->Value());
316  trace_bug('Failed to write scheduling resource.');
317  return '5.2';
318  }
319 
320  return '1.2'; // Scheduling invitation delivered successfully
321 
322 }
323 
324 
335 function deliverItipCancel( vCalendar $iTIP, vProperty $attendee, WritableCollection $attendee_inbox ) {
336  $attendee_inbox->WriteCalendarMember($iTIP, false);
337  //TODO: header( "Debug: Could maybe do the iMIP message dance canceling for attendee: ".$attendee->Value());
338 }
339 
340 require_once('Multipart.php');
341 require_once('EMail.php');
342 
350 function doImipMessage($method, $to_email, vCalendar $itip) {
351  global $c, $request;
352 
353  dbg_error_log( 'schedule', 'Sending iMIP %s message to %s', $method, $to_email );
354  $mime = new MultiPart();
355  $mime->addPart( $itip->Render(), 'text/calendar; charset=UTF-8; method='.$method );
356 
357  $friendly_part = isset($c->iMIP->template[$method]) ? $c->iMIP->template[$method] : <<<EOTEMPLATE
358 This is a meeting ##METHOD## which your e-mail program should be able to
359 import into your calendar. Alternatively you could save the attachment
360 and load that into your calendar instead.
361 EOTEMPLATE;
362 
363  $components = $itip->GetComponents( 'VTIMEZONE',false);
364 
365  $replaceable = array( 'METHOD', 'DTSTART', 'DTEND', 'SUMMARY', 'DESCRIPTION', 'URL' );
366  foreach( $replaceable AS $pname ) {
367  $search = '##'.$pname.'##';
368  if ( strstr($friendly_part,$search) !== false ) {
369  $property = $itip->GetProperty($pname);
370  if ( empty($property) )
371  $property = $components[0]->GetProperty($pname);
372 
373  if ( empty($property) )
374  $replace = '';
375  else {
376  switch( $pname ) {
377  case 'DTSTART':
378  case 'DTEND':
379  $when = new RepeatRuleDateTime($property);
380  $replace = $when->format('c');
381  break;
382  default:
383  $replace = $property->GetValue();
384  }
385  }
386  $friendly_part = str_replace($search, $replace, $friendly_part);
387  }
388  }
389  $mime->addPart( $friendly_part, 'text/plain' );
390 
391  $email = new EMail();
392  $email->SetFrom($request->principal->email());
393  $email->AddTo($to_email);
394 
395  $email->SetSubject( $components[0]->GetPValue('SUMMARY') );
396  $email->SetBody($mime->getMimeParts());
397 
398  if ( isset($c->iMIP->pretend_email) ) {
399  $email->Pretend($mime->getMimeHeaders());
400  }
401  else if ( !isset($c->iMIP->send_email) || !$c->iMIP->send_email) {
402  $email->PretendLog($mime->getMimeHeaders());
403  }
404  else {
405  $email->Send($mime->getMimeHeaders());
406  }
407 }
WriteCalendarMember(vCalendar $vcal, $create_resource, $do_scheduling=false, $segment_name=null, $log_action=false)
actualDeleteCalendarMember( $member_dav_name)
GetProperty( $name)
IsInSchedulingCollection( $type='any')